This commit is contained in:
MayaWolf 2018-04-17 01:14:13 +02:00
parent 959cac855a
commit 128c638ad4
30 changed files with 388 additions and 220 deletions

View File

@ -102,6 +102,7 @@
this.sizer = <HTMLTextAreaElement>this.$refs['sizer'];
this.sizer.style.cssText = styles.cssText;
this.sizer.style.height = '0';
this.sizer.style.minHeight = '0';
this.sizer.style.overflow = 'hidden';
this.sizer.style.position = 'absolute';
this.sizer.style.top = '0';

View File

@ -45,7 +45,7 @@ export let defaultButtons: ReadonlyArray<EditorButton> = [
key: Keys.KeyS
},
{
title: 'Color (Ctrl+D)\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.',
title: 'Color (Ctrl+D)\n\nStyles text with a color. Valid colors are: red, orange, yellow, green, cyan, blue, purple, pink, black, brown, white and gray.',
tag: 'color',
startText: '[color=]',
icon: 'fa-eye-dropper',

View File

@ -1,5 +1,5 @@
<template>
<modal :buttons="false" :action="l('chat.channels')" @close="closed" dialog-class="w-100 channel-list">
<modal :buttons="false" :action="l('chat.channels')" @open="opened" @close="closed" dialog-class="w-100 channel-list">
<div style="display:flex;flex-direction:column">
<tabs style="flex-shrink:0" :tabs="[l('channelList.public'), l('channelList.private')]" v-model="tab"></tabs>
<div style="display: flex; flex-direction: column">
@ -88,6 +88,10 @@
this.hide();
}
opened(): void {
core.channels.requestChannelsIfNeeded(30000);
}
closed(): void {
this.createName = '';
}

View File

@ -16,6 +16,10 @@
@Prop({required: true})
readonly text!: string;
mounted(): void {
core.channels.requestChannelsIfNeeded(300000);
}
joinChannel(): void {
if(this.channel === undefined || !this.channel.isJoined)
core.channels.join(this.id);

View File

@ -48,33 +48,29 @@
type BBCodeNode = Node & {bbcodeTag?: string, bbcodeParam?: string, bbcodeHide?: boolean};
function copyNode(str: string, node: BBCodeNode, range: Range, flags: {endFound?: true}): string {
if(node === range.endContainer) flags.endFound = true;
function copyNode(str: string, node: BBCodeNode, end: Node, range: Range, flags: {endFound?: true}): string {
if(node === end) flags.endFound = true;
if(node.bbcodeTag !== undefined)
str = `[${node.bbcodeTag}${node.bbcodeParam !== undefined ? `=${node.bbcodeParam}` : ''}]${str}[/${node.bbcodeTag}]`;
if(node.nextSibling !== null && !flags.endFound) {
if(node instanceof HTMLElement && getComputedStyle(node).display === 'block') str += '\r\n';
str += scanNode(node.nextSibling!, range, flags);
str += scanNode(node.nextSibling!, end, range, flags);
}
if(node.parentElement === null) return str;
return copyNode(str, node.parentNode!, range, flags);
return copyNode(str, node.parentNode!, end, range, flags);
}
function scanNode(node: BBCodeNode, range: Range, flags: {endFound?: true}, hide?: boolean): string {
function scanNode(node: BBCodeNode, end: Node, range: Range, flags: {endFound?: true}, hide?: boolean): string {
let str = '';
hide = hide || node.bbcodeHide;
if(node === range.endContainer) {
if(node instanceof HTMLElement && node.children.length === 1 && node.firstElementChild instanceof HTMLImageElement)
str += scanNode(node.firstElementChild, range, flags, hide);
flags.endFound = true;
}
if(node === end) flags.endFound = true;
if(node.bbcodeTag !== undefined) str += `[${node.bbcodeTag}${node.bbcodeParam !== undefined ? `=${node.bbcodeParam}` : ''}]`;
if(node instanceof Text) str += node === range.endContainer ? node.nodeValue!.substr(0, range.endOffset) : node.nodeValue;
else if(node instanceof HTMLImageElement) str += node.alt;
if(node.firstChild !== null && !flags.endFound) str += scanNode(node.firstChild, range, flags, hide);
if(node.firstChild !== null && !flags.endFound) str += scanNode(node.firstChild, end, range, flags, hide);
if(node.bbcodeTag !== undefined) str += `[/${node.bbcodeTag}]`;
if(node instanceof HTMLElement && getComputedStyle(node).display === 'block') str += '\r\n';
if(node.nextSibling !== null && !flags.endFound) str += scanNode(node.nextSibling, range, flags, hide);
if(node.nextSibling !== null && !flags.endFound) str += scanNode(node.nextSibling, end, range, flags, hide);
return hide ? '' : str;
}
@ -105,12 +101,15 @@
const selection = document.getSelection();
if(selection.isCollapsed) return;
const range = selection.getRangeAt(0);
const start = range.startContainer;
let startValue = start.nodeValue !== null ?
start.nodeValue.substring(range.startOffset, start === range.endContainer ? range.endOffset : undefined) : '';
if(start instanceof HTMLElement && start.children.length === 1 && start.firstElementChild instanceof HTMLImageElement)
startValue += scanNode(start.firstElementChild, range, {});
e.clipboardData.setData('text/plain', copyNode(startValue, start, range, {}));
let start = range.startContainer, end = range.endContainer;
let startValue: string;
if(start instanceof HTMLElement) {
start = start.childNodes[range.startOffset];
startValue = start instanceof HTMLImageElement ? start.alt : scanNode(start.firstChild!, end, range, {});
} else
startValue = start.nodeValue!.substring(range.startOffset, start === range.endContainer ? range.endOffset : undefined);
if(end instanceof HTMLElement) end = end.childNodes[range.endOffset - 1];
e.clipboardData.setData('text/plain', copyNode(startValue, start, end, range, {}));
e.preventDefault();
}) as EventListener);
window.addEventListener('keydown', (e) => {

View File

@ -62,10 +62,10 @@
<div class="input-group-prepend">
<div class="input-group-text"><span class="fas fa-search"></span></div>
</div>
<input v-model="searchInput" @keydown.esc="showSearch = false; searchInput = ''" @keypress="lastSearchInput = Date.now()"
<input v-model="searchInput" @keydown.esc="hideSearch" @keypress="lastSearchInput = Date.now()"
:placeholder="l('chat.search')" ref="searchField" class="form-control"/>
<a class="btn btn-sm btn-light" style="position:absolute;right:5px;top:50%;transform:translateY(-50%);line-height:0;z-index:10"
@click="showSearch = false"><i class="fas fa-times"></i></a>
@click="hideSearch"><i class="fas fa-times"></i></a>
</div>
<div class="border-top messages" :class="'messages-' + conversation.mode" style="flex:1;overflow:auto;margin-top:2px"
ref="messages" @scroll="onMessagesScroll">
@ -203,6 +203,11 @@
clearInterval(this.searchTimer);
}
hideSearch(): void {
this.showSearch = false;
this.searchInput = '';
}
get conversation(): Conversation {
return core.conversations.selectedConversation;
}

View File

@ -8,32 +8,41 @@
</template>
<div class="form-group row" style="flex-shrink:0" v-show="showFilters">
<label for="character" class="col-sm-2 col-form-label">{{l('logs.character')}}</label>
<div class="col-sm-10">
<div :class="canZip ? 'col-sm-8 col-10 col-xl-9' : 'col-sm-10'">
<select class="form-control" v-model="selectedCharacter" id="character" @change="loadCharacter">
<option value="">{{l('logs.selectCharacter')}}</option>
<option v-for="character in characters">{{character}}</option>
</select>
</div>
<div class="col-2 col-xl-1" v-if="canZip">
<button @click="downloadCharacter" class="btn btn-secondary form-control" :disabled="!selectedCharacter"><span
class="fa fa-download"></span></button>
</div>
</div>
<div class="form-group row" style="flex-shrink:0" v-show="showFilters">
<label class="col-sm-2 col-form-label">{{l('logs.conversation')}}</label>
<div class="col-sm-10">
<div :class="canZip ? 'col-sm-8 col-10 col-xl-9' : 'col-sm-10'">
<filterable-select v-model="selectedConversation" :options="conversations" :filterFunc="filterConversation"
:placeholder="l('filter')">
<template slot-scope="s">
{{s.option && ((s.option.key[0] == '#' ? '#' : '') + s.option.name) || l('logs.selectConversation')}}</template>
{{s.option && ((s.option.key[0] == '#' ? '#' : '') + s.option.name) || l('logs.selectConversation')}}
</template>
</filterable-select>
</div>
<div class="col-2 col-xl-1" v-if="canZip">
<button @click="downloadConversation" class="btn btn-secondary form-control" :disabled="!selectedConversation"><span
class="fa fa-download"></span></button>
</div>
</div>
<div class="form-group row" style="flex-shrink:0" v-show="showFilters">
<label for="date" class="col-sm-2 col-form-label">{{l('logs.date')}}</label>
<div class="col-sm-8 col-10">
<div class="col-sm-8 col-10 col-xl-9">
<select class="form-control" v-model="selectedDate" id="date" @change="loadMessages">
<option :value="null">{{l('logs.selectDate')}}</option>
<option v-for="date in dates" :value="date.getTime()">{{formatDate(date)}}</option>
</select>
</div>
<div class="col-2">
<div class="col-2 col-xl-1">
<button @click="downloadDay" class="btn btn-secondary form-control" :disabled="!selectedDate"><span
class="fa fa-download"></span></button>
</div>
@ -58,18 +67,19 @@
import FilterableSelect from '../components/FilterableSelect.vue';
import Modal from '../components/Modal.vue';
import {Keys} from '../keys';
import {getKey, messageToString} from './common';
import {formatTime, getKey, messageToString} from './common';
import core from './core';
import {Conversation, Logs as LogInterface} from './interfaces';
import l from './localize';
import MessageView from './message_view';
import Zip from './zip';
function formatDate(this: void, date: Date): string {
return format(date, 'YYYY-MM-DD');
}
function formatTime(this: void, date: Date): string {
return format(date, 'YYYY-MM-DD HH:mm');
function getLogs(messages: ReadonlyArray<Conversation.Message>): string {
return messages.reduce((acc, x) => acc + messageToString(x, (date) => formatTime(date, true)), '');
}
@Component({
@ -91,6 +101,7 @@
characters: ReadonlyArray<string> = [];
selectedCharacter = core.connection.character;
showFilters = true;
canZip = core.logs.canZip;
get filteredMessages(): ReadonlyArray<Conversation.Message> {
if(this.filter.length === 0) return this.messages;
@ -131,22 +142,47 @@
await this.loadMessages();
}
download(file: string, logs: ReadonlyArray<Conversation.Message>): void {
download(file: string, logs: string): void {
const a = document.createElement('a');
a.target = '_blank';
a.href = `data:${encodeURIComponent(file)},${encodeURIComponent(logs.map((x) => messageToString(x, formatTime)).join(''))}`;
a.href = logs;
a.setAttribute('download', file);
a.style.display = 'none';
document.body.appendChild(a);
setTimeout(() => {
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(logs);
});
}
downloadDay(): void {
if(this.selectedConversation === null || this.selectedDate === null || this.messages.length === 0) return;
this.download(`${this.selectedConversation.name}-${formatDate(new Date(this.selectedDate))}.txt`, this.messages);
const name = `${this.selectedConversation.name}-${formatDate(new Date(this.selectedDate))}.txt`;
this.download(name, `data:${encodeURIComponent(name)},${encodeURIComponent(getLogs(this.messages))}`);
}
async downloadConversation(): Promise<void> {
if(this.selectedConversation === null) return;
const zip = new Zip();
for(const date of this.dates) {
const messages = await core.logs.getLogs(this.selectedCharacter, this.selectedConversation.key, date);
zip.addFile(`${formatDate(date)}.txt`, getLogs(messages));
}
this.download(`${this.selectedConversation.name}.zip`, URL.createObjectURL(zip.build()));
}
async downloadCharacter(): Promise<void> {
if(this.selectedCharacter === '' || !confirm(l('logs.confirmExport', this.selectedCharacter))) return;
const zip = new Zip();
for(const conv of this.conversations) {
zip.addFile(`${conv.name}/`, '');
const dates = await core.logs.getLogDates(this.selectedCharacter, conv.key);
for(const date of dates) {
const messages = await core.logs.getLogs(this.selectedCharacter, conv.key, date);
zip.addFile(`${conv.name}/${formatDate(date)}.txt`, getLogs(messages));
}
}
this.download(`${this.selectedCharacter}.zip`, URL.createObjectURL(zip.build()));
}
async onOpen(): Promise<void> {

View File

@ -13,7 +13,7 @@
import Component from 'vue-class-component';
import CustomDialog from '../components/custom_dialog';
import Modal from '../components/Modal.vue';
import ChannelView from './ChannelView.vue';
import ChannelView from './ChannelTagView.vue';
import core from './core';
import {Character, Conversation} from './interfaces';
import l from './localize';

View File

@ -1,7 +1,8 @@
<template>
<modal :action="l('settings.action')" @submit="submit" @close="init()" id="settings" dialogClass="w-100">
<tabs style="flex-shrink:0;margin-bottom:10px" :tabs="tabs" v-model="selectedTab"></tabs>
<div v-show="selectedTab == 'general'">
<tabs style="flex-shrink:0;margin-bottom:10px" v-model="selectedTab"
:tabs="[l('settings.tabs.general'), l('settings.tabs.notifications'), l('settings.tabs.import')]"></tabs>
<div v-show="selectedTab == 0">
<div class="form-group">
<label class="control-label" for="disallowedTags">{{l('settings.disallowedTags')}}</label>
<input id="disallowedTags" class="form-control" v-model="disallowedTags"/>
@ -63,7 +64,7 @@
<input id="fontSize" type="number" min="10" max="24" class="form-control" v-model="fontSize"/>
</div>
</div>
<div v-show="selectedTab == 'notifications'">
<div v-show="selectedTab == 1">
<div class="form-group">
<label class="control-label" for="playSound">
<input type="checkbox" id="playSound" v-model="playSound"/>
@ -111,8 +112,8 @@
</label>
</div>
</div>
<div v-show="selectedTab == 'import'" style="display:flex;padding-top:10px">
<select id="import" class="form-control" v-model="importCharacter" style="flex:1;">
<div v-show="selectedTab == 2" style="display:flex;padding-top:10px">
<select id="import" class="form-control" v-model="importCharacter" style="flex:1;margin-right:10px">
<option value="">{{l('settings.import.selectCharacter')}}</option>
<option v-for="character in availableImports" :value="character">{{character}}</option>
</select>
@ -136,7 +137,7 @@
export default class SettingsView extends CustomDialog {
l = l;
availableImports: ReadonlyArray<string> = [];
selectedTab = 'general';
selectedTab = '0';
importCharacter = '';
playSound!: boolean;
clickOpensMessage!: boolean;
@ -205,13 +206,6 @@
core.conversations.reloadSettings();
}
get tabs(): {readonly [key: string]: string} {
const tabs: {[key: string]: string} = {};
(this.availableImports.length > 0 ? ['general', 'notifications', 'import'] : ['general', 'notifications'])
.forEach((item) => tabs[item] = l(`settings.tabs.${item}`));
return tabs;
}
async submit(): Promise<void> {
core.state.settings = {
playSound: this.playSound,

View File

@ -3,7 +3,7 @@ import {CoreBBCodeParser} from '../bbcode/core';
//tslint:disable-next-line:match-default-export-name
import BaseEditor from '../bbcode/Editor.vue';
import {BBCodeTextTag} from '../bbcode/parser';
import ChannelView from './ChannelView.vue';
import ChannelView from './ChannelTagView.vue';
import {characterImage} from './common';
import core from './core';
import {Character} from './interfaces';

View File

@ -1,4 +1,4 @@
import {format, isToday} from 'date-fns';
import {isToday} from 'date-fns';
import {Keys} from '../keys';
import {Character, Conversation, Settings as ISettings} from './interfaces';
@ -13,13 +13,10 @@ export function characterImage(this: void | never, character: string): string {
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;
let c = str.charCodeAt(i);
if(c > 0xD800 && c < 0xD8FF) //surrogate pairs
c = (c - 0xD800) * 0x400 + str.charCodeAt(++i) - 0xDC00 + 0x10000;
byteLen += c < 0x80 ? 1 : c < 0x800 ? 2 : c < 0x10000 ? 3 : c < 0x200000 ? 4 : c < 0x4000000 ? 5 : 6;
}
return byteLen;
}
@ -54,9 +51,13 @@ export class ConversationSettings implements Conversation.Settings {
defaultHighlights = true;
}
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');
function pad(num: number): string | number {
return num < 10 ? `0${num}` : num;
}
export function formatTime(this: void | never, date: Date, noDate: boolean = false): string {
if(!noDate && isToday(date)) return `${pad(date.getHours())}:${pad(date.getMinutes())}`;
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}`;
}
export function messageToString(this: void | never, msg: Conversation.Message, timeFormatter: (date: Date) => string = formatTime): string {
@ -75,6 +76,7 @@ export function getKey(e: KeyboardEvent): Keys {
export function errorToString(e: any): string {
return e instanceof Error ? e.message : e !== undefined ? e.toString() : '';
}
//tslint:enable
let messageId = 0;

View File

@ -727,7 +727,8 @@ export default function(this: void): Interfaces.State {
const message = new EventMessage(l(key, `[user]${data.character}[/user]`, status, decodeHTML(data.statusmsg)), time);
await addEventMessage(message);
const conv = state.privateMap[data.character.toLowerCase()];
if(conv !== undefined && core.state.settings.eventMessages && conv !== state.selectedConversation) await conv.addMessage(message);
if(conv !== undefined && (!core.state.settings.eventMessages || conv !== state.selectedConversation))
await conv.addMessage(message);
});
connection.onMessage('SYS', async(data, time) => {
state.selectedConversation.infoText = data.message;

View File

@ -133,6 +133,7 @@ export interface Logs {
getLogs(character: string, key: string, date: Date): Promise<ReadonlyArray<Conversation.Message>>
getLogDates(character: string, key: string): Promise<ReadonlyArray<Date>>
getAvailableCharacters(): Promise<ReadonlyArray<string>>
canZip: boolean;
}
export namespace Settings {

View File

@ -7,6 +7,7 @@ const strings: {[key: string]: string | undefined} = {
'action.copyWithoutBBCode': 'Copy without BBCode',
'action.paste': 'Paste',
'action.copyLink': 'Copy Link',
'action.incognito': 'Open in Incognito mode',
'action.suggestions': 'Suggestions',
'action.open': 'Show',
'action.close': 'Close',
@ -81,6 +82,7 @@ const strings: {[key: string]: string | undefined} = {
'logs.title': 'Logs',
'logs.character': 'Character',
'logs.conversation': 'Conversation',
'logs.confirmExport': 'Are you sure you would like to export all logs for {0}? If you have a lot of logs, this might take a while.',
'logs.date': 'Date',
'logs.selectCharacter': 'Select a character...',
'logs.selectConversation': 'Select a conversation...',
@ -168,7 +170,7 @@ Current log location: {1}`,
'settings.fontSize': 'Font size (experimental)',
'settings.showNeedsReply': 'Show indicator on PM tabs with unanswered messages',
'settings.defaultHighlights': 'Use global highlight words',
'settings.colorBookmarks': 'Show bookmarks in a different colour',
'settings.colorBookmarks': 'Show friends/bookmarks in a different colour',
'settings.beta': 'Opt-in to test unstable prerelease updates',
'fixLogs.action': 'Fix corrupted logs',
'fixLogs.text': `There are a few reason log files can become corrupted - log files from old versions with bugs that have since been fixed or incomplete file operations caused by computer crashes are the most common.

60
chat/zip.ts Normal file
View File

@ -0,0 +1,60 @@
let crcTable!: number[];
export default class Zip {
private blob: (object | string)[] = [];
private files: {header: object[], offset: number, name: string}[] = [];
private offset = 0;
constructor() {
if(crcTable !== undefined!) return;
crcTable = [];
for(let c, n = 0; n < 256; n++) {
c = n;
for(let k = 0; k < 8; k++)
c = ((c & 1) ? ((c >>> 1) ^ 0xEDB88320) : (c >>> 1)); //tslint:disable-line:strict-boolean-expressions
crcTable[n] = c;
}
}
addFile(name: string, content: string): void {
let crc = -1;
let length = 0;
for(let i = 0, strlen = content.length; i < strlen; ++i) {
let c = content.charCodeAt(i);
if(c > 0xD800 && c < 0xD8FF) //surrogate pairs
c = (c - 0xD800) * 0x400 + content.charCodeAt(++i) - 0xDC00 + 0x10000;
let l = c < 0x80 ? 1 : c < 0x800 ? 2 : c < 0x10000 ? 3 : c < 0x200000 ? 4 : c < 0x4000000 ? 5 : 6;
length += l;
let byte = l === 1 ? c : ((0xFF00 >> l) % 256) | (c >>> (l - 1) * 6);
--l;
while(true) {
crc = (crc >>> 8) ^ crcTable[(crc ^ byte) & 0xFF];
if(--l >= 0) byte = ((c >>> (l * 6)) & 0x3F) | 0x80;
else break;
}
}
crc = (crc ^ (-1)) >>> 0;
const file = {
header: [Uint16Array.of(0, 0, 0, 0, 0), Uint32Array.of(crc, length, length), Uint16Array.of(name.length, 0)],
offset: this.offset, name
};
this.blob.push(Uint32Array.of(0x04034B50));
this.blob.push(...file.header);
this.blob.push(name, content);
this.offset += name.length + length + 30;
this.files.push(file);
}
build(): Blob {
const start = this.offset;
for(const file of this.files) {
this.blob.push(Uint16Array.of(0x4B50, 0x0201, 0));
this.blob.push(...file.header);
this.blob.push(Uint16Array.of(0, 0, 0, 0, 0), Uint32Array.of(file.offset), file.name);
this.offset += file.name.length + 46;
}
this.blob.push(Uint16Array.of(0x4B50, 0x0605, 0, 0, this.files.length, this.files.length),
Uint32Array.of(this.offset - start, start), Uint16Array.of(0));
return new Blob(this.blob);
}
}

View File

@ -9,7 +9,7 @@
</h4>
<button type="button" class="close" @click="hide" aria-label="Close" v-show="!keepOpen">&times;</button>
</div>
<div class="modal-body" style="overflow:auto">
<div class="modal-body" style="overflow:auto" tabindex="-1">
<slot></slot>
</div>
<div class="modal-footer" v-if="buttons">

View File

@ -1,6 +1,6 @@
{
"name": "fchat",
"version": "3.0.0",
"version": "3.0.3",
"author": "The F-List Team",
"description": "F-List.net Chat Client",
"main": "main.js",

View File

@ -30,7 +30,7 @@
* @see {@link https://github.com/f-list/exported|GitHub repo}
*/
import Axios from 'axios';
import {exec} from 'child_process';
import {exec, execSync} from 'child_process';
import * as electron from 'electron';
import * as fs from 'fs';
import * as path from 'path';
@ -87,6 +87,26 @@ if(process.env.NODE_ENV === 'production') {
console.log(`%c${l('consoleWarning.body')}`, 'font-size: 16pt; color:red');
});
}
let browser: string | undefined;
function openIncognito(url: string): void {
if(browser === undefined)
try { //tslint:disable-next-line:max-line-length
browser = execSync(`FOR /F "skip=2 tokens=3" %A IN ('REG QUERY HKCU\\Software\\Microsoft\\Windows\\Shell\\Associations\\UrlAssociations\\http\\UserChoice /v ProgId') DO @(echo %A)`)
.toString().trim();
} catch(e) {
console.error(e);
}
switch(browser) {
case 'FirefoxURL':
exec(`start firefox.exe -private-window ${url}`);
break;
case 'ChromeHTML':
exec(`start chrome.exe -incognito ${url}`);
break;
default:
exec(`start iexplore.exe -private ${url}`);
}
}
const webContents = electron.remote.getCurrentWebContents();
webContents.on('context-menu', (_, props) => {
@ -116,7 +136,7 @@ webContents.on('context-menu', (_, props) => {
accelerator: 'CmdOrCtrl+V',
enabled: props.editFlags.canPaste
});
else if(props.linkURL.length > 0 && props.mediaType === 'none' && props.linkURL.substr(0, props.pageURL.length) !== props.pageURL)
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'),
@ -127,7 +147,13 @@ webContents.on('context-menu', (_, props) => {
electron.clipboard.writeText(props.linkURL);
}
});
else if(hasText)
if(process.platform === 'win32')
menuTemplate.push({
id: 'incognito',
label: l('action.incognito'),
click: () => openIncognito(props.linkURL)
});
} else if(hasText)
menuTemplate.push({
label: l('action.copyWithoutBBCode'),
enabled: can('Copy'),

View File

@ -93,18 +93,17 @@ export function serializeMessage(message: Message): {serialized: Buffer, size: n
return {serialized: buffer, size: offset + 2};
}
function deserializeMessage(buffer: Buffer, characterGetter: (name: string) => Character = (name) => core.characters.get(name),
unsafe: boolean = noAssert): {end: number, message: Conversation.Message} {
const time = buffer.readUInt32LE(0, unsafe);
const type = buffer.readUInt8(4, unsafe);
const senderLength = buffer.readUInt8(5, unsafe);
let offset = senderLength + 6;
const sender = buffer.toString('utf8', 6, offset);
function deserializeMessage(buffer: Buffer, offset: number = 0,
characterGetter: (name: string) => Character = (name) => core.characters.get(name),
unsafe: boolean = noAssert): {size: number, message: Conversation.Message} {
const time = buffer.readUInt32LE(offset, unsafe);
const type = buffer.readUInt8(offset += 4, unsafe);
const senderLength = buffer.readUInt8(offset += 1, unsafe);
const sender = buffer.toString('utf8', offset += 1, offset += senderLength);
const messageLength = buffer.readUInt16LE(offset, unsafe);
offset += 2;
const text = buffer.toString('utf8', offset, offset += messageLength);
const text = buffer.toString('utf8', offset += 2, offset + messageLength);
const message = new MessageImpl(type, characterGetter(sender), text, new Date(time * 1000));
return {message, end: offset + 2};
return {message, size: senderLength + messageLength + 10};
}
export function fixLogs(character: string): void {
@ -126,7 +125,7 @@ export function fixLogs(character: string): void {
while(pos < size) {
buffer.fill(-1);
fs.readSync(fd, buffer, 0, 50100, pos);
const deserialized = deserializeMessage(buffer, (name) => ({
const deserialized = deserializeMessage(buffer, 0, (name) => ({
gender: 'None', status: 'online', statusText: '', isFriend: false, isBookmarked: false, isChatOp: false,
isIgnored: false, name
}), false);
@ -138,8 +137,8 @@ export function fixLogs(character: string): void {
fs.writeSync(indexFd, buffer, 0, 7);
lastDay = day;
}
if(buffer.readUInt16LE(deserialized.end - 2) !== deserialized.end - 2) throw new Error();
pos += deserialized.end;
if(buffer.readUInt16LE(deserialized.size - 2) !== deserialized.size - 2) throw new Error();
pos += deserialized.size;
}
} catch {
fs.ftruncateSync(fd, pos);
@ -174,6 +173,7 @@ function loadIndex(name: string): Index {
}
export class Logs implements Logging {
canZip = true;
private index: Index = {};
private loadedIndex?: Index;
private loadedCharacter?: string;
@ -226,19 +226,20 @@ export class Logs implements Logging {
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(character, key);
const fd = fs.openSync(file, 'r');
let pos = index.offsets[dateOffset];
const size = dateOffset + 1 < index.offsets.length ? index.offsets[dateOffset + 1] : (fs.fstatSync(fd)).size;
while(pos < size) {
fs.readSync(fd, buffer, 0, 50100, pos);
const deserialized = deserializeMessage(buffer);
messages.push(deserialized.message);
pos += deserialized.end;
}
const pos = index.offsets[dateOffset];
const fd = fs.openSync(getLogFile(character, key), 'r');
const end = dateOffset + 1 < index.offsets.length ? index.offsets[dateOffset + 1] : (fs.fstatSync(fd)).size;
const length = end - pos;
const buffer = Buffer.allocUnsafe(length);
fs.readSync(fd, buffer, 0, length, pos);
fs.closeSync(fd);
let offset = 0;
while(offset < length) {
const deserialized = deserializeMessage(buffer, offset);
messages.push(deserialized.message);
offset += deserialized.size;
}
return messages;
}

View File

@ -86,6 +86,7 @@ class State implements Interfaces.State {
joinedChannels: Channel[] = [];
joinedMap: {[key: string]: Channel | undefined} = {};
handlers: Interfaces.EventHandler[] = [];
lastRequest = 0;
constructor(private connection: Connection) {
}
@ -110,13 +111,19 @@ class State implements Interfaces.State {
getChannel(id: string): Channel | undefined {
return this.joinedMap[id.toLowerCase()];
}
requestChannelsIfNeeded(maxAge: number): void {
if(this.lastRequest + maxAge > Date.now()) return;
this.lastRequest = Date.now();
this.connection.send('CHA');
this.connection.send('ORS');
}
}
let state: State;
export default function(this: void, connection: Connection, characters: Character.State): Interfaces.State {
state = new State(connection);
let getChannelTimer: NodeJS.Timer | undefined;
let rejoin: string[] | undefined;
connection.onEvent('connecting', (isReconnect) => {
if(isReconnect && rejoin === undefined) rejoin = Object.keys(state.joinedMap);
@ -128,16 +135,6 @@ export default function(this: void, connection: Connection, characters: Characte
queuedJoin(rejoin);
rejoin = undefined;
}
const getChannels = () => {
connection.send('CHA');
connection.send('ORS');
};
getChannels();
if(getChannelTimer !== undefined) clearInterval(getChannelTimer);
getChannelTimer = setInterval(getChannels, 60000);
});
connection.onEvent('closed', () => {
if(getChannelTimer !== undefined) clearInterval(getChannelTimer);
});
connection.onMessage('CHA', (data) => {
@ -269,5 +266,13 @@ export default function(this: void, connection: Connection, characters: Characte
};
connection.onMessage('AOP', globalHandler);
connection.onMessage('DOP', globalHandler);
connection.onMessage('RTB', (data) => {
if(data.type !== 'trackadd' && data.type !== 'trackrem' && data.type !== 'friendadd' && data.type !== 'friendremove') return;
for(const key in state.joinedMap) {
const channel = state.joinedMap[key]!;
const member = channel.members[data.name];
if(member !== undefined) channel.reSortMember(member);
}
});
return state;
}

View File

@ -36,6 +36,7 @@ export default class Connection implements Interfaces.Connection {
try {
this.ticket = await this.ticketProvider();
} catch(e) {
if(this.reconnectTimer !== undefined) this.reconnect();
return this.invokeErrorHandlers(<Error>e, true);
}
try {
@ -85,6 +86,7 @@ export default class Connection implements Interfaces.Connection {
close(): void {
if(this.reconnectTimer !== undefined) clearTimeout(this.reconnectTimer);
this.reconnectTimer = undefined;
this.cleanClose = true;
if(this.socket !== undefined) this.socket.close();
}

View File

@ -193,6 +193,7 @@ export namespace Channel {
onEvent(handler: EventHandler): void
getChannelItem(id: string): ListItem | undefined
getChannel(id: string): Channel | undefined
requestChannelsIfNeeded(maxAge: number): void;
}
export const enum Rank {

View File

@ -8,8 +8,8 @@ android {
applicationId "net.f_list.fchat"
minSdkVersion 19
targetSdkVersion 27
versionCode 13
versionName "3.0.0"
versionCode 14
versionName "3.0.3"
}
buildTypes {
release {

View File

@ -37,6 +37,7 @@ export class GeneralSettings {
type Index = {[key: string]: {name: string, dates: number[]} | undefined};
export class Logs implements Logging {
canZip = false;
private index: Index = {};
private loadedIndex?: Index;
private loadedCharacter?: string;

View File

@ -1,6 +1,6 @@
{
"name": "net.f_list.fchat",
"version": "3.0.0",
"version": "3.0.3",
"displayName": "F-Chat",
"author": "The F-List Team",
"description": "F-List.net Chat Client",

View File

@ -5,11 +5,11 @@
"description": "F-List Exported",
"license": "MIT",
"devDependencies": {
"@fortawesome/fontawesome-free-webfonts": "^1.0.5",
"@types/node": "^9.6.0",
"@fortawesome/fontawesome-free-webfonts": "^1.0.6",
"@types/node": "^9.6.5",
"@types/sortablejs": "^1.3.31",
"axios": "^0.18.0",
"bootstrap": "^4.0.0",
"bootstrap": "^4.1.0",
"css-loader": "^0.28.11",
"date-fns": "^1.28.5",
"electron": "^1.8.4",
@ -23,10 +23,10 @@
"node-sass": "^4.8.3",
"optimize-css-assets-webpack-plugin": "^4.0.0",
"qs": "^6.5.1",
"raven-js": "^3.24.0",
"sass-loader": "^6.0.7",
"raven-js": "^3.24.1",
"sass-loader": "^7.0.1",
"sortablejs": "^1.6.0",
"ts-loader": "^4.1.0",
"ts-loader": "^4.2.0",
"tslib": "^1.7.1",
"tslint": "^5.7.0",
"typescript": "^2.8.1",
@ -35,11 +35,12 @@
"vue-loader": "^14.2.2",
"vue-property-decorator": "^6.0.0",
"vue-template-compiler": "^2.5.16",
"webpack": "^4.3.0"
"webpack": "^4.5.0"
},
"dependencies": {
"@types/lodash": "^4.14.106",
"@types/lodash": "^4.14.107",
"keytar": "^4.2.1",
"spellchecker": "^3.4.3"
"spellchecker": "^3.4.3",
"style-loader": "^0.20.3"
}
}

View File

@ -42,7 +42,8 @@ import Connection from '../fchat/connection';
import '../scss/fa.scss'; //tslint:disable-line:no-import-side-effect
import {Logs, SettingsStore} from './logs';
if(typeof Promise !== 'function' || typeof Notification !== 'function') //tslint:disable-line:strict-type-predicates
//@ts-ignore
if(typeof window.Promise !== 'function' || typeof window.Notification !== 'function') //tslint:disable-line:strict-type-predicates
alert('Your browser is too old to be supported by F-Chat 3.0. Please update to a newer version.');
const version = (<{version: string}>require('./package.json')).version; //tslint:disable-line:no-require-imports

View File

@ -62,23 +62,29 @@ async function getIndex(db: IDBDatabase): Promise<Index> {
}
export class Logs implements Logging {
canZip = true;
index?: Index;
loadedDb?: IDBDatabase;
loadedCharacter?: string;
loadedIndex?: Index;
db!: IDBDatabase;
db?: IDBDatabase;
constructor() {
core.connection.onEvent('connecting', async() => {
const characters = (await this.getAvailableCharacters());
if(characters.indexOf(core.connection.character) === -1)
window.localStorage.setItem(charactersKey, JSON.stringify(characters.concat(core.connection.character)));
this.db = await openDatabase(core.connection.character);
this.index = await getIndex(this.db);
try {
this.db = await openDatabase(core.connection.character);
this.index = await getIndex(this.db);
} catch(e) {
console.error(e);
}
});
}
async logMessage(conversation: Conversation, message: Conversation.Message): Promise<void> {
if(this.db === undefined) return;
let conv = this.index![conversation.key];
if(conv === undefined) {
const cTrans = this.db.transaction(['conversations'], 'readwrite');
@ -95,6 +101,7 @@ export class Logs implements Logging {
}
async getBacklog(conversation: Conversation): Promise<ReadonlyArray<Conversation.Message>> {
if(this.db === undefined) return [];
const trans = this.db.transaction(['logs']);
const conv = this.index![conversation.key];
if(conv === undefined) return [];
@ -105,14 +112,18 @@ export class Logs implements Logging {
private async loadIndex(character: string): Promise<Index> {
if(character === this.loadedCharacter) return this.loadedIndex!;
this.loadedCharacter = character;
if(character === core.connection.character) {
this.loadedDb = this.db;
this.loadedIndex = this.index;
} else {
this.loadedDb = await openDatabase(character);
this.loadedIndex = await getIndex(this.loadedDb);
}
} else
try {
this.loadedDb = await openDatabase(character);
this.loadedIndex = await getIndex(this.loadedDb);
} catch(e) {
console.error(e);
return {};
}
this.loadedCharacter = character;
return this.loadedIndex!;
}
@ -122,8 +133,10 @@ export class Logs implements Logging {
}
async getLogs(character: string, key: string, date: Date): Promise<ReadonlyArray<Conversation.Message>> {
const id = (await this.loadIndex(character))[key]!.id;
const trans = this.loadedDb!.transaction(['logs']);
await this.loadIndex(character);
if(this.loadedDb === undefined) return [];
const id = this.loadedIndex![key]!.id;
const trans = this.loadedDb.transaction(['logs']);
const day = Math.floor(date.getTime() / dayMs - date.getTimezoneOffset() / 1440);
return iterate(trans.objectStore('logs').index('conversation-day').openCursor(getComposite(id, day)),
(value: StoredMessage) => value.type === Conversation.Message.Type.Event ? new EventMessage(value.text, value.time) :
@ -131,8 +144,10 @@ export class Logs implements Logging {
}
async getLogDates(character: string, key: string): Promise<ReadonlyArray<Date>> {
const id = (await this.loadIndex(character))[key]!.id;
const trans = this.loadedDb!.transaction(['logs']);
await this.loadIndex(character);
if(this.loadedDb === undefined) return [];
const id = this.loadedIndex![key]!.id;
const trans = this.loadedDb.transaction(['logs']);
const bound = IDBKeyRange.bound(getComposite(id, 0), getComposite(id, 1000000));
return iterate(trans.objectStore('logs').index('conversation-day').openCursor(bound, 'nextunique'), (value: StoredMessage) => {
const date = new Date((hasComposite ? <number>value.day : decode((<string>value.day).substr(2))) * dayMs);
@ -147,11 +162,11 @@ export class Logs implements Logging {
}
export class SettingsStore implements Settings.Store {
async get<K extends keyof Settings.Keys>(key: K): Promise<Settings.Keys[K] | undefined> {
const stored = window.localStorage.getItem(`${core.connection.character}.settings.${key}`);
async get<K extends keyof Settings.Keys>(key: K, character: string = core.connection.character): Promise<Settings.Keys[K] | undefined> {
const stored = window.localStorage.getItem(`${character}.settings.${key}`);
if(stored === null) {
if(key === 'pinned') {
const tabs20 = window.localStorage.getItem(`tabs_${core.connection.character.toLowerCase()}`);
const tabs20 = window.localStorage.getItem(`tabs_${character.toLowerCase().replace(' ', '_')}`);
if(tabs20 !== null)
try {
const tabs = JSON.parse(tabs20) as {type: string, id: string, title: string}[];

View File

@ -1,6 +1,6 @@
{
"name": "net.f_list.fchat",
"version": "3.0.0",
"version": "3.0.3",
"displayName": "F-Chat",
"author": "The F-List Team",
"description": "F-List.net Chat Client",

184
yarn.lock
View File

@ -22,21 +22,21 @@
"7zip-bin-mac" "~1.0.1"
"7zip-bin-win" "~2.2.0"
"@fortawesome/fontawesome-free-webfonts@^1.0.5":
version "1.0.5"
resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-free-webfonts/-/fontawesome-free-webfonts-1.0.5.tgz#3b9e023b6230cdb144204f08e77178c7694e9045"
"@fortawesome/fontawesome-free-webfonts@^1.0.6":
version "1.0.6"
resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-free-webfonts/-/fontawesome-free-webfonts-1.0.6.tgz#3dd13aa1a7466bff8fb0a401e1771c011ef2ca14"
"@types/lodash@^4.14.106":
version "4.14.106"
resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.106.tgz#6093e9a02aa567ddecfe9afadca89e53e5dce4dd"
"@types/lodash@^4.14.107":
version "4.14.107"
resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.107.tgz#b2d2ae3958bfb8ff828495cbe12214af9e4d035e"
"@types/node@*", "@types/node@^9.6.0":
version "9.6.0"
resolved "https://registry.yarnpkg.com/@types/node/-/node-9.6.0.tgz#d3480ee666df9784b1001a1872a2f6ccefb6c2d7"
"@types/node@*", "@types/node@^9.6.5":
version "9.6.5"
resolved "https://registry.yarnpkg.com/@types/node/-/node-9.6.5.tgz#ee700810fdf49ac1c399fc5980b7559b3e5a381d"
"@types/node@^8.0.24":
version "8.10.0"
resolved "https://registry.yarnpkg.com/@types/node/-/node-8.10.0.tgz#f5d649cc49af8ed6507d15dc6e9b43fe8b927540"
version "8.10.8"
resolved "https://registry.yarnpkg.com/@types/node/-/node-8.10.8.tgz#794cba23cc9f8d9715f6543fa8827433b5f5cd3b"
"@types/sortablejs@^1.3.31":
version "1.3.32"
@ -318,8 +318,8 @@ aws-sign2@~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"
version "1.7.0"
resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.7.0.tgz#d4d0e9b9dbfca77bf08eeb0a8a471550fe39e289"
axios@^0.18.0:
version "0.18.0"
@ -423,9 +423,9 @@ boom@5.x.x:
dependencies:
hoek "4.x.x"
bootstrap@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/bootstrap/-/bootstrap-4.0.0.tgz#ceb03842c145fcc1b9b4e15da2a05656ba68469a"
bootstrap@^4.1.0:
version "4.1.0"
resolved "https://registry.yarnpkg.com/bootstrap/-/bootstrap-4.1.0.tgz#110b05c31a236d56dbc9adcda6dd16f53738a28a"
boxen@^1.2.1:
version "1.3.0"
@ -455,16 +455,14 @@ braces@^1.8.2:
repeat-element "^1.1.2"
braces@^2.3.0, braces@^2.3.1:
version "2.3.1"
resolved "https://registry.yarnpkg.com/braces/-/braces-2.3.1.tgz#7086c913b4e5a08dbe37ac0ee6a2500c4ba691bb"
version "2.3.2"
resolved "https://registry.yarnpkg.com/braces/-/braces-2.3.2.tgz#5979fd3f14cd531565e5fa2df1abfff1dfaee729"
dependencies:
arr-flatten "^1.1.0"
array-unique "^0.3.2"
define-property "^1.0.0"
extend-shallow "^2.0.1"
fill-range "^4.0.0"
isobject "^3.0.1"
kind-of "^6.0.2"
repeat-element "^1.1.2"
snapdragon "^0.8.1"
snapdragon-node "^2.0.1"
@ -476,8 +474,8 @@ brorand@^1.0.1:
resolved "https://registry.yarnpkg.com/brorand/-/brorand-1.1.0.tgz#12c25efe40a45e3c323eb8675a0a0ce57b22371f"
browserify-aes@^1.0.0, browserify-aes@^1.0.4:
version "1.1.1"
resolved "https://registry.yarnpkg.com/browserify-aes/-/browserify-aes-1.1.1.tgz#38b7ab55edb806ff2dcda1a7f1620773a477c49f"
version "1.2.0"
resolved "https://registry.yarnpkg.com/browserify-aes/-/browserify-aes-1.2.0.tgz#326734642f403dabc3003209853bb70ad428ef48"
dependencies:
buffer-xor "^1.0.3"
cipher-base "^1.0.0"
@ -487,16 +485,16 @@ browserify-aes@^1.0.0, browserify-aes@^1.0.4:
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"
version "1.0.1"
resolved "https://registry.yarnpkg.com/browserify-cipher/-/browserify-cipher-1.0.1.tgz#8d6474c1b870bfdabcd3bcfcc1934a10e94f15f0"
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"
version "1.0.1"
resolved "https://registry.yarnpkg.com/browserify-des/-/browserify-des-1.0.1.tgz#3343124db6d7ad53e26a8826318712bdc8450f9c"
dependencies:
cipher-base "^1.0.1"
des.js "^1.0.0"
@ -694,8 +692,8 @@ caniuse-api@^1.5.2:
lodash.uniq "^4.5.0"
caniuse-db@^1.0.30000529, caniuse-db@^1.0.30000634, caniuse-db@^1.0.30000639:
version "1.0.30000821"
resolved "https://registry.yarnpkg.com/caniuse-db/-/caniuse-db-1.0.30000821.tgz#3fcdc67c446a94a9cdd848248a4e3e54b2da7419"
version "1.0.30000830"
resolved "https://registry.yarnpkg.com/caniuse-db/-/caniuse-db-1.0.30000830.tgz#6e45255b345649fd15ff59072da1e12bb3de2f13"
capture-stack-trace@^1.0.0:
version "1.0.0"
@ -765,8 +763,8 @@ chownr@^1.0.1:
resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.0.1.tgz#e2a75042a9551908bebd25b8523d5f9769d79181"
chrome-trace-event@^0.1.1:
version "0.1.2"
resolved "https://registry.yarnpkg.com/chrome-trace-event/-/chrome-trace-event-0.1.2.tgz#90f36885d5345a50621332f0717b595883d5d982"
version "0.1.3"
resolved "https://registry.yarnpkg.com/chrome-trace-event/-/chrome-trace-event-0.1.3.tgz#d395af2d31c87b90a716c831fe326f69768ec084"
chromium-pickle-js@^0.2.0:
version "0.2.0"
@ -1002,8 +1000,8 @@ cosmiconfig@^2.1.0, cosmiconfig@^2.1.1:
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"
version "4.0.1"
resolved "https://registry.yarnpkg.com/create-ecdh/-/create-ecdh-4.0.1.tgz#44223dfed533193ba5ba54e0df5709b89acf1f82"
dependencies:
bn.js "^4.1.0"
elliptic "^6.0.0"
@ -1015,17 +1013,18 @@ create-error-class@^3.0.0:
capture-stack-trace "^1.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"
version "1.2.0"
resolved "https://registry.yarnpkg.com/create-hash/-/create-hash-1.2.0.tgz#889078af11a63756bcfb59bd221996be3a9ef196"
dependencies:
cipher-base "^1.0.1"
inherits "^2.0.1"
ripemd160 "^2.0.0"
md5.js "^1.3.4"
ripemd160 "^2.0.1"
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"
version "1.1.7"
resolved "https://registry.yarnpkg.com/create-hmac/-/create-hmac-1.1.7.tgz#69170c78b3ab957147b2b8b04572e47ead2243ff"
dependencies:
cipher-base "^1.0.3"
create-hash "^1.1.0"
@ -1265,8 +1264,8 @@ diff@^3.2.0:
resolved "https://registry.yarnpkg.com/diff/-/diff-3.5.0.tgz#800c0dd1e0a8bfbc95835c202ad220fe317e5a12"
diffie-hellman@^5.0.0:
version "5.0.2"
resolved "https://registry.yarnpkg.com/diffie-hellman/-/diffie-hellman-5.0.2.tgz#b5835739270cfe26acf632099fded2a07f209e5e"
version "5.0.3"
resolved "https://registry.yarnpkg.com/diffie-hellman/-/diffie-hellman-5.0.3.tgz#40e8ee98f55a2149607146921c63e1ae5f3d2875"
dependencies:
bn.js "^4.1.0"
miller-rabin "^4.0.0"
@ -1475,8 +1474,8 @@ electron-publish@20.8.1:
mime "^2.2.0"
electron-to-chromium@^1.2.7:
version "1.3.41"
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.41.tgz#7e33643e00cd85edfd17e04194f6d00e73737235"
version "1.3.42"
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.42.tgz#95c33bf01d0cc405556aec899fe61fd4d76ea0f9"
electron-updater@^2.21.4:
version "2.21.4"
@ -2232,8 +2231,10 @@ https-browserify@^1.0.0:
resolved "https://registry.yarnpkg.com/https-browserify/-/https-browserify-1.0.0.tgz#ec06c10e0a34c0f2faf199f7fd7fc78fffd03c73"
iconv-lite@^0.4.19:
version "0.4.19"
resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.19.tgz#f7468f60135f5e5dad3399c0a81be9a1603a082b"
version "0.4.21"
resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.21.tgz#c47f8733d02171189ebc4a400f3218d348094798"
dependencies:
safer-buffer "^2.1.0"
icss-replace-symbols@^1.1.0:
version "1.1.0"
@ -2930,8 +2931,8 @@ mime-types@^2.1.12, mime-types@~2.1.17, mime-types@~2.1.7:
mime-db "~1.33.0"
mime@^2.2.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/mime/-/mime-2.2.0.tgz#161e541965551d3b549fa1114391e3a3d55b923b"
version "2.3.1"
resolved "https://registry.yarnpkg.com/mime/-/mime-2.3.1.tgz#b1621c54d63b97c47d3cfe7f7215f7d64517c369"
mimic-fn@^1.0.0:
version "1.2.0"
@ -2942,8 +2943,8 @@ mimic-response@^1.0.0:
resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-1.0.0.tgz#df3d3652a73fded6b9b0b24146e6fd052353458e"
minimalistic-assert@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.0.tgz#702be2dda6b37f4836bcb3f5db56641b64a1d3d3"
version "1.0.1"
resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz#2e194de044626d4a10e7f7fbc00ce73e83e4d5c7"
minimalistic-crypto-utils@^1.0.0, minimalistic-crypto-utils@^1.0.1:
version "1.0.1"
@ -3045,8 +3046,8 @@ nanomatch@^1.2.9:
to-regex "^3.0.1"
neo-async@^2.5.0:
version "2.5.0"
resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.5.0.tgz#76b1c823130cca26acfbaccc8fbaf0a2fa33b18f"
version "2.5.1"
resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.5.1.tgz#acb909e327b1e87ec9ef15f41b8a269512ad41ee"
node-abi@^2.2.0:
version "2.3.0"
@ -3347,8 +3348,8 @@ parallel-transform@^1.1.0:
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"
version "5.1.1"
resolved "https://registry.yarnpkg.com/parse-asn1/-/parse-asn1-5.1.1.tgz#f6bf293818332bd0dab54efb16087724745e6ca8"
dependencies:
asn1.js "^4.0.0"
browserify-aes "^1.0.0"
@ -3765,8 +3766,8 @@ postcss@^6.0.1, postcss@^6.0.8:
supports-color "^5.3.0"
prebuild-install@^2.4.1:
version "2.5.1"
resolved "https://registry.yarnpkg.com/prebuild-install/-/prebuild-install-2.5.1.tgz#0f234140a73760813657c413cdccdda58296b1da"
version "2.5.3"
resolved "https://registry.yarnpkg.com/prebuild-install/-/prebuild-install-2.5.3.tgz#9f65f242782d370296353710e9bc843490c19f69"
dependencies:
detect-libc "^1.0.3"
expand-template "^1.0.2"
@ -3793,8 +3794,8 @@ preserve@^0.2.0:
resolved "https://registry.yarnpkg.com/preserve/-/preserve-0.2.0.tgz#815ed1f6ebc65926f865b310c0713bcb3315ce4b"
prettier@^1.7.0:
version "1.11.1"
resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.11.1.tgz#61e43fc4cd44e68f2b0dfc2c38cd4bb0fccdcc75"
version "1.12.0"
resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.12.0.tgz#d26fc5894b9230de97629b39cae225b503724ce8"
pretty-bytes@^1.0.2:
version "1.0.4"
@ -3831,8 +3832,8 @@ pseudomap@^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"
version "4.0.2"
resolved "https://registry.yarnpkg.com/public-encrypt/-/public-encrypt-4.0.2.tgz#46eb9107206bf73489f8b85b69d91334c6610994"
dependencies:
bn.js "^4.1.0"
browserify-rsa "^4.0.0"
@ -3925,9 +3926,9 @@ randomfill@^1.0.3:
randombytes "^2.0.5"
safe-buffer "^5.1.0"
raven-js@^3.24.0:
version "3.24.0"
resolved "https://registry.yarnpkg.com/raven-js/-/raven-js-3.24.0.tgz#59464d8bc4b3812ae87a282e9bb98ecad5b4b047"
raven-js@^3.24.1:
version "3.24.1"
resolved "https://registry.yarnpkg.com/raven-js/-/raven-js-3.24.1.tgz#7327e08786248517eedd36205bf50f1047821ffa"
rc@^1.0.1, rc@^1.1.2, rc@^1.1.6, rc@^1.1.7, rc@^1.2.1:
version "1.2.6"
@ -3968,15 +3969,15 @@ read-pkg@^1.0.0:
path-type "^1.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.6, readable-stream@^2.1.4, readable-stream@^2.1.5, readable-stream@^2.2.2, readable-stream@^2.3.3, readable-stream@^2.3.5:
version "2.3.5"
resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.5.tgz#b4f85003a938cbb6ecbce2a124fb1012bd1a838d"
version "2.3.6"
resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.6.tgz#b11c27d88b8ff1fbe070643cf94b0c79ae1b0aaf"
dependencies:
core-util-is "~1.0.0"
inherits "~2.0.3"
isarray "~1.0.0"
process-nextick-args "~2.0.0"
safe-buffer "~5.1.1"
string_decoder "~1.0.3"
string_decoder "~1.1.1"
util-deprecate "~1.0.1"
readable-stream@~1.1.9:
@ -4184,8 +4185,8 @@ resolve-url@^0.2.1:
resolved "https://registry.yarnpkg.com/resolve-url/-/resolve-url-0.2.1.tgz#2c637fe77c893afd2a663fe21aa9080068e2052a"
resolve@^1.3.2, resolve@^1.4.0, resolve@^1.5.0:
version "1.6.0"
resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.6.0.tgz#0fbd21278b27b4004481c395349e7aba60a9ff5c"
version "1.7.1"
resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.7.1.tgz#aadd656374fd298aee895bc026b8297418677fd3"
dependencies:
path-parse "^1.0.5"
@ -4222,6 +4223,10 @@ safe-regex@^1.1.0:
dependencies:
ret "~0.1.10"
safer-buffer@^2.1.0:
version "2.1.2"
resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a"
sanitize-filename@^1.6.1:
version "1.6.1"
resolved "https://registry.yarnpkg.com/sanitize-filename/-/sanitize-filename-1.6.1.tgz#612da1c96473fa02dccda92dcd5b4ab164a6772a"
@ -4237,9 +4242,9 @@ sass-graph@^2.2.4:
scss-tokenizer "^0.2.3"
yargs "^7.0.0"
sass-loader@^6.0.7:
version "6.0.7"
resolved "https://registry.yarnpkg.com/sass-loader/-/sass-loader-6.0.7.tgz#dd2fdb3e7eeff4a53f35ba6ac408715488353d00"
sass-loader@^7.0.1:
version "7.0.1"
resolved "https://registry.yarnpkg.com/sass-loader/-/sass-loader-7.0.1.tgz#fd937259ccba3a9cfe0d5f8a98746d48adfcc261"
dependencies:
clone-deep "^2.0.1"
loader-utils "^1.0.1"
@ -4574,9 +4579,9 @@ string-width@^2.0.0, string-width@^2.1.1:
is-fullwidth-code-point "^2.0.0"
strip-ansi "^4.0.0"
string_decoder@^1.0.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.0.tgz#384f322ee8a848e500effde99901bba849c5d403"
string_decoder@^1.0.0, string_decoder@~1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8"
dependencies:
safe-buffer "~5.1.0"
@ -4584,12 +4589,6 @@ 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, stringstream@~0.0.5:
version "0.0.5"
resolved "https://registry.yarnpkg.com/stringstream/-/stringstream-0.0.5.tgz#4e484cd4de5a0bbbee18e46307710a8a81621878"
@ -4626,6 +4625,13 @@ 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"
style-loader@^0.20.3:
version "0.20.3"
resolved "https://registry.yarnpkg.com/style-loader/-/style-loader-0.20.3.tgz#ebef06b89dec491bcb1fdb3452e913a6fd1c10c4"
dependencies:
loader-utils "^1.1.0"
schema-utils "^0.4.5"
sumchecker@^1.2.0:
version "1.3.1"
resolved "https://registry.yarnpkg.com/sumchecker/-/sumchecker-1.3.1.tgz#79bb3b4456dd04f18ebdbc0d703a1d1daec5105d"
@ -4748,8 +4754,8 @@ timed-out@^4.0.0:
resolved "https://registry.yarnpkg.com/timed-out/-/timed-out-4.0.1.tgz#f32eacac5a175bea25d7fab565ab3ed8741ef56f"
timers-browserify@^2.0.4:
version "2.0.6"
resolved "https://registry.yarnpkg.com/timers-browserify/-/timers-browserify-2.0.6.tgz#241e76927d9ca05f4d959819022f5b3664b64bae"
version "2.0.7"
resolved "https://registry.yarnpkg.com/timers-browserify/-/timers-browserify-2.0.7.tgz#e74093629cb62c20332af587ddc0c86b4ba97a05"
dependencies:
setimmediate "^1.0.4"
@ -4801,9 +4807,9 @@ truncate-utf8-bytes@^1.0.0:
dependencies:
utf8-byte-length "^1.0.1"
ts-loader@^4.1.0:
version "4.1.0"
resolved "https://registry.yarnpkg.com/ts-loader/-/ts-loader-4.1.0.tgz#6216e75600941df3270bc4a7125e20aefb2dc5ea"
ts-loader@^4.2.0:
version "4.2.0"
resolved "https://registry.yarnpkg.com/ts-loader/-/ts-loader-4.2.0.tgz#c380c399fc81f82cad0e3044f9c1f775ecde6efa"
dependencies:
chalk "^2.3.0"
enhanced-resolve "^4.0.0"
@ -4833,8 +4839,8 @@ tslint@^5.7.0:
tsutils "^2.12.1"
tsutils@^2.12.1:
version "2.24.0"
resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-2.24.0.tgz#a84134358e10c42039fed0a8f1548413f118aff7"
version "2.26.1"
resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-2.26.1.tgz#9e4a0cb9ff173863f34c22a961969081270d1878"
dependencies:
tslib "^1.8.1"
@ -4949,8 +4955,8 @@ upath@^1.0.0:
resolved "https://registry.yarnpkg.com/upath/-/upath-1.0.4.tgz#ee2321ba0a786c50973db043a50b7bcba822361d"
update-notifier@^2.4.0:
version "2.4.0"
resolved "https://registry.yarnpkg.com/update-notifier/-/update-notifier-2.4.0.tgz#f9b4c700fbfd4ec12c811587258777d563d8c866"
version "2.5.0"
resolved "https://registry.yarnpkg.com/update-notifier/-/update-notifier-2.5.0.tgz#d0744593e13f161e406acb1d9408b72cad08aff6"
dependencies:
boxen "^1.2.1"
chalk "^2.0.1"
@ -5111,9 +5117,9 @@ webpack-sources@^1.0.1, webpack-sources@^1.1.0:
source-list-map "^2.0.0"
source-map "~0.6.1"
webpack@^4.3.0:
version "4.3.0"
resolved "https://registry.yarnpkg.com/webpack/-/webpack-4.3.0.tgz#0b0c1e211311b3995dd25aed47ab46ea658be070"
webpack@^4.5.0:
version "4.5.0"
resolved "https://registry.yarnpkg.com/webpack/-/webpack-4.5.0.tgz#1e6f71e148ead02be265ff2879c9cd6bb30b8848"
dependencies:
acorn "^5.0.0"
acorn-dynamic-import "^3.0.0"