3.0.7 - electron-builder removed

This commit is contained in:
MayaWolf 2018-08-10 18:59:37 +02:00
parent 4d8f6c3670
commit 7151bf916e
49 changed files with 2191 additions and 1893 deletions

View File

@ -124,7 +124,7 @@
core.register('conversations', Conversations());
core.connection.onEvent('closed', async(isReconnect) => {
if(isReconnect) (<Modal>this.$refs['reconnecting']).show(true);
if(this.connected) await core.notifications.playSound('logout');
if(this.connected) core.notifications.playSound('logout');
this.connected = false;
this.connecting = false;
document.title = l('title');
@ -138,7 +138,7 @@
this.error = '';
this.connecting = false;
this.connected = true;
await core.notifications.playSound('login');
core.notifications.playSound('login');
document.title = l('title.connected', core.connection.character);
});
core.watch(() => core.conversations.hasNew, (hasNew) => {

View File

@ -4,7 +4,7 @@
@touchend="$refs['userMenu'].handleEvent($event)">
<sidebar id="sidebar" :label="l('chat.menu')" icon="fa-bars">
<img :src="characterImage(ownCharacter.name)" v-if="showAvatars" style="float:left;margin-right:5px;width:60px"/>
<a href="#" target="_blank" :href="ownCharacterLink" class="btn" style="margin-right:5px">{{ownCharacter.name}}</a>
<a target="_blank" :href="ownCharacterLink" class="btn" style="margin-right:5px">{{ownCharacter.name}}</a>
<a href="#" @click.prevent="logOut" class="btn"><i class="fas fa-sign-out-alt"></i>{{l('chat.logout')}}</a><br/>
<div>
{{l('chat.status')}}

View File

@ -26,7 +26,7 @@
<span v-show="conversation.channel.id.substr(0, 4) !== 'adh-'" class="fa fa-star" :title="l('channel.official')"
style="margin-right:5px;vertical-align:sub"></span>
<h5 style="margin:0;display:inline;vertical-align:middle">{{conversation.name}}</h5>
<a @click="descriptionExpanded = !descriptionExpanded" class="btn">
<a href="#" @click.prevent="descriptionExpanded = !descriptionExpanded" class="btn">
<span class="fa" :class="{'fa-chevron-down': !descriptionExpanded, 'fa-chevron-up': descriptionExpanded}"></span>
<span class="btn-text">{{l('channel.description')}}</span>
</a>
@ -69,13 +69,13 @@
<a class="btn btn-sm btn-light" style="position:absolute;right:5px;top:50%;transform:translateY(-50%);line-height:0;z-index:10"
@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">
<div class="border-top messages" :class="'messages-' + conversation.mode" ref="messages" @scroll="onMessagesScroll"
style="flex:1;overflow:auto;margin-top:2px;position:relative">
<template v-for="message in messages">
<message-view :message="message" :channel="conversation.channel" :key="message.id"
:classes="message == conversation.lastRead ? 'last-read' : ''">
</message-view>
<span v-if="message.sfc && message.sfc.action == 'report'" :key="message.id">
<span v-if="message.sfc && message.sfc.action == 'report'" :key="'r' + message.id">
<a :href="'https://www.f-list.net/fchat/getLog.php?log=' + message.sfc.logid"
v-if="message.sfc.logid" target="_blank">{{l('events.report.viewLog')}}</a>
<span v-else>{{l('events.report.noLog')}}</span>
@ -174,6 +174,8 @@
keypressHandler!: EventListener;
scrolledDown = true;
scrolledUp = false;
adCountdown = 0;
adsMode = l('channel.mode.ads');
mounted(): void {
this.extraButtons = [{
@ -203,6 +205,21 @@
this.search = this.searchInput;
}, 500);
this.messageView = <HTMLElement>this.$refs['messages'];
this.$watch('conversation.nextAd', (value: number) => {
const setAdCountdown = () => {
const diff = ((<Conversation.ChannelConversation>this.conversation).nextAd - Date.now()) / 1000;
if(diff <= 0) {
if(this.adCountdown !== 0) window.clearInterval(this.adCountdown);
this.adCountdown = 0;
this.adsMode = l('channel.mode.ads');
} else this.adsMode = l('channel.mode.ads.countdown', Math.floor(diff / 60), Math.floor(diff % 60));
};
if(Date.now() < value) {
if(this.adCountdown === 0)
this.adCountdown = window.setInterval(setAdCountdown, 1000);
setAdCountdown();
}
});
}
destroyed(): void {
@ -252,9 +269,13 @@
}
onMessagesScroll(): void {
if(this.messageView.scrollTop < 50 && !this.scrolledUp) {
if(this.messageView.scrollTop < 20) {
if(!this.scrolledUp) {
const firstMessage = this.messageView.firstElementChild;
if(this.conversation.loadMore() && firstMessage !== null)
this.$nextTick(() => setTimeout(() => this.messageView.scrollTop = (<HTMLElement>firstMessage).offsetTop, 0));
}
this.scrolledUp = true;
this.conversation.loadMore();
} else this.scrolledUp = false;
this.scrolledDown = this.messageView.scrollTop + this.messageView.offsetHeight >= this.messageView.scrollHeight - 15;
}
@ -313,7 +334,7 @@
else if(getKey(e) === Keys.Enter) {
if(e.shiftKey === this.settings.enterSend) return;
e.preventDefault();
await this.conversation.send();
setImmediate(async() => this.conversation.send());
}
}
}
@ -335,13 +356,6 @@
}
}
get adsMode(): string | undefined {
if(!Conversation.isChannel(this.conversation)) return;
if(this.conversation.adCountdown <= 0) return l('channel.mode.ads');
else return l('channel.mode.ads.countdown',
Math.floor(this.conversation.adCountdown / 60).toString(), (this.conversation.adCountdown % 60).toString());
}
get characterImage(): string {
return characterImage(this.conversation.name);
}
@ -390,9 +404,9 @@
}
.chat-info-text {
display:flex;
align-items:center;
flex:1 51%;
display: flex;
align-items: center;
flex: 1 51%;
@media (max-width: breakpoint-max(xs)) {
flex-basis: 100%;
}

View File

@ -246,9 +246,11 @@
this.dates[this.dateOffset++]);
this.messages = messages.concat(this.messages);
const noOverflow = list.offsetHeight === list.scrollHeight;
const firstMessage = <HTMLElement>list.firstElementChild!;
this.$nextTick(() => {
if(list.offsetHeight === list.scrollHeight) return this.onMessagesScroll();
else if(noOverflow) list.scrollTop = list.scrollHeight;
else if(noOverflow) setTimeout(() => list.scrollTop = list.scrollHeight, 0);
else setTimeout(() => list.scrollTop = firstMessage.offsetTop, 0);
});
}
}

View File

@ -39,7 +39,7 @@
</div>
<div class="form-group">
<label class="control-label" for="idleTimer">{{l('settings.idleTimer')}}</label>
<input id="idleTimer" class="form-control" type="number" v-model="idleTimer"/>
<input id="idleTimer" class="form-control" type="number" v-model="idleTimer" min="0" max="1440"/>
</div>
<div class="form-group">
<label class="control-label" for="messageSeparators">
@ -160,7 +160,7 @@
alwaysNotify!: boolean;
logMessages!: boolean;
logAds!: boolean;
fontSize!: number;
fontSize!: string;
showNeedsReply!: boolean;
enterSend!: boolean;
colorBookmarks!: boolean;
@ -192,7 +192,7 @@
this.alwaysNotify = settings.alwaysNotify;
this.logMessages = settings.logMessages;
this.logAds = settings.logAds;
this.fontSize = settings.fontSize;
this.fontSize = settings.fontSize.toString();
this.showNeedsReply = settings.showNeedsReply;
this.enterSend = settings.enterSend;
this.colorBookmarks = settings.colorBookmarks;
@ -215,6 +215,8 @@
}
async submit(): Promise<void> {
const idleTimer = parseInt(this.idleTimer, 10);
const fontSize = parseInt(this.fontSize, 10);
core.state.settings = {
playSound: this.playSound,
clickOpensMessage: this.clickOpensMessage,
@ -224,14 +226,14 @@
highlightWords: this.highlightWords.split(',').map((x) => x.trim()).filter((x) => x.length),
showAvatars: this.showAvatars,
animatedEicons: this.animatedEicons,
idleTimer: this.idleTimer.length > 0 ? parseInt(this.idleTimer, 10) : 0,
idleTimer: isNaN(idleTimer) ? 0 : idleTimer < 0 ? 0 : idleTimer > 1440 ? 1440 : idleTimer,
messageSeparators: this.messageSeparators,
eventMessages: this.eventMessages,
joinMessages: this.joinMessages,
alwaysNotify: this.alwaysNotify,
logMessages: this.logMessages,
logAds: this.logAds,
fontSize: isNaN(this.fontSize) ? 14 : this.fontSize < 10 ? 10 : this.fontSize > 24 ? 24 : this.fontSize,
fontSize: isNaN(fontSize) ? 14 : fontSize < 10 ? 10 : fontSize > 24 ? 24 : fontSize,
showNeedsReply: this.showNeedsReply,
enterSend: this.enterSend,
colorBookmarks: this.colorBookmarks,

View File

@ -3,7 +3,6 @@ import {WebSocketConnection} from '../fchat';
export default class Socket implements WebSocketConnection {
static host = 'wss://chat.f-list.net:9799';
private socket: WebSocket;
private errorHandler: ((error: Error) => void) | undefined;
private lastHandler: Promise<void> = Promise.resolve();
constructor() {
@ -16,7 +15,10 @@ export default class Socket implements WebSocketConnection {
onMessage(handler: (message: string) => void): void {
this.socket.addEventListener('message', (e) => {
this.lastHandler = this.lastHandler.then(() => handler(<string>e.data), this.errorHandler);
this.lastHandler = this.lastHandler.then(() => handler(<string>e.data), (err) => {
window.requestAnimationFrame(() => { throw err; });
handler(<string>e.data);
});
});
}
@ -29,7 +31,6 @@ export default class Socket implements WebSocketConnection {
}
onError(handler: (error: Error) => void): void {
this.errorHandler = handler;
this.socket.addEventListener('error', () => handler(new Error()));
}

View File

@ -81,10 +81,11 @@ abstract class Conversation implements Interfaces.Conversation {
this.enteredText = this.lastSent;
}
loadMore(): void {
if(this.messages.length >= this.allMessages.length) return;
loadMore(): boolean {
if(this.messages.length >= this.allMessages.length) return false;
this.maxMessages += 50;
this.messages = this.allMessages.slice(-this.maxMessages);
return true;
}
show(): void {
@ -198,7 +199,7 @@ class ChannelConversation extends Conversation implements Interfaces.ChannelConv
readonly context = CommandContext.Channel;
readonly name = this.channel.name;
isSendingAds = this.channel.mode === 'ads';
adCountdown = 0;
nextAd = 0;
private chat: Interfaces.Message[] = [];
private ads: Interfaces.Message[] = [];
private both: Interfaces.Message[] = [];
@ -284,6 +285,13 @@ class ChannelConversation extends Conversation implements Interfaces.ChannelConv
this.addModeMessage('both', message);
}
clear(): void {
this.messages = [];
this.chat.length = 0;
this.ads.length = 0;
this.both.length = 0;
}
close(): void {
core.connection.send('LCH', {channel: this.channel.id});
}
@ -296,17 +304,13 @@ class ChannelConversation extends Conversation implements Interfaces.ChannelConv
protected async doSend(): Promise<void> {
const isAd = this.isSendingAds;
if(isAd && this.adCountdown > 0) return;
if(isAd && Date.now() < this.nextAd) return;
core.connection.send(isAd ? 'LRP' : 'MSG', {channel: this.channel.id, message: this.enteredText});
await 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 = '';
if(isAd)
this.nextAd = Date.now() + core.connection.vars.lfrp_flood * 1000;
else this.enteredText = '';
}
}

View File

@ -63,7 +63,7 @@ export namespace Conversation {
export interface ChannelConversation extends TabConversation {
readonly channel: Channel
mode: Channel.Mode
readonly adCountdown: number
readonly nextAd: number
isSendingAds: boolean
}
@ -116,7 +116,7 @@ export namespace Conversation {
clear(): void
loadLastSent(): void
show(): void
loadMore(): void
loadMore(): boolean
}
}
@ -181,7 +181,7 @@ export type Settings = Settings.Settings;
export interface Notifications {
isInBackground: boolean
notify(conversation: Conversation, title: string, body: string, icon: string, sound: string): Promise<void>
playSound(sound: string): Promise<void>
playSound(sound: string): void
requestPermission(): Promise<void>
initSounds(sounds: ReadonlyArray<string>): Promise<void>
}

View File

@ -87,6 +87,10 @@ const strings: {[key: string]: string | undefined} = {
'logs.selectCharacter': 'Select a character...',
'logs.selectConversation': 'Select a conversation...',
'logs.allDates': 'Show all',
'logs.corruption.desktop': 'Log corruption has been detected. This is usually caused by a crash/force close or power loss mid-write. Please use the "Fix corrupted logs" option for this character to restore proper functionality.',
'logs.corruption.mobile': 'Log corruption has been detected. This is usually caused by a crash/force close or power loss mid-write. Will now attempt to fix corrupted logs.',
'logs.corruption.mobile.success': 'Your logs have been fixed.',
'logs.corruption.mobile.error': 'Unable to fix corrupted logs. Please clear the application data or reinstall the app.',
'user.profile': 'Profile',
'user.message': 'Open conversation',
'user.messageJump': 'View conversation',
@ -384,6 +388,10 @@ Once this process has started, do not interrupt it or your logs will get corrupt
'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.scop': 'Promote to Super COP',
'commands.scop.help': 'Promotes a character to super channel operator, making them an operator in all public channels.',
'commands.scdeop': 'Demote from Super COP',
'commands.scdeop.help': 'Demotes a character from super channel operator.',
'commands.reloadconfig': 'Reload config',
'commands.reloadconfig.help': 'Reload server-side config from disk.',
'commands.reloadconfig.param0': 'Save?',
@ -412,13 +420,13 @@ Any existing FChat 3.0 data for this character will be overwritten.`,
'importer.error': 'There was an error importing your settings. The defaults will be used.'
};
export default function l(key: string, ...args: string[]): string {
export default function l(key: string, ...args: (string | number)[]): 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]);
str = str.replace(new RegExp(`\\{${i}\\}`, 'igm'), args[i].toString());
return str;
}

View File

@ -13,17 +13,15 @@ export default class Notifications implements Interface {
async notify(conversation: Conversation, title: string, body: string, icon: string, sound: string): Promise<void> {
if(!this.shouldNotify(conversation)) return;
await this.playSound(sound);
this.playSound(sound);
if(core.state.settings.notifications && (<any>Notification).permission === 'granted') { //tslint:disable-line:no-any
const notification = new Notification(title, this.getOptions(conversation, body, icon));
notification.onclick = () => {
conversation.show();
window.focus();
notification.close();
if('close' in notification) notification.close();
};
window.setTimeout(() => {
notification.close();
}, 5000);
if('close' in notification) window.setTimeout(() => notification.close(), 5000);
}
}
@ -36,20 +34,22 @@ export default class Notifications implements Interface {
};
}
async playSound(sound: string): Promise<void> {
playSound(sound: string): void {
if(!core.state.settings.playSound) return;
const audio = <HTMLAudioElement>document.getElementById(`soundplayer-${sound}`);
audio.volume = 1;
audio.muted = false;
return audio.play();
const promise = audio.play();
if(promise instanceof Promise) promise.catch((e) => console.error(e));
}
initSounds(sounds: ReadonlyArray<string>): Promise<void> { //tslint:disable-line:promise-function-async
async initSounds(sounds: ReadonlyArray<string>): Promise<void> {
const promises = [];
for(const sound of sounds) {
const id = `soundplayer-${sound}`;
if(document.getElementById(id) !== null) continue;
const audio = document.createElement('audio');
audio.preload = 'auto';
audio.id = id;
for(const name in codecs) {
const src = document.createElement('source');
@ -63,7 +63,7 @@ export default class Notifications implements Interface {
audio.muted = true;
const promise = audio.play();
if(promise instanceof Promise)
promises.push(promise);
promises.push(promise.catch((e) => console.error(e)));
}
return <any>Promise.all(promises); //tslint:disable-line:no-any
}

View File

@ -269,6 +269,16 @@ const commands: {readonly [key: string]: Command | undefined} = {
context: CommandContext.Channel,
params: [{type: ParamType.Character}]
},
scop: {
exec: (_, character: string) => core.connection.send('SCP', {action: 'add', character}),
permission: Permission.Admin,
params: [{type: ParamType.Character}]
},
scdeop: {
exec: (_, character: string) => core.connection.send('SCP', {action: 'remove', character}),
permission: Permission.Admin,
params: [{type: ParamType.Character}]
},
oplist: {
exec: (conv: ChannelConversation) => core.connection.send('COL', {channel: conv.channel.id}),
context: CommandContext.Channel

View File

@ -1,4 +1,4 @@
import {RavenStatic} from 'raven-js';
import * as Raven from 'raven-js';
import Vue from 'vue';
/*tslint:disable:no-unsafe-any no-any*///hack
@ -13,7 +13,7 @@ function formatComponentName(vm: any): string {
//tslint:enable
/*tslint:disable:no-unbound-method strict-type-predicates*///hack
export default function VueRaven(this: void, raven: RavenStatic): RavenStatic {
function VueRaven(this: void, raven: Raven.RavenStatic): Raven.RavenStatic {
if(typeof Vue.config !== 'object') return raven;
const oldOnError = Vue.config.errorHandler;
Vue.config.errorHandler = (error: Error, vm: Vue, info: string): void => {
@ -44,4 +44,27 @@ export default function VueRaven(this: void, raven: RavenStatic): RavenStatic {
return raven;
}
//tslint:enable
//tslint:enable
export function setupRaven(dsn: string, version: string): void {
Raven.config(dsn, {
release: version,
dataCallback: (data: {culprit?: string, exception?: {values: {stacktrace: {frames: {filename: string}[]}}[]}}) => {
if(data.culprit !== undefined) {
const end = data.culprit.lastIndexOf('?');
data.culprit = `~${data.culprit.substring(data.culprit.lastIndexOf('/'), end === -1 ? undefined : end)}`;
}
if(data.exception !== undefined)
for(const ex of data.exception.values)
for(const frame of ex.stacktrace.frames) {
const index = frame.filename.lastIndexOf('/');
const endIndex = frame.filename.lastIndexOf('?');
frame.filename =
`~${frame.filename.substring(index !== -1 ? index : 0, endIndex === -1 ? undefined : endIndex)}`;
}
}
}).addPlugin(VueRaven, Vue).install();
(<Window & {onunhandledrejection(e: PromiseRejectionEvent): void}>window).onunhandledrejection = (e: PromiseRejectionEvent) => {
Raven.captureException(<Error>e.reason);
};
}

View File

@ -1,8 +1,10 @@
import {getByteLength} from './common';
let crcTable!: number[];
export default class Zip {
private blob: (object | string)[] = [];
private files: {header: object[], offset: number, name: string}[] = [];
private blob: BlobPart[] = [];
private files: {header: BlobPart[], offset: number, name: string}[] = [];
private offset = 0;
constructor() {
@ -19,6 +21,7 @@ export default class Zip {
addFile(name: string, content: string): void {
let crc = -1;
let length = 0;
const nameLength = getByteLength(name);
for(let i = 0, strlen = content.length; i < strlen; ++i) {
let c = content.charCodeAt(i);
if(c > 0xD800 && c < 0xD8FF) //surrogate pairs
@ -35,13 +38,13 @@ export default class Zip {
}
crc = (crc ^ (-1)) >>> 0;
const file = {
header: [Uint16Array.of(0, 0, 0, 0, 0), Uint32Array.of(crc, length, length), Uint16Array.of(name.length, 0)],
header: [Uint16Array.of(0, 0, 0, 0, 0), Uint32Array.of(crc, length, length), Uint16Array.of(nameLength, 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.offset += nameLength + length + 30;
this.files.push(file);
}
@ -51,7 +54,7 @@ export default class Zip {
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.offset += getByteLength(file.name) + 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));

View File

@ -266,4 +266,9 @@
html, body, #page {
height: 100%;
}
*:not([draggable]), *::after, *::before {
-webkit-user-drag: none;
-webkit-app-region: no-drag;
}
</style>

View File

@ -81,6 +81,7 @@
l = l;
hasUpdate = false;
platform = process.platform;
lockTab = false;
mounted(): void {
this.addTab();
@ -193,26 +194,30 @@
}
addTab(): void {
if(this.lockTab) return;
const tray = new electron.remote.Tray(trayIcon);
tray.setToolTip(l('title'));
tray.on('click', (_) => this.trayClicked(tab));
const view = new electron.remote.BrowserView();
view.setAutoResize({width: true, height: true});
view.webContents.loadURL(url.format({
pathname: path.join(__dirname, 'index.html'),
protocol: 'file:',
slashes: true,
query: {settings: JSON.stringify(this.settings)}
}));
electron.ipcRenderer.send('tab-added', view.webContents.id);
const tab = {active: false, view, user: undefined, hasNew: false, tray};
tray.setContextMenu(electron.remote.Menu.buildFromTemplate(this.createTrayMenu(tab)));
this.tabs.push(tab);
this.tabMap[view.webContents.id] = tab;
this.show(tab);
this.lockTab = true;
view.webContents.loadURL(url.format({
pathname: path.join(__dirname, 'index.html'),
protocol: 'file:',
slashes: true,
query: {settings: JSON.stringify(this.settings)}
}));
view.webContents.on('did-stop-loading', () => this.lockTab = false);
}
show(tab: Tab): void {
if(this.lockTab) return;
this.activeTab = tab;
browserWindow.setBrowserView(tab.view);
tab.view.setBounds(getWindowBounds());
@ -313,7 +318,7 @@
#window-tabs {
h4 {
margin: 0 34px 0 77px;
margin: 0 15px 0 77px;
}
.btn, li a {

View File

@ -1,15 +0,0 @@
{
"name": "fchat",
"version": "3.0.6",
"author": "The F-List Team",
"description": "F-List.net Chat Client",
"main": "main.js",
"license": "MIT",
"devDependencies": {
"electron": "^2.0.2"
},
"dependencies": {
"keytar": "^4.2.1",
"spellchecker": "^3.4.4"
}
}

BIN
electron/build/dmg.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 89 KiB

BIN
electron/build/dmg@2x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 267 KiB

View File

@ -32,14 +32,11 @@
import Axios from 'axios';
import {exec, execSync} from 'child_process';
import * as electron from 'electron';
import * as fs from 'fs';
import * as path from 'path';
import * as qs from 'querystring';
import * as Raven from 'raven-js';
import Vue from 'vue';
import {getKey} from '../chat/common';
import l from '../chat/localize';
import VueRaven from '../chat/vue-raven';
import {setupRaven} from '../chat/vue-raven';
import {Keys} from '../keys';
import {GeneralSettings, nativeRequire} from './common';
import * as SlimcatImporter from './importer';
@ -67,21 +64,7 @@ const spellchecker = new sc.Spellchecker();
Axios.defaults.params = { __fchat: `desktop/${electron.remote.app.getVersion()}` };
if(process.env.NODE_ENV === 'production') {
Raven.config('https://a9239b17b0a14f72ba85e8729b9d1612@sentry.f-list.net/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('/'))}`;
if(data.exception !== undefined)
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): void}>window).onunhandledrejection = (e: PromiseRejectionEvent) => {
Raven.captureException(<Error>e.reason);
};
setupRaven('https://a9239b17b0a14f72ba85e8729b9d1612@sentry.f-list.net/2', electron.remote.app.getVersion());
electron.remote.getCurrentWebContents().on('devtools-opened', () => {
console.log(`%c${l('consoleWarning.head')}`, 'background: red; color: yellow; font-size: 30pt');
@ -171,12 +154,7 @@ webContents.on('context-menu', (_, props) => {
const corrections = spellchecker.getCorrectionsForMisspelling(props.misspelledWord);
menuTemplate.unshift({
label: l('spellchecker.add'),
click: () => {
if(customDictionary.indexOf(props.misspelledWord) !== -1) return;
spellchecker.add(props.misspelledWord);
customDictionary.push(props.misspelledWord);
fs.writeFile(customDictionaryPath, JSON.stringify(customDictionary), () => {/**/});
}
click: () => electron.ipcRenderer.send('dictionary-add', props.misspelledWord)
}, {type: 'separator'});
if(corrections.length > 0)
menuTemplate.unshift(...corrections.map((correction: string) => ({
@ -184,14 +162,10 @@ webContents.on('context-menu', (_, props) => {
click: () => webContents.replaceMisspelling(correction)
})));
else menuTemplate.unshift({enabled: false, label: l('spellchecker.noCorrections')});
} else if(customDictionary.indexOf(props.selectionText) !== -1)
} else if(settings.customDictionary.indexOf(props.selectionText) !== -1)
menuTemplate.unshift({
label: l('spellchecker.remove'),
click: () => {
spellchecker.remove(props.selectionText);
customDictionary.splice(customDictionary.indexOf(props.selectionText), 1);
fs.writeFile(customDictionaryPath, JSON.stringify(customDictionary), () => {/**/});
}
click: () => electron.ipcRenderer.send('dictionary-remove', props.selectionText)
}, {type: 'separator'});
if(menuTemplate.length > 0) electron.remote.Menu.buildFromTemplate(menuTemplate).popup({});
@ -201,10 +175,14 @@ let dictDir = path.join(electron.remote.app.getPath('userData'), 'spellchecker')
if(process.platform === 'win32')
exec(`for /d %I in ("${dictDir}") do @echo %~sI`, (_, stdout) => { dictDir = stdout.trim(); });
electron.webFrame.setSpellCheckProvider('', false, {spellCheck: (text) => !spellchecker.isMisspelled(text)});
electron.ipcRenderer.on('settings', async(_: Event, s: GeneralSettings) => spellchecker.setDictionary(s.spellcheckLang, dictDir));
electron.ipcRenderer.on('settings', async(_: Event, s: GeneralSettings) => {
settings = s;
spellchecker.setDictionary(s.spellcheckLang, dictDir);
for(const word of s.customDictionary) spellchecker.add(word);
});
const params = <{[key: string]: string | undefined}>qs.parse(window.location.search.substr(1));
const settings = <GeneralSettings>JSON.parse(params['settings']!);
let settings = <GeneralSettings>JSON.parse(params['settings']!);
if(params['import'] !== undefined)
try {
if(SlimcatImporter.canImportGeneral() && confirm(l('importer.importGeneral'))) {
@ -214,11 +192,6 @@ if(params['import'] !== undefined)
} catch {
alert(l('importer.error'));
}
spellchecker.setDictionary(settings.spellcheckLang, dictDir);
const customDictionaryPath = path.join(settings.logDirectory, 'words');
const customDictionary = fs.existsSync(customDictionaryPath) ? <string[]>JSON.parse(fs.readFileSync(customDictionaryPath, 'utf8')) : [];
for(const word of customDictionary) spellchecker.add(word);
//tslint:disable-next-line:no-unused-expression
new Index({

View File

@ -14,6 +14,7 @@ export class GeneralSettings {
theme = 'default';
version = electron.app.getVersion();
beta = false;
customDictionary: string[] = [];
}
export function mkdir(dir: string): void {

View File

@ -40,7 +40,7 @@ export async function ensureDictionary(lang: string): Promise<void> {
const filePath = path.join(dictDir, `${lang}.${type}`);
const downloaded = downloadedDictionaries[file.name];
if(downloaded === undefined || downloaded.hash !== file.hash || !fs.existsSync(filePath)) {
await writeFile(filePath, new Buffer((await Axios.get<string>(`${downloadUrl}${file.name}`, requestConfig)).data));
await writeFile(filePath, Buffer.from((await Axios.get<string>(`${downloadUrl}${file.name}`, requestConfig)).data));
downloadedDictionaries[file.name] = file;
await writeFile(downloadedPath, JSON.stringify(downloadedDictionaries));
}

View File

@ -1,6 +1,7 @@
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 {Character, Conversation, Logs as Logging, Settings} from '../chat/interfaces';
@ -14,7 +15,7 @@ declare module '../chat/interfaces' {
}
const dayMs = 86400000;
const read = promisify(fs.read);
const noAssert = process.env.NODE_ENV === 'production';
function writeFile(p: fs.PathLike | number, data: string | object | number,
@ -110,43 +111,51 @@ export function fixLogs(character: string): void {
const dir = getLogDir(character);
const files = fs.readdirSync(dir);
const buffer = Buffer.allocUnsafe(50100);
for(const file of files)
if(file.substr(-4) !== '.idx') {
const fd = fs.openSync(path.join(dir, file), 'r+');
const indexFd = fs.openSync(path.join(dir, `${file}.idx`), 'r+');
fs.readSync(indexFd, buffer, 0, 1, 0);
let pos = 0, lastDay = 0;
const nameEnd = buffer.readUInt8(0, noAssert) + 1;
fs.readSync(indexFd, buffer, 0, nameEnd, null); //tslint:disable-line:no-null-keyword
buffer.toString('utf8', 1, nameEnd);
fs.ftruncateSync(indexFd, nameEnd);
const size = (fs.fstatSync(fd)).size;
try {
while(pos < size) {
buffer.fill(-1);
fs.readSync(fd, buffer, 0, 50100, pos);
const deserialized = deserializeMessage(buffer, 0, (name) => ({
gender: 'None', status: 'online', statusText: '', isFriend: false, isBookmarked: false, isChatOp: false,
isIgnored: false, name
}), false);
const time = deserialized.message.time;
const day = Math.floor(time.getTime() / dayMs - time.getTimezoneOffset() / 1440);
if(day > lastDay) {
buffer.writeUInt16LE(day, 0, noAssert);
buffer.writeUIntLE(pos, 2, 5, noAssert);
fs.writeSync(indexFd, buffer, 0, 7);
lastDay = day;
}
if(buffer.readUInt16LE(deserialized.size - 2) !== deserialized.size - 2) throw new Error();
pos += deserialized.size;
}
} catch {
fs.ftruncateSync(fd, pos);
} finally {
fs.closeSync(fd);
fs.closeSync(indexFd);
}
for(const file of files) {
const full = path.join(dir, file);
if(file.substr(-4) === '.idx') {
if(!fs.existsSync(full.slice(0, -4))) fs.unlinkSync(full);
continue;
}
const fd = fs.openSync(full, 'r+');
const indexPath = path.join(dir, `${file}.idx`);
if(!fs.existsSync(indexPath)) {
fs.unlinkSync(full);
continue;
}
const indexFd = fs.openSync(indexPath, 'r+');
fs.readSync(indexFd, buffer, 0, 1, 0);
let pos = 0, lastDay = 0;
const nameEnd = buffer.readUInt8(0, noAssert) + 1;
fs.ftruncateSync(indexFd, nameEnd);
fs.readSync(indexFd, buffer, 0, nameEnd, null); //tslint:disable-line:no-null-keyword
const size = (fs.fstatSync(fd)).size;
try {
while(pos < size) {
buffer.fill(-1);
fs.readSync(fd, buffer, 0, 50100, pos);
const deserialized = deserializeMessage(buffer, 0, (name) => ({
gender: 'None', status: 'online', statusText: '', isFriend: false, isBookmarked: false, isChatOp: false,
isIgnored: false, name
}), false);
const time = deserialized.message.time;
const day = Math.floor(time.getTime() / dayMs - time.getTimezoneOffset() / 1440);
if(day > lastDay) {
buffer.writeUInt16LE(day, 0, noAssert);
buffer.writeUIntLE(pos, 2, 5, noAssert);
fs.writeSync(indexFd, buffer, 0, 7);
lastDay = day;
}
if(buffer.readUInt16LE(deserialized.size - 2) !== deserialized.size - 2) throw new Error();
pos += deserialized.size;
}
} catch {
fs.ftruncateSync(fd, pos);
} finally {
fs.closeSync(fd);
fs.closeSync(indexFd);
}
}
}
function loadIndex(name: string): Index {
@ -155,19 +164,23 @@ function loadIndex(name: string): Index {
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, noAssert));
try {
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, noAssert));
}
index[file.slice(0, -4).toLowerCase()] = item;
} catch {
alert(l('logs.corruption.desktop'));
}
index[file.slice(0, -4).toLowerCase()] = item;
}
return index;
}
@ -190,18 +203,24 @@ export class Logs implements Logging {
let count = 20;
let messages = new Array<Conversation.Message>(count);
const fd = fs.openSync(file, 'r');
let pos = fs.fstatSync(fd).size;
const buffer = Buffer.allocUnsafe(65536);
while(pos > 0 && count > 0) {
fs.readSync(fd, buffer, 0, 2, pos - 2);
const length = buffer.readUInt16LE(0);
pos = pos - length - 2;
fs.readSync(fd, buffer, 0, length, pos);
messages[--count] = deserializeMessage(buffer).message;
try {
let pos = fs.fstatSync(fd).size;
const buffer = Buffer.allocUnsafe(65536);
while(pos > 0 && count > 0) {
fs.readSync(fd, buffer, 0, 2, pos - 2);
const length = buffer.readUInt16LE(0);
pos = pos - length - 2;
fs.readSync(fd, buffer, 0, length, pos);
messages[--count] = deserializeMessage(buffer).message;
}
if(count !== 0) messages = messages.slice(count);
return messages;
} catch {
alert(l('logs.corruption.desktop'));
return [];
} finally {
fs.closeSync(fd);
}
if(count !== 0) messages = messages.slice(count);
fs.closeSync(fd);
return messages;
}
private getIndex(name: string): Index {
@ -229,18 +248,25 @@ export class Logs implements Logging {
const messages: Conversation.Message[] = [];
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;
try {
const end = dateOffset + 1 < index.offsets.length ? index.offsets[dateOffset + 1] : (fs.fstatSync(fd)).size;
const length = end - pos;
const buffer = Buffer.allocUnsafe(length);
await read(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;
} catch {
alert(l('logs.corruption.desktop'));
return [];
} finally {
fs.closeSync(fd);
}
return messages;
}
logMessage(conversation: {key: string, name: string}, message: Message): void {
@ -262,6 +288,7 @@ export class Logs implements Logging {
async getAvailableCharacters(): Promise<ReadonlyArray<string>> {
const baseDir = core.state.generalSettings!.logDirectory;
mkdir(baseDir);
return (fs.readdirSync(baseDir)).filter((x) => fs.lstatSync(path.join(baseDir, x)).isDirectory());
}
}

View File

@ -31,7 +31,6 @@
*/
import * as electron from 'electron';
import log from 'electron-log'; //tslint:disable-line:match-default-export-name
import {autoUpdater} from 'electron-updater';
import * as fs from 'fs';
import * as path from 'path';
import * as url from 'url';
@ -53,12 +52,12 @@ let tabCount = 0;
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.');
let shouldImportSettings = false;
const settingsDir = path.join(baseDir, 'data');
mkdir(settingsDir);
const settingsFile = path.join(settingsDir, 'settings');
const settings = new GeneralSettings();
async function setDictionary(lang: string | undefined): Promise<void> {
if(lang !== undefined) await ensureDictionary(lang);
@ -66,19 +65,6 @@ async function setDictionary(lang: string | undefined): Promise<void> {
setGeneralSettings(settings);
}
const settingsDir = path.join(electron.app.getPath('userData'), 'data');
mkdir(settingsDir);
const settingsFile = path.join(settingsDir, 'settings');
const settings = new GeneralSettings();
let shouldImportSettings = false;
if(!fs.existsSync(settingsFile)) shouldImportSettings = true;
else
try {
Object.assign(settings, <GeneralSettings>JSON.parse(fs.readFileSync(settingsFile, 'utf8')));
} catch(e) {
log.error(`Error loading settings: ${e}`);
}
function setGeneralSettings(value: GeneralSettings): void {
fs.writeFileSync(path.join(settingsDir, 'settings'), JSON.stringify(value));
for(const w of electron.webContents.getAllWebContents()) w.send('settings', settings);
@ -150,7 +136,21 @@ function showPatchNotes(): void {
}
function onReady(): void {
app.setAppUserModelId('net.f-list.f-chat');
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.');
if(!fs.existsSync(settingsFile)) shouldImportSettings = true;
else
try {
Object.assign(settings, <GeneralSettings>JSON.parse(fs.readFileSync(settingsFile, 'utf8')));
} catch(e) {
log.error(`Error loading settings: ${e}`);
}
app.setAppUserModelId('com.squirrel.fchat.F-Chat');
app.on('open-file', createWindow);
if(settings.version !== app.getVersion()) {
@ -159,11 +159,12 @@ function onReady(): void {
setGeneralSettings(settings);
}
const updaterUrl = `https://client.f-list.net/${process.platform}`;
if(process.env.NODE_ENV === 'production') {
autoUpdater.channel = settings.beta ? 'beta' : 'latest';
autoUpdater.checkForUpdates(); //tslint:disable-line:no-floating-promises
const updateTimer = setInterval(async() => autoUpdater.checkForUpdates(), 3600000);
autoUpdater.on('update-downloaded', () => {
electron.autoUpdater.setFeedURL({url: updaterUrl + (settings.beta ? '?channel=beta' : ''), serverType: 'json'});
setTimeout(() => electron.autoUpdater.checkForUpdates(), 10000);
const updateTimer = setInterval(() => electron.autoUpdater.checkForUpdates(), 3600000);
electron.autoUpdater.on('update-downloaded', () => {
clearInterval(updateTimer);
const menu = electron.Menu.getApplicationMenu()!;
const item = menu.getMenuItemById('update') as MenuItem | null;
@ -175,7 +176,7 @@ function onReady(): void {
label: l('action.update'),
click: () => {
for(const w of windows) w.webContents.send('quit');
autoUpdater.quitAndInstall(false, true);
electron.autoUpdater.quitAndInstall();
}
}, {
label: l('help.changelog'),
@ -186,12 +187,12 @@ function onReady(): void {
electron.Menu.setApplicationMenu(menu);
for(const w of windows) w.webContents.send('update-available', true);
});
autoUpdater.on('update-not-available', () => {
(<any>autoUpdater).downloadedUpdateHelper.clear(); //tslint:disable-line:no-any no-unsafe-any
electron.autoUpdater.on('update-not-available', () => {
for(const w of windows) w.webContents.send('update-available', false);
const item = electron.Menu.getApplicationMenu()!.getMenuItemById('update') as MenuItem | null;
if(item !== null) item.visible = false;
});
electron.autoUpdater.on('error', (e) => log.error(e));
}
const viewItem = {
@ -275,8 +276,8 @@ function onReady(): void {
click: async(item: Electron.MenuItem) => {
settings.beta = item.checked;
setGeneralSettings(settings);
autoUpdater.channel = item.checked ? 'beta' : 'latest';
return autoUpdater.checkForUpdates();
electron.autoUpdater.setFeedURL({url: updaterUrl + (item.checked ? '?channel=beta' : ''), serverType: 'json'});
return electron.autoUpdater.checkForUpdates();
}
}, {
label: l('fixLogs.action'),
@ -360,6 +361,15 @@ function onReady(): void {
else characters.push(character);
e.returnValue = true;
});
electron.ipcMain.on('dictionary-add', (_: Event, word: string) => {
if(settings.customDictionary.indexOf(word) !== -1) return;
settings.customDictionary.push(word);
setGeneralSettings(settings);
});
electron.ipcMain.on('dictionary-remove', (_: Event, word: string) => {
settings.customDictionary.splice(settings.customDictionary.indexOf(word), 1);
setGeneralSettings(settings);
});
electron.ipcMain.on('disconnect', (_: Event, character: string) => characters.splice(characters.indexOf(character), 1));
const emptyBadge = electron.nativeImage.createEmpty();
//tslint:disable-next-line:no-require-imports
@ -372,7 +382,7 @@ function onReady(): void {
createWindow();
}
const running = process.env.NODE_ENV === 'production' && app.makeSingleInstance(createWindow);
if(running) app.quit();
const isSquirrelStart = require('electron-squirrel-startup'); //tslint:disable-line:no-require-imports
if(isSquirrelStart || process.env.NODE_ENV === 'production' && app.makeSingleInstance(createWindow)) app.quit();
else app.on('ready', onReady);
app.on('window-all-closed', () => app.quit());

View File

@ -9,7 +9,7 @@ const browserWindow = remote.getCurrentWindow();
export default class Notifications extends BaseNotifications {
async notify(conversation: Conversation, title: string, body: string, icon: string, sound: string): Promise<void> {
if(!this.shouldNotify(conversation)) return;
await this.playSound(sound);
this.playSound(sound);
browserWindow.flashFrame(true);
if(core.state.settings.notifications) {
const notification = new Notification(title, this.getOptions(conversation, body, icon));

122
electron/pack.js Normal file
View File

@ -0,0 +1,122 @@
const path = require('path');
const pkg = require(path.join(__dirname, 'package.json'));
const fs = require('fs');
const child_process = require('child_process');
function mkdir(dir) {
try {
fs.mkdirSync(dir);
} catch(e) {
if(!(e instanceof Error)) throw e;
switch(e.code) {
case 'ENOENT':
const dirname = path.dirname(dir);
if(dirname === dir) throw e;
mkdir(dirname);
mkdir(dir);
break;
default:
try {
const stat = fs.statSync(dir);
if(stat.isDirectory()) return;
} catch(e) {
console.log(e);
}
throw e;
}
}
}
const distDir = path.join(__dirname, 'dist');
const isBeta = pkg.version.indexOf('beta') !== -1;
const spellcheckerPath = 'node_modules/spellchecker/build/Release/spellchecker.node',
keytarPath = 'node_modules/keytar/build/Release/keytar.node';
mkdir(path.dirname(path.join(__dirname, 'app', spellcheckerPath)));
mkdir(path.dirname(path.join(__dirname, 'app', keytarPath)));
fs.copyFileSync(spellcheckerPath, path.join(__dirname, 'app', spellcheckerPath));
fs.copyFileSync(keytarPath, path.join(__dirname, 'app', keytarPath));
require('electron-packager')({
dir: path.join(__dirname, 'app'),
out: distDir,
overwrite: true,
name: 'F-Chat',
icon: path.join(__dirname, 'build', 'icon'),
ignore: ['\.map$'],
osxSign: process.argv.length > 2 ? {identity: process.argv[2]} : false,
prune: false
}).then((appPaths) => {
if(process.platform === 'win32') {
console.log('Creating Windows installer');
const icon = path.join(__dirname, 'build', 'icon.ico');
const setupName = `F-Chat Setup.exe`;
if(fs.existsSync(path.join(distDir, setupName))) fs.unlinkSync(path.join(distDir, setupName));
const nupkgName = path.join(distDir, `fchat-${pkg.version}-full.nupkg`);
const deltaName = path.join(distDir, `fchat-${pkg.version}-delta.nupkg`);
if(fs.existsSync(nupkgName)) fs.unlinkSync(nupkgName);
if(fs.existsSync(deltaName)) fs.unlinkSync(deltaName);
if(process.argv.length <= 3) console.warn('Warning: Creating unsigned installer');
require('electron-winstaller').createWindowsInstaller({
appDirectory: appPaths[0],
outputDirectory: distDir,
iconUrl: icon,
setupIcon: icon,
noMsi: true,
exe: 'F-Chat.exe',
title: 'F-Chat',
setupExe: setupName,
remoteReleases: 'https://client.f-list.net/win32/' + (isBeta ? '?channel=beta' : ''),
signWithParams: process.argv.length > 3 ? `/a /f ${process.argv[2]} /p ${process.argv[3]} /fd sha256 /tr http://timestamp.digicert.com /td sha256` : undefined
}).catch((e) => console.log(`Error while creating installer: ${e.message}`));
} else if(process.platform === 'darwin') {
console.log('Creating Mac DMG');
const target = path.join(distDir, `F-Chat.dmg`);
if(fs.existsSync(target)) fs.unlinkSync(target);
const appPath = path.join(appPaths[0], 'F-Chat.app');
if(process.argv.length <= 2) console.warn('Warning: Creating unsigned DMG');
require('appdmg')({
basepath: appPaths[0],
target,
specification: {
title: 'F-Chat',
icon: path.join(__dirname, 'build', 'icon.png'),
background: path.join(__dirname, 'build', 'dmg.png'),
contents: [{x: 555, y: 345, type: 'link', path: '/Applications'}, {x: 555, y: 105, type: 'file', path: appPath}],
'code-sign': process.argv.length > 2 ? {
'signing-identity': process.argv[2]
} : undefined
}
}).on('error', console.error);
const zipName = `F-Chat_${pkg.version}.zip`;
const zipPath = path.join(distDir, zipName);
if(fs.existsSync(zipPath)) fs.unlinkSync(zipPath);
const child = child_process.spawn('zip', ['-r', '-y', '-9', zipPath, 'F-Chat.app'], {cwd: appPaths[0]});
child.stdout.on('data', () => {});
child.stderr.on('data', (data) => console.error(data.toString()));
fs.writeFileSync(path.join(distDir, 'updates.json'), JSON.stringify({
releases: [{version: pkg.version, updateTo: {url: 'https://client.f-list.net/darwin/' + zipName}}],
currentRelease: pkg.version
}));
} else {
console.log('Creating Linux AppImage');
fs.renameSync(path.join(appPaths[0], 'F-Chat'), path.join(appPaths[0], 'AppRun'));
fs.copyFileSync(path.join(__dirname, 'build', 'icon.png'), path.join(appPaths[0], 'icon.png'));
fs.symlinkSync(path.join(appPaths[0], 'icon.png'), path.join(appPaths[0], '.DirIcon'));
fs.writeFileSync(path.join(appPaths[0], 'fchat.desktop'), '[Desktop Entry]\nName=F-Chat\nExec=AppRun\nIcon=icon\nType=Application\nCategories=GTK;GNOME;Utility;');
require('axios').get('https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-x86_64.AppImage', {responseType: 'stream'}).then((res) => {
const downloaded = path.join(distDir, 'appimagetool.AppImage');
res.data.pipe(fs.createWriteStream(downloaded));
res.data.on('end', () => {
const args = [appPaths[0], 'fchat.AppImage', '-u', 'zsync|https://client.f-list.net/fchat.AppImage.zsync'];
if(process.argv.length > 2) args.push('-s', '--sign-key', process.argv[2]);
else console.warn('Warning: Creating unsigned AppImage');
if(process.argv.length > 3) args.push('--sign-args', `--passphrase=${process.argv[3]}`);
child_process.spawn(downloaded, ['--appimage-extract'], {cwd: distDir}).on('close', () => {
const child = child_process.spawn(path.join(distDir, 'squashfs-root', 'AppRun'), args, {cwd: distDir});
child.stdout.on('data', (data) => console.log(data.toString()));
child.stderr.on('data', (data) => console.error(data.toString()));
});
});
}, (e) => console.error(`HTTP error: ${e.message}`));
}
}, (e) => console.log(`Error while packaging: ${e.message}`));

View File

@ -1,38 +1,16 @@
{
"name": "fchat",
"version": "3.0.0",
"author": "The F-List Team",
"description": "F-List.net Chat Client",
"main": "main.js",
"license": "MIT",
"scripts": {
"build": "node ../webpack development",
"build:dist": "node ../webpack production",
"watch": "node ../webpack watch",
"start": "../node_modules/.bin/electron app"
},
"build": {
"appId": "net.f-list.f-chat",
"productName": "F-Chat",
"files": [
"*",
"sounds",
"themes",
"!**/*.map",
"!node_modules/",
"node_modules/**/*.node"
],
"asar": false,
"nsis": {
"oneClick": false,
"allowToChangeInstallationDirectory": true
},
"linux": {
"category": "Network"
},
"publish": {
"provider": "generic",
"url": "https://client.f-list.net/"
}
}
"name": "fchat",
"version": "3.0.7",
"author": "The F-List Team",
"description": "F-List.net Chat Client",
"main": "main.js",
"id": "fchat",
"license": "MIT",
"scripts": {
"build": "node ../webpack development",
"build:dist": "node ../webpack production",
"watch": "node ../webpack watch",
"start": "../node_modules/.bin/electron app",
"pack": "node ./pack"
}
}

View File

@ -6,7 +6,7 @@ const OptimizeCssAssetsPlugin = require('optimize-css-assets-webpack-plugin');
const VueLoaderPlugin = require('vue-loader/lib/plugin');
const mainConfig = {
entry: [path.join(__dirname, 'main.ts'), path.join(__dirname, 'application.json')],
entry: [path.join(__dirname, 'main.ts'), path.join(__dirname, 'package.json')],
output: {
path: __dirname + '/app',
filename: 'main.js'
@ -23,7 +23,7 @@ const mainConfig = {
transpileOnly: true
}
},
{test: path.join(__dirname, 'application.json'), loader: 'file-loader?name=package.json', type: 'javascript/auto'},
{test: path.join(__dirname, 'package.json'), loader: 'file-loader?name=package.json', type: 'javascript/auto'},
{test: /\.(png|html)$/, loader: 'file-loader?name=[name].[ext]'}
]
},
@ -45,7 +45,7 @@ const mainConfig = {
}, rendererConfig = {
entry: {
chat: [path.join(__dirname, 'chat.ts'), path.join(__dirname, 'index.html')],
window: [path.join(__dirname, 'window.ts'), path.join(__dirname, 'window.html')]
window: [path.join(__dirname, 'window.ts'), path.join(__dirname, 'window.html'), path.join(__dirname, 'build', 'tray@2x.png')]
},
output: {
path: __dirname + '/app',

View File

@ -42,6 +42,7 @@ export namespace Connection {
RLL: {channel: string, dice: 'bottle' | string} | {recipient: string, dice: 'bottle' | string},
RMO: {channel: string, mode: Channel.Mode},
RST: {channel: string, status: 'public' | 'private'},
SCP: {action: 'add' | 'remove', character: string}
RWD: {character: string},
SFC: {action: 'report', report: string, tab?: string, logid: number} | {action: 'confirm', callid: number},
STA: {status: Character.Status, statusmsg: string},

View File

@ -8,12 +8,12 @@ android {
applicationId "net.f_list.fchat"
minSdkVersion 19
targetSdkVersion 27
versionCode 17
versionName "3.0.6"
versionCode 18
versionName "3.0.7"
}
buildTypes {
release {
minifyEnabled false
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}

View File

@ -1,19 +1,21 @@
package net.f_list.fchat
import android.content.Context
import android.util.SparseArray
import android.webkit.JavascriptInterface
import org.json.JSONArray
import org.json.JSONStringer
import java.io.File
import java.io.FileInputStream
import java.io.FileOutputStream
import java.io.RandomAccessFile
import java.nio.ByteBuffer
import java.nio.ByteOrder
import java.nio.CharBuffer
import java.util.*
class Logs(private val ctx: Context) {
data class IndexItem(val name: String, val index: MutableMap<Int, Long> = HashMap(), val dates: MutableList<Int> = LinkedList())
data class IndexItem(val name: String, val index: MutableMap<Int, Int> = LinkedHashMap(), val offsets: MutableList<Long> = ArrayList())
private var index: MutableMap<String, IndexItem>? = null
private var loadedIndex: MutableMap<String, IndexItem>? = null
@ -42,8 +44,8 @@ class Logs(private val ctx: Context) {
buffer.limit(read)
while(buffer.position() < buffer.limit()) {
val key = buffer.short.toInt()
indexItem.index[key] = buffer.int.toLong() or (buffer.get().toLong() shl 32)
indexItem.dates.add(key)
indexItem.index[key] = indexItem.offsets.size
indexItem.offsets.add(buffer.int.toLong() or (buffer.get().toLong() shl 32))
}
index[file.nameWithoutExtension] = indexItem
}
@ -60,7 +62,7 @@ class Logs(private val ctx: Context) {
loadedIndex = index
val json = JSONStringer().`object`()
for(item in index!!)
json.key(item.key).`object`().key("name").value(item.value.name).key("dates").value(JSONArray(item.value.dates)).endObject()
json.key(item.key).`object`().key("name").value(item.value.name).key("dates").value(JSONArray(item.value.index.keys)).endObject()
return json.endObject().toString()
}
@ -70,7 +72,7 @@ class Logs(private val ctx: Context) {
val file = File(baseDir, key)
buffer.clear()
if(!index!!.containsKey(key)) {
index!![key] = IndexItem(conversation, HashMap())
index!![key] = IndexItem(conversation)
buffer.position(1)
encoder.encode(CharBuffer.wrap(conversation), buffer, true)
buffer.put(0, (buffer.position() - 1).toByte())
@ -79,10 +81,9 @@ class Logs(private val ctx: Context) {
if(!item.index.containsKey(day)) {
buffer.putShort(day.toShort())
val size = file.length()
item.index[day] = size
item.dates.add(day)
buffer.putInt((size and 0xffffffffL).toInt())
buffer.put((size shr 32).toByte())
item.index[day] = item.offsets.size
item.offsets.add(size)
buffer.putInt((size and 0xffffffffL).toInt()).put((size shr 32).toByte())
FileOutputStream(File(baseDir, "$key.idx"), true).use { file ->
buffer.flip()
file.channel.write(buffer)
@ -141,20 +142,22 @@ class Logs(private val ctx: Context) {
@JavascriptInterface
fun getLogsN(character: String, key: String, date: Int): String {
val offset = loadedIndex!![key]?.index?.get(date) ?: return "[]"
val indexItem = loadedIndex!![key] ?: return "[]"
val dateKey = indexItem.index[date] ?: return "[]"
val json = JSONStringer()
json.array()
FileInputStream(File(ctx.filesDir, "$character/logs/$key")).use { stream ->
val channel = stream.channel
channel.position(offset)
while(channel.position() < channel.size()) {
buffer.clear()
val oldPosition = channel.position()
channel.read(buffer)
buffer.rewind()
deserializeMessage(buffer, json, date)
if(buffer.position() == 0) break
channel.position(oldPosition + buffer.position() + 2)
val start = indexItem.offsets[dateKey]
val end = if(dateKey >= indexItem.offsets.size - 1) channel.size() else indexItem.offsets[dateKey + 1]
channel.position(start)
val buffer = ByteBuffer.allocateDirect((end - start).toInt()).order(ByteOrder.LITTLE_ENDIAN)
channel.read(buffer)
buffer.rewind()
while(buffer.position() < buffer.limit()) {
deserializeMessage(buffer, json)
buffer.limit(buffer.capacity())
buffer.position(buffer.position() + 2)
}
}
return json.endArray().toString()
@ -165,7 +168,7 @@ class Logs(private val ctx: Context) {
loadedIndex = if(character == this.character) this.index else this.loadIndex(character)
val json = JSONStringer().`object`()
for(item in loadedIndex!!)
json.key(item.key).`object`().key("name").value(item.value.name).key("dates").value(JSONArray(item.value.dates)).endObject()
json.key(item.key).`object`().key("name").value(item.value.name).key("dates").value(JSONArray(item.value.index.keys)).endObject()
return json.endObject().toString()
}
@ -174,22 +177,83 @@ class Logs(private val ctx: Context) {
return JSONArray(ctx.filesDir.listFiles().filter { it.isDirectory }.map { it.name }).toString()
}
private fun deserializeMessage(buffer: ByteBuffer, json: JSONStringer, checkDate: Int = -1) {
val date = buffer.int
if(checkDate != -1 && date / 86400 != checkDate) return
@JavascriptInterface
fun repair() {
val files = baseDir.listFiles()
val indexBuffer = ByteBuffer.allocateDirect(7).order(ByteOrder.LITTLE_ENDIAN)
for(entry in files) {
if(entry.name.endsWith(".idx")) continue
RandomAccessFile("$entry.idx", "rw").use { idx ->
buffer.clear()
buffer.limit(1)
idx.channel.read(buffer)
idx.channel.truncate((buffer.get(0) + 1).toLong())
idx.channel.position(idx.channel.size())
RandomAccessFile(entry, "rw").use { file ->
var lastDay = 0
val size = file.channel.size()
var pos = 0L
try {
while(file.channel.position() < size) {
buffer.clear()
pos = file.channel.position()
val read = file.channel.read(buffer)
var success = false
buffer.flip()
while(buffer.remaining() > 10) {
val offset = buffer.position()
val day = buffer.int / 86400
buffer.get()
val senderLength = buffer.get()
if(buffer.remaining() < senderLength + 4) break
buffer.limit(buffer.position() + senderLength)
decoder.decode(buffer)
buffer.limit(read)
val textLength = buffer.short.toInt()
if(buffer.remaining() < textLength + 2) break
buffer.limit(buffer.position() + textLength)
decoder.decode(buffer)
buffer.limit(read)
val messageSize = buffer.position() - offset
if(messageSize != buffer.short.toInt()) throw Exception()
if(day > lastDay) {
lastDay = day
indexBuffer.position(0)
indexBuffer.putShort(day.toShort())
indexBuffer.putInt((pos and 0xffffffffL).toInt()).put((pos shr 32).toByte())
indexBuffer.position(0)
idx.channel.write(indexBuffer)
}
pos += messageSize + 2
success = true
}
if(!success) throw Exception()
file.channel.position(pos)
}
} catch(e: Exception) {
file.channel.truncate(pos)
}
}
}
}
}
private fun deserializeMessage(buffer: ByteBuffer, json: JSONStringer) {
val start = buffer.position()
json.`object`()
json.key("time")
json.value(date)
json.value(buffer.int)
json.key("type")
json.value(buffer.get())
json.key("sender")
val senderLength = buffer.get()
buffer.limit(6 + senderLength)
buffer.limit(start + 6 + senderLength)
json.value(decoder.decode(buffer))
buffer.limit(buffer.capacity())
val textLength = buffer.short.toInt() and 0xffff
json.key("text")
buffer.limit(8 + senderLength + textLength)
buffer.limit(start + 8 + senderLength + textLength)
json.value(decoder.decode(buffer))
json.endObject()
}

View File

@ -30,9 +30,7 @@
* @see {@link https://github.com/f-list/exported|GitHub repo}
*/
import Axios from 'axios';
import * as Raven from 'raven-js';
import Vue from 'vue';
import VueRaven from '../chat/vue-raven';
import {setupRaven} from '../chat/vue-raven';
import Index from './Index.vue';
const version = (<{version: string}>require('./package.json')).version; //tslint:disable-line:no-require-imports
@ -40,23 +38,8 @@ const version = (<{version: string}>require('./package.json')).version; //tslint
Axios.defaults.params = { __fchat: `mobile-${platform}/${version}` };
};
if(process.env.NODE_ENV === 'production') {
Raven.config('https://a9239b17b0a14f72ba85e8729b9d1612@sentry.f-list.net/2', {
release: `mobile-${version}`,
dataCallback: (data: {culprit: string, exception?: {values: {stacktrace: {frames: {filename: string}[]}}[]}}) => {
data.culprit = `~${data.culprit.substr(data.culprit.lastIndexOf('/'))}`;
if(data.exception !== undefined)
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): void}>window).onunhandledrejection = (e: PromiseRejectionEvent) => {
Raven.captureException(<Error>e.reason);
};
}
if(process.env.NODE_ENV === 'production')
setupRaven('https://a9239b17b0a14f72ba85e8729b9d1612@sentry.f-list.net/2', `mobile-${version}`);
new Index({ //tslint:disable-line:no-unused-expression
el: '#app'

View File

@ -1,6 +1,7 @@
import {Message as MessageImpl} from '../chat/common';
import core from '../chat/core';
import {Conversation, Logs as Logging, Settings} from '../chat/interfaces';
import l from '../chat/localize';
declare global {
const NativeFile: {
@ -20,6 +21,7 @@ declare global {
message: string): Promise<void>;
getBacklog(key: string): Promise<ReadonlyArray<NativeMessage>>;
getLogs(character: string, key: string, date: number): Promise<ReadonlyArray<NativeMessage>>
repair(character: string): Promise<void>
};
}
@ -41,10 +43,16 @@ export class Logs implements Logging {
private index: Index = {};
private loadedIndex?: Index;
private loadedCharacter?: string;
attemptedFix = false;
constructor() {
core.connection.onEvent('connecting', async() => {
this.index = await NativeLogs.init(core.connection.character);
this.attemptedFix = false;
try {
this.index = await NativeLogs.init(core.connection.character);
} catch {
await this.fixLogs(core.connection.character);
}
});
}
@ -59,20 +67,35 @@ export class Logs implements Logging {
}
async getBacklog(conversation: Conversation): Promise<ReadonlyArray<Conversation.Message>> {
return (await NativeLogs.getBacklog(conversation.key))
.map((x) => new MessageImpl(x.type, core.characters.get(x.sender), x.text, new Date(x.time * 1000)));
try {
return (await NativeLogs.getBacklog(conversation.key))
.map((x) => new MessageImpl(x.type, core.characters.get(x.sender), x.text, new Date(x.time * 1000)));
} catch {
await this.fixLogs(this.loadedCharacter!);
return [];
}
}
private async getIndex(name: string): Promise<Index> {
if(this.loadedCharacter === name) return this.loadedIndex!;
this.loadedCharacter = name;
return this.loadedIndex = name === core.connection.character ? this.index : await NativeLogs.loadIndex(name);
try {
return this.loadedIndex = name === core.connection.character ? this.index : await NativeLogs.loadIndex(name);
} catch {
await this.fixLogs(name);
return {};
}
}
async getLogs(character: string, key: string, date: Date): Promise<ReadonlyArray<Conversation.Message>> {
await NativeLogs.loadIndex(character);
return (await NativeLogs.getLogs(character, key, Math.floor(date.getTime() / dayMs - date.getTimezoneOffset() / 1440)))
.map((x) => new MessageImpl(x.type, core.characters.get(x.sender), x.text, new Date(x.time * 1000)));
try {
await NativeLogs.loadIndex(character);
return (await NativeLogs.getLogs(character, key, Math.floor(date.getTime() / dayMs - date.getTimezoneOffset() / 1440)))
.map((x) => new MessageImpl(x.type, core.characters.get(x.sender), x.text, new Date(x.time * 1000)));
} catch {
await this.fixLogs(character);
return [];
}
}
async getLogDates(character: string, key: string): Promise<ReadonlyArray<Date>> {
@ -94,6 +117,19 @@ export class Logs implements Logging {
async getAvailableCharacters(): Promise<ReadonlyArray<string>> {
return NativeLogs.getCharacters();
}
async fixLogs(character: string): Promise<void> {
if(this.attemptedFix) return alert(l('logs.corruption.mobile.error'));
this.attemptedFix = true;
alert(l('logs.corruption.mobile'));
try {
await NativeLogs.repair(character);
this.index = await NativeLogs.init(core.connection.character);
alert(l('logs.corruption.mobile.success'));
} catch {
alert(l('logs.corruption.mobile.error'));
}
}
}
export async function getGeneralSettings(): Promise<GeneralSettings | undefined> {

View File

@ -3,13 +3,18 @@ import WebKit
class IndexItem: Encodable {
let name: String
var index = NSMutableOrderedSet()
var dates = [UInt16]()
var dates = NSMutableOrderedSet()
var offsets = [UInt64]()
init(_ name: String) {
self.name = name
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(name, forKey: .name)
try container.encode(dates.array as! [UInt16], forKey: .dates)
}
private enum CodingKeys: String, CodingKey {
case name
case dates
@ -19,7 +24,7 @@ class IndexItem: Encodable {
class Logs: NSObject, WKScriptMessageHandler {
let fm = FileManager.default;
let baseDir = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask).first!
var buffer = UnsafeMutableRawPointer.allocate(bytes: 51000, alignedTo: 1)
var buffer = UnsafeMutableRawPointer.allocate(byteCount: 51000, alignment: 1)
var logDir: URL!
var character: String?
var index: [String: IndexItem]!
@ -43,6 +48,8 @@ class Logs: NSObject, WKScriptMessageHandler {
result = try getBacklog(data["key"] as! String)
case "getLogs":
result = try getLogs(data["character"] as! String, data["key"] as! String, (data["date"] as! NSNumber).uint16Value)
case "repair":
try repair(data["character"] as! String)
default:
message.webView!.evaluateJavaScript("nativeError('\(key)',new Error('Unknown message type'))")
return
@ -65,13 +72,13 @@ class Logs: NSObject, WKScriptMessageHandler {
let name = String(data: data.subdata(with: NSMakeRange(1, nameLength)), encoding: .utf8)!
var offset = nameLength + 1
let indexItem = IndexItem(name)
if (data.length - offset) % 7 != 0 { throw NSError(domain: "Log corruption", code: 0) }
while offset < data.length {
var date: UInt16 = 0
data.getBytes(&date, range: NSMakeRange(offset, 2))
indexItem.dates.append(date)
var o: UInt64 = 0
data.getBytes(&o, range: NSMakeRange(offset + 2, 5))
indexItem.index.add(date)
indexItem.dates.add(date)
indexItem.offsets.append(o)
offset += 7
}
@ -85,6 +92,7 @@ class Logs: NSObject, WKScriptMessageHandler {
try fm.createDirectory(at: logDir, withIntermediateDirectories: true, attributes: nil)
index = try getIndex(name)
loadedIndex = index
character = name
return String(data: try JSONEncoder().encode(index), encoding: .utf8)!
}
@ -104,7 +112,7 @@ class Logs: NSObject, WKScriptMessageHandler {
if(indexItem == nil) { fm.createFile(atPath: url.path, contents: nil) }
let fd = try FileHandle(forWritingTo: url)
fd.seekToEndOfFile()
if(!(indexItem?.index.contains(day) ?? false)) {
if(!(indexItem?.dates.contains(day) ?? false)) {
let indexFile = url.appendingPathExtension("idx")
if(indexItem == nil) { fm.createFile(atPath: indexFile.path, contents: nil) }
let indexFd = try FileHandle(forWritingTo: indexFile)
@ -120,9 +128,8 @@ class Logs: NSObject, WKScriptMessageHandler {
write(indexFd.fileDescriptor, &day, 2)
var offset = fd.offsetInFile
write(indexFd.fileDescriptor, &offset, 5)
indexItem!.index.add(indexItem!.offsets.count)
indexItem!.dates.add(day)
indexItem!.offsets.append(offset)
indexItem!.dates.append(day)
}
let start = fd.offsetInFile
write(fd.fileDescriptor, &time, 4)
@ -150,6 +157,7 @@ class Logs: NSObject, WKScriptMessageHandler {
file.seek(toFileOffset: file.offsetInFile - 2)
read(file.fileDescriptor, buffer, 2)
let length = buffer.load(as: UInt16.self)
if(length > file.offsetInFile - 2) { throw NSError(domain: "Log corruption", code: 0) }
let newOffset = file.offsetInFile - UInt64(length + 2)
file.seek(toFileOffset: newOffset)
read(file.fileDescriptor, buffer, Int(length))
@ -161,14 +169,14 @@ class Logs: NSObject, WKScriptMessageHandler {
func getLogs(_ character: String, _ key: String, _ date: UInt16) throws -> String {
let index = loadedIndex![key]
guard let indexKey = index?.index.index(of: date) else { return "[]" }
guard let indexKey = index?.dates.index(of: date) else { return "[]" }
let url = baseDir.appendingPathComponent("\(character)/logs/\(key)", isDirectory: false)
let file = try FileHandle(forReadingFrom: url)
let start = index!.offsets[indexKey]
let end = indexKey >= index!.offsets.count - 1 ? file.seekToEndOfFile() : index!.offsets[indexKey + 1]
file.seek(toFileOffset: start)
let length = Int(end - start)
let buffer = UnsafeMutableRawPointer.allocate(bytes: length, alignedTo: 1)
let buffer = UnsafeMutableRawPointer.allocate(byteCount: length, alignment: 1)
read(file.fileDescriptor, buffer, length)
var json = "["
var offset = 0
@ -185,19 +193,67 @@ class Logs: NSObject, WKScriptMessageHandler {
return String(data: try JSONEncoder().encode(loadedIndex), encoding: .utf8)!
}
func decodeString(_ buffer: UnsafeMutableRawPointer, _ offset: Int, _ length: Int) -> String? {
return String(bytesNoCopy: buffer.advanced(by: offset), length: length, encoding: .utf8, freeWhenDone: false)
}
func deserializeMessage(_ buffer: UnsafeMutableRawPointer, _ o: Int) throws -> (String, Int) {
var offset = o
let date = buffer.advanced(by: offset).bindMemory(to: UInt32.self, capacity: 1).pointee
let type = buffer.load(fromByteOffset: offset + 4, as: UInt8.self)
let senderLength = Int(buffer.load(fromByteOffset: offset + 5, as: UInt8.self))
guard let sender = String(bytesNoCopy: buffer.advanced(by: offset + 6), length: senderLength, encoding: .utf8, freeWhenDone: false) else {
guard let sender = decodeString(buffer, offset + 6, senderLength) else {
throw NSError(domain: "Log corruption", code: 0)
}
offset += senderLength + 6
let textLength = Int(buffer.advanced(by: offset).bindMemory(to: UInt16.self, capacity: 1).pointee)
guard let text = String(bytesNoCopy: buffer.advanced(by: offset + 2), length: textLength, encoding: .utf8, freeWhenDone: false) else {
guard let text = decodeString(buffer, offset + 2, textLength) else {
throw NSError(domain: "Log corruption", code: 0)
}
return ("{\"time\":\(date),\"type\":\(type),\"sender\":\(File.escape(sender)),\"text\":\(File.escape(text))}", offset + textLength + 2)
}
func repair(_ character: String) throws {
let files = try fm.contentsOfDirectory(at: baseDir.appendingPathComponent("\(character)/logs", isDirectory: true), includingPropertiesForKeys: nil, options: [.skipsHiddenFiles])
for file in files {
if(file.lastPathComponent.hasSuffix(".idx")) { continue }
let indexFd = try FileHandle(forUpdating: file.appendingPathExtension("idx"))
read(indexFd.fileDescriptor, buffer, 1)
indexFd.truncateFile(atOffset: UInt64(buffer.load(as: UInt8.self) + 1))
let fd = try FileHandle(forUpdating: file)
let size = fd.seekToEndOfFile()
fd.seek(toFileOffset: 0)
var lastDay = 0, pos = UInt64(0)
do {
while fd.offsetInFile < size {
pos = fd.offsetInFile
let max = read(fd.fileDescriptor, buffer, 51000)
var offset = 0
while offset + 10 < max {
let day = buffer.advanced(by: offset).bindMemory(to: UInt32.self, capacity: 1).pointee / 86400
let senderLength = Int(buffer.load(fromByteOffset: offset + 5, as: UInt8.self))
if offset + senderLength + 10 > max { break }
let sender = decodeString(buffer, offset + 6, senderLength)
let textLength = Int(buffer.advanced(by: offset + senderLength + 6).bindMemory(to: UInt16.self, capacity: 1).pointee)
if(offset + senderLength + textLength + 10 > max) { break }
let text = decodeString(buffer, offset + senderLength + 8, textLength)
let mark = senderLength + textLength + 8
let size = buffer.advanced(by: offset + mark).bindMemory(to: UInt16.self, capacity: 1).pointee
if(size != mark || sender == nil || text == nil) { throw NSError(domain: "", code: 0) }
if(day > lastDay) {
lastDay = Int(day)
write(indexFd.fileDescriptor, &lastDay, 2)
write(indexFd.fileDescriptor, &pos, 5)
}
offset = offset + mark + 2
pos = pos + UInt64(mark + 2)
}
if(offset == 0) { throw NSError(domain: "", code: 0) }
fd.seek(toFileOffset: pos)
}
} catch {
fd.truncateFile(atOffset: pos)
}
}
}
}

View File

@ -77,5 +77,8 @@ window.NativeLogs = {
},
getCharacters: function() {
return sendMessage('Logs', 'getCharacters', {});
},
repair: function(character) {
return sendMessage('Logs', 'repair', {character: character});
}
};

View File

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

View File

@ -1,46 +1,54 @@
{
"name": "flist-exported",
"version": "1.0.0",
"author": "The F-List Team",
"description": "F-List Exported",
"license": "MIT",
"devDependencies": {
"@fortawesome/fontawesome-free-webfonts": "^1.0.6",
"@types/node": "^10.3.3",
"@types/sortablejs": "^1.3.31",
"axios": "^0.18.0",
"bootstrap": "^4.1.0",
"css-loader": "^0.28.11",
"date-fns": "^1.28.5",
"electron": "^2.0.2",
"electron-builder": "^20.8.1",
"electron-log": "^2.2.9",
"electron-updater": "^2.21.4",
"extract-text-webpack-plugin": "4.0.0-beta.0",
"file-loader": "^1.1.10",
"fork-ts-checker-webpack-plugin": "^0.4.1",
"lodash": "^4.16.4",
"node-sass": "^4.8.3",
"optimize-css-assets-webpack-plugin": "^4.0.0",
"qs": "^6.5.1",
"raven-js": "^3.24.1",
"sass-loader": "^7.0.1",
"sortablejs": "^1.6.0",
"ts-loader": "^4.2.0",
"tslib": "^1.7.1",
"tslint": "^5.7.0",
"typescript": "^2.8.1",
"vue": "^2.5.16",
"vue-class-component": "^6.0.0",
"vue-loader": "^15.2.4",
"vue-property-decorator": "^6.0.0",
"vue-template-compiler": "^2.5.16",
"webpack": "^4.5.0"
},
"dependencies": {
"@types/lodash": "^4.14.107",
"keytar": "^4.2.1",
"spellchecker": "^3.4.3",
"style-loader": "^0.21.0"
}
"name": "flist-exported",
"version": "1.0.0",
"author": "The F-List Team",
"description": "F-List Exported",
"license": "MIT",
"devDependencies": {
"@fortawesome/fontawesome-free-webfonts": "^1.0.6",
"@types/lodash": "^4.14.116",
"@types/node": "^10.5.6",
"@types/sortablejs": "^1.3.31",
"axios": "^0.18.0",
"bootstrap": "^4.1.3",
"css-loader": "^1.0.0",
"date-fns": "^1.28.5",
"electron": "2.0.2",
"electron-log": "^2.2.16",
"electron-packager": "^12.1.0",
"electron-rebuild": "^1.8.2",
"extract-text-webpack-plugin": "4.0.0-beta.0",
"file-loader": "^1.1.10",
"fork-ts-checker-webpack-plugin": "^0.4.4",
"lodash": "^4.16.4",
"node-sass": "^4.8.3",
"optimize-css-assets-webpack-plugin": "^5.0.0",
"qs": "^6.5.1",
"raven-js": "^3.26.4",
"sass-loader": "^7.1.0",
"sortablejs": "^1.6.0",
"style-loader": "^0.21.0",
"ts-loader": "^4.2.0",
"tslib": "^1.7.1",
"tslint": "^5.7.0",
"typescript": "^3.0.1",
"vue": "^2.5.17",
"vue-class-component": "^6.0.0",
"vue-loader": "^15.2.6",
"vue-property-decorator": "^7.0.0",
"vue-template-compiler": "^2.5.17",
"webpack": "^4.16.4"
},
"dependencies": {
"keytar": "^4.2.1",
"spellchecker": "^3.4.3"
},
"optionalDependencies": {
"appdmg": "^0.5.2",
"electron-squirrel-startup": "^1.0.0",
"electron-winstaller": "^2.6.4"
},
"scripts": {
"postinstall": "electron-rebuild -o spellchecker,keytar"
}
}

View File

@ -12,13 +12,15 @@ All necessary files to build F-Chat 3.0 as an Electron, mobile or web applicatio
- 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)
- Change into the `electron` directory.
- Run `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).
- Run `yarn pack`. The generated installer is placed into the `dist` directory.
- On Windows you can add the path to and password for a code signing certificate as arguments.
- On Mac you can add your code signing identity as an argument. `zip` is required to be installed.
- On Linux you can add a GPG key for signing as an argument. `mksquashfs` and `zsyncmake` are required to be installed.
## Building for Mobile
- Change into the `mobile` directory.

View File

@ -280,4 +280,8 @@ $genders: (
border-radius: 100%;
line-height: 0;
box-shadow: 0 1px 4px #000;
}
.nav-link {
cursor: pointer;
}

View File

@ -1,5 +1,5 @@
$blue-color: #06f;
.blackText {
text-shadow: $gray-600 1px 1px 1px, $gray-600 -1px 1px 1px, $gray-600 1px -1px 1px, $gray-600 -1px -1px 1px;
text-shadow: $gray-600 1px 1px, $gray-600 -1px 1px, $gray-600 1px -1px, $gray-600 -1px -1px;
}

View File

@ -59,7 +59,4 @@ $pagination-active-color: $link-color;
$text-background-color: $gray-100;
$text-background-color-disabled: $gray-200;
// Dark theme helpers
$theme-is-dark: true;
@import "invert";

View File

@ -1,9 +1,9 @@
.purpleText {
text-shadow: #306 1px 1px 1px, #306 -1px 1px 1px, #306 1px -1px 1px, #306 -1px -1px 1px;
text-shadow: #306 1px 1px, #306 -1px 1px, #306 1px -1px, #306 -1px -1px;
}
.blackText {
text-shadow: $gray-600 1px 1px 1px, $gray-600 -1px 1px 1px, $gray-600 1px -1px 1px, $gray-600 -1px -1px 1px;
text-shadow: $gray-600 1px 1px, $gray-600 -1px 1px, $gray-600 1px -1px, $gray-600 -1px -1px;
}
$blue-color: #06f;

View File

@ -57,7 +57,4 @@ $pagination-active-color: $link-color;
$text-background-color: $gray-200;
$text-background-color-disabled: $gray-100;
// Dark theme helpers
$theme-is-dark: true;
@import "invert";

View File

@ -15,6 +15,5 @@
}
// Alert color levels
$alert-bg-level: 7;
$alert-border-level: 6;
$alert-color-level: -8;
$alert-border-level: 4;
$theme-is-dark: true;

View File

@ -3,14 +3,14 @@
"@fortawesome/fontawesome-free-webfonts@^1.0.3":
version "1.0.4"
resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-free-webfonts/-/fontawesome-free-webfonts-1.0.4.tgz#bac5d89755bf3bc2d2b4deee47d92febf641bb1f"
version "1.0.9"
resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-free-webfonts/-/fontawesome-free-webfonts-1.0.9.tgz#72f2c10453422aba0d338fa6a9cb761b50ba24d5"
abbrev@1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8"
ajv@^5.1.0:
ajv@^5.1.0, ajv@^5.3.0:
version "5.5.2"
resolved "https://registry.yarnpkg.com/ajv/-/ajv-5.5.2.tgz#73b5eeca3fab653e3d3f9422b341ad42205dc965"
dependencies:
@ -27,6 +27,10 @@ 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"
@ -36,8 +40,8 @@ aproba@^1.0.3:
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"
version "1.1.5"
resolved "https://registry.yarnpkg.com/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz#4b35c2944f062a8bfcda66410760350fe9ddfc21"
dependencies:
delegates "^1.0.0"
readable-stream "^2.0.6"
@ -47,17 +51,15 @@ array-find-index@^1.0.1:
resolved "https://registry.yarnpkg.com/array-find-index/-/array-find-index-1.0.2.tgz#df010aa1287e164bbda6f9723b0a96a1ec4187a1"
asn1@~0.2.3:
version "0.2.3"
resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.3.tgz#dac8787713c9966849fc8180777ebe9c1ddf3b86"
version "0.2.4"
resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.4.tgz#8d2475dfab553bb33e77b54e59e880bb8ce23136"
dependencies:
safer-buffer "~2.1.0"
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-foreach@^0.1.3:
version "0.1.3"
resolved "https://registry.yarnpkg.com/async-foreach/-/async-foreach-0.1.3.tgz#36121f845c0578172de419a97dbeb1d16ec34542"
@ -66,25 +68,21 @@ 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"
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"
aws4@^1.6.0, aws4@^1.8.0:
version "1.8.0"
resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.8.0.tgz#f0e003d9ca9e7f59c7a508945d7b2ef9a04a542f"
balanced-match@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767"
bcrypt-pbkdf@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.1.tgz#63bc5dcb61331b92bc05fd528953c33462a06f8d"
version "1.0.2"
resolved "https://registry.yarnpkg.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz#a4301d389b6a43f9b67ff3ca11a3f6637e360e9e"
dependencies:
tweetnacl "^0.14.3"
@ -94,27 +92,9 @@ block-stream@*:
dependencies:
inherits "~2.0.0"
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@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/bootstrap/-/bootstrap-4.0.0.tgz#ceb03842c145fcc1b9b4e15da2a05656ba68469a"
version "4.1.3"
resolved "https://registry.yarnpkg.com/bootstrap/-/bootstrap-4.1.3.tgz#0eb371af2c8448e8c210411d0cb824a6409a12be"
brace-expansion@^1.1.7:
version "1.1.11"
@ -142,10 +122,6 @@ camelcase@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-3.0.0.tgz#32fc4b9fcdaf845fcdf7e73bb97cac2261f0ab0a"
caseless@~0.11.0:
version "0.11.0"
resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.11.0.tgz#715b96ea9841593cc33067923f5ec60ebda4f7d7"
caseless@~0.12.0:
version "0.12.0"
resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc"
@ -176,16 +152,12 @@ 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"
combined-stream@1.0.6, combined-stream@^1.0.5, combined-stream@~1.0.5:
combined-stream@1.0.6, combined-stream@~1.0.5, combined-stream@~1.0.6:
version "1.0.6"
resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.6.tgz#723e7df6e801ac5613113a7e445a9b69cb632818"
dependencies:
delayed-stream "~1.0.0"
commander@^2.9.0:
version "2.14.1"
resolved "https://registry.yarnpkg.com/commander/-/commander-2.14.1.tgz#2235123e37af8ca3c65df45b026dbd357b01b9aa"
concat-map@0.0.1:
version "0.0.1"
resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
@ -205,18 +177,6 @@ cross-spawn@^3.0.0:
lru-cache "^4.0.1"
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"
currently-unhandled@^0.4.1:
version "0.4.1"
resolved "https://registry.yarnpkg.com/currently-unhandled/-/currently-unhandled-0.4.1.tgz#988df33feab191ef799a61369dd76c17adf957ea"
@ -242,14 +202,15 @@ delegates@^1.0.0:
resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a"
ecc-jsbn@~0.1.1:
version "0.1.1"
resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.1.tgz#0fc73a9ed5f0d53c38193398523ef7e543777505"
version "0.1.2"
resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz#3a83a904e54353287874c564b7549386849a98c9"
dependencies:
jsbn "~0.1.0"
safer-buffer "^2.1.0"
error-ex@^1.2.0:
version "1.3.1"
resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.1.tgz#f855a86ce61adc4e8621c3cda21e7a7612c3a8dc"
version "1.3.2"
resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf"
dependencies:
is-arrayish "^0.2.1"
@ -257,9 +218,9 @@ escape-string-regexp@^1.0.2:
version "1.0.5"
resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4"
extend@~3.0.0, extend@~3.0.1:
version "3.0.1"
resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.1.tgz#a755ea7bc1adfcc5a31ce7e762dbaadc5e636444"
extend@~3.0.1, extend@~3.0.2:
version "3.0.2"
resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa"
extsprintf@1.3.0:
version "1.3.0"
@ -288,15 +249,7 @@ 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"
form-data@~2.3.1:
form-data@~2.3.1, form-data@~2.3.2:
version "2.3.2"
resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.3.2.tgz#4970498be604c20c005d4f5c23aecd21d6b49099"
dependencies:
@ -331,24 +284,14 @@ gauge@~2.7.3:
wide-align "^1.1.0"
gaze@^1.0.0:
version "1.1.2"
resolved "https://registry.yarnpkg.com/gaze/-/gaze-1.1.2.tgz#847224677adb8870d679257ed3388fdb61e40105"
version "1.1.3"
resolved "https://registry.yarnpkg.com/gaze/-/gaze-1.1.3.tgz#c441733e13b927ac8c0ff0b4c3b033f28812924a"
dependencies:
globule "^1.0.0"
generate-function@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/generate-function/-/generate-function-2.0.0.tgz#6858fe7c0969b7d4e9093337647ac79f60dfbe74"
generate-object-property@^1.1.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/generate-object-property/-/generate-object-property-1.2.0.tgz#9c0e1c40308ce804f4783618b937fa88f99d50d0"
dependencies:
is-property "^1.0.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"
version "1.0.3"
resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-1.0.3.tgz#f978fa4c90d1dfe7ff2d6beda2a515e713bdcf4a"
get-stdin@^4.0.1:
version "4.0.1"
@ -382,11 +325,11 @@ glob@^7.0.0, glob@^7.0.3, glob@^7.0.5, glob@~7.1.1:
path-is-absolute "^1.0.0"
globule@^1.0.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/globule/-/globule-1.2.0.tgz#1dc49c6822dd9e8a2fa00ba2a295006e8664bd09"
version "1.2.1"
resolved "https://registry.yarnpkg.com/globule/-/globule-1.2.1.tgz#5dffb1b191f22d20797a9369b49eab4e9839696d"
dependencies:
glob "~7.1.1"
lodash "~4.17.4"
lodash "~4.17.10"
minimatch "~3.0.2"
graceful-fs@^4.1.2:
@ -397,15 +340,6 @@ 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@~2.0.6:
version "2.0.6"
resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-2.0.6.tgz#cdcbc08188265ad119b6a5a7c8ab70eecfb5d27d"
dependencies:
chalk "^1.1.1"
commander "^2.9.0"
is-my-json-valid "^2.12.4"
pinkie-promise "^2.0.0"
har-validator@~5.0.3:
version "5.0.3"
resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-5.0.3.tgz#ba402c266194f15956ef15e0fcf242993f6a7dfd"
@ -413,6 +347,13 @@ har-validator@~5.0.3:
ajv "^5.1.0"
har-schema "^2.0.0"
har-validator@~5.1.0:
version "5.1.0"
resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-5.1.0.tgz#44657f5688a22cfd4b72486e81b3a3fb11742c29"
dependencies:
ajv "^5.3.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"
@ -423,43 +364,9 @@ has-unicode@^2.0.0:
version "2.0.1"
resolved "https://registry.yarnpkg.com/has-unicode/-/has-unicode-2.0.1.tgz#e0e6fe6a28cf51138855e086d1691e771de2a8b9"
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"
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.1"
resolved "https://registry.yarnpkg.com/hoek/-/hoek-4.2.1.tgz#9634502aa12c445dd5a7c5734b572bb8738aacbb"
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"
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"
version "2.7.1"
resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.7.1.tgz#97f236977bd6e125408930ff6de3eec6281ec047"
http-signature@~1.2.0:
version "1.2.0"
@ -516,23 +423,9 @@ is-fullwidth-code-point@^1.0.0:
dependencies:
number-is-nan "^1.0.0"
is-my-ip-valid@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/is-my-ip-valid/-/is-my-ip-valid-1.0.0.tgz#7b351b8e8edd4d3995d4d066680e664d94696824"
is-my-json-valid@^2.12.4:
version "2.17.2"
resolved "https://registry.yarnpkg.com/is-my-json-valid/-/is-my-json-valid-2.17.2.tgz#6b2103a288e94ef3de5cf15d29dd85fc4b78d65c"
dependencies:
generate-function "^2.0.0"
generate-object-property "^1.1.0"
is-my-ip-valid "^1.0.0"
jsonpointer "^4.0.0"
xtend "^4.0.0"
is-property@^1.0.0:
version "1.0.2"
resolved "https://registry.yarnpkg.com/is-property/-/is-property-1.0.2.tgz#57fe1c4e48474edd65b09911f26b1cd4095dda84"
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-typedarray@~1.0.0:
version "1.0.0"
@ -555,8 +448,8 @@ isstream@~0.1.2:
resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a"
js-base64@^2.1.8:
version "2.4.3"
resolved "https://registry.yarnpkg.com/js-base64/-/js-base64-2.4.3.tgz#2e545ec2b0f2957f41356510205214e98fad6582"
version "2.4.8"
resolved "https://registry.yarnpkg.com/js-base64/-/js-base64-2.4.8.tgz#57a9b130888f956834aa40c5b165ba59c758f033"
jsbn@~0.1.0:
version "0.1.1"
@ -574,10 +467,6 @@ 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"
jsonpointer@^4.0.0:
version "4.0.1"
resolved "https://registry.yarnpkg.com/jsonpointer/-/jsonpointer-4.0.1.tgz#4fd92cb34e0e9db3c89c8622ecf51f9b978c6cb9"
jsprim@^1.2.2:
version "1.4.1"
resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.1.tgz#313e66bc1e5cc06e438bc1b7499c2e5c56acb6a2"
@ -615,9 +504,9 @@ lodash.mergewith@^4.6.0:
version "4.6.1"
resolved "https://registry.yarnpkg.com/lodash.mergewith/-/lodash.mergewith-4.6.1.tgz#639057e726c3afbdb3e7d42741caa8d6e4335927"
lodash@^4.0.0, lodash@~4.17.4:
version "4.17.5"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.5.tgz#99a92d65c0272debe8c96b6057bc8fbfa3bed511"
lodash@^4.0.0, lodash@~4.17.10:
version "4.17.10"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.10.tgz#1b7793cf7259ea38fb3661d4d38b3260af8ae4e7"
loud-rejection@^1.0.0:
version "1.6.0"
@ -627,8 +516,8 @@ loud-rejection@^1.0.0:
signal-exit "^3.0.0"
lru-cache@^4.0.1:
version "4.1.1"
resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-4.1.1.tgz#622e32e82488b49279114a4f9ecf45e7cd6bba55"
version "4.1.3"
resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-4.1.3.tgz#a1175cf3496dfc8436c156c334b4955992bce69c"
dependencies:
pseudomap "^1.0.2"
yallist "^2.1.2"
@ -652,17 +541,17 @@ meow@^3.7.0:
redent "^1.0.0"
trim-newlines "^1.0.0"
mime-db@~1.33.0:
version "1.33.0"
resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.33.0.tgz#a3492050a5cb9b63450541e39d9788d2272783db"
mime-db@~1.35.0:
version "1.35.0"
resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.35.0.tgz#0569d657466491283709663ad379a99b90d9ab47"
mime-types@^2.1.12, mime-types@~2.1.17, mime-types@~2.1.7:
version "2.1.18"
resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.18.tgz#6f323f60a83d11146f831ff11fd66e2fe5503bb8"
mime-types@^2.1.12, mime-types@~2.1.17, mime-types@~2.1.19:
version "2.1.19"
resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.19.tgz#71e464537a7ef81c15f2db9d97e913fc0ff606f0"
dependencies:
mime-db "~1.33.0"
mime-db "~1.35.0"
"minimatch@2 || 3", minimatch@^3.0.2, minimatch@^3.0.4, minimatch@~3.0.2:
"minimatch@2 || 3", minimatch@^3.0.4, minimatch@~3.0.2:
version "3.0.4"
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083"
dependencies:
@ -682,31 +571,30 @@ minimist@^1.1.3:
dependencies:
minimist "0.0.8"
nan@^2.3.2:
version "2.9.2"
resolved "https://registry.yarnpkg.com/nan/-/nan-2.9.2.tgz#f564d75f5f8f36a6d9456cca7a6c4fe488ab7866"
nan@^2.10.0:
version "2.10.0"
resolved "https://registry.yarnpkg.com/nan/-/nan-2.10.0.tgz#96d0cd610ebd58d4b4de9cc0c6828cda99c7548f"
node-gyp@^3.3.1:
version "3.6.2"
resolved "https://registry.yarnpkg.com/node-gyp/-/node-gyp-3.6.2.tgz#9bfbe54562286284838e750eac05295853fa1c60"
node-gyp@^3.8.0:
version "3.8.0"
resolved "https://registry.yarnpkg.com/node-gyp/-/node-gyp-3.8.0.tgz#540304261c330e80d0d5edce253a68cb3964218c"
dependencies:
fstream "^1.0.0"
glob "^7.0.3"
graceful-fs "^4.1.2"
minimatch "^3.0.2"
mkdirp "^0.5.0"
nopt "2 || 3"
npmlog "0 || 1 || 2 || 3 || 4"
osenv "0"
request "2"
request "^2.87.0"
rimraf "2"
semver "~5.3.0"
tar "^2.0.0"
which "1"
node-sass@^4.7.2:
version "4.7.2"
resolved "https://registry.yarnpkg.com/node-sass/-/node-sass-4.7.2.tgz#9366778ba1469eb01438a9e8592f4262bcb6794e"
version "4.9.3"
resolved "https://registry.yarnpkg.com/node-sass/-/node-sass-4.9.3.tgz#f407cf3d66f78308bb1e346b24fa428703196224"
dependencies:
async-foreach "^0.1.3"
chalk "^1.1.1"
@ -720,10 +608,10 @@ node-sass@^4.7.2:
lodash.mergewith "^4.6.0"
meow "^3.7.0"
mkdirp "^0.5.1"
nan "^2.3.2"
node-gyp "^3.3.1"
nan "^2.10.0"
node-gyp "^3.8.0"
npmlog "^4.0.0"
request "~2.79.0"
request "2.87.0"
sass-graph "^2.2.4"
stdout-stream "^1.4.0"
"true-case-path" "^1.0.2"
@ -756,10 +644,14 @@ 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:
oauth-sign@~0.8.2:
version "0.8.2"
resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.8.2.tgz#46a6ab7f0aead8deae9ec0565780b7d4efeb9d43"
oauth-sign@~0.9.0:
version "0.9.0"
resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.9.0.tgz#47a7b016baa68b5fa0ecf3dee08a85c679ac6455"
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"
@ -841,17 +733,17 @@ pseudomap@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/pseudomap/-/pseudomap-1.0.2.tgz#f052a28da70e618917ef0a8ac34c1ae5a68286b3"
psl@^1.1.24:
version "1.1.29"
resolved "https://registry.yarnpkg.com/psl/-/psl-1.1.29.tgz#60f580d360170bb722a797cc704411e6da850c67"
punycode@^1.4.1:
version "1.4.1"
resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e"
qs@~6.3.0:
version "6.3.2"
resolved "https://registry.yarnpkg.com/qs/-/qs-6.3.2.tgz#e75bd5f6e268122a2a0e0bda630b2550c166502c"
qs@~6.5.1:
version "6.5.1"
resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.1.tgz#349cdf6eef89ec45c12d7d5eb3fc0c870343a6d8"
qs@~6.5.1, qs@~6.5.2:
version "6.5.2"
resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36"
read-pkg-up@^1.0.1:
version "1.0.1"
@ -869,15 +761,15 @@ read-pkg@^1.0.0:
path-type "^1.0.0"
readable-stream@^2.0.1, readable-stream@^2.0.6:
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"
redent@^1.0.0:
@ -893,9 +785,9 @@ repeating@^2.0.0:
dependencies:
is-finite "^1.0.0"
request@2:
version "2.83.0"
resolved "https://registry.yarnpkg.com/request/-/request-2.83.0.tgz#ca0b65da02ed62935887808e6f510381034e3356"
request@2.87.0:
version "2.87.0"
resolved "https://registry.yarnpkg.com/request/-/request-2.87.0.tgz#32f00235cd08d482b4d0d68db93a829c0ed5756e"
dependencies:
aws-sign2 "~0.7.0"
aws4 "^1.6.0"
@ -905,7 +797,6 @@ request@2:
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"
@ -915,35 +806,34 @@ request@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"
request@~2.79.0:
version "2.79.0"
resolved "https://registry.yarnpkg.com/request/-/request-2.79.0.tgz#4dfe5bf6be8b8cdc37fcf93e04b65577722710de"
request@^2.87.0:
version "2.88.0"
resolved "https://registry.yarnpkg.com/request/-/request-2.88.0.tgz#9c2fca4f7d35b592efe57c7f0a55e81052124fef"
dependencies:
aws-sign2 "~0.6.0"
aws4 "^1.2.1"
caseless "~0.11.0"
combined-stream "~1.0.5"
extend "~3.0.0"
aws-sign2 "~0.7.0"
aws4 "^1.8.0"
caseless "~0.12.0"
combined-stream "~1.0.6"
extend "~3.0.2"
forever-agent "~0.6.1"
form-data "~2.1.1"
har-validator "~2.0.6"
hawk "~3.1.3"
http-signature "~1.1.0"
form-data "~2.3.2"
har-validator "~5.1.0"
http-signature "~1.2.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"
qs "~6.3.0"
stringstream "~0.0.4"
tough-cookie "~2.3.0"
tunnel-agent "~0.4.1"
uuid "^3.0.0"
mime-types "~2.1.19"
oauth-sign "~0.9.0"
performance-now "^2.1.0"
qs "~6.5.2"
safe-buffer "^5.1.2"
tough-cookie "~2.4.3"
tunnel-agent "^0.6.0"
uuid "^3.3.2"
require-directory@^2.1.1:
version "2.1.1"
@ -959,9 +849,13 @@ rimraf@2:
dependencies:
glob "^7.0.5"
safe-buffer@^5.0.1, 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"
safe-buffer@^5.0.1, safe-buffer@^5.1.1, safe-buffer@^5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1:
version "5.1.2"
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d"
safer-buffer@^2.0.2, safer-buffer@^2.1.0, safer-buffer@~2.1.0:
version "2.1.2"
resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a"
sass-graph@^2.2.4:
version "2.2.4"
@ -995,18 +889,6 @@ 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.1.0"
resolved "https://registry.yarnpkg.com/sntp/-/sntp-2.1.0.tgz#2c6cec14fedc2222739caf9b5c3d85d1cc5a2cc8"
dependencies:
hoek "4.x.x"
source-map@^0.4.2:
version "0.4.4"
resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.4.4.tgz#eba4f5da9c0dc999de68032d8b4f76173652036b"
@ -1036,13 +918,14 @@ spdx-license-ids@^3.0.0:
resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-3.0.0.tgz#7a7cd28470cc6d3a1cfe6d66886f6bc430d3ac87"
sshpk@^1.7.0:
version "1.13.1"
resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.13.1.tgz#512df6da6287144316dc4c18fe1cf1d940739be3"
version "1.14.2"
resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.14.2.tgz#c6fc61648a3d9c4e764fd3fcdf4ea105e492ba98"
dependencies:
asn1 "~0.2.3"
assert-plus "^1.0.0"
dashdash "^1.12.0"
getpass "^0.1.1"
safer-buffer "^2.0.2"
optionalDependencies:
bcrypt-pbkdf "^1.0.0"
ecc-jsbn "~0.1.1"
@ -1063,22 +946,31 @@ string-width@^1.0.1, string-width@^1.0.2:
is-fullwidth-code-point "^1.0.0"
strip-ansi "^3.0.0"
string_decoder@~1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.0.3.tgz#0fc67d7c141825de94282dd536bec6b9bce860ab"
"string-width@^1.0.2 || 2":
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@~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"
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@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-2.0.0.tgz#6219a85616520491f35788bdbf1447a99c7e6b0e"
@ -1103,12 +995,19 @@ tar@^2.0.0:
fstream "^1.0.2"
inherits "2"
tough-cookie@~2.3.0, tough-cookie@~2.3.3:
tough-cookie@~2.3.3:
version "2.3.4"
resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.3.4.tgz#ec60cee38ac675063ffc97a5c18970578ee83655"
dependencies:
punycode "^1.4.1"
tough-cookie@~2.4.3:
version "2.4.3"
resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.4.3.tgz#53f36da3f47783b0925afa06ff9f3b165280f781"
dependencies:
psl "^1.1.24"
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"
@ -1125,10 +1024,6 @@ tunnel-agent@^0.6.0:
dependencies:
safe-buffer "^5.0.1"
tunnel-agent@~0.4.1:
version "0.4.3"
resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.4.3.tgz#6373db76909fe570e08d73583365ed828a74eeeb"
tweetnacl@^0.14.3, tweetnacl@~0.14.0:
version "0.14.5"
resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64"
@ -1137,13 +1032,13 @@ 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, uuid@^3.1.0:
version "3.2.1"
resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.2.1.tgz#12c528bb9d58d0b9265d9a2f6f0fe8be17ff1f14"
uuid@^3.1.0, uuid@^3.3.2:
version "3.3.2"
resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.3.2.tgz#1b4af4955eb3077c501c23872fc6513811587131"
validate-npm-package-license@^3.0.1:
version "3.0.3"
resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.3.tgz#81643bcbef1bdfecd4623793dc4648948ba98338"
version "3.0.4"
resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz#fc91f6b9c7ba15c857f4cb2c5defeec39d4f410a"
dependencies:
spdx-correct "^3.0.0"
spdx-expression-parse "^3.0.0"
@ -1161,16 +1056,16 @@ which-module@^1.0.0:
resolved "https://registry.yarnpkg.com/which-module/-/which-module-1.0.0.tgz#bba63ca861948994ff307736089e3b96026c2a4f"
which@1, which@^1.2.9:
version "1.3.0"
resolved "https://registry.yarnpkg.com/which/-/which-1.3.0.tgz#ff04bdfc010ee547d780bec38e1ac1c2777d253a"
version "1.3.1"
resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a"
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"
version "1.1.3"
resolved "https://registry.yarnpkg.com/wide-align/-/wide-align-1.1.3.tgz#ae074e6bdc0c14a431e804e624549c633b000457"
dependencies:
string-width "^1.0.2"
string-width "^1.0.2 || 2"
wrap-ansi@^2.0.0:
version "2.1.0"
@ -1183,10 +1078,6 @@ wrappy@1:
version "1.0.2"
resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
xtend@^4.0.0:
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"

View File

@ -63,6 +63,7 @@
],
"cyclomatic-complexity": false,
"eofline": false,
"file-name-casing": false,
"forin": false,
"interface-name": false,
"interface-over-type-literal": false,

View File

@ -30,12 +30,10 @@
* @see {@link https://github.com/f-list/exported|GitHub repo}
*/
import Axios from 'axios';
import * as Raven from 'raven-js';
import Vue from 'vue';
import Chat from '../chat/Chat.vue';
import {init as initCore} from '../chat/core';
import l from '../chat/localize';
import VueRaven from '../chat/vue-raven';
import {setupRaven} from '../chat/vue-raven';
import Socket from '../chat/WebSocket';
import Connection from '../fchat/connection';
import '../scss/fa.scss'; //tslint:disable-line:no-import-side-effect
@ -49,27 +47,8 @@ if(typeof window.Promise !== 'function' || typeof window.Notification !== 'funct
const version = (<{version: string}>require('./package.json')).version; //tslint:disable-line:no-require-imports
Axios.defaults.params = { __fchat: `web/${version}` };
if(process.env.NODE_ENV === 'production') {
Raven.config('https://a9239b17b0a14f72ba85e8729b9d1612@sentry.f-list.net/2', {
release: `web-${version}`,
dataCallback: (data: {culprit?: string, exception?: {values: {stacktrace: {frames: {filename: string}[]}}[]}}) => {
if(data.culprit !== undefined) {
const end = data.culprit.lastIndexOf('?');
data.culprit = `~${data.culprit.substring(data.culprit.lastIndexOf('/'), end === -1 ? undefined : end)}`;
}
if(data.exception !== undefined)
for(const ex of data.exception.values)
for(const frame of ex.stacktrace.frames) {
const index = frame.filename.lastIndexOf('/');
const endIndex = frame.filename.lastIndexOf('?');
frame.filename = `~${frame.filename.substring(index !== -1 ? index : 0, endIndex === -1 ? undefined : endIndex)}`;
}
}
}).addPlugin(VueRaven, Vue).install();
(<Window & {onunhandledrejection(e: PromiseRejectionEvent): void}>window).onunhandledrejection = (e: PromiseRejectionEvent) => {
Raven.captureException(<Error>e.reason);
};
}
if(process.env.NODE_ENV === 'production')
setupRaven('https://a9239b17b0a14f72ba85e8729b9d1612@sentry.f-list.net/2', `web-${version}`);
declare const chatSettings: {account: string, theme: string, characters: ReadonlyArray<string>, defaultCharacter: string | null};

View File

@ -7,19 +7,17 @@ export default class Notifications extends BaseNotifications {
async notify(conversation: Conversation, title: string, body: string, icon: string, sound: string): Promise<void> {
if(!this.shouldNotify(conversation)) return;
try {
return super.notify(conversation, title, body, icon, sound);
await super.notify(conversation, title, body, icon, sound);
} catch {
(async() => { //tslint:disable-line:no-floating-promises
//tslint:disable-next-line:no-require-imports no-submodule-imports
await navigator.serviceWorker.register(<string>require('file-loader!./sw.js'));
const reg = await navigator.serviceWorker.ready;
await reg.showNotification(title, this.getOptions(conversation, body, icon));
navigator.serviceWorker.onmessage = (e) => {
const conv = core.conversations.byKey((<{key: string}>e.data).key);
if(conv !== undefined) conv.show();
window.focus();
};
})();
//tslint:disable-next-line:no-require-imports no-submodule-imports
await navigator.serviceWorker.register(<string>require('file-loader!./sw.js'));
const reg = await navigator.serviceWorker.ready;
await reg.showNotification(title, this.getOptions(conversation, body, icon));
navigator.serviceWorker.onmessage = (e) => {
const conv = core.conversations.byKey((<{key: string}>e.data).key);
if(conv !== undefined) conv.show();
window.focus();
};
}
}
}

View File

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

2500
yarn.lock

File diff suppressed because it is too large Load Diff