diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c9dec81 --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +node_modules/ +/electron/app +/electron/dist +/cordova/platforms +/cordova/plugins +/cordova/www +*.vue.ts \ No newline at end of file diff --git a/bbcode/Editor.vue b/bbcode/Editor.vue new file mode 100644 index 0000000..fa59003 --- /dev/null +++ b/bbcode/Editor.vue @@ -0,0 +1,198 @@ + + + \ No newline at end of file diff --git a/bbcode/core.ts b/bbcode/core.ts new file mode 100644 index 0000000..a65bc17 --- /dev/null +++ b/bbcode/core.ts @@ -0,0 +1,86 @@ +import {BBCodeCustomTag, BBCodeParser, BBCodeSimpleTag} from './parser'; + +const urlFormat = '((?:(?:https?|ftps?|irc):)?\\/\\/[^\\s\\/$.?#"\']+\\.[^\\s"]*)'; +export const findUrlRegex = new RegExp(`((?!\\[url(?:\\]|=))(?:.{4}[^\\s])\\s+|^.{0,4}\\s|^)${urlFormat}`, 'g'); +export const urlRegex = new RegExp(`^${urlFormat}$`); + +function domain(url: string): string | undefined { + const pieces = urlRegex.exec(url); + if(pieces === null) return; + const match = pieces[1].match(/(?:(https?|ftps?|irc):)?\/\/(?:www.)?([^\/]+)/); + return match !== null ? match[2] : undefined; +} + +function fixURL(url: string): string { + if(/^www\./.test(url)) + url = `https://${url}`; + return url.replace(/ /g, '%20'); +} + +export class CoreBBCodeParser extends BBCodeParser { + /*tslint:disable-next-line:typedef*///https://github.com/palantir/tslint/issues/711 + constructor(public makeLinksClickable = true) { + super(); + this.addTag('b', new BBCodeSimpleTag('b', 'strong')); + this.addTag('i', new BBCodeSimpleTag('i', 'em')); + this.addTag('u', new BBCodeSimpleTag('u', 'u')); + this.addTag('s', new BBCodeSimpleTag('s', 'del')); + this.addTag('noparse', new BBCodeSimpleTag('noparse', 'span', [], [])); + this.addTag('sub', new BBCodeSimpleTag('sub', 'sub', [], ['b', 'i', 'u', 's'])); + this.addTag('sup', new BBCodeSimpleTag('sup', 'sup', [], ['b', 'i', 'u', 's'])); + this.addTag('color', new BBCodeCustomTag('color', (parser, parent, param) => { + const el = parser.createElement('span'); + parent.appendChild(el); + const cregex = /^(red|blue|white|yellow|pink|gray|green|orange|purple|black|brown|cyan)$/; + if(!cregex.test(param)) { + parser.warning('Invalid color parameter provided.'); + return el; + } + el.className = `${param}Text`; + return el; + })); + this.addTag('url', new BBCodeCustomTag('url', (parser, parent, _) => { + const el = parser.createElement('span'); + parent.appendChild(el); + return el; + }, (parser, element, _, param) => { + const content = element.innerText.trim(); + while(element.firstChild !== null) element.removeChild(element.firstChild); + + let url: string, display: string = content; + if(param.length > 0) { + url = param.trim(); + if(content.length === 0) display = param; + } else if(content.length > 0) url = content; + else { + parser.warning('url tag contains no url.'); + element.innerText = ''; //Dafuq!? + return; + } + + // This fixes problems where content based urls are marked as invalid if they contain spaces. + url = fixURL(url); + if(!urlRegex.test(url)) { + element.innerText = `[BAD URL] ${url}`; + return; + } + const a = parser.createElement('a'); + a.href = url; + a.rel = 'nofollow noreferrer noopener'; + a.target = '_blank'; + a.className = 'link-graphic'; + a.title = url; + a.innerText = display; + element.appendChild(a); + const span = document.createElement('span'); + span.className = 'link-domain'; + span.textContent = ` [${domain(url)}]`; + element.appendChild(span); + }, [])); + } + + parseEverything(input: string): HTMLElement { + if(this.makeLinksClickable && input.length > 0) input = input.replace(findUrlRegex, '$1[url]$2[/url]'); + return super.parseEverything(input); + } +} \ No newline at end of file diff --git a/bbcode/editor.ts b/bbcode/editor.ts new file mode 100644 index 0000000..cfdbb91 --- /dev/null +++ b/bbcode/editor.ts @@ -0,0 +1,96 @@ +import * as Vue from 'vue'; + +export interface EditorButton { + title: string; + tag: string; + icon: string; + key?: string; + class?: string; + startText?: string; + endText?: string; + handler?(vm: Vue): void; +} + +export interface EditorSelection { + start: number; + end: number; + length: number; + text: string; +} +/*tslint:disable:max-line-length*/ +export let defaultButtons: ReadonlyArray = [ + { + title: 'Bold (Ctrl+B)\n\nMakes text appear with a bold weight.', + tag: 'b', + icon: 'fa-bold', + key: 'b' + }, + { + title: 'Italic (Ctrl+I)\n\nMakes text appear with an italic style.', + tag: 'i', + icon: 'fa-italic', + key: 'i' + }, + { + title: 'Underline (Ctrl+U)\n\nMakes text appear with an underline beneath it.', + tag: 'u', + icon: 'fa-underline', + key: 'u' + }, + { + title: 'Strikethrough (Ctrl+S)\n\nPlaces a horizontal line through the text. Usually used to signify a correction or redaction without omitting the text.', + tag: 's', + icon: 'fa-strikethrough', + key: 's' + }, + { + title: 'Color (Ctrl+Q)\n\nStyles text with a color. Valid colors are: red, orange, yellow, green, cyan, blue, purple, pink, black, white, gray, primary, secondary, accent, and contrast.', + tag: 'color', + startText: '[color=]', + icon: 'fa-eyedropper', + key: 'q' + }, + { + title: 'Superscript (Ctrl+↑)\n\nLifts text above the text baseline. Makes text slightly smaller. Cannot be nested.', + tag: 'sup', + icon: 'fa-superscript', + key: 'ArrowUp' + }, + { + title: 'Subscript (Ctrl+↓)\n\nPushes text below the text baseline. Makes text slightly smaller. Cannot be nested.', + tag: 'sub', + icon: 'fa-subscript', + key: 'ArrowDown' + }, + { + title: 'URL (Ctrl+L)\n\nCreates a clickable link to another page of your choosing.', + tag: 'url', + startText: '[url=]', + icon: 'fa-link', + key: 'l' + }, + { + title: 'User (Ctrl+R)\n\nLinks to a character\'s profile.', + tag: 'user', + icon: 'fa-user', + key: 'r' + }, + { + title: 'Icon (Ctrl+O)\n\nShows a character\'s profile image, linking to their profile.', + tag: 'icon', + icon: 'fa-user-circle', + key: 'o' + }, + { + title: 'EIcon (Ctrl+E)\n\nShows a previously uploaded eicon. If the icon is a gif, it will be shown as animated unless a user has turned this off.', + tag: 'eicon', + icon: 'fa-smile-o', + key: 'e' + }, + { + title: 'Noparse (Ctrl+N)\n\nAll BBCode placed within this tag will be ignored and treated as text. Great for sharing structure without it being rendered.', + tag: 'noparse', + icon: 'fa-ban', + key: 'n' + } +]; \ No newline at end of file diff --git a/bbcode/parser.ts b/bbcode/parser.ts new file mode 100644 index 0000000..3fef1d7 --- /dev/null +++ b/bbcode/parser.ts @@ -0,0 +1,252 @@ +export abstract class BBCodeTag { + noClosingTag = false; + allowedTags: {[tag: string]: boolean | undefined} | undefined; + + constructor(public tag: string, tagList?: string[]) { + if(tagList !== undefined) + this.setAllowedTags(tagList); + } + + isAllowed(tag: string): boolean { + return this.allowedTags === undefined || this.allowedTags[tag] !== undefined; + } + + setAllowedTags(allowed: string[]): void { + this.allowedTags = {}; + for(const tag of allowed) + this.allowedTags[tag] = true; + } + + //tslint:disable-next-line:no-empty + afterClose(_: BBCodeParser, __: HTMLElement, ___: HTMLElement, ____?: string): void { + } + + abstract createElement(parser: BBCodeParser, parent: HTMLElement, param: string): HTMLElement; +} + +export class BBCodeSimpleTag extends BBCodeTag { + + constructor(tag: string, private elementName: keyof ElementTagNameMap, private classes?: string[], tagList?: string[]) { + super(tag, tagList); + } + + createElement(parser: BBCodeParser, parent: HTMLElement, param: string): HTMLElement { + if(param.length > 0) + parser.warning('Unexpected parameter'); + const el = parser.createElement(this.elementName); + if(this.classes !== undefined) + el.className = this.classes.join(' '); + parent.appendChild(el); + /*tslint:disable-next-line:no-unsafe-any*/// false positive + return el; + } +} + +export type CustomElementCreator = (parser: BBCodeParser, parent: HTMLElement, param: string) => HTMLElement; +export type CustomCloser = (parser: BBCodeParser, current: HTMLElement, parent: HTMLElement, param: string) => void; + +export class BBCodeCustomTag extends BBCodeTag { + constructor(tag: string, private customCreator: CustomElementCreator, private customCloser?: CustomCloser, tagList?: string[]) { + super(tag, tagList); + } + + createElement(parser: BBCodeParser, parent: HTMLElement, param: string): HTMLElement { + return this.customCreator(parser, parent, param); + } + + afterClose(parser: BBCodeParser, current: HTMLElement, parent: HTMLElement, param: string): void { + if(this.customCloser !== undefined) + this.customCloser(parser, current, parent, param); + } +} + +enum BufferType { Raw, Tag } + +class ParserTag { + constructor(public tag: string, public param: string, public element: HTMLElement, public parent: HTMLElement, + public line: number, public column: number) { + } + + appendElement(child: HTMLElement): void { + this.element.appendChild(child); + } + + append(content: string, start: number, end: number): void { + if(content.length === 0) + return; + this.element.appendChild(document.createTextNode(content.substring(start, end))); + } +} + +export class BBCodeParser { + private _warnings: string[] = []; + private _tags: {[tag: string]: BBCodeTag | undefined} = {}; + private _line: number; + private _column: number; + private _currentTag: ParserTag; + private _storeWarnings = false; + + parseEverything(input: string): HTMLElement { + if(input.length === 0) + return this.createElement('span'); + this._warnings = []; + this._line = 1; + this._column = 1; + const stack: ParserTag[] = this.parse(input, 0, input.length); + + for(let i = stack.length - 1; i > 0; i--) { + this._currentTag = stack.pop(); + this.warning('Automatically closing tag at end of input.'); + } + if(process.env.NODE_ENV !== 'production' && this._warnings.length > 0) + console.log(this._warnings); + return stack[0].element; + } + + createElement(tag: K | keyof ElementTagNameMap): HTMLElementTagNameMap[K] { + return document.createElement(tag); + } + + addTag(tag: string, impl: BBCodeTag): void { + this._tags[tag] = impl; + } + + removeTag(tag: string): void { + delete this._tags[tag]; + } + + get warnings(): ReadonlyArray { + return this._warnings; + } + + set storeWarnings(store: boolean) { + this._storeWarnings = store; + if(!store) + this._warnings = []; + } + + warning(message: string): void { + if(!this._storeWarnings) + return; + const cur = this._currentTag; + const newMessage = `Error on ${this._line}:${this._column} while inside tag [${cur.tag} @ ${cur.line}:${cur.column}]: ${message}`; + this._warnings.push(newMessage); + } + + private parse(input: string, start: number, end: number): ParserTag[] { + const ignoreClosing: {[key: string]: number} = {}; + + function ignoreNextClosingTag(tagName: string): void { + //tslint:disable-next-line:strict-boolean-expressions + ignoreClosing[tagName] = (ignoreClosing[tagName] || 0) + 1; + } + + const stack: ParserTag[] = []; + + function stackTop(): ParserTag { + return stack[stack.length - 1]; + } + + function quickReset(i: number): void { + stackTop().append(input, start, i + 1); + start = i + 1; + curType = BufferType.Raw; + } + + let curType: BufferType = BufferType.Raw; + // Root tag collects output. + const root = this.createElement('span'); + const rootTag = new ParserTag('', '', root, root, 1, 1); + stack.push(rootTag); + this._currentTag = rootTag; + let paramStart = -1; + for(let i = start; i < end; ++i) { + const c = input[i]; + ++this._column; + if(c === '\n') { + ++this._line; + this._column = 1; + quickReset(i); + stackTop().appendElement(this.createElement('br')); + } + switch(curType) { + case BufferType.Raw: + if(c === '[') { + stackTop().append(input, start, i); + start = i; + curType = BufferType.Tag; + } + break; + case BufferType.Tag: + if(c === '[') { + stackTop().append(input, start, i); + start = i; + } else if(c === '=' && paramStart === -1) + paramStart = i; + else if(c === ']') { + const paramIndex = paramStart === -1 ? i : paramStart; + let tagKey = input.substring(start + 1, paramIndex).trim(); + if(tagKey.length === 0) { + quickReset(i); + continue; + } + let param = ''; + if(paramStart !== -1) + param = input.substring(paramStart + 1, i).trim(); + paramStart = -1; + const close = tagKey[0] === '/'; + if(close) tagKey = tagKey.substr(1).trim(); + if(typeof this._tags[tagKey] === 'undefined') { + quickReset(i); + continue; + } + if(!close) { + let allowed = true; + for(let k = stack.length - 1; k > 0; --k) { + allowed = allowed && this._tags[stack[k].tag]!.isAllowed(tagKey); + if(!allowed) + break; + } + if(!allowed) { + ignoreNextClosingTag(tagKey); + quickReset(i); + continue; + } + const parent = stackTop().element; + const el = this._tags[tagKey]!.createElement(this, parent, param); + if(!this._tags[tagKey]!.noClosingTag) + stack.push(new ParserTag(tagKey, param, el, parent, this._line, this._column)); + } else if(ignoreClosing[tagKey] > 0) { + ignoreClosing[tagKey] -= 1; + stackTop().append(input, start, i + 1); + } else { + let closed = false; + for(let k = stack.length - 1; k >= 0; --k) { + if(stack[k].tag !== tagKey) continue; + for(let y = stack.length - 1; y >= k; --y) { + const closeTag = stack.pop(); + this._currentTag = closeTag; + if(y > k) + this.warning(`Unexpected closing ${tagKey} tag. Needed ${closeTag.tag} tag instead.`); + this._tags[closeTag.tag]!.afterClose(this, closeTag.element, closeTag.parent, closeTag.param); + } + this._currentTag = stackTop(); + closed = true; + break; + } + if(!closed) { + this.warning(`Found closing ${tagKey} tag that was never opened.`); + stackTop().append(input, start, i + 1); + } + } + start = i + 1; + curType = BufferType.Raw; + } + } + } + if(start < input.length) + stackTop().append(input, start, input.length); + + return stack; + } +} \ No newline at end of file diff --git a/chat/ChannelList.vue b/chat/ChannelList.vue new file mode 100644 index 0000000..4fcd6c8 --- /dev/null +++ b/chat/ChannelList.vue @@ -0,0 +1,96 @@ + + + \ No newline at end of file diff --git a/chat/ChannelView.vue b/chat/ChannelView.vue new file mode 100644 index 0000000..a800993 --- /dev/null +++ b/chat/ChannelView.vue @@ -0,0 +1,32 @@ + + + \ No newline at end of file diff --git a/chat/CharacterSearch.vue b/chat/CharacterSearch.vue new file mode 100644 index 0000000..8adc553 --- /dev/null +++ b/chat/CharacterSearch.vue @@ -0,0 +1,129 @@ + + + + + \ No newline at end of file diff --git a/chat/Chat.vue b/chat/Chat.vue new file mode 100644 index 0000000..9e6a244 --- /dev/null +++ b/chat/Chat.vue @@ -0,0 +1,90 @@ + + + \ No newline at end of file diff --git a/chat/ChatView.vue b/chat/ChatView.vue new file mode 100644 index 0000000..447fce0 --- /dev/null +++ b/chat/ChatView.vue @@ -0,0 +1,290 @@ + + + + + \ No newline at end of file diff --git a/chat/CommandHelp.vue b/chat/CommandHelp.vue new file mode 100644 index 0000000..f5c3e56 --- /dev/null +++ b/chat/CommandHelp.vue @@ -0,0 +1,100 @@ + + + + + \ No newline at end of file diff --git a/chat/ConversationSettings.vue b/chat/ConversationSettings.vue new file mode 100644 index 0000000..8bcfc8d --- /dev/null +++ b/chat/ConversationSettings.vue @@ -0,0 +1,83 @@ + + + \ No newline at end of file diff --git a/chat/ConversationView.vue b/chat/ConversationView.vue new file mode 100644 index 0000000..bd92934 --- /dev/null +++ b/chat/ConversationView.vue @@ -0,0 +1,326 @@ + + + + + \ No newline at end of file diff --git a/chat/Logs.vue b/chat/Logs.vue new file mode 100644 index 0000000..c099369 --- /dev/null +++ b/chat/Logs.vue @@ -0,0 +1,148 @@ + + + \ No newline at end of file diff --git a/chat/ManageChannel.vue b/chat/ManageChannel.vue new file mode 100644 index 0000000..57a6c54 --- /dev/null +++ b/chat/ManageChannel.vue @@ -0,0 +1,112 @@ + + + \ No newline at end of file diff --git a/chat/RecentConversations.vue b/chat/RecentConversations.vue new file mode 100644 index 0000000..8a6a34a --- /dev/null +++ b/chat/RecentConversations.vue @@ -0,0 +1,36 @@ + + + \ No newline at end of file diff --git a/chat/ReportDialog.vue b/chat/ReportDialog.vue new file mode 100644 index 0000000..5adc171 --- /dev/null +++ b/chat/ReportDialog.vue @@ -0,0 +1,88 @@ + + + \ No newline at end of file diff --git a/chat/SettingsView.vue b/chat/SettingsView.vue new file mode 100644 index 0000000..0f30ced --- /dev/null +++ b/chat/SettingsView.vue @@ -0,0 +1,214 @@ + + + + + \ No newline at end of file diff --git a/chat/StatusSwitcher.vue b/chat/StatusSwitcher.vue new file mode 100644 index 0000000..2c8a17f --- /dev/null +++ b/chat/StatusSwitcher.vue @@ -0,0 +1,81 @@ + + + \ No newline at end of file diff --git a/chat/UserList.vue b/chat/UserList.vue new file mode 100644 index 0000000..42c22d4 --- /dev/null +++ b/chat/UserList.vue @@ -0,0 +1,89 @@ + + + + + \ No newline at end of file diff --git a/chat/UserMenu.vue b/chat/UserMenu.vue new file mode 100644 index 0000000..d56f970 --- /dev/null +++ b/chat/UserMenu.vue @@ -0,0 +1,201 @@ + + + + + \ No newline at end of file diff --git a/chat/WebSocket.ts b/chat/WebSocket.ts new file mode 100644 index 0000000..fc5be2d --- /dev/null +++ b/chat/WebSocket.ts @@ -0,0 +1,35 @@ +import {WebSocketConnection} from '../fchat/interfaces'; +import l from './localize'; + +export default class Socket implements WebSocketConnection { + static host = 'wss://chat.f-list.net:9799'; + socket: WebSocket; + + constructor() { + this.socket = new WebSocket(Socket.host); + } + + close(): void { + this.socket.close(); + } + + onMessage(handler: (message: string) => void): void { + this.socket.addEventListener('message', (e) => handler(e.data)); + } + + onOpen(handler: () => void): void { + this.socket.addEventListener('open', handler); + } + + onClose(handler: () => void): void { + this.socket.addEventListener('close', handler); + } + + onError(handler: (error: Error) => void): void { + this.socket.addEventListener('error', () => handler(new Error(l('login.connectError')))); + } + + send(message: string): void { + this.socket.send(message); + } +} \ No newline at end of file diff --git a/chat/assets/attention.mp3 b/chat/assets/attention.mp3 new file mode 100644 index 0000000..7c91b8e Binary files /dev/null and b/chat/assets/attention.mp3 differ diff --git a/chat/assets/attention.ogg b/chat/assets/attention.ogg new file mode 100644 index 0000000..1c7533f Binary files /dev/null and b/chat/assets/attention.ogg differ diff --git a/chat/assets/attention.wav b/chat/assets/attention.wav new file mode 100644 index 0000000..7c91b8e Binary files /dev/null and b/chat/assets/attention.wav differ diff --git a/chat/assets/chat.mp3 b/chat/assets/chat.mp3 new file mode 100644 index 0000000..b8009c6 Binary files /dev/null and b/chat/assets/chat.mp3 differ diff --git a/chat/assets/chat.ogg b/chat/assets/chat.ogg new file mode 100644 index 0000000..22d8311 Binary files /dev/null and b/chat/assets/chat.ogg differ diff --git a/chat/assets/chat.wav b/chat/assets/chat.wav new file mode 100644 index 0000000..9fadb52 Binary files /dev/null and b/chat/assets/chat.wav differ diff --git a/chat/assets/login.mp3 b/chat/assets/login.mp3 new file mode 100644 index 0000000..3efb419 Binary files /dev/null and b/chat/assets/login.mp3 differ diff --git a/chat/assets/login.ogg b/chat/assets/login.ogg new file mode 100644 index 0000000..12fbda1 Binary files /dev/null and b/chat/assets/login.ogg differ diff --git a/chat/assets/login.wav b/chat/assets/login.wav new file mode 100644 index 0000000..28d6e70 Binary files /dev/null and b/chat/assets/login.wav differ diff --git a/chat/assets/logout.mp3 b/chat/assets/logout.mp3 new file mode 100644 index 0000000..bbb7c29 Binary files /dev/null and b/chat/assets/logout.mp3 differ diff --git a/chat/assets/logout.ogg b/chat/assets/logout.ogg new file mode 100644 index 0000000..0f521c6 Binary files /dev/null and b/chat/assets/logout.ogg differ diff --git a/chat/assets/logout.wav b/chat/assets/logout.wav new file mode 100644 index 0000000..62b76a5 Binary files /dev/null and b/chat/assets/logout.wav differ diff --git a/chat/assets/modalert.mp3 b/chat/assets/modalert.mp3 new file mode 100644 index 0000000..06812fc Binary files /dev/null and b/chat/assets/modalert.mp3 differ diff --git a/chat/assets/modalert.ogg b/chat/assets/modalert.ogg new file mode 100644 index 0000000..1a3b341 Binary files /dev/null and b/chat/assets/modalert.ogg differ diff --git a/chat/assets/modalert.wav b/chat/assets/modalert.wav new file mode 100644 index 0000000..d7927c8 Binary files /dev/null and b/chat/assets/modalert.wav differ diff --git a/chat/assets/newnote.mp3 b/chat/assets/newnote.mp3 new file mode 100644 index 0000000..4ad4b91 Binary files /dev/null and b/chat/assets/newnote.mp3 differ diff --git a/chat/assets/newnote.ogg b/chat/assets/newnote.ogg new file mode 100644 index 0000000..2460224 Binary files /dev/null and b/chat/assets/newnote.ogg differ diff --git a/chat/assets/newnote.wav b/chat/assets/newnote.wav new file mode 100644 index 0000000..5ebe2c2 Binary files /dev/null and b/chat/assets/newnote.wav differ diff --git a/chat/assets/system.mp3 b/chat/assets/system.mp3 new file mode 100644 index 0000000..17f2701 Binary files /dev/null and b/chat/assets/system.mp3 differ diff --git a/chat/assets/system.ogg b/chat/assets/system.ogg new file mode 100644 index 0000000..7ad5efb Binary files /dev/null and b/chat/assets/system.ogg differ diff --git a/chat/assets/system.wav b/chat/assets/system.wav new file mode 100644 index 0000000..4ce5876 Binary files /dev/null and b/chat/assets/system.wav differ diff --git a/chat/bbcode.ts b/chat/bbcode.ts new file mode 100644 index 0000000..0c91756 --- /dev/null +++ b/chat/bbcode.ts @@ -0,0 +1,129 @@ +import Vue, {Component, CreateElement, RenderContext, VNode} from 'vue'; +import {CoreBBCodeParser} from '../bbcode/core'; +//tslint:disable-next-line:match-default-export-name +import BaseEditor from '../bbcode/Editor.vue'; +import {BBCodeCustomTag} from '../bbcode/parser'; +import ChannelView from './ChannelView.vue'; +import {characterImage} from './common'; +import core from './core'; +import {Character} from './interfaces'; +import UserView from './user_view'; + +export const BBCodeView: Component = { + functional: true, + render(this: Vue, createElement: CreateElement, context: RenderContext): VNode { + /*tslint:disable:no-unsafe-any*///because we're not actually supposed to do any of this + context.data.hook = { + insert(): void { + if(vnode.elm !== undefined) + vnode.elm.appendChild(core.bbCodeParser.parseEverything( + context.props.text !== undefined ? context.props.text : context.props.unsafeText)); + }, + destroy(): void { + const element = ((vnode.elm).firstChild); + if(element.cleanup !== undefined) element.cleanup(); + } + }; + context.data.staticClass = `bbcode${context.data.staticClass !== undefined ? ` ${context.data.staticClass}` : ''}`; + const vnode = createElement('span', context.data); + vnode.key = context.props.text; + return vnode; + //tslint:enable + } +}; + +export class Editor extends BaseEditor { + parser = core.bbCodeParser; +} + +export type BBCodeElement = HTMLElement & {cleanup?(): void}; + +export default class BBCodeParser extends CoreBBCodeParser { + cleanup: Vue[] = []; + + constructor() { + super(); + this.addTag('user', new BBCodeCustomTag('user', (parser, parent) => { + const el = parser.createElement('span'); + parent.appendChild(el); + return el; + }, (parser, element, _, param) => { + if(param.length > 0) + parser.warning('Unexpected parameter on user tag.'); + const content = element.innerText; + element.innerText = ''; + const uregex = /^[a-zA-Z0-9_\-\s]+$/; + if(!uregex.test(content)) + return; + const view = new UserView({el: element, propsData: {character: core.characters.get(content)}}); + this.cleanup.push(view); + }, [])); + this.addTag('icon', new BBCodeCustomTag('icon', (parser, parent) => { + const el = parser.createElement('span'); + parent.appendChild(el); + return el; + }, (parser, element, parent, param) => { + if(param.length > 0) + parser.warning('Unexpected parameter on icon tag.'); + const content = element.innerText; + const uregex = /^[a-zA-Z0-9_\-\s]+$/; + if(!uregex.test(content)) + return; + const img = parser.createElement('img'); + img.src = characterImage(content); + img.style.cursor = 'pointer'; + img.className = 'characterAvatarIcon'; + img.title = img.alt = content; + (img).character = core.characters.get(content); + parent.replaceChild(img, element); + }, [])); + this.addTag('eicon', new BBCodeCustomTag('eicon', (parser, parent) => { + const el = parser.createElement('span'); + parent.appendChild(el); + return el; + }, (parser, element, parent, param) => { + if(param.length > 0) + parser.warning('Unexpected parameter on eicon tag.'); + const content = element.innerText; + const uregex = /^[a-zA-Z0-9_\-\s]+$/; + if(!uregex.test(content)) + return; + const extension = core.state.settings.animatedEicons ? 'gif' : 'png'; + const img = parser.createElement('img'); + img.src = `https://static.f-list.net/images/eicon/${content.toLowerCase()}.${extension}`; + img.title = img.alt = content; + img.className = 'characterAvatarIcon'; + parent.replaceChild(img, element); + }, [])); + this.addTag('session', new BBCodeCustomTag('session', (parser, parent) => { + const el = parser.createElement('span'); + parent.appendChild(el); + return el; + }, (_, element, __, param) => { + const content = element.innerText; + element.innerText = ''; + const view = new ChannelView({el: element, propsData: {id: content, text: param}}); + this.cleanup.push(view); + }, [])); + this.addTag('channel', new BBCodeCustomTag('channel', (parser, parent) => { + const el = parser.createElement('span'); + parent.appendChild(el); + return el; + }, (_, element, __, ___) => { + const content = element.innerText; + element.innerText = ''; + const view = new ChannelView({el: element, propsData: {id: content, text: content}}); + this.cleanup.push(view); + }, [])); + } + + parseEverything(input: string): BBCodeElement { + const elm = super.parseEverything(input); + if(this.cleanup.length > 0) + elm.cleanup = ((cleanup: Vue[]) => () => { + for(const component of cleanup) component.$destroy(); + })(this.cleanup); + this.cleanup = []; + return elm; + } +} \ No newline at end of file diff --git a/chat/common.ts b/chat/common.ts new file mode 100644 index 0000000..ae0b7e9 --- /dev/null +++ b/chat/common.ts @@ -0,0 +1,96 @@ +import {format, isToday} from 'date-fns'; +import {Character, Conversation, Settings as ISettings} from './interfaces'; + +export function profileLink(this: void | never, character: string): string { + return `https://www.f-list.net/c/${character}`; +} + +export function characterImage(this: void | never, character: string): string { + return `https://static.f-list.net/images/avatar/${character.toLowerCase()}.png`; +} + +export function getByteLength(this: void | never, str: string): number { + let byteLen = 0; + for(let i = 0; i < str.length; i++) { + const c = str.charCodeAt(i); + byteLen += c < (1 << 7) ? 1 : + c < (1 << 11) ? 2 : + c < (1 << 16) ? 3 : + c < (1 << 21) ? 4 : + c < (1 << 26) ? 5 : + c < (1 << 31) ? 6 : Number.NaN; + } + return byteLen; +} + +export class Settings implements ISettings { + playSound = true; + clickOpensMessage = false; + disallowedTags: string[] = []; + notifications = true; + highlight = true; + highlightWords: string[] = []; + showAvatars = true; + animatedEicons = true; + idleTimer = 0; + messageSeparators = false; + eventMessages = true; + joinMessages = false; + alwaysNotify = false; + logMessages = true; + logAds = false; +} + +export class ConversationSettings implements Conversation.Settings { + notify = Conversation.Setting.Default; + highlight = Conversation.Setting.Default; + highlightWords: string[] = []; + joinMessages = Conversation.Setting.Default; +} + +export function formatTime(this: void | never, date: Date): string { + if(isToday(date)) return format(date, 'HH:mm'); + return format(date, 'YYYY-MM-DD HH:mm'); +} + +export function messageToString(this: void | never, msg: Conversation.Message, timeFormatter: (date: Date) => string = formatTime): string { + let text = `[${timeFormatter(msg.time)}] `; + if(msg.type !== Conversation.Message.Type.Event) + text += (msg.type === Conversation.Message.Type.Action ? '*' : '') + msg.sender.name + + (msg.type === Conversation.Message.Type.Message ? ':' : ''); + return `${text} ${msg.text}\r\n`; +} + +export function getKey(e: KeyboardEvent): string { + /*tslint:disable-next-line:strict-boolean-expressions no-any*///because of old browsers. + return e.key || (e).keyIdentifier; +} + +/*tslint:disable:no-any no-unsafe-any*///because errors can be any +export function errorToString(e: any): string { + return e instanceof Error ? e.message : e !== undefined ? e.toString() : ''; +} +//tslint:enable + +export async function requestNotificationsPermission(): Promise { + if(Notification !== undefined) await Notification.requestPermission(); +} + +let messageId = 0; + +export class Message implements Conversation.ChatMessage { + readonly id = ++messageId; + isHighlight = false; + + constructor(readonly type: Conversation.Message.Type, readonly sender: Character, readonly text: string, + readonly time: Date = new Date()) { + } +} + +export class EventMessage implements Conversation.EventMessage { + readonly id = ++messageId; + readonly type = Conversation.Message.Type.Event; + + constructor(readonly text: string, readonly time: Date = new Date()) { + } +} \ No newline at end of file diff --git a/chat/conversations.ts b/chat/conversations.ts new file mode 100644 index 0000000..470909f --- /dev/null +++ b/chat/conversations.ts @@ -0,0 +1,682 @@ +//tslint:disable:no-floating-promises +import {queuedJoin} from '../fchat/channels'; +import {decodeHTML} from '../fchat/common'; +import {characterImage, ConversationSettings, EventMessage, Message, messageToString} from './common'; +import core from './core'; +import {Channel, Character, Connection, Conversation as Interfaces} from './interfaces'; +import l from './localize'; +import {CommandContext, isCommand, parse as parseCommand} from './slash_commands'; +import MessageType = Interfaces.Message.Type; +function createMessage(this: void, type: MessageType, sender: Character, text: string, time?: Date): Message { + if(type === MessageType.Message && text.match(/^\/me\b/) !== null) { + type = MessageType.Action; + text = text.substr(text.charAt(4) === ' ' ? 4 : 3); + } + return new Message(type, sender, text, time); +} + +function safeAddMessage(this: void, messages: Interfaces.Message[], message: Interfaces.Message, max: number): void { + if(messages.length >= max) messages.shift(); + messages.push(message); +} + +abstract class Conversation implements Interfaces.Conversation { + abstract enteredText: string; + abstract readonly name: string; + messages: Interfaces.Message[] = []; + errorText = ''; + unread = Interfaces.UnreadState.None; + lastRead: Interfaces.Message | undefined = undefined; + infoText = ''; + abstract readonly maxMessageLength: number | undefined; + _settings: Interfaces.Settings; + protected abstract context: CommandContext; + protected maxMessages = 100; + protected allMessages: Interfaces.Message[]; + private lastSent = ''; + + constructor(readonly key: string, public _isPinned: boolean) { + } + + get settings(): Interfaces.Settings { + //tslint:disable-next-line:strict-boolean-expressions + return this._settings || (this._settings = state.settings[this.key] || new ConversationSettings()); + } + + set settings(value: Interfaces.Settings) { + this._settings = value; + state.setSettings(this.key, value); + } + + get isPinned(): boolean { + return this._isPinned; + } + + set isPinned(value: boolean) { + if(value === this._isPinned) return; + this._isPinned = value; + state.savePinned(); + } + + get reportMessages(): ReadonlyArray { + return this.allMessages; + } + + send(): void { + if(this.enteredText.length === 0) return; + if(isCommand(this.enteredText)) { + const parsed = parseCommand(this.enteredText, this.context); + if(typeof parsed === 'string') this.errorText = parsed; + else { + parsed.call(this); + this.lastSent = this.enteredText; + this.enteredText = ''; + } + } else { + this.lastSent = this.enteredText; + this.doSend(); + } + } + + abstract addMessage(message: Interfaces.Message): void; + + loadLastSent(): void { + this.enteredText = this.lastSent; + } + + loadMore(): void { + if(this.messages.length >= this.allMessages.length) return; + this.maxMessages += 100; + this.messages = this.allMessages.slice(-this.maxMessages); + } + + show(): void { + state.show(this); + } + + onHide(): void { + this.errorText = ''; + this.lastRead = this.messages[this.messages.length - 1]; + this.maxMessages = 100; + this.messages = this.allMessages.slice(-this.maxMessages); + } + + abstract close(): void; + + protected safeAddMessage(message: Interfaces.Message): void { + safeAddMessage(this.allMessages, message, 500); + safeAddMessage(this.messages, message, this.maxMessages); + } + + protected abstract doSend(): void; +} + +class PrivateConversation extends Conversation implements Interfaces.PrivateConversation { + readonly name = this.character.name; + readonly context = CommandContext.Private; + typingStatus: Interfaces.TypingStatus = 'clear'; + readonly maxMessageLength = core.connection.vars.priv_max; + private _enteredText = ''; + private ownTypingStatus: Interfaces.TypingStatus = 'clear'; + private timer: number | undefined; + private logPromise = core.logs.getBacklog(this).then((messages) => { + this.allMessages.unshift(...messages); + this.messages = this.allMessages.slice(); + }); + + constructor(readonly character: Character) { + super(character.name.toLowerCase(), state.pinned.private.indexOf(character.name) !== -1); + this.lastRead = this.messages[this.messages.length - 1]; + this.allMessages = []; + } + + get enteredText(): string { + return this._enteredText; + } + + set enteredText(value: string) { + this._enteredText = value; + if(this.timer !== undefined) clearTimeout(this.timer); + if(value.length > 0) { + if(this.ownTypingStatus !== 'typing') this.setOwnTyping('typing'); + this.timer = window.setTimeout(() => this.setOwnTyping('paused'), 5000); + } else if(this.ownTypingStatus !== 'clear') this.setOwnTyping('clear'); + } + + addMessage(message: Interfaces.Message): void { + this.safeAddMessage(message); + if(message.type !== Interfaces.Message.Type.Event) { + if(core.state.settings.logMessages) this.logPromise.then(() => core.logs.logMessage(this, message)); + if(this.settings.notify !== Interfaces.Setting.False) + core.notifications.notify(this, message.sender.name, message.text, characterImage(message.sender.name), 'attention'); + if(this !== state.selectedConversation) + this.unread = Interfaces.UnreadState.Mention; + this.typingStatus = 'clear'; + } + } + + close(): void { + state.privateConversations.splice(state.privateConversations.indexOf(this), 1); + delete state.privateMap[this.character.name.toLowerCase()]; + state.savePinned(); + if(state.selectedConversation === this) state.show(state.consoleTab); + } + + sort(newIndex: number): void { + state.privateConversations.splice(state.privateConversations.indexOf(this), 1); + state.privateConversations.splice(newIndex, 0, this); + state.savePinned(); + } + + protected doSend(): void { + if(this.character.status === 'offline') { + this.errorText = l('chat.errorOffline', this.character.name); + return; + } else if(this.character.isIgnored) { + this.errorText = l('chat.errorIgnored', this.character.name); + return; + } + core.connection.send('PRI', {recipient: this.name, message: this.enteredText}); + const message = createMessage(MessageType.Message, core.characters.ownCharacter, this.enteredText); + this.safeAddMessage(message); + core.logs.logMessage(this, message); + this.enteredText = ''; + } + + private setOwnTyping(status: Interfaces.TypingStatus): void { + this.ownTypingStatus = status; + core.connection.send('TPN', {character: this.name, status}); + } +} + +class ChannelConversation extends Conversation implements Interfaces.ChannelConversation { + readonly context = CommandContext.Channel; + readonly name = this.channel.name; + isSendingAds = this.channel.mode === 'ads'; + adCountdown = 0; + private chat: Interfaces.Message[] = []; + private ads: Interfaces.Message[] = []; + private both: Interfaces.Message[] = []; + private _mode: Channel.Mode; + private adEnteredText = ''; + private chatEnteredText = ''; + private logPromise = core.logs.getBacklog(this).then((messages) => { + this.both.unshift(...messages); + this.chat.unshift(...this.both.filter((x) => x.type !== MessageType.Ad)); + this.ads.unshift(...this.both.filter((x) => x.type === MessageType.Ad)); + this.lastRead = this.messages[this.messages.length - 1]; + this.mode = this.channel.mode; + }); + + constructor(readonly channel: Channel) { + super(`#${channel.id.replace(/[^\w- ]/gi, '')}`, state.pinned.channels.indexOf(channel.id) !== -1); + core.watch(function(): Channel.Mode | undefined { + const c = this.channels.getChannel(channel.id); + return c !== undefined ? c.mode : undefined; + }, (value) => { + if(value === undefined) return; + this.mode = value; + if(value !== 'both') this.isSendingAds = value === 'ads'; + }); + } + + get maxMessageLength(): number { + return core.connection.vars[this.isSendingAds ? 'lfrp_max' : 'chat_max']; + } + + get mode(): Channel.Mode { + return this._mode; + } + + set mode(mode: Channel.Mode) { + this._mode = mode; + this.maxMessages = 100; + this.allMessages = this[mode]; + this.messages = this.allMessages.slice(-this.maxMessages); + } + + get enteredText(): string { + return this.isSendingAds ? this.adEnteredText : this.chatEnteredText; + } + + set enteredText(value: string) { + if(this.isSendingAds) this.adEnteredText = value; + else this.chatEnteredText = value; + } + + get reportMessages(): ReadonlyArray { + return this.both; + } + + addModeMessage(mode: Channel.Mode, message: Interfaces.Message): void { + if(this._mode === mode) this.safeAddMessage(message); + else safeAddMessage(this[mode], message, 500); + } + + addMessage(message: Interfaces.Message): void { + if((message.type === MessageType.Message || message.type === MessageType.Ad) && message.text.match(/^\/warn\b/) !== null + && (this.channel.members[message.sender.name]!.rank > Channel.Rank.Member || message.sender.isChatOp)) + message = new Message(MessageType.Warn, message.sender, message.text.substr(6), message.time); + + if(message.type === MessageType.Ad) { + this.addModeMessage('ads', message); + if(core.state.settings.logAds) this.logPromise.then(() => core.logs.logMessage(this, message)); + } else { + this.addModeMessage('chat', message); + if(message.type !== Interfaces.Message.Type.Event) { + if(message.type === Interfaces.Message.Type.Warn) this.addModeMessage('ads', message); + if(core.state.settings.logMessages) this.logPromise.then(() => core.logs.logMessage(this, message)); + if(this !== state.selectedConversation && this.unread === Interfaces.UnreadState.None) + this.unread = Interfaces.UnreadState.Unread; + } else this.addModeMessage('ads', message); + } + this.addModeMessage('both', message); + } + + close(): void { + core.connection.send('LCH', {channel: this.channel.id}); + } + + sort(newIndex: number): void { + state.channelConversations.splice(state.channelConversations.indexOf(this), 1); + state.channelConversations.splice(newIndex, 0, this); + state.savePinned(); + } + + protected doSend(): void { + const isAd = this.isSendingAds; + core.connection.send(isAd ? 'LRP' : 'MSG', {channel: this.channel.id, message: this.enteredText}); + this.addMessage( + createMessage(isAd ? MessageType.Ad : MessageType.Message, core.characters.ownCharacter, this.enteredText, new Date())); + if(isAd) { + this.adCountdown = core.connection.vars.lfrp_flood; + const interval = setInterval(() => { + this.adCountdown -= 1; + if(this.adCountdown === 0) clearInterval(interval); + }, 1000); + } else this.enteredText = ''; + } +} + +class ConsoleConversation extends Conversation { + readonly context = CommandContext.Console; + readonly name = l('chat.consoleTab'); + readonly maxMessageLength = undefined; + enteredText = ''; + + constructor() { + super('_', false); + this.allMessages = []; + } + + //tslint:disable-next-line:no-empty + close(): void { + } + + addMessage(message: Interfaces.Message): void { + this.safeAddMessage(message); + if(core.state.settings.logMessages) core.logs.logMessage(this, message); + if(this !== state.selectedConversation) this.unread = Interfaces.UnreadState.Unread; + } + + protected doSend(): void { + this.errorText = l('chat.consoleChat'); + } +} + +class State implements Interfaces.State { + privateConversations: PrivateConversation[] = []; + channelConversations: ChannelConversation[] = []; + privateMap: {[key: string]: PrivateConversation | undefined} = {}; + channelMap: {[key: string]: ChannelConversation | undefined} = {}; + consoleTab: ConsoleConversation; + selectedConversation: Conversation = this.consoleTab; + recent: Interfaces.RecentConversation[] = []; + pinned: {channels: string[], private: string[]}; + settings: {[key: string]: Interfaces.Settings}; + + getPrivate(character: Character): PrivateConversation { + const key = character.name.toLowerCase(); + let conv = state.privateMap[key]; + if(conv !== undefined) return conv; + conv = new PrivateConversation(character); + this.privateConversations.push(conv); + this.privateMap[key] = conv; + state.addRecent(conv); + return conv; + } + + byKey(key: string): Conversation | undefined { + if(key === '_') return this.consoleTab; + return (key[0] === '#' ? this.channelMap : this.privateMap)[key]; + } + + savePinned(): void { + this.pinned.channels = this.channelConversations.filter((x) => x.isPinned).map((x) => x.channel.id); + this.pinned.private = this.privateConversations.filter((x) => x.isPinned).map((x) => x.name); + core.settingsStore.set('pinned', this.pinned); + } + + setSettings(key: string, value: Interfaces.Settings): void { + this.settings[key] = value; + core.settingsStore.set('conversationSettings', this.settings); + } + + addRecent(conversation: Conversation): void { + /*tslint:disable-next-line:no-any*///TS isn't smart enough for this + const remove = (predicate: (item: any) => boolean) => { + for(let i = 0; i < this.recent.length; ++i) + if(predicate(this.recent[i])) { + this.recent.splice(i, 1); + break; + } + }; + if(Interfaces.isChannel(conversation)) { + remove((c) => c.channel === conversation.channel.id); + this.recent.unshift({channel: conversation.channel.id, name: conversation.channel.name}); + } else { + remove((c) => c.character === conversation.name); + state.recent.unshift({character: conversation.name}); + } + if(this.recent.length >= 50) this.recent.pop(); + core.settingsStore.set('recent', this.recent); + } + + show(conversation: Conversation): void { + this.selectedConversation.onHide(); + conversation.unread = Interfaces.UnreadState.None; + this.selectedConversation = conversation; + } + + async reloadSettings(): Promise { + //tslint:disable:strict-boolean-expressions + this.pinned = await core.settingsStore.get('pinned') || {private: [], channels: []}; + for(const conversation of this.channelConversations) + conversation._isPinned = this.pinned.channels.indexOf(conversation.channel.id) !== -1; + for(const conversation of this.privateConversations) + conversation._isPinned = this.pinned.private.indexOf(conversation.name) !== -1; + this.recent = await core.settingsStore.get('recent') || []; + const settings = <{[key: string]: ConversationSettings}> await core.settingsStore.get('conversationSettings') || {}; + //tslint:disable-next-line:forin + for(const key in settings) { + const settingsItem = new ConversationSettings(); + for(const itemKey in settings[key]) + settingsItem[itemKey] = settings[key][itemKey]; + settings[key] = settingsItem; + const conv = (key[0] === '#' ? this.channelMap : this.privateMap)[key]; + if(conv !== undefined) conv._settings = settingsItem; + } + this.settings = settings; + //tslint:enable + } +} + +let state: State; + +function addEventMessage(this: void, message: Interfaces.Message): void { + state.consoleTab.addMessage(message); + if(core.state.settings.eventMessages && state.selectedConversation !== state.consoleTab) state.selectedConversation.addMessage(message); +} + +function isOfInterest(this: void, character: Character): boolean { + return character.isFriend || character.isBookmarked || state.privateMap[character.name.toLowerCase()] !== undefined; +} + +export default function(this: void): Interfaces.State { + state = new State(); + const connection = core.connection; + connection.onEvent('connecting', async(isReconnect) => { + state.channelConversations = []; + state.channelMap = {}; + if(!isReconnect) state.consoleTab = new ConsoleConversation(); + state.selectedConversation = state.consoleTab; + await state.reloadSettings(); + }); + connection.onEvent('connected', (isReconnect) => { + if(isReconnect) return; + for(const item of state.pinned.private) state.getPrivate(core.characters.get(item)); + queuedJoin(state.pinned.channels.slice()); + }); + core.channels.onEvent((type, channel) => { + const key = channel.id.toLowerCase(); + if(type === 'join') { + const conv = new ChannelConversation(channel); + state.channelMap[key] = conv; + state.channelConversations.push(conv); + state.addRecent(conv); + } else { + const conv = state.channelMap[key]!; + state.channelConversations.splice(state.channelConversations.indexOf(conv), 1); + delete state.channelMap[key]; + state.savePinned(); + if(state.selectedConversation === conv) state.show(state.consoleTab); + } + }); + + connection.onMessage('PRI', (data, time) => { + const char = core.characters.get(data.character); + if(char.isIgnored) return connection.send('IGN', {action: 'notify', character: data.character}); + const message = createMessage(MessageType.Message, char, decodeHTML(data.message), time); + const conv = state.getPrivate(char); + conv.addMessage(message); + }); + connection.onMessage('MSG', (data, time) => { + const char = core.characters.get(data.character); + if(char.isIgnored) return; + const conversation = state.channelMap[data.channel.toLowerCase()]!; + const message = createMessage(MessageType.Message, char, decodeHTML(data.message), time); + conversation.addMessage(message); + + let words: string[]; + if(conversation.settings.highlight !== Interfaces.Setting.Default) { + words = conversation.settings.highlightWords.slice(); + if(conversation.settings.highlight === Interfaces.Setting.True) words.push(core.connection.character); + } else { + words = core.state.settings.highlightWords.slice(); + if(core.state.settings.highlight) words.push(core.connection.character); + } + //tslint:disable-next-line:no-null-keyword + const results = words.length > 0 ? message.text.match(new RegExp(`\\b(${words.join('|')})\\b`, 'i')) : null; + if(results !== null) { + core.notifications.notify(conversation, data.character, l('chat.highlight', results[0], conversation.name, message.text), + characterImage(data.character), 'attention'); + if(conversation !== state.selectedConversation) conversation.unread = Interfaces.UnreadState.Mention; + message.isHighlight = true; + } else if(conversation.settings.notify === Interfaces.Setting.True) + core.notifications.notify(conversation, conversation.name, messageToString(message), + characterImage(data.character), 'attention'); + }); + connection.onMessage('LRP', (data, time) => { + const char = core.characters.get(data.character); + if(char.isIgnored) return; + const conv = state.channelMap[data.channel.toLowerCase()]!; + conv.addMessage(new Message(MessageType.Ad, char, decodeHTML(data.message), time)); + }); + connection.onMessage('RLL', (data, time) => { + const sender = core.characters.get(data.character); + if(sender.isIgnored) return; + let text: string; + if(data.type === 'bottle') + text = l('chat.bottle', `[user]${data.target}[/user]`); + else { + const results = data.results.length > 1 ? `${data.results.join('+')} = ${data.endresult}` : data.endresult.toString(); + text = l('chat.roll', data.rolls.join('+'), results); + } + const message = new Message(MessageType.Roll, sender, text, time); + if('channel' in data) { + const conversation = state.channelMap[(<{channel: string}>data).channel.toLowerCase()]!; + conversation.addMessage(message); + if(data.type === 'bottle' && data.target === core.connection.character) + core.notifications.notify(conversation, conversation.name, messageToString(message), + characterImage(data.character), 'attention'); + } else { + const char = core.characters.get( + data.character === connection.character ? (<{recipient: string}>data).recipient : data.character); + if(char.isIgnored) return connection.send('IGN', {action: 'notify', character: data.character}); + const conversation = state.getPrivate(char); + conversation.addMessage(message); + } + }); + connection.onMessage('NLN', (data, time) => { + const message = new EventMessage(l('events.login', `[user]${data.identity}[/user]`), time); + if(isOfInterest(core.characters.get(data.identity))) addEventMessage(message); + const conv = state.privateMap[data.identity.toLowerCase()]; + if(conv !== undefined && core.state.settings.eventMessages && conv !== state.selectedConversation) conv.addMessage(message); + }); + connection.onMessage('FLN', (data, time) => { + const message = new EventMessage(l('events.logout', `[user]${data.character}[/user]`), time); + if(isOfInterest(core.characters.get(data.character))) addEventMessage(message); + const conv = state.privateMap[data.character.toLowerCase()]; + if(conv === undefined) return; + conv.typingStatus = 'clear'; + if(core.state.settings.eventMessages && conv !== state.selectedConversation) conv.addMessage(message); + }); + connection.onMessage('TPN', (data) => { + const conv = state.privateMap[data.character.toLowerCase()]; + if(conv !== undefined) conv.typingStatus = data.status; + }); + connection.onMessage('CBU', (data, time) => { + const text = l('events.ban', data.channel, data.character, data.operator); + state.channelMap[data.channel.toLowerCase()]!.infoText = text; + addEventMessage(new EventMessage(text, time)); + }); + connection.onMessage('CKU', (data, time) => { + const text = l('events.kick', data.channel, data.character, data.operator); + state.channelMap[data.channel.toLowerCase()]!.infoText = text; + addEventMessage(new EventMessage(text, time)); + }); + connection.onMessage('CTU', (data, time) => { + const text = l('events.timeout', data.channel, data.character, data.operator, data.length.toString()); + state.channelMap[data.channel.toLowerCase()]!.infoText = text; + addEventMessage(new EventMessage(text, time)); + }); + connection.onMessage('HLO', (data, time) => addEventMessage(new EventMessage(data.message, time))); + connection.onMessage('BRO', (data, time) => { + const text = l('events.broadcast', `[user]${data.character}[/user]`, decodeHTML(data.message.substr(data.character.length + 23))); + addEventMessage(new EventMessage(text, time)); + }); + connection.onMessage('CIU', (data, time) => { + const text = l('events.invite', `[user]${data.sender}[/user]`, `[session=${data.title}]${data.name}[/session]`); + addEventMessage(new EventMessage(text, time)); + }); + connection.onMessage('ERR', (data, time) => { + state.selectedConversation.errorText = data.message; + addEventMessage(new EventMessage(`[color=red]${l('events.error', data.message)}[/color]`, time)); + }); + connection.onMessage('RTB', (data, time) => { + let url = 'https://www.f-list.net/'; + let text: string, character: string; + if(data.type === 'comment') { //tslint:disable-line:prefer-switch + switch(data.target_type) { + case 'newspost': + url += `newspost/${data.target_id}/#Comment${data.id}`; + break; + case 'bugreport': + url += `view_bugreport.php?id=/${data.target_id}/#${data.id}`; + break; + case 'changelog': + url += `log.php?id=/${data.target_id}/#${data.id}`; + break; + case 'feature': + url += `vote.php?id=/${data.target_id}/#${data.id}`; + } + const key = `events.rtbComment${(data.parent_id !== 0 ? 'Reply' : '')}`; + text = l(key, `[user]${data.name}[/user]`, l(`events.rtbComment_${data.target_type}`), `[url=${url}]${data.target}[/url]`); + character = data.name; + } else if(data.type === 'note') { + text = l('events.rtb_note', `[user]${data.sender}[/user]`, `[url=${url}view_note.php?note_id=${data.id}]${data.subject}[/url]`); + character = data.sender; + } else if(data.type === 'friendrequest') { + text = l(`events.rtb_friendrequest`, `[user]${data.name}[/user]`); + character = data.name; + } else { + switch(data.type) { + case 'grouprequest': + url += 'panel/group_requests.php'; + break; + case 'bugreport': + url += `view_bugreport.php?id=${data.id}`; + break; + case 'helpdeskticket': + url += `view_ticket.php?id=${data.id}`; + break; + case 'helpdeskreply': + url += `view_ticket.php?id=${data.id}`; + break; + case 'featurerequest': + url += `vote.php?fid=${data.id}`; + break; + default: //TODO + return; + } + text = l(`events.rtb_${data.type}`, `[user]${data.name}[/user]`, + data.title !== undefined ? `[url=${url}]${data.title}[/url]` : url); + character = data.name; + } + addEventMessage(new EventMessage(text, time)); + if(data.type === 'note') + core.notifications.notify(state.consoleTab, character, text, characterImage(character), 'newnote'); + }); + type SFCMessage = (Interfaces.Message & {sfc: Connection.ServerCommands['SFC'] & {confirmed?: true}}); + const sfcList: SFCMessage[] = []; + connection.onMessage('SFC', (data, time) => { + let text: string, message: Interfaces.Message; + if(data.action === 'report') { + text = l('events.report', `[user]${data.character}[/user]`, decodeHTML(data.tab), decodeHTML(data.report)); + core.notifications.notify(state.consoleTab, data.character, text, characterImage(data.character), 'modalert'); + message = new EventMessage(text, time); + safeAddMessage(sfcList, message, 500); + (message).sfc = data; + } else { + text = l('events.report.confirmed', `[user]${data.moderator}[/user]`, `[user]${data.character}[/user]`); + for(const item of sfcList) + if(item.sfc.logid === data.logid) { + item.sfc.confirmed = true; + break; + } + message = new EventMessage(text, time); + } + addEventMessage(message); + }); + connection.onMessage('STA', (data, time) => { + if(data.character === core.connection.character) { + addEventMessage(new EventMessage(l(data.statusmsg.length > 0 ? 'events.status.ownMessage' : 'events.status.own', + l(`status.${data.status}`), decodeHTML(data.statusmsg)), time)); + return; + } + const char = core.characters.get(data.character); + if(!isOfInterest(char)) return; + const status = l(`status.${data.status}`); + const key = data.statusmsg.length > 0 ? 'events.status.message' : 'events.status'; + const message = new EventMessage(l(key, `[user]${data.character}[/user]`, status, decodeHTML(data.statusmsg)), time); + addEventMessage(message); + const conv = state.privateMap[data.character.toLowerCase()]; + if(conv !== undefined && core.state.settings.eventMessages && conv !== state.selectedConversation) conv.addMessage(message); + }); + connection.onMessage('SYS', (data, time) => { + state.selectedConversation.infoText = data.message; + addEventMessage(new EventMessage(data.message, time)); + }); + connection.onMessage('JCH', (data, time) => { + if(data.character.identity === core.connection.character) return; + const conv = state.channelMap[data.channel.toLowerCase()]!; + if(conv.settings.joinMessages === Interfaces.Setting.False || conv.settings.joinMessages === Interfaces.Setting.Default && + !core.state.settings.joinMessages) return; + const text = l('events.channelJoin', `[user]${data.character.identity}[/user]`); + conv.addMessage(new EventMessage(text, time)); + }); + connection.onMessage('LCH', (data, time) => { + if(data.character === core.connection.character) return; + const conv = state.channelMap[data.channel.toLowerCase()]!; + if(conv.settings.joinMessages === Interfaces.Setting.False || conv.settings.joinMessages === Interfaces.Setting.Default && + !core.state.settings.joinMessages) return; + const text = l('events.channelLeave', `[user]${data.character}[/user]`); + conv.addMessage(new EventMessage(text, time)); + }); + connection.onMessage('ZZZ', (data, time) => { + state.selectedConversation.infoText = data.message; + addEventMessage(new EventMessage(data.message, time)); + }); + //TODO connection.onMessage('UPT', data => + return state; +} \ No newline at end of file diff --git a/chat/core.ts b/chat/core.ts new file mode 100644 index 0000000..8393aca --- /dev/null +++ b/chat/core.ts @@ -0,0 +1,104 @@ +import Vue from 'vue'; +import {WatchHandler} from 'vue/types/options'; +import BBCodeParser from './bbcode'; +import {Settings as SettingsImpl} from './common'; +import {Channel, Character, Connection, Conversation, Logs, Notifications, Settings, State as StateInterface} from './interfaces'; + +function createBBCodeParser(): BBCodeParser { + const parser = new BBCodeParser(); + for(const tag of state.settings.disallowedTags) + parser.removeTag(tag); + return parser; +} + +class State implements StateInterface { + _settings: Settings | undefined = undefined; + + get settings(): Settings { + if(this._settings === undefined) throw new Error('Settings load failed.'); + return this._settings; + } + + set settings(value: Settings) { + this._settings = value; + //tslint:disable-next-line:no-floating-promises + if(data.settingsStore !== undefined) data.settingsStore.set('settings', value); + data.bbCodeParser = createBBCodeParser(); + } +} + +interface VueState { + readonly channels: Channel.State + readonly characters: Character.State + readonly conversations: Conversation.State + readonly state: StateInterface +} + +const state = new State(); + +const vue = new Vue({ + data: { + channels: undefined, + characters: undefined, + conversations: undefined, + state + } +}); + +const data = { + connection: undefined, + logs: undefined, + settingsStore: undefined, + state: vue.state, + bbCodeParser: undefined, + conversations: undefined, + channels: undefined, + characters: undefined, + notifications: undefined, + register(this: void | never, module: 'characters' | 'conversations' | 'channels', + subState: Channel.State | Character.State | Conversation.State): void { + Vue.set(vue, module, subState); + data[module] = subState; + }, + watch(getter: (this: VueState) => T, callback: WatchHandler): void { + vue.$watch(getter, callback); + }, + async reloadSettings(): Promise { + const settings = new SettingsImpl(); + const loadedSettings = await core.settingsStore.get('settings'); + if(loadedSettings !== undefined) + for(const key in loadedSettings) settings[key] = loadedSettings[key]; + state._settings = settings; + } +}; + +export function init(this: void, connection: Connection, logsClass: new() => Logs.Basic, settingsClass: new() => Settings.Store, + notificationsClass: new() => Notifications): void { + data.connection = connection; + data.logs = new logsClass(); + data.settingsStore = new settingsClass(); + data.notifications = new notificationsClass(); + connection.onEvent('connecting', async() => { + await data.reloadSettings(); + data.bbCodeParser = createBBCodeParser(); + }); +} + +const core = <{ + readonly connection: Connection + readonly logs: Logs.Basic + readonly state: StateInterface + readonly settingsStore: Settings.Store + readonly conversations: Conversation.State + readonly characters: Character.State + readonly channels: Channel.State + readonly bbCodeParser: BBCodeParser + readonly notifications: Notifications + register(module: 'conversations', state: Conversation.State): void + register(module: 'channels', state: Channel.State): void + register(module: 'characters', state: Character.State): void + reloadSettings(): void + watch(getter: (this: VueState) => T, callback: WatchHandler): void +}>data; /*tslint:disable-line:no-any*///hack + +export default core; \ No newline at end of file diff --git a/chat/interfaces.ts b/chat/interfaces.ts new file mode 100644 index 0000000..3df474e --- /dev/null +++ b/chat/interfaces.ts @@ -0,0 +1,179 @@ +//tslint:disable:no-shadowed-variable +declare global { + interface Function { + //tslint:disable-next-line:ban-types no-any + bind(this: T, thisArg: any): T; + //tslint:disable-next-line:ban-types no-any + bind(this: (t: T) => TReturn, thisArg: any, arg: T): () => TReturn; + } +} + +import {Channel, Character} from '../fchat/interfaces'; +export {Connection, Channel, Character} from '../fchat/interfaces'; +export const userStatuses = ['online', 'looking', 'away', 'busy', 'dnd']; +export const channelModes = ['chat', 'ads', 'both']; + +export namespace Conversation { + export interface EventMessage { + readonly type: Message.Type.Event, + readonly text: string, + readonly time: Date + } + + export interface ChatMessage { + readonly type: Message.Type, + readonly sender: Character, + readonly text: string, + readonly time: Date + readonly isHighlight: boolean + } + + export type Message = EventMessage | ChatMessage; + + export namespace Message { + export enum Type { + Message, + Action, + Ad, + Roll, + Warn, + Event + } + } + + export type RecentConversation = {readonly channel: string, readonly name: string} | {readonly character: string}; + + export type TypingStatus = 'typing' | 'paused' | 'clear'; + + interface TabConversation extends Conversation { + isPinned: boolean + readonly maxMessageLength: number + close(): void + sort(newIndex: number): void + } + + export interface PrivateConversation extends TabConversation { + readonly character: Character + readonly typingStatus: TypingStatus + } + + export interface ChannelConversation extends TabConversation { + readonly channel: Channel + mode: Channel.Mode + readonly adCountdown: number + isSendingAds: boolean + } + + export function isPrivate(conversation: Conversation): conversation is PrivateConversation { + return (>conversation).character !== undefined; + } + + export function isChannel(conversation: Conversation): conversation is ChannelConversation { + return (>conversation).channel !== undefined; + } + + export interface State { + readonly privateConversations: ReadonlyArray + readonly channelConversations: ReadonlyArray + readonly consoleTab: Conversation + readonly recent: ReadonlyArray + readonly selectedConversation: Conversation + byKey(key: string): Conversation | undefined + getPrivate(character: Character): PrivateConversation + reloadSettings(): void + } + + export enum Setting { + True, False, Default + } + + export interface Settings { + readonly notify: Setting; + readonly highlight: Setting; + readonly highlightWords: ReadonlyArray; + readonly joinMessages: Setting; + } + + export const enum UnreadState { None, Unread, Mention } + + export interface Conversation { + enteredText: string; + infoText: string; + readonly name: string; + readonly messages: ReadonlyArray; + readonly reportMessages: ReadonlyArray; + readonly lastRead: Message | undefined + errorText: string + readonly key: string + readonly unread: UnreadState + settings: Settings + send(): void + loadLastSent(): void + show(): void + loadMore(): void + } +} + +export type Conversation = Conversation.Conversation; + +export namespace Logs { + export interface Basic { + logMessage(conversation: Conversation, message: Conversation.Message): void + getBacklog(conversation: Conversation): Promise> + } + + export interface Persistent extends Basic { + readonly conversations: ReadonlyArray<{readonly id: string, readonly name: string}> + getLogs(key: string, date: Date): Promise> + getLogDates(key: string): ReadonlyArray + } + + export function isPersistent(logs: Basic): logs is Persistent { + return (>logs).getLogs !== undefined; + } +} + +export namespace Settings { + export type Keys = { + settings: Settings, + pinned: {channels: string[], private: string[]}, + conversationSettings: {[key: string]: Conversation.Settings} + recent: Conversation.RecentConversation[] + }; + + export interface Store { + get(key: K, character?: string): Promise + getAvailableCharacters(): Promise> | undefined + set(key: K, value: Keys[K]): Promise + } + + export interface Settings { + readonly playSound: boolean; + readonly clickOpensMessage: boolean; + readonly disallowedTags: ReadonlyArray; + readonly notifications: boolean; + readonly highlight: boolean; + readonly highlightWords: ReadonlyArray; + readonly showAvatars: boolean; + readonly animatedEicons: boolean; + readonly idleTimer: number; + readonly messageSeparators: boolean; + readonly eventMessages: boolean; + readonly joinMessages: boolean; + readonly alwaysNotify: boolean; + readonly logMessages: boolean; + readonly logAds: boolean; + } +} + +export type Settings = Settings.Settings; + +export interface Notifications { + isInBackground: boolean + notify(conversation: Conversation, title: string, body: string, icon: string, sound: string): void + playSound(sound: string): void +} + +export interface State { + settings: Settings +} \ No newline at end of file diff --git a/chat/localize.ts b/chat/localize.ts new file mode 100644 index 0000000..747d089 --- /dev/null +++ b/chat/localize.ts @@ -0,0 +1,360 @@ +/*tslint:disable:max-line-length object-literal-sort-keys object-literal-key-quotes*/ +const strings: {[key: string]: string | undefined} = { + 'action.edit': 'Edit', + 'action.view': 'View', + 'action.cut': 'Cut', + 'action.copy': 'Copy', + 'action.paste': 'Paste', + 'action.copyLink': 'Copy Link', + 'action.suggestions': 'Suggestions', + 'action.open': 'Show', + 'action.quit': 'Exit', + 'action.updateAvailable': 'UPDATE AVAILABLE', + 'action.update': 'Restart now!', + 'action.cancel': 'Cancel', + 'help.fchat': 'FChat 3.0 Help and Changelog', + 'help.rules': 'F-List Rules', + 'help.faq': 'F-List FAQ', + 'help.report': 'How to report a user', + 'title': 'FChat 3.0', + 'version': 'Version {0}', + 'filter': 'Type to filter...', + 'login.account': 'Username', + 'login.password': 'Password', + 'login.host': 'Host', + 'login.advanced': 'Show advanced settings', + 'login.save': 'Save login', + 'login.error': 'Error logging you in: Could not connect to server', + 'login.submit': 'Log in', + 'login.working': 'Logging in...', + 'login.selectCharacter': 'Select a character', + 'login.connect': 'Connect', + 'login.connecting': 'Connecting...', + 'login.connectError': 'Connection error: Could not connect to server', + 'channelList.public': 'Official channels', + 'channelList.private': 'Open rooms', + 'channelList.create': 'Create room', + 'channelList.createName': 'Room name', + 'chat.logout': 'Log out', + 'chat.status': 'Status:', + 'chat.setStatus': 'Set status', + 'chat.setStatus.status': 'Status', + 'chat.setStatus.message': 'Status message (optional)', + 'chat.menu': 'Menu', + 'chat.channels': 'Channels', + 'chat.pms': 'PMs', + 'chat.consoleTab': 'Console', + 'chat.confirmLeave': 'You are still connected to chat. Would you like to disconnect?', + 'chat.highlight': 'mentioned {0} in {1}:\n{2}', + 'chat.roll': 'rolls {0}: {1}', + 'chat.bottle': 'spins the bottle: {0}', + 'chat.adCountdown': 'You must wait {0}m{1}s to post another ad in this channel.', + 'chat.consoleChat': 'You cannot chat here.', + 'chat.typing.typing': '{0} is typing...', + 'chat.typing.paused': '{0} has entered text.', + 'chat.errorOffline': '{0} is offline, you cannot send them a message right now.', + 'chat.errorIgnored': 'You are ignoring {0}. If you would like to send them a message, please unignore them first.', + 'chat.disconnected': 'You were disconnected from chat.\nAttempting to reconnect.', + 'chat.disconnected.title': 'Disconnected', + 'chat.ignoreList': 'You are currently ignoring: {0}', + 'logs.title': 'Logs', + 'logs.conversation': 'Conversation', + 'logs.date': 'Date', + 'user.profile': 'Profile', + 'user.message': 'Open conversation', + 'user.messageJump': 'View conversation', + 'user.bookmark': 'Bookmark', + 'user.unbookmark': 'Unbookmark', + 'user.ignore': 'Ignore', + 'user.unignore': 'Unignore', + 'user.memo': 'View memo', + 'user.memo.action': 'Update memo', + 'user.report': 'Report user', + 'user.channelKick': 'Kick from channel', + 'user.chatKick': 'Chat kick', + 'users.title': 'People', + 'users.friends': 'Friends', + 'users.bookmarks': 'Bookmarks', + 'users.members': 'Members', + 'chat.report': 'Alert Staff', + 'chat.report.description': ` +[color=red]Before you alert the moderators, PLEASE READ:[/color] +If you're just having personal trouble with someone, right-click their name and ignore them. +Please make sure what you're reporting is a violation of the site's [url=https://wiki.f-list.net/Code_of_Conduct]Code of Conduct[/url] otherwise nothing will be done. + +This tool is intended for chat moderation. If you have a question, please visit our [url=https://wiki.f-list.net/Frequently_Asked_Questions]FAQ[/url] first, and if that doesn't help, join [session=Helpdesk]Helpdesk[/session] and ask your question there. + +If your problem lies anywhere outside of the chat, please send in a Ticket instead. + +For a more comprehensive guide as how and when to report another user, please [url=https://wiki.f-list.net/How_to_Report_a_User]consult this page.[/url] + +Please provide a brief summary of your problem and the rules that have been violated. +[color=red]DO NOT PASTE LOGS INTO THIS FIELD. +SELECT THE TAB YOU WISH TO REPORT, LOGS ARE AUTOMATICALLY ATTACHED[/color]`, + 'chat.report.channel.user': 'Reporting user {0} in channel {1}', + 'chat.report.channel': 'General report for channel {0}', + 'chat.report.channel.description': 'If you wish to report a specific user, please right-click them and select "Report".', + 'chat.report.private': 'Reporting private conversation with user {0}', + 'chat.report.text': 'Report text', + 'chat.recentConversations': 'Recent conversations', + 'settings.tabs.general': 'General', + 'settings.tabs.notifications': 'Notifications', + 'settings.tabs.import': 'Import', + 'settings.open': 'Settings', + 'settings.action': 'Change settings', + 'settings.import': 'Import settings', + 'settings.import.selectCharacter': 'Select a character', + 'settings.import.confirm': `You are importing settings from your character {0}. +This will overwrite any and all settings, pinned conversations and conversation settings of character {1}. +Logs and recent conversations will not be touched. +You may need to log out and back in for some settings to take effect. +Are you sure?`, + 'settings.playSound': 'Play notification sounds', + 'settings.notifications': 'Display notifications', + 'settings.clickOpensMessage': 'Clicking users opens messages (instead of their profile)', + 'settings.disallowedTags': 'Disallowed BBCode tags (comma-separated)', + 'settings.highlight': 'Notify for messages containing your name', + 'settings.highlightWords': 'Custom highlight notify words (comma-separated)', + 'settings.showAvatars': 'Show character avatars', + 'settings.animatedEicons': 'Animate [eicon]s', + 'settings.idleTimer': 'Idle timer (minutes, clear to disable)', + 'settings.messageSeparators': 'Display separators between messages', + 'settings.eventMessages': 'Also display console messages in current tab', + 'settings.joinMessages': 'Display join/leave messages in channels', + 'settings.alwaysNotify': 'Always notify for PMs/highlights, even when looking at the tab', + 'settings.closeToTray': 'Close to tray', + 'settings.spellcheck': 'Spellcheck', + 'settings.spellcheck.disabled': 'Disabled', + 'settings.theme': 'Theme', + 'settings.logMessages': 'Log messages', + 'settings.logAds': 'Log ads', + 'conversationSettings.title': 'Settings', + 'conversationSettings.action': 'Edit settings for {0}', + 'conversationSettings.default': 'Default', + 'conversationSettings.true': 'Yes', + 'conversationSettings.false': 'No', + 'conversationSettings.notify': 'Notify for messages', + 'channel.mode.ads': 'Ads', + 'channel.mode.chat': 'Chat', + 'channel.mode.both': 'Both', + 'channel.official': 'Official channel', + 'channel.description': 'Description', + 'manageChannel.open': 'Manage', + 'manageChannel.action': 'Manage {0}', + 'manageChannel.submit': 'Save settings', + 'manageChannel.mods': 'Channel moderators', + 'manageChannel.modAdd': 'Add moderator', + 'manageChannel.modAddName': 'New moderator name', + 'manageChannel.isPublic': 'Is public (i.e. in the channel list; anyone can join without an invite)', + 'manageChannel.mode': 'Allowed messages', + 'manageChannel.description': 'Description', + 'characterSearch.open': 'Character Search', + 'characterSearch.action': 'Search characters', + 'characterSearch.again': 'Start another search', + 'characterSearch.results': 'Results', + 'characterSearch.kinks': 'Kinks', + 'characterSearch.kinkNotice': 'Must select at least one kink.', + 'characterSearch.genders': 'Genders', + 'characterSearch.orientations': 'Orientations', + 'characterSearch.languages': 'Languages', + 'characterSearch.furryprefs': 'Furry preferences', + 'characterSearch.roles': 'Dom/sub roles', + 'characterSearch.positions': 'Positions', + 'characterSearch.error.noResults': 'There were no search results.', + 'characterSearch.error.throttle': 'You must wait five seconds between searches.', + 'characterSearch.error.tooManyResults': 'There are too many search results, please narrow your search.', + 'events.broadcast': '{0} has broadcast {1}', + 'events.invite': '{0} has invited you to join {1}', + 'events.error': 'Error: {0}', + 'events.rtbCommentReply': '{0} replied to your comment on the {1}: {2}', + 'events.rtbComment': '{0} commented on your {1}: {2}', + 'events.rtbComment_bugreport': 'bug report', + 'events.rtbComment_changelog': 'changelog post', + 'events.rtbComment_feature': 'feature request', + 'events.rtbComment_newspost': 'news post', + 'events.rtb_note': '{0} has sent you a note: {1}', + 'events.rtb_bugreport': '{0} submitted a bug report: {1}', + 'events.rtb_featurerequest': '{0} submitted a feature request: {1}', + 'events.rtb_grouprequest': '{0} requested a group named: {1}', + 'events.rtb_helpdeskreply': '{0} replied to [url={1}]a help desk ticket you are involved in[/url].', + 'events.rtb_helpdeskticket': '{0} submitted a help desk ticket: {1}', + 'events.rtb_friendrequest': '{0} has sent you a friend request.', + 'events.report': '[b][color=red]MODERATOR ALERT[/color][/b] - Report by {0}:\nCurrent tab: {1}\nReport: {2}', + 'events.report.confirmed': '{0} is handling {1}\'s report.', + 'events.report.confirm': 'Confirm report', + 'events.report.viewLog': 'View log', + 'events.status': '{0} is now {1}.', + 'events.status.message': '{0} is now {1}: {2}', + 'events.status.own': 'You are now {0}.', + 'events.status.ownMessage': 'You are now {0}: {1}', + 'events.ban': '{2} has banned {1} from {0}.', + 'events.timeout': '{2} has timed out {1} from {0} for {3} minutes.', + 'events.kick': '{2} has kicked {1} from {0}.', + 'events.login': '{0} has logged in.', + 'events.logout': '{0} has logged out.', + 'events.channelJoin': '{0} has joined the channel.', + 'events.channelLeave': '{0} has left the channel.', + 'commands.unknown': 'Unknown command. For a list of valid commands, please click the ? button.', + 'commands.badContext': 'This command cannot be used here. Please use the Help (click the ? button) if you need further information.', + 'commands.tooFewParams': 'This command requires more parameters. Please use the Help (click the ? button) if you need further information.', + 'commands.invalidParam': 'The value for the parameter {0} is invalid. Please use the Help (click the ? button) if you need further information.', + 'commands.invalidCharacter': 'The character you entered is not online. Put the name in double quotes if you want to override. Please use the Help (click the ? button) if you need further information.', + 'commands.help': 'Command Help', + 'commands.help.syntax': 'Syntax: {0}', + 'commands.help.contextChannel': 'This command can be executed in a channel tab.', + 'commands.help.contextPrivate': 'This command can be executed in a private conversation tab.', + 'commands.help.contextChonsole': 'This command can be executed in the console tab.', + 'commands.help.permissionRoomOp': 'This command requires you to be an operator in the selected channel.', + 'commands.help.permissionRoomOwner': 'This command requires you to be the owner of the selected channel.', + 'commands.help.permissionChannelMod': 'This command requires you to be an official channel moderator.', + 'commands.help.permissionChatOp': 'This command requires you to be a global chat operator.', + 'commands.help.permissionAdmin': 'This command requires you to be an admin.', + 'commands.help.parameters': 'Parameters:', + 'commands.help.paramOptional': '{0} (optional):', + 'commands.param_character': 'Character', + 'commands.param_character.help': 'The name of a character. Must be valid and logged in - override by putting in double quotes.', + 'commands.reward': 'Reward', + 'commands.reward.help': 'Reward a user, giving them a special status until they change it or log out.', + 'commands.greports': 'Pending reports', + 'commands.greports.help': 'Requests a list of pending chat reports from the server.', + 'commands.join': 'Join channel', + 'commands.join.help': 'Joins the channel with the given name/ID.', + 'commands.join.param0': 'Channel ID', + 'commands.join.param0.help': 'The name/ID of the channel to join. For official channels, this is the name, for private rooms this is the ID.', + 'commands.close': 'Close tab', + 'commands.close.help': 'Closes the currently viewed PM or channel tab.', + 'commands.uptime': 'Uptime', + 'commands.uptime.help': 'Requests statistics about server uptime.', + 'commands.status': 'Set status', + 'commands.status.help': 'Sets your status along with an optional message.', + 'commands.status.param0': 'Status', + 'commands.status.param0.help': 'A valid status, namely "online", "busy", "looking", "away", "dnd" or "busy".', + 'commands.status.param1': 'Message', + 'commands.status.param1.help': 'An optional status message of up to 255 bytes.', + 'commands.priv': 'Open conversation', + 'commands.priv.help': 'Opens a conversation with the given character.', + 'commands.broadcast': 'Chat broadcast', + 'commands.broadcast.help': 'Broadcast a message, alerting all currently connected characters.', + 'commands.broadcast.param0': 'Message', + 'commands.broadcast.param0.help': 'Broadcast message. May contain valid chat BBCode.', + 'commands.makeroom': 'Create private room', + 'commands.makeroom.help': 'Creates a private room. Only people you /invite will be able to join it, and it will not be listed, until you open it with /openroom.', + 'commands.makeroom.param0': 'Room name', + 'commands.makeroom.param0.help': 'A name for your new private room. Must be 1-64 in length.', + 'commands.ignore': 'Ignore a character', + 'commands.ignore.help': 'Ignores the given character, and discards all of their messages.', + 'commands.unignore': 'Unignore a character', + 'commands.unignore.help': 'Removes the given character from your ignore list, and allows them to send you messages again.', + 'commands.ignorelist': 'Ignore list', + 'commands.ignorelist.help': 'Lists all of the characters currently on your ignore list.', + 'commands.roll': 'Dice roll', + 'commands.roll.help': 'Rolls dice (RNG), displaying the result to all members of the current tab.', + 'commands.roll.param0': 'Dice', + 'commands.roll.param0.help': 'Syntax: [1-9]d[1-100]. Addition and subtraction of rolls and fixed numbers is also possible. Example: /roll 1d6+1d20-5', + 'commands.bottle': 'Spin the bottle', + 'commands.bottle.help': 'Spins a bottle, randomly selecting a member of the current tab and displaying it to all.', + 'commands.ad': 'Post as ad', + 'commands.ad.help': 'A quick way to post an ad in the current channel. You may receive an error if ads are not allowed in that channel.', + 'commands.ad.param0': 'Message', + 'commands.ad.param0.help': 'The message to post as an ad.', + 'commands.me': 'Post as action', + 'commands.me.help': 'This will cause your message to be formatted differently, as an action your character is performing.', + 'commands.me.param0': 'Message', + 'commands.me.param0.help': 'The message to post as an action - the action you would like your character to perform.', + 'commands.warn': 'Warn channel', + 'commands.warn.help': 'Provides a way for channel moderators to warn/alert members. This message will be formatted differently, and is often used as a warning before moderator action.', + 'commands.warn.param0': 'Message', + 'commands.warn.param0.help': 'The message to post as a warning.', + 'commands.kick': 'Channel kick', + 'commands.kick.help': 'Removes a character from the current channel. They are free to rejoin - use /ban or /timeout if you want to get rid of them for a longer period of time.', + 'commands.ban': 'Channel ban', + 'commands.ban.help': 'Bans a character from the current channel. They will not be able to rejoin unless and until you undo this with /unban.', + 'commands.unban': 'Channel unban', + 'commands.unban.help': 'Unbans a character from the current channel, allowing them to rejoin.', + 'commands.banlist': 'Channel ban list', + 'commands.banlist.help': 'Requests the ban list for the current channel. The server will reply with a system response, which you will be able to view in the Console tab.', + 'commands.timeout': 'Channel timeout', + 'commands.timeout.help': 'Temporarily bans the given character from the current channel. Mind the comma in the syntax!', + 'commands.timeout.param1': 'Duration', + 'commands.timeout.param1.help': 'The number of minutes to ban the character for.', + 'commands.op': 'Promote to Channel OP', + 'commands.op.help': 'Promotes a character to channel OP in the current channel.', + 'commands.deop': 'Demote from Channel OP', + 'commands.deop.help': 'Demotes a character from channel OP in the current channel.', + 'commands.oplist': 'List Channel OPs', + 'commands.oplist.help': 'Lists all the OPs of the current channel.', + 'commands.setowner': 'Set channel owner', + 'commands.setowner.help': 'Set the owner of a channel to another character. The previous owner will be demoted to a member.', + 'commands.invite': 'Invite to room', + 'commands.invite.help': 'Invites a character to the current channel. This will allow them to join it even if it is a closed room. You can revoke this with /kick, /ban or /timeout.', + 'commands.closeroom': 'Close room', + 'commands.closeroom.help': 'Closes the current channel. This will only allow people you /invite to join it, and remove it from the rooms list.', + 'commands.openroom': 'Open room', + 'commands.openroom.help': 'Opens the current channel. This will allow anyone to join it, and let it be listed in the rooms list.', + 'commands.killchannel': 'Destroy room', + 'commands.killchannel.help': 'PERMANENTLY kills/destroys/removes the current room. All associated settings and prestige will be lost. Make sure this is what you want to do, you cannot undo it.', + 'commands.createchannel': 'Create official channel', + 'commands.createchannel.help': 'Creates an official, staff-moderated room.', + 'commands.createchannel.param0': 'Channel name', + 'commands.createchannel.param0.help': 'A name for the new official channel.', + 'commands.setmode': 'Set room mode', + 'commands.setmode.help': 'Set whether ads and/or chat are allowed in the current channel.', + 'commands.setmode.param0': 'Mode', + 'commands.setmode.param0.help': 'A valid room mode, namely "ads", "chat" or "both".', + 'commands.setdescription': 'Set room description', + 'commands.setdescription.help': 'Set the description for the current room.', + 'commands.setdescription.param0': 'Description', + 'commands.setdescription.param0.help': 'New description for the room. May contain up to 50,000 characters, and valid chat BBCode.', + 'commands.code': 'Copy channel code', + 'commands.code.help': 'Copies a BBCode link to the current channel into your clipboard. This can be pasted anywhere else on chat to render a link to this channel.', + 'commands.code.success': 'Channel code copied to your clipboard.', + 'commands.gkick': 'Chat kick', + 'commands.gkick.help': 'Removes a character from the chat. They are free to rejoin - use /gban or /gtimeout if you want to get rid of them for a longer period of time.', + 'commands.gban': 'Chat ban', + 'commands.gban.help': 'Bans a character from the chat. They will not be able to reconnect unless and until you undo this with /unban.', + 'commands.gunban': 'Chat unban', + 'commands.gunban.help': 'Unbans a character from the chat, allowing them to reconnect.', + 'commands.gtimeout': 'Chat timeout', + 'commands.gtimeout.help': 'Temporarily bans the given character from F-Chat. Mind the comma in the syntax!', + 'commands.gtimeout.param1': 'Duration', + 'commands.gtimeout.param1.help': 'The number of minutes to ban the character for.', + 'commands.gtimeout.param2': 'Reason', + 'commands.gtimeout.param2.help': 'The reason for the chat timeout.', + 'commands.gop': 'Promote to Chat OP', + 'commands.gop.help': 'Promotes a character to global chat OP.', + 'commands.gdeop': 'Demote from Chat OP', + 'commands.gdeop.help': 'Demotes a character from global chat OP.', + 'commands.reloadconfig': 'Reload config', + 'commands.reloadconfig.help': 'Reload server-side config from disk.', + 'commands.reloadconfig.param0': 'Save?', + 'commands.reloadconfig.param0.help': 'Save ops, bans and channels to disk first.', + 'commands.xyzzy': 'Debug', + 'commands.xyzzy.help': 'Execute debug command on the server.', + 'commands.xyzzy.param0': 'Command', + 'commands.xyzzy.param0.help': 'The command to execute.', + 'commands.xyzzy.param1': 'Arguments', + 'commands.xyzzy.param1.help': 'The arguments to the command.', + 'status.online': 'Online', + 'status.away': 'Away', + 'status.busy': 'Busy', + 'status.looking': 'Looking', + 'status.dnd': 'Do Not Disturb', + 'status.idle': 'Idle', + 'status.offline': 'Offline', + 'status.crown': 'Rewarded by Admin', + 'importer.importGeneral': 'slimCat data has been detected on your computer.\nWould you like to import general settings?', + 'importer.importCharacter': 'slimCat data for this character has been detected on your computer.\nWould you like to import settings and logs?\nThis may take a while.\nAny existing FChat 3.0 data for this character will be overwritten.', + 'importer.importing': 'Importing data', + 'importer.importingNote': 'Importing logs. This may take a couple of minutes. Please do not close the application, even if it appears to hang for a while, as you may end up with incomplete logs.' +}; + +export default function l(key: string, ...args: string[]): string { + let i = args.length; + let str = strings[key]; + if(str === undefined) + if(process.env.NODE_ENV !== 'production') throw new Error(`String ${key} does not exist.`); + else return ''; + while(i-- > 0) + str = str.replace(new RegExp(`\\{${i}\\}`, 'igm'), args[i]); + return str; +} \ No newline at end of file diff --git a/chat/localstorage.ts b/chat/localstorage.ts new file mode 100644 index 0000000..6fc53fa --- /dev/null +++ b/chat/localstorage.ts @@ -0,0 +1,74 @@ +import {Conversation, Logs as Logging, Settings} from './interfaces'; +import core from './core'; +import {Message} from './common'; + +export class Logs implements Logging.Basic { + logMessage(conversation: Conversation, message: Conversation.Message) { + const key = 'logs.' + conversation.key; + const previous = window.localStorage.getItem(key); + const serialized = this.serialize(message); + let data = previous ? previous + serialized : serialized; + while(data.length > 100000) { + data = data.substr(this.deserialize(data, 0).index); + } + window.localStorage.setItem(key, data); + } + + getBacklog(conversation: Conversation) { + let messages: Conversation.Message[] = []; + const str = window.localStorage.getItem('logs.' + conversation.key); + if(!str) return Promise.resolve(messages); + let index = str.length; + while(true) { + index -= (str.charCodeAt(index - 2) << 8 | str.charCodeAt(index - 1)) + 2; + messages.unshift(this.deserialize(str, index).message); + if(index == 0) break; + } + return Promise.resolve(messages); + } + + private serialize(message: Conversation.Message) { + const time = message.time.getTime() / 1000; + let str = String.fromCharCode(time >> 24) + String.fromCharCode(time >> 16) + String.fromCharCode(time >> 8) + String.fromCharCode(time % 256); + str += String.fromCharCode(message.type); + if(message.type !== Conversation.Message.Type.Event) { + str += String.fromCharCode(message.sender.name.length); + str += message.sender.name; + } else str += '\0'; + const textLength = message.text.length; + str += String.fromCharCode(textLength >> 8) + String.fromCharCode(textLength % 256); + str += message.text; + const length = str.length; + str += String.fromCharCode(length >> 8) + String.fromCharCode(length % 256); + return str; + } + + private deserialize(str: string, index: number): {message: Conversation.Message, index: number} { + const time = str.charCodeAt(index++) << 24 | str.charCodeAt(index++) << 16 | str.charCodeAt(index++) << 8 | str.charCodeAt(index++); + const type = str.charCodeAt(index++); + const senderLength = str.charCodeAt(index++); + const sender = str.substring(index, index += senderLength); + const messageLength = str.charCodeAt(index++) << 8 | str.charCodeAt(index++); + const message = str.substring(index, index += messageLength); + return { + message: new Message(type, core.characters.get(sender), message, new Date(time * 1000)), + index: index + }; + } +} + +export class SettingsStore implements Settings.Store { + get(key: K) { + const stored = window.localStorage.getItem('settings.' + key); + return Promise.resolve(stored && JSON.parse(stored)); + } + + set(key: K, value: Settings.Keys[K]) { + window.localStorage.setItem('settings.' + key, JSON.stringify(value)); + return Promise.resolve(); + } + + getAvailableCharacters() { + return undefined; + } +} \ No newline at end of file diff --git a/chat/message_view.ts b/chat/message_view.ts new file mode 100644 index 0000000..c78c71d --- /dev/null +++ b/chat/message_view.ts @@ -0,0 +1,46 @@ +import Vue, {Component, CreateElement, RenderContext, VNode, VNodeChildren} from 'vue'; +import {BBCodeView} from './bbcode'; +import {formatTime} from './common'; +import core from './core'; +import {Conversation} from './interfaces'; +import UserView from './user_view'; +// TODO convert this to single-file once Vue supports it for functional components. +// template: +// [{{formatTime(message.time)}}] +// * +// +// : +// + +const userPostfix: {[key: number]: string | undefined} = { + [Conversation.Message.Type.Message]: ': ', + [Conversation.Message.Type.Ad]: ': ', + [Conversation.Message.Type.Action]: '' +}; +//tslint:disable-next-line:variable-name +const MessageView: Component = { + functional: true, + render(this: Vue, createElement: CreateElement, context: RenderContext): VNode { + /*tslint:disable:no-unsafe-any*///context.props is any + const message: Conversation.Message = context.props.message; + const children: (VNode | string | VNodeChildren)[] = [`[${formatTime(message.time)}] `]; + /*tslint:disable-next-line:prefer-template*///unreasonable here + let classes = `message message-${Conversation.Message.Type[message.type].toLowerCase()}` + + (core.state.settings.messageSeparators ? ' message-block' : '') + + (message.type !== Conversation.Message.Type.Event && message.sender.name === core.connection.character ? ' message-own' : '') + + ((context.props.classes !== undefined) ? ` ${context.props.classes}` : ''); + if(message.type !== Conversation.Message.Type.Event) { + children.push((message.type === Conversation.Message.Type.Action) ? '*' : '', + createElement(UserView, {props: {character: message.sender, channel: context.props.channel}}), + userPostfix[message.type] !== undefined ? userPostfix[message.type]! : ' '); + if(message.isHighlight) classes += ' message-highlight'; + } + children.push(createElement(BBCodeView, {props: {unsafeText: message.text}})); + const node = createElement('div', {attrs: {class: classes}}, children); + node.key = context.data.key; + return node; + //tslint:enable + } +}; + +export default MessageView; \ No newline at end of file diff --git a/chat/notifications.ts b/chat/notifications.ts new file mode 100644 index 0000000..0050d34 --- /dev/null +++ b/chat/notifications.ts @@ -0,0 +1,42 @@ +import core from './core'; +import {Conversation, Notifications as Interface} from './interfaces'; + +const codecs: {[key: string]: string} = {mpeg: 'mp3', wav: 'wav', ogg: 'ogg'}; + +export default class Notifications implements Interface { + isInBackground = false; + + notify(conversation: Conversation, title: string, body: string, icon: string, sound: string): void { + if(!this.isInBackground && conversation === core.conversations.selectedConversation && !core.state.settings.alwaysNotify) return; + this.playSound(sound); + if(core.state.settings.notifications) { + /*tslint:disable-next-line:no-object-literal-type-assertion*///false positive + const notification = new Notification(title, {body, icon, silent: true}); + notification.onclick = () => { + conversation.show(); + window.focus(); + notification.close(); + }; + } + } + + playSound(sound: string): void { + if(!core.state.settings.playSound) return; + const id = `soundplayer-${sound}`; + let audio = document.getElementById(id); + if(audio === null) { + audio = document.createElement('audio'); + audio.id = id; + //tslint:disable-next-line:forin + for(const name in codecs) { + const src = document.createElement('source'); + src.type = `audio/${name}`; + //tslint:disable-next-line:no-require-imports + src.src = require(`./assets/${sound}.${codecs[name]}`); + audio.appendChild(src); + } + } + //tslint:disable-next-line:no-floating-promises + audio.play(); + } +} \ No newline at end of file diff --git a/chat/qs.d.ts b/chat/qs.d.ts new file mode 100644 index 0000000..1f98645 --- /dev/null +++ b/chat/qs.d.ts @@ -0,0 +1,3 @@ +declare module 'qs' { + export function stringify(data: object): string; +} \ No newline at end of file diff --git a/chat/slash_commands.ts b/chat/slash_commands.ts new file mode 100644 index 0000000..7e0c399 --- /dev/null +++ b/chat/slash_commands.ts @@ -0,0 +1,352 @@ +import core from './core'; +import {Character, Conversation, userStatuses} from './interfaces'; +import l from './localize'; +import ChannelConversation = Conversation.ChannelConversation; +import PrivateConversation = Conversation.PrivateConversation; + +export const enum ParamType { + String, Number, Character, Enum +} + +const defaultDelimiters: {[key: number]: string | undefined} = {[ParamType.Character]: ',', [ParamType.String]: ''}; + +export function isCommand(this: void, text: string): boolean { + return text.charAt(0) === '/' && text.substr(1, 2) !== 'me' && text.substr(1, 4) !== 'warn'; +} + +export function parse(this: void | never, input: string, context: CommandContext): ((this: Conversation) => void) | string { + const commandEnd = input.indexOf(' '); + const name = input.substring(1, commandEnd !== -1 ? commandEnd : undefined); + const command = commands[name]; + if(command === undefined) return l('commands.unknown'); + const args = `${commandEnd !== -1 ? input.substr(commandEnd + 1) : ''}`; + if(command.context !== undefined && (command.context & context) === 0) return l('commands.badContext'); + + let index = 0; + const values: (string | number)[] = []; + + if(command.params !== undefined) + for(let i = 0; i < command.params.length; ++i) { + const param = command.params[i]; + if(index === -1) + if(param.optional !== undefined) continue; + else return l('commands.tooFewParams'); + let delimiter = param.delimiter !== undefined ? param.delimiter : defaultDelimiters[param.type]; + if(delimiter === undefined) delimiter = ' '; + const endIndex = delimiter.length > 0 ? args.indexOf(delimiter, index) : args.length; + const value = args.substring(index, endIndex !== -1 ? endIndex : undefined); + if(value.length === 0) + if(param.optional !== undefined) continue; + else return l('commands.tooFewParams'); + values[i] = value; + switch(param.type) { + case ParamType.String: + if(i === command.params.length - 1) values[i] = args.substr(index); + continue; + case ParamType.Enum: + if((param.options !== undefined ? param.options : []).indexOf(value) === -1) + return l('commands.invalidParam', l(`commands.${name}.param${i}`)); + break; + case ParamType.Number: + console.log(value); + const num = parseInt(value, 10); + if(isNaN(num)) + return l('commands.invalidParam', l(`commands.${name}.param${i}`)); + values[i] = num; + break; + case ParamType.Character: + if(value.charAt(0) === '"' && value.charAt(value.length - 1) === '"') { + values[i] = value.substring(1, value.length - 1); + break; + } + const char = core.characters.get(value); + if(char.status === 'offline') return l('commands.invalidCharacter'); + } + index = endIndex + 1; + } + if(command.context !== undefined) + return function(this: Conversation): void { + command.exec(this, ...values); + }; + else return () => command.exec(...values); +} + +export const enum CommandContext { + Console = 1 << 0, + Channel = 1 << 1, + Private = 1 << 2 +} + +export enum Permission { + RoomOp = -1, + RoomOwner = -2, + ChannelMod = 4, + ChatOp = 2, + Admin = 1 +} + +export interface Command { + readonly context?: CommandContext, //default implicit Console | Channel | Private + readonly permission?: Permission + readonly documented?: false, //default true + readonly params?: { + readonly type: ParamType + readonly options?: ReadonlyArray, //default undefined + readonly optional?: true, //default false + readonly delimiter?: string, //default ' ' (',' for type: Character) + validator?(data: string | number): boolean //default undefined + }[] + exec(context?: Conversation | string | number, ...params: (string | number | undefined)[]): void +} + +const commands: {readonly [key: string]: Command | undefined} = { + me: { + exec: () => 'stub', + context: CommandContext.Channel | CommandContext.Private, + params: [{type: ParamType.String}] + }, + reward: { + exec: (character: string) => core.connection.send('RWD', {character}), + permission: Permission.Admin, + params: [{type: ParamType.Character}] + }, + greports: { + permission: Permission.ChannelMod, + exec: () => core.connection.send('PCR') + }, + join: { + exec: (channel: string) => core.connection.send('JCH', {channel}), + params: [{type: ParamType.String}] + }, + close: { + exec: (conv: PrivateConversation | ChannelConversation) => conv.close(), + context: CommandContext.Private | CommandContext.Channel + }, + priv: { + exec: (character: string) => core.conversations.getPrivate(core.characters.get(character)).show(), + params: [{type: ParamType.Character}] + }, + uptime: { + exec: () => core.connection.send('UPT') + }, + status: { + //tslint:disable-next-line:no-inferrable-types + exec: (status: Character.Status, statusmsg: string = '') => core.connection.send('STA', {status, statusmsg}), + params: [{type: ParamType.Enum, options: userStatuses}, {type: ParamType.String, optional: true}] + }, + ad: { + exec: (conv: ChannelConversation, message: string) => + core.connection.send('LRP', {channel: conv.channel.id, message}), + context: CommandContext.Channel, + params: [{type: ParamType.String}] + }, + roll: { + exec: (conv: ChannelConversation | PrivateConversation, dice: string) => { + if(Conversation.isChannel(conv)) core.connection.send('RLL', {channel: conv.channel.id, dice}); + else core.connection.send('RLL', {recipient: conv.character.name, dice}); + }, + context: CommandContext.Channel | CommandContext.Private, + params: [{type: ParamType.String}] + }, + bottle: { + exec: (conv: ChannelConversation | PrivateConversation) => { + if(Conversation.isChannel(conv)) core.connection.send('RLL', {channel: conv.channel.id, dice: 'bottle'}); + else core.connection.send('RLL', {recipient: conv.character.name, dice: 'bottle'}); + }, + context: CommandContext.Channel | CommandContext.Private + }, + warn: { + exec: () => 'stub', + permission: Permission.RoomOp, + context: CommandContext.Channel, + params: [{type: ParamType.String}] + }, + kick: { + exec: (conv: ChannelConversation, character: string) => + core.connection.send('CKU', {channel: conv.channel.id, character}), + permission: Permission.RoomOp, + context: CommandContext.Channel, + params: [{type: ParamType.Character}] + }, + ban: { + exec: (conv: ChannelConversation, character: string) => + core.connection.send('CBU', {channel: conv.channel.id, character}), + permission: Permission.RoomOp, + context: CommandContext.Channel, + params: [{type: ParamType.Character}] + }, + unban: { + exec: (conv: ChannelConversation, character: string) => + core.connection.send('CUB', {channel: conv.channel.id, character}), + permission: Permission.RoomOp, + context: CommandContext.Channel, + params: [{type: ParamType.Character}] + }, + banlist: { + exec: (conv: ChannelConversation) => core.connection.send('CBL', {channel: conv.channel.id}), + permission: Permission.RoomOp, + context: CommandContext.Channel + }, + timeout: { + exec: (conv: ChannelConversation, character: string, length: number) => + core.connection.send('CTU', {channel: conv.channel.id, character, length}), + permission: Permission.RoomOp, + context: CommandContext.Channel, + params: [{type: ParamType.Character, delimiter: ','}, {type: ParamType.Number, validator: (x) => x >= 1}] + }, + gkick: { + exec: (character: string) => core.connection.send('KIK', {character}), + permission: Permission.ChatOp, + params: [{type: ParamType.Character}] + }, + gban: { + exec: (character: string) => core.connection.send('ACB', {character}), + permission: Permission.ChatOp, + params: [{type: ParamType.Character}] + }, + gunban: { + exec: (character: string) => core.connection.send('UNB', {character}), + permission: Permission.ChatOp, + params: [{type: ParamType.Character}] + }, + gtimeout: { + exec: (character: string, time: number, reason: string) => + core.connection.send('TMO', {character, time, reason}), + permission: Permission.ChatOp, + params: [{type: ParamType.Character, delimiter: ','}, {type: ParamType.Number, validator: (x) => x >= 1}, {type: ParamType.String}] + }, + setowner: { + exec: (conv: ChannelConversation, character: string) => + core.connection.send('CSO', {channel: conv.channel.id, character}), + permission: Permission.RoomOwner, + context: CommandContext.Channel, + params: [{type: ParamType.Character}] + }, + ignore: { + exec: (character: string) => core.connection.send('IGN', {action: 'add', character}), + params: [{type: ParamType.Character}] + }, + unignore: { + exec: (character: string) => core.connection.send('IGN', {action: 'delete', character}), + params: [{type: ParamType.Character}] + }, + ignorelist: { + exec: () => core.conversations.selectedConversation.infoText = l('chat.ignoreList', core.characters.ignoreList.join(', ')) + }, + makeroom: { + exec: (channel: string) => core.connection.send('CCR', {channel}), + params: [{type: ParamType.String}] + }, + gop: { + exec: (character: string) => core.connection.send('AOP', {character}), + permission: Permission.Admin, + params: [{type: ParamType.Character}] + }, + gdeop: { + exec: (character: string) => core.connection.send('DOP', {character}), + permission: Permission.Admin, + params: [{type: ParamType.Character}] + }, + op: { + exec: (conv: ChannelConversation, character: string) => + core.connection.send('COA', {channel: conv.channel.id, character}), + permission: Permission.RoomOwner, + context: CommandContext.Channel, + params: [{type: ParamType.Character}] + }, + deop: { + exec: (conv: ChannelConversation, character: string) => + core.connection.send('COR', {channel: conv.channel.id, character}), + permission: Permission.RoomOwner, + context: CommandContext.Channel, + params: [{type: ParamType.Character}] + }, + oplist: { + exec: (conv: ChannelConversation) => core.connection.send('COL', {channel: conv.channel.id}), + context: CommandContext.Channel + }, + invite: { + exec: (conv: ChannelConversation, character: string) => + core.connection.send('CIU', {channel: conv.channel.id, character}), + permission: Permission.RoomOp, + context: CommandContext.Channel, + params: [{type: ParamType.Character}] + }, + closeroom: { + exec: (conv: ChannelConversation) => { + core.connection.send('RST', {channel: conv.channel.id, status: 'private'}); + core.connection.send('ORS'); + }, + permission: Permission.RoomOwner, + context: CommandContext.Channel + }, + openroom: { + exec: (conv: ChannelConversation) => { + core.connection.send('RST', {channel: conv.channel.id, status: 'public'}); + core.connection.send('ORS'); + }, + permission: Permission.RoomOwner, + context: CommandContext.Channel + }, + setmode: { + exec: (conv: ChannelConversation, mode: 'ads' | 'chat' | 'both') => + core.connection.send('RMO', {channel: conv.channel.id, mode}), + permission: Permission.RoomOwner, + context: CommandContext.Channel, + params: [{type: ParamType.Enum, options: ['ads', 'chat', 'both']}] + }, + setdescription: { + exec: (conv: ChannelConversation, description: string) => + core.connection.send('CDS', {channel: conv.channel.id, description}), + permission: Permission.RoomOp, + context: CommandContext.Channel, + params: [{type: ParamType.String}] + }, + code: { + exec: (conv: ChannelConversation) => { + const active = document.activeElement; + const elm = document.createElement('textarea'); + elm.value = `[session=${conv.channel.name}]${conv.channel.id}[/session]`; + document.body.appendChild(elm); + elm.select(); + document.execCommand('copy'); + document.body.removeChild(elm); + active.focus(); + conv.infoText = l('commands.code.success'); + }, + permission: Permission.RoomOwner, + context: CommandContext.Channel + }, + killchannel: { + exec: (conv: ChannelConversation) => core.connection.send('KIC', {channel: conv.channel.id}), + permission: Permission.RoomOwner, + context: CommandContext.Channel + }, + createchannel: { + exec: (channel: string) => core.connection.send('CRC', {channel}), + permission: Permission.ChatOp, + params: [{type: ParamType.String}] + }, + broadcast: { + exec: (message: string) => core.connection.send('BRO', {message}), + permission: Permission.Admin, + params: [{type: ParamType.String}] + }, + reloadconfig: { + exec: (save?: 'save') => core.connection.send('RLD', save !== undefined ? {save} : undefined), + permission: Permission.Admin, + params: [{type: ParamType.Enum, options: ['save'], optional: true}] + }, + xyzzy: { + exec: (command: string, arg: string) => core.connection.send('ZZZ', {command, arg}), + permission: Permission.Admin, + params: [{type: ParamType.String, delimiter: ' '}, {type: ParamType.String}] + }, + elf: { + exec: () => core.conversations.selectedConversation.infoText = + 'Now no one can say there\'s "not enough Elf." It\'s a well-kept secret, but elves love headpets. You should try it sometime.', + documented: false + } +}; + +export default commands; \ No newline at end of file diff --git a/chat/user_view.ts b/chat/user_view.ts new file mode 100644 index 0000000..e78820c --- /dev/null +++ b/chat/user_view.ts @@ -0,0 +1,57 @@ +// TODO convert this to single-file once Vue supports it for functional components. +//template: +// {{character.name}} + +import Vue, {CreateElement, RenderContext, VNode} from 'vue'; +import {Channel, Character} from './interfaces'; + +export function getStatusIcon(status: Character.Status): string { + switch(status) { + case 'online': + return 'fa-user-o'; + case 'looking': + return 'fa-eye'; + case 'dnd': + return 'fa-minus-circle'; + case 'offline': + return 'fa-ban'; + case 'away': + return 'fa-circle-o'; + case 'busy': + return 'fa-cog'; + case 'idle': + return 'fa-hourglass'; + case 'crown': + return 'fa-birthday-cake'; + } +} + +//tslint:disable-next-line:variable-name +const UserView = Vue.extend({ + functional: true, + render(this: Vue, createElement: CreateElement, context?: RenderContext): VNode { + const props = <{character: Character, channel?: Channel, showStatus?: true}>( + /*tslint:disable-next-line:no-unsafe-any*///false positive + context !== undefined && context.props !== undefined ? context.props : this.$options.propsData); + const character = props.character; + let rankIcon; + if(character.isChatOp) rankIcon = 'fa-diamond'; + else if(props.channel !== undefined) { + const member = props.channel.members[character.name]; + if(member !== undefined) + rankIcon = member.rank === Channel.Rank.Owner ? 'fa-asterisk' : + member.rank === Channel.Rank.Op ? (props.channel.id.substr(0, 4) === 'adh-' ? 'fa-at' : 'fa-play') : ''; + else rankIcon = ''; + } else rankIcon = ''; + + const html = (props.showStatus !== undefined ? `` : '') + + (rankIcon !== '' ? `` : '') + character.name; + return createElement('span', { + attrs: {class: `user-view gender-${character.gender !== undefined ? character.gender.toLowerCase() : 'none'}`}, + domProps: {character, channel: props.channel, innerHTML: html} + }); + } +}); + +export default UserView; \ No newline at end of file diff --git a/chat/vue-raven.ts b/chat/vue-raven.ts new file mode 100644 index 0000000..6f7490e --- /dev/null +++ b/chat/vue-raven.ts @@ -0,0 +1,46 @@ +import {RavenStatic} from 'raven-js'; +import Vue from 'vue'; + +/*tslint:disable:no-unsafe-any no-any*///hack +function formatComponentName(vm: any): string { + if(vm.$root === vm) return ''; + const name = vm._isVue + ? vm.$options.name || vm.$options._componentTag + : vm.name; + return (name ? `component <${name}>` : 'anonymous component') + (vm._isVue && vm.$options.__file ? ` at ${vm.$options.__file}` : ''); +} +//tslint:enable + +/*tslint:disable:no-unbound-method strict-type-predicates*///hack +export default function VueRaven(this: void, raven: RavenStatic): RavenStatic { + if(typeof Vue.config !== 'object') return raven; + const oldOnError = Vue.config.errorHandler; + Vue.config.errorHandler = (error: Error, vm: Vue, info: string): void => { + raven.captureException(error, { + extra: { + componentName: formatComponentName(vm), + //propsData: vm.$options.propsData, + info + } + }); + + if(typeof oldOnError === 'function') oldOnError.call(this, error, vm); + else console.log(error); + }; + + const oldOnWarn = Vue.config.warnHandler; + Vue.config.warnHandler = (message: string, vm: Vue, trace: string): void => { + raven.captureMessage(message + trace, { + extra: { + componentName: formatComponentName(vm) + //propsData: vm.$options.propsData + } + }); + console.warn(`${message}: ${trace}`); + if(typeof oldOnWarn === 'function') + oldOnWarn.call(this, message, vm, trace); + }; + + return raven; +} +//tslint:enable \ No newline at end of file diff --git a/components/FilterableSelect.vue b/components/FilterableSelect.vue new file mode 100644 index 0000000..e0e1d6a --- /dev/null +++ b/components/FilterableSelect.vue @@ -0,0 +1,110 @@ + + + + + \ No newline at end of file diff --git a/components/Modal.vue b/components/Modal.vue new file mode 100644 index 0000000..db03a97 --- /dev/null +++ b/components/Modal.vue @@ -0,0 +1,114 @@ + + + + + \ No newline at end of file diff --git a/components/custom_dialog.ts b/components/custom_dialog.ts new file mode 100644 index 0000000..b19c8e2 --- /dev/null +++ b/components/custom_dialog.ts @@ -0,0 +1,12 @@ +import Vue from 'vue'; +import Modal from './Modal.vue'; + +export default class CustomDialog extends Vue { + show(): void { + (this.$children[0]).show(); + } + + hide(): void { + (this.$children[0]).hide(); + } +} \ No newline at end of file diff --git a/cordova/Index.vue b/cordova/Index.vue new file mode 100644 index 0000000..80301fa --- /dev/null +++ b/cordova/Index.vue @@ -0,0 +1,135 @@ + + + + + \ No newline at end of file diff --git a/cordova/chat.ts b/cordova/chat.ts new file mode 100644 index 0000000..1fc7960 --- /dev/null +++ b/cordova/chat.ts @@ -0,0 +1,61 @@ +/** + * @license + * MIT License + * + * Copyright (c) 2017 F-List + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + * This license header applies to this file and all of the non-third-party assets it includes. + * @file The entry point for the Cordova version of F-Chat 3.0. + * @copyright 2017 F-List + * @author Maya Wolf + * @version 3.0 + * @see {@link https://github.com/f-list/exported|GitHub repo} + */ +import 'bootstrap/js/dropdown.js'; +import 'bootstrap/js/modal.js'; +import * as Raven from 'raven-js'; +import Vue from 'vue'; +import VueRaven from '../chat/vue-raven'; +import {init as fsInit} from './filesystem'; +import Index from './Index.vue'; + +if(process.env.NODE_ENV === 'production') { + Raven.config('https://af3e6032460e418cb794b1799e536f37@sentry.newtsin.space/2', { + release: `android-${require('./package.json').version}`, //tslint:disable-line:no-require-imports no-unsafe-any + dataCallback: (data: {culprit: string, exception: {values: {stacktrace: {frames: {filename: string}[]}}[]}}) => { + data.culprit = `~${data.culprit.substr(data.culprit.lastIndexOf('/'))}`; + for(const ex of data.exception.values) + for(const frame of ex.stacktrace.frames) { + const index = frame.filename.lastIndexOf('/'); + frame.filename = index !== -1 ? `~${frame.filename.substr(index)}` : frame.filename; + } + } + }).addPlugin(VueRaven, Vue).install(); + (window).onunhandledrejection = (e: PromiseRejectionEvent) => { + Raven.captureException(e.reason); + }; +} + +fsInit().then(() => { //tslint:disable-line:no-floating-promises + new Index({ //tslint:disable-line:no-unused-expression + el: '#app' + }); +}); \ No newline at end of file diff --git a/cordova/config.xml b/cordova/config.xml new file mode 100644 index 0000000..6a141d5 --- /dev/null +++ b/cordova/config.xml @@ -0,0 +1,27 @@ + + + F-Chat 3.0 + + A cross-platform F-Chat client. + + The F-list Team + + + + + + + + + + + + + + + + + + + + diff --git a/cordova/filesystem.ts b/cordova/filesystem.ts new file mode 100644 index 0000000..a2af6c8 --- /dev/null +++ b/cordova/filesystem.ts @@ -0,0 +1,262 @@ +import {getByteLength, Message as MessageImpl} from '../chat/common'; +import core from '../chat/core'; +import {Conversation, Logs as Logging, Settings} from '../chat/interfaces'; + +declare global { + class TextEncoder { + readonly encoding: string; + + encode(input?: string, options?: {stream: boolean}): Uint8Array; + } + + class TextDecoder { + readonly encoding: string; + readonly fatal: boolean; + readonly ignoreBOM: boolean; + + constructor(utfLabel?: string, options?: {fatal?: boolean, ignoreBOM?: boolean}) + + decode(input?: ArrayBufferView, options?: {stream: boolean}): string; + } +} + +const dayMs = 86400000; +let fs: FileSystem; + +export class GeneralSettings { + account = ''; + password = ''; + host = 'wss://chat.f-list.net:9799'; + theme = 'dark'; +} + +type Index = {[key: string]: {name: string, index: {[key: number]: number | undefined}} | undefined}; + +/*tslint:disable:promise-function-async*///all of these are simple wrappers +export function init(): Promise { + return new Promise((resolve, reject) => { + document.addEventListener('deviceready', () => { + window.requestFileSystem(LocalFileSystem.PERSISTENT, 0, (f) => { + fs = f; + resolve(); + }, reject); + }); + }); +} + +function readAsString(file: Blob): Promise { + return new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.onloadend = () => resolve(reader.result); + reader.onerror = reject; + reader.readAsText(file); + }); +} + +function readAsArrayBuffer(file: Blob): Promise { + return new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.onloadend = () => resolve(reader.result); + reader.onerror = reject; + reader.readAsArrayBuffer(file); + }); +} + +function getFile(root: DirectoryEntry, path: string): Promise { + return new Promise((resolve, reject) => { + root.getFile(path, {create: false}, (entry) => entry.file((file) => { + resolve(file); + }, reject), (e) => { + if(e.code === FileError.NOT_FOUND_ERR) resolve(undefined); + else reject(e); + }); + }); +} + +function getWriter(root: DirectoryEntry, path: string): Promise { + return new Promise((resolve, reject) => root.getFile(path, {create: true}, + (file) => file.createWriter(resolve, reject), reject)); +} + +function getDir(root: DirectoryEntry, name: string): Promise { + return new Promise((resolve, reject) => root.getDirectory(name, {create: true}, resolve, reject)); +} + +function getEntries(root: DirectoryEntry): Promise> { + const reader = root.createReader(); + return new Promise>((resolve, reject) => reader.readEntries(resolve, reject)); +} + +//tslib:enable + +function serializeMessage(message: Conversation.Message): Blob { + const name = message.type !== Conversation.Message.Type.Event ? message.sender.name : ''; + const buffer = new ArrayBuffer(8); + const dv = new DataView(buffer); + dv.setUint32(0, message.time.getTime() / 1000); + dv.setUint8(4, message.type); + const senderLength = getByteLength(name); + dv.setUint8(5, senderLength); + const textLength = getByteLength(message.text); + dv.setUint16(6, textLength); + return new Blob([buffer, name, message.text, String.fromCharCode(senderLength + textLength + 10)]); +} + +function deserializeMessage(buffer: ArrayBuffer): {message: Conversation.Message, end: number} { + const dv = new DataView(buffer, 0, 8); + const time = dv.getUint32(0); + const type = dv.getUint8(4); + const senderLength = dv.getUint8(5); + const messageLength = dv.getUint16(6); + let index = 8; + const sender = decoder.decode(new DataView(buffer, index, senderLength)); + index += senderLength; + const text = decoder.decode(new DataView(buffer, index, messageLength)); + return {message: new MessageImpl(type, core.characters.get(sender), text, new Date(time)), end: index + messageLength + 2}; +} + +const decoder = new TextDecoder('utf8'); + +export class Logs implements Logging.Persistent { + private index: Index = {}; + private logDir: DirectoryEntry; + + constructor() { + core.connection.onEvent('connecting', async() => { + this.index = {}; + const charDir = await getDir(fs.root, core.connection.character); + this.logDir = await getDir(charDir, 'logs'); + const entries = await getEntries(this.logDir); + for(const entry of entries) + if(entry.name.substr(-4) === '.idx') { + const file = await new Promise((s, j) => (entry).file(s, j)); + const buffer = await readAsArrayBuffer(file); + const dv = new DataView(buffer); + let offset = dv.getUint8(0); + const name = decoder.decode(new DataView(buffer, 1, offset++)); + const index: {[key: number]: number} = {}; + for(; offset < dv.byteLength; offset += 7) { + const key = dv.getUint16(offset); + index[key] = dv.getUint32(offset + 2) << 8 | dv.getUint8(offset + 6); + } + this.index[entry.name.slice(0, -4).toLowerCase()] = {name, index}; + } + }); + } + + async logMessage(conversation: Conversation, message: Conversation.Message): Promise { + return new Promise((resolve, reject) => { + this.logDir.getFile(conversation.key, {create: true}, (file) => { + const serialized = serializeMessage(message); + const date = Math.floor(message.time.getTime() / dayMs); + let indexBuffer: {}[] | undefined; + let index = this.index[conversation.key]; + if(index !== undefined) { + if(index.index[date] === undefined) indexBuffer = []; + } else { + index = this.index[conversation.key] = {name: conversation.name, index: {}}; + const nameLength = getByteLength(conversation.name); + indexBuffer = [String.fromCharCode(nameLength), conversation.name]; + } + if(indexBuffer !== undefined) + file.getMetadata((data) => { + index!.index[date] = data.size; + const dv = new DataView(new ArrayBuffer(7)); + dv.setUint16(0, date); + dv.setUint32(2, data.size >> 8); + dv.setUint8(6, data.size % 256); + indexBuffer!.push(dv); + this.logDir.getFile(`${conversation.key}.idx`, {create: true}, (indexFile) => { + indexFile.createWriter((writer) => writer.write(new Blob(indexBuffer)), reject); + }, reject); + }, reject); + file.createWriter((writer) => writer.write(serialized), reject); + resolve(); + }, reject); + }); + } + + async getBacklog(conversation: Conversation): Promise { + const file = await getFile(this.logDir, conversation.key); + if(file === undefined) return []; + let count = 20; + let messages = new Array(count); + let pos = file.size; + while(pos > 0 && count > 0) { + const length = new DataView(await readAsArrayBuffer(file)).getUint16(0); + pos = pos - length - 2; + messages[--count] = deserializeMessage(await readAsArrayBuffer(file.slice(pos, pos + length))).message; + } + if(count !== 0) messages = messages.slice(count); + return messages; + } + + async getLogs(key: string, date: Date): Promise { + const file = await getFile(this.logDir, key); + if(file === undefined) return []; + const messages: Conversation.Message[] = []; + const day = date.getTime() / dayMs; + const index = this.index[key]; + if(index === undefined) return []; + let pos = index.index[date.getTime() / dayMs]; + if(pos === undefined) return []; + while(pos < file.size) { + const deserialized = deserializeMessage(await readAsArrayBuffer(file.slice(pos, pos + 51000))); + if(Math.floor(deserialized.message.time.getTime() / dayMs) !== day) break; + messages.push(deserialized.message); + pos += deserialized.end; + } + return messages; + } + + getLogDates(key: string): ReadonlyArray { + const entry = this.index[key]; + if(entry === undefined) return []; + const dates = []; + for(const date in entry.index) //tslint:disable-line:forin + dates.push(new Date(parseInt(date, 10) * dayMs)); + return dates; + } + + get conversations(): ReadonlyArray<{id: string, name: string}> { + const conversations: {id: string, name: string}[] = []; + for(const key in this.index) conversations.push({id: key, name: this.index[key]!.name}); + conversations.sort((x, y) => (x.name < y.name ? -1 : (x.name > y.name ? 1 : 0))); + return conversations; + } +} + +export async function getGeneralSettings(): Promise { + const file = await getFile(fs.root, 'settings'); + if(file === undefined) return undefined; + return JSON.parse(await readAsString(file)); +} + +export async function setGeneralSettings(value: GeneralSettings): Promise { + const writer = await getWriter(fs.root, 'settings'); + writer.write(new Blob([JSON.stringify(value)])); +} + +async function getSettingsDir(character: string = core.connection.character): Promise { + return new Promise((resolve, reject) => { + fs.root.getDirectory(character, {create: true}, resolve, reject); + }); +} + +export class SettingsStore implements Settings.Store { + async get(key: K, character?: string): Promise { + const dir = await getSettingsDir(character); + const file = await getFile(dir, key); + if(file === undefined) return undefined; + return JSON.parse(await readAsString(file)); + } + + async set(key: K, value: Settings.Keys[K]): Promise { + const writer = await getWriter(await getSettingsDir(), key); + writer.write(new Blob([JSON.stringify(value)])); + } + + async getAvailableCharacters(): Promise { + return (await getEntries(fs.root)).filter((x) => x.isDirectory).map((x) => x.name); + } +} \ No newline at end of file diff --git a/cordova/index.html b/cordova/index.html new file mode 100644 index 0000000..b756e2a --- /dev/null +++ b/cordova/index.html @@ -0,0 +1,14 @@ + + + + + + FChat 3.0 + + +
+
+ + + + \ No newline at end of file diff --git a/cordova/notifications.ts b/cordova/notifications.ts new file mode 100644 index 0000000..4fc404a --- /dev/null +++ b/cordova/notifications.ts @@ -0,0 +1,66 @@ +import core from '../chat/core'; +import {Conversation} from '../chat/interfaces'; +import BaseNotifications from '../chat/notifications'; //tslint:disable-line:match-default-export-name + +//tslint:disable +declare global { + interface Options { + id?: number + title?: string + text?: string + every?: string + at?: Date | null + badge?: number + sound?: string + data?: any + icon?: string + smallIcon?: string + ongoing?: boolean + led?: string + } + + interface CordovaPlugins { + notification: { + local: { + getDefaults(): Options + setDefaults(options: Options): void + schedule(notification: Options, callback?: Function, scope?: Object, args?: {skipPermissions: boolean}): void + update(notification: Options, callback?: Function, scope?: Object, args?: {skipPermissions: boolean}): void + clear(ids: string, callback?: Function, scope?: Object): void + clearAll(callback?: Function, scope?: Object): void + cancel(ids: string, callback?: Function, scope?: Object): void + cancelAll(callback?: Function, scope?: Object): void + isPresent(id: string, callback?: Function, scope?: Object): void + isTriggered(id: string, callback?: Function, scope?: Object): void + getAllIds(callback?: Function, scope?: Object): void + getScheduledIds(callback?: Function, scope?: Object): void + getTriggeredIds(callback?: Function, scope?: Object): void + get(ids?: number[], callback?: Function, scope?: Object): void + getScheduled(ids?: number[], callback?: Function, scope?: Object): void + getTriggered(ids?: number[], callback?: Function, scope?: Object): void + hasPermission(callback?: Function, scope?: Object): void + registerPermission(callback?: Function, scope?: Object): void + on(event: 'schedule' | 'update' | 'click' | 'trigger', handler: (notification: Options) => void): void + un(event: 'schedule' | 'update' | 'click' | 'trigger', handler: (notification: Options) => void): void + } + } + } +} +//tslint:enable +document.addEventListener('deviceready', () => { + cordova.plugins.notification.local.on('click', (notification) => { + const conv = core.conversations.byKey((<{conversation: string}>notification.data).conversation); + if(conv !== undefined) conv.show(); + }); +}); + +export default class Notifications extends BaseNotifications { + notify(conversation: Conversation, title: string, body: string, icon: string, sound: string): void { + if(!this.isInBackground && conversation === core.conversations.selectedConversation && !core.state.settings.alwaysNotify) return; + this.playSound(sound); + if(core.state.settings.notifications) + cordova.plugins.notification.local.schedule({ + title, text: body, sound, icon, smallIcon: icon, data: {conversation: conversation.key} + }); + } +} \ No newline at end of file diff --git a/cordova/package.json b/cordova/package.json new file mode 100644 index 0000000..832a1a6 --- /dev/null +++ b/cordova/package.json @@ -0,0 +1,36 @@ +{ + "name": "fchat", + "version": "0.1.0", + "author": "The F-List Team", + "description": "F-List.net Chat Client", + "main": "main.js", + "license": "MIT", + "cordova": { + "plugins": { + "cordova-plugin-whitelist": {}, + "cordova-plugin-file": {}, + "de.appplant.cordova.plugin.local-notification": {} + }, + "platforms": [ + "android" + ] + }, + "scripts": { + "build": "../node_modules/.bin/webpack", + "build:dist": "../node_modules/.bin/webpack --env production", + "watch": "../node_modules/.bin/webpack --watch" + }, + "dependencies": { + "cordova-android": "^6.2.3", + "cordova-plugin-app-event": "^1.2.1", + "cordova-plugin-compat": "^1.0.0", + "cordova-plugin-device": "^1.1.6", + "cordova-plugin-file": "^4.3.3", + "cordova-plugin-whitelist": "^1.3.2", + "de.appplant.cordova.plugin.local-notification": "^0.8.5" + }, + "devDependencies": { + "@types/cordova": "^0.0.34", + "qs": "^6.5.0" + } +} \ No newline at end of file diff --git a/cordova/tsconfig.json b/cordova/tsconfig.json new file mode 100644 index 0000000..0d69c38 --- /dev/null +++ b/cordova/tsconfig.json @@ -0,0 +1,29 @@ +{ + "compilerOptions": { + "target": "es5", + "lib": [ + "dom", + "es5", + "scripthost", + "es2015.iterable", + "es2015.promise" + ], + "allowSyntheticDefaultImports": true, + "module": "commonjs", + "sourceMap": true, + "experimentalDecorators": true, + "allowJs": true, + "outDir": "build", + "noEmitHelpers": true, + "importHelpers": true, + "forceConsistentCasingInFileNames": true, + "strict": true, + "noImplicitReturns": true, + "noUnusedLocals": true, + "noUnusedParameters": true + }, + "include": ["*.ts", "../**/*.d.ts"], + "exclude": [ + "node_modules" + ] +} \ No newline at end of file diff --git a/cordova/webpack.config.js b/cordova/webpack.config.js new file mode 100644 index 0000000..5ad465f --- /dev/null +++ b/cordova/webpack.config.js @@ -0,0 +1,73 @@ +const path = require('path'); +const webpack = require('webpack'); +const UglifyPlugin = require('uglifyjs-webpack-plugin'); +const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin'); +const exportLoader = require('../export-loader'); + +const config = { + entry: { + chat: [__dirname + '/chat.ts', __dirname + '/index.html'] + }, + output: { + path: __dirname + '/www', + filename: '[name].js' + }, + context: __dirname, + module: { + loaders: [ + { + test: /\.ts$/, + loader: 'ts-loader', + options: { + appendTsSuffixTo: [/\.vue$/], + configFile: __dirname + '/tsconfig.json', + transpileOnly: true + } + }, + { + test: /\.vue$/, + loader: 'vue-loader', + options: { + preLoaders: {ts: 'export-loader'}, + preserveWhitespace: false + } + }, + {test: /\.eot(\?v=\d+\.\d+\.\d+)?$/, loader: 'file-loader'}, + {test: /\.(woff|woff2)$/, loader: 'url-loader?prefix=font/&limit=5000'}, + {test: /\.ttf(\?v=\d+\.\d+\.\d+)?$/, loader: 'url-loader?limit=10000&mimetype=application/octet-stream'}, + {test: /\.svg(\?v=\d+\.\d+\.\d+)?$/, loader: 'url-loader?limit=10000&mimetype=image/svg+xml'}, + {test: /\.(wav|mp3|ogg)$/, loader: 'file-loader?name=sounds/[name].[ext]'}, + {test: /\.(png|html)$/, loader: 'file-loader?name=[name].[ext]'}, + {test: /\.less/, use: ['css-loader', 'less-loader']} + ] + }, + plugins: [ + new webpack.ProvidePlugin({ + '$': 'jquery/dist/jquery.slim.js', + 'jQuery': 'jquery/dist/jquery.slim.js', + 'window.jQuery': 'jquery/dist/jquery.slim.js' + }), + new ForkTsCheckerWebpackPlugin({workers: 2, async: false, tslint: path.join(__dirname, '../tslint.json')}), + exportLoader.delayTypecheck + ], + resolve: { + 'extensions': ['.ts', '.js', '.vue', '.css'] + }, + resolveLoader: { + modules: [ + 'node_modules', path.join(__dirname, '../') + ] + } +}; + +module.exports = function(env) { + const dist = env === 'production'; + config.plugins.push(new webpack.DefinePlugin({ + 'process.env.NODE_ENV': JSON.stringify(dist ? 'production' : 'development') + })); + if(dist) { + config.devtool = 'source-map'; + config.plugins.push(new UglifyPlugin({sourceMap: true})); + } + return config; +}; \ No newline at end of file diff --git a/cordova/yarn.lock b/cordova/yarn.lock new file mode 100644 index 0000000..85b5a16 --- /dev/null +++ b/cordova/yarn.lock @@ -0,0 +1,236 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@types/cordova@^0.0.34": + version "0.0.34" + resolved "https://registry.yarnpkg.com/@types/cordova/-/cordova-0.0.34.tgz#ea7addf74ecec3d7629827a0c39e2c9addc73d04" + +abbrev@1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8" + +android-versions@^1.2.0: + version "1.2.1" + resolved "https://registry.yarnpkg.com/android-versions/-/android-versions-1.2.1.tgz#3f50baf693e73a512c3c5403542291cead900063" + +ansi@^0.3.1: + version "0.3.1" + resolved "https://registry.yarnpkg.com/ansi/-/ansi-0.3.1.tgz#0c42d4fb17160d5a9af1e484bace1c66922c1b21" + +balanced-match@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" + +base64-js@0.0.8: + version "0.0.8" + resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-0.0.8.tgz#1101e9544f4a76b1bc3b26d452ca96d7a35e7978" + +big-integer@^1.6.7: + version "1.6.25" + resolved "https://registry.yarnpkg.com/big-integer/-/big-integer-1.6.25.tgz#1de45a9f57542ac20121c682f8d642220a34e823" + +bplist-parser@^0.1.0: + version "0.1.1" + resolved "https://registry.yarnpkg.com/bplist-parser/-/bplist-parser-0.1.1.tgz#d60d5dcc20cba6dc7e1f299b35d3e1f95dafbae6" + dependencies: + big-integer "^1.6.7" + +brace-expansion@^1.1.7: + version "1.1.8" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.8.tgz#c07b211c7c952ec1f8efd51a77ef0d1d3990a292" + dependencies: + balanced-match "^1.0.0" + concat-map "0.0.1" + +concat-map@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" + +cordova-android@^6.2.3: + version "6.3.0" + resolved "https://registry.yarnpkg.com/cordova-android/-/cordova-android-6.3.0.tgz#da5418433d25c75a5977b428244bbe437d0128d2" + dependencies: + android-versions "^1.2.0" + cordova-common "^2.1.0" + elementtree "0.1.6" + nopt "^3.0.1" + properties-parser "^0.2.3" + q "^1.4.1" + shelljs "^0.5.3" + +cordova-common@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/cordova-common/-/cordova-common-2.1.0.tgz#bb357ee1b9825031ed9db3c56b592efe973d1640" + dependencies: + ansi "^0.3.1" + bplist-parser "^0.1.0" + cordova-registry-mapper "^1.1.8" + elementtree "0.1.6" + glob "^5.0.13" + minimatch "^3.0.0" + osenv "^0.1.3" + plist "^1.2.0" + q "^1.4.1" + semver "^5.0.1" + shelljs "^0.5.3" + underscore "^1.8.3" + unorm "^1.3.3" + +cordova-plugin-app-event@>=1.1.0, cordova-plugin-app-event@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/cordova-plugin-app-event/-/cordova-plugin-app-event-1.2.1.tgz#0eebb14132aa43bb2e5c081a9abdbd97ca2d8132" + +cordova-plugin-compat@^1.0.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/cordova-plugin-compat/-/cordova-plugin-compat-1.2.0.tgz#0bc65757276ebd920c012ce920e274177576373e" + +cordova-plugin-device@*, cordova-plugin-device@^1.1.6: + version "1.1.6" + resolved "https://registry.yarnpkg.com/cordova-plugin-device/-/cordova-plugin-device-1.1.6.tgz#2d21764cad7c9b801523e4e09a30e024b249334b" + +cordova-plugin-file@^4.3.3: + version "4.3.3" + resolved "https://registry.yarnpkg.com/cordova-plugin-file/-/cordova-plugin-file-4.3.3.tgz#012e97aa1afb91f84916e6341b548366d23de9b9" + +cordova-plugin-whitelist@^1.3.2: + version "1.3.2" + resolved "https://registry.yarnpkg.com/cordova-plugin-whitelist/-/cordova-plugin-whitelist-1.3.2.tgz#5b6335feb9f5301f3c013b9096cb8885bdbd5076" + +cordova-registry-mapper@^1.1.8: + version "1.1.15" + resolved "https://registry.yarnpkg.com/cordova-registry-mapper/-/cordova-registry-mapper-1.1.15.tgz#e244b9185b8175473bff6079324905115f83dc7c" + +de.appplant.cordova.plugin.local-notification@^0.8.5: + version "0.8.5" + resolved "https://registry.yarnpkg.com/de.appplant.cordova.plugin.local-notification/-/de.appplant.cordova.plugin.local-notification-0.8.5.tgz#e0c6a86ea52ac4f41dba67521d91a58a9a42a3bd" + dependencies: + cordova-plugin-app-event ">=1.1.0" + cordova-plugin-device "*" + +elementtree@0.1.6: + version "0.1.6" + resolved "https://registry.yarnpkg.com/elementtree/-/elementtree-0.1.6.tgz#2ac4c46ea30516c8c4cbdb5e3ac7418e592de20c" + dependencies: + sax "0.3.5" + +glob@^5.0.13: + version "5.0.15" + resolved "https://registry.yarnpkg.com/glob/-/glob-5.0.15.tgz#1bc936b9e02f4a603fcc222ecf7633d30b8b93b1" + dependencies: + inflight "^1.0.4" + inherits "2" + minimatch "2 || 3" + once "^1.3.0" + path-is-absolute "^1.0.0" + +inflight@^1.0.4: + version "1.0.6" + resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" + dependencies: + once "^1.3.0" + wrappy "1" + +inherits@2: + version "2.0.3" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" + +lodash@^3.5.0: + version "3.10.1" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-3.10.1.tgz#5bf45e8e49ba4189e17d482789dfd15bd140b7b6" + +"minimatch@2 || 3", minimatch@^3.0.0: + version "3.0.4" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" + dependencies: + brace-expansion "^1.1.7" + +nopt@^3.0.1: + version "3.0.6" + resolved "https://registry.yarnpkg.com/nopt/-/nopt-3.0.6.tgz#c6465dbf08abcd4db359317f79ac68a646b28ff9" + dependencies: + abbrev "1" + +once@^1.3.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" + dependencies: + wrappy "1" + +os-homedir@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/os-homedir/-/os-homedir-1.0.2.tgz#ffbc4988336e0e833de0c168c7ef152121aa7fb3" + +os-tmpdir@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274" + +osenv@^0.1.3: + version "0.1.4" + resolved "https://registry.yarnpkg.com/osenv/-/osenv-0.1.4.tgz#42fe6d5953df06c8064be6f176c3d05aaaa34644" + dependencies: + os-homedir "^1.0.0" + os-tmpdir "^1.0.0" + +path-is-absolute@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" + +plist@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/plist/-/plist-1.2.0.tgz#084b5093ddc92506e259f874b8d9b1afb8c79593" + dependencies: + base64-js "0.0.8" + util-deprecate "1.0.2" + xmlbuilder "4.0.0" + xmldom "0.1.x" + +properties-parser@^0.2.3: + version "0.2.3" + resolved "https://registry.yarnpkg.com/properties-parser/-/properties-parser-0.2.3.tgz#f7591255f707abbff227c7b56b637dbb0373a10f" + +q@^1.4.1: + version "1.5.0" + resolved "https://registry.yarnpkg.com/q/-/q-1.5.0.tgz#dd01bac9d06d30e6f219aecb8253ee9ebdc308f1" + +qs@^6.5.0: + version "6.5.1" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.1.tgz#349cdf6eef89ec45c12d7d5eb3fc0c870343a6d8" + +sax@0.3.5: + version "0.3.5" + resolved "https://registry.yarnpkg.com/sax/-/sax-0.3.5.tgz#88fcfc1f73c0c8bbd5b7c776b6d3f3501eed073d" + +semver@^5.0.1: + version "5.4.1" + resolved "https://registry.yarnpkg.com/semver/-/semver-5.4.1.tgz#e059c09d8571f0540823733433505d3a2f00b18e" + +shelljs@^0.5.3: + version "0.5.3" + resolved "https://registry.yarnpkg.com/shelljs/-/shelljs-0.5.3.tgz#c54982b996c76ef0c1e6b59fbdc5825f5b713113" + +underscore@^1.8.3: + version "1.8.3" + resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.8.3.tgz#4f3fb53b106e6097fcf9cb4109f2a5e9bdfa5022" + +unorm@^1.3.3: + version "1.4.1" + resolved "https://registry.yarnpkg.com/unorm/-/unorm-1.4.1.tgz#364200d5f13646ca8bcd44490271335614792300" + +util-deprecate@1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" + +wrappy@1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" + +xmlbuilder@4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/xmlbuilder/-/xmlbuilder-4.0.0.tgz#98b8f651ca30aa624036f127d11cc66dc7b907a3" + dependencies: + lodash "^3.5.0" + +xmldom@0.1.x: + version "0.1.27" + resolved "https://registry.yarnpkg.com/xmldom/-/xmldom-0.1.27.tgz#d501f97b3bdb403af8ef9ecc20573187aadac0e9" diff --git a/electron/Index.vue b/electron/Index.vue new file mode 100644 index 0000000..369ee8d --- /dev/null +++ b/electron/Index.vue @@ -0,0 +1,352 @@ + + + + + \ No newline at end of file diff --git a/electron/application.json b/electron/application.json new file mode 100644 index 0000000..819265e --- /dev/null +++ b/electron/application.json @@ -0,0 +1,15 @@ +{ + "name": "fchat", + "version": "0.1.29", + "author": "The F-List Team", + "description": "F-List.net Chat Client", + "main": "main.js", + "license": "MIT", + "devDependencies": { + "electron": "^1.8.0" + }, + "dependencies": { + "keytar": "^4.0.4", + "spellchecker": "^3.4.3" + } +} diff --git a/electron/build/icon.icns b/electron/build/icon.icns new file mode 100644 index 0000000..35bb384 Binary files /dev/null and b/electron/build/icon.icns differ diff --git a/electron/build/icon.ico b/electron/build/icon.ico new file mode 100644 index 0000000..5cbf9da Binary files /dev/null and b/electron/build/icon.ico differ diff --git a/electron/build/icon.png b/electron/build/icon.png new file mode 100644 index 0000000..9cfae3a Binary files /dev/null and b/electron/build/icon.png differ diff --git a/electron/chat.ts b/electron/chat.ts new file mode 100644 index 0000000..7f2a1d2 --- /dev/null +++ b/electron/chat.ts @@ -0,0 +1,67 @@ +/** + * @license + * MIT License + * + * Copyright (c) 2017 F-List + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + * This license header applies to this file and all of the non-third-party assets it includes. + * @file The entry point for the Electron renderer of F-Chat 3.0. + * @copyright 2017 F-List + * @author Maya Wolf + * @version 3.0 + * @see {@link https://github.com/f-list/exported|GitHub repo} + */ +import 'bootstrap/js/dropdown.js'; +import 'bootstrap/js/modal.js'; +import * as electron from 'electron'; +import * as Raven from 'raven-js'; +import Vue from 'vue'; +import VueRaven from '../chat/vue-raven'; +import Index from './Index.vue'; + +if(process.env.NODE_ENV === 'production') { + Raven.config('https://af3e6032460e418cb794b1799e536f37@sentry.newtsin.space/2', { + release: electron.remote.app.getVersion(), + dataCallback(data: {culprit: string, exception: {values: {stacktrace: {frames: {filename: string}[]}}[]}}): void { + data.culprit = `~${data.culprit.substr(data.culprit.lastIndexOf('/'))}`; + for(const ex of data.exception.values) + for(const frame of ex.stacktrace.frames) { + const index = frame.filename.lastIndexOf('/'); + frame.filename = index !== -1 ? `~${frame.filename.substr(index)}` : frame.filename; + } + } + }).addPlugin(VueRaven, Vue).install(); + (window).onunhandledrejection = (e: PromiseRejectionEvent) => { + Raven.captureException(e.reason); + }; +} + +//tslint:disable-next-line:no-unused-expression +new Index({ + el: '#app' +}); + +electron.ipcRenderer.on('focus', (_: Event, message: boolean) => message ? window.focus() : window.blur()); + +document.addEventListener('keydown', (e: KeyboardEvent) => { + if(e.which === 123) + electron.remote.getCurrentWebContents().toggleDevTools(); +}); \ No newline at end of file diff --git a/electron/common.ts b/electron/common.ts new file mode 100644 index 0000000..e3f287f --- /dev/null +++ b/electron/common.ts @@ -0,0 +1,31 @@ +import * as fs from 'fs'; +import * as path from 'path'; + +export function mkdir(dir: string): void { + try { + fs.mkdirSync(dir); + } catch(e) { + if(!(e instanceof Error)) throw e; + switch((e).code) { + case 'ENOENT': + mkdir(path.dirname(dir)); + mkdir(dir); + break; + default: + try { + const stat = fs.statSync(dir); + if(stat.isDirectory()) return; + } catch(e) { + console.log(e); + } + throw e; + } + } +} + +//tslint:disable +const Module = require('module'); +export function nativeRequire(module: string): T { + return Module.prototype.require.call({paths: Module._nodeModulePaths(__dirname)}, module); +} +//tslint:enable \ No newline at end of file diff --git a/electron/filesystem.ts b/electron/filesystem.ts new file mode 100644 index 0000000..1712590 --- /dev/null +++ b/electron/filesystem.ts @@ -0,0 +1,233 @@ +import * as electron from 'electron'; +import * as fs from 'fs'; +import * as path from 'path'; +import {promisify} from 'util'; +import {Message as MessageImpl} from '../chat/common'; +import core from '../chat/core'; +import {Conversation, Logs as Logging, Settings} from '../chat/interfaces'; +import {mkdir} from './common'; + +const dayMs = 86400000; +const baseDir = path.join(electron.remote.app.getPath('userData'), 'data'); +mkdir(baseDir); +const readFile = promisify(fs.readFile); +const writeFile = promisify(fs.writeFile); +const readdir = promisify(fs.readdir); +const open = promisify(fs.open); +const fstat = promisify(fs.fstat); +const read = promisify(fs.read); + +const noAssert = process.env.NODE_ENV === 'production'; + +export class GeneralSettings { + account = ''; + closeToTray = true; + host = 'wss://chat.f-list.net:9799'; + spellcheckLang: string | undefined = 'en-GB'; + theme = 'default'; +} + +export type Message = Conversation.EventMessage | { + readonly sender: {readonly name: string} + readonly text: string + readonly time: Date + readonly type: Conversation.Message.Type +}; + +interface IndexItem { + index: {[key: number]: number | undefined} + name: string + offsets: number[] +} + +interface Index { + [key: string]: IndexItem | undefined +} + +export function getLogDir(this: void, character: string = core.connection.character): string { + const dir = path.join(baseDir, character, 'logs'); + mkdir(dir); + return dir; +} + +function getLogFile(this: void, key: string): string { + return path.join(getLogDir(), key); +} + +export function checkIndex(this: void, index: Index, message: Message, key: string, name: string, + size: number | (() => number)): Buffer | undefined { + const date = Math.floor(message.time.getTime() / dayMs - message.time.getTimezoneOffset() / 1440); + let buffer: Buffer, offset = 0; + let item = index[key]; + if(item !== undefined) { + if(item.index[date] !== undefined) return; + buffer = Buffer.allocUnsafe(7); + } else { + index[key] = item = {name, index: {}, offsets: []}; + const nameLength = Buffer.byteLength(name); + buffer = Buffer.allocUnsafe(nameLength + 8); + buffer.writeUInt8(nameLength, 0, noAssert); + buffer.write(name, 1); + offset = nameLength + 1; + } + const newValue = typeof size === 'function' ? size() : size; + item.index[date] = item.offsets.length; + item.offsets.push(newValue); + buffer.writeUInt16LE(date, offset, noAssert); + buffer.writeUIntLE(newValue, offset + 2, 5, noAssert); + return buffer; +} + +export function serializeMessage(message: Message): {serialized: Buffer, size: number} { + const name = message.type !== Conversation.Message.Type.Event ? message.sender.name : ''; + const senderLength = Buffer.byteLength(name); + const messageLength = Buffer.byteLength(message.text); + const buffer = Buffer.allocUnsafe(senderLength + messageLength + 10); + buffer.writeUInt32LE(message.time.getTime() / 1000, 0, noAssert); + buffer.writeUInt8(message.type, 4, noAssert); + buffer.writeUInt8(senderLength, 5, noAssert); + buffer.write(name, 6); + let offset = senderLength + 6; + buffer.writeUInt16LE(messageLength, offset, noAssert); + buffer.write(message.text, offset += 2); + buffer.writeUInt16LE(offset += messageLength, offset, noAssert); + return {serialized: buffer, size: offset + 2}; +} + +function deserializeMessage(buffer: Buffer): {end: number, message: Conversation.Message} { + const time = buffer.readUInt32LE(0, noAssert); + const type = buffer.readUInt8(4, noAssert); + const senderLength = buffer.readUInt8(5, noAssert); + let offset = senderLength + 6; + const sender = buffer.toString('utf8', 6, offset); + const messageLength = buffer.readUInt16LE(offset, noAssert); + offset += 2; + const text = buffer.toString('utf8', offset, offset += messageLength); + const message = new MessageImpl(type, core.characters.get(sender), text, new Date(time * 1000)); + return {message, end: offset + 2}; +} + +export class Logs implements Logging.Persistent { + private index: Index = {}; + + constructor() { + core.connection.onEvent('connecting', () => { + this.index = {}; + const dir = getLogDir(); + const files = fs.readdirSync(dir); + for(const file of files) + if(file.substr(-4) === '.idx') { + const content = fs.readFileSync(path.join(dir, file)); + let offset = content.readUInt8(0, noAssert) + 1; + const item: IndexItem = { + name: content.toString('utf8', 1, offset), + index: {}, + offsets: new Array(content.length - offset) + }; + for(; offset < content.length; offset += 7) { + const key = content.readUInt16LE(offset); + item.index[key] = item.offsets.length; + item.offsets.push(content.readUIntLE(offset + 2, 5)); + } + this.index[file.slice(0, -4).toLowerCase()] = item; + } + }); + } + + async getBacklog(conversation: Conversation): Promise> { + const file = getLogFile(conversation.key); + if(!fs.existsSync(file)) return []; + let count = 20; + let messages = new Array(count); + const fd = await open(file, 'r'); + let pos = (await fstat(fd)).size; + const buffer = Buffer.allocUnsafe(65536); + while(pos > 0 && count > 0) { + await read(fd, buffer, 0, 2, pos - 2); + const length = buffer.readUInt16LE(0); + pos = pos - length - 2; + await read(fd, buffer, 0, length, pos); + messages[--count] = deserializeMessage(buffer).message; + } + if(count !== 0) messages = messages.slice(count); + return messages; + } + + getLogDates(key: string): ReadonlyArray { + const entry = this.index[key]; + if(entry === undefined) return []; + const dayOffset = new Date().getTimezoneOffset() * 60000; + const dates = []; + for(const date in entry.index) dates.push(new Date(parseInt(date, 10) * dayMs + dayOffset)); + return dates; + } + + async getLogs(key: string, date: Date): Promise> { + const index = this.index[key]; + if(index === undefined) return []; + const dateOffset = index.index[Math.floor(date.getTime() / dayMs - date.getTimezoneOffset() / 1440)]; + if(dateOffset === undefined) return []; + const buffer = Buffer.allocUnsafe(50100); + const messages: Conversation.Message[] = []; + const file = getLogFile(key); + const fd = await open(file, 'r'); + let pos = index.offsets[dateOffset]; + const size = dateOffset + 1 < index.offsets.length ? index.offsets[dateOffset + 1] : (await fstat(fd)).size; + while(pos < size) { + await read(fd, buffer, 0, 50100, pos); + const deserialized = deserializeMessage(buffer); + messages.push(deserialized.message); + pos += deserialized.end; + } + return messages; + } + + logMessage(conversation: {key: string, name: string}, message: Message): void { + const file = getLogFile(conversation.key); + const buffer = serializeMessage(message).serialized; + const hasIndex = this.index[conversation.key] !== undefined; + const indexBuffer = checkIndex(this.index, message, conversation.key, conversation.name, + () => fs.existsSync(file) ? fs.statSync(file).size : 0); + if(indexBuffer !== undefined) fs.writeFileSync(`${file}.idx`, indexBuffer, {flag: hasIndex ? 'a' : 'wx'}); + fs.writeFileSync(file, buffer, {flag: 'a'}); + } + + get conversations(): ReadonlyArray<{id: string, name: string}> { + const conversations: {id: string, name: string}[] = []; + for(const key in this.index) conversations.push({id: key, name: this.index[key]!.name}); + conversations.sort((x, y) => (x.name < y.name ? -1 : (x.name > y.name ? 1 : 0))); + return conversations; + } +} + +export function getGeneralSettings(): GeneralSettings | undefined { + const file = path.join(baseDir, 'settings'); + if(!fs.existsSync(file)) return undefined; + return JSON.parse(fs.readFileSync(file, 'utf8')); +} + +export function setGeneralSettings(value: GeneralSettings): void { + fs.writeFileSync(path.join(baseDir, 'settings'), JSON.stringify(value)); +} + +function getSettingsDir(character: string = core.connection.character): string { + const dir = path.join(baseDir, character, 'settings'); + mkdir(dir); + return dir; +} + +export class SettingsStore implements Settings.Store { + async get(key: K, character?: string): Promise { + const file = path.join(getSettingsDir(character), key); + if(!fs.existsSync(file)) return undefined; + return JSON.parse(await readFile(file, 'utf8')); + } + + async getAvailableCharacters(): Promise> { + return (await readdir(baseDir)).filter((x) => fs.lstatSync(path.join(baseDir, x)).isDirectory()); + } + + async set(key: K, value: Settings.Keys[K]): Promise { + await writeFile(path.join(getSettingsDir(), key), JSON.stringify(value)); + } +} \ No newline at end of file diff --git a/electron/importer.ts b/electron/importer.ts new file mode 100644 index 0000000..3d78061 --- /dev/null +++ b/electron/importer.ts @@ -0,0 +1,261 @@ +import {addMinutes} from 'date-fns'; +import * as fs from 'fs'; +import * as path from 'path'; +import {promisify} from 'util'; +import {Settings} from '../chat/common'; +import {Conversation} from '../chat/interfaces'; +import {checkIndex, GeneralSettings, getLogDir, Message as LogMessage, serializeMessage, SettingsStore} from './filesystem'; + +function getRoamingDir(): string | undefined { + const appdata = process.env.APPDATA; + if(appdata === undefined || appdata.length === 0) return; + return path.join(appdata, 'slimCat'); +} + +function getLocalDir(): string | undefined { + const appdata = process.env.LOCALAPPDATA; + if(appdata === undefined || appdata.length === 0) return; + return path.join(appdata, 'slimCat'); +} + +function getSettingsDir(character: string): string | undefined { + const dir = getRoamingDir(); + if(dir === undefined) return; + let charDir = path.join(dir, character); + if(fs.existsSync(charDir)) return charDir; + charDir = path.join(dir, '!Defaults'); + if(fs.existsSync(charDir)) return charDir; + return; +} + +export function canImportGeneral(): boolean { + const dir = getLocalDir(); + return dir !== undefined && fs.existsSync(dir); +} + +export function canImportCharacter(character: string): boolean { + return getSettingsDir(character) !== undefined; +} + +export function importGeneral(): GeneralSettings | undefined { + let dir = getLocalDir(); + let files: string[] = []; + if(dir !== undefined) + files = files.concat(...fs.readdirSync(dir).map((x) => { + const subdir = path.join(dir, x); + return fs.readdirSync(subdir).map((y) => path.join(subdir, y, 'user.config')); + })); + dir = getRoamingDir(); + if(dir !== undefined) files.push(path.join(dir, '!preferences.xml')); + let file = ''; + for(let max = 0, i = 0; i < files.length; ++i) { + const time = fs.statSync(files[i]).mtime.getTime(); + if(time > max) { + max = time; + file = files[i]; + } + } + if(file.length === 0) return; + let elm = new DOMParser().parseFromString(fs.readFileSync(file, 'utf8'), 'application/xml').firstElementChild; + const data = new GeneralSettings(); + if(file.slice(-3) === 'xml') { + if(elm === null) return; + let elements; + if((elements = elm.getElementsByTagName('Username')).length > 0) + data.account = elements[0].textContent; + if((elements = elm.getElementsByTagName('Host')).length > 0) + data.host = elements[0].textContent; + } else { + if(elm !== null) elm = elm.firstElementChild; + if(elm !== null) elm = elm.firstElementChild; + if(elm === null) return; + const config = elm.getElementsByTagName('setting'); + for(const element of config) { + if(element.firstElementChild === null || element.firstElementChild.textContent === null) continue; + if(element.getAttribute('name') === 'UserName') data.account = element.firstElementChild.textContent; + else if(element.getAttribute('name') === 'Host') data.host = element.firstElementChild.textContent; + } + } + return data; +} + +const charRegex = /([A-Za-z0-9][A-Za-z0-9 \-_]{0,18}[A-Za-z0-9\-_])\b/; + +function createMessage(line: string, ownCharacter: string, name: string, isChannel: boolean, date: Date): LogMessage | undefined { + let type = Conversation.Message.Type.Message; + let sender: string | null; + let text: string; + + let lineIndex = line.indexOf(']'); + if(lineIndex === -1) return; + const time = line.substring(1, lineIndex); + let h = parseInt(time.substr(0, 2), 10); + const m = parseInt(time.substr(3, 2), 10); + if(time.slice(-2) === 'AM') h -= 12; + lineIndex += 2; + if(line[lineIndex] === '[') { + type = Conversation.Message.Type.Roll; + let endIndex = line.indexOf('[', lineIndex += 6); + if(endIndex - lineIndex > 20) endIndex = lineIndex + 20; + sender = line.substring(lineIndex, endIndex); + text = line.substring(endIndex + 6); + } else { + if(lineIndex + ownCharacter.length <= line.length && line.substr(lineIndex, ownCharacter.length) === ownCharacter) + sender = ownCharacter; + else if(!isChannel && lineIndex + name.length <= line.length && line.substr(lineIndex, name.length) === name) + sender = name; + else { + const matched = charRegex.exec(line.substr(lineIndex, 21)); + sender = matched !== null && matched.length > 1 ? matched[1] : ''; + } + lineIndex += sender.length; + if(line[lineIndex] === ':') { + ++lineIndex; + if(line[lineIndex] === ' ') ++lineIndex; + if(line.substr(lineIndex, 3) === '/me') { + type = Conversation.Message.Type.Action; + lineIndex += 3; + } + } else type = Conversation.Message.Type.Action; + text = line.substr(lineIndex); + } + return {type, sender: {name: sender}, text, time: addMinutes(date, h * 60 + m)}; +} + +async function importSettings(dir: string): Promise { + const settings = new Settings(); + const settingsStore = new SettingsStore(); + const buffer = fs.readFileSync(path.join(dir, 'Global', '!settings.xml')); + const content = buffer.toString('utf8', (buffer[0] === 0xEF && buffer[1] === 0xBB && buffer[2] === 0xBF) ? 3 : 0); + const config = new DOMParser().parseFromString(content, 'application/xml').firstElementChild; + if(config === null) return; + + function getValue(name: string): string | undefined { + if(config === null) return; + const elm = config.getElementsByTagName(name)[0]; + return elm !== undefined && elm.textContent !== null ? elm.textContent : undefined; + } + + if(getValue('AllowColors') === 'false') settings.disallowedTags.push('color'); + if(getValue('AllowIcons') === 'false') settings.disallowedTags.push('icon', 'eicon'); + if(getValue('AllowSound') === 'false') settings.playSound = false; + if(getValue('CheckForOwnName') === 'false') settings.highlight = false; + const idleTime = getValue('AutoIdleTime'); + if(getValue('AllowAutoIdle') === 'true' && idleTime !== undefined) + settings.idleTimer = parseInt(idleTime, 10); + const highlightWords = getValue('GlobalNotifyTerms'); + if(highlightWords !== undefined) + settings.highlightWords = highlightWords.split(',').map((x) => x.trim()).filter((x) => x.length); + if(getValue('ShowNotificationsGlobal') === 'false') settings.notifications = false; + if(getValue('ShowAvatars') === 'false') settings.showAvatars = false; + if(getValue('PlaySoundEvenWhenTabIsFocused') === 'true') settings.alwaysNotify = true; + await settingsStore.set('settings', settings); + + const pinned = {channels: [], private: []}; + const elements = config.getElementsByTagName('SavedChannels')[0].getElementsByTagName('channel'); + for(const element of elements) { + const item = element.textContent; + if(item !== null && pinned.channels.indexOf(item) === -1) pinned.channels.push(item); + } + await settingsStore.set('pinned', pinned); +} + +const knownOfficialChannels = ['Canon Characters', 'Monster\'s Lair', 'German IC', 'Humans/Humanoids', 'Warhammer General', + 'Love and Affection', 'Transformation', 'Hyper Endowed', 'Force/Non-Con', 'Diapers/Infantilism', 'Avians', 'Politics', 'Lesbians', + 'Superheroes', 'Footplay', 'Sadism/Masochism', 'German Politics', 'Para/Multi-Para RP', 'Micro/Macro', 'Ferals / Bestiality', + 'Gamers', 'Gay Males', 'Story Driven LFRP', 'Femdom', 'German OOC', 'World of Warcraft', 'Ageplay', 'German Furry', 'Scat Play', + 'Hermaphrodites', 'RP Dark City', 'All in the Family', 'Inflation', 'Development', 'Fantasy', 'Frontpage', 'Pokefurs', 'Medical Play', + 'Domination/Submission', 'Latex', 'Fat and Pudgy', 'Muscle Bound', 'Furries', 'RP Bar', 'The Slob Den', 'Artists / Writers', + 'Mind Control', 'Ass Play', 'Sex Driven LFRP', 'Gay Furry Males', 'Vore', 'Non-Sexual RP', 'Equestria ', 'Sci-fi', 'Watersports', + 'Straight Roleplay', 'Gore', 'Cuntboys', 'Femboy', 'Bondage', 'Cum Lovers', 'Transgender', 'Pregnancy and Impregnation', + 'Canon Characters OOC', 'Dragons', 'Helpdesk']; + +export async function importCharacter(ownCharacter: string, progress: (progress: number) => void): Promise { + const write = promisify(fs.write); + const dir = getSettingsDir(ownCharacter); + if(dir === undefined) return; + await importSettings(dir); + const adRegex = /Ad at \[.*?]:/; + const logRegex = /^(Ad at \[.*?]:|\[\d{2}.\d{2}.*] (\[user][A-Za-z0-9 \-_]|[A-Za-z0-9 \-_]))/; + const subdirs = fs.readdirSync(dir); + for(let i = 0; i < subdirs.length; ++i) { + progress(i / subdirs.length); + const subdir = subdirs[i]; + const subdirPath = path.join(dir, subdir); + if(subdir === '!Notifications' || subdir === 'Global' || !fs.lstatSync(subdirPath).isDirectory()) continue; + + const channelMarker = subdir.indexOf('('); + let key: string, name: string; + let isChannel = false; + if(channelMarker !== -1) { + isChannel = true; + key = `#${subdir.slice(channelMarker + 1, -1)}`.toLowerCase(); + name = subdir.substring(0, channelMarker - 1); + } else { + name = subdir; + if(knownOfficialChannels.indexOf(subdir) !== -1) { + key = `#${subdir}`.toLowerCase(); + isChannel = true; + } else key = subdir.toLowerCase(); + } + + const logFile = path.join(getLogDir(ownCharacter), key); + if(fs.existsSync(logFile)) fs.unlinkSync(logFile); + if(fs.existsSync(`${logFile}.idx`)) fs.unlinkSync(`${logFile}.idx`); + let logFd, indexFd; + const logIndex = {}; + let size = 0; + const files = fs.readdirSync(subdirPath); + for(const file of files.map((filename) => { + const date = path.basename(filename, '.txt').split('-'); + return {name: filename, date: new Date(parseInt(date[2], 10), parseInt(date[0], 10) - 1, parseInt(date[1], 10))}; + }).sort((x, y) => x.date.getTime() - y.date.getTime())) { + if(isNaN(file.date.getTime())) continue; + const content = fs.readFileSync(path.join(subdirPath, file.name), 'utf8'); + let index = 0, start = 0; + let ignoreLine = false; + while(index < content.length) { + if(index === start && adRegex.test(content.substr(start, 14))) + ignoreLine = true; + else { + const char = content[index]; + if(ignoreLine) { + if(char === '\n') { + const nextLine = content.substr(index + 1, 29); + if(logRegex.test(nextLine)) { + ignoreLine = false; + start = index + 1; + } + } + ++index; + continue; + } else if(char === '\r' || char === '\n') { + const nextLine = content.substr(index + (char === '\r' ? 2 : 1), 29); + if(logRegex.test(nextLine) || content.length - index <= 2) { + const line = content.substring(start, index); + const message = createMessage(line, ownCharacter, name, isChannel, file.date); + if(message === undefined) { + index += (char === '\r') ? 2 : 1; + continue; + } + if(indexFd === undefined || logFd === undefined) { + logFd = fs.openSync(logFile, 'a'); + indexFd = fs.openSync(`${logFile}.idx`, 'a'); + } + const indexBuffer = checkIndex(logIndex, message, key, name, size); + if(indexBuffer !== undefined) await write(indexFd, indexBuffer); + const serialized = serializeMessage(message); + await write(logFd, serialized.serialized); + size += serialized.size; + if(char === '\r') ++index; + start = index + 1; + } else if(char === '\r') ++index; + } + } + ++index; + } + } + if(indexFd !== undefined) fs.closeSync(indexFd); + if(logFd !== undefined) fs.closeSync(logFd); + } +} \ No newline at end of file diff --git a/electron/index.html b/electron/index.html new file mode 100644 index 0000000..a911886 --- /dev/null +++ b/electron/index.html @@ -0,0 +1,12 @@ + + + + + FChat 3.0 + + +
+
+ + + \ No newline at end of file diff --git a/electron/main.ts b/electron/main.ts new file mode 100644 index 0000000..db93e1f --- /dev/null +++ b/electron/main.ts @@ -0,0 +1,150 @@ +/** + * @license + * MIT License + * + * Copyright (c) 2017 F-List + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + * This license header applies to this file and all of the non-third-party assets it includes. + * @file The entry point for the Electron main thread of F-Chat 3.0. + * @copyright 2017 F-List + * @author Maya Wolf + * @version 3.0 + * @see {@link https://github.com/f-list/exported|GitHub repo} + */ +import * as electron from 'electron'; +import log from 'electron-log'; +import {autoUpdater} from 'electron-updater'; +import * as path from 'path'; +import * as url from 'url'; +import {mkdir} from './common'; +import * as windowState from './window_state'; + +// Module to control application life. +const app = electron.app; +const datadir = process.argv.filter((x) => x.startsWith('--datadir=')); +if(datadir.length > 0) app.setPath('userData', datadir[0].substr('--datadir='.length)); + +// Keep a global reference of the window object, if you don't, the window will +// be closed automatically when the JavaScript object is garbage collected. +let mainWindow: Electron.BrowserWindow | undefined; + +const baseDir = app.getPath('userData'); +mkdir(baseDir); +autoUpdater.logger = log; +log.transports.file.level = 'debug'; +log.transports.console.level = 'debug'; +log.transports.file.maxSize = 5 * 1024 * 1024; +log.transports.file.file = path.join(baseDir, 'log.txt'); +log.info('Starting application.'); + +function sendUpdaterStatusToWindow(status: string, progress?: object): void { + log.info(status); + mainWindow!.webContents.send('updater-status', status, progress); +} + +const updaterEvents = ['checking-for-update', 'update-available', 'update-not-available', 'error', 'update-downloaded']; +for(const eventName of updaterEvents) + autoUpdater.on(eventName, () => { + sendUpdaterStatusToWindow(eventName); + }); + +autoUpdater.on('download-progress', (_, progress: object) => { + sendUpdaterStatusToWindow('download-progress', progress); +}); + +function runUpdater(): void { + //tslint:disable-next-line:no-floating-promises + autoUpdater.checkForUpdates(); + //tslint:disable-next-line:no-floating-promises + setInterval(() => { autoUpdater.checkForUpdates(); }, 3600000); + electron.ipcMain.on('install-update', () => { + autoUpdater.quitAndInstall(false, true); + }); +} + +function bindWindowEvents(window: Electron.BrowserWindow): void { + // Prevent page navigation by opening links in an external browser. + const openLinkExternally = (e: Event, linkUrl: string) => { + e.preventDefault(); + electron.shell.openExternal(linkUrl); + }; + + window.webContents.on('will-navigate', openLinkExternally); + window.webContents.on('new-window', openLinkExternally); + // Fix focus events not properly propagating down to the document. + window.on('focus', () => mainWindow!.webContents.send('focus', true)); + window.on('blur', () => mainWindow!.webContents.send('focus', false)); + + // Save window state when it is being closed. + window.on('close', () => windowState.setSavedWindowState(window)); +} + +function createWindow(): void { + const lastState = windowState.getSavedWindowState(); + const windowProperties = {...lastState, center: lastState.x === undefined}; + // Create the browser window. + mainWindow = new electron.BrowserWindow(windowProperties); + if(lastState.maximized) + mainWindow.maximize(); + + // and load the index.html of the app. + mainWindow.loadURL(url.format({ + pathname: path.join(__dirname, 'index.html'), + protocol: 'file:', + slashes: true + })); + + bindWindowEvents(mainWindow); + + // Open the DevTools. + // mainWindow.webContents.openDevTools() + + // Emitted when the window is closed. + mainWindow.on('closed', () => { + // Dereference the window object, usually you would store windows + // in an array if your app supports multi windows, this is the time + // when you should delete the corresponding element. + mainWindow = undefined; + }); + + if(process.env.NODE_ENV === 'production') runUpdater(); +} + +// This method will be called when Electron has finished +// initialization and is ready to create browser windows. +// Some APIs can only be used after this event occurs. +app.on('ready', createWindow); + +// Quit when all windows are closed. +app.on('window-all-closed', () => { + // On OS X it is common for applications and their menu bar + // to stay active until the user quits explicitly with Cmd + Q + if(process.platform !== 'darwin') app.quit(); +}); + +app.on('activate', () => { + // On OS X it's common to re-create a window in the app when the + // dock icon is clicked and there are no other windows open. + if(mainWindow === undefined) createWindow(); +}); + +// In this file you can include the rest of your app's specific main process +// code. You can also put them in separate files and require them here. \ No newline at end of file diff --git a/electron/menu.ts b/electron/menu.ts new file mode 100644 index 0000000..da5330b --- /dev/null +++ b/electron/menu.ts @@ -0,0 +1,94 @@ +import * as electron from 'electron'; +import l from '../chat/localize'; + +export function createContextMenu(props: Electron.ContextMenuParams & {editFlags: {[key: string]: boolean}}): + Electron.MenuItemConstructorOptions[] { + const hasText = props.selectionText.trim().length > 0; + const can = (type: string) => props.editFlags[`can${type}`] && hasText; + + const menuTemplate: Electron.MenuItemConstructorOptions[] = []; + if(hasText || props.isEditable) + menuTemplate.push({ + id: 'copy', + label: l('action.copy'), + role: can('Copy') ? 'copy' : '', + enabled: can('Copy') + }); + if(props.isEditable) + menuTemplate.push({ + id: 'cut', + label: l('action.cut'), + role: can('Cut') ? 'cut' : '', + enabled: can('Cut') + }, { + id: 'paste', + label: l('action.paste'), + role: props.editFlags.canPaste ? 'paste' : '', + enabled: props.editFlags.canPaste + }); + else if(props.linkURL.length > 0 && props.mediaType === 'none' && props.linkURL.substr(0, props.pageURL.length) !== props.pageURL) + menuTemplate.push({ + id: 'copyLink', + label: l('action.copyLink'), + click(): void { + if(process.platform === 'darwin') + electron.clipboard.writeBookmark(props.linkText, props.linkURL); + else + electron.clipboard.writeText(props.linkURL); + } + }); + return menuTemplate; +} + +export function createAppMenu(): Electron.MenuItemConstructorOptions[] { + const viewItem = { + label: l('action.view'), + submenu: [ + {role: 'resetzoom'}, + {role: 'zoomin'}, + {role: 'zoomout'}, + {type: 'separator'}, + {role: 'togglefullscreen'} + ] + }; + const menu: Electron.MenuItemConstructorOptions[] = [ + { + label: l('title') + }, { + label: l('action.edit'), + submenu: [ + {role: 'undo'}, + {role: 'redo'}, + {type: 'separator'}, + {role: 'cut'}, + {role: 'copy'}, + {role: 'paste'}, + {role: 'selectall'} + ] + }, viewItem, { + role: 'help', + submenu: [ + { + label: l('help.fchat'), + click: () => electron.shell.openExternal('https://wiki.f-list.net/F-Chat_3.0') + }, + { + label: l('help.rules'), + click: () => electron.shell.openExternal('https://wiki.f-list.net/Rules') + }, + { + label: l('help.faq'), + click: () => electron.shell.openExternal('https://wiki.f-list.net/Frequently_Asked_Questions') + }, + { + label: l('help.report'), + click: () => electron.shell.openExternal('https://wiki.f-list.net/How_to_Report_a_User#In_chat') + }, + {label: l('version', electron.remote.app.getVersion()), enabled: false} + ] + } + ]; + if(process.env.NODE_ENV !== 'production') + viewItem.submenu.unshift({role: 'reload'}, {role: 'forcereload'}, {role: 'toggledevtools'}, {type: 'separator'}); + return menu; +} \ No newline at end of file diff --git a/electron/notifications.ts b/electron/notifications.ts new file mode 100644 index 0000000..bf1ed47 --- /dev/null +++ b/electron/notifications.ts @@ -0,0 +1,26 @@ +import {remote} from 'electron'; +import core from '../chat/core'; +import {Conversation} from '../chat/interfaces'; +//tslint:disable-next-line:match-default-export-name +import BaseNotifications from '../chat/notifications'; + +export default class Notifications extends BaseNotifications { + notify(conversation: Conversation, title: string, body: string, icon: string, sound: string): void { + if(!this.isInBackground && conversation === core.conversations.selectedConversation && !core.state.settings.alwaysNotify) return; + this.playSound(sound); + remote.getCurrentWindow().flashFrame(true); + if(core.state.settings.notifications) { + /*tslint:disable-next-line:no-object-literal-type-assertion*///false positive + const notification = new Notification(title, { + body, + icon: core.state.settings.showAvatars ? icon : undefined, + silent: true + }); + notification.onclick = () => { + conversation.show(); + remote.getCurrentWindow().focus(); + notification.close(); + }; + } + } +} \ No newline at end of file diff --git a/electron/package.json b/electron/package.json new file mode 100644 index 0000000..8944971 --- /dev/null +++ b/electron/package.json @@ -0,0 +1,46 @@ +{ + "name": "fchat", + "version": "3.0.0", + "author": "The F-List Team", + "description": "F-List.net Chat Client", + "main": "main.js", + "license": "MIT", + "dependencies": { + "keytar": "^4.0.4", + "spellchecker": "^3.4.3" + }, + "devDependencies": { + "electron": "^1.8.0", + "electron-builder": "^19.33.0", + "electron-log": "^2.2.9", + "electron-updater": "^2.8.9", + "extract-text-webpack-plugin": "^3.0.0" + }, + "scripts": { + "build": "../node_modules/.bin/webpack", + "build:dist": "../node_modules/.bin/webpack --env production", + "watch": "../node_modules/.bin/webpack --watch", + "start": "electron app" + }, + "build": { + "appId": "net.f-list.f-chat", + "productName": "F-Chat", + "files": [ + "*", + "sounds", + "themes", + "!**/*.map", + "!node_modules/", + "node_modules/**/*.node" + ], + "asar": false, + "linux": { + "category": "Network" + }, + "publish": { + "provider": "generic", + "url": "https://toys.in.newtsin.space/chat-updater", + "channel": "latest" + } + } +} \ No newline at end of file diff --git a/electron/qs.ts b/electron/qs.ts new file mode 100644 index 0000000..ae4cdb7 --- /dev/null +++ b/electron/qs.ts @@ -0,0 +1,2 @@ +import * as qs from 'querystring'; +export = qs; \ No newline at end of file diff --git a/electron/spellchecker.ts b/electron/spellchecker.ts new file mode 100644 index 0000000..e56807f --- /dev/null +++ b/electron/spellchecker.ts @@ -0,0 +1,51 @@ +import Axios from 'axios'; +import * as fs from 'fs'; +import * as path from 'path'; +import {promisify} from 'util'; +import {mkdir, nativeRequire} from './common'; + +process.env.SPELLCHECKER_PREFER_HUNSPELL = '1'; +const downloadUrl = 'https://github.com/wooorm/dictionaries/raw/master/dictionaries/'; +const dir = `${__dirname}/spellchecker`; +mkdir(dir); +//tslint:disable-next-line +const sc = nativeRequire<{ + Spellchecker: { + new(): { + isMisspelled(x: string): boolean, + setDictionary(name: string | undefined, dir: string): void, + getCorrectionsForMisspelling(word: string): ReadonlyArray + } + } +}>('spellchecker/build/Release/spellchecker.node'); +let availableDictionaries: string[] | undefined; +const writeFile = promisify(fs.writeFile); +const requestConfig = {responseType: 'arraybuffer'}; +const spellchecker = new sc.Spellchecker(); + +export async function getAvailableDictionaries(): Promise> { + if(availableDictionaries !== undefined) return availableDictionaries; + const dicts = (<{name: string}[]>(await Axios.get('https://api.github.com/repos/wooorm/dictionaries/contents/dictionaries')).data) + .map((x: {name: string}) => x.name); + availableDictionaries = dicts; + return dicts; +} + +export async function setDictionary(lang: string | undefined): Promise { + const dictName = lang !== undefined ? lang.replace('-', '_') : undefined; + if(dictName !== undefined) { + const dicPath = path.join(dir, `${dictName}.dic`); + if(!fs.existsSync(dicPath)) { + await writeFile(dicPath, new Buffer((await Axios.get(`${downloadUrl}${lang}/index.dic`, requestConfig)).data)); + await writeFile(path.join(dir, `${dictName}.aff`), + new Buffer((await Axios.get(`${downloadUrl}${lang}/index.aff`, requestConfig)).data)); + } + } + spellchecker.setDictionary(dictName, dir); +} + +export function getCorrections(word: string): ReadonlyArray { + return spellchecker.getCorrectionsForMisspelling(word); +} + +export const check = (text: string) => !spellchecker.isMisspelled(text); \ No newline at end of file diff --git a/electron/tsconfig.json b/electron/tsconfig.json new file mode 100644 index 0000000..fe08b82 --- /dev/null +++ b/electron/tsconfig.json @@ -0,0 +1,24 @@ +{ + "compilerOptions": { + "target": "es6", + "allowSyntheticDefaultImports": true, + "module": "commonjs", + "sourceMap": true, + "experimentalDecorators": true, + "allowJs": true, + "outDir": "build", + "noEmitHelpers": true, + "importHelpers": true, + "forceConsistentCasingInFileNames": true, + "strict": true, + "noImplicitReturns": true, + "noUnusedLocals": true, + "noUnusedParameters": true + }, + "include": ["*.ts", "../**/*.d.ts"], + "exclude": [ + "node_modules", + "dist", + "app" + ] +} \ No newline at end of file diff --git a/electron/webpack.config.js b/electron/webpack.config.js new file mode 100644 index 0000000..0f6c27d --- /dev/null +++ b/electron/webpack.config.js @@ -0,0 +1,96 @@ +const path = require('path'); +const webpack = require('webpack'); +const UglifyPlugin = require('uglifyjs-webpack-plugin'); +const ExtractTextPlugin = require('extract-text-webpack-plugin'); +const fs = require('fs'); +const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin'); +const exportLoader = require('../export-loader'); + +const config = { + entry: { + chat: [path.join(__dirname, 'chat.ts')], + main: [path.join(__dirname, 'main.ts'), path.join(__dirname, 'index.html'), path.join(__dirname, 'application.json')] + }, + output: { + path: __dirname + '/app', + filename: '[name].js' + }, + context: __dirname, + target: 'electron', + module: { + loaders: [ + { + test: /\.vue$/, + loader: 'vue-loader', + options: { + preLoaders: {ts: 'export-loader'}, + preserveWhitespace: false + } + }, + { + test: /\.ts$/, + loader: 'ts-loader', + options: { + appendTsSuffixTo: [/\.vue$/], + configFile: __dirname + '/tsconfig.json', + transpileOnly: true + } + }, + {test: /\.eot(\?v=\d+\.\d+\.\d+)?$/, loader: 'file-loader'}, + {test: /\.(woff|woff2)$/, loader: 'url-loader?prefix=font/&limit=5000'}, + {test: /\.ttf(\?v=\d+\.\d+\.\d+)?$/, loader: 'url-loader?limit=10000&mimetype=application/octet-stream'}, + {test: /\.svg(\?v=\d+\.\d+\.\d+)?$/, loader: 'url-loader?limit=10000&mimetype=image/svg+xml'}, + {test: /\.(wav|mp3|ogg)$/, loader: 'file-loader?name=sounds/[name].[ext]'}, + {test: /\.(png|html)$/, loader: 'file-loader?name=[name].[ext]'}, + {test: /application.json$/, loader: 'file-loader?name=package.json'} + ] + }, + node: { + __dirname: false, + __filename: false + }, + plugins: [ + new webpack.ProvidePlugin({ + '$': 'jquery/dist/jquery.slim.js', + 'jQuery': 'jquery/dist/jquery.slim.js', + 'window.jQuery': 'jquery/dist/jquery.slim.js' + }), + new ForkTsCheckerWebpackPlugin({workers: 2, async: false, tslint: path.join(__dirname, '../tslint.json')}), + exportLoader.delayTypecheck + ], + resolve: { + extensions: ['.ts', '.js', '.vue', '.css'], + alias: {qs: path.join(__dirname, 'qs.ts')} + }, + resolveLoader: { + modules: [ + 'node_modules', path.join(__dirname, '../') + ] + } +}; + +module.exports = function(env) { + const dist = env === 'production'; + const themesDir = path.join(__dirname, '../less/themes/chat'); + const themes = fs.readdirSync(themesDir); + const cssOptions = {use: [{loader: 'css-loader', options: {minimize: dist}}, 'less-loader']}; + for(const theme of themes) { + const absPath = path.join(themesDir, theme); + config.entry.chat.push(absPath); + const plugin = new ExtractTextPlugin('themes/' + theme.slice(0, -5) + '.css'); + config.plugins.push(plugin); + config.module.loaders.push({test: absPath, use: plugin.extract(cssOptions)}); + } + if(dist) { + config.devtool = 'source-map'; + config.plugins.push( + new UglifyPlugin({sourceMap: true}), + new webpack.DefinePlugin({ + 'process.env.NODE_ENV': JSON.stringify('production') + }) + ); + } else { + //config.devtool = 'cheap-module-eval-source-map'; + } + return config; +}; \ No newline at end of file diff --git a/electron/window_state.ts b/electron/window_state.ts new file mode 100644 index 0000000..1671c2e --- /dev/null +++ b/electron/window_state.ts @@ -0,0 +1,60 @@ +import {app, screen} from 'electron'; +import log from 'electron-log'; +import * as fs from 'fs'; +import * as path from 'path'; + +const baseDir = path.join(app.getPath('userData'), 'data'); +const windowStatePath = path.join(baseDir, 'window.json'); + +interface SavedWindowState { + x?: number + y?: number + height: number + width: number + maximized: boolean +} + +function mapToScreen(state: SavedWindowState): SavedWindowState { + let x = state.x !== undefined ? state.x : 0; + let y = state.y !== undefined ? state.y : 0; + const primaryDisplay = screen.getPrimaryDisplay(); + const targetDisplay = screen.getDisplayMatching({x, y, height: state.height, width: state.width}); + if(primaryDisplay.scaleFactor !== 1 && targetDisplay.id !== primaryDisplay.id) { + x /= primaryDisplay.scaleFactor; + y /= primaryDisplay.scaleFactor; + } + state.x = x > 0 ? x : undefined; + state.y = y > 0 ? y : undefined; + return state; +} + +export function setSavedWindowState(window: Electron.BrowserWindow): void { + const bounds = window.getBounds(); + const maximized = window.isMaximized(); + const windowState: SavedWindowState = { + height: bounds.height, + maximized, + width: bounds.width, + x: bounds.x, + y: bounds.y + }; + fs.writeFileSync(windowStatePath, JSON.stringify(windowState)); +} + +export function getSavedWindowState(): SavedWindowState { + const defaultState = { + height: 768, + maximized: false, + width: 1024 + }; + if(!fs.existsSync(windowStatePath)) + return defaultState; + try { + let savedState = JSON.parse(fs.readFileSync(windowStatePath, 'utf-8')); + savedState = mapToScreen(savedState); + return savedState; + } catch (e) { + log.error(e); + return defaultState; + } +} \ No newline at end of file diff --git a/electron/yarn.lock b/electron/yarn.lock new file mode 100644 index 0000000..aff8a5c --- /dev/null +++ b/electron/yarn.lock @@ -0,0 +1,1927 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"7zip-bin-linux@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/7zip-bin-linux/-/7zip-bin-linux-1.1.0.tgz#2ca309fd6a2102e18bd81e3a5d91b39db9adab71" + +"7zip-bin-mac@^1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/7zip-bin-mac/-/7zip-bin-mac-1.0.1.tgz#3e68778bbf0926adc68159427074505d47555c02" + +"7zip-bin-win@^2.1.1": + version "2.1.1" + resolved "https://registry.yarnpkg.com/7zip-bin-win/-/7zip-bin-win-2.1.1.tgz#8acfc28bb34e53a9476b46ae85a97418e6035c20" + +"7zip-bin@^2.2.4": + version "2.2.4" + resolved "https://registry.yarnpkg.com/7zip-bin/-/7zip-bin-2.2.4.tgz#5d0a7da759258b7fa59121fddcec7cb65938a85c" + optionalDependencies: + "7zip-bin-linux" "^1.1.0" + "7zip-bin-mac" "^1.0.1" + "7zip-bin-win" "^2.1.1" + +"@types/node@^8.0.24": + version "8.0.28" + resolved "https://registry.yarnpkg.com/@types/node/-/node-8.0.28.tgz#86206716f8d9251cf41692e384264cbd7058ad60" + +ajv-keywords@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-2.1.0.tgz#a296e17f7bfae7c1ce4f7e0de53d29cb32162df0" + +ajv@^4.9.1: + version "4.11.8" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-4.11.8.tgz#82ffb02b29e662ae53bdc20af15947706739c536" + dependencies: + co "^4.6.0" + json-stable-stringify "^1.0.1" + +ajv@^5.0.0, ajv@^5.2.2: + version "5.2.3" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-5.2.3.tgz#c06f598778c44c6b161abafe3466b81ad1814ed2" + dependencies: + co "^4.6.0" + fast-deep-equal "^1.0.0" + json-schema-traverse "^0.3.0" + json-stable-stringify "^1.0.1" + +ansi-align@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/ansi-align/-/ansi-align-2.0.0.tgz#c36aeccba563b89ceb556f3690f0b1d9e3547f7f" + dependencies: + string-width "^2.0.0" + +ansi-regex@^2.0.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df" + +ansi-regex@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-3.0.0.tgz#ed0317c322064f79466c02966bddb605ab37d998" + +ansi-styles@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-2.2.1.tgz#b432dd3358b634cf75e1e4664368240533c1ddbe" + +ansi-styles@^3.1.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.0.tgz#c159b8d5be0f9e5a6f346dab94f16ce022161b88" + dependencies: + color-convert "^1.9.0" + +any-promise@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/any-promise/-/any-promise-1.3.0.tgz#abc6afeedcea52e809cdc0376aed3ce39635d17f" + +app-package-builder@1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/app-package-builder/-/app-package-builder-1.0.3.tgz#39eeb95d3e67c9e0444c20bbcec6f3ea03ce2033" + dependencies: + bluebird-lst "^1.0.3" + builder-util "^2.0.5" + builder-util-runtime "^1.0.5" + fs-extra-p "^4.4.2" + int64-buffer "^0.1.9" + js-yaml "^3.10.0" + +argparse@^1.0.7: + version "1.0.9" + resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.9.tgz#73d83bc263f86e97f8cc4f6bae1b0e90a7d22c86" + dependencies: + sprintf-js "~1.0.2" + +array-find-index@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/array-find-index/-/array-find-index-1.0.2.tgz#df010aa1287e164bbda6f9723b0a96a1ec4187a1" + +asar-integrity@0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/asar-integrity/-/asar-integrity-0.2.1.tgz#68a49aaafa407c0720ae683f9aefda09fdce60f1" + dependencies: + bluebird-lst "^1.0.3" + fs-extra-p "^4.4.2" + +asn1@~0.2.3: + version "0.2.3" + resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.3.tgz#dac8787713c9966849fc8180777ebe9c1ddf3b86" + +assert-plus@1.0.0, assert-plus@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-1.0.0.tgz#f12e0f3c5d77b0b1cdd9146942e4e96c1e4dd525" + +assert-plus@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-0.2.0.tgz#d74e1b87e7affc0db8aadb7021f3fe48101ab234" + +async-exit-hook@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/async-exit-hook/-/async-exit-hook-2.0.1.tgz#8bd8b024b0ec9b1c01cccb9af9db29bd717dfaf3" + +async@^2.4.1: + version "2.5.0" + resolved "https://registry.yarnpkg.com/async/-/async-2.5.0.tgz#843190fd6b7357a0b9e1c956edddd5ec8462b54d" + dependencies: + lodash "^4.14.0" + +asynckit@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" + +aws-sign2@~0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.6.0.tgz#14342dd38dbcc94d0e5b87d763cd63612c0e794f" + +aws4@^1.2.1: + version "1.6.0" + resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.6.0.tgz#83ef5ca860b2b32e4a0deedee8c771b9db57471e" + +balanced-match@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" + +base64-js@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.2.0.tgz#a39992d723584811982be5e290bb6a53d86700f1" + +bcrypt-pbkdf@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.1.tgz#63bc5dcb61331b92bc05fd528953c33462a06f8d" + dependencies: + tweetnacl "^0.14.3" + +big.js@^3.1.3: + version "3.1.3" + resolved "https://registry.yarnpkg.com/big.js/-/big.js-3.1.3.tgz#4cada2193652eb3ca9ec8e55c9015669c9806978" + +bluebird-lst@^1.0.2, bluebird-lst@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/bluebird-lst/-/bluebird-lst-1.0.3.tgz#cc56c18660eff0a0b86e2c33d1659618f7005158" + dependencies: + bluebird "^3.5.0" + +bluebird@^3.5.0: + version "3.5.0" + resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.5.0.tgz#791420d7f551eea2897453a8a77653f96606d67c" + +boom@2.x.x: + version "2.10.1" + resolved "https://registry.yarnpkg.com/boom/-/boom-2.10.1.tgz#39c8918ceff5799f83f9492a848f625add0c766f" + dependencies: + hoek "2.x.x" + +boxen@^1.0.0: + version "1.2.1" + resolved "https://registry.yarnpkg.com/boxen/-/boxen-1.2.1.tgz#0f11e7fe344edb9397977fc13ede7f64d956481d" + dependencies: + ansi-align "^2.0.0" + camelcase "^4.0.0" + chalk "^2.0.1" + cli-boxes "^1.0.0" + string-width "^2.0.0" + term-size "^1.2.0" + widest-line "^1.0.0" + +brace-expansion@^1.1.7: + version "1.1.8" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.8.tgz#c07b211c7c952ec1f8efd51a77ef0d1d3990a292" + dependencies: + balanced-match "^1.0.0" + concat-map "0.0.1" + +builder-util-runtime@1.0.6, builder-util-runtime@^1.0.5, builder-util-runtime@^1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/builder-util-runtime/-/builder-util-runtime-1.0.6.tgz#3087c39608470fa1b6ee90a4c565d96bd768c531" + dependencies: + bluebird-lst "^1.0.3" + debug "^3.1.0" + fs-extra-p "^4.4.2" + +builder-util@2.0.6, builder-util@^2.0.2, builder-util@^2.0.5: + version "2.0.6" + resolved "https://registry.yarnpkg.com/builder-util/-/builder-util-2.0.6.tgz#2a3eec93c77709e45f879c2eccdcbe5f29a70271" + dependencies: + "7zip-bin" "^2.2.4" + bluebird-lst "^1.0.3" + builder-util-runtime "^1.0.6" + chalk "^2.1.0" + debug "^3.1.0" + fs-extra-p "^4.4.2" + ini "^1.3.4" + is-ci "^1.0.10" + js-yaml "^3.10.0" + lazy-val "^1.0.2" + node-emoji "^1.8.1" + semver "^5.4.1" + source-map-support "^0.4.18" + stat-mode "^0.2.2" + temp-file "^2.0.3" + tunnel-agent "^0.6.0" + +builtin-modules@^1.0.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-1.1.1.tgz#270f076c5a72c02f5b65a47df94c5fe3a278892f" + +camelcase-keys@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/camelcase-keys/-/camelcase-keys-2.1.0.tgz#308beeaffdf28119051efa1d932213c91b8f92e7" + dependencies: + camelcase "^2.0.0" + map-obj "^1.0.0" + +camelcase@^2.0.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-2.1.1.tgz#7c1d16d679a1bbe59ca02cacecfb011e201f5a1f" + +camelcase@^4.0.0, camelcase@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-4.1.0.tgz#d545635be1e33c542649c69173e5de6acfae34dd" + +capture-stack-trace@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/capture-stack-trace/-/capture-stack-trace-1.0.0.tgz#4a6fa07399c26bba47f0b2496b4d0fb408c5550d" + +caseless@~0.12.0: + version "0.12.0" + resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc" + +chalk@^1.0.0: + version "1.1.3" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-1.1.3.tgz#a8115c55e4a702fe4d150abd3872822a7e09fc98" + dependencies: + ansi-styles "^2.2.1" + escape-string-regexp "^1.0.2" + has-ansi "^2.0.0" + strip-ansi "^3.0.0" + supports-color "^2.0.0" + +chalk@^2.0.1, chalk@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.1.0.tgz#ac5becf14fa21b99c6c92ca7a7d7cfd5b17e743e" + dependencies: + ansi-styles "^3.1.0" + escape-string-regexp "^1.0.5" + supports-color "^4.0.0" + +chromium-pickle-js@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/chromium-pickle-js/-/chromium-pickle-js-0.2.0.tgz#04a106672c18b085ab774d983dfa3ea138f22205" + +ci-info@^1.0.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-1.1.1.tgz#47b44df118c48d2597b56d342e7e25791060171a" + +cli-boxes@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/cli-boxes/-/cli-boxes-1.0.0.tgz#4fa917c3e59c94a004cd61f8ee509da651687143" + +cliui@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/cliui/-/cliui-3.2.0.tgz#120601537a916d29940f934da3b48d585a39213d" + dependencies: + string-width "^1.0.1" + strip-ansi "^3.0.1" + wrap-ansi "^2.0.0" + +co@^4.6.0: + version "4.6.0" + resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184" + +code-point-at@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77" + +color-convert@^1.9.0: + version "1.9.0" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.0.tgz#1accf97dd739b983bf994d56fec8f95853641b7a" + dependencies: + color-name "^1.1.1" + +color-convert@~0.5.0: + version "0.5.3" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-0.5.3.tgz#bdb6c69ce660fadffe0b0007cc447e1b9f7282bd" + +color-name@^1.1.1: + version "1.1.3" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" + +combined-stream@^1.0.5, combined-stream@~1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.5.tgz#938370a57b4a51dea2c77c15d5c5fdf895164009" + dependencies: + delayed-stream "~1.0.0" + +compare-version@^0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/compare-version/-/compare-version-0.1.2.tgz#0162ec2d9351f5ddd59a9202cba935366a725080" + +concat-map@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" + +concat-stream@1.6.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.6.0.tgz#0aac662fd52be78964d5532f694784e70110acf7" + dependencies: + inherits "^2.0.3" + readable-stream "^2.2.2" + typedarray "^0.0.6" + +configstore@^3.0.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/configstore/-/configstore-3.1.1.tgz#094ee662ab83fad9917678de114faaea8fcdca90" + dependencies: + dot-prop "^4.1.0" + graceful-fs "^4.1.2" + make-dir "^1.0.0" + unique-string "^1.0.0" + write-file-atomic "^2.0.0" + xdg-basedir "^3.0.0" + +core-util-is@1.0.2, core-util-is@~1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" + +create-error-class@^3.0.0: + version "3.0.2" + resolved "https://registry.yarnpkg.com/create-error-class/-/create-error-class-3.0.2.tgz#06be7abef947a3f14a30fd610671d401bca8b7b6" + dependencies: + capture-stack-trace "^1.0.0" + +cross-spawn@^5.0.1: + version "5.1.0" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-5.1.0.tgz#e8bd0efee58fcff6f8f94510a0a554bbfa235449" + dependencies: + lru-cache "^4.0.1" + shebang-command "^1.2.0" + which "^1.2.9" + +cryptiles@2.x.x: + version "2.0.5" + resolved "https://registry.yarnpkg.com/cryptiles/-/cryptiles-2.0.5.tgz#3bdfecdc608147c1c67202fa291e7dca59eaa3b8" + dependencies: + boom "2.x.x" + +crypto-random-string@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/crypto-random-string/-/crypto-random-string-1.0.0.tgz#a230f64f568310e1498009940790ec99545bca7e" + +cuint@^0.2.2: + version "0.2.2" + resolved "https://registry.yarnpkg.com/cuint/-/cuint-0.2.2.tgz#408086d409550c2631155619e9fa7bcadc3b991b" + +currently-unhandled@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/currently-unhandled/-/currently-unhandled-0.4.1.tgz#988df33feab191ef799a61369dd76c17adf957ea" + dependencies: + array-find-index "^1.0.1" + +dashdash@^1.12.0: + version "1.14.1" + resolved "https://registry.yarnpkg.com/dashdash/-/dashdash-1.14.1.tgz#853cfa0f7cbe2fed5de20326b8dd581035f6e2f0" + dependencies: + assert-plus "^1.0.0" + +debug@2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/debug/-/debug-2.2.0.tgz#f87057e995b1a1f6ae6a4960664137bc56f039da" + dependencies: + ms "0.7.1" + +debug@^2.1.3, debug@^2.2.0: + version "2.6.8" + resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.8.tgz#e731531ca2ede27d188222427da17821d68ff4fc" + dependencies: + ms "2.0.0" + +debug@^2.6.8: + version "2.6.9" + resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" + dependencies: + ms "2.0.0" + +debug@^3.0.0, debug@^3.0.1, debug@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261" + dependencies: + ms "2.0.0" + +decamelize@^1.1.1, decamelize@^1.1.2: + version "1.2.0" + resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" + +deep-extend@~0.4.0: + version "0.4.2" + resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.4.2.tgz#48b699c27e334bf89f10892be432f6e4c7d34a7f" + +delayed-stream@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" + +dmg-builder@2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/dmg-builder/-/dmg-builder-2.1.0.tgz#fdeaa0d210bb3e27f31d9ceb485d90cfac3fdedc" + dependencies: + bluebird-lst "^1.0.3" + builder-util "^2.0.2" + debug "^3.0.1" + fs-extra-p "^4.4.2" + iconv-lite "^0.4.19" + js-yaml "^3.10.0" + parse-color "^1.0.0" + +dot-prop@^4.1.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/dot-prop/-/dot-prop-4.2.0.tgz#1f19e0c2e1aa0e32797c49799f2837ac6af69c57" + dependencies: + is-obj "^1.0.0" + +dotenv-expand@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/dotenv-expand/-/dotenv-expand-4.0.1.tgz#68fddc1561814e0a10964111057ff138ced7d7a8" + +dotenv@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-4.0.0.tgz#864ef1379aced55ce6f95debecdce179f7a0cd1d" + +duplexer3@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/duplexer3/-/duplexer3-0.1.4.tgz#ee01dd1cac0ed3cbc7fdbea37dc0a8f1ce002ce2" + +ecc-jsbn@~0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.1.tgz#0fc73a9ed5f0d53c38193398523ef7e543777505" + dependencies: + jsbn "~0.1.0" + +ejs@^2.5.7: + version "2.5.7" + resolved "https://registry.yarnpkg.com/ejs/-/ejs-2.5.7.tgz#cc872c168880ae3c7189762fd5ffc00896c9518a" + +electron-builder@^19.33.0: + version "19.33.0" + resolved "https://registry.yarnpkg.com/electron-builder/-/electron-builder-19.33.0.tgz#2b0ac41fb9ece5b07f3df57f21064ac3e705f225" + dependencies: + "7zip-bin" "^2.2.4" + app-package-builder "1.0.3" + asar-integrity "0.2.1" + async-exit-hook "^2.0.1" + bluebird-lst "^1.0.3" + builder-util "2.0.6" + builder-util-runtime "1.0.6" + chalk "^2.1.0" + chromium-pickle-js "^0.2.0" + cuint "^0.2.2" + debug "^3.1.0" + dmg-builder "2.1.0" + dotenv "^4.0.0" + dotenv-expand "^4.0.1" + ejs "^2.5.7" + electron-download-tf "4.3.4" + electron-osx-sign "0.4.7" + electron-publish "19.32.0" + fs-extra-p "^4.4.2" + hosted-git-info "^2.5.0" + is-ci "^1.0.10" + isbinaryfile "^3.0.2" + js-yaml "^3.10.0" + lazy-val "^1.0.2" + minimatch "^3.0.4" + normalize-package-data "^2.4.0" + plist "^2.1.0" + read-config-file "1.1.1" + sanitize-filename "^1.6.1" + semver "^5.4.1" + temp-file "^2.0.3" + update-notifier "^2.2.0" + yargs "^9.0.1" + +electron-download-tf@4.3.4: + version "4.3.4" + resolved "https://registry.yarnpkg.com/electron-download-tf/-/electron-download-tf-4.3.4.tgz#b03740b2885aa2ad3f8784fae74df427f66d5165" + dependencies: + debug "^3.0.0" + env-paths "^1.0.0" + fs-extra "^4.0.1" + minimist "^1.2.0" + nugget "^2.0.1" + path-exists "^3.0.0" + rc "^1.2.1" + semver "^5.4.1" + sumchecker "^2.0.2" + +electron-download@^3.0.1: + version "3.3.0" + resolved "https://registry.yarnpkg.com/electron-download/-/electron-download-3.3.0.tgz#2cfd54d6966c019c4d49ad65fbe65cc9cdef68c8" + dependencies: + debug "^2.2.0" + fs-extra "^0.30.0" + home-path "^1.0.1" + minimist "^1.2.0" + nugget "^2.0.0" + path-exists "^2.1.0" + rc "^1.1.2" + semver "^5.3.0" + sumchecker "^1.2.0" + +electron-is-dev@^0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/electron-is-dev/-/electron-is-dev-0.3.0.tgz#14e6fda5c68e9e4ecbeff9ccf037cbd7c05c5afe" + +electron-log@^2.2.9: + version "2.2.9" + resolved "https://registry.yarnpkg.com/electron-log/-/electron-log-2.2.9.tgz#e0484cb1a8a84593095e3b69f47361ae15d73bdf" + +electron-osx-sign@0.4.7: + version "0.4.7" + resolved "https://registry.yarnpkg.com/electron-osx-sign/-/electron-osx-sign-0.4.7.tgz#1d75647a82748eacd48bea70616ec83ffade3ee5" + dependencies: + bluebird "^3.5.0" + compare-version "^0.1.2" + debug "^2.6.8" + isbinaryfile "^3.0.2" + minimist "^1.2.0" + plist "^2.1.0" + +electron-publish@19.32.0: + version "19.32.0" + resolved "https://registry.yarnpkg.com/electron-publish/-/electron-publish-19.32.0.tgz#ee25b1cc3a0d54a37a01679bff07b28925ff2818" + dependencies: + bluebird-lst "^1.0.3" + builder-util "^2.0.5" + builder-util-runtime "^1.0.6" + chalk "^2.1.0" + fs-extra-p "^4.4.2" + mime "^2.0.3" + +electron-updater@^2.8.9: + version "2.10.1" + resolved "https://registry.yarnpkg.com/electron-updater/-/electron-updater-2.10.1.tgz#5763bda9153c08459d7058d9afedccc178b88304" + dependencies: + bluebird-lst "^1.0.3" + builder-util-runtime "^1.0.6" + electron-is-dev "^0.3.0" + fs-extra-p "^4.4.2" + js-yaml "^3.10.0" + lazy-val "^1.0.2" + lodash.isequal "^4.5.0" + semver "^5.4.1" + source-map-support "^0.4.18" + xelement "^1.0.16" + +electron@^1.8.0: + version "1.8.0" + resolved "https://registry.yarnpkg.com/electron/-/electron-1.8.0.tgz#896f429b1e664f496f62b9cc7ee6a67a71375f31" + dependencies: + "@types/node" "^8.0.24" + electron-download "^3.0.1" + extract-zip "^1.0.3" + +emojis-list@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/emojis-list/-/emojis-list-2.1.0.tgz#4daa4d9db00f9819880c79fa457ae5b09a1fd389" + +env-paths@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/env-paths/-/env-paths-1.0.0.tgz#4168133b42bb05c38a35b1ae4397c8298ab369e0" + +error-ex@^1.2.0: + version "1.3.1" + resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.1.tgz#f855a86ce61adc4e8621c3cda21e7a7612c3a8dc" + dependencies: + is-arrayish "^0.2.1" + +es6-promise@^4.0.5: + version "4.1.1" + resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-4.1.1.tgz#8811e90915d9a0dba36274f0b242dbda78f9c92a" + +escape-string-regexp@^1.0.2, escape-string-regexp@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" + +esprima@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.0.tgz#4499eddcd1110e0b218bacf2fa7f7f59f55ca804" + +execa@^0.7.0: + version "0.7.0" + resolved "https://registry.yarnpkg.com/execa/-/execa-0.7.0.tgz#944becd34cc41ee32a63a9faf27ad5a65fc59777" + dependencies: + cross-spawn "^5.0.1" + get-stream "^3.0.0" + is-stream "^1.1.0" + npm-run-path "^2.0.0" + p-finally "^1.0.0" + signal-exit "^3.0.0" + strip-eof "^1.0.0" + +extend@~3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.1.tgz#a755ea7bc1adfcc5a31ce7e762dbaadc5e636444" + +extract-text-webpack-plugin@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/extract-text-webpack-plugin/-/extract-text-webpack-plugin-3.0.0.tgz#90caa7907bc449f335005e3ac7532b41b00de612" + dependencies: + async "^2.4.1" + loader-utils "^1.1.0" + schema-utils "^0.3.0" + webpack-sources "^1.0.1" + +extract-zip@^1.0.3: + version "1.6.5" + resolved "https://registry.yarnpkg.com/extract-zip/-/extract-zip-1.6.5.tgz#99a06735b6ea20ea9b705d779acffcc87cff0440" + dependencies: + concat-stream "1.6.0" + debug "2.2.0" + mkdirp "0.5.0" + yauzl "2.4.1" + +extsprintf@1.3.0, extsprintf@^1.2.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.3.0.tgz#96918440e3041a7a414f8c52e3c574eb3c3e1e05" + +fast-deep-equal@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-1.0.0.tgz#96256a3bc975595eb36d82e9929d060d893439ff" + +fd-slicer@~1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/fd-slicer/-/fd-slicer-1.0.1.tgz#8b5bcbd9ec327c5041bf9ab023fd6750f1177e65" + dependencies: + pend "~1.2.0" + +find-up@^1.0.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-1.1.2.tgz#6b2e9822b1a2ce0a60ab64d610eccad53cb24d0f" + dependencies: + path-exists "^2.0.0" + pinkie-promise "^2.0.0" + +find-up@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-2.1.0.tgz#45d1b7e506c717ddd482775a2b77920a3c0c57a7" + dependencies: + locate-path "^2.0.0" + +forever-agent@~0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91" + +form-data@~2.1.1: + version "2.1.4" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.1.4.tgz#33c183acf193276ecaa98143a69e94bfee1750d1" + dependencies: + asynckit "^0.4.0" + combined-stream "^1.0.5" + mime-types "^2.1.12" + +fs-extra-p@^4.4.0, fs-extra-p@^4.4.2: + version "4.4.2" + resolved "https://registry.yarnpkg.com/fs-extra-p/-/fs-extra-p-4.4.2.tgz#b6abd882a9b89f41b977771d3da788ce38f085f3" + dependencies: + bluebird-lst "^1.0.2" + fs-extra "^4.0.2" + +fs-extra@^0.30.0: + version "0.30.0" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-0.30.0.tgz#f233ffcc08d4da7d432daa449776989db1df93f0" + dependencies: + graceful-fs "^4.1.2" + jsonfile "^2.1.0" + klaw "^1.0.0" + path-is-absolute "^1.0.0" + rimraf "^2.2.8" + +fs-extra@^4.0.1, fs-extra@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-4.0.2.tgz#f91704c53d1b461f893452b0c307d9997647ab6b" + dependencies: + graceful-fs "^4.1.2" + jsonfile "^4.0.0" + universalify "^0.1.0" + +fs.realpath@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" + +get-caller-file@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-1.0.2.tgz#f702e63127e7e231c160a80c1554acb70d5047e5" + +get-stdin@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-4.0.1.tgz#b968c6b0a04384324902e8bf1a5df32579a450fe" + +get-stream@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-3.0.0.tgz#8e943d1358dc37555054ecbe2edb05aa174ede14" + +getpass@^0.1.1: + version "0.1.7" + resolved "https://registry.yarnpkg.com/getpass/-/getpass-0.1.7.tgz#5eff8e3e684d569ae4cb2b1282604e8ba62149fa" + dependencies: + assert-plus "^1.0.0" + +glob@^7.0.5: + version "7.1.2" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.2.tgz#c19c9df9a028702d678612384a6552404c636d15" + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.0.4" + once "^1.3.0" + path-is-absolute "^1.0.0" + +got@^6.7.1: + version "6.7.1" + resolved "https://registry.yarnpkg.com/got/-/got-6.7.1.tgz#240cd05785a9a18e561dc1b44b41c763ef1e8db0" + dependencies: + create-error-class "^3.0.0" + duplexer3 "^0.1.4" + get-stream "^3.0.0" + is-redirect "^1.0.0" + is-retry-allowed "^1.0.0" + is-stream "^1.0.0" + lowercase-keys "^1.0.0" + safe-buffer "^5.0.1" + timed-out "^4.0.0" + unzip-response "^2.0.1" + url-parse-lax "^1.0.0" + +graceful-fs@^4.1.11, graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.1.9: + version "4.1.11" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.11.tgz#0e8bdfe4d1ddb8854d64e04ea7c00e2a026e5658" + +har-schema@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-1.0.5.tgz#d263135f43307c02c602afc8fe95970c0151369e" + +har-validator@~4.2.1: + version "4.2.1" + resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-4.2.1.tgz#33481d0f1bbff600dd203d75812a6a5fba002e2a" + dependencies: + ajv "^4.9.1" + har-schema "^1.0.5" + +has-ansi@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/has-ansi/-/has-ansi-2.0.0.tgz#34f5049ce1ecdf2b0649af3ef24e45ed35416d91" + dependencies: + ansi-regex "^2.0.0" + +has-flag@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-2.0.0.tgz#e8207af1cc7b30d446cc70b734b5e8be18f88d51" + +hawk@~3.1.3: + version "3.1.3" + resolved "https://registry.yarnpkg.com/hawk/-/hawk-3.1.3.tgz#078444bd7c1640b0fe540d2c9b73d59678e8e1c4" + dependencies: + boom "2.x.x" + cryptiles "2.x.x" + hoek "2.x.x" + sntp "1.x.x" + +hoek@2.x.x: + version "2.16.3" + resolved "https://registry.yarnpkg.com/hoek/-/hoek-2.16.3.tgz#20bb7403d3cea398e91dc4710a8ff1b8274a25ed" + +home-path@^1.0.1: + version "1.0.5" + resolved "https://registry.yarnpkg.com/home-path/-/home-path-1.0.5.tgz#788b29815b12d53bacf575648476e6f9041d133f" + +hosted-git-info@^2.1.4, hosted-git-info@^2.5.0: + version "2.5.0" + resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.5.0.tgz#6d60e34b3abbc8313062c3b798ef8d901a07af3c" + +http-signature@~1.1.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.1.1.tgz#df72e267066cd0ac67fb76adf8e134a8fbcf91bf" + dependencies: + assert-plus "^0.2.0" + jsprim "^1.2.2" + sshpk "^1.7.0" + +iconv-lite@^0.4.19: + version "0.4.19" + resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.19.tgz#f7468f60135f5e5dad3399c0a81be9a1603a082b" + +import-lazy@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/import-lazy/-/import-lazy-2.1.0.tgz#05698e3d45c88e8d7e9d92cb0584e77f096f3e43" + +imurmurhash@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" + +indent-string@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-2.1.0.tgz#8e2d48348742121b4a8218b7a137e9a52049dc80" + dependencies: + repeating "^2.0.0" + +inflight@^1.0.4: + version "1.0.6" + resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" + dependencies: + once "^1.3.0" + wrappy "1" + +inherits@2, inherits@^2.0.3, inherits@~2.0.1, inherits@~2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" + +ini@^1.3.4, ini@~1.3.0: + version "1.3.4" + resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.4.tgz#0537cb79daf59b59a1a517dff706c86ec039162e" + +int64-buffer@^0.1.9: + version "0.1.9" + resolved "https://registry.yarnpkg.com/int64-buffer/-/int64-buffer-0.1.9.tgz#9e039da043b24f78b196b283e04653ef5e990f61" + +invert-kv@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/invert-kv/-/invert-kv-1.0.0.tgz#104a8e4aaca6d3d8cd157a8ef8bfab2d7a3ffdb6" + +is-arrayish@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" + +is-builtin-module@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-builtin-module/-/is-builtin-module-1.0.0.tgz#540572d34f7ac3119f8f76c30cbc1b1e037affbe" + dependencies: + builtin-modules "^1.0.0" + +is-ci@^1.0.10: + version "1.0.10" + resolved "https://registry.yarnpkg.com/is-ci/-/is-ci-1.0.10.tgz#f739336b2632365061a9d48270cd56ae3369318e" + dependencies: + ci-info "^1.0.0" + +is-finite@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-finite/-/is-finite-1.0.2.tgz#cc6677695602be550ef11e8b4aa6305342b6d0aa" + dependencies: + number-is-nan "^1.0.0" + +is-fullwidth-code-point@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz#ef9e31386f031a7f0d643af82fde50c457ef00cb" + dependencies: + number-is-nan "^1.0.0" + +is-fullwidth-code-point@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz#a3b30a5c4f199183167aaab93beefae3ddfb654f" + +is-npm@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-npm/-/is-npm-1.0.0.tgz#f2fb63a65e4905b406c86072765a1a4dc793b9f4" + +is-obj@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-obj/-/is-obj-1.0.1.tgz#3e4729ac1f5fde025cd7d83a896dab9f4f67db0f" + +is-redirect@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-redirect/-/is-redirect-1.0.0.tgz#1d03dded53bd8db0f30c26e4f95d36fc7c87dc24" + +is-retry-allowed@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/is-retry-allowed/-/is-retry-allowed-1.1.0.tgz#11a060568b67339444033d0125a61a20d564fb34" + +is-stream@^1.0.0, is-stream@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44" + +is-typedarray@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" + +is-utf8@^0.2.0: + version "0.2.1" + resolved "https://registry.yarnpkg.com/is-utf8/-/is-utf8-0.2.1.tgz#4b0da1442104d1b336340e80797e865cf39f7d72" + +isarray@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf" + +isarray@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" + +isbinaryfile@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/isbinaryfile/-/isbinaryfile-3.0.2.tgz#4a3e974ec0cba9004d3fc6cde7209ea69368a621" + +isexe@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" + +isstream@~0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a" + +js-yaml@^3.10.0: + version "3.10.0" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.10.0.tgz#2e78441646bd4682e963f22b6e92823c309c62dc" + dependencies: + argparse "^1.0.7" + esprima "^4.0.0" + +jsbn@~0.1.0: + version "0.1.1" + resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513" + +json-schema-traverse@^0.3.0: + version "0.3.1" + resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz#349a6d44c53a51de89b40805c5d5e59b417d3340" + +json-schema@0.2.3: + version "0.2.3" + resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.2.3.tgz#b480c892e59a2f05954ce727bd3f2a4e882f9e13" + +json-stable-stringify@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/json-stable-stringify/-/json-stable-stringify-1.0.1.tgz#9a759d39c5f2ff503fd5300646ed445f88c4f9af" + dependencies: + jsonify "~0.0.0" + +json-stringify-safe@~5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb" + +json5@^0.5.0, json5@^0.5.1: + version "0.5.1" + resolved "https://registry.yarnpkg.com/json5/-/json5-0.5.1.tgz#1eade7acc012034ad84e2396767ead9fa5495821" + +jsonfile@^2.1.0: + version "2.4.0" + resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-2.4.0.tgz#3736a2b428b87bbda0cc83b53fa3d633a35c2ae8" + optionalDependencies: + graceful-fs "^4.1.6" + +jsonfile@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-4.0.0.tgz#8771aae0799b64076b76640fca058f9c10e33ecb" + optionalDependencies: + graceful-fs "^4.1.6" + +jsonify@~0.0.0: + version "0.0.0" + resolved "https://registry.yarnpkg.com/jsonify/-/jsonify-0.0.0.tgz#2c74b6ee41d93ca51b7b5aaee8f503631d252a73" + +jsprim@^1.2.2: + version "1.4.1" + resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.1.tgz#313e66bc1e5cc06e438bc1b7499c2e5c56acb6a2" + dependencies: + assert-plus "1.0.0" + extsprintf "1.3.0" + json-schema "0.2.3" + verror "1.10.0" + +keytar@^4.0.4: + version "4.0.4" + resolved "https://registry.yarnpkg.com/keytar/-/keytar-4.0.4.tgz#59a306f448a1c6a309cd68cb29129095a8c8b1db" + dependencies: + nan "2.5.1" + +klaw@^1.0.0: + version "1.3.1" + resolved "https://registry.yarnpkg.com/klaw/-/klaw-1.3.1.tgz#4088433b46b3b1ba259d78785d8e96f73ba02439" + optionalDependencies: + graceful-fs "^4.1.9" + +latest-version@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/latest-version/-/latest-version-3.1.0.tgz#a205383fea322b33b5ae3b18abee0dc2f356ee15" + dependencies: + package-json "^4.0.0" + +lazy-val@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/lazy-val/-/lazy-val-1.0.2.tgz#d9b07fb1fce54cbc99b3c611de431b83249369b6" + +lcid@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/lcid/-/lcid-1.0.0.tgz#308accafa0bc483a3867b4b6f2b9506251d1b835" + dependencies: + invert-kv "^1.0.0" + +load-json-file@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-1.1.0.tgz#956905708d58b4bab4c2261b04f59f31c99374c0" + dependencies: + graceful-fs "^4.1.2" + parse-json "^2.2.0" + pify "^2.0.0" + pinkie-promise "^2.0.0" + strip-bom "^2.0.0" + +load-json-file@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-2.0.0.tgz#7947e42149af80d696cbf797bcaabcfe1fe29ca8" + dependencies: + graceful-fs "^4.1.2" + parse-json "^2.2.0" + pify "^2.0.0" + strip-bom "^3.0.0" + +loader-utils@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-1.1.0.tgz#c98aef488bcceda2ffb5e2de646d6a754429f5cd" + dependencies: + big.js "^3.1.3" + emojis-list "^2.0.0" + json5 "^0.5.0" + +locate-path@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-2.0.0.tgz#2b568b265eec944c6d9c0de9c3dbbbca0354cd8e" + dependencies: + p-locate "^2.0.0" + path-exists "^3.0.0" + +lodash.isequal@^4.5.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/lodash.isequal/-/lodash.isequal-4.5.0.tgz#415c4478f2bcc30120c22ce10ed3226f7d3e18e0" + +lodash.toarray@^4.4.0: + version "4.4.0" + resolved "https://registry.yarnpkg.com/lodash.toarray/-/lodash.toarray-4.4.0.tgz#24c4bfcd6b2fba38bfd0594db1179d8e9b656561" + +lodash@^4.14.0: + version "4.17.4" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.4.tgz#78203a4d1c328ae1d86dca6460e369b57f4055ae" + +loud-rejection@^1.0.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/loud-rejection/-/loud-rejection-1.6.0.tgz#5b46f80147edee578870f086d04821cf998e551f" + dependencies: + currently-unhandled "^0.4.1" + signal-exit "^3.0.0" + +lowercase-keys@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-1.0.0.tgz#4e3366b39e7f5457e35f1324bdf6f88d0bfc7306" + +lru-cache@^4.0.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-4.1.1.tgz#622e32e82488b49279114a4f9ecf45e7cd6bba55" + dependencies: + pseudomap "^1.0.2" + yallist "^2.1.2" + +make-dir@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-1.0.0.tgz#97a011751e91dd87cfadef58832ebb04936de978" + dependencies: + pify "^2.3.0" + +map-obj@^1.0.0, map-obj@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/map-obj/-/map-obj-1.0.1.tgz#d933ceb9205d82bdcf4886f6742bdc2b4dea146d" + +mem@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/mem/-/mem-1.1.0.tgz#5edd52b485ca1d900fe64895505399a0dfa45f76" + dependencies: + mimic-fn "^1.0.0" + +meow@^3.1.0: + version "3.7.0" + resolved "https://registry.yarnpkg.com/meow/-/meow-3.7.0.tgz#72cb668b425228290abbfa856892587308a801fb" + dependencies: + camelcase-keys "^2.0.0" + decamelize "^1.1.2" + loud-rejection "^1.0.0" + map-obj "^1.0.1" + minimist "^1.1.3" + normalize-package-data "^2.3.4" + object-assign "^4.0.1" + read-pkg-up "^1.0.1" + redent "^1.0.0" + trim-newlines "^1.0.0" + +mime-db@~1.29.0: + version "1.29.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.29.0.tgz#48d26d235589651704ac5916ca06001914266878" + +mime-types@^2.1.12, mime-types@~2.1.7: + version "2.1.16" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.16.tgz#2b858a52e5ecd516db897ac2be87487830698e23" + dependencies: + mime-db "~1.29.0" + +mime@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/mime/-/mime-2.0.3.tgz#4353337854747c48ea498330dc034f9f4bbbcc0b" + +mimic-fn@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-1.1.0.tgz#e667783d92e89dbd342818b5230b9d62a672ad18" + +minimatch@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" + dependencies: + brace-expansion "^1.1.7" + +minimist@0.0.8: + version "0.0.8" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d" + +minimist@^1.1.0, minimist@^1.1.3, minimist@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284" + +mkdirp@0.5.0: + version "0.5.0" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.0.tgz#1d73076a6df986cd9344e15e71fcc05a4c9abf12" + dependencies: + minimist "0.0.8" + +ms@0.7.1: + version "0.7.1" + resolved "https://registry.yarnpkg.com/ms/-/ms-0.7.1.tgz#9cd13c03adbff25b65effde7ce864ee952017098" + +ms@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" + +nan@2.5.1: + version "2.5.1" + resolved "https://registry.yarnpkg.com/nan/-/nan-2.5.1.tgz#d5b01691253326a97a2bbee9e61c55d8d60351e2" + +nan@^2.0.0: + version "2.7.0" + resolved "https://registry.yarnpkg.com/nan/-/nan-2.7.0.tgz#d95bf721ec877e08db276ed3fc6eb78f9083ad46" + +node-emoji@^1.8.1: + version "1.8.1" + resolved "https://registry.yarnpkg.com/node-emoji/-/node-emoji-1.8.1.tgz#6eec6bfb07421e2148c75c6bba72421f8530a826" + dependencies: + lodash.toarray "^4.4.0" + +normalize-package-data@^2.3.2, normalize-package-data@^2.3.4, normalize-package-data@^2.4.0: + version "2.4.0" + resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.4.0.tgz#12f95a307d58352075a04907b84ac8be98ac012f" + dependencies: + hosted-git-info "^2.1.4" + is-builtin-module "^1.0.0" + semver "2 || 3 || 4 || 5" + validate-npm-package-license "^3.0.1" + +npm-run-path@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-2.0.2.tgz#35a9232dfa35d7067b4cb2ddf2357b1871536c5f" + dependencies: + path-key "^2.0.0" + +nugget@^2.0.0, nugget@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/nugget/-/nugget-2.0.1.tgz#201095a487e1ad36081b3432fa3cada4f8d071b0" + dependencies: + debug "^2.1.3" + minimist "^1.1.0" + pretty-bytes "^1.0.2" + progress-stream "^1.1.0" + request "^2.45.0" + single-line-log "^1.1.2" + throttleit "0.0.2" + +number-is-nan@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/number-is-nan/-/number-is-nan-1.0.1.tgz#097b602b53422a522c1afb8790318336941a011d" + +oauth-sign@~0.8.1: + version "0.8.2" + resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.8.2.tgz#46a6ab7f0aead8deae9ec0565780b7d4efeb9d43" + +object-assign@^4.0.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" + +object-keys@~0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-0.4.0.tgz#28a6aae7428dd2c3a92f3d95f21335dd204e0336" + +once@^1.3.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" + dependencies: + wrappy "1" + +os-locale@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/os-locale/-/os-locale-2.1.0.tgz#42bc2900a6b5b8bd17376c8e882b65afccf24bf2" + dependencies: + execa "^0.7.0" + lcid "^1.0.0" + mem "^1.1.0" + +p-finally@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/p-finally/-/p-finally-1.0.0.tgz#3fbcfb15b899a44123b34b6dcc18b724336a2cae" + +p-limit@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-1.1.0.tgz#b07ff2d9a5d88bec806035895a2bab66a27988bc" + +p-locate@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-2.0.0.tgz#20a0103b222a70c8fd39cc2e580680f3dde5ec43" + dependencies: + p-limit "^1.1.0" + +package-json@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/package-json/-/package-json-4.0.1.tgz#8869a0401253661c4c4ca3da6c2121ed555f5eed" + dependencies: + got "^6.7.1" + registry-auth-token "^3.0.1" + registry-url "^3.0.3" + semver "^5.1.0" + +parse-color@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/parse-color/-/parse-color-1.0.0.tgz#7b748b95a83f03f16a94f535e52d7f3d94658619" + dependencies: + color-convert "~0.5.0" + +parse-json@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-2.2.0.tgz#f480f40434ef80741f8469099f8dea18f55a4dc9" + dependencies: + error-ex "^1.2.0" + +path-exists@^2.0.0, path-exists@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-2.1.0.tgz#0feb6c64f0fc518d9a754dd5efb62c7022761f4b" + dependencies: + pinkie-promise "^2.0.0" + +path-exists@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-3.0.0.tgz#ce0ebeaa5f78cb18925ea7d810d7b59b010fd515" + +path-is-absolute@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" + +path-key@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/path-key/-/path-key-2.0.1.tgz#411cadb574c5a140d3a4b1910d40d80cc9f40b40" + +path-type@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/path-type/-/path-type-1.1.0.tgz#59c44f7ee491da704da415da5a4070ba4f8fe441" + dependencies: + graceful-fs "^4.1.2" + pify "^2.0.0" + pinkie-promise "^2.0.0" + +path-type@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/path-type/-/path-type-2.0.0.tgz#f012ccb8415b7096fc2daa1054c3d72389594c73" + dependencies: + pify "^2.0.0" + +pend@~1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/pend/-/pend-1.2.0.tgz#7a57eb550a6783f9115331fcf4663d5c8e007a50" + +performance-now@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-0.2.0.tgz#33ef30c5c77d4ea21c5a53869d91b56d8f2555e5" + +pify@^2.0.0, pify@^2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c" + +pinkie-promise@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/pinkie-promise/-/pinkie-promise-2.0.1.tgz#2135d6dfa7a358c069ac9b178776288228450ffa" + dependencies: + pinkie "^2.0.0" + +pinkie@^2.0.0: + version "2.0.4" + resolved "https://registry.yarnpkg.com/pinkie/-/pinkie-2.0.4.tgz#72556b80cfa0d48a974e80e77248e80ed4f7f870" + +plist@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/plist/-/plist-2.1.0.tgz#57ccdb7a0821df21831217a3cad54e3e146a1025" + dependencies: + base64-js "1.2.0" + xmlbuilder "8.2.2" + xmldom "0.1.x" + +prepend-http@^1.0.1: + version "1.0.4" + resolved "https://registry.yarnpkg.com/prepend-http/-/prepend-http-1.0.4.tgz#d4f4562b0ce3696e41ac52d0e002e57a635dc6dc" + +pretty-bytes@^1.0.2: + version "1.0.4" + resolved "https://registry.yarnpkg.com/pretty-bytes/-/pretty-bytes-1.0.4.tgz#0a22e8210609ad35542f8c8d5d2159aff0751c84" + dependencies: + get-stdin "^4.0.1" + meow "^3.1.0" + +process-nextick-args@~1.0.6: + version "1.0.7" + resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-1.0.7.tgz#150e20b756590ad3f91093f25a4f2ad8bff30ba3" + +progress-stream@^1.1.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/progress-stream/-/progress-stream-1.2.0.tgz#2cd3cfea33ba3a89c9c121ec3347abe9ab125f77" + dependencies: + speedometer "~0.1.2" + through2 "~0.2.3" + +pseudomap@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/pseudomap/-/pseudomap-1.0.2.tgz#f052a28da70e618917ef0a8ac34c1ae5a68286b3" + +punycode@^1.4.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e" + +qs@~6.4.0: + version "6.4.0" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.4.0.tgz#13e26d28ad6b0ffaa91312cd3bf708ed351e7233" + +rc@^1.0.1, rc@^1.1.2, rc@^1.1.6, rc@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.1.tgz#2e03e8e42ee450b8cb3dce65be1bf8974e1dfd95" + dependencies: + deep-extend "~0.4.0" + ini "~1.3.0" + minimist "^1.2.0" + strip-json-comments "~2.0.1" + +read-config-file@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/read-config-file/-/read-config-file-1.1.1.tgz#26b3d38a3ccbdacae9c168dc479828e68878a5d6" + dependencies: + ajv "^5.2.2" + ajv-keywords "^2.1.0" + bluebird-lst "^1.0.3" + fs-extra-p "^4.4.0" + js-yaml "^3.10.0" + json5 "^0.5.1" + lazy-val "^1.0.2" + +read-pkg-up@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-1.0.1.tgz#9d63c13276c065918d57f002a57f40a1b643fb02" + dependencies: + find-up "^1.0.0" + read-pkg "^1.0.0" + +read-pkg-up@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-2.0.0.tgz#6b72a8048984e0c41e79510fd5e9fa99b3b549be" + dependencies: + find-up "^2.0.0" + read-pkg "^2.0.0" + +read-pkg@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-1.1.0.tgz#f5ffaa5ecd29cb31c0474bca7d756b6bb29e3f28" + dependencies: + load-json-file "^1.0.0" + normalize-package-data "^2.3.2" + path-type "^1.0.0" + +read-pkg@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-2.0.0.tgz#8ef1c0623c6a6db0dc6713c4bfac46332b2368f8" + dependencies: + load-json-file "^2.0.0" + normalize-package-data "^2.3.2" + path-type "^2.0.0" + +readable-stream@^2.2.2: + version "2.3.3" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.3.tgz#368f2512d79f9d46fdfc71349ae7878bbc1eb95c" + dependencies: + core-util-is "~1.0.0" + inherits "~2.0.3" + isarray "~1.0.0" + process-nextick-args "~1.0.6" + safe-buffer "~5.1.1" + string_decoder "~1.0.3" + util-deprecate "~1.0.1" + +readable-stream@~1.1.9: + version "1.1.14" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.1.14.tgz#7cf4c54ef648e3813084c636dd2079e166c081d9" + dependencies: + core-util-is "~1.0.0" + inherits "~2.0.1" + isarray "0.0.1" + string_decoder "~0.10.x" + +redent@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/redent/-/redent-1.0.0.tgz#cf916ab1fd5f1f16dfb20822dd6ec7f730c2afde" + dependencies: + indent-string "^2.1.0" + strip-indent "^1.0.1" + +registry-auth-token@^3.0.1: + version "3.3.1" + resolved "https://registry.yarnpkg.com/registry-auth-token/-/registry-auth-token-3.3.1.tgz#fb0d3289ee0d9ada2cbb52af5dfe66cb070d3006" + dependencies: + rc "^1.1.6" + safe-buffer "^5.0.1" + +registry-url@^3.0.3: + version "3.1.0" + resolved "https://registry.yarnpkg.com/registry-url/-/registry-url-3.1.0.tgz#3d4ef870f73dde1d77f0cf9a381432444e174942" + dependencies: + rc "^1.0.1" + +repeating@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/repeating/-/repeating-2.0.1.tgz#5214c53a926d3552707527fbab415dbc08d06dda" + dependencies: + is-finite "^1.0.0" + +request@^2.45.0: + version "2.81.0" + resolved "https://registry.yarnpkg.com/request/-/request-2.81.0.tgz#c6928946a0e06c5f8d6f8a9333469ffda46298a0" + dependencies: + aws-sign2 "~0.6.0" + aws4 "^1.2.1" + caseless "~0.12.0" + combined-stream "~1.0.5" + extend "~3.0.0" + forever-agent "~0.6.1" + form-data "~2.1.1" + har-validator "~4.2.1" + hawk "~3.1.3" + http-signature "~1.1.0" + is-typedarray "~1.0.0" + isstream "~0.1.2" + json-stringify-safe "~5.0.1" + mime-types "~2.1.7" + oauth-sign "~0.8.1" + performance-now "^0.2.0" + qs "~6.4.0" + safe-buffer "^5.0.1" + stringstream "~0.0.4" + tough-cookie "~2.3.0" + tunnel-agent "^0.6.0" + uuid "^3.0.0" + +require-directory@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" + +require-main-filename@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-1.0.1.tgz#97f717b69d48784f5f526a6c5aa8ffdda055a4d1" + +rimraf@^2.2.8: + version "2.6.1" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.1.tgz#c2338ec643df7a1b7fe5c54fa86f57428a55f33d" + dependencies: + glob "^7.0.5" + +safe-buffer@^5.0.1, safe-buffer@~5.1.0, safe-buffer@~5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.1.tgz#893312af69b2123def71f57889001671eeb2c853" + +sanitize-filename@^1.6.1: + version "1.6.1" + resolved "https://registry.yarnpkg.com/sanitize-filename/-/sanitize-filename-1.6.1.tgz#612da1c96473fa02dccda92dcd5b4ab164a6772a" + dependencies: + truncate-utf8-bytes "^1.0.0" + +sax@^1.2.1: + version "1.2.4" + resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9" + +schema-utils@^0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-0.3.0.tgz#f5877222ce3e931edae039f17eb3716e7137f8cf" + dependencies: + ajv "^5.0.0" + +semver-diff@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/semver-diff/-/semver-diff-2.1.0.tgz#4bbb8437c8d37e4b0cf1a68fd726ec6d645d6d36" + dependencies: + semver "^5.0.3" + +"semver@2 || 3 || 4 || 5", semver@^5.0.3, semver@^5.1.0, semver@^5.3.0, semver@^5.4.1: + version "5.4.1" + resolved "https://registry.yarnpkg.com/semver/-/semver-5.4.1.tgz#e059c09d8571f0540823733433505d3a2f00b18e" + +set-blocking@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" + +shebang-command@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea" + dependencies: + shebang-regex "^1.0.0" + +shebang-regex@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-1.0.0.tgz#da42f49740c0b42db2ca9728571cb190c98efea3" + +signal-exit@^3.0.0, signal-exit@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.2.tgz#b5fdc08f1287ea1178628e415e25132b73646c6d" + +single-line-log@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/single-line-log/-/single-line-log-1.1.2.tgz#c2f83f273a3e1a16edb0995661da0ed5ef033364" + dependencies: + string-width "^1.0.1" + +sntp@1.x.x: + version "1.0.9" + resolved "https://registry.yarnpkg.com/sntp/-/sntp-1.0.9.tgz#6541184cc90aeea6c6e7b35e2659082443c66198" + dependencies: + hoek "2.x.x" + +source-list-map@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/source-list-map/-/source-list-map-2.0.0.tgz#aaa47403f7b245a92fbc97ea08f250d6087ed085" + +source-map-support@^0.4.18: + version "0.4.18" + resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.4.18.tgz#0286a6de8be42641338594e97ccea75f0a2c585f" + dependencies: + source-map "^0.5.6" + +source-map@^0.5.6, source-map@~0.5.3: + version "0.5.6" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.6.tgz#75ce38f52bf0733c5a7f0c118d81334a2bb5f412" + +spdx-correct@~1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-1.0.2.tgz#4b3073d933ff51f3912f03ac5519498a4150db40" + dependencies: + spdx-license-ids "^1.0.2" + +spdx-expression-parse@~1.0.0: + version "1.0.4" + resolved "https://registry.yarnpkg.com/spdx-expression-parse/-/spdx-expression-parse-1.0.4.tgz#9bdf2f20e1f40ed447fbe273266191fced51626c" + +spdx-license-ids@^1.0.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-1.2.2.tgz#c9df7a3424594ade6bd11900d596696dc06bac57" + +speedometer@~0.1.2: + version "0.1.4" + resolved "https://registry.yarnpkg.com/speedometer/-/speedometer-0.1.4.tgz#9876dbd2a169d3115402d48e6ea6329c8816a50d" + +spellchecker@^3.4.3: + version "3.4.3" + resolved "https://registry.yarnpkg.com/spellchecker/-/spellchecker-3.4.3.tgz#1c4dead6da6f3654888d34ae62e57fbe53dcdd60" + dependencies: + any-promise "^1.3.0" + nan "^2.0.0" + +sprintf-js@~1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" + +sshpk@^1.7.0: + version "1.13.1" + resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.13.1.tgz#512df6da6287144316dc4c18fe1cf1d940739be3" + dependencies: + asn1 "~0.2.3" + assert-plus "^1.0.0" + dashdash "^1.12.0" + getpass "^0.1.1" + optionalDependencies: + bcrypt-pbkdf "^1.0.0" + ecc-jsbn "~0.1.1" + jsbn "~0.1.0" + tweetnacl "~0.14.0" + +stat-mode@^0.2.2: + version "0.2.2" + resolved "https://registry.yarnpkg.com/stat-mode/-/stat-mode-0.2.2.tgz#e6c80b623123d7d80cf132ce538f346289072502" + +string-width@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-1.0.2.tgz#118bdf5b8cdc51a2a7e70d211e07e2b0b9b107d3" + dependencies: + code-point-at "^1.0.0" + is-fullwidth-code-point "^1.0.0" + strip-ansi "^3.0.0" + +string-width@^2.0.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-2.1.1.tgz#ab93f27a8dc13d28cac815c462143a6d9012ae9e" + dependencies: + is-fullwidth-code-point "^2.0.0" + strip-ansi "^4.0.0" + +string_decoder@~0.10.x: + version "0.10.31" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-0.10.31.tgz#62e203bc41766c6c28c9fc84301dab1c5310fa94" + +string_decoder@~1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.0.3.tgz#0fc67d7c141825de94282dd536bec6b9bce860ab" + dependencies: + safe-buffer "~5.1.0" + +stringstream@~0.0.4: + version "0.0.5" + resolved "https://registry.yarnpkg.com/stringstream/-/stringstream-0.0.5.tgz#4e484cd4de5a0bbbee18e46307710a8a81621878" + +strip-ansi@^3.0.0, strip-ansi@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-3.0.1.tgz#6a385fb8853d952d5ff05d0e8aaf94278dc63dcf" + dependencies: + ansi-regex "^2.0.0" + +strip-ansi@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-4.0.0.tgz#a8479022eb1ac368a871389b635262c505ee368f" + dependencies: + ansi-regex "^3.0.0" + +strip-bom@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-2.0.0.tgz#6219a85616520491f35788bdbf1447a99c7e6b0e" + dependencies: + is-utf8 "^0.2.0" + +strip-bom@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3" + +strip-eof@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/strip-eof/-/strip-eof-1.0.0.tgz#bb43ff5598a6eb05d89b59fcd129c983313606bf" + +strip-indent@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/strip-indent/-/strip-indent-1.0.1.tgz#0c7962a6adefa7bbd4ac366460a638552ae1a0a2" + dependencies: + get-stdin "^4.0.1" + +strip-json-comments@~2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a" + +sumchecker@^1.2.0: + version "1.3.1" + resolved "https://registry.yarnpkg.com/sumchecker/-/sumchecker-1.3.1.tgz#79bb3b4456dd04f18ebdbc0d703a1d1daec5105d" + dependencies: + debug "^2.2.0" + es6-promise "^4.0.5" + +sumchecker@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/sumchecker/-/sumchecker-2.0.2.tgz#0f42c10e5d05da5d42eea3e56c3399a37d6c5b3e" + dependencies: + debug "^2.2.0" + +supports-color@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-2.0.0.tgz#535d045ce6b6363fa40117084629995e9df324c7" + +supports-color@^4.0.0: + version "4.4.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-4.4.0.tgz#883f7ddabc165142b2a61427f3352ded195d1a3e" + dependencies: + has-flag "^2.0.0" + +temp-file@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/temp-file/-/temp-file-2.0.3.tgz#0de2540629fc77a6406ca56f50214d1f224947ac" + dependencies: + async-exit-hook "^2.0.1" + bluebird-lst "^1.0.3" + fs-extra-p "^4.4.0" + lazy-val "^1.0.2" + +term-size@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/term-size/-/term-size-1.2.0.tgz#458b83887f288fc56d6fffbfad262e26638efa69" + dependencies: + execa "^0.7.0" + +throttleit@0.0.2: + version "0.0.2" + resolved "https://registry.yarnpkg.com/throttleit/-/throttleit-0.0.2.tgz#cfedf88e60c00dd9697b61fdd2a8343a9b680eaf" + +through2@~0.2.3: + version "0.2.3" + resolved "https://registry.yarnpkg.com/through2/-/through2-0.2.3.tgz#eb3284da4ea311b6cc8ace3653748a52abf25a3f" + dependencies: + readable-stream "~1.1.9" + xtend "~2.1.1" + +timed-out@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/timed-out/-/timed-out-4.0.1.tgz#f32eacac5a175bea25d7fab565ab3ed8741ef56f" + +tough-cookie@~2.3.0: + version "2.3.2" + resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.3.2.tgz#f081f76e4c85720e6c37a5faced737150d84072a" + dependencies: + punycode "^1.4.1" + +trim-newlines@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/trim-newlines/-/trim-newlines-1.0.0.tgz#5887966bb582a4503a41eb524f7d35011815a613" + +truncate-utf8-bytes@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/truncate-utf8-bytes/-/truncate-utf8-bytes-1.0.2.tgz#405923909592d56f78a5818434b0b78489ca5f2b" + dependencies: + utf8-byte-length "^1.0.1" + +tunnel-agent@^0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.6.0.tgz#27a5dea06b36b04a0a9966774b290868f0fc40fd" + dependencies: + safe-buffer "^5.0.1" + +tweetnacl@^0.14.3, tweetnacl@~0.14.0: + version "0.14.5" + resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64" + +typedarray@^0.0.6: + version "0.0.6" + resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" + +unique-string@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/unique-string/-/unique-string-1.0.0.tgz#9e1057cca851abb93398f8b33ae187b99caec11a" + dependencies: + crypto-random-string "^1.0.0" + +universalify@^0.1.0: + version "0.1.1" + resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.1.tgz#fa71badd4437af4c148841e3b3b165f9e9e590b7" + +unzip-response@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/unzip-response/-/unzip-response-2.0.1.tgz#d2f0f737d16b0615e72a6935ed04214572d56f97" + +update-notifier@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/update-notifier/-/update-notifier-2.2.0.tgz#1b5837cf90c0736d88627732b661c138f86de72f" + dependencies: + boxen "^1.0.0" + chalk "^1.0.0" + configstore "^3.0.0" + import-lazy "^2.1.0" + is-npm "^1.0.0" + latest-version "^3.0.0" + semver-diff "^2.0.0" + xdg-basedir "^3.0.0" + +url-parse-lax@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/url-parse-lax/-/url-parse-lax-1.0.0.tgz#7af8f303645e9bd79a272e7a14ac68bc0609da73" + dependencies: + prepend-http "^1.0.1" + +utf8-byte-length@^1.0.1: + version "1.0.4" + resolved "https://registry.yarnpkg.com/utf8-byte-length/-/utf8-byte-length-1.0.4.tgz#f45f150c4c66eee968186505ab93fcbb8ad6bf61" + +util-deprecate@~1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" + +uuid@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.1.0.tgz#3dd3d3e790abc24d7b0d3a034ffababe28ebbc04" + +validate-npm-package-license@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.1.tgz#2804babe712ad3379459acfbe24746ab2c303fbc" + dependencies: + spdx-correct "~1.0.0" + spdx-expression-parse "~1.0.0" + +verror@1.10.0: + version "1.10.0" + resolved "https://registry.yarnpkg.com/verror/-/verror-1.10.0.tgz#3a105ca17053af55d6e270c1f8288682e18da400" + dependencies: + assert-plus "^1.0.0" + core-util-is "1.0.2" + extsprintf "^1.2.0" + +webpack-sources@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-1.0.1.tgz#c7356436a4d13123be2e2426a05d1dad9cbe65cf" + dependencies: + source-list-map "^2.0.0" + source-map "~0.5.3" + +which-module@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a" + +which@^1.2.9: + version "1.3.0" + resolved "https://registry.yarnpkg.com/which/-/which-1.3.0.tgz#ff04bdfc010ee547d780bec38e1ac1c2777d253a" + dependencies: + isexe "^2.0.0" + +widest-line@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/widest-line/-/widest-line-1.0.0.tgz#0c09c85c2a94683d0d7eaf8ee097d564bf0e105c" + dependencies: + string-width "^1.0.1" + +wrap-ansi@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-2.1.0.tgz#d8fc3d284dd05794fe84973caecdd1cf824fdd85" + dependencies: + string-width "^1.0.1" + strip-ansi "^3.0.1" + +wrappy@1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" + +write-file-atomic@^2.0.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/write-file-atomic/-/write-file-atomic-2.3.0.tgz#1ff61575c2e2a4e8e510d6fa4e243cce183999ab" + dependencies: + graceful-fs "^4.1.11" + imurmurhash "^0.1.4" + signal-exit "^3.0.2" + +xdg-basedir@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/xdg-basedir/-/xdg-basedir-3.0.0.tgz#496b2cc109eca8dbacfe2dc72b603c17c5870ad4" + +xelement@^1.0.16: + version "1.0.16" + resolved "https://registry.yarnpkg.com/xelement/-/xelement-1.0.16.tgz#900bb46c20fc2dffadff778a9d2dc36699d0ff7e" + dependencies: + sax "^1.2.1" + +xmlbuilder@8.2.2: + version "8.2.2" + resolved "https://registry.yarnpkg.com/xmlbuilder/-/xmlbuilder-8.2.2.tgz#69248673410b4ba42e1a6136551d2922335aa773" + +xmldom@0.1.x: + version "0.1.27" + resolved "https://registry.yarnpkg.com/xmldom/-/xmldom-0.1.27.tgz#d501f97b3bdb403af8ef9ecc20573187aadac0e9" + +xtend@~2.1.1: + version "2.1.2" + resolved "https://registry.yarnpkg.com/xtend/-/xtend-2.1.2.tgz#6efecc2a4dad8e6962c4901b337ce7ba87b5d28b" + dependencies: + object-keys "~0.4.0" + +y18n@^3.2.1: + version "3.2.1" + resolved "https://registry.yarnpkg.com/y18n/-/y18n-3.2.1.tgz#6d15fba884c08679c0d77e88e7759e811e07fa41" + +yallist@^2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/yallist/-/yallist-2.1.2.tgz#1c11f9218f076089a47dd512f93c6699a6a81d52" + +yargs-parser@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-7.0.0.tgz#8d0ac42f16ea55debd332caf4c4038b3e3f5dfd9" + dependencies: + camelcase "^4.1.0" + +yargs@^9.0.1: + version "9.0.1" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-9.0.1.tgz#52acc23feecac34042078ee78c0c007f5085db4c" + dependencies: + camelcase "^4.1.0" + cliui "^3.2.0" + decamelize "^1.1.1" + get-caller-file "^1.0.1" + os-locale "^2.0.0" + read-pkg-up "^2.0.0" + require-directory "^2.1.1" + require-main-filename "^1.0.1" + set-blocking "^2.0.0" + string-width "^2.0.0" + which-module "^2.0.0" + y18n "^3.2.1" + yargs-parser "^7.0.0" + +yauzl@2.4.1: + version "2.4.1" + resolved "https://registry.yarnpkg.com/yauzl/-/yauzl-2.4.1.tgz#9528f442dab1b2284e58b4379bb194e22e0c4005" + dependencies: + fd-slicer "~1.0.1" diff --git a/export-loader.js b/export-loader.js new file mode 100644 index 0000000..b6d9bf9 --- /dev/null +++ b/export-loader.js @@ -0,0 +1,15 @@ +const fs = require('fs'); +module.exports = function(source) { + fs.writeFileSync(this.resourcePath + '.ts', source); + return source; +}; + +module.exports.delayTypecheck = function() { + let callback; + this.plugin('fork-ts-checker-service-before-start', (c) => callback = c); + this.plugin('after-compile', (compilation, c) => { + if(compilation.compiler.parentCompilation) return c(); + callback(); + c(); + }); +}; \ No newline at end of file diff --git a/fchat/channels.ts b/fchat/channels.ts new file mode 100644 index 0000000..4880f70 --- /dev/null +++ b/fchat/channels.ts @@ -0,0 +1,247 @@ +import {decodeHTML} from './common'; +import {Channel as Interfaces, Character, Connection} from './interfaces'; + +export function queuedJoin(this: void, channels: string[]): void { + const timer: NodeJS.Timer = setInterval(() => { + const channel = channels.shift(); + if(channel === undefined) return clearInterval(timer); + state.join(channel); + }, 100); +} + +function sortMember(this: void | never, array: Interfaces.Member[], member: Interfaces.Member): void { + const name = member.character.name; + let i = 0; + for(; i < array.length; ++i) { + const other = array[i]; + if(other.character.isChatOp && !member.character.isChatOp) continue; + if(member.character.isChatOp && !other.character.isChatOp) break; + if(other.rank > member.rank) continue; + if(member.rank > other.rank) break; + if(name < other.character.name) break; + } + array.splice(i, 0, member); +} + +class Channel implements Interfaces.Channel { + description = ''; + opList: string[]; + owner = ''; + mode: Interfaces.Mode = 'both'; + members: {[key: string]: {character: Character, rank: Interfaces.Rank} | undefined} = {}; + sortedMembers: Interfaces.Member[] = []; + + constructor(readonly id: string, readonly name: string) { + } + + addMember(member: Interfaces.Member): void { + this.members[member.character.name] = member; + sortMember(this.sortedMembers, member); + } + + removeMember(name: string): void { + const member = this.members[name]; + if(member !== undefined) { + delete this.members[name]; + this.sortedMembers.splice(this.sortedMembers.indexOf(member), 1); + } + } + + reSortMember(member: Interfaces.Member): void { + this.sortedMembers.splice(this.sortedMembers.indexOf(member), 1); + sortMember(this.sortedMembers, member); + } + + createMember(character: Character): {character: Character, rank: Interfaces.Rank} { + return { + character, + rank: this.owner === character.name ? Interfaces.Rank.Owner : + this.opList.indexOf(character.name) !== -1 ? Interfaces.Rank.Op : Interfaces.Rank.Member + }; + } +} + +class ListItem implements Interfaces.ListItem { + isJoined = false; + + constructor(readonly id: string, readonly name: string, public memberCount: number) { + } +} + +class State implements Interfaces.State { + officialChannels: {readonly [key: string]: ListItem | undefined} = {}; + openRooms: {readonly [key: string]: ListItem | undefined} = {}; + joinedChannels: Channel[] = []; + handlers: Interfaces.EventHandler[] = []; + joinedKeys: {[key: string]: number | undefined} = {}; + + constructor(private connection: Connection) { + } + + join(channel: string): void { + this.connection.send('JCH', {channel}); + } + + leave(channel: string): void { + this.connection.send('LCH', {channel}); + } + + addChannel(channel: Channel): void { + this.joinedKeys[channel.id] = this.joinedChannels.length; + this.joinedChannels.push(channel); + for(const handler of this.handlers) handler('join', channel); + } + + removeChannel(channel: Channel): void { + this.joinedChannels.splice(this.joinedKeys[channel.id]!, 1); + delete this.joinedKeys[channel.id]; + for(const handler of this.handlers) handler('leave', channel); + } + + getChannelItem(id: string): ListItem | undefined { + id = id.toLowerCase(); + return (id.substr(0, 4) === 'adh-' ? this.openRooms : this.officialChannels)[id]; + } + + onEvent(handler: Interfaces.EventHandler): void { + this.handlers.push(handler); + } + + getChannel(id: string): Channel | undefined { + const key = this.joinedKeys[id.toLowerCase()]; + return key !== undefined ? this.joinedChannels[key] : undefined; + } +} + +let state: State; + +export default function(this: void, connection: Connection, characters: Character.State): Interfaces.State { + state = new State(connection); + let getChannelTimer: NodeJS.Timer | undefined; + connection.onEvent('connecting', () => { + state.joinedChannels = []; + state.joinedKeys = {}; + }); + connection.onEvent('connected', (isReconnect) => { + if(isReconnect) queuedJoin(Object.keys(state.joinedChannels)); + const getChannels = () => { + connection.send('CHA'); + connection.send('ORS'); + }; + getChannels(); + if(getChannelTimer !== undefined) clearInterval(getChannelTimer); + getChannelTimer = setInterval(getChannels, 60000); + }); + + connection.onMessage('CHA', (data) => { + const channels: {[key: string]: ListItem} = {}; + for(const channel of data.channels) { + const id = channel.name.toLowerCase(); + const item = new ListItem(id, channel.name, channel.characters); + if(state.joinedKeys[id] !== undefined) item.isJoined = true; + channels[id] = item; + } + state.officialChannels = channels; + }); + connection.onMessage('ORS', (data) => { + const channels: {[key: string]: ListItem} = {}; + for(const channel of data.channels) { + const id = channel.name.toLowerCase(); + const item = new ListItem(id, decodeHTML(channel.title), channel.characters); + if(state.joinedKeys[id] !== undefined) item.isJoined = true; + channels[id] = item; + } + state.openRooms = channels; + }); + connection.onMessage('JCH', (data) => { + const item = state.getChannelItem(data.channel); + if(data.character.identity === connection.character) { + state.addChannel(new Channel(data.channel.toLowerCase(), decodeHTML(data.title))); + if(item !== undefined) item.isJoined = true; + } else { + const channel = state.getChannel(data.channel)!; + channel.addMember(channel.createMember(characters.get(data.character.identity))); + if(item !== undefined) item.memberCount++; + } + }); + connection.onMessage('ICH', (data) => { + const channel = state.getChannel(data.channel)!; + channel.mode = data.mode; + const members: {[key: string]: Interfaces.Member} = {}; + const sorted: Interfaces.Member[] = []; + for(const user of data.users) { + const name = user.identity; + const member = channel.createMember(characters.get(name)); + members[name] = member; + sortMember(sorted, member); + } + channel.members = members; + channel.sortedMembers = sorted; + const item = state.getChannelItem(data.channel); + if(item !== undefined) item.memberCount = data.users.length; + }); + connection.onMessage('CDS', (data) => state.getChannel(data.channel)!.description = decodeHTML(data.description)); + connection.onMessage('LCH', (data) => { + const channel = state.getChannel(data.channel); + if(channel === undefined) return; + const item = state.getChannelItem(data.channel); + if(data.character === connection.character) { + state.removeChannel(channel); + if(item !== undefined) item.isJoined = false; + } else { + channel.removeMember(data.character); + if(item !== undefined) item.memberCount--; + } + }); + connection.onMessage('COA', (data) => { + const channel = state.getChannel(data.channel)!; + channel.opList.push(data.character); + const member = channel.members[data.character]; + if(member === undefined || member.rank === Interfaces.Rank.Owner) return; + member.rank = Interfaces.Rank.Op; + channel.reSortMember(member); + }); + connection.onMessage('COL', (data) => { + const channel = state.getChannel(data.channel)!; + channel.owner = data.oplist[0]; + channel.opList = data.oplist.slice(1); + }); + connection.onMessage('COR', (data) => { + const channel = state.getChannel(data.channel)!; + channel.opList.splice(channel.opList.indexOf(data.character), 1); + const member = channel.members[data.character]; + if(member === undefined || member.rank === Interfaces.Rank.Owner) return; + member.rank = Interfaces.Rank.Member; + channel.reSortMember(member); + }); + connection.onMessage('CSO', (data) => { + const channel = state.getChannel(data.channel)!; + const oldOwner = channel.members[channel.owner]; + if(oldOwner !== undefined) { + oldOwner.rank = Interfaces.Rank.Member; + channel.reSortMember(oldOwner); + } + channel.owner = data.character; + const newOwner = channel.members[data.character]; + if(newOwner !== undefined) { + newOwner.rank = Interfaces.Rank.Owner; + channel.reSortMember(newOwner); + } + }); + connection.onMessage('RMO', (data) => state.getChannel(data.channel)!.mode = data.mode); + connection.onMessage('FLN', (data) => { + for(const key in state.joinedKeys) + state.getChannel(key)!.removeMember(data.character); + }); + const globalHandler = (data: Connection.ServerCommands['AOP'] | Connection.ServerCommands['DOP']) => { + //tslint:disable-next-line:forin + for(const key in state.joinedKeys) { + const channel = state.getChannel(key)!; + const member = channel.members[data.character]; + if(member !== undefined) channel.reSortMember(member); + } + }; + connection.onMessage('AOP', globalHandler); + connection.onMessage('DOP', globalHandler); + return state; +} \ No newline at end of file diff --git a/fchat/characters.ts b/fchat/characters.ts new file mode 100644 index 0000000..f69a9f6 --- /dev/null +++ b/fchat/characters.ts @@ -0,0 +1,150 @@ +import {decodeHTML} from './common'; +import {Character as Interfaces, Connection} from './interfaces'; + +class Character implements Interfaces.Character { + gender: Interfaces.Gender; + status: Interfaces.Status = 'offline'; + statusText = ''; + isFriend = false; + isBookmarked = false; + isChatOp = false; + isIgnored = false; + + constructor(readonly name: string) { + } +} + +class State implements Interfaces.State { + characters: {[key: string]: Character | undefined} = {}; + ownCharacter: Character = undefined; /*tslint:disable-line:no-any*///hack + friends: Character[] = []; + bookmarks: Character[] = []; + ignoreList: string[] = []; + opList: string[] = []; + friendList: string[] = []; + bookmarkList: string[] = []; + + get(name: string): Character { + const key = name.toLowerCase(); + let char = this.characters[key]; + if(char === undefined) { + char = new Character(name); + char.isFriend = this.friendList.indexOf(name) !== -1; + char.isBookmarked = this.bookmarkList.indexOf(name) !== -1; + char.isChatOp = this.opList.indexOf(name) !== -1; + char.isIgnored = this.ignoreList.indexOf(key) !== -1; + this.characters[key] = char; + } + return char; + } + + setStatus(character: Character, status: Interfaces.Status, text: string): void { + if(character.status === 'offline' && status !== 'offline') { + if(character.isFriend) this.friends.push(character); + if(character.isBookmarked) this.bookmarks.push(character); + } else if(status === 'offline' && character.status !== 'offline') { + if(character.isFriend) this.friends.splice(this.friends.indexOf(character), 1); + if(character.isBookmarked) this.bookmarks.splice(this.bookmarks.indexOf(character), 1); + } + character.status = status; + character.statusText = decodeHTML(text); + } +} + +let state: State; + +export default function(this: void, connection: Connection): Interfaces.State { + state = new State(); + let reconnectStatus: Connection.ClientCommands['STA']; + connection.onEvent('connecting', async(isReconnect) => { + state.friends = []; + state.bookmarks = []; + state.bookmarkList = (<{characters: string[]}>await connection.queryApi('bookmark-list.php')).characters; + state.friendList = ((<{friends: {source: string, dest: string, last_online: number}[]}>await connection.queryApi('friend-list.php')) + .friends).map((x) => x.dest); + //tslint:disable-next-line:forin + for(const key in state.characters) { + const character = state.characters[key]!; + character.isFriend = state.friendList.indexOf(character.name) !== -1; + character.isBookmarked = state.bookmarkList.indexOf(character.name) !== -1; + character.status = 'offline'; + character.statusText = ''; + } + if(isReconnect && (state.ownCharacter) !== undefined) + reconnectStatus = {status: state.ownCharacter.status, statusmsg: state.ownCharacter.statusText}; + }); + connection.onEvent('connected', async(isReconnect) => { + if(!isReconnect) return; + connection.send('STA', reconnectStatus); + //tslint:disable-next-line:forin + for(const key in state.characters) { + const char = state.characters[key]!; + char.isIgnored = state.ignoreList.indexOf(key) !== -1; + char.isChatOp = state.opList.indexOf(char.name) !== -1; + } + }); + connection.onMessage('IGN', (data) => { + switch(data.action) { + case 'init': + state.ignoreList = data.characters.slice(); + break; + case 'add': + state.ignoreList.push(data.character.toLowerCase()); + state.get(data.character).isIgnored = true; + break; + case 'delete': + state.ignoreList.splice(state.ignoreList.indexOf(data.character.toLowerCase()), 1); + state.get(data.character).isIgnored = false; + } + }); + connection.onMessage('ADL', (data) => state.opList = data.ops.slice()); + connection.onMessage('LIS', (data) => { + for(const char of data.characters) { + const character = state.get(char[0]); + character.gender = char[1]; + state.setStatus(character, char[2], char[3]); + } + }); + connection.onMessage('FLN', (data) => { + state.setStatus(state.get(data.character), 'offline', ''); + }); + connection.onMessage('NLN', (data) => { + const character = state.get(data.identity); + if(data.identity === connection.character) state.ownCharacter = character; + character.gender = data.gender; + state.setStatus(character, data.status, ''); + }); + connection.onMessage('STA', (data) => { + state.setStatus(state.get(data.character), data.status, data.statusmsg); + }); + connection.onMessage('AOP', (data) => { + state.opList.push(data.character); + const char = state.get(data.character); + char.isChatOp = true; + }); + connection.onMessage('DOP', (data) => { + state.opList.splice(state.opList.indexOf(data.character), 1); + const char = state.get(data.character); + char.isChatOp = false; + }); + connection.onMessage('RTB', (data) => { + switch(data.type) { + case 'trackadd': + state.bookmarkList.push(data.name); + state.get(data.name).isBookmarked = true; + break; + case 'trackrem': + state.bookmarkList.splice(state.bookmarkList.indexOf(data.name), 1); + state.get(data.name).isBookmarked = false; + break; + case 'friendadd': + state.friendList.push(data.name); + state.get(data.name).isFriend = true; + break; + case 'friendremove': + state.friendList.splice(state.friendList.indexOf(data.name), 1); + state.get(data.name).isFriend = false; + } + }); + return state; +} \ No newline at end of file diff --git a/fchat/common.ts b/fchat/common.ts new file mode 100644 index 0000000..5912951 --- /dev/null +++ b/fchat/common.ts @@ -0,0 +1,5 @@ +const ltRegex = /</gi, gtRegex = />/gi, ampRegex = /&/gi; + +export function decodeHTML(this: void | never, str: string): string { + return str.replace(ltRegex, '<').replace(gtRegex, '>').replace(ampRegex, '&'); +} \ No newline at end of file diff --git a/fchat/connection.ts b/fchat/connection.ts new file mode 100644 index 0000000..bf00043 --- /dev/null +++ b/fchat/connection.ts @@ -0,0 +1,160 @@ +import Axios, {AxiosResponse} from 'axios'; +import * as qs from 'qs'; +import {Connection as Interfaces, WebSocketConnection} from './interfaces'; + +const fatalErrors = [2, 3, 4, 9, 30, 31, 33, 39, 40, 62, -4]; +const dieErrors = [9, 30, 31, 39]; + +async function queryApi(this: void, endpoint: string, data: object): Promise { + return Axios.post(`https://www.f-list.net/json/api/${endpoint}`, qs.stringify(data)); +} + +export default class Connection implements Interfaces.Connection { + character: string; + vars: Interfaces.Vars & {[key: string]: string} = {}; //tslint:disable-line:no-any + protected socket: WebSocketConnection | undefined = undefined; + private messageHandlers: {[key in keyof Interfaces.ServerCommands]?: Interfaces.CommandHandler[]} = {}; + private connectionHandlers: {[key in Interfaces.EventType]?: Interfaces.EventHandler[]} = {}; + private errorHandlers: ((error: Error) => void)[] = []; + private ticket: string; + private cleanClose = false; + private reconnectTimer: NodeJS.Timer; + private ticketProvider: Interfaces.TicketProvider; + private reconnectDelay = 0; + + constructor(private readonly socketProvider: new() => WebSocketConnection, private readonly account: string, + ticketProvider: Interfaces.TicketProvider | string) { + this.ticketProvider = typeof ticketProvider === 'string' ? async() => this.getTicket(ticketProvider) : ticketProvider; + } + + async connect(character: string): Promise { + this.cleanClose = false; + const isReconnect = this.character === character; + this.character = character; + this.ticket = await this.ticketProvider(); + await this.invokeHandlers('connecting', isReconnect); + const socket = this.socket = new this.socketProvider(); + socket.onOpen(() => { + this.send('IDN', { + account: this.account, + character: this.character, + cname: 'F-Chat', + cversion: '3.0', + method: 'ticket', + ticket: this.ticket + }); + }); + socket.onMessage((msg: string) => { + const type = msg.substr(0, 3); + const data = msg.length > 6 ? JSON.parse(msg.substr(4)) : undefined; + this.handleMessage(type, data); + }); + socket.onClose(async() => { + if(!this.cleanClose) { + setTimeout(async() => this.connect(this.character), this.reconnectDelay); + this.reconnectDelay = this.reconnectDelay >= 30000 ? 60000 : this.reconnectDelay >= 10000 ? 30000 : 10000; + } + this.socket = undefined; + await this.invokeHandlers('closed', !this.cleanClose); + }); + socket.onError((error: Error) => { + for(const handler of this.errorHandlers) handler(error); + }); + } + + close(): void { + clearTimeout(this.reconnectTimer); + this.cleanClose = true; + if(this.socket !== undefined) this.socket.close(); + } + + async queryApi(endpoint: string, data?: {account?: string, ticket?: string}): Promise { + if(data === undefined) data = {}; + data.account = this.account; + data.ticket = this.ticket; + let res = <{error: string}>(await queryApi(endpoint, data)).data; + if(res.error === 'Invalid ticket.' || res.error === 'Your login ticket has expired (five minutes) or no ticket requested.') { + data.ticket = this.ticket = await this.ticketProvider(); + res = <{error: string}>(await queryApi(endpoint, data)).data; + } + if(res.error !== '') throw new Error(res.error); + return res; + } + + onError(handler: (error: Error) => void): void { + this.errorHandlers.push(handler); + } + + onEvent(type: Interfaces.EventType, handler: Interfaces.EventHandler): void { + let handlers = this.connectionHandlers[type]; + if(handlers === undefined) handlers = this.connectionHandlers[type] = []; + handlers.push(handler); + } + + offEvent(type: Interfaces.EventType, handler: Interfaces.EventHandler): void { + const handlers = this.connectionHandlers[type]; + if(handlers === undefined) return; + handlers.splice(handlers.indexOf(handler), 1); + } + + onMessage(type: K, handler: Interfaces.CommandHandler): void { + let handlers: (Interfaces.CommandHandler[] | undefined) = this.messageHandlers[type]; + if(handlers === undefined) handlers = this.messageHandlers[type] = []; + handlers.push(handler); + } + + offMessage(type: K, handler: Interfaces.CommandHandler): void { + const handlers: (Interfaces.CommandHandler[] | undefined) = this.messageHandlers[type]; + if(handlers === undefined) return; + handlers.splice(handlers.indexOf(handler), 1); + } + + send(command: K, data?: Interfaces.ClientCommands[K]): void { + if(this.socket !== undefined) + this.socket.send(command + (data !== undefined ? ` ${JSON.stringify(data)}` : '')); + } + + //tslint:disable:no-unsafe-any no-any + protected handleMessage(type: T, data: any): void { + switch(type) { + case 'VAR': + this.vars[data.variable] = data.value; + break; + case 'PIN': + this.send('PIN'); + break; + case 'ERR': + if(fatalErrors.indexOf(data.number) !== -1) { + const error = new Error(data.message); + for(const handler of this.errorHandlers) handler(error); + if(dieErrors.indexOf(data.number) !== -1) this.close(); + else this.socket!.close(); + } + break; + case 'NLN': + if(data.identity === this.character) { + this.invokeHandlers('connected', this.reconnectDelay !== 0); //tslint:disable-line:no-floating-promises + this.reconnectDelay = 0; + } + } + const time = new Date(); + const handlers: Interfaces.CommandHandler[] | undefined = this.messageHandlers[type]; + if(handlers !== undefined) + for(const handler of handlers) handler(data, time); + } + + //tslint:enable + + private async getTicket(password: string): Promise { + const data = <{ticket?: string, error: string}>(await Axios.post('https://www.f-list.net/json/getApiTicket.php', qs.stringify( + {account: this.account, password, no_friends: true, no_bookmarks: true, no_characters: true}))).data; + if(data.ticket !== undefined) return data.ticket; + throw new Error(data.error); + } + + private async invokeHandlers(type: Interfaces.EventType, isReconnect: boolean): Promise { + const handlers = this.connectionHandlers[type]; + if(handlers === undefined) return; + for(const handler of handlers) await handler(isReconnect); + } +} \ No newline at end of file diff --git a/fchat/index.ts b/fchat/index.ts new file mode 100644 index 0000000..b36c46e --- /dev/null +++ b/fchat/index.ts @@ -0,0 +1,5 @@ +export {default as Characters} from './characters'; +export {default as Channels} from './channels'; +export {default as ChatConnection} from './connection'; +export {Connection, Character, Channel, WebSocketConnection} from './interfaces'; +export {decodeHTML} from './common'; \ No newline at end of file diff --git a/fchat/interfaces.ts b/fchat/interfaces.ts new file mode 100644 index 0000000..70a7803 --- /dev/null +++ b/fchat/interfaces.ts @@ -0,0 +1,238 @@ +//tslint:disable:no-shadowed-variable +export namespace Connection { + export type ClientCommands = { + ACB: {character: string}, + AOP: {character: string}, + BRO: {message: string}, + CBL: {channel: string}, + CBU: {character: string, channel: string}, + CCR: {channel: string}, + CDS: {channel: string, description: string}, + CHA: undefined, + CIU: {channel: string, character: string}, + CKU: {channel: string, character: string}, + COA: {channel: string, character: string}, + COL: {channel: string}, + COR: {channel: string, character: string}, + CRC: {channel: string}, + CSO: {character: string, channel: string}, + CTU: {channel: string, character: string, length: number}, + CUB: {channel: string, character: string}, + DOP: {character: string}, + FKS: { + kinks: ReadonlyArray, genders?: ReadonlyArray, orientations?: ReadonlyArray, + languages?: ReadonlyArray, furryprefs?: ReadonlyArray, roles?: ReadonlyArray + }, + FRL: undefined + IDN: {method: 'ticket', account: string, ticket: string, character: string, cname: string, cversion: string}, + IGN: {action: 'add' | 'delete' | 'notify', character: string} | {action: 'list'}, + JCH: {channel: string}, + KIC: {channel: string}, + KIK: {character: string}, + KIN: {character: string}, + LCH: {channel: string}, + LRP: {channel: string, message: string}, + MSG: {channel: string, message: string}, + ORS: undefined, + PCR: undefined, + PIN: undefined, + PRI: {recipient: string, message: string}, + PRO: {character: string}, + RLD: {save: string} | undefined, + RLL: {channel: string, dice: 'bottle' | string} | {recipient: string, dice: 'bottle' | string}, + RMO: {channel: string, mode: Channel.Mode}, + RST: {channel: string, status: 'public' | 'private'}, + RWD: {character: string}, + SFC: {action: 'report', report: string, tab?: string, logid: number} | {action: 'confirm', callid: number}, + STA: {status: Character.Status, statusmsg: string}, + TMO: {character: string, time: number, reason: string}, + TPN: {character: string, status: Character.TypingStatus}, + UNB: {character: string}, + UPT: undefined, + ZZZ: {command: string, arg: string} + }; + + export type ServerCommands = { + ADL: {ops: ReadonlyArray}, + AOP: {character: string}, + BRO: {message: string, character: string}, + CBU: {operator: string, channel: string, character: string}, + CDS: {channel: string, description: string}, + CHA: {channels: ReadonlyArray<{name: string, mode: Channel.Mode, characters: number}>}, + CIU: {sender: string, title: string, name: string}, + CKU: {operator: string, channel: string, character: string}, + COA: {character: string, channel: string}, + COL: {channel: string, oplist: ReadonlyArray}, + CON: {count: number}, + COR: {character: string, channel: string}, + CSO: {character: string, channel: string}, + CTU: {operator: string, channel: string, length: number, character: string}, + DOP: {character: string}, + ERR: {number: number, message: string}, + FKS: {characters: ReadonlyArray, kinks: ReadonlyArray}, + FLN: {character: string}, + FRL: {characters: ReadonlyArray}, + HLO: {message: string}, + ICH: {users: ReadonlyArray<{identity: string}>, channel: string, mode: Channel.Mode}, + IDN: {character: string}, + IGN: {action: 'add' | 'delete', character: string} | {action: 'list' | 'init', characters: ReadonlyArray} + JCH: {channel: string, character: {identity: string}, title: string}, + KID: {type: 'start' | 'end', message: string} | {type: 'custom', key: number, value: number}, + LCH: {channel: string, character: string}, + LIS: {characters: ReadonlyArray<[string, Character.Gender, Character.Status, string]>}, + LRP: {character: string, message: string, channel: string}, + MSG: {character: string, message: string, channel: string}, + NLN: {identity: string, gender: Character.Gender, status: 'online'}, + ORS: {channels: ReadonlyArray<{name: string, title: string, characters: number}>}, + PIN: undefined, + PRD: {type: 'start' | 'end', message: string} | {type: 'info' | 'select', key: string, value: string}, + PRI: {character: string, message: string}, + RLL: { + type: 'dice', results: ReadonlyArray, message: string, rolls: ReadonlyArray, + character: string, endresult: number, channel: string + } | { + type: 'dice', results: ReadonlyArray, message: string, rolls: ReadonlyArray, + character: string, endresult: number, recipient: string + } | + {type: 'bottle', message: string, character: string, target: string, channel: string} | + {type: 'bottle', message: string, character: string, target: string, recipient: string}, + RMO: {mode: Channel.Mode, channel: string}, + RTB: { + type: 'comment', target_type: 'newspost' | 'bugreport' | 'changelog' | 'feature', + id: number, target_id: number, parent_id: number, name: string, target: string + } | {type: 'note', sender: string, subject: string, id: number} | { + type: 'grouprequest' | 'bugreport' | 'helpdeskticket' | 'helpdeskreply' | 'featurerequest', + name: string, id: number, title?: string + } | {type: 'trackadd' | 'trackrem' | 'friendadd' | 'friendremove' | 'friendrequest', name: string}, + SFC: {action: 'confirm', moderator: string, character: string, timestamp: string, tab: string, logid: number} | + {callid: number, action: 'report', report: string, timestamp: string, character: string, tab: string, logid: number}, + STA: {status: Character.Status, character: string, statusmsg: string}, + SYS: {message: string, channel?: string}, + TPN: {character: string, status: Character.TypingStatus}, + UPT: {time: number, starttime: number, startstring: string, accepted: number, channels: number, users: number, maxusers: number}, + VAR: {variable: string, value: number | ReadonlyArray} + ZZZ: {message: string} + }; + + export type CommandHandler = (data: ServerCommands[T], date: Date) => void; + export type TicketProvider = () => Promise; + export type EventType = 'connecting' | 'connected' | 'closed'; + export type EventHandler = (isReconnect: boolean) => Promise | void; + + export interface Vars { + readonly chat_max: number + readonly priv_max: number + readonly lfrp_max: number + //readonly cds_max: number + readonly lfrp_flood: number + readonly msg_flood: number + //readonly sta_flood: number + readonly permissions: number + readonly icon_blacklist: ReadonlyArray + } + + export interface Connection { + readonly character: string + readonly vars: Vars + connect(character: string): void + close(): void + onMessage(type: K, handler: CommandHandler): void + offMessage(type: K, handler: CommandHandler): void + onEvent(type: EventType, handler: EventHandler): void + offEvent(type: EventType, handler: EventHandler): void + onError(handler: (error: Error) => void): void + send(type: 'CHA' | 'FRL' | 'ORS' | 'PCR' | 'PIN' | 'UPT'): void + send(type: K, data: ClientCommands[K]): void + queryApi(endpoint: string, data?: object): Promise + } +} +export type Connection = Connection.Connection; + +export namespace Character { + export type Gender = 'None' | 'Male' | 'Female' | 'Shemale' | 'Herm' | 'Male-Herm' | 'Cunt-boy' | 'Transgender'; + export type Status = 'offline' | 'online' | 'away' | 'idle' | 'looking' | 'busy' | 'dnd' | 'crown'; + export type TypingStatus = 'typing' | 'paused' | 'clear'; + + export interface State { + readonly ownCharacter: Character + readonly friends: ReadonlyArray + readonly bookmarks: ReadonlyArray + readonly ignoreList: ReadonlyArray + readonly opList: ReadonlyArray + readonly friendList: ReadonlyArray + readonly bookmarkList: ReadonlyArray + + get(name: string): Character + } + + export interface Character { + readonly name: string + readonly gender: Gender | undefined + readonly status: Status + readonly statusText: string + readonly isFriend: boolean + readonly isBookmarked: boolean + readonly isChatOp: boolean + readonly isIgnored: boolean + } +} + +export type Character = Character.Character; + +export namespace Channel { + export type EventHandler = (type: 'join' | 'leave', channel: Channel) => void; + + export interface State { + readonly officialChannels: {readonly [key: string]: (ListItem | undefined)}; + readonly openRooms: {readonly [key: string]: (ListItem | undefined)}; + readonly joinedChannels: ReadonlyArray; + + join(name: string): void; + leave(name: string): void; + onEvent(handler: EventHandler): void + getChannelItem(id: string): ListItem | undefined + getChannel(id: string): Channel | undefined + } + + export const enum Rank { + Member, + Op, + Owner + } + + export type Mode = 'chat' | 'ads' | 'both'; + + export interface Member { + readonly character: Character, + readonly rank: Rank + } + + export interface ListItem { + readonly id: string; + readonly name: string; + readonly memberCount: number; + readonly isJoined: boolean; + } + + export interface Channel { + readonly id: string; + readonly name: string; + readonly description: string; + readonly mode: Mode; + readonly members: {readonly [key: string]: Member | undefined}; + readonly sortedMembers: ReadonlyArray; + readonly opList: ReadonlyArray; + readonly owner: string; + } +} + +export type Channel = Channel.Channel; + +export interface WebSocketConnection { + close(): void + onMessage(handler: (message: string) => void): void + onOpen(handler: () => void): void + onClose(handler: () => void): void + onError(handler: (error: Error) => void): void + send(message: string): void +} \ No newline at end of file diff --git a/less/bbcode.less b/less/bbcode.less new file mode 100644 index 0000000..bdf7394 --- /dev/null +++ b/less/bbcode.less @@ -0,0 +1,122 @@ +.redText { + color: @red-color; +} + +.blueText { + color: @blue-color; +} + +.greenText { + color: @green-color; +} + +.yellowText { + color: @yellow-color; +} + +.cyanText { + color: @cyan-color; +} + +.purpleText { + color: @purple-color; +} + +.brownText { + color: @brown-color; +} + +.pinkText { + color: @pink-color; +} + +.grayText { + color: @gray-color; +} + +.orangeText { + color: @orange-color; +} + +.whiteText { + color: @white-color; +} + +.blackColor { + color: @black-color; +} + +/* Tweak these to be consistent with how bootstrap does sizing. */ +span.bigText { + font-size: 1.4em; +} + +span.smallText { + font-size: 0.8em; +} + +span.leftText { + display: block; + text-align: left; +} + +span.centerText { + display: block; + text-align: center; +} + +span.rightText { + display: block; + text-align: right; +} + +span.justifyText { + display: block; + text-align: justify; +} + +span.indentText { + padding-left: 3em; +} + +.characterAvatarIcon { + display: inline; + height: 50px; + width: 50px; +} + +.collapseHeaderText { + font-weight: bold; + cursor: pointer; + width: 100%; + min-height: @line-height-computed; +} + +.collapseHeader { + .well; + padding: 5px; + border-color: @collapse-border; + background-color: @collapse-header-bg; +} + +.collapseBlock { + max-height: 0; + margin-left: 0.5em; + transition: max-height 0.2s; + overflow-y: hidden; +} + +.styledText, .bbcode { + max-width: 100%; + word-wrap: break-word; + a { + text-decoration: underline; + &:hover { + text-decoration: none; + } + } +} + +.link-domain { + color: @gray-light; +} diff --git a/less/bbcode_editor.less b/less/bbcode_editor.less new file mode 100644 index 0000000..617a17c --- /dev/null +++ b/less/bbcode_editor.less @@ -0,0 +1,15 @@ +.bbcodeEditorButton { + .btn-default(); + padding: (@padding-base-vertical/2.0) (@padding-base-horizontal/2.0); +} + +.bbcodeTextAreaTextArea { + textarea& { + min-height: 150px; + } +} + +.bbcodePreviewWarnings { + .alert(); + .alert-danger(); +} diff --git a/less/character_editor.less b/less/character_editor.less new file mode 100644 index 0000000..37bff8a --- /dev/null +++ b/less/character_editor.less @@ -0,0 +1,78 @@ +.bbcodeTextArea { + max-width: 100%; + min-height: 200px; +} + +.kinkChoice.selected { + font-weight: bold; +} + +.characterEditorSidebar { + position: fixed; +} + +.characterList.characterListSelected { + border-width: 2px; + border-color: @characterListSelectedColor; +} + +// Character image editor. +.characterImage { + width: 250px; + height: 300px; + border-radius: 25px; + overflow: hidden; + border: 2px #111 solid; + display: inline-block; + margin-left: 10px; +} + +.characterImage.characterImageSelected { + border-color: @characterListSelectedColor; +} + +.characterImagePreview { + width: 200px; + height: 200px; + float: left; + background-size: contain; + background-repeat: no-repeat; +} + +.characterImageActions { + width: 46px; + float: right; + padding-top: 10px; + text-align: center; +} + +.characterImageActions a { + width: 30px; + height: 30px; + display: inline-block; + padding-bottom: 15px; +} + +.characterImage a img { + width: 100%; + height: 100%; +} + +.characterImageDescription { + width: 100%; + height: 100px; + clear: both; + box-sizing: border-box; + padding: 10px; + position: relative; + overflow-y: scroll; +} + +.kink-list-enter-active, .kink-list-leave-active { + transition: all 0.2s; +} + +.kink-list-enter, .kink-list-leave-to { + opacity: 0; + transform: translateX(100px); +} diff --git a/less/character_page.less b/less/character_page.less new file mode 100644 index 0000000..2f655c2 --- /dev/null +++ b/less/character_page.less @@ -0,0 +1,97 @@ +// Kinkes +.subkinkList.closed { + display: none; +} +.subkink { + cursor: pointer; +} + +.characterPageAvatar { + height: 100px; + width: 100px; +} + +// Inline images +.imageBlock { + max-width: 100%; + height: auto; +} + +// Quick Compare +.stockKink.quickCompareActive { + border: 1px solid @quickCompareActiveColor; +} +.stockKink.quickCompareFave { + background-color: @quickCompareFaveColor; +} +.stockKink.quickCompareYes { + background-color: @quickCompareYesColor; +} +.stockKink.quickCompareMaybe { + background-color: @quickCompareMaybeColor; +} +.stockKink.quickCompareNo { + background-color: @quickCompareNoColor; +} + +// Kink Group Highlighting +.highlightedKink { + font-weight: bolder; +} + +// Guestbook +.guestbookPager { + display: inline-block; + width: 50%; +} + +.characterSubTitle { + font-size: @font-size-small; + font-style: italic; +} + +.characterPageName { + font-size: @font-size-h3; + font-weight: bold; +} + +.characterImages { + .container-fluid(); +} + +.characterPageImage { + .col-xs-2(); + .img-thumbnail(); + border: none; + display: inline-block; + img { + .center-block(); + } +} + +.guestbook-post { + .row(); +} + +.guestbook-avatar { + width: 50px; + float: left; +} + +.guestbook-contents { + .well(); +} + +.guestbook-contents.deleted { + .alert-warning(); +} + +.guestbook-reply { + .guestbook-body { + :before { + content: "Reply: "; + } + } + .well(); + .alert-info(); +} diff --git a/less/chat.less b/less/chat.less new file mode 100644 index 0000000..ac4f388 --- /dev/null +++ b/less/chat.less @@ -0,0 +1,193 @@ +@import "~bootstrap/less/variables.less"; + +.bg-solid-text { + background: @text-background-color +} + +.link-preview { + background: @text-background-color; + border-top-right-radius: 2px; + bottom: 0; + left: 0; + max-width: 40%; + overflow-x: hidden; + padding: 0.2em 0.5em; + font-size: 12px; + position: fixed; + text-overflow: ellipsis; + white-space: nowrap; + z-index: 100000; + + &.right { + left: auto; + right: 0; + border-top-left-radius: 2px; + border-top-right-radius: 0; + } +} + +.has-new { + background-color: @state-danger-bg !important; +} + +.overlay-disable { + position: absolute; + opacity: 0.8; + height: 100%; + width: 100%; + display: flex; + justify-content: center; + align-items: center; + z-index: 1; + background: #ddd; + color: #000; +} + +.sidebar { + position: absolute; + top: 0; + bottom: 0; + background: @body-bg; + z-index: 10; + + .body { + display: none; + } + + .expander { + display: block; + position: absolute; + padding: 5px 6px; + border-color: @btn-default-border; + border-top-right-radius: 0; + border-top-left-radius: 0; + @media(min-width: @screen-sm-min) { + .name { + display: none; + } + + &:hover .name { + display: inline; + } + } + } + + &.sidebar-left { + border-right: solid 1px @panel-default-border; + left: 0; + + .expander { + transform: rotate(270deg) translate3d(0, 0, 0); + transform-origin: 100% 0; + right: 0; + } + } + + &.sidebar-right { + border-left: solid 1px @panel-default-border; + right: 0; + + .expander { + transform: rotate(90deg) translate3d(0, 0, 0); + transform-origin: 0 0; + } + } +} + +.sidebar-fixed() { + position: static; + .body { + display: block; + } + .expander { + display: none; + } +} + +.chat-text-box { + min-height: initial !important; + max-height: 250px; + resize: none; +} + +.border-top { + border-top: solid 1px @panel-default-border; +} + +.message { + word-wrap: break-word; + word-break: break-word; +} + +.message-block { + padding: 1px 0; + &:not(:last-child) { + border-bottom: solid 1px @panel-default-border; + } +} + +.message-warn { + background-color: @state-danger-bg; + color: @state-danger-text; +} + +.messages-both { + .message-ad { + background-color: @state-info-bg; + } +} + +.message-event { + color: @gray-light; +} + +.message-highlight { + background-color: @state-success-bg; +} + +.message-action .bbcode { + font-style: italic; + i, em { + font-style: normal; + } +} + +.last-read { + border-bottom: solid 2px @state-success-bg !important; +} + +.fa.active { + color: @brand-success; +} + +.gender-shemale { + color: #CC66FF; +} + +.gender-herm { + color: #9B30FF; +} + +.gender-none { + color: @gray; +} + +.gender-female { + color: #FF6699; +} + +.gender-male { + color: #6699FF; +} + +.gender-male-herm { + color: #007FFF; +} + +.gender-transgender { + color: #EE8822; +} + +.gender-cunt-boy { + color: #00CC66; +} \ No newline at end of file diff --git a/less/comments.less b/less/comments.less new file mode 100644 index 0000000..f43605c --- /dev/null +++ b/less/comments.less @@ -0,0 +1,14 @@ +@comment-grid-columns: 50; +.comment-offset-1 { + margin-left: percentage((1 / @comment-grid-columns)); +} + +.comment-well { + .well(); + margin-bottom: 0px; +} + +.comment-well.warning { + background-color: @state-warning-bg; + border-color: @state-warning-border; +} diff --git a/less/core.less b/less/core.less new file mode 100644 index 0000000..1b357ba --- /dev/null +++ b/less/core.less @@ -0,0 +1,41 @@ +.flash-messages-fixed { + top: 0px; + left: auto; + right: auto; + width: 100%; + height: auto; + position: fixed; + z-index: 900000; +} + +.flash-message { + .alert(); + border-bottom-color: rgba(0, 0, 0, 0.3); + margin-bottom: 0; +} + +.flash-message-enter-active, .flash-message-leave-active { + transition: all 0.2s; +} + +.flash-message-enter, .flash-message-leave-to { + opacity: 0; + transform: translateX(100px); +} + +.character-menu-item { + width: 250px; + .character-link { + display: inline-block; + padding-right: 0; + } + .character-edit-link { + margin-left: auto; + padding: 3px 20px 2px 0; + display: inline-block; + } +} + +.sidebar-top-padded { + margin-top: 20px; +} \ No newline at end of file diff --git a/less/eicons_editor.less b/less/eicons_editor.less new file mode 100644 index 0000000..6ad36e0 --- /dev/null +++ b/less/eicons_editor.less @@ -0,0 +1,45 @@ +// User icon editor. +.characterImageIcon { + width: 150px; + height: 200px; + border-radius: 25px; + overflow: hidden; + border: 2px #111 solid; + display: inline-block; + margin-left: 10px; +} + +.characterImagePreviewIcon { + width: 100px; + height: 100px; + float: left; + background-size: contain; + background-repeat: no-repeat; +} + +.characterImageActionsIcon { + width: 46px; + float: right; + padding-top: 10px; + text-align: center; +} + +.characterImageActionsIcon a { + width: 30px; + height: 30px; + display: inline-block; + padding-bottom: 15px; +} + +.characterImageIcon a img { + width: 100%; + height: 100%; +} + +.characterImageDescriptionIcon { + width: 100%; + clear: both; + box-sizing: border-box; + padding: 10px; + position: relative; +} diff --git a/less/flist_overrides.less b/less/flist_overrides.less new file mode 100644 index 0000000..2b24cc0 --- /dev/null +++ b/less/flist_overrides.less @@ -0,0 +1,19 @@ +hr { + margin-top: 5px; + margin-bottom: 5px; +} + +.modal-dialog.modal-wide { + width: 95%; +} + +.panel-title { + font-weight: bold; +} + +// Fix weird style where this is overwritten and cannot be styled inside a well. +.well { + blockquote { + border-color: @blockquote-border-color; + } +} \ No newline at end of file diff --git a/less/flist_variables.less b/less/flist_variables.less new file mode 100644 index 0000000..045c793 --- /dev/null +++ b/less/flist_variables.less @@ -0,0 +1,35 @@ +@red-color: #f00; +@green-color: #0f0; +@blue-color: #00f; +@yellow-color: #ff0; +@cyan-color: #0ff; +@purple-color: #f0f; +@white-color: #fff; +@black-color: #000; +@brown-color: #8a6d3b; +@pink-color: #faa; +@gray-color: #cccc; +@orange-color: #f60; +@collapse-border: darken(@well-border, 25%); + +@quickCompareActiveColor: @black-color; +@quickCompareFaveColor: @brand-success; +@quickCompareYesColor: @brand-info; +@quickCompareMaybeColor: @brand-warning; +@quickCompareNoColor: @brand-danger; + +@characterListSelectedColor: @brand-success; + +@note-conversation-you-bg: @alert-info-bg; +@note-conversation-you-text: @alert-info-text; +@note-conversation-you-border: @alert-info-border; +@note-conversation-them-bg: @well-bg; +@note-conversation-them-text: @text-color; +@note-conversation-them-border: @well-border; + +@nav-link-hover-color: @link-color; + +@collapse-header-bg: @well-bg; + +@text-background-color: @body-bg; +@text-background-color-disabled: @gray-lighter; diff --git a/less/kink_editor.less b/less/kink_editor.less new file mode 100644 index 0000000..ffb6062 --- /dev/null +++ b/less/kink_editor.less @@ -0,0 +1,4 @@ +.kink-editor-panel { + position: fixed; + width: 55%; +} diff --git a/less/notes.less b/less/notes.less new file mode 100644 index 0000000..113ccfb --- /dev/null +++ b/less/notes.less @@ -0,0 +1,48 @@ +.note-folder { + clear: both; + width: 100%; + .note-folder-total, .note-folder-unread { + width: 3.5rem; + display: inline-block; + float: left; + text-align: center; + } + .note-folder-unread { + font-weight: bold; + float: right; + } + .note-folder-name { + width: auto; + display: inline-block; + float: left; + } + .note-folder-icon { + display: inline-block; + float: right; + } +} + +.note-folder-create { +} + +.conversation-from-me, .conversation-from-them { + margin-bottom: 5px; + max-width: percentage(((@grid-columns - 4) / @grid-columns)); + word-wrap: break-word; +} + +.conversation-from-me { + margin-right: percentage(( 4 / @grid-columns )); + background-color: @note-conversation-you-bg; + color: @note-conversation-you-text; + border-color: @note-conversation-you-border; + //text-align: left; +} + +.conversation-from-them { + margin-left: percentage(( 4 / @grid-columns )); + background-color: @note-conversation-them-bg; + color: @note-conversation-them-text; + border-color: @note-conversation-them-border; + //text-align: right; +} diff --git a/less/tag_input.less b/less/tag_input.less new file mode 100644 index 0000000..d469d4f --- /dev/null +++ b/less/tag_input.less @@ -0,0 +1,50 @@ +.tag-input-control { + display: inline-block; + margin-bottom: 0; + vertical-align: middle; + height: @input-height-base; // Make inputs at least the height of their button counterpart (base line-height + padding + border) + width: 100%; + padding: @padding-base-vertical @padding-base-horizontal; + font-size: @font-size-base; + line-height: @line-height-base; + color: @input-color; + background-color: @input-bg; + background-image: none; // Reset unusual Firefox-on-Android default style; see https://github.com/necolas/normalize.css/issues/214 + border: 1px solid @input-border; + border-radius: @input-border-radius; // Note: This has no effect on s in CSS. + .box-shadow(inset 0 1px 1px rgba(0, 0, 0, .075)); + .transition(~"border-color ease-in-out .15s, box-shadow ease-in-out .15s"); + + .tag-input { + background-color: @input-bg; + border: none; + width: auto; + &:focus { + box-shadow: none; + outline: none; + } + } +} + +.form-inline .tag-input-control { + width: auto; +} + +.tag-error { + border: 1px solid @state-danger-border; + background-color: @state-danger-bg; + .tag-input { + text-color: @state-danger-text; + background-color: @state-danger-bg; + } +} + +.suggestion-important { + font-weight: bold !important; +} + +.suggestion-description { + display: block; + font-style: italic; + font-size: @font-size-small; +} \ No newline at end of file diff --git a/less/themes/chat/dark.less b/less/themes/chat/dark.less new file mode 100644 index 0000000..5bb8271 --- /dev/null +++ b/less/themes/chat/dark.less @@ -0,0 +1,40 @@ +@import "../variables/dark.less"; + +.nav-tabs > li > a:hover { + background-color: @gray-darker; +} + +.modal .nav-tabs > li.active > a { + background-color: @gray-dark; +} + +.message-own { + background-color: @gray-darker; +} + +// Apply variables to theme. +@import "../theme_base_chat.less"; + +* { + &::-webkit-scrollbar-track { + box-shadow: inset 0 0 8px @panel-default-border; + border-radius: 10px; + } + + &::-webkit-scrollbar { + width: 12px; + } + + &::-webkit-scrollbar-thumb { + border-radius: 10px; + box-shadow: inset 0 0 4px rgba(0, 0, 0, 0.8); + background-color: @gray-dark; + &:hover { + background-color: @gray; + } + + &:active { + background-color: @gray-light; + } + } +} \ No newline at end of file diff --git a/less/themes/chat/default.less b/less/themes/chat/default.less new file mode 100644 index 0000000..0725a16 --- /dev/null +++ b/less/themes/chat/default.less @@ -0,0 +1,36 @@ +@import "../variables/default.less"; + +.message-own { + background-color: @gray-lighter; +} + +.whiteText { + text-shadow: 1px 1px @gray; +} + +// Apply variables to theme. +@import "../theme_base_chat.less"; + +* { + &::-webkit-scrollbar-track { + box-shadow: inset 0 0 8px @gray; + border-radius: 10px; + } + + &::-webkit-scrollbar { + width: 12px; + } + + &::-webkit-scrollbar-thumb { + border-radius: 10px; + box-shadow: inset 0 0 4px rgba(0, 0, 0, 0.8); + background-color: @gray-lighter; + &:hover { + background-color: @gray-light; + } + + &:active { + background-color: @gray; + } + } +} \ No newline at end of file diff --git a/less/themes/chat/light.less b/less/themes/chat/light.less new file mode 100644 index 0000000..97de57b --- /dev/null +++ b/less/themes/chat/light.less @@ -0,0 +1,36 @@ +@import "../variables/light.less"; + +.message-own { + background-color: @gray-lighter; +} + +.whiteText { + text-shadow: 1px 1px @gray; +} + +// Apply variables to theme. +@import "../theme_base_chat.less"; + +* { + &::-webkit-scrollbar-track { + box-shadow: inset 0 0 8px @gray-light; + border-radius: 10px; + } + + &::-webkit-scrollbar { + width: 12px; + } + + &::-webkit-scrollbar-thumb { + border-radius: 10px; + box-shadow: inset 0 0 4px rgba(0, 0, 0, 0.8); + background-color: @gray-lighter; + &:hover { + background-color: @gray-light; + } + + &:active { + background-color: @gray; + } + } +} \ No newline at end of file diff --git a/less/themes/site/dark.less b/less/themes/site/dark.less new file mode 100644 index 0000000..b54fc25 --- /dev/null +++ b/less/themes/site/dark.less @@ -0,0 +1,4 @@ +@import "../variables/dark.less"; + +// Apply variables to theme. +@import "../theme_base.less"; diff --git a/less/themes/site/default.less b/less/themes/site/default.less new file mode 100644 index 0000000..c8b563d --- /dev/null +++ b/less/themes/site/default.less @@ -0,0 +1,4 @@ +@import "../variables/default.less"; + +// Apply variables to theme. +@import "../theme_base.less"; diff --git a/less/themes/site/light.less b/less/themes/site/light.less new file mode 100644 index 0000000..86c7dcf --- /dev/null +++ b/less/themes/site/light.less @@ -0,0 +1,4 @@ +@import "../variables/light.less"; + +// Apply variables to theme. +@import "../theme_base.less"; diff --git a/less/themes/theme_base.less b/less/themes/theme_base.less new file mode 100644 index 0000000..1efd7d7 --- /dev/null +++ b/less/themes/theme_base.less @@ -0,0 +1,64 @@ +/*! + * Bootstrap v3.3.5 (http://getbootstrap.com) + * Copyright 2011-2015 Twitter, Inc. + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + */ +// Core variables and mixins +//@import "variables.less"; // This file should be drawn in through a theme file and then overwritten. +@import "~bootstrap/less/mixins.less"; +// Reset and dependencies +@import "~bootstrap/less/normalize.less"; +//@import "print.less"; +//@import "glyphicons.less"; +// Core CSS +@import "~bootstrap/less/scaffolding.less"; +@import "~bootstrap/less/type.less"; +//@import "code.less"; +@import "~bootstrap/less/grid.less"; +@import "~bootstrap/less/tables.less"; +@import "~bootstrap/less/forms.less"; +@import "~bootstrap/less/buttons.less"; +// Components +@import "~bootstrap/less/component-animations.less"; +@import "~bootstrap/less/dropdowns.less"; +@import "~bootstrap/less/button-groups.less"; +//@import "input-groups.less"; +@import "~bootstrap/less/navs.less"; +@import "~bootstrap/less/navbar.less"; +//@import "breadcrumbs.less"; +@import "~bootstrap/less/pagination.less"; +@import "~bootstrap/less/pager.less"; +@import "~bootstrap/less/labels.less"; +@import "~bootstrap/less/badges.less"; +//@import "jumbotron.less"; +//@import "thumbnails.less"; +@import "~bootstrap/less/alerts.less"; +//@import "progress-bars.less"; +//@import "media.less"; +//@import "list-group.less"; +@import "~bootstrap/less/panels.less"; +//@import "responsive-embed.less"; +@import "~bootstrap/less/wells.less"; +@import "~bootstrap/less/close.less"; +// Components w/ JavaScript +@import "~bootstrap/less/modals.less"; +//@import "tooltip.less"; +//@import "popovers.less"; +//@import "carousel.less"; +// Utility classes +@import "~bootstrap/less/utilities.less"; +//@import "responsive-utilities.less"; +@import "~font-awesome/less/font-awesome.less"; +@import "../core.less"; +@import "../character_editor.less"; +@import "../character_page.less"; +@import "../eicons_editor.less"; +@import "../bbcode_editor.less"; +@import "../bbcode.less"; +@import "../comments.less"; +@import "../tickets.less"; +@import "../notes.less"; +@import "../threads.less"; +@import "../kink_editor.less"; +@import "../flist_overrides.less"; +@import "../tag_input.less"; diff --git a/less/themes/theme_base_chat.less b/less/themes/theme_base_chat.less new file mode 100644 index 0000000..00685bd --- /dev/null +++ b/less/themes/theme_base_chat.less @@ -0,0 +1,56 @@ +/*! + * Bootstrap v3.3.5 (http://getbootstrap.com) + * Copyright 2011-2015 Twitter, Inc. + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + */ +// Core variables and mixins +//@import "variables.less"; // This file should be drawn in through a theme file and then overwritten. +@import "~bootstrap/less/mixins.less"; +// Reset and dependencies +@import "~bootstrap/less/normalize.less"; +//@import "print.less"; +//@import "glyphicons.less"; +// Core CSS +@import "~bootstrap/less/scaffolding.less"; +@import "~bootstrap/less/type.less"; +//@import "code.less"; +@import "~bootstrap/less/grid.less"; +@import "~bootstrap/less/tables.less"; +@import "~bootstrap/less/forms.less"; +@import "~bootstrap/less/buttons.less"; +// Components +@import "~bootstrap/less/component-animations.less"; +@import "~bootstrap/less/dropdowns.less"; +@import "~bootstrap/less/button-groups.less"; +//@import "input-groups.less"; +@import "~bootstrap/less/navs.less"; +@import "~bootstrap/less/navbar.less"; +//@import "breadcrumbs.less"; +//@import "~bootstrap/less/pagination.less"; +//@import "~bootstrap/less/pager.less"; +@import "~bootstrap/less/labels.less"; +//@import "~bootstrap/less/badges.less"; +//@import "jumbotron.less"; +//@import "thumbnails.less"; +@import "~bootstrap/less/alerts.less"; +@import "~bootstrap/less/progress-bars.less"; +//@import "media.less"; +@import "~bootstrap/less/list-group.less"; +@import "~bootstrap/less/panels.less"; +//@import "responsive-embed.less"; +@import "~bootstrap/less/wells.less"; +@import "~bootstrap/less/close.less"; +// Components w/ JavaScript +@import "~bootstrap/less/modals.less"; +//@import "tooltip.less"; +//@import "popovers.less"; +//@import "carousel.less"; +// Utility classes +@import "~bootstrap/less/utilities.less"; +//@import "responsive-utilities.less"; +@import "~font-awesome/less/font-awesome.less"; +@import "../core.less"; +@import "../bbcode_editor.less"; +@import "../bbcode.less"; +@import "../flist_overrides.less"; +@import "../chat.less"; diff --git a/less/themes/variables/dark.less b/less/themes/variables/dark.less new file mode 100644 index 0000000..e90bba3 --- /dev/null +++ b/less/themes/variables/dark.less @@ -0,0 +1,114 @@ +//Import variable defaults first. +@import "~bootstrap/less/variables.less"; +@import "../../flist_variables.less"; + +@gray-base: #080810; +@gray-darker: lighten(@gray-base, 15%); +@gray-dark: lighten(@gray-base, 25%); +@gray: lighten(@gray-base, 55%); +@gray-light: lighten(@gray-base, 76.7%); +@gray-lighter: lighten(@gray-base, 93.5%); + +// @body-bg: #262626; +@body-bg: darken(@text-background-color-disabled, 3%); +@text-color: @gray-lighter; +@text-color-disabled: @gray; +@link-color: darken(@gray-lighter, 15%); + +@brand-warning: #c26c00; +@brand-danger: #930300; +@brand-success: #009900; +@brand-info: #0447af; +@brand-primary: @brand-info; + +@state-info-bg: darken(@brand-info, 15%); +@state-info-text: lighten(@brand-info, 30%); +@state-success-bg: darken(@brand-success, 15%); +@state-success-text: lighten(@brand-success, 30%); +@state-warning-bg: darken(@brand-warning, 15%); +@state-warning-text: lighten(@brand-warning, 30%); +@state-danger-bg: darken(@brand-danger, 15%); +@state-danger-text: lighten(@brand-danger, 30%); + +@text-background-color: @gray-dark; +@text-background-color-disabled: @gray-darker; +@border-color: lighten(spin(@text-background-color, -10), 15%); +@border-color-active: lighten(spin(@text-background-color, -10), 25%); +@border-color-disabled: darken(spin(@text-background-color-disabled, -10), 8%); + +@hover-bg: lighten(@gray-dark, 15%); + + +@hr-border: @text-color; + +@panel-bg: @text-background-color; +@panel-default-heading-bg: @gray; +@panel-default-border: @border-color; + +@input-color: @gray-light; +@input-bg: @text-background-color; +@input-bg-disabled: @text-background-color-disabled; +@input-border: @border-color; +@input-border-focus: @gray; + +@dropdown-bg: @text-background-color; +@dropdown-color: @text-color; +@dropdown-link-color: @link-color; +@dropdown-link-hover-color: @gray-dark; +@dropdown-link-hover-bg: @gray-light; + +@navbar-default-bg: @text-background-color; +@navbar-default-color: @text-color; +@navbar-default-link-color: @link-color; +@navbar-default-link-hover-color: @link-hover-color; + +@nav-link-hover-bg: @gray-light; +@nav-link-hover-color: @gray-dark; + +@nav-tabs-border-color: @border-color; +@nav-tabs-link-hover-border-color: @border-color; +@nav-tabs-active-link-hover-bg: @body-bg; +@nav-tabs-active-link-hover-color: @text-color; +@nav-tabs-active-link-hover-border-color: @border-color; + +@component-active-color: @gray-dark; +@component-active-bg: @gray-light; + +@list-group-bg: @gray-darker; +@list-group-border: @gray-dark; +@list-group-link-color: @text-color; +@list-group-hover-bg: @gray-dark; + +@btn-default-bg: @text-background-color; +@btn-default-color: @text-color; +@btn-default-border: @border-color; + +@pagination-bg: @text-background-color; +@pagination-color: @text-color; +@pagination-border: @border-color; +@pagination-disabled-bg: @text-background-color-disabled; +@pagination-disabled-color: @text-color-disabled; +@pagination-disabled-border: @border-color-disabled; +@pagination-active-bg: @gray; +@pagination-active-color: @gray-lighter; +@pagination-active-border: @border-color-active; + +@modal-content-bg: @text-background-color; +@modal-footer-border-color: lighten(spin(@modal-content-bg, -10), 15%); +@modal-header-border-color: @modal-footer-border-color; + +@badge-color: @gray-darker; + +@close-color: saturate(@text-color, 10%); +@close-text-shadow: 0 1px 0 @text-color; + +@well-bg: @text-background-color; +@well-border: @border-color; + +@blockquote-border-color: @border-color-active; + +@collapse-border: desaturate(@well-border, 20%); +@collapse-header-bg: desaturate(@well-bg, 20%); + +@white-color: @text-color; +@purple-color: @gray-light; \ No newline at end of file diff --git a/less/themes/variables/default.less b/less/themes/variables/default.less new file mode 100644 index 0000000..57e0d47 --- /dev/null +++ b/less/themes/variables/default.less @@ -0,0 +1,8 @@ +//Import variable defaults first. +@import "~bootstrap/less/variables.less"; +@import "../../flist_variables.less"; + + +// Update variables here. +// @body-bg: #00ff00; +@hr-border: @text-color; \ No newline at end of file diff --git a/less/themes/variables/light.less b/less/themes/variables/light.less new file mode 100644 index 0000000..74316d2 --- /dev/null +++ b/less/themes/variables/light.less @@ -0,0 +1,7 @@ +//Import variable defaults first. +@import "~bootstrap/less/variables.less"; +@import "../../flist_variables.less"; + +// Update variables here. +// @body-bg: #00ff00; +@hr-border: @text-color; \ No newline at end of file diff --git a/less/threads.less b/less/threads.less new file mode 100644 index 0000000..38d2eeb --- /dev/null +++ b/less/threads.less @@ -0,0 +1,32 @@ +.threadPost { + .well(); + .row(); + margin-bottom: 0px; +} + +.threadPost.comment { + background-color: @state-info-bg; +} + +.threadPost.setting { + background-color: @state-success-bg; +} + +.threadPost.deleted { + background-color: @state-warning-bg; +} + +.setting-post { + .dropdown(); +} + +.setting-post-popout { + .dropdown-menu(); + display: block; + top: auto; + bottom: 100%; + margin-bottom: 2px; + max-height: 250px; + overflow: scroll; + overflow-x: hidden; +} \ No newline at end of file diff --git a/less/tickets.less b/less/tickets.less new file mode 100644 index 0000000..5b1ed61 --- /dev/null +++ b/less/tickets.less @@ -0,0 +1,9 @@ +.ticket-reply-well { + .well(); + margin-bottom: 0px; +} + +.ticket-reply-well.staff { + background-color: @state-info-bg; + border-color: @state-info-border; +} \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..8794a30 --- /dev/null +++ b/package.json @@ -0,0 +1,36 @@ +{ + "name": "flist-exported", + "version": "1.0.0", + "author": "The F-List Team", + "description": "F-List Exported", + "license": "MIT", + "devDependencies": { + "@types/bootstrap": "^3.3.35", + "@types/jquery": "^3.2.11", + "@types/node": "^8.0.31", + "@types/sortablejs": "^1.3.31", + "axios": "^0.16.2", + "bootstrap": "^3.3.7", + "css-loader": "^0.28.4", + "date-fns": "^1.28.5", + "file-loader": "^0.11.2", + "font-awesome": "^4.7.0", + "fork-ts-checker-webpack-plugin": "^0.2.8", + "jquery": "^3.2.1", + "less": "^2.7.2", + "less-loader": "^4.0.4", + "raven-js": "^3.17.0", + "sortablejs": "^1.6.0", + "ts-loader": "^2.3.2", + "tslint": "^5.7.0", + "typescript": "^2.4.2", + "uglifyjs-webpack-plugin": "1.0.0-beta.2", + "url-loader": "^0.5.9", + "vue": "^2.4.2", + "vue-class-component": "^5.0.2", + "vue-loader": "^13.0.4", + "vue-property-decorator": "^5.2.1", + "vue-template-compiler": "^2.4.2", + "webpack": "^3.5.4" + } +} \ No newline at end of file diff --git a/readme.md b/readme.md new file mode 100644 index 0000000..c0783e9 --- /dev/null +++ b/readme.md @@ -0,0 +1,37 @@ +# F-List Exported +This repository contains the open source parts of F-list and F-Chat 3.0. +All necessary files to build F-Chat 3.0 as an Electron, Cordova or web application are included. + +## Setting up a Dev Environment + - Clone the repo + - Install [Yarn](https://yarnpkg.com/en/docs/install) + - Change into the cloned directory and run `yarn install`. + - To build native Node assets, you will need to install Python 2.7 and the Visual C++ 2015 Build tools. [More information can be found in the node-gyp docs.](https://github.com/nodejs/node-gyp#installation) + - IntelliJ IDEA is recommended for development. + +## Building for Electron + - Change into the `electron` directory. + - Run `yarn install` and then `yarn build`/`yarn watch` to build assets. They are placed into the `app` directory. + - You will probably need to rebuild the native dependencies (`spellchecker` and `keytar´) for electron. To do so, run `npm rebuild {NAME} --target={ELECTRON_VERSION} --arch=x64 --dist-url=https://atom.io/download/electron`. [See the electron documentation for more info.](https://github.com/electron/electron/blob/master/docs/tutorial/using-native-node-modules.md) + - Run `yarn start` to start the app in debug mode. Use `Ctrl+Shift+I` to open the Chromium debugger. + +### Packaging +See https://electron.atom.io/docs/tutorial/application-distribution/ + - Run `yarn build:dist` to create a minified production build. + - Run `./node_modules/.bin/electron-builder` with [options specifying the platform you want to build for](https://www.electron.build/cli). + +## Building for Cordova + - Change into the `cordova` directory. + - Install Cordova using `yarn global add cordova`. + - Run `yarn install`. + - Create a `www` directory inside the `cordova` directory and then run `cordova prepare` to install dependencies. + - Run `cordova requirements` to see whether all requirements for building are installed. + - Run `yarn build`/`yarn watch` to build assets. They are placed into the `www` directory. + - Run `cordova build`. For Android, the generated APK is now in `platforms/android/build/outputs/apk`. + +## Dependencies +Note: Adding *and upgrading* dependencies should only be done with prior consideration and subsequent testing. + +That's why `yarn.lock` exists and is version controlled. + +To upgrade NPM dependencies, run `yarn upgrade` locally. Run `yarn outdated` to see pending upgrades. \ No newline at end of file diff --git a/sfc.d.ts b/sfc.d.ts new file mode 100644 index 0000000..377c5eb --- /dev/null +++ b/sfc.d.ts @@ -0,0 +1,4 @@ +declare module '*.vue' { + import Vue from 'vue'; + export default Vue; +} \ No newline at end of file diff --git a/tslint.json b/tslint.json new file mode 100644 index 0000000..b0b1654 --- /dev/null +++ b/tslint.json @@ -0,0 +1,175 @@ +{ + "defaultSeverity": "warning", + "extends": [ + "tslint:all" + ], + "jsRules": { + "align": [ + true, + "elements", + "members", + "parameters", + "statements" + ], + "comment-format": false, + "curly": [ + true, + "as-needed" + ], + "eofline": false, + "linebreak-style": false, + "trailing-comma": [ + true, + { + "multiline": "never", + "singleline": "never" + } + ], + "max-line-length": [ + true, + 140 + ], + "newline-before-return": false, + "quotemark": [ + true, + "single" + ], + "whitespace": [ + true, + "check-decl", + "check-operator", + "check-separator", + "check-preblock", + "check-rest-spread" + ] + }, + "rules": { + "align": [ + true, + "members", + "parameters", + "statements" + ], + "array-type": [ + true, + "array" + ], + "comment-format": false, + "completed-docs": false, + "curly": [ + true, + "as-needed" + ], + "cyclomatic-complexity": false, + "eofline": false, + "interface-name": false, + "interface-over-type-literal": false, + "linebreak-style": false, + "max-classes-per-file": false, + "max-line-length": [ + true, + 140 + ], + "member-access": [ + true, + "no-public" + ], + "member-ordering": [ + true, + { + "order": "fields-first" + } + ], + "newline-before-return": false, + "no-angle-bracket-type-assertion": false, + "no-bitwise": false, + "no-conditional-assignment": false, + //disabled for Vue components + "no-consecutive-blank-lines": false, + "no-console": false, + "no-default-export": false, + "no-floating-promises": [true, "AxiosPromise"], + "no-import-side-effect": [ + true, + { + "ignore-module": "bootstrap/js/.*\\.js$" + } + ], + "no-magic-numbers": false, + "no-namespace": false, + "no-non-null-assertion": false, + "no-parameter-properties": false, + "no-parameter-reassignment": false, + //covered by --noImplicitAny + "no-string-literal": false, + "no-submodule-imports": [true, "vue", "bootstrap"], + "no-unused-variable": false, + "no-void-expression": false, + //covered by no-var-keyword and compiler + "no-use-before-declare": false, + //covered by no-require-imports + "no-var-requires": false, + "object-literal-sort-keys": false, + "object-literal-key-quotes": [ + true, + "as-needed" + ], + "one-variable-per-declaration": false, + "only-arrow-functions": [ + true, + "allow-declarations" + ], + "prefer-function-over-method": [ + true, + "allow-public" + ], + "quotemark": [ + true, + "single" + ], + "return-undefined": false, + "semicolon": [ + true, + "always", + "ignore-interfaces" + ], + "space-before-function-paren": [ + true, + "never" + ], + "switch-default": false, + "trailing-comma": [ + true, + { + "multiline": "never", + "singleline": "never" + } + ], + "type-literal-delimiter": false, + "typedef": [ + //enable this fully once inference is properly supported: https://github.com/palantir/tslint/issues/711 + true, + "call-signature", + "parameter", + "property-declaration" + ], + "variable-name": [ + true, + "ban-keywords", + "allow-leading-underscore" + ], + "whitespace": [ + true, + "check-decl", + "check-operator", + "check-separator", + "check-type", + "check-preblock", + "check-type-operator", + "check-rest-spread" + ], + "vue-props": true, + "no-return-await": true + }, + "rulesDirectory": ["./tslint"] +} \ No newline at end of file diff --git a/tslint/noReturnAwaitRule.js b/tslint/noReturnAwaitRule.js new file mode 100644 index 0000000..4961cd1 --- /dev/null +++ b/tslint/noReturnAwaitRule.js @@ -0,0 +1,36 @@ +"use strict"; +exports.__esModule = true; +var tslib_1 = require("tslib"); +var Lint = require("tslint"); +var ts = require("typescript"); +var Rule = /** @class */ (function (_super) { + tslib_1.__extends(Rule, _super); + function Rule() { + return _super !== null && _super.apply(this, arguments) || this; + } + Rule.prototype.apply = function (sourceFile) { + return this.applyWithFunction(sourceFile, walk, undefined); + }; + return Rule; +}(Lint.Rules.AbstractRule)); +exports.Rule = Rule; +function walk(ctx) { + if (ctx.sourceFile.isDeclarationFile) + return; + return ts.forEachChild(ctx.sourceFile, cb); + function cb(node) { + if (node.kind !== ts.SyntaxKind.ReturnStatement || node.expression === undefined) + return ts.forEachChild(node, cb); + var curNode = node.expression; + while (true) { + switch (curNode.kind) { + case ts.SyntaxKind.ParenthesizedExpression: + curNode = curNode.expression; + continue; + case ts.SyntaxKind.AwaitExpression: + ctx.addFailureAtNode(node, 'return await is redundant'); + } + break; + } + } +} diff --git a/tslint/vuePropsRule.js b/tslint/vuePropsRule.js new file mode 100644 index 0000000..db98e6e --- /dev/null +++ b/tslint/vuePropsRule.js @@ -0,0 +1,42 @@ +"use strict"; +exports.__esModule = true; +var tslib_1 = require("tslib"); +var Lint = require("tslint"); +var ts = require("typescript"); +var Rule = /** @class */ (function (_super) { + tslib_1.__extends(Rule, _super); + function Rule() { + return _super !== null && _super.apply(this, arguments) || this; + } + Rule.prototype.applyWithProgram = function (sourceFile, program) { + return this.applyWithFunction(sourceFile, walk, undefined, program.getTypeChecker()); + }; + return Rule; +}(Lint.Rules.TypedRule)); +exports.Rule = Rule; +function walk(ctx, checker) { + if (ctx.sourceFile.isDeclarationFile) + return; + return ts.forEachChild(ctx.sourceFile, cb); + function cb(node) { + if (node.kind !== ts.SyntaxKind.PropertyDeclaration || !node.decorators) + return ts.forEachChild(node, cb); + for (var _i = 0, _a = node.decorators; _i < _a.length; _i++) { + var decorator = _a[_i]; + var call = decorator.expression; + var propSymbol = checker.getTypeAtLocation(call.expression).symbol; + if (propSymbol.name === 'Prop' && + propSymbol.parent.name.endsWith('node_modules/vue-property-decorator/lib/vue-property-decorator"')) { + if (!node.modifiers || !node.modifiers.some(function (x) { return x.kind === ts.SyntaxKind.ReadonlyKeyword; })) + ctx.addFailureAtNode(node.name, 'Vue property should be readonly'); + if (call.arguments.length > 0 && call.arguments[0].properties.map(function (x) { return x.name.getText(); }) + .some(function (x) { return x === 'default' || x === 'required'; })) { + if (node.questionToken !== undefined) + ctx.addFailureAtNode(node.name, 'Vue property is required and should not be optional.'); + } + else if (node.questionToken === undefined) + ctx.addFailureAtNode(node.name, 'Vue property should be optional - it is not required and has no default value.'); + } + } + } +} diff --git a/yarn.lock b/yarn.lock new file mode 100644 index 0000000..15ef520 --- /dev/null +++ b/yarn.lock @@ -0,0 +1,3378 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@types/bootstrap@^3.3.35": + version "3.3.36" + resolved "https://registry.yarnpkg.com/@types/bootstrap/-/bootstrap-3.3.36.tgz#5ca0b81655427398fe7c8728bccdc395069093d0" + dependencies: + "@types/jquery" "*" + +"@types/jquery@*", "@types/jquery@^3.2.11": + version "3.2.12" + resolved "https://registry.yarnpkg.com/@types/jquery/-/jquery-3.2.12.tgz#f496823108c3874c97c9a822e675a3926ee64b46" + +"@types/node@^8.0.31": + version "8.0.31" + resolved "https://registry.yarnpkg.com/@types/node/-/node-8.0.31.tgz#d9af61093cf4bfc9f066ca34de0175012cfb0ce9" + +"@types/sortablejs@^1.3.32": + version "1.3.32" + resolved "https://registry.yarnpkg.com/@types/sortablejs/-/sortablejs-1.3.32.tgz#e7185fc4cd381c477564cd2375e095094064563d" + +abbrev@1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8" + +acorn-dynamic-import@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/acorn-dynamic-import/-/acorn-dynamic-import-2.0.2.tgz#c752bd210bef679501b6c6cb7fc84f8f47158cc4" + dependencies: + acorn "^4.0.3" + +acorn@^4.0.3: + version "4.0.13" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-4.0.13.tgz#105495ae5361d697bd195c825192e1ad7f253787" + +acorn@^5.0.0: + version "5.1.2" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.1.2.tgz#911cb53e036807cf0fa778dc5d370fbd864246d7" + +ajv-keywords@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-2.1.0.tgz#a296e17f7bfae7c1ce4f7e0de53d29cb32162df0" + +ajv@^4.9.1: + version "4.11.8" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-4.11.8.tgz#82ffb02b29e662ae53bdc20af15947706739c536" + dependencies: + co "^4.6.0" + json-stable-stringify "^1.0.1" + +ajv@^5.0.0, ajv@^5.1.0, ajv@^5.1.5: + version "5.2.3" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-5.2.3.tgz#c06f598778c44c6b161abafe3466b81ad1814ed2" + dependencies: + co "^4.6.0" + fast-deep-equal "^1.0.0" + json-schema-traverse "^0.3.0" + json-stable-stringify "^1.0.1" + +align-text@^0.1.1, align-text@^0.1.3: + version "0.1.4" + resolved "https://registry.yarnpkg.com/align-text/-/align-text-0.1.4.tgz#0cd90a561093f35d0a99256c22b7069433fad117" + dependencies: + kind-of "^3.0.2" + longest "^1.0.1" + repeat-string "^1.5.2" + +alphanum-sort@^1.0.1, alphanum-sort@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/alphanum-sort/-/alphanum-sort-1.0.2.tgz#97a1119649b211ad33691d9f9f486a8ec9fbe0a3" + +ansi-regex@^2.0.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df" + +ansi-regex@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-3.0.0.tgz#ed0317c322064f79466c02966bddb605ab37d998" + +ansi-styles@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-2.2.1.tgz#b432dd3358b634cf75e1e4664368240533c1ddbe" + +ansi-styles@^3.1.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.0.tgz#c159b8d5be0f9e5a6f346dab94f16ce022161b88" + dependencies: + color-convert "^1.9.0" + +anymatch@^1.3.0: + version "1.3.2" + resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-1.3.2.tgz#553dcb8f91e3c889845dfdba34c77721b90b9d7a" + dependencies: + micromatch "^2.1.5" + normalize-path "^2.0.0" + +aproba@^1.0.3, aproba@^1.1.1: + version "1.2.0" + resolved "https://registry.yarnpkg.com/aproba/-/aproba-1.2.0.tgz#6802e6264efd18c790a1b0d517f0f2627bf2c94a" + +are-we-there-yet@~1.1.2: + version "1.1.4" + resolved "https://registry.yarnpkg.com/are-we-there-yet/-/are-we-there-yet-1.1.4.tgz#bb5dca382bb94f05e15194373d16fd3ba1ca110d" + dependencies: + delegates "^1.0.0" + readable-stream "^2.0.6" + +argparse@^1.0.7: + version "1.0.9" + resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.9.tgz#73d83bc263f86e97f8cc4f6bae1b0e90a7d22c86" + dependencies: + sprintf-js "~1.0.2" + +arr-diff@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/arr-diff/-/arr-diff-2.0.0.tgz#8f3b827f955a8bd669697e4a4256ac3ceae356cf" + dependencies: + arr-flatten "^1.0.1" + +arr-flatten@^1.0.1: + version "1.1.0" + resolved "https://registry.yarnpkg.com/arr-flatten/-/arr-flatten-1.1.0.tgz#36048bbff4e7b47e136644316c99669ea5ae91f1" + +array-unique@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/array-unique/-/array-unique-0.2.1.tgz#a1d97ccafcbc2625cc70fadceb36a50c58b01a53" + +asap@~2.0.3: + version "2.0.6" + resolved "https://registry.yarnpkg.com/asap/-/asap-2.0.6.tgz#e50347611d7e690943208bbdafebcbc2fb866d46" + +asn1.js@^4.0.0: + version "4.9.1" + resolved "https://registry.yarnpkg.com/asn1.js/-/asn1.js-4.9.1.tgz#48ba240b45a9280e94748990ba597d216617fd40" + dependencies: + bn.js "^4.0.0" + inherits "^2.0.1" + minimalistic-assert "^1.0.0" + +asn1@~0.2.3: + version "0.2.3" + resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.3.tgz#dac8787713c9966849fc8180777ebe9c1ddf3b86" + +assert-plus@1.0.0, assert-plus@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-1.0.0.tgz#f12e0f3c5d77b0b1cdd9146942e4e96c1e4dd525" + +assert-plus@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-0.2.0.tgz#d74e1b87e7affc0db8aadb7021f3fe48101ab234" + +assert@^1.1.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/assert/-/assert-1.4.1.tgz#99912d591836b5a6f5b345c0f07eefc08fc65d91" + dependencies: + util "0.10.3" + +async-each@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/async-each/-/async-each-1.0.1.tgz#19d386a1d9edc6e7c1c85d388aedbcc56d33602d" + +async@^2.1.2: + version "2.5.0" + resolved "https://registry.yarnpkg.com/async/-/async-2.5.0.tgz#843190fd6b7357a0b9e1c956edddd5ec8462b54d" + dependencies: + lodash "^4.14.0" + +asynckit@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" + +autoprefixer@^6.3.1: + version "6.7.7" + resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-6.7.7.tgz#1dbd1c835658e35ce3f9984099db00585c782014" + dependencies: + browserslist "^1.7.6" + caniuse-db "^1.0.30000634" + normalize-range "^0.1.2" + num2fraction "^1.2.2" + postcss "^5.2.16" + postcss-value-parser "^3.2.3" + +aws-sign2@~0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.6.0.tgz#14342dd38dbcc94d0e5b87d763cd63612c0e794f" + +aws-sign2@~0.7.0: + version "0.7.0" + resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.7.0.tgz#b46e890934a9591f2d2f6f86d7e6a9f1b3fe76a8" + +aws4@^1.2.1, aws4@^1.6.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.6.0.tgz#83ef5ca860b2b32e4a0deedee8c771b9db57471e" + +axios@^0.16.2: + version "0.16.2" + resolved "https://registry.yarnpkg.com/axios/-/axios-0.16.2.tgz#ba4f92f17167dfbab40983785454b9ac149c3c6d" + dependencies: + follow-redirects "^1.2.3" + is-buffer "^1.1.5" + +babel-code-frame@^6.11.0, babel-code-frame@^6.22.0: + version "6.26.0" + resolved "https://registry.yarnpkg.com/babel-code-frame/-/babel-code-frame-6.26.0.tgz#63fd43f7dc1e3bb7ce35947db8fe369a3f58c74b" + dependencies: + chalk "^1.1.3" + esutils "^2.0.2" + js-tokens "^3.0.2" + +balanced-match@^0.4.2: + version "0.4.2" + resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-0.4.2.tgz#cb3f3e3c732dc0f01ee70b403f302e61d7709838" + +balanced-match@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" + +base64-js@^1.0.2: + version "1.2.1" + resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.2.1.tgz#a91947da1f4a516ea38e5b4ec0ec3773675e0886" + +bcrypt-pbkdf@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.1.tgz#63bc5dcb61331b92bc05fd528953c33462a06f8d" + dependencies: + tweetnacl "^0.14.3" + +big.js@^3.1.3: + version "3.2.0" + resolved "https://registry.yarnpkg.com/big.js/-/big.js-3.2.0.tgz#a5fc298b81b9e0dca2e458824784b65c52ba588e" + +binary-extensions@^1.0.0: + version "1.10.0" + resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-1.10.0.tgz#9aeb9a6c5e88638aad171e167f5900abe24835d0" + +block-stream@*: + version "0.0.9" + resolved "https://registry.yarnpkg.com/block-stream/-/block-stream-0.0.9.tgz#13ebfe778a03205cfe03751481ebb4b3300c126a" + dependencies: + inherits "~2.0.0" + +bluebird@^3.1.1, bluebird@^3.5.0: + version "3.5.0" + resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.5.0.tgz#791420d7f551eea2897453a8a77653f96606d67c" + +bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.1.1, bn.js@^4.4.0: + version "4.11.8" + resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.11.8.tgz#2cde09eb5ee341f484746bb0309b3253b1b1442f" + +boom@2.x.x: + version "2.10.1" + resolved "https://registry.yarnpkg.com/boom/-/boom-2.10.1.tgz#39c8918ceff5799f83f9492a848f625add0c766f" + dependencies: + hoek "2.x.x" + +boom@4.x.x: + version "4.3.1" + resolved "https://registry.yarnpkg.com/boom/-/boom-4.3.1.tgz#4f8a3005cb4a7e3889f749030fd25b96e01d2e31" + dependencies: + hoek "4.x.x" + +boom@5.x.x: + version "5.2.0" + resolved "https://registry.yarnpkg.com/boom/-/boom-5.2.0.tgz#5dd9da6ee3a5f302077436290cb717d3f4a54e02" + dependencies: + hoek "4.x.x" + +bootstrap@^3.3.7: + version "3.3.7" + resolved "https://registry.yarnpkg.com/bootstrap/-/bootstrap-3.3.7.tgz#5a389394549f23330875a3b150656574f8a9eb71" + +brace-expansion@^1.1.7: + version "1.1.8" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.8.tgz#c07b211c7c952ec1f8efd51a77ef0d1d3990a292" + dependencies: + balanced-match "^1.0.0" + concat-map "0.0.1" + +braces@^1.8.2: + version "1.8.5" + resolved "https://registry.yarnpkg.com/braces/-/braces-1.8.5.tgz#ba77962e12dff969d6b76711e914b737857bf6a7" + dependencies: + expand-range "^1.8.1" + preserve "^0.2.0" + repeat-element "^1.1.2" + +brorand@^1.0.1: + version "1.1.0" + resolved "https://registry.yarnpkg.com/brorand/-/brorand-1.1.0.tgz#12c25efe40a45e3c323eb8675a0a0ce57b22371f" + +browserify-aes@^1.0.0, browserify-aes@^1.0.4: + version "1.0.8" + resolved "https://registry.yarnpkg.com/browserify-aes/-/browserify-aes-1.0.8.tgz#c8fa3b1b7585bb7ba77c5560b60996ddec6d5309" + dependencies: + buffer-xor "^1.0.3" + cipher-base "^1.0.0" + create-hash "^1.1.0" + evp_bytestokey "^1.0.3" + inherits "^2.0.1" + safe-buffer "^5.0.1" + +browserify-cipher@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/browserify-cipher/-/browserify-cipher-1.0.0.tgz#9988244874bf5ed4e28da95666dcd66ac8fc363a" + dependencies: + browserify-aes "^1.0.4" + browserify-des "^1.0.0" + evp_bytestokey "^1.0.0" + +browserify-des@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/browserify-des/-/browserify-des-1.0.0.tgz#daa277717470922ed2fe18594118a175439721dd" + dependencies: + cipher-base "^1.0.1" + des.js "^1.0.0" + inherits "^2.0.1" + +browserify-rsa@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/browserify-rsa/-/browserify-rsa-4.0.1.tgz#21e0abfaf6f2029cf2fafb133567a701d4135524" + dependencies: + bn.js "^4.1.0" + randombytes "^2.0.1" + +browserify-sign@^4.0.0: + version "4.0.4" + resolved "https://registry.yarnpkg.com/browserify-sign/-/browserify-sign-4.0.4.tgz#aa4eb68e5d7b658baa6bf6a57e630cbd7a93d298" + dependencies: + bn.js "^4.1.1" + browserify-rsa "^4.0.0" + create-hash "^1.1.0" + create-hmac "^1.1.2" + elliptic "^6.0.0" + inherits "^2.0.1" + parse-asn1 "^5.0.0" + +browserify-zlib@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/browserify-zlib/-/browserify-zlib-0.1.4.tgz#bb35f8a519f600e0fa6b8485241c979d0141fb2d" + dependencies: + pako "~0.2.0" + +browserslist@^1.3.6, browserslist@^1.5.2, browserslist@^1.7.6: + version "1.7.7" + resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-1.7.7.tgz#0bd76704258be829b2398bb50e4b62d1a166b0b9" + dependencies: + caniuse-db "^1.0.30000639" + electron-to-chromium "^1.2.7" + +buffer-xor@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/buffer-xor/-/buffer-xor-1.0.3.tgz#26e61ed1422fb70dd42e6e36729ed51d855fe8d9" + +buffer@^4.3.0: + version "4.9.1" + resolved "https://registry.yarnpkg.com/buffer/-/buffer-4.9.1.tgz#6d1bb601b07a4efced97094132093027c95bc298" + dependencies: + base64-js "^1.0.2" + ieee754 "^1.1.4" + isarray "^1.0.0" + +builtin-modules@^1.0.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-1.1.1.tgz#270f076c5a72c02f5b65a47df94c5fe3a278892f" + +builtin-status-codes@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz#85982878e21b98e1c66425e03d0174788f569ee8" + +cacache@^9.2.9: + version "9.2.9" + resolved "https://registry.yarnpkg.com/cacache/-/cacache-9.2.9.tgz#f9d7ffe039851ec94c28290662afa4dd4bb9e8dd" + dependencies: + bluebird "^3.5.0" + chownr "^1.0.1" + glob "^7.1.2" + graceful-fs "^4.1.11" + lru-cache "^4.1.1" + mississippi "^1.3.0" + mkdirp "^0.5.1" + move-concurrently "^1.0.1" + promise-inflight "^1.0.1" + rimraf "^2.6.1" + ssri "^4.1.6" + unique-filename "^1.1.0" + y18n "^3.2.1" + +camelcase@^1.0.2: + version "1.2.1" + resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-1.2.1.tgz#9bb5304d2e0b56698b2c758b08a3eaa9daa58a39" + +camelcase@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-4.1.0.tgz#d545635be1e33c542649c69173e5de6acfae34dd" + +caniuse-api@^1.5.2: + version "1.6.1" + resolved "https://registry.yarnpkg.com/caniuse-api/-/caniuse-api-1.6.1.tgz#b534e7c734c4f81ec5fbe8aca2ad24354b962c6c" + dependencies: + browserslist "^1.3.6" + caniuse-db "^1.0.30000529" + lodash.memoize "^4.1.2" + lodash.uniq "^4.5.0" + +caniuse-db@^1.0.30000529, caniuse-db@^1.0.30000634, caniuse-db@^1.0.30000639: + version "1.0.30000740" + resolved "https://registry.yarnpkg.com/caniuse-db/-/caniuse-db-1.0.30000740.tgz#03fcaaa176e3ed075895f72d46c1a12149bbeac9" + +caseless@~0.12.0: + version "0.12.0" + resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc" + +center-align@^0.1.1: + version "0.1.3" + resolved "https://registry.yarnpkg.com/center-align/-/center-align-0.1.3.tgz#aa0d32629b6ee972200411cbd4461c907bc2b7ad" + dependencies: + align-text "^0.1.3" + lazy-cache "^1.0.3" + +chalk@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-1.1.3.tgz#a8115c55e4a702fe4d150abd3872822a7e09fc98" + dependencies: + ansi-styles "^2.2.1" + escape-string-regexp "^1.0.2" + has-ansi "^2.0.0" + strip-ansi "^3.0.0" + supports-color "^2.0.0" + +chalk@^2.0.1, chalk@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.1.0.tgz#ac5becf14fa21b99c6c92ca7a7d7cfd5b17e743e" + dependencies: + ansi-styles "^3.1.0" + escape-string-regexp "^1.0.5" + supports-color "^4.0.0" + +chokidar@^1.7.0: + version "1.7.0" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-1.7.0.tgz#798e689778151c8076b4b360e5edd28cda2bb468" + dependencies: + anymatch "^1.3.0" + async-each "^1.0.0" + glob-parent "^2.0.0" + inherits "^2.0.1" + is-binary-path "^1.0.0" + is-glob "^2.0.0" + path-is-absolute "^1.0.0" + readdirp "^2.0.0" + optionalDependencies: + fsevents "^1.0.0" + +chownr@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.0.1.tgz#e2a75042a9551908bebd25b8523d5f9769d79181" + +cipher-base@^1.0.0, cipher-base@^1.0.1, cipher-base@^1.0.3: + version "1.0.4" + resolved "https://registry.yarnpkg.com/cipher-base/-/cipher-base-1.0.4.tgz#8760e4ecc272f4c363532f926d874aae2c1397de" + dependencies: + inherits "^2.0.1" + safe-buffer "^5.0.1" + +clap@^1.0.9: + version "1.2.3" + resolved "https://registry.yarnpkg.com/clap/-/clap-1.2.3.tgz#4f36745b32008492557f46412d66d50cb99bce51" + dependencies: + chalk "^1.1.3" + +cliui@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/cliui/-/cliui-2.1.0.tgz#4b475760ff80264c762c3a1719032e91c7fea0d1" + dependencies: + center-align "^0.1.1" + right-align "^0.1.1" + wordwrap "0.0.2" + +cliui@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/cliui/-/cliui-3.2.0.tgz#120601537a916d29940f934da3b48d585a39213d" + dependencies: + string-width "^1.0.1" + strip-ansi "^3.0.1" + wrap-ansi "^2.0.0" + +clone@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/clone/-/clone-1.0.2.tgz#260b7a99ebb1edfe247538175f783243cb19d149" + +clone@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/clone/-/clone-2.1.1.tgz#d217d1e961118e3ac9a4b8bba3285553bf647cdb" + +co@^4.6.0: + version "4.6.0" + resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184" + +coa@~1.0.1: + version "1.0.4" + resolved "https://registry.yarnpkg.com/coa/-/coa-1.0.4.tgz#a9ef153660d6a86a8bdec0289a5c684d217432fd" + dependencies: + q "^1.1.2" + +code-point-at@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77" + +color-convert@^1.3.0, color-convert@^1.9.0: + version "1.9.0" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.0.tgz#1accf97dd739b983bf994d56fec8f95853641b7a" + dependencies: + color-name "^1.1.1" + +color-name@^1.0.0, color-name@^1.1.1: + version "1.1.3" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" + +color-string@^0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/color-string/-/color-string-0.3.0.tgz#27d46fb67025c5c2fa25993bfbf579e47841b991" + dependencies: + color-name "^1.0.0" + +color@^0.11.0: + version "0.11.4" + resolved "https://registry.yarnpkg.com/color/-/color-0.11.4.tgz#6d7b5c74fb65e841cd48792ad1ed5e07b904d764" + dependencies: + clone "^1.0.2" + color-convert "^1.3.0" + color-string "^0.3.0" + +colormin@^1.0.5: + version "1.1.2" + resolved "https://registry.yarnpkg.com/colormin/-/colormin-1.1.2.tgz#ea2f7420a72b96881a38aae59ec124a6f7298133" + dependencies: + color "^0.11.0" + css-color-names "0.0.4" + has "^1.0.1" + +colors@^1.1.2, colors@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/colors/-/colors-1.1.2.tgz#168a4701756b6a7f51a12ce0c97bfa28c084ed63" + +combined-stream@^1.0.5, combined-stream@~1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.5.tgz#938370a57b4a51dea2c77c15d5c5fdf895164009" + dependencies: + delayed-stream "~1.0.0" + +commander@^2.9.0, commander@~2.11.0: + version "2.11.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-2.11.0.tgz#157152fd1e7a6c8d98a5b715cf376df928004563" + +commondir@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b" + +concat-map@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" + +concat-stream@^1.5.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.6.0.tgz#0aac662fd52be78964d5532f694784e70110acf7" + dependencies: + inherits "^2.0.3" + readable-stream "^2.2.2" + typedarray "^0.0.6" + +console-browserify@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/console-browserify/-/console-browserify-1.1.0.tgz#f0241c45730a9fc6323b206dbf38edc741d0bb10" + dependencies: + date-now "^0.1.4" + +console-control-strings@^1.0.0, console-control-strings@~1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/console-control-strings/-/console-control-strings-1.1.0.tgz#3d7cf4464db6446ea644bf4b39507f9851008e8e" + +consolidate@^0.14.0: + version "0.14.5" + resolved "https://registry.yarnpkg.com/consolidate/-/consolidate-0.14.5.tgz#5a25047bc76f73072667c8cb52c989888f494c63" + dependencies: + bluebird "^3.1.1" + +constants-browserify@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/constants-browserify/-/constants-browserify-1.0.0.tgz#c20b96d8c617748aaf1c16021760cd27fcb8cb75" + +copy-concurrently@^1.0.0: + version "1.0.5" + resolved "https://registry.yarnpkg.com/copy-concurrently/-/copy-concurrently-1.0.5.tgz#92297398cae34937fcafd6ec8139c18051f0b5e0" + dependencies: + aproba "^1.1.1" + fs-write-stream-atomic "^1.0.8" + iferr "^0.1.5" + mkdirp "^0.5.1" + rimraf "^2.5.4" + run-queue "^1.0.0" + +core-util-is@1.0.2, core-util-is@~1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" + +cosmiconfig@^2.1.0, cosmiconfig@^2.1.1: + version "2.2.2" + resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-2.2.2.tgz#6173cebd56fac042c1f4390edf7af6c07c7cb892" + dependencies: + is-directory "^0.3.1" + js-yaml "^3.4.3" + minimist "^1.2.0" + object-assign "^4.1.0" + os-homedir "^1.0.1" + parse-json "^2.2.0" + require-from-string "^1.1.0" + +create-ecdh@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/create-ecdh/-/create-ecdh-4.0.0.tgz#888c723596cdf7612f6498233eebd7a35301737d" + dependencies: + bn.js "^4.1.0" + elliptic "^6.0.0" + +create-hash@^1.1.0, create-hash@^1.1.2: + version "1.1.3" + resolved "https://registry.yarnpkg.com/create-hash/-/create-hash-1.1.3.tgz#606042ac8b9262750f483caddab0f5819172d8fd" + dependencies: + cipher-base "^1.0.1" + inherits "^2.0.1" + ripemd160 "^2.0.0" + sha.js "^2.4.0" + +create-hmac@^1.1.0, create-hmac@^1.1.2, create-hmac@^1.1.4: + version "1.1.6" + resolved "https://registry.yarnpkg.com/create-hmac/-/create-hmac-1.1.6.tgz#acb9e221a4e17bdb076e90657c42b93e3726cf06" + dependencies: + cipher-base "^1.0.3" + create-hash "^1.1.0" + inherits "^2.0.1" + ripemd160 "^2.0.0" + safe-buffer "^5.0.1" + sha.js "^2.4.8" + +cross-spawn@^5.0.1: + version "5.1.0" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-5.1.0.tgz#e8bd0efee58fcff6f8f94510a0a554bbfa235449" + dependencies: + lru-cache "^4.0.1" + shebang-command "^1.2.0" + which "^1.2.9" + +cryptiles@2.x.x: + version "2.0.5" + resolved "https://registry.yarnpkg.com/cryptiles/-/cryptiles-2.0.5.tgz#3bdfecdc608147c1c67202fa291e7dca59eaa3b8" + dependencies: + boom "2.x.x" + +cryptiles@3.x.x: + version "3.1.2" + resolved "https://registry.yarnpkg.com/cryptiles/-/cryptiles-3.1.2.tgz#a89fbb220f5ce25ec56e8c4aa8a4fd7b5b0d29fe" + dependencies: + boom "5.x.x" + +crypto-browserify@^3.11.0: + version "3.11.1" + resolved "https://registry.yarnpkg.com/crypto-browserify/-/crypto-browserify-3.11.1.tgz#948945efc6757a400d6e5e5af47194d10064279f" + dependencies: + browserify-cipher "^1.0.0" + browserify-sign "^4.0.0" + create-ecdh "^4.0.0" + create-hash "^1.1.0" + create-hmac "^1.1.0" + diffie-hellman "^5.0.0" + inherits "^2.0.1" + pbkdf2 "^3.0.3" + public-encrypt "^4.0.0" + randombytes "^2.0.0" + +css-color-names@0.0.4: + version "0.0.4" + resolved "https://registry.yarnpkg.com/css-color-names/-/css-color-names-0.0.4.tgz#808adc2e79cf84738069b646cb20ec27beb629e0" + +css-loader@^0.28.4: + version "0.28.7" + resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-0.28.7.tgz#5f2ee989dd32edd907717f953317656160999c1b" + dependencies: + babel-code-frame "^6.11.0" + css-selector-tokenizer "^0.7.0" + cssnano ">=2.6.1 <4" + icss-utils "^2.1.0" + loader-utils "^1.0.2" + lodash.camelcase "^4.3.0" + object-assign "^4.0.1" + postcss "^5.0.6" + postcss-modules-extract-imports "^1.0.0" + postcss-modules-local-by-default "^1.0.1" + postcss-modules-scope "^1.0.0" + postcss-modules-values "^1.1.0" + postcss-value-parser "^3.3.0" + source-list-map "^2.0.0" + +css-selector-tokenizer@^0.7.0: + version "0.7.0" + resolved "https://registry.yarnpkg.com/css-selector-tokenizer/-/css-selector-tokenizer-0.7.0.tgz#e6988474ae8c953477bf5e7efecfceccd9cf4c86" + dependencies: + cssesc "^0.1.0" + fastparse "^1.1.1" + regexpu-core "^1.0.0" + +cssesc@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-0.1.0.tgz#c814903e45623371a0477b40109aaafbeeaddbb4" + +"cssnano@>=2.6.1 <4": + version "3.10.0" + resolved "https://registry.yarnpkg.com/cssnano/-/cssnano-3.10.0.tgz#4f38f6cea2b9b17fa01490f23f1dc68ea65c1c38" + dependencies: + autoprefixer "^6.3.1" + decamelize "^1.1.2" + defined "^1.0.0" + has "^1.0.1" + object-assign "^4.0.1" + postcss "^5.0.14" + postcss-calc "^5.2.0" + postcss-colormin "^2.1.8" + postcss-convert-values "^2.3.4" + postcss-discard-comments "^2.0.4" + postcss-discard-duplicates "^2.0.1" + postcss-discard-empty "^2.0.1" + postcss-discard-overridden "^0.1.1" + postcss-discard-unused "^2.2.1" + postcss-filter-plugins "^2.0.0" + postcss-merge-idents "^2.1.5" + postcss-merge-longhand "^2.0.1" + postcss-merge-rules "^2.0.3" + postcss-minify-font-values "^1.0.2" + postcss-minify-gradients "^1.0.1" + postcss-minify-params "^1.0.4" + postcss-minify-selectors "^2.0.4" + postcss-normalize-charset "^1.1.0" + postcss-normalize-url "^3.0.7" + postcss-ordered-values "^2.1.0" + postcss-reduce-idents "^2.2.2" + postcss-reduce-initial "^1.0.0" + postcss-reduce-transforms "^1.0.3" + postcss-svgo "^2.1.1" + postcss-unique-selectors "^2.0.2" + postcss-value-parser "^3.2.3" + postcss-zindex "^2.0.1" + +csso@~2.3.1: + version "2.3.2" + resolved "https://registry.yarnpkg.com/csso/-/csso-2.3.2.tgz#ddd52c587033f49e94b71fc55569f252e8ff5f85" + dependencies: + clap "^1.0.9" + source-map "^0.5.3" + +cyclist@~0.2.2: + version "0.2.2" + resolved "https://registry.yarnpkg.com/cyclist/-/cyclist-0.2.2.tgz#1b33792e11e914a2fd6d6ed6447464444e5fa640" + +d@1: + version "1.0.0" + resolved "https://registry.yarnpkg.com/d/-/d-1.0.0.tgz#754bb5bfe55451da69a58b94d45f4c5b0462d58f" + dependencies: + es5-ext "^0.10.9" + +dashdash@^1.12.0: + version "1.14.1" + resolved "https://registry.yarnpkg.com/dashdash/-/dashdash-1.14.1.tgz#853cfa0f7cbe2fed5de20326b8dd581035f6e2f0" + dependencies: + assert-plus "^1.0.0" + +date-fns@^1.28.5: + version "1.28.5" + resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-1.28.5.tgz#257cfc45d322df45ef5658665967ee841cd73faf" + +date-now@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/date-now/-/date-now-0.1.4.tgz#eaf439fd4d4848ad74e5cc7dbef200672b9e345b" + +de-indent@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/de-indent/-/de-indent-1.0.2.tgz#b2038e846dc33baa5796128d0804b455b8c1e21d" + +debug@^2.2.0, debug@^2.4.5: + version "2.6.9" + resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" + dependencies: + ms "2.0.0" + +decamelize@^1.0.0, decamelize@^1.1.1, decamelize@^1.1.2: + version "1.2.0" + resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" + +deep-extend@~0.4.0: + version "0.4.2" + resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.4.2.tgz#48b699c27e334bf89f10892be432f6e4c7d34a7f" + +defined@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/defined/-/defined-1.0.0.tgz#c98d9bcef75674188e110969151199e39b1fa693" + +delayed-stream@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" + +delegates@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a" + +des.js@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/des.js/-/des.js-1.0.0.tgz#c074d2e2aa6a8a9a07dbd61f9a15c2cd83ec8ecc" + dependencies: + inherits "^2.0.1" + minimalistic-assert "^1.0.0" + +diff@^3.2.0: + version "3.3.1" + resolved "https://registry.yarnpkg.com/diff/-/diff-3.3.1.tgz#aa8567a6eed03c531fc89d3f711cd0e5259dec75" + +diffie-hellman@^5.0.0: + version "5.0.2" + resolved "https://registry.yarnpkg.com/diffie-hellman/-/diffie-hellman-5.0.2.tgz#b5835739270cfe26acf632099fded2a07f209e5e" + dependencies: + bn.js "^4.1.0" + miller-rabin "^4.0.0" + randombytes "^2.0.0" + +domain-browser@^1.1.1: + version "1.1.7" + resolved "https://registry.yarnpkg.com/domain-browser/-/domain-browser-1.1.7.tgz#867aa4b093faa05f1de08c06f4d7b21fdf8698bc" + +duplexify@^3.1.2, duplexify@^3.4.2: + version "3.5.1" + resolved "https://registry.yarnpkg.com/duplexify/-/duplexify-3.5.1.tgz#4e1516be68838bc90a49994f0b39a6e5960befcd" + dependencies: + end-of-stream "^1.0.0" + inherits "^2.0.1" + readable-stream "^2.0.0" + stream-shift "^1.0.0" + +ecc-jsbn@~0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.1.tgz#0fc73a9ed5f0d53c38193398523ef7e543777505" + dependencies: + jsbn "~0.1.0" + +electron-to-chromium@^1.2.7: + version "1.3.24" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.24.tgz#9b7b88bb05ceb9fa016a177833cc2dde388f21b6" + +elliptic@^6.0.0: + version "6.4.0" + resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.4.0.tgz#cac9af8762c85836187003c8dfe193e5e2eae5df" + dependencies: + bn.js "^4.4.0" + brorand "^1.0.1" + hash.js "^1.0.0" + hmac-drbg "^1.0.0" + inherits "^2.0.1" + minimalistic-assert "^1.0.0" + minimalistic-crypto-utils "^1.0.0" + +emojis-list@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/emojis-list/-/emojis-list-2.1.0.tgz#4daa4d9db00f9819880c79fa457ae5b09a1fd389" + +end-of-stream@^1.0.0, end-of-stream@^1.1.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.0.tgz#7a90d833efda6cfa6eac0f4949dbb0fad3a63206" + dependencies: + once "^1.4.0" + +enhanced-resolve@^3.0.0, enhanced-resolve@^3.4.0: + version "3.4.1" + resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-3.4.1.tgz#0421e339fd71419b3da13d129b3979040230476e" + dependencies: + graceful-fs "^4.1.2" + memory-fs "^0.4.0" + object-assign "^4.0.1" + tapable "^0.2.7" + +errno@^0.1.1, errno@^0.1.3, errno@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/errno/-/errno-0.1.4.tgz#b896e23a9e5e8ba33871fc996abd3635fc9a1c7d" + dependencies: + prr "~0.0.0" + +error-ex@^1.2.0: + version "1.3.1" + resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.1.tgz#f855a86ce61adc4e8621c3cda21e7a7612c3a8dc" + dependencies: + is-arrayish "^0.2.1" + +es5-ext@^0.10.14, es5-ext@^0.10.9, es5-ext@~0.10.14: + version "0.10.30" + resolved "https://registry.yarnpkg.com/es5-ext/-/es5-ext-0.10.30.tgz#7141a16836697dbabfaaaeee41495ce29f52c939" + dependencies: + es6-iterator "2" + es6-symbol "~3.1" + +es6-iterator@2, es6-iterator@^2.0.1, es6-iterator@~2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/es6-iterator/-/es6-iterator-2.0.1.tgz#8e319c9f0453bf575d374940a655920e59ca5512" + dependencies: + d "1" + es5-ext "^0.10.14" + es6-symbol "^3.1" + +es6-map@^0.1.3: + version "0.1.5" + resolved "https://registry.yarnpkg.com/es6-map/-/es6-map-0.1.5.tgz#9136e0503dcc06a301690f0bb14ff4e364e949f0" + dependencies: + d "1" + es5-ext "~0.10.14" + es6-iterator "~2.0.1" + es6-set "~0.1.5" + es6-symbol "~3.1.1" + event-emitter "~0.3.5" + +es6-set@~0.1.5: + version "0.1.5" + resolved "https://registry.yarnpkg.com/es6-set/-/es6-set-0.1.5.tgz#d2b3ec5d4d800ced818db538d28974db0a73ccb1" + dependencies: + d "1" + es5-ext "~0.10.14" + es6-iterator "~2.0.1" + es6-symbol "3.1.1" + event-emitter "~0.3.5" + +es6-symbol@3.1.1, es6-symbol@^3.1, es6-symbol@^3.1.1, es6-symbol@~3.1, es6-symbol@~3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/es6-symbol/-/es6-symbol-3.1.1.tgz#bf00ef4fdab6ba1b46ecb7b629b4c7ed5715cc77" + dependencies: + d "1" + es5-ext "~0.10.14" + +es6-weak-map@^2.0.1: + version "2.0.2" + resolved "https://registry.yarnpkg.com/es6-weak-map/-/es6-weak-map-2.0.2.tgz#5e3ab32251ffd1538a1f8e5ffa1357772f92d96f" + dependencies: + d "1" + es5-ext "^0.10.14" + es6-iterator "^2.0.1" + es6-symbol "^3.1.1" + +escape-string-regexp@^1.0.2, escape-string-regexp@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" + +escope@^3.6.0: + version "3.6.0" + resolved "https://registry.yarnpkg.com/escope/-/escope-3.6.0.tgz#e01975e812781a163a6dadfdd80398dc64c889c3" + dependencies: + es6-map "^0.1.3" + es6-weak-map "^2.0.1" + esrecurse "^4.1.0" + estraverse "^4.1.1" + +esprima@^2.6.0: + version "2.7.3" + resolved "https://registry.yarnpkg.com/esprima/-/esprima-2.7.3.tgz#96e3b70d5779f6ad49cd032673d1c312767ba581" + +esprima@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.0.tgz#4499eddcd1110e0b218bacf2fa7f7f59f55ca804" + +esrecurse@^4.1.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.2.0.tgz#fa9568d98d3823f9a41d91e902dcab9ea6e5b163" + dependencies: + estraverse "^4.1.0" + object-assign "^4.0.1" + +estraverse@^4.1.0, estraverse@^4.1.1: + version "4.2.0" + resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.2.0.tgz#0dee3fed31fcd469618ce7342099fc1afa0bdb13" + +esutils@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.2.tgz#0abf4f1caa5bcb1f7a9d8acc6dea4faaa04bac9b" + +event-emitter@~0.3.5: + version "0.3.5" + resolved "https://registry.yarnpkg.com/event-emitter/-/event-emitter-0.3.5.tgz#df8c69eef1647923c7157b9ce83840610b02cc39" + dependencies: + d "1" + es5-ext "~0.10.14" + +events@^1.0.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/events/-/events-1.1.1.tgz#9ebdb7635ad099c70dcc4c2a1f5004288e8bd924" + +evp_bytestokey@^1.0.0, evp_bytestokey@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz#7fcbdb198dc71959432efe13842684e0525acb02" + dependencies: + md5.js "^1.3.4" + safe-buffer "^5.1.1" + +execa@^0.7.0: + version "0.7.0" + resolved "https://registry.yarnpkg.com/execa/-/execa-0.7.0.tgz#944becd34cc41ee32a63a9faf27ad5a65fc59777" + dependencies: + cross-spawn "^5.0.1" + get-stream "^3.0.0" + is-stream "^1.1.0" + npm-run-path "^2.0.0" + p-finally "^1.0.0" + signal-exit "^3.0.0" + strip-eof "^1.0.0" + +expand-brackets@^0.1.4: + version "0.1.5" + resolved "https://registry.yarnpkg.com/expand-brackets/-/expand-brackets-0.1.5.tgz#df07284e342a807cd733ac5af72411e581d1177b" + dependencies: + is-posix-bracket "^0.1.0" + +expand-range@^1.8.1: + version "1.8.2" + resolved "https://registry.yarnpkg.com/expand-range/-/expand-range-1.8.2.tgz#a299effd335fe2721ebae8e257ec79644fc85337" + dependencies: + fill-range "^2.1.0" + +extend@~3.0.0, extend@~3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.1.tgz#a755ea7bc1adfcc5a31ce7e762dbaadc5e636444" + +extglob@^0.3.1: + version "0.3.2" + resolved "https://registry.yarnpkg.com/extglob/-/extglob-0.3.2.tgz#2e18ff3d2f49ab2765cec9023f011daa8d8349a1" + dependencies: + is-extglob "^1.0.0" + +extsprintf@1.3.0, extsprintf@^1.2.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.3.0.tgz#96918440e3041a7a414f8c52e3c574eb3c3e1e05" + +fast-deep-equal@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-1.0.0.tgz#96256a3bc975595eb36d82e9929d060d893439ff" + +fastparse@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/fastparse/-/fastparse-1.1.1.tgz#d1e2643b38a94d7583b479060e6c4affc94071f8" + +file-loader@^0.11.2: + version "0.11.2" + resolved "https://registry.yarnpkg.com/file-loader/-/file-loader-0.11.2.tgz#4ff1df28af38719a6098093b88c82c71d1794a34" + dependencies: + loader-utils "^1.0.2" + +filename-regex@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/filename-regex/-/filename-regex-2.0.1.tgz#c1c4b9bee3e09725ddb106b75c1e301fe2f18b26" + +fill-range@^2.1.0: + version "2.2.3" + resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-2.2.3.tgz#50b77dfd7e469bc7492470963699fe7a8485a723" + dependencies: + is-number "^2.1.0" + isobject "^2.0.0" + randomatic "^1.1.3" + repeat-element "^1.1.2" + repeat-string "^1.5.2" + +find-cache-dir@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/find-cache-dir/-/find-cache-dir-1.0.0.tgz#9288e3e9e3cc3748717d39eade17cf71fc30ee6f" + dependencies: + commondir "^1.0.1" + make-dir "^1.0.0" + pkg-dir "^2.0.0" + +find-up@^2.0.0, find-up@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-2.1.0.tgz#45d1b7e506c717ddd482775a2b77920a3c0c57a7" + dependencies: + locate-path "^2.0.0" + +flatten@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/flatten/-/flatten-1.0.2.tgz#dae46a9d78fbe25292258cc1e780a41d95c03782" + +flush-write-stream@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/flush-write-stream/-/flush-write-stream-1.0.2.tgz#c81b90d8746766f1a609a46809946c45dd8ae417" + dependencies: + inherits "^2.0.1" + readable-stream "^2.0.4" + +follow-redirects@^1.2.3: + version "1.2.4" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.2.4.tgz#355e8f4d16876b43f577b0d5ce2668b9723214ea" + dependencies: + debug "^2.4.5" + +font-awesome@^4.7.0: + version "4.7.0" + resolved "https://registry.yarnpkg.com/font-awesome/-/font-awesome-4.7.0.tgz#8fa8cf0411a1a31afd07b06d2902bb9fc815a133" + +for-in@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/for-in/-/for-in-1.0.2.tgz#81068d295a8142ec0ac726c6e2200c30fb6d5e80" + +for-own@^0.1.4: + version "0.1.5" + resolved "https://registry.yarnpkg.com/for-own/-/for-own-0.1.5.tgz#5265c681a4f294dabbf17c9509b6763aa84510ce" + dependencies: + for-in "^1.0.1" + +forever-agent@~0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91" + +fork-ts-checker-webpack-plugin@^0.2.8: + version "0.2.8" + resolved "https://registry.yarnpkg.com/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-0.2.8.tgz#66dc841c29ff8345e0a30755ddeb4ccc3213e210" + dependencies: + babel-code-frame "^6.22.0" + chalk "^1.1.3" + chokidar "^1.7.0" + lodash.endswith "^4.2.1" + lodash.isfunction "^3.0.8" + lodash.isstring "^4.0.1" + lodash.startswith "^4.2.1" + +form-data@~2.1.1: + version "2.1.4" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.1.4.tgz#33c183acf193276ecaa98143a69e94bfee1750d1" + dependencies: + asynckit "^0.4.0" + combined-stream "^1.0.5" + mime-types "^2.1.12" + +form-data@~2.3.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.3.1.tgz#6fb94fbd71885306d73d15cc497fe4cc4ecd44bf" + dependencies: + asynckit "^0.4.0" + combined-stream "^1.0.5" + mime-types "^2.1.12" + +from2@^2.1.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/from2/-/from2-2.3.0.tgz#8bfb5502bde4a4d36cfdeea007fcca21d7e382af" + dependencies: + inherits "^2.0.1" + readable-stream "^2.0.0" + +fs-write-stream-atomic@^1.0.8: + version "1.0.10" + resolved "https://registry.yarnpkg.com/fs-write-stream-atomic/-/fs-write-stream-atomic-1.0.10.tgz#b47df53493ef911df75731e70a9ded0189db40c9" + dependencies: + graceful-fs "^4.1.2" + iferr "^0.1.5" + imurmurhash "^0.1.4" + readable-stream "1 || 2" + +fs.realpath@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" + +fsevents@^1.0.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-1.1.2.tgz#3282b713fb3ad80ede0e9fcf4611b5aa6fc033f4" + dependencies: + nan "^2.3.0" + node-pre-gyp "^0.6.36" + +fstream-ignore@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/fstream-ignore/-/fstream-ignore-1.0.5.tgz#9c31dae34767018fe1d249b24dada67d092da105" + dependencies: + fstream "^1.0.0" + inherits "2" + minimatch "^3.0.0" + +fstream@^1.0.0, fstream@^1.0.10, fstream@^1.0.2: + version "1.0.11" + resolved "https://registry.yarnpkg.com/fstream/-/fstream-1.0.11.tgz#5c1fb1f117477114f0632a0eb4b71b3cb0fd3171" + dependencies: + graceful-fs "^4.1.2" + inherits "~2.0.0" + mkdirp ">=0.5 0" + rimraf "2" + +function-bind@^1.0.2: + version "1.1.1" + resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" + +gauge@~2.7.3: + version "2.7.4" + resolved "https://registry.yarnpkg.com/gauge/-/gauge-2.7.4.tgz#2c03405c7538c39d7eb37b317022e325fb018bf7" + dependencies: + aproba "^1.0.3" + console-control-strings "^1.0.0" + has-unicode "^2.0.0" + object-assign "^4.1.0" + signal-exit "^3.0.0" + string-width "^1.0.1" + strip-ansi "^3.0.1" + wide-align "^1.1.0" + +get-caller-file@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-1.0.2.tgz#f702e63127e7e231c160a80c1554acb70d5047e5" + +get-stream@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-3.0.0.tgz#8e943d1358dc37555054ecbe2edb05aa174ede14" + +getpass@^0.1.1: + version "0.1.7" + resolved "https://registry.yarnpkg.com/getpass/-/getpass-0.1.7.tgz#5eff8e3e684d569ae4cb2b1282604e8ba62149fa" + dependencies: + assert-plus "^1.0.0" + +glob-base@^0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/glob-base/-/glob-base-0.3.0.tgz#dbb164f6221b1c0b1ccf82aea328b497df0ea3c4" + dependencies: + glob-parent "^2.0.0" + is-glob "^2.0.0" + +glob-parent@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-2.0.0.tgz#81383d72db054fcccf5336daa902f182f6edbb28" + dependencies: + is-glob "^2.0.0" + +glob@^7.0.5, glob@^7.1.1, glob@^7.1.2: + version "7.1.2" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.2.tgz#c19c9df9a028702d678612384a6552404c636d15" + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.0.4" + once "^1.3.0" + path-is-absolute "^1.0.0" + +graceful-fs@^4.1.11, graceful-fs@^4.1.2: + version "4.1.11" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.11.tgz#0e8bdfe4d1ddb8854d64e04ea7c00e2a026e5658" + +har-schema@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-1.0.5.tgz#d263135f43307c02c602afc8fe95970c0151369e" + +har-schema@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-2.0.0.tgz#a94c2224ebcac04782a0d9035521f24735b7ec92" + +har-validator@~4.2.1: + version "4.2.1" + resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-4.2.1.tgz#33481d0f1bbff600dd203d75812a6a5fba002e2a" + dependencies: + ajv "^4.9.1" + har-schema "^1.0.5" + +har-validator@~5.0.3: + version "5.0.3" + resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-5.0.3.tgz#ba402c266194f15956ef15e0fcf242993f6a7dfd" + dependencies: + ajv "^5.1.0" + har-schema "^2.0.0" + +has-ansi@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/has-ansi/-/has-ansi-2.0.0.tgz#34f5049ce1ecdf2b0649af3ef24e45ed35416d91" + dependencies: + ansi-regex "^2.0.0" + +has-flag@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-1.0.0.tgz#9d9e793165ce017a00f00418c43f942a7b1d11fa" + +has-flag@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-2.0.0.tgz#e8207af1cc7b30d446cc70b734b5e8be18f88d51" + +has-unicode@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/has-unicode/-/has-unicode-2.0.1.tgz#e0e6fe6a28cf51138855e086d1691e771de2a8b9" + +has@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/has/-/has-1.0.1.tgz#8461733f538b0837c9361e39a9ab9e9704dc2f28" + dependencies: + function-bind "^1.0.2" + +hash-base@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/hash-base/-/hash-base-2.0.2.tgz#66ea1d856db4e8a5470cadf6fce23ae5244ef2e1" + dependencies: + inherits "^2.0.1" + +hash-base@^3.0.0: + version "3.0.4" + resolved "https://registry.yarnpkg.com/hash-base/-/hash-base-3.0.4.tgz#5fc8686847ecd73499403319a6b0a3f3f6ae4918" + dependencies: + inherits "^2.0.1" + safe-buffer "^5.0.1" + +hash-sum@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/hash-sum/-/hash-sum-1.0.2.tgz#33b40777754c6432573c120cc3808bbd10d47f04" + +hash.js@^1.0.0, hash.js@^1.0.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/hash.js/-/hash.js-1.1.3.tgz#340dedbe6290187151c1ea1d777a3448935df846" + dependencies: + inherits "^2.0.3" + minimalistic-assert "^1.0.0" + +hawk@3.1.3, hawk@~3.1.3: + version "3.1.3" + resolved "https://registry.yarnpkg.com/hawk/-/hawk-3.1.3.tgz#078444bd7c1640b0fe540d2c9b73d59678e8e1c4" + dependencies: + boom "2.x.x" + cryptiles "2.x.x" + hoek "2.x.x" + sntp "1.x.x" + +hawk@~6.0.2: + version "6.0.2" + resolved "https://registry.yarnpkg.com/hawk/-/hawk-6.0.2.tgz#af4d914eb065f9b5ce4d9d11c1cb2126eecc3038" + dependencies: + boom "4.x.x" + cryptiles "3.x.x" + hoek "4.x.x" + sntp "2.x.x" + +he@^1.1.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/he/-/he-1.1.1.tgz#93410fd21b009735151f8868c2f271f3427e23fd" + +hmac-drbg@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/hmac-drbg/-/hmac-drbg-1.0.1.tgz#d2745701025a6c775a6c545793ed502fc0c649a1" + dependencies: + hash.js "^1.0.3" + minimalistic-assert "^1.0.0" + minimalistic-crypto-utils "^1.0.1" + +hoek@2.x.x: + version "2.16.3" + resolved "https://registry.yarnpkg.com/hoek/-/hoek-2.16.3.tgz#20bb7403d3cea398e91dc4710a8ff1b8274a25ed" + +hoek@4.x.x: + version "4.2.0" + resolved "https://registry.yarnpkg.com/hoek/-/hoek-4.2.0.tgz#72d9d0754f7fe25ca2d01ad8f8f9a9449a89526d" + +hosted-git-info@^2.1.4: + version "2.5.0" + resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.5.0.tgz#6d60e34b3abbc8313062c3b798ef8d901a07af3c" + +html-comment-regex@^1.1.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/html-comment-regex/-/html-comment-regex-1.1.1.tgz#668b93776eaae55ebde8f3ad464b307a4963625e" + +http-signature@~1.1.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.1.1.tgz#df72e267066cd0ac67fb76adf8e134a8fbcf91bf" + dependencies: + assert-plus "^0.2.0" + jsprim "^1.2.2" + sshpk "^1.7.0" + +http-signature@~1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.2.0.tgz#9aecd925114772f3d95b65a60abb8f7c18fbace1" + dependencies: + assert-plus "^1.0.0" + jsprim "^1.2.2" + sshpk "^1.7.0" + +https-browserify@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/https-browserify/-/https-browserify-0.0.1.tgz#3f91365cabe60b77ed0ebba24b454e3e09d95a82" + +icss-replace-symbols@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/icss-replace-symbols/-/icss-replace-symbols-1.1.0.tgz#06ea6f83679a7749e386cfe1fe812ae5db223ded" + +icss-utils@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/icss-utils/-/icss-utils-2.1.0.tgz#83f0a0ec378bf3246178b6c2ad9136f135b1c962" + dependencies: + postcss "^6.0.1" + +ieee754@^1.1.4: + version "1.1.8" + resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.1.8.tgz#be33d40ac10ef1926701f6f08a2d86fbfd1ad3e4" + +iferr@^0.1.5: + version "0.1.5" + resolved "https://registry.yarnpkg.com/iferr/-/iferr-0.1.5.tgz#c60eed69e6d8fdb6b3104a1fcbca1c192dc5b501" + +image-size@~0.5.0: + version "0.5.5" + resolved "https://registry.yarnpkg.com/image-size/-/image-size-0.5.5.tgz#09dfd4ab9d20e29eb1c3e80b8990378df9e3cb9c" + +imurmurhash@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" + +indexes-of@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/indexes-of/-/indexes-of-1.0.1.tgz#f30f716c8e2bd346c7b67d3df3915566a7c05607" + +indexof@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/indexof/-/indexof-0.0.1.tgz#82dc336d232b9062179d05ab3293a66059fd435d" + +inflight@^1.0.4: + version "1.0.6" + resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" + dependencies: + once "^1.3.0" + wrappy "1" + +inherits@2, inherits@^2.0.1, inherits@^2.0.3, inherits@~2.0.0, inherits@~2.0.1, inherits@~2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" + +inherits@2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.1.tgz#b17d08d326b4423e568eff719f91b0b1cbdf69f1" + +ini@~1.3.0: + version "1.3.4" + resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.4.tgz#0537cb79daf59b59a1a517dff706c86ec039162e" + +interpret@^1.0.0: + version "1.0.4" + resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.0.4.tgz#820cdd588b868ffb191a809506d6c9c8f212b1b0" + +invert-kv@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/invert-kv/-/invert-kv-1.0.0.tgz#104a8e4aaca6d3d8cd157a8ef8bfab2d7a3ffdb6" + +is-absolute-url@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-absolute-url/-/is-absolute-url-2.1.0.tgz#50530dfb84fcc9aa7dbe7852e83a37b93b9f2aa6" + +is-arrayish@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" + +is-binary-path@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-1.0.1.tgz#75f16642b480f187a711c814161fd3a4a7655898" + dependencies: + binary-extensions "^1.0.0" + +is-buffer@^1.1.5: + version "1.1.5" + resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.5.tgz#1f3b26ef613b214b88cbca23cc6c01d87961eecc" + +is-builtin-module@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-builtin-module/-/is-builtin-module-1.0.0.tgz#540572d34f7ac3119f8f76c30cbc1b1e037affbe" + dependencies: + builtin-modules "^1.0.0" + +is-directory@^0.3.1: + version "0.3.1" + resolved "https://registry.yarnpkg.com/is-directory/-/is-directory-0.3.1.tgz#61339b6f2475fc772fd9c9d83f5c8575dc154ae1" + +is-dotfile@^1.0.0: + version "1.0.3" + resolved "https://registry.yarnpkg.com/is-dotfile/-/is-dotfile-1.0.3.tgz#a6a2f32ffd2dfb04f5ca25ecd0f6b83cf798a1e1" + +is-equal-shallow@^0.1.3: + version "0.1.3" + resolved "https://registry.yarnpkg.com/is-equal-shallow/-/is-equal-shallow-0.1.3.tgz#2238098fc221de0bcfa5d9eac4c45d638aa1c534" + dependencies: + is-primitive "^2.0.0" + +is-extendable@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/is-extendable/-/is-extendable-0.1.1.tgz#62b110e289a471418e3ec36a617d472e301dfc89" + +is-extglob@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-1.0.0.tgz#ac468177c4943405a092fc8f29760c6ffc6206c0" + +is-fullwidth-code-point@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz#ef9e31386f031a7f0d643af82fde50c457ef00cb" + dependencies: + number-is-nan "^1.0.0" + +is-fullwidth-code-point@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz#a3b30a5c4f199183167aaab93beefae3ddfb654f" + +is-glob@^2.0.0, is-glob@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-2.0.1.tgz#d096f926a3ded5600f3fdfd91198cb0888c2d863" + dependencies: + is-extglob "^1.0.0" + +is-number@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-number/-/is-number-2.1.0.tgz#01fcbbb393463a548f2f466cce16dece49db908f" + dependencies: + kind-of "^3.0.2" + +is-number@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/is-number/-/is-number-3.0.0.tgz#24fd6201a4782cf50561c810276afc7d12d71195" + dependencies: + kind-of "^3.0.2" + +is-plain-obj@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-1.1.0.tgz#71a50c8429dfca773c92a390a4a03b39fcd51d3e" + +is-posix-bracket@^0.1.0: + version "0.1.1" + resolved "https://registry.yarnpkg.com/is-posix-bracket/-/is-posix-bracket-0.1.1.tgz#3334dc79774368e92f016e6fbc0a88f5cd6e6bc4" + +is-primitive@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/is-primitive/-/is-primitive-2.0.0.tgz#207bab91638499c07b2adf240a41a87210034575" + +is-stream@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44" + +is-svg@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-svg/-/is-svg-2.1.0.tgz#cf61090da0d9efbcab8722deba6f032208dbb0e9" + dependencies: + html-comment-regex "^1.1.0" + +is-typedarray@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" + +isarray@1.0.0, isarray@^1.0.0, isarray@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" + +isexe@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" + +isobject@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/isobject/-/isobject-2.1.0.tgz#f065561096a3f1da2ef46272f815c840d87e0c89" + dependencies: + isarray "1.0.0" + +isstream@~0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a" + +jquery@^3.2.1: + version "3.2.1" + resolved "https://registry.yarnpkg.com/jquery/-/jquery-3.2.1.tgz#5c4d9de652af6cd0a770154a631bba12b015c787" + +js-base64@^2.1.9: + version "2.3.2" + resolved "https://registry.yarnpkg.com/js-base64/-/js-base64-2.3.2.tgz#a79a923666372b580f8e27f51845c6f7e8fbfbaf" + +js-tokens@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-3.0.2.tgz#9866df395102130e38f7f996bceb65443209c25b" + +js-yaml@^3.4.3: + version "3.10.0" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.10.0.tgz#2e78441646bd4682e963f22b6e92823c309c62dc" + dependencies: + argparse "^1.0.7" + esprima "^4.0.0" + +js-yaml@~3.7.0: + version "3.7.0" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.7.0.tgz#5c967ddd837a9bfdca5f2de84253abe8a1c03b80" + dependencies: + argparse "^1.0.7" + esprima "^2.6.0" + +jsbn@~0.1.0: + version "0.1.1" + resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513" + +jsesc@~0.5.0: + version "0.5.0" + resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-0.5.0.tgz#e7dee66e35d6fc16f710fe91d5cf69f70f08911d" + +json-loader@^0.5.4: + version "0.5.7" + resolved "https://registry.yarnpkg.com/json-loader/-/json-loader-0.5.7.tgz#dca14a70235ff82f0ac9a3abeb60d337a365185d" + +json-schema-traverse@^0.3.0: + version "0.3.1" + resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz#349a6d44c53a51de89b40805c5d5e59b417d3340" + +json-schema@0.2.3: + version "0.2.3" + resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.2.3.tgz#b480c892e59a2f05954ce727bd3f2a4e882f9e13" + +json-stable-stringify@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/json-stable-stringify/-/json-stable-stringify-1.0.1.tgz#9a759d39c5f2ff503fd5300646ed445f88c4f9af" + dependencies: + jsonify "~0.0.0" + +json-stringify-safe@~5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb" + +json5@^0.5.0, json5@^0.5.1: + version "0.5.1" + resolved "https://registry.yarnpkg.com/json5/-/json5-0.5.1.tgz#1eade7acc012034ad84e2396767ead9fa5495821" + +jsonify@~0.0.0: + version "0.0.0" + resolved "https://registry.yarnpkg.com/jsonify/-/jsonify-0.0.0.tgz#2c74b6ee41d93ca51b7b5aaee8f503631d252a73" + +jsprim@^1.2.2: + version "1.4.1" + resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.1.tgz#313e66bc1e5cc06e438bc1b7499c2e5c56acb6a2" + dependencies: + assert-plus "1.0.0" + extsprintf "1.3.0" + json-schema "0.2.3" + verror "1.10.0" + +kind-of@^3.0.2: + version "3.2.2" + resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-3.2.2.tgz#31ea21a734bab9bbb0f32466d893aea51e4a3c64" + dependencies: + is-buffer "^1.1.5" + +kind-of@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-4.0.0.tgz#20813df3d712928b207378691a45066fae72dd57" + dependencies: + is-buffer "^1.1.5" + +lazy-cache@^1.0.3: + version "1.0.4" + resolved "https://registry.yarnpkg.com/lazy-cache/-/lazy-cache-1.0.4.tgz#a1d78fc3a50474cb80845d3b3b6e1da49a446e8e" + +lcid@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/lcid/-/lcid-1.0.0.tgz#308accafa0bc483a3867b4b6f2b9506251d1b835" + dependencies: + invert-kv "^1.0.0" + +less-loader@^4.0.4: + version "4.0.5" + resolved "https://registry.yarnpkg.com/less-loader/-/less-loader-4.0.5.tgz#ae155a7406cac6acd293d785587fcff0f478c4dd" + dependencies: + clone "^2.1.1" + loader-utils "^1.1.0" + pify "^2.3.0" + +less@^2.7.2: + version "2.7.2" + resolved "https://registry.yarnpkg.com/less/-/less-2.7.2.tgz#368d6cc73e1fb03981183280918743c5dcf9b3df" + optionalDependencies: + errno "^0.1.1" + graceful-fs "^4.1.2" + image-size "~0.5.0" + mime "^1.2.11" + mkdirp "^0.5.0" + promise "^7.1.1" + request "^2.72.0" + source-map "^0.5.3" + +load-json-file@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-2.0.0.tgz#7947e42149af80d696cbf797bcaabcfe1fe29ca8" + dependencies: + graceful-fs "^4.1.2" + parse-json "^2.2.0" + pify "^2.0.0" + strip-bom "^3.0.0" + +loader-runner@^2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/loader-runner/-/loader-runner-2.3.0.tgz#f482aea82d543e07921700d5a46ef26fdac6b8a2" + +loader-utils@^1.0.2, loader-utils@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-1.1.0.tgz#c98aef488bcceda2ffb5e2de646d6a754429f5cd" + dependencies: + big.js "^3.1.3" + emojis-list "^2.0.0" + json5 "^0.5.0" + +locate-path@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-2.0.0.tgz#2b568b265eec944c6d9c0de9c3dbbbca0354cd8e" + dependencies: + p-locate "^2.0.0" + path-exists "^3.0.0" + +lodash.camelcase@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz#b28aa6288a2b9fc651035c7711f65ab6190331a6" + +lodash.endswith@^4.2.1: + version "4.2.1" + resolved "https://registry.yarnpkg.com/lodash.endswith/-/lodash.endswith-4.2.1.tgz#fed59ac1738ed3e236edd7064ec456448b37bc09" + +lodash.isfunction@^3.0.8: + version "3.0.8" + resolved "https://registry.yarnpkg.com/lodash.isfunction/-/lodash.isfunction-3.0.8.tgz#4db709fc81bc4a8fd7127a458a5346c5cdce2c6b" + +lodash.isstring@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/lodash.isstring/-/lodash.isstring-4.0.1.tgz#d527dfb5456eca7cc9bb95d5daeaf88ba54a5451" + +lodash.memoize@^4.1.2: + version "4.1.2" + resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe" + +lodash.startswith@^4.2.1: + version "4.2.1" + resolved "https://registry.yarnpkg.com/lodash.startswith/-/lodash.startswith-4.2.1.tgz#c598c4adce188a27e53145731cdc6c0e7177600c" + +lodash.uniq@^4.5.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773" + +lodash@^4.14.0: + version "4.17.4" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.4.tgz#78203a4d1c328ae1d86dca6460e369b57f4055ae" + +longest@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/longest/-/longest-1.0.1.tgz#30a0b2da38f73770e8294a0d22e6625ed77d0097" + +lru-cache@^4.0.1, lru-cache@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-4.1.1.tgz#622e32e82488b49279114a4f9ecf45e7cd6bba55" + dependencies: + pseudomap "^1.0.2" + yallist "^2.1.2" + +macaddress@^0.2.8: + version "0.2.8" + resolved "https://registry.yarnpkg.com/macaddress/-/macaddress-0.2.8.tgz#5904dc537c39ec6dbefeae902327135fa8511f12" + +make-dir@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-1.0.0.tgz#97a011751e91dd87cfadef58832ebb04936de978" + dependencies: + pify "^2.3.0" + +math-expression-evaluator@^1.2.14: + version "1.2.17" + resolved "https://registry.yarnpkg.com/math-expression-evaluator/-/math-expression-evaluator-1.2.17.tgz#de819fdbcd84dccd8fae59c6aeb79615b9d266ac" + +md5.js@^1.3.4: + version "1.3.4" + resolved "https://registry.yarnpkg.com/md5.js/-/md5.js-1.3.4.tgz#e9bdbde94a20a5ac18b04340fc5764d5b09d901d" + dependencies: + hash-base "^3.0.0" + inherits "^2.0.1" + +mem@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/mem/-/mem-1.1.0.tgz#5edd52b485ca1d900fe64895505399a0dfa45f76" + dependencies: + mimic-fn "^1.0.0" + +memory-fs@^0.4.0, memory-fs@~0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/memory-fs/-/memory-fs-0.4.1.tgz#3a9a20b8462523e447cfbc7e8bb80ed667bfc552" + dependencies: + errno "^0.1.3" + readable-stream "^2.0.1" + +micromatch@^2.1.5: + version "2.3.11" + resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-2.3.11.tgz#86677c97d1720b363431d04d0d15293bd38c1565" + dependencies: + arr-diff "^2.0.0" + array-unique "^0.2.1" + braces "^1.8.2" + expand-brackets "^0.1.4" + extglob "^0.3.1" + filename-regex "^2.0.0" + is-extglob "^1.0.0" + is-glob "^2.0.1" + kind-of "^3.0.2" + normalize-path "^2.0.1" + object.omit "^2.0.0" + parse-glob "^3.0.4" + regex-cache "^0.4.2" + +miller-rabin@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/miller-rabin/-/miller-rabin-4.0.1.tgz#f080351c865b0dc562a8462966daa53543c78a4d" + dependencies: + bn.js "^4.0.0" + brorand "^1.0.1" + +mime-db@~1.30.0: + version "1.30.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.30.0.tgz#74c643da2dd9d6a45399963465b26d5ca7d71f01" + +mime-types@^2.1.12, mime-types@~2.1.17, mime-types@~2.1.7: + version "2.1.17" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.17.tgz#09d7a393f03e995a79f8af857b70a9e0ab16557a" + dependencies: + mime-db "~1.30.0" + +mime@1.3.x: + version "1.3.6" + resolved "https://registry.yarnpkg.com/mime/-/mime-1.3.6.tgz#591d84d3653a6b0b4a3b9df8de5aa8108e72e5e0" + +mime@^1.2.11: + version "1.4.1" + resolved "https://registry.yarnpkg.com/mime/-/mime-1.4.1.tgz#121f9ebc49e3766f311a76e1fa1c8003c4b03aa6" + +mimic-fn@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-1.1.0.tgz#e667783d92e89dbd342818b5230b9d62a672ad18" + +minimalistic-assert@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.0.tgz#702be2dda6b37f4836bcb3f5db56641b64a1d3d3" + +minimalistic-crypto-utils@^1.0.0, minimalistic-crypto-utils@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz#f6c00c1c0b082246e5c4d99dfb8c7c083b2b582a" + +minimatch@^3.0.0, minimatch@^3.0.2, minimatch@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" + dependencies: + brace-expansion "^1.1.7" + +minimist@0.0.8: + version "0.0.8" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d" + +minimist@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284" + +mississippi@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/mississippi/-/mississippi-1.3.0.tgz#d201583eb12327e3c5c1642a404a9cacf94e34f5" + dependencies: + concat-stream "^1.5.0" + duplexify "^3.4.2" + end-of-stream "^1.1.0" + flush-write-stream "^1.0.0" + from2 "^2.1.0" + parallel-transform "^1.1.0" + pump "^1.0.0" + pumpify "^1.3.3" + stream-each "^1.1.0" + through2 "^2.0.0" + +"mkdirp@>=0.5 0", mkdirp@^0.5.0, mkdirp@^0.5.1, mkdirp@~0.5.0, mkdirp@~0.5.1: + version "0.5.1" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903" + dependencies: + minimist "0.0.8" + +move-concurrently@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/move-concurrently/-/move-concurrently-1.0.1.tgz#be2c005fda32e0b29af1f05d7c4b33214c701f92" + dependencies: + aproba "^1.1.1" + copy-concurrently "^1.0.0" + fs-write-stream-atomic "^1.0.8" + mkdirp "^0.5.1" + rimraf "^2.5.4" + run-queue "^1.0.3" + +ms@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" + +nan@^2.3.0: + version "2.7.0" + resolved "https://registry.yarnpkg.com/nan/-/nan-2.7.0.tgz#d95bf721ec877e08db276ed3fc6eb78f9083ad46" + +node-libs-browser@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/node-libs-browser/-/node-libs-browser-2.0.0.tgz#a3a59ec97024985b46e958379646f96c4b616646" + dependencies: + assert "^1.1.1" + browserify-zlib "^0.1.4" + buffer "^4.3.0" + console-browserify "^1.1.0" + constants-browserify "^1.0.0" + crypto-browserify "^3.11.0" + domain-browser "^1.1.1" + events "^1.0.0" + https-browserify "0.0.1" + os-browserify "^0.2.0" + path-browserify "0.0.0" + process "^0.11.0" + punycode "^1.2.4" + querystring-es3 "^0.2.0" + readable-stream "^2.0.5" + stream-browserify "^2.0.1" + stream-http "^2.3.1" + string_decoder "^0.10.25" + timers-browserify "^2.0.2" + tty-browserify "0.0.0" + url "^0.11.0" + util "^0.10.3" + vm-browserify "0.0.4" + +node-pre-gyp@^0.6.36: + version "0.6.38" + resolved "https://registry.yarnpkg.com/node-pre-gyp/-/node-pre-gyp-0.6.38.tgz#e92a20f83416415bb4086f6d1fb78b3da73d113d" + dependencies: + hawk "3.1.3" + mkdirp "^0.5.1" + nopt "^4.0.1" + npmlog "^4.0.2" + rc "^1.1.7" + request "2.81.0" + rimraf "^2.6.1" + semver "^5.3.0" + tar "^2.2.1" + tar-pack "^3.4.0" + +nopt@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/nopt/-/nopt-4.0.1.tgz#d0d4685afd5415193c8c7505602d0d17cd64474d" + dependencies: + abbrev "1" + osenv "^0.1.4" + +normalize-package-data@^2.3.2: + version "2.4.0" + resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.4.0.tgz#12f95a307d58352075a04907b84ac8be98ac012f" + dependencies: + hosted-git-info "^2.1.4" + is-builtin-module "^1.0.0" + semver "2 || 3 || 4 || 5" + validate-npm-package-license "^3.0.1" + +normalize-path@^2.0.0, normalize-path@^2.0.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-2.1.1.tgz#1ab28b556e198363a8c1a6f7e6fa20137fe6aed9" + dependencies: + remove-trailing-separator "^1.0.1" + +normalize-range@^0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/normalize-range/-/normalize-range-0.1.2.tgz#2d10c06bdfd312ea9777695a4d28439456b75942" + +normalize-url@^1.4.0: + version "1.9.1" + resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-1.9.1.tgz#2cc0d66b31ea23036458436e3620d85954c66c3c" + dependencies: + object-assign "^4.0.1" + prepend-http "^1.0.0" + query-string "^4.1.0" + sort-keys "^1.0.0" + +npm-run-path@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-2.0.2.tgz#35a9232dfa35d7067b4cb2ddf2357b1871536c5f" + dependencies: + path-key "^2.0.0" + +npmlog@^4.0.2: + version "4.1.2" + resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-4.1.2.tgz#08a7f2a8bf734604779a9efa4ad5cc717abb954b" + dependencies: + are-we-there-yet "~1.1.2" + console-control-strings "~1.1.0" + gauge "~2.7.3" + set-blocking "~2.0.0" + +num2fraction@^1.2.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/num2fraction/-/num2fraction-1.2.2.tgz#6f682b6a027a4e9ddfa4564cd2589d1d4e669ede" + +number-is-nan@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/number-is-nan/-/number-is-nan-1.0.1.tgz#097b602b53422a522c1afb8790318336941a011d" + +oauth-sign@~0.8.1, oauth-sign@~0.8.2: + version "0.8.2" + resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.8.2.tgz#46a6ab7f0aead8deae9ec0565780b7d4efeb9d43" + +object-assign@^4.0.1, object-assign@^4.1.0: + version "4.1.1" + resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" + +object.omit@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/object.omit/-/object.omit-2.0.1.tgz#1a9c744829f39dbb858c76ca3579ae2a54ebd1fa" + dependencies: + for-own "^0.1.4" + is-extendable "^0.1.1" + +once@^1.3.0, once@^1.3.1, once@^1.3.3, once@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" + dependencies: + wrappy "1" + +os-browserify@^0.2.0: + version "0.2.1" + resolved "https://registry.yarnpkg.com/os-browserify/-/os-browserify-0.2.1.tgz#63fc4ccee5d2d7763d26bbf8601078e6c2e0044f" + +os-homedir@^1.0.0, os-homedir@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/os-homedir/-/os-homedir-1.0.2.tgz#ffbc4988336e0e833de0c168c7ef152121aa7fb3" + +os-locale@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/os-locale/-/os-locale-2.1.0.tgz#42bc2900a6b5b8bd17376c8e882b65afccf24bf2" + dependencies: + execa "^0.7.0" + lcid "^1.0.0" + mem "^1.1.0" + +os-tmpdir@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274" + +osenv@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/osenv/-/osenv-0.1.4.tgz#42fe6d5953df06c8064be6f176c3d05aaaa34644" + dependencies: + os-homedir "^1.0.0" + os-tmpdir "^1.0.0" + +p-finally@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/p-finally/-/p-finally-1.0.0.tgz#3fbcfb15b899a44123b34b6dcc18b724336a2cae" + +p-limit@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-1.1.0.tgz#b07ff2d9a5d88bec806035895a2bab66a27988bc" + +p-locate@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-2.0.0.tgz#20a0103b222a70c8fd39cc2e580680f3dde5ec43" + dependencies: + p-limit "^1.1.0" + +pako@~0.2.0: + version "0.2.9" + resolved "https://registry.yarnpkg.com/pako/-/pako-0.2.9.tgz#f3f7522f4ef782348da8161bad9ecfd51bf83a75" + +parallel-transform@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/parallel-transform/-/parallel-transform-1.1.0.tgz#d410f065b05da23081fcd10f28854c29bda33b06" + dependencies: + cyclist "~0.2.2" + inherits "^2.0.3" + readable-stream "^2.1.5" + +parse-asn1@^5.0.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/parse-asn1/-/parse-asn1-5.1.0.tgz#37c4f9b7ed3ab65c74817b5f2480937fbf97c712" + dependencies: + asn1.js "^4.0.0" + browserify-aes "^1.0.0" + create-hash "^1.1.0" + evp_bytestokey "^1.0.0" + pbkdf2 "^3.0.3" + +parse-glob@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/parse-glob/-/parse-glob-3.0.4.tgz#b2c376cfb11f35513badd173ef0bb6e3a388391c" + dependencies: + glob-base "^0.3.0" + is-dotfile "^1.0.0" + is-extglob "^1.0.0" + is-glob "^2.0.0" + +parse-json@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-2.2.0.tgz#f480f40434ef80741f8469099f8dea18f55a4dc9" + dependencies: + error-ex "^1.2.0" + +path-browserify@0.0.0: + version "0.0.0" + resolved "https://registry.yarnpkg.com/path-browserify/-/path-browserify-0.0.0.tgz#a0b870729aae214005b7d5032ec2cbbb0fb4451a" + +path-exists@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-3.0.0.tgz#ce0ebeaa5f78cb18925ea7d810d7b59b010fd515" + +path-is-absolute@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" + +path-key@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/path-key/-/path-key-2.0.1.tgz#411cadb574c5a140d3a4b1910d40d80cc9f40b40" + +path-parse@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.5.tgz#3c1adf871ea9cd6c9431b6ea2bd74a0ff055c4c1" + +path-type@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/path-type/-/path-type-2.0.0.tgz#f012ccb8415b7096fc2daa1054c3d72389594c73" + dependencies: + pify "^2.0.0" + +pbkdf2@^3.0.3: + version "3.0.14" + resolved "https://registry.yarnpkg.com/pbkdf2/-/pbkdf2-3.0.14.tgz#a35e13c64799b06ce15320f459c230e68e73bade" + dependencies: + create-hash "^1.1.2" + create-hmac "^1.1.4" + ripemd160 "^2.0.1" + safe-buffer "^5.0.1" + sha.js "^2.4.8" + +performance-now@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-0.2.0.tgz#33ef30c5c77d4ea21c5a53869d91b56d8f2555e5" + +performance-now@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b" + +pify@^2.0.0, pify@^2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c" + +pkg-dir@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-2.0.0.tgz#f6d5d1109e19d63edf428e0bd57e12777615334b" + dependencies: + find-up "^2.1.0" + +postcss-calc@^5.2.0: + version "5.3.1" + resolved "https://registry.yarnpkg.com/postcss-calc/-/postcss-calc-5.3.1.tgz#77bae7ca928ad85716e2fda42f261bf7c1d65b5e" + dependencies: + postcss "^5.0.2" + postcss-message-helpers "^2.0.0" + reduce-css-calc "^1.2.6" + +postcss-colormin@^2.1.8: + version "2.2.2" + resolved "https://registry.yarnpkg.com/postcss-colormin/-/postcss-colormin-2.2.2.tgz#6631417d5f0e909a3d7ec26b24c8a8d1e4f96e4b" + dependencies: + colormin "^1.0.5" + postcss "^5.0.13" + postcss-value-parser "^3.2.3" + +postcss-convert-values@^2.3.4: + version "2.6.1" + resolved "https://registry.yarnpkg.com/postcss-convert-values/-/postcss-convert-values-2.6.1.tgz#bbd8593c5c1fd2e3d1c322bb925dcae8dae4d62d" + dependencies: + postcss "^5.0.11" + postcss-value-parser "^3.1.2" + +postcss-discard-comments@^2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/postcss-discard-comments/-/postcss-discard-comments-2.0.4.tgz#befe89fafd5b3dace5ccce51b76b81514be00e3d" + dependencies: + postcss "^5.0.14" + +postcss-discard-duplicates@^2.0.1: + version "2.1.0" + resolved "https://registry.yarnpkg.com/postcss-discard-duplicates/-/postcss-discard-duplicates-2.1.0.tgz#b9abf27b88ac188158a5eb12abcae20263b91932" + dependencies: + postcss "^5.0.4" + +postcss-discard-empty@^2.0.1: + version "2.1.0" + resolved "https://registry.yarnpkg.com/postcss-discard-empty/-/postcss-discard-empty-2.1.0.tgz#d2b4bd9d5ced5ebd8dcade7640c7d7cd7f4f92b5" + dependencies: + postcss "^5.0.14" + +postcss-discard-overridden@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/postcss-discard-overridden/-/postcss-discard-overridden-0.1.1.tgz#8b1eaf554f686fb288cd874c55667b0aa3668d58" + dependencies: + postcss "^5.0.16" + +postcss-discard-unused@^2.2.1: + version "2.2.3" + resolved "https://registry.yarnpkg.com/postcss-discard-unused/-/postcss-discard-unused-2.2.3.tgz#bce30b2cc591ffc634322b5fb3464b6d934f4433" + dependencies: + postcss "^5.0.14" + uniqs "^2.0.0" + +postcss-filter-plugins@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/postcss-filter-plugins/-/postcss-filter-plugins-2.0.2.tgz#6d85862534d735ac420e4a85806e1f5d4286d84c" + dependencies: + postcss "^5.0.4" + uniqid "^4.0.0" + +postcss-load-config@^1.1.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/postcss-load-config/-/postcss-load-config-1.2.0.tgz#539e9afc9ddc8620121ebf9d8c3673e0ce50d28a" + dependencies: + cosmiconfig "^2.1.0" + object-assign "^4.1.0" + postcss-load-options "^1.2.0" + postcss-load-plugins "^2.3.0" + +postcss-load-options@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/postcss-load-options/-/postcss-load-options-1.2.0.tgz#b098b1559ddac2df04bc0bb375f99a5cfe2b6d8c" + dependencies: + cosmiconfig "^2.1.0" + object-assign "^4.1.0" + +postcss-load-plugins@^2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/postcss-load-plugins/-/postcss-load-plugins-2.3.0.tgz#745768116599aca2f009fad426b00175049d8d92" + dependencies: + cosmiconfig "^2.1.1" + object-assign "^4.1.0" + +postcss-merge-idents@^2.1.5: + version "2.1.7" + resolved "https://registry.yarnpkg.com/postcss-merge-idents/-/postcss-merge-idents-2.1.7.tgz#4c5530313c08e1d5b3bbf3d2bbc747e278eea270" + dependencies: + has "^1.0.1" + postcss "^5.0.10" + postcss-value-parser "^3.1.1" + +postcss-merge-longhand@^2.0.1: + version "2.0.2" + resolved "https://registry.yarnpkg.com/postcss-merge-longhand/-/postcss-merge-longhand-2.0.2.tgz#23d90cd127b0a77994915332739034a1a4f3d658" + dependencies: + postcss "^5.0.4" + +postcss-merge-rules@^2.0.3: + version "2.1.2" + resolved "https://registry.yarnpkg.com/postcss-merge-rules/-/postcss-merge-rules-2.1.2.tgz#d1df5dfaa7b1acc3be553f0e9e10e87c61b5f721" + dependencies: + browserslist "^1.5.2" + caniuse-api "^1.5.2" + postcss "^5.0.4" + postcss-selector-parser "^2.2.2" + vendors "^1.0.0" + +postcss-message-helpers@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/postcss-message-helpers/-/postcss-message-helpers-2.0.0.tgz#a4f2f4fab6e4fe002f0aed000478cdf52f9ba60e" + +postcss-minify-font-values@^1.0.2: + version "1.0.5" + resolved "https://registry.yarnpkg.com/postcss-minify-font-values/-/postcss-minify-font-values-1.0.5.tgz#4b58edb56641eba7c8474ab3526cafd7bbdecb69" + dependencies: + object-assign "^4.0.1" + postcss "^5.0.4" + postcss-value-parser "^3.0.2" + +postcss-minify-gradients@^1.0.1: + version "1.0.5" + resolved "https://registry.yarnpkg.com/postcss-minify-gradients/-/postcss-minify-gradients-1.0.5.tgz#5dbda11373703f83cfb4a3ea3881d8d75ff5e6e1" + dependencies: + postcss "^5.0.12" + postcss-value-parser "^3.3.0" + +postcss-minify-params@^1.0.4: + version "1.2.2" + resolved "https://registry.yarnpkg.com/postcss-minify-params/-/postcss-minify-params-1.2.2.tgz#ad2ce071373b943b3d930a3fa59a358c28d6f1f3" + dependencies: + alphanum-sort "^1.0.1" + postcss "^5.0.2" + postcss-value-parser "^3.0.2" + uniqs "^2.0.0" + +postcss-minify-selectors@^2.0.4: + version "2.1.1" + resolved "https://registry.yarnpkg.com/postcss-minify-selectors/-/postcss-minify-selectors-2.1.1.tgz#b2c6a98c0072cf91b932d1a496508114311735bf" + dependencies: + alphanum-sort "^1.0.2" + has "^1.0.1" + postcss "^5.0.14" + postcss-selector-parser "^2.0.0" + +postcss-modules-extract-imports@^1.0.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/postcss-modules-extract-imports/-/postcss-modules-extract-imports-1.2.0.tgz#66140ecece38ef06bf0d3e355d69bf59d141ea85" + dependencies: + postcss "^6.0.1" + +postcss-modules-local-by-default@^1.0.1: + version "1.2.0" + resolved "https://registry.yarnpkg.com/postcss-modules-local-by-default/-/postcss-modules-local-by-default-1.2.0.tgz#f7d80c398c5a393fa7964466bd19500a7d61c069" + dependencies: + css-selector-tokenizer "^0.7.0" + postcss "^6.0.1" + +postcss-modules-scope@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/postcss-modules-scope/-/postcss-modules-scope-1.1.0.tgz#d6ea64994c79f97b62a72b426fbe6056a194bb90" + dependencies: + css-selector-tokenizer "^0.7.0" + postcss "^6.0.1" + +postcss-modules-values@^1.1.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/postcss-modules-values/-/postcss-modules-values-1.3.0.tgz#ecffa9d7e192518389f42ad0e83f72aec456ea20" + dependencies: + icss-replace-symbols "^1.1.0" + postcss "^6.0.1" + +postcss-normalize-charset@^1.1.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/postcss-normalize-charset/-/postcss-normalize-charset-1.1.1.tgz#ef9ee71212d7fe759c78ed162f61ed62b5cb93f1" + dependencies: + postcss "^5.0.5" + +postcss-normalize-url@^3.0.7: + version "3.0.8" + resolved "https://registry.yarnpkg.com/postcss-normalize-url/-/postcss-normalize-url-3.0.8.tgz#108f74b3f2fcdaf891a2ffa3ea4592279fc78222" + dependencies: + is-absolute-url "^2.0.0" + normalize-url "^1.4.0" + postcss "^5.0.14" + postcss-value-parser "^3.2.3" + +postcss-ordered-values@^2.1.0: + version "2.2.3" + resolved "https://registry.yarnpkg.com/postcss-ordered-values/-/postcss-ordered-values-2.2.3.tgz#eec6c2a67b6c412a8db2042e77fe8da43f95c11d" + dependencies: + postcss "^5.0.4" + postcss-value-parser "^3.0.1" + +postcss-reduce-idents@^2.2.2: + version "2.4.0" + resolved "https://registry.yarnpkg.com/postcss-reduce-idents/-/postcss-reduce-idents-2.4.0.tgz#c2c6d20cc958284f6abfbe63f7609bf409059ad3" + dependencies: + postcss "^5.0.4" + postcss-value-parser "^3.0.2" + +postcss-reduce-initial@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/postcss-reduce-initial/-/postcss-reduce-initial-1.0.1.tgz#68f80695f045d08263a879ad240df8dd64f644ea" + dependencies: + postcss "^5.0.4" + +postcss-reduce-transforms@^1.0.3: + version "1.0.4" + resolved "https://registry.yarnpkg.com/postcss-reduce-transforms/-/postcss-reduce-transforms-1.0.4.tgz#ff76f4d8212437b31c298a42d2e1444025771ae1" + dependencies: + has "^1.0.1" + postcss "^5.0.8" + postcss-value-parser "^3.0.1" + +postcss-selector-parser@^2.0.0, postcss-selector-parser@^2.2.2: + version "2.2.3" + resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-2.2.3.tgz#f9437788606c3c9acee16ffe8d8b16297f27bb90" + dependencies: + flatten "^1.0.2" + indexes-of "^1.0.1" + uniq "^1.0.1" + +postcss-svgo@^2.1.1: + version "2.1.6" + resolved "https://registry.yarnpkg.com/postcss-svgo/-/postcss-svgo-2.1.6.tgz#b6df18aa613b666e133f08adb5219c2684ac108d" + dependencies: + is-svg "^2.0.0" + postcss "^5.0.14" + postcss-value-parser "^3.2.3" + svgo "^0.7.0" + +postcss-unique-selectors@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/postcss-unique-selectors/-/postcss-unique-selectors-2.0.2.tgz#981d57d29ddcb33e7b1dfe1fd43b8649f933ca1d" + dependencies: + alphanum-sort "^1.0.1" + postcss "^5.0.4" + uniqs "^2.0.0" + +postcss-value-parser@^3.0.1, postcss-value-parser@^3.0.2, postcss-value-parser@^3.1.1, postcss-value-parser@^3.1.2, postcss-value-parser@^3.2.3, postcss-value-parser@^3.3.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-3.3.0.tgz#87f38f9f18f774a4ab4c8a232f5c5ce8872a9d15" + +postcss-zindex@^2.0.1: + version "2.2.0" + resolved "https://registry.yarnpkg.com/postcss-zindex/-/postcss-zindex-2.2.0.tgz#d2109ddc055b91af67fc4cb3b025946639d2af22" + dependencies: + has "^1.0.1" + postcss "^5.0.4" + uniqs "^2.0.0" + +postcss@^5.0.10, postcss@^5.0.11, postcss@^5.0.12, postcss@^5.0.13, postcss@^5.0.14, postcss@^5.0.16, postcss@^5.0.2, postcss@^5.0.4, postcss@^5.0.5, postcss@^5.0.6, postcss@^5.0.8, postcss@^5.2.16: + version "5.2.17" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-5.2.17.tgz#cf4f597b864d65c8a492b2eabe9d706c879c388b" + dependencies: + chalk "^1.1.3" + js-base64 "^2.1.9" + source-map "^0.5.6" + supports-color "^3.2.3" + +postcss@^6.0.1, postcss@^6.0.6: + version "6.0.12" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-6.0.12.tgz#6b0155089d2d212f7bd6a0cecd4c58c007403535" + dependencies: + chalk "^2.1.0" + source-map "^0.5.7" + supports-color "^4.4.0" + +prepend-http@^1.0.0: + version "1.0.4" + resolved "https://registry.yarnpkg.com/prepend-http/-/prepend-http-1.0.4.tgz#d4f4562b0ce3696e41ac52d0e002e57a635dc6dc" + +preserve@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/preserve/-/preserve-0.2.0.tgz#815ed1f6ebc65926f865b310c0713bcb3315ce4b" + +prettier@^1.7.0: + version "1.7.3" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.7.3.tgz#8e6974725273914b1c47439959dd3d3ba53664b6" + +process-nextick-args@~1.0.6: + version "1.0.7" + resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-1.0.7.tgz#150e20b756590ad3f91093f25a4f2ad8bff30ba3" + +process@^0.11.0: + version "0.11.10" + resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182" + +promise-inflight@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/promise-inflight/-/promise-inflight-1.0.1.tgz#98472870bf228132fcbdd868129bad12c3c029e3" + +promise@^7.1.1: + version "7.3.1" + resolved "https://registry.yarnpkg.com/promise/-/promise-7.3.1.tgz#064b72602b18f90f29192b8b1bc418ffd1ebd3bf" + dependencies: + asap "~2.0.3" + +prr@~0.0.0: + version "0.0.0" + resolved "https://registry.yarnpkg.com/prr/-/prr-0.0.0.tgz#1a84b85908325501411853d0081ee3fa86e2926a" + +pseudomap@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/pseudomap/-/pseudomap-1.0.2.tgz#f052a28da70e618917ef0a8ac34c1ae5a68286b3" + +public-encrypt@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/public-encrypt/-/public-encrypt-4.0.0.tgz#39f699f3a46560dd5ebacbca693caf7c65c18cc6" + dependencies: + bn.js "^4.1.0" + browserify-rsa "^4.0.0" + create-hash "^1.1.0" + parse-asn1 "^5.0.0" + randombytes "^2.0.1" + +pump@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/pump/-/pump-1.0.2.tgz#3b3ee6512f94f0e575538c17995f9f16990a5d51" + dependencies: + end-of-stream "^1.1.0" + once "^1.3.1" + +pumpify@^1.3.3: + version "1.3.5" + resolved "https://registry.yarnpkg.com/pumpify/-/pumpify-1.3.5.tgz#1b671c619940abcaeac0ad0e3a3c164be760993b" + dependencies: + duplexify "^3.1.2" + inherits "^2.0.1" + pump "^1.0.0" + +punycode@1.3.2: + version "1.3.2" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.3.2.tgz#9653a036fb7c1ee42342f2325cceefea3926c48d" + +punycode@^1.2.4, punycode@^1.4.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e" + +q@^1.1.2: + version "1.5.0" + resolved "https://registry.yarnpkg.com/q/-/q-1.5.0.tgz#dd01bac9d06d30e6f219aecb8253ee9ebdc308f1" + +qs@~6.4.0: + version "6.4.0" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.4.0.tgz#13e26d28ad6b0ffaa91312cd3bf708ed351e7233" + +qs@~6.5.1: + version "6.5.1" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.1.tgz#349cdf6eef89ec45c12d7d5eb3fc0c870343a6d8" + +query-string@^4.1.0: + version "4.3.4" + resolved "https://registry.yarnpkg.com/query-string/-/query-string-4.3.4.tgz#bbb693b9ca915c232515b228b1a02b609043dbeb" + dependencies: + object-assign "^4.1.0" + strict-uri-encode "^1.0.0" + +querystring-es3@^0.2.0: + version "0.2.1" + resolved "https://registry.yarnpkg.com/querystring-es3/-/querystring-es3-0.2.1.tgz#9ec61f79049875707d69414596fd907a4d711e73" + +querystring@0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/querystring/-/querystring-0.2.0.tgz#b209849203bb25df820da756e747005878521620" + +randomatic@^1.1.3: + version "1.1.7" + resolved "https://registry.yarnpkg.com/randomatic/-/randomatic-1.1.7.tgz#c7abe9cc8b87c0baa876b19fde83fd464797e38c" + dependencies: + is-number "^3.0.0" + kind-of "^4.0.0" + +randombytes@^2.0.0, randombytes@^2.0.1: + version "2.0.5" + resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.0.5.tgz#dc009a246b8d09a177b4b7a0ae77bc570f4b1b79" + dependencies: + safe-buffer "^5.1.0" + +raven-js@^3.17.0: + version "3.17.0" + resolved "https://registry.yarnpkg.com/raven-js/-/raven-js-3.17.0.tgz#779457ac7910512c3c2cc9bb6d0a9eeb59a969ec" + +rc@^1.1.7: + version "1.2.1" + resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.1.tgz#2e03e8e42ee450b8cb3dce65be1bf8974e1dfd95" + dependencies: + deep-extend "~0.4.0" + ini "~1.3.0" + minimist "^1.2.0" + strip-json-comments "~2.0.1" + +read-pkg-up@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-2.0.0.tgz#6b72a8048984e0c41e79510fd5e9fa99b3b549be" + dependencies: + find-up "^2.0.0" + read-pkg "^2.0.0" + +read-pkg@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-2.0.0.tgz#8ef1c0623c6a6db0dc6713c4bfac46332b2368f8" + dependencies: + load-json-file "^2.0.0" + normalize-package-data "^2.3.2" + path-type "^2.0.0" + +"readable-stream@1 || 2", readable-stream@^2.0.0, readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.4, readable-stream@^2.0.5, readable-stream@^2.0.6, readable-stream@^2.1.4, readable-stream@^2.1.5, readable-stream@^2.2.2, readable-stream@^2.2.6: + version "2.3.3" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.3.tgz#368f2512d79f9d46fdfc71349ae7878bbc1eb95c" + dependencies: + core-util-is "~1.0.0" + inherits "~2.0.3" + isarray "~1.0.0" + process-nextick-args "~1.0.6" + safe-buffer "~5.1.1" + string_decoder "~1.0.3" + util-deprecate "~1.0.1" + +readdirp@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-2.1.0.tgz#4ed0ad060df3073300c48440373f72d1cc642d78" + dependencies: + graceful-fs "^4.1.2" + minimatch "^3.0.2" + readable-stream "^2.0.2" + set-immediate-shim "^1.0.1" + +reduce-css-calc@^1.2.6: + version "1.3.0" + resolved "https://registry.yarnpkg.com/reduce-css-calc/-/reduce-css-calc-1.3.0.tgz#747c914e049614a4c9cfbba629871ad1d2927716" + dependencies: + balanced-match "^0.4.2" + math-expression-evaluator "^1.2.14" + reduce-function-call "^1.0.1" + +reduce-function-call@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/reduce-function-call/-/reduce-function-call-1.0.2.tgz#5a200bf92e0e37751752fe45b0ab330fd4b6be99" + dependencies: + balanced-match "^0.4.2" + +reflect-metadata@^0.1.9: + version "0.1.10" + resolved "https://registry.yarnpkg.com/reflect-metadata/-/reflect-metadata-0.1.10.tgz#b4f83704416acad89988c9b15635d47e03b9344a" + +regenerate@^1.2.1: + version "1.3.3" + resolved "https://registry.yarnpkg.com/regenerate/-/regenerate-1.3.3.tgz#0c336d3980553d755c39b586ae3b20aa49c82b7f" + +regex-cache@^0.4.2: + version "0.4.4" + resolved "https://registry.yarnpkg.com/regex-cache/-/regex-cache-0.4.4.tgz#75bdc58a2a1496cec48a12835bc54c8d562336dd" + dependencies: + is-equal-shallow "^0.1.3" + +regexpu-core@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-1.0.0.tgz#86a763f58ee4d7c2f6b102e4764050de7ed90c6b" + dependencies: + regenerate "^1.2.1" + regjsgen "^0.2.0" + regjsparser "^0.1.4" + +regjsgen@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/regjsgen/-/regjsgen-0.2.0.tgz#6c016adeac554f75823fe37ac05b92d5a4edb1f7" + +regjsparser@^0.1.4: + version "0.1.5" + resolved "https://registry.yarnpkg.com/regjsparser/-/regjsparser-0.1.5.tgz#7ee8f84dc6fa792d3fd0ae228d24bd949ead205c" + dependencies: + jsesc "~0.5.0" + +remove-trailing-separator@^1.0.1: + version "1.1.0" + resolved "https://registry.yarnpkg.com/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz#c24bce2a283adad5bc3f58e0d48249b92379d8ef" + +repeat-element@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/repeat-element/-/repeat-element-1.1.2.tgz#ef089a178d1483baae4d93eb98b4f9e4e11d990a" + +repeat-string@^1.5.2: + version "1.6.1" + resolved "https://registry.yarnpkg.com/repeat-string/-/repeat-string-1.6.1.tgz#8dcae470e1c88abc2d600fff4a776286da75e637" + +request@2.81.0: + version "2.81.0" + resolved "https://registry.yarnpkg.com/request/-/request-2.81.0.tgz#c6928946a0e06c5f8d6f8a9333469ffda46298a0" + dependencies: + aws-sign2 "~0.6.0" + aws4 "^1.2.1" + caseless "~0.12.0" + combined-stream "~1.0.5" + extend "~3.0.0" + forever-agent "~0.6.1" + form-data "~2.1.1" + har-validator "~4.2.1" + hawk "~3.1.3" + http-signature "~1.1.0" + is-typedarray "~1.0.0" + isstream "~0.1.2" + json-stringify-safe "~5.0.1" + mime-types "~2.1.7" + oauth-sign "~0.8.1" + performance-now "^0.2.0" + qs "~6.4.0" + safe-buffer "^5.0.1" + stringstream "~0.0.4" + tough-cookie "~2.3.0" + tunnel-agent "^0.6.0" + uuid "^3.0.0" + +request@^2.72.0: + version "2.83.0" + resolved "https://registry.yarnpkg.com/request/-/request-2.83.0.tgz#ca0b65da02ed62935887808e6f510381034e3356" + dependencies: + aws-sign2 "~0.7.0" + aws4 "^1.6.0" + caseless "~0.12.0" + combined-stream "~1.0.5" + extend "~3.0.1" + forever-agent "~0.6.1" + form-data "~2.3.1" + har-validator "~5.0.3" + hawk "~6.0.2" + http-signature "~1.2.0" + is-typedarray "~1.0.0" + isstream "~0.1.2" + json-stringify-safe "~5.0.1" + mime-types "~2.1.17" + oauth-sign "~0.8.2" + performance-now "^2.1.0" + qs "~6.5.1" + safe-buffer "^5.1.1" + stringstream "~0.0.5" + tough-cookie "~2.3.3" + tunnel-agent "^0.6.0" + uuid "^3.1.0" + +require-directory@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" + +require-from-string@^1.1.0: + version "1.2.1" + resolved "https://registry.yarnpkg.com/require-from-string/-/require-from-string-1.2.1.tgz#529c9ccef27380adfec9a2f965b649bbee636418" + +require-main-filename@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-1.0.1.tgz#97f717b69d48784f5f526a6c5aa8ffdda055a4d1" + +resolve@^1.3.2, resolve@^1.3.3: + version "1.4.0" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.4.0.tgz#a75be01c53da25d934a98ebd0e4c4a7312f92a86" + dependencies: + path-parse "^1.0.5" + +right-align@^0.1.1: + version "0.1.3" + resolved "https://registry.yarnpkg.com/right-align/-/right-align-0.1.3.tgz#61339b722fe6a3515689210d24e14c96148613ef" + dependencies: + align-text "^0.1.1" + +rimraf@2, rimraf@^2.5.1, rimraf@^2.5.4, rimraf@^2.6.1: + version "2.6.2" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.2.tgz#2ed8150d24a16ea8651e6d6ef0f47c4158ce7a36" + dependencies: + glob "^7.0.5" + +ripemd160@^2.0.0, ripemd160@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/ripemd160/-/ripemd160-2.0.1.tgz#0f4584295c53a3628af7e6d79aca21ce57d1c6e7" + dependencies: + hash-base "^2.0.0" + inherits "^2.0.1" + +run-queue@^1.0.0, run-queue@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/run-queue/-/run-queue-1.0.3.tgz#e848396f057d223f24386924618e25694161ec47" + dependencies: + aproba "^1.1.1" + +safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1, safe-buffer@~5.1.0, safe-buffer@~5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.1.tgz#893312af69b2123def71f57889001671eeb2c853" + +sax@~1.2.1: + version "1.2.4" + resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9" + +schema-utils@^0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-0.3.0.tgz#f5877222ce3e931edae039f17eb3716e7137f8cf" + dependencies: + ajv "^5.0.0" + +"semver@2 || 3 || 4 || 5", semver@^5.0.1, semver@^5.3.0: + version "5.4.1" + resolved "https://registry.yarnpkg.com/semver/-/semver-5.4.1.tgz#e059c09d8571f0540823733433505d3a2f00b18e" + +set-blocking@^2.0.0, set-blocking@~2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" + +set-immediate-shim@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/set-immediate-shim/-/set-immediate-shim-1.0.1.tgz#4b2b1b27eb808a9f8dcc481a58e5e56f599f3f61" + +setimmediate@^1.0.4: + version "1.0.5" + resolved "https://registry.yarnpkg.com/setimmediate/-/setimmediate-1.0.5.tgz#290cbb232e306942d7d7ea9b83732ab7856f8285" + +sha.js@^2.4.0, sha.js@^2.4.8: + version "2.4.9" + resolved "https://registry.yarnpkg.com/sha.js/-/sha.js-2.4.9.tgz#98f64880474b74f4a38b8da9d3c0f2d104633e7d" + dependencies: + inherits "^2.0.1" + safe-buffer "^5.0.1" + +shebang-command@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea" + dependencies: + shebang-regex "^1.0.0" + +shebang-regex@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-1.0.0.tgz#da42f49740c0b42db2ca9728571cb190c98efea3" + +signal-exit@^3.0.0: + version "3.0.2" + resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.2.tgz#b5fdc08f1287ea1178628e415e25132b73646c6d" + +sntp@1.x.x: + version "1.0.9" + resolved "https://registry.yarnpkg.com/sntp/-/sntp-1.0.9.tgz#6541184cc90aeea6c6e7b35e2659082443c66198" + dependencies: + hoek "2.x.x" + +sntp@2.x.x: + version "2.0.2" + resolved "https://registry.yarnpkg.com/sntp/-/sntp-2.0.2.tgz#5064110f0af85f7cfdb7d6b67a40028ce52b4b2b" + dependencies: + hoek "4.x.x" + +sort-keys@^1.0.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/sort-keys/-/sort-keys-1.1.2.tgz#441b6d4d346798f1b4e49e8920adfba0e543f9ad" + dependencies: + is-plain-obj "^1.0.0" + +sortablejs@^1.6.1: + version "1.6.1" + resolved "https://registry.yarnpkg.com/sortablejs/-/sortablejs-1.6.1.tgz#d120d103fbb9f60c7db27814a1384072e6c6e083" + +source-list-map@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/source-list-map/-/source-list-map-2.0.0.tgz#aaa47403f7b245a92fbc97ea08f250d6087ed085" + +source-map@^0.5.3, source-map@^0.5.6, source-map@^0.5.7, source-map@~0.5.1, source-map@~0.5.3: + version "0.5.7" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc" + +spdx-correct@~1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-1.0.2.tgz#4b3073d933ff51f3912f03ac5519498a4150db40" + dependencies: + spdx-license-ids "^1.0.2" + +spdx-expression-parse@~1.0.0: + version "1.0.4" + resolved "https://registry.yarnpkg.com/spdx-expression-parse/-/spdx-expression-parse-1.0.4.tgz#9bdf2f20e1f40ed447fbe273266191fced51626c" + +spdx-license-ids@^1.0.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-1.2.2.tgz#c9df7a3424594ade6bd11900d596696dc06bac57" + +sprintf-js@~1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" + +sshpk@^1.7.0: + version "1.13.1" + resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.13.1.tgz#512df6da6287144316dc4c18fe1cf1d940739be3" + dependencies: + asn1 "~0.2.3" + assert-plus "^1.0.0" + dashdash "^1.12.0" + getpass "^0.1.1" + optionalDependencies: + bcrypt-pbkdf "^1.0.0" + ecc-jsbn "~0.1.1" + jsbn "~0.1.0" + tweetnacl "~0.14.0" + +ssri@^4.1.6: + version "4.1.6" + resolved "https://registry.yarnpkg.com/ssri/-/ssri-4.1.6.tgz#0cb49b6ac84457e7bdd466cb730c3cb623e9a25b" + dependencies: + safe-buffer "^5.1.0" + +stream-browserify@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/stream-browserify/-/stream-browserify-2.0.1.tgz#66266ee5f9bdb9940a4e4514cafb43bb71e5c9db" + dependencies: + inherits "~2.0.1" + readable-stream "^2.0.2" + +stream-each@^1.1.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/stream-each/-/stream-each-1.2.0.tgz#1e95d47573f580d814dc0ff8cd0f66f1ce53c991" + dependencies: + end-of-stream "^1.1.0" + stream-shift "^1.0.0" + +stream-http@^2.3.1: + version "2.7.2" + resolved "https://registry.yarnpkg.com/stream-http/-/stream-http-2.7.2.tgz#40a050ec8dc3b53b33d9909415c02c0bf1abfbad" + dependencies: + builtin-status-codes "^3.0.0" + inherits "^2.0.1" + readable-stream "^2.2.6" + to-arraybuffer "^1.0.0" + xtend "^4.0.0" + +stream-shift@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/stream-shift/-/stream-shift-1.0.0.tgz#d5c752825e5367e786f78e18e445ea223a155952" + +strict-uri-encode@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz#279b225df1d582b1f54e65addd4352e18faa0713" + +string-width@^1.0.1, string-width@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-1.0.2.tgz#118bdf5b8cdc51a2a7e70d211e07e2b0b9b107d3" + dependencies: + code-point-at "^1.0.0" + is-fullwidth-code-point "^1.0.0" + strip-ansi "^3.0.0" + +string-width@^2.0.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-2.1.1.tgz#ab93f27a8dc13d28cac815c462143a6d9012ae9e" + dependencies: + is-fullwidth-code-point "^2.0.0" + strip-ansi "^4.0.0" + +string_decoder@^0.10.25: + version "0.10.31" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-0.10.31.tgz#62e203bc41766c6c28c9fc84301dab1c5310fa94" + +string_decoder@~1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.0.3.tgz#0fc67d7c141825de94282dd536bec6b9bce860ab" + dependencies: + safe-buffer "~5.1.0" + +stringstream@~0.0.4, stringstream@~0.0.5: + version "0.0.5" + resolved "https://registry.yarnpkg.com/stringstream/-/stringstream-0.0.5.tgz#4e484cd4de5a0bbbee18e46307710a8a81621878" + +strip-ansi@^3.0.0, strip-ansi@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-3.0.1.tgz#6a385fb8853d952d5ff05d0e8aaf94278dc63dcf" + dependencies: + ansi-regex "^2.0.0" + +strip-ansi@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-4.0.0.tgz#a8479022eb1ac368a871389b635262c505ee368f" + dependencies: + ansi-regex "^3.0.0" + +strip-bom@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3" + +strip-eof@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/strip-eof/-/strip-eof-1.0.0.tgz#bb43ff5598a6eb05d89b59fcd129c983313606bf" + +strip-json-comments@~2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a" + +supports-color@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-2.0.0.tgz#535d045ce6b6363fa40117084629995e9df324c7" + +supports-color@^3.2.3: + version "3.2.3" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-3.2.3.tgz#65ac0504b3954171d8a64946b2ae3cbb8a5f54f6" + dependencies: + has-flag "^1.0.0" + +supports-color@^4.0.0, supports-color@^4.2.1, supports-color@^4.4.0: + version "4.4.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-4.4.0.tgz#883f7ddabc165142b2a61427f3352ded195d1a3e" + dependencies: + has-flag "^2.0.0" + +svgo@^0.7.0: + version "0.7.2" + resolved "https://registry.yarnpkg.com/svgo/-/svgo-0.7.2.tgz#9f5772413952135c6fefbf40afe6a4faa88b4bb5" + dependencies: + coa "~1.0.1" + colors "~1.1.2" + csso "~2.3.1" + js-yaml "~3.7.0" + mkdirp "~0.5.1" + sax "~1.2.1" + whet.extend "~0.9.9" + +tapable@^0.2.7: + version "0.2.8" + resolved "https://registry.yarnpkg.com/tapable/-/tapable-0.2.8.tgz#99372a5c999bf2df160afc0d74bed4f47948cd22" + +tar-pack@^3.4.0: + version "3.4.0" + resolved "https://registry.yarnpkg.com/tar-pack/-/tar-pack-3.4.0.tgz#23be2d7f671a8339376cbdb0b8fe3fdebf317984" + dependencies: + debug "^2.2.0" + fstream "^1.0.10" + fstream-ignore "^1.0.5" + once "^1.3.3" + readable-stream "^2.1.4" + rimraf "^2.5.1" + tar "^2.2.1" + uid-number "^0.0.6" + +tar@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/tar/-/tar-2.2.1.tgz#8e4d2a256c0e2185c6b18ad694aec968b83cb1d1" + dependencies: + block-stream "*" + fstream "^1.0.2" + inherits "2" + +through2@^2.0.0: + version "2.0.3" + resolved "https://registry.yarnpkg.com/through2/-/through2-2.0.3.tgz#0004569b37c7c74ba39c43f3ced78d1ad94140be" + dependencies: + readable-stream "^2.1.5" + xtend "~4.0.1" + +timers-browserify@^2.0.2: + version "2.0.4" + resolved "https://registry.yarnpkg.com/timers-browserify/-/timers-browserify-2.0.4.tgz#96ca53f4b794a5e7c0e1bd7cc88a372298fa01e6" + dependencies: + setimmediate "^1.0.4" + +to-arraybuffer@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/to-arraybuffer/-/to-arraybuffer-1.0.1.tgz#7d229b1fcc637e466ca081180836a7aabff83f43" + +tough-cookie@~2.3.0, tough-cookie@~2.3.3: + version "2.3.3" + resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.3.3.tgz#0b618a5565b6dea90bf3425d04d55edc475a7561" + dependencies: + punycode "^1.4.1" + +ts-loader@^2.3.2: + version "2.3.7" + resolved "https://registry.yarnpkg.com/ts-loader/-/ts-loader-2.3.7.tgz#a9028ced473bee12f28a75f9c5b139979d33f2fc" + dependencies: + chalk "^2.0.1" + enhanced-resolve "^3.0.0" + loader-utils "^1.0.2" + semver "^5.0.1" + +tslib@^1.7.1: + version "1.7.1" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.7.1.tgz#bc8004164691923a79fe8378bbeb3da2017538ec" + +tslint@^5.7.0: + version "5.7.0" + resolved "https://registry.yarnpkg.com/tslint/-/tslint-5.7.0.tgz#c25e0d0c92fa1201c2bc30e844e08e682b4f3552" + dependencies: + babel-code-frame "^6.22.0" + colors "^1.1.2" + commander "^2.9.0" + diff "^3.2.0" + glob "^7.1.1" + minimatch "^3.0.4" + resolve "^1.3.2" + semver "^5.3.0" + tslib "^1.7.1" + tsutils "^2.8.1" + +tsutils@^2.8.1: + version "2.10.0" + resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-2.10.0.tgz#ae94511df2656eb06e4424056fba5c388887040c" + dependencies: + tslib "^1.7.1" + +tty-browserify@0.0.0: + version "0.0.0" + resolved "https://registry.yarnpkg.com/tty-browserify/-/tty-browserify-0.0.0.tgz#a157ba402da24e9bf957f9aa69d524eed42901a6" + +tunnel-agent@^0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.6.0.tgz#27a5dea06b36b04a0a9966774b290868f0fc40fd" + dependencies: + safe-buffer "^5.0.1" + +tweetnacl@^0.14.3, tweetnacl@~0.14.0: + version "0.14.5" + resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64" + +typedarray@^0.0.6: + version "0.0.6" + resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" + +typescript@^2.4.2: + version "2.5.3" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-2.5.3.tgz#df3dcdc38f3beb800d4bc322646b04a3f6ca7f0d" + +uglify-es@^3.0.24: + version "3.1.2" + resolved "https://registry.yarnpkg.com/uglify-es/-/uglify-es-3.1.2.tgz#b22cfca950f0632092aff42bb46979d83b5ec6f5" + dependencies: + commander "~2.11.0" + source-map "~0.5.1" + +uglify-js@^2.8.29: + version "2.8.29" + resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-2.8.29.tgz#29c5733148057bb4e1f75df35b7a9cb72e6a59dd" + dependencies: + source-map "~0.5.1" + yargs "~3.10.0" + optionalDependencies: + uglify-to-browserify "~1.0.0" + +uglify-to-browserify@~1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/uglify-to-browserify/-/uglify-to-browserify-1.0.2.tgz#6e0924d6bda6b5afe349e39a6d632850a0f882b7" + +uglifyjs-webpack-plugin@1.0.0-beta.2: + version "1.0.0-beta.2" + resolved "https://registry.yarnpkg.com/uglifyjs-webpack-plugin/-/uglifyjs-webpack-plugin-1.0.0-beta.2.tgz#3652fd4011afed1956566755d545f8b3fec867b4" + dependencies: + cacache "^9.2.9" + find-cache-dir "^1.0.0" + schema-utils "^0.3.0" + source-map "^0.5.6" + uglify-es "^3.0.24" + webpack-sources "^1.0.1" + worker-farm "^1.4.1" + +uglifyjs-webpack-plugin@^0.4.6: + version "0.4.6" + resolved "https://registry.yarnpkg.com/uglifyjs-webpack-plugin/-/uglifyjs-webpack-plugin-0.4.6.tgz#b951f4abb6bd617e66f63eb891498e391763e309" + dependencies: + source-map "^0.5.6" + uglify-js "^2.8.29" + webpack-sources "^1.0.1" + +uid-number@^0.0.6: + version "0.0.6" + resolved "https://registry.yarnpkg.com/uid-number/-/uid-number-0.0.6.tgz#0ea10e8035e8eb5b8e4449f06da1c730663baa81" + +uniq@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/uniq/-/uniq-1.0.1.tgz#b31c5ae8254844a3a8281541ce2b04b865a734ff" + +uniqid@^4.0.0: + version "4.1.1" + resolved "https://registry.yarnpkg.com/uniqid/-/uniqid-4.1.1.tgz#89220ddf6b751ae52b5f72484863528596bb84c1" + dependencies: + macaddress "^0.2.8" + +uniqs@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/uniqs/-/uniqs-2.0.0.tgz#ffede4b36b25290696e6e165d4a59edb998e6b02" + +unique-filename@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/unique-filename/-/unique-filename-1.1.0.tgz#d05f2fe4032560871f30e93cbe735eea201514f3" + dependencies: + unique-slug "^2.0.0" + +unique-slug@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/unique-slug/-/unique-slug-2.0.0.tgz#db6676e7c7cc0629878ff196097c78855ae9f4ab" + dependencies: + imurmurhash "^0.1.4" + +url-loader@^0.5.9: + version "0.5.9" + resolved "https://registry.yarnpkg.com/url-loader/-/url-loader-0.5.9.tgz#cc8fea82c7b906e7777019250869e569e995c295" + dependencies: + loader-utils "^1.0.2" + mime "1.3.x" + +url@^0.11.0: + version "0.11.0" + resolved "https://registry.yarnpkg.com/url/-/url-0.11.0.tgz#3838e97cfc60521eb73c525a8e55bfdd9e2e28f1" + dependencies: + punycode "1.3.2" + querystring "0.2.0" + +util-deprecate@~1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" + +util@0.10.3, util@^0.10.3: + version "0.10.3" + resolved "https://registry.yarnpkg.com/util/-/util-0.10.3.tgz#7afb1afe50805246489e3db7fe0ed379336ac0f9" + dependencies: + inherits "2.0.1" + +uuid@^3.0.0, uuid@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.1.0.tgz#3dd3d3e790abc24d7b0d3a034ffababe28ebbc04" + +validate-npm-package-license@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.1.tgz#2804babe712ad3379459acfbe24746ab2c303fbc" + dependencies: + spdx-correct "~1.0.0" + spdx-expression-parse "~1.0.0" + +vendors@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/vendors/-/vendors-1.0.1.tgz#37ad73c8ee417fb3d580e785312307d274847f22" + +verror@1.10.0: + version "1.10.0" + resolved "https://registry.yarnpkg.com/verror/-/verror-1.10.0.tgz#3a105ca17053af55d6e270c1f8288682e18da400" + dependencies: + assert-plus "^1.0.0" + core-util-is "1.0.2" + extsprintf "^1.2.0" + +vm-browserify@0.0.4: + version "0.0.4" + resolved "https://registry.yarnpkg.com/vm-browserify/-/vm-browserify-0.0.4.tgz#5d7ea45bbef9e4a6ff65f95438e0a87c357d5a73" + dependencies: + indexof "0.0.1" + +vue-class-component@^5.0.0, vue-class-component@^5.0.2: + version "5.0.2" + resolved "https://registry.yarnpkg.com/vue-class-component/-/vue-class-component-5.0.2.tgz#3dcdc005c58c4e88d8ec2d46e01c74f4d90135c8" + +vue-hot-reload-api@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/vue-hot-reload-api/-/vue-hot-reload-api-2.1.0.tgz#9ca58a6e0df9078554ce1708688b6578754d86de" + +vue-loader@^13.0.4: + version "13.0.5" + resolved "https://registry.yarnpkg.com/vue-loader/-/vue-loader-13.0.5.tgz#3639e9fb3940cded7f0fc34429277fb6de70a57c" + dependencies: + consolidate "^0.14.0" + hash-sum "^1.0.2" + loader-utils "^1.1.0" + lru-cache "^4.1.1" + postcss "^6.0.6" + postcss-load-config "^1.1.0" + postcss-selector-parser "^2.0.0" + prettier "^1.7.0" + resolve "^1.3.3" + source-map "^0.5.6" + vue-hot-reload-api "^2.1.0" + vue-style-loader "^3.0.0" + vue-template-es2015-compiler "^1.5.3" + +vue-property-decorator@^5.2.1: + version "5.3.0" + resolved "https://registry.yarnpkg.com/vue-property-decorator/-/vue-property-decorator-5.3.0.tgz#f0fa1d0efd3c569ff2009fe9546cd2fb33ed7f43" + dependencies: + reflect-metadata "^0.1.9" + vue-class-component "^5.0.0" + +vue-style-loader@^3.0.0: + version "3.0.3" + resolved "https://registry.yarnpkg.com/vue-style-loader/-/vue-style-loader-3.0.3.tgz#623658f81506aef9d121cdc113a4f5c9cac32df7" + dependencies: + hash-sum "^1.0.2" + loader-utils "^1.0.2" + +vue-template-compiler@^2.4.2: + version "2.4.4" + resolved "https://registry.yarnpkg.com/vue-template-compiler/-/vue-template-compiler-2.4.4.tgz#2cde3b704124985c27d50b5387c9691ba515fb57" + dependencies: + de-indent "^1.0.2" + he "^1.1.0" + +vue-template-es2015-compiler@^1.5.3: + version "1.5.3" + resolved "https://registry.yarnpkg.com/vue-template-es2015-compiler/-/vue-template-es2015-compiler-1.5.3.tgz#22787de4e37ebd9339b74223bc467d1adee30545" + +vue@^2.4.2: + version "2.4.4" + resolved "https://registry.yarnpkg.com/vue/-/vue-2.4.4.tgz#ea9550b96a71465fd2b8b17b61673b3561861789" + +watchpack@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-1.4.0.tgz#4a1472bcbb952bd0a9bb4036801f954dfb39faac" + dependencies: + async "^2.1.2" + chokidar "^1.7.0" + graceful-fs "^4.1.2" + +webpack-sources@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-1.0.1.tgz#c7356436a4d13123be2e2426a05d1dad9cbe65cf" + dependencies: + source-list-map "^2.0.0" + source-map "~0.5.3" + +webpack@^3.5.4: + version "3.6.0" + resolved "https://registry.yarnpkg.com/webpack/-/webpack-3.6.0.tgz#a89a929fbee205d35a4fa2cc487be9cbec8898bc" + dependencies: + acorn "^5.0.0" + acorn-dynamic-import "^2.0.0" + ajv "^5.1.5" + ajv-keywords "^2.0.0" + async "^2.1.2" + enhanced-resolve "^3.4.0" + escope "^3.6.0" + interpret "^1.0.0" + json-loader "^0.5.4" + json5 "^0.5.1" + loader-runner "^2.3.0" + loader-utils "^1.1.0" + memory-fs "~0.4.1" + mkdirp "~0.5.0" + node-libs-browser "^2.0.0" + source-map "^0.5.3" + supports-color "^4.2.1" + tapable "^0.2.7" + uglifyjs-webpack-plugin "^0.4.6" + watchpack "^1.4.0" + webpack-sources "^1.0.1" + yargs "^8.0.2" + +whet.extend@~0.9.9: + version "0.9.9" + resolved "https://registry.yarnpkg.com/whet.extend/-/whet.extend-0.9.9.tgz#f877d5bf648c97e5aa542fadc16d6a259b9c11a1" + +which-module@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a" + +which@^1.2.9: + version "1.3.0" + resolved "https://registry.yarnpkg.com/which/-/which-1.3.0.tgz#ff04bdfc010ee547d780bec38e1ac1c2777d253a" + dependencies: + isexe "^2.0.0" + +wide-align@^1.1.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/wide-align/-/wide-align-1.1.2.tgz#571e0f1b0604636ebc0dfc21b0339bbe31341710" + dependencies: + string-width "^1.0.2" + +window-size@0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/window-size/-/window-size-0.1.0.tgz#5438cd2ea93b202efa3a19fe8887aee7c94f9c9d" + +wordwrap@0.0.2: + version "0.0.2" + resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-0.0.2.tgz#b79669bb42ecb409f83d583cad52ca17eaa1643f" + +worker-farm@^1.4.1: + version "1.5.0" + resolved "https://registry.yarnpkg.com/worker-farm/-/worker-farm-1.5.0.tgz#adfdf0cd40581465ed0a1f648f9735722afd5c8d" + dependencies: + errno "^0.1.4" + xtend "^4.0.1" + +wrap-ansi@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-2.1.0.tgz#d8fc3d284dd05794fe84973caecdd1cf824fdd85" + dependencies: + string-width "^1.0.1" + strip-ansi "^3.0.1" + +wrappy@1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" + +xtend@^4.0.0, xtend@^4.0.1, xtend@~4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.1.tgz#a5c6d532be656e23db820efb943a1f04998d63af" + +y18n@^3.2.1: + version "3.2.1" + resolved "https://registry.yarnpkg.com/y18n/-/y18n-3.2.1.tgz#6d15fba884c08679c0d77e88e7759e811e07fa41" + +yallist@^2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/yallist/-/yallist-2.1.2.tgz#1c11f9218f076089a47dd512f93c6699a6a81d52" + +yargs-parser@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-7.0.0.tgz#8d0ac42f16ea55debd332caf4c4038b3e3f5dfd9" + dependencies: + camelcase "^4.1.0" + +yargs@^8.0.2: + version "8.0.2" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-8.0.2.tgz#6299a9055b1cefc969ff7e79c1d918dceb22c360" + dependencies: + camelcase "^4.1.0" + cliui "^3.2.0" + decamelize "^1.1.1" + get-caller-file "^1.0.1" + os-locale "^2.0.0" + read-pkg-up "^2.0.0" + require-directory "^2.1.1" + require-main-filename "^1.0.1" + set-blocking "^2.0.0" + string-width "^2.0.0" + which-module "^2.0.0" + y18n "^3.2.1" + yargs-parser "^7.0.0" + +yargs@~3.10.0: + version "3.10.0" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-3.10.0.tgz#f7ee7bd857dd7c1d2d38c0e74efbd681d1431fd1" + dependencies: + camelcase "^1.0.2" + cliui "^2.1.0" + decamelize "^1.0.0" + window-size "0.1.0"