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

View File

@ -4,7 +4,7 @@
@touchend="$refs['userMenu'].handleEvent($event)"> @touchend="$refs['userMenu'].handleEvent($event)">
<sidebar id="sidebar" :label="l('chat.menu')" icon="fa-bars"> <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"/> <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/> <a href="#" @click.prevent="logOut" class="btn"><i class="fas fa-sign-out-alt"></i>{{l('chat.logout')}}</a><br/>
<div> <div>
{{l('chat.status')}} {{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')" <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> style="margin-right:5px;vertical-align:sub"></span>
<h5 style="margin:0;display:inline;vertical-align:middle">{{conversation.name}}</h5> <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="fa" :class="{'fa-chevron-down': !descriptionExpanded, 'fa-chevron-up': descriptionExpanded}"></span>
<span class="btn-text">{{l('channel.description')}}</span> <span class="btn-text">{{l('channel.description')}}</span>
</a> </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" <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> @click="hideSearch"><i class="fas fa-times"></i></a>
</div> </div>
<div class="border-top messages" :class="'messages-' + conversation.mode" style="flex:1;overflow:auto;margin-top:2px" <div class="border-top messages" :class="'messages-' + conversation.mode" ref="messages" @scroll="onMessagesScroll"
ref="messages" @scroll="onMessagesScroll"> style="flex:1;overflow:auto;margin-top:2px;position:relative">
<template v-for="message in messages"> <template v-for="message in messages">
<message-view :message="message" :channel="conversation.channel" :key="message.id" <message-view :message="message" :channel="conversation.channel" :key="message.id"
:classes="message == conversation.lastRead ? 'last-read' : ''"> :classes="message == conversation.lastRead ? 'last-read' : ''">
</message-view> </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" <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> v-if="message.sfc.logid" target="_blank">{{l('events.report.viewLog')}}</a>
<span v-else>{{l('events.report.noLog')}}</span> <span v-else>{{l('events.report.noLog')}}</span>
@ -174,6 +174,8 @@
keypressHandler!: EventListener; keypressHandler!: EventListener;
scrolledDown = true; scrolledDown = true;
scrolledUp = false; scrolledUp = false;
adCountdown = 0;
adsMode = l('channel.mode.ads');
mounted(): void { mounted(): void {
this.extraButtons = [{ this.extraButtons = [{
@ -203,6 +205,21 @@
this.search = this.searchInput; this.search = this.searchInput;
}, 500); }, 500);
this.messageView = <HTMLElement>this.$refs['messages']; 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 { destroyed(): void {
@ -252,9 +269,13 @@
} }
onMessagesScroll(): void { 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.scrolledUp = true;
this.conversation.loadMore();
} else this.scrolledUp = false; } else this.scrolledUp = false;
this.scrolledDown = this.messageView.scrollTop + this.messageView.offsetHeight >= this.messageView.scrollHeight - 15; this.scrolledDown = this.messageView.scrollTop + this.messageView.offsetHeight >= this.messageView.scrollHeight - 15;
} }
@ -313,7 +334,7 @@
else if(getKey(e) === Keys.Enter) { else if(getKey(e) === Keys.Enter) {
if(e.shiftKey === this.settings.enterSend) return; if(e.shiftKey === this.settings.enterSend) return;
e.preventDefault(); 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 { get characterImage(): string {
return characterImage(this.conversation.name); return characterImage(this.conversation.name);
} }
@ -390,9 +404,9 @@
} }
.chat-info-text { .chat-info-text {
display:flex; display: flex;
align-items:center; align-items: center;
flex:1 51%; flex: 1 51%;
@media (max-width: breakpoint-max(xs)) { @media (max-width: breakpoint-max(xs)) {
flex-basis: 100%; flex-basis: 100%;
} }

View File

@ -246,9 +246,11 @@
this.dates[this.dateOffset++]); this.dates[this.dateOffset++]);
this.messages = messages.concat(this.messages); this.messages = messages.concat(this.messages);
const noOverflow = list.offsetHeight === list.scrollHeight; const noOverflow = list.offsetHeight === list.scrollHeight;
const firstMessage = <HTMLElement>list.firstElementChild!;
this.$nextTick(() => { this.$nextTick(() => {
if(list.offsetHeight === list.scrollHeight) return this.onMessagesScroll(); 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>
<div class="form-group"> <div class="form-group">
<label class="control-label" for="idleTimer">{{l('settings.idleTimer')}}</label> <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>
<div class="form-group"> <div class="form-group">
<label class="control-label" for="messageSeparators"> <label class="control-label" for="messageSeparators">
@ -160,7 +160,7 @@
alwaysNotify!: boolean; alwaysNotify!: boolean;
logMessages!: boolean; logMessages!: boolean;
logAds!: boolean; logAds!: boolean;
fontSize!: number; fontSize!: string;
showNeedsReply!: boolean; showNeedsReply!: boolean;
enterSend!: boolean; enterSend!: boolean;
colorBookmarks!: boolean; colorBookmarks!: boolean;
@ -192,7 +192,7 @@
this.alwaysNotify = settings.alwaysNotify; this.alwaysNotify = settings.alwaysNotify;
this.logMessages = settings.logMessages; this.logMessages = settings.logMessages;
this.logAds = settings.logAds; this.logAds = settings.logAds;
this.fontSize = settings.fontSize; this.fontSize = settings.fontSize.toString();
this.showNeedsReply = settings.showNeedsReply; this.showNeedsReply = settings.showNeedsReply;
this.enterSend = settings.enterSend; this.enterSend = settings.enterSend;
this.colorBookmarks = settings.colorBookmarks; this.colorBookmarks = settings.colorBookmarks;
@ -215,6 +215,8 @@
} }
async submit(): Promise<void> { async submit(): Promise<void> {
const idleTimer = parseInt(this.idleTimer, 10);
const fontSize = parseInt(this.fontSize, 10);
core.state.settings = { core.state.settings = {
playSound: this.playSound, playSound: this.playSound,
clickOpensMessage: this.clickOpensMessage, clickOpensMessage: this.clickOpensMessage,
@ -224,14 +226,14 @@
highlightWords: this.highlightWords.split(',').map((x) => x.trim()).filter((x) => x.length), highlightWords: this.highlightWords.split(',').map((x) => x.trim()).filter((x) => x.length),
showAvatars: this.showAvatars, showAvatars: this.showAvatars,
animatedEicons: this.animatedEicons, 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, messageSeparators: this.messageSeparators,
eventMessages: this.eventMessages, eventMessages: this.eventMessages,
joinMessages: this.joinMessages, joinMessages: this.joinMessages,
alwaysNotify: this.alwaysNotify, alwaysNotify: this.alwaysNotify,
logMessages: this.logMessages, logMessages: this.logMessages,
logAds: this.logAds, 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, showNeedsReply: this.showNeedsReply,
enterSend: this.enterSend, enterSend: this.enterSend,
colorBookmarks: this.colorBookmarks, colorBookmarks: this.colorBookmarks,

View File

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

View File

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

View File

@ -63,7 +63,7 @@ export namespace Conversation {
export interface ChannelConversation extends TabConversation { export interface ChannelConversation extends TabConversation {
readonly channel: Channel readonly channel: Channel
mode: Channel.Mode mode: Channel.Mode
readonly adCountdown: number readonly nextAd: number
isSendingAds: boolean isSendingAds: boolean
} }
@ -116,7 +116,7 @@ export namespace Conversation {
clear(): void clear(): void
loadLastSent(): void loadLastSent(): void
show(): void show(): void
loadMore(): void loadMore(): boolean
} }
} }
@ -181,7 +181,7 @@ export type Settings = Settings.Settings;
export interface Notifications { export interface Notifications {
isInBackground: boolean isInBackground: boolean
notify(conversation: Conversation, title: string, body: string, icon: string, sound: string): Promise<void> 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> requestPermission(): Promise<void>
initSounds(sounds: ReadonlyArray<string>): 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.selectCharacter': 'Select a character...',
'logs.selectConversation': 'Select a conversation...', 'logs.selectConversation': 'Select a conversation...',
'logs.allDates': 'Show all', '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.profile': 'Profile',
'user.message': 'Open conversation', 'user.message': 'Open conversation',
'user.messageJump': 'View 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.gop.help': 'Promotes a character to global chat OP.',
'commands.gdeop': 'Demote from Chat OP', 'commands.gdeop': 'Demote from Chat OP',
'commands.gdeop.help': 'Demotes a character from global 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': 'Reload config',
'commands.reloadconfig.help': 'Reload server-side config from disk.', 'commands.reloadconfig.help': 'Reload server-side config from disk.',
'commands.reloadconfig.param0': 'Save?', '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.' '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 i = args.length;
let str = strings[key]; let str = strings[key];
if(str === undefined) if(str === undefined)
if(process.env.NODE_ENV !== 'production') throw new Error(`String ${key} does not exist.`); if(process.env.NODE_ENV !== 'production') throw new Error(`String ${key} does not exist.`);
else return ''; else return '';
while(i-- > 0) while(i-- > 0)
str = str.replace(new RegExp(`\\{${i}\\}`, 'igm'), args[i]); str = str.replace(new RegExp(`\\{${i}\\}`, 'igm'), args[i].toString());
return str; 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> { async notify(conversation: Conversation, title: string, body: string, icon: string, sound: string): Promise<void> {
if(!this.shouldNotify(conversation)) return; 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 if(core.state.settings.notifications && (<any>Notification).permission === 'granted') { //tslint:disable-line:no-any
const notification = new Notification(title, this.getOptions(conversation, body, icon)); const notification = new Notification(title, this.getOptions(conversation, body, icon));
notification.onclick = () => { notification.onclick = () => {
conversation.show(); conversation.show();
window.focus(); window.focus();
notification.close(); if('close' in notification) notification.close();
}; };
window.setTimeout(() => { if('close' in notification) window.setTimeout(() => notification.close(), 5000);
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; if(!core.state.settings.playSound) return;
const audio = <HTMLAudioElement>document.getElementById(`soundplayer-${sound}`); const audio = <HTMLAudioElement>document.getElementById(`soundplayer-${sound}`);
audio.volume = 1; audio.volume = 1;
audio.muted = false; 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 = []; const promises = [];
for(const sound of sounds) { for(const sound of sounds) {
const id = `soundplayer-${sound}`; const id = `soundplayer-${sound}`;
if(document.getElementById(id) !== null) continue; if(document.getElementById(id) !== null) continue;
const audio = document.createElement('audio'); const audio = document.createElement('audio');
audio.preload = 'auto';
audio.id = id; audio.id = id;
for(const name in codecs) { for(const name in codecs) {
const src = document.createElement('source'); const src = document.createElement('source');
@ -63,7 +63,7 @@ export default class Notifications implements Interface {
audio.muted = true; audio.muted = true;
const promise = audio.play(); const promise = audio.play();
if(promise instanceof Promise) 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 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, context: CommandContext.Channel,
params: [{type: ParamType.Character}] 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: { oplist: {
exec: (conv: ChannelConversation) => core.connection.send('COL', {channel: conv.channel.id}), exec: (conv: ChannelConversation) => core.connection.send('COL', {channel: conv.channel.id}),
context: CommandContext.Channel context: CommandContext.Channel

View File

@ -1,4 +1,4 @@
import {RavenStatic} from 'raven-js'; import * as Raven from 'raven-js';
import Vue from 'vue'; import Vue from 'vue';
/*tslint:disable:no-unsafe-any no-any*///hack /*tslint:disable:no-unsafe-any no-any*///hack
@ -13,7 +13,7 @@ function formatComponentName(vm: any): string {
//tslint:enable //tslint:enable
/*tslint:disable:no-unbound-method strict-type-predicates*///hack /*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; if(typeof Vue.config !== 'object') return raven;
const oldOnError = Vue.config.errorHandler; const oldOnError = Vue.config.errorHandler;
Vue.config.errorHandler = (error: Error, vm: Vue, info: string): void => { 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; 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[]; let crcTable!: number[];
export default class Zip { export default class Zip {
private blob: (object | string)[] = []; private blob: BlobPart[] = [];
private files: {header: object[], offset: number, name: string}[] = []; private files: {header: BlobPart[], offset: number, name: string}[] = [];
private offset = 0; private offset = 0;
constructor() { constructor() {
@ -19,6 +21,7 @@ export default class Zip {
addFile(name: string, content: string): void { addFile(name: string, content: string): void {
let crc = -1; let crc = -1;
let length = 0; let length = 0;
const nameLength = getByteLength(name);
for(let i = 0, strlen = content.length; i < strlen; ++i) { for(let i = 0, strlen = content.length; i < strlen; ++i) {
let c = content.charCodeAt(i); let c = content.charCodeAt(i);
if(c > 0xD800 && c < 0xD8FF) //surrogate pairs if(c > 0xD800 && c < 0xD8FF) //surrogate pairs
@ -35,13 +38,13 @@ export default class Zip {
} }
crc = (crc ^ (-1)) >>> 0; crc = (crc ^ (-1)) >>> 0;
const file = { 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 offset: this.offset, name
}; };
this.blob.push(Uint32Array.of(0x04034B50)); this.blob.push(Uint32Array.of(0x04034B50));
this.blob.push(...file.header); this.blob.push(...file.header);
this.blob.push(name, content); this.blob.push(name, content);
this.offset += name.length + length + 30; this.offset += nameLength + length + 30;
this.files.push(file); this.files.push(file);
} }
@ -51,7 +54,7 @@ export default class Zip {
this.blob.push(Uint16Array.of(0x4B50, 0x0201, 0)); this.blob.push(Uint16Array.of(0x4B50, 0x0201, 0));
this.blob.push(...file.header); this.blob.push(...file.header);
this.blob.push(Uint16Array.of(0, 0, 0, 0, 0), Uint32Array.of(file.offset), file.name); 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), this.blob.push(Uint16Array.of(0x4B50, 0x0605, 0, 0, this.files.length, this.files.length),
Uint32Array.of(this.offset - start, start), Uint16Array.of(0)); Uint32Array.of(this.offset - start, start), Uint16Array.of(0));

View File

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

View File

@ -81,6 +81,7 @@
l = l; l = l;
hasUpdate = false; hasUpdate = false;
platform = process.platform; platform = process.platform;
lockTab = false;
mounted(): void { mounted(): void {
this.addTab(); this.addTab();
@ -193,26 +194,30 @@
} }
addTab(): void { addTab(): void {
if(this.lockTab) return;
const tray = new electron.remote.Tray(trayIcon); const tray = new electron.remote.Tray(trayIcon);
tray.setToolTip(l('title')); tray.setToolTip(l('title'));
tray.on('click', (_) => this.trayClicked(tab)); tray.on('click', (_) => this.trayClicked(tab));
const view = new electron.remote.BrowserView(); const view = new electron.remote.BrowserView();
view.setAutoResize({width: true, height: true}); 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); electron.ipcRenderer.send('tab-added', view.webContents.id);
const tab = {active: false, view, user: undefined, hasNew: false, tray}; const tab = {active: false, view, user: undefined, hasNew: false, tray};
tray.setContextMenu(electron.remote.Menu.buildFromTemplate(this.createTrayMenu(tab))); tray.setContextMenu(electron.remote.Menu.buildFromTemplate(this.createTrayMenu(tab)));
this.tabs.push(tab); this.tabs.push(tab);
this.tabMap[view.webContents.id] = tab; this.tabMap[view.webContents.id] = tab;
this.show(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 { show(tab: Tab): void {
if(this.lockTab) return;
this.activeTab = tab; this.activeTab = tab;
browserWindow.setBrowserView(tab.view); browserWindow.setBrowserView(tab.view);
tab.view.setBounds(getWindowBounds()); tab.view.setBounds(getWindowBounds());
@ -313,7 +318,7 @@
#window-tabs { #window-tabs {
h4 { h4 {
margin: 0 34px 0 77px; margin: 0 15px 0 77px;
} }
.btn, li a { .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 Axios from 'axios';
import {exec, execSync} from 'child_process'; import {exec, execSync} from 'child_process';
import * as electron from 'electron'; import * as electron from 'electron';
import * as fs from 'fs';
import * as path from 'path'; import * as path from 'path';
import * as qs from 'querystring'; import * as qs from 'querystring';
import * as Raven from 'raven-js';
import Vue from 'vue';
import {getKey} from '../chat/common'; import {getKey} from '../chat/common';
import l from '../chat/localize'; import l from '../chat/localize';
import VueRaven from '../chat/vue-raven'; import {setupRaven} from '../chat/vue-raven';
import {Keys} from '../keys'; import {Keys} from '../keys';
import {GeneralSettings, nativeRequire} from './common'; import {GeneralSettings, nativeRequire} from './common';
import * as SlimcatImporter from './importer'; import * as SlimcatImporter from './importer';
@ -67,21 +64,7 @@ const spellchecker = new sc.Spellchecker();
Axios.defaults.params = { __fchat: `desktop/${electron.remote.app.getVersion()}` }; Axios.defaults.params = { __fchat: `desktop/${electron.remote.app.getVersion()}` };
if(process.env.NODE_ENV === 'production') { if(process.env.NODE_ENV === 'production') {
Raven.config('https://a9239b17b0a14f72ba85e8729b9d1612@sentry.f-list.net/2', { setupRaven('https://a9239b17b0a14f72ba85e8729b9d1612@sentry.f-list.net/2', electron.remote.app.getVersion());
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);
};
electron.remote.getCurrentWebContents().on('devtools-opened', () => { electron.remote.getCurrentWebContents().on('devtools-opened', () => {
console.log(`%c${l('consoleWarning.head')}`, 'background: red; color: yellow; font-size: 30pt'); 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); const corrections = spellchecker.getCorrectionsForMisspelling(props.misspelledWord);
menuTemplate.unshift({ menuTemplate.unshift({
label: l('spellchecker.add'), label: l('spellchecker.add'),
click: () => { click: () => electron.ipcRenderer.send('dictionary-add', props.misspelledWord)
if(customDictionary.indexOf(props.misspelledWord) !== -1) return;
spellchecker.add(props.misspelledWord);
customDictionary.push(props.misspelledWord);
fs.writeFile(customDictionaryPath, JSON.stringify(customDictionary), () => {/**/});
}
}, {type: 'separator'}); }, {type: 'separator'});
if(corrections.length > 0) if(corrections.length > 0)
menuTemplate.unshift(...corrections.map((correction: string) => ({ menuTemplate.unshift(...corrections.map((correction: string) => ({
@ -184,14 +162,10 @@ webContents.on('context-menu', (_, props) => {
click: () => webContents.replaceMisspelling(correction) click: () => webContents.replaceMisspelling(correction)
}))); })));
else menuTemplate.unshift({enabled: false, label: l('spellchecker.noCorrections')}); 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({ menuTemplate.unshift({
label: l('spellchecker.remove'), label: l('spellchecker.remove'),
click: () => { click: () => electron.ipcRenderer.send('dictionary-remove', props.selectionText)
spellchecker.remove(props.selectionText);
customDictionary.splice(customDictionary.indexOf(props.selectionText), 1);
fs.writeFile(customDictionaryPath, JSON.stringify(customDictionary), () => {/**/});
}
}, {type: 'separator'}); }, {type: 'separator'});
if(menuTemplate.length > 0) electron.remote.Menu.buildFromTemplate(menuTemplate).popup({}); 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') if(process.platform === 'win32')
exec(`for /d %I in ("${dictDir}") do @echo %~sI`, (_, stdout) => { dictDir = stdout.trim(); }); exec(`for /d %I in ("${dictDir}") do @echo %~sI`, (_, stdout) => { dictDir = stdout.trim(); });
electron.webFrame.setSpellCheckProvider('', false, {spellCheck: (text) => !spellchecker.isMisspelled(text)}); 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 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) if(params['import'] !== undefined)
try { try {
if(SlimcatImporter.canImportGeneral() && confirm(l('importer.importGeneral'))) { if(SlimcatImporter.canImportGeneral() && confirm(l('importer.importGeneral'))) {
@ -214,11 +192,6 @@ if(params['import'] !== undefined)
} catch { } catch {
alert(l('importer.error')); 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 //tslint:disable-next-line:no-unused-expression
new Index({ new Index({

View File

@ -14,6 +14,7 @@ export class GeneralSettings {
theme = 'default'; theme = 'default';
version = electron.app.getVersion(); version = electron.app.getVersion();
beta = false; beta = false;
customDictionary: string[] = [];
} }
export function mkdir(dir: string): void { 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 filePath = path.join(dictDir, `${lang}.${type}`);
const downloaded = downloadedDictionaries[file.name]; const downloaded = downloadedDictionaries[file.name];
if(downloaded === undefined || downloaded.hash !== file.hash || !fs.existsSync(filePath)) { 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; downloadedDictionaries[file.name] = file;
await writeFile(downloadedPath, JSON.stringify(downloadedDictionaries)); await writeFile(downloadedPath, JSON.stringify(downloadedDictionaries));
} }

View File

@ -1,6 +1,7 @@
import * as electron from 'electron'; import * as electron from 'electron';
import * as fs from 'fs'; import * as fs from 'fs';
import * as path from 'path'; import * as path from 'path';
import {promisify} from 'util';
import {Message as MessageImpl} from '../chat/common'; import {Message as MessageImpl} from '../chat/common';
import core from '../chat/core'; import core from '../chat/core';
import {Character, Conversation, Logs as Logging, Settings} from '../chat/interfaces'; import {Character, Conversation, Logs as Logging, Settings} from '../chat/interfaces';
@ -14,7 +15,7 @@ declare module '../chat/interfaces' {
} }
const dayMs = 86400000; const dayMs = 86400000;
const read = promisify(fs.read);
const noAssert = process.env.NODE_ENV === 'production'; const noAssert = process.env.NODE_ENV === 'production';
function writeFile(p: fs.PathLike | number, data: string | object | number, 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 dir = getLogDir(character);
const files = fs.readdirSync(dir); const files = fs.readdirSync(dir);
const buffer = Buffer.allocUnsafe(50100); const buffer = Buffer.allocUnsafe(50100);
for(const file of files) for(const file of files) {
if(file.substr(-4) !== '.idx') { const full = path.join(dir, file);
const fd = fs.openSync(path.join(dir, file), 'r+'); if(file.substr(-4) === '.idx') {
const indexFd = fs.openSync(path.join(dir, `${file}.idx`), 'r+'); if(!fs.existsSync(full.slice(0, -4))) fs.unlinkSync(full);
fs.readSync(indexFd, buffer, 0, 1, 0); continue;
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);
}
} }
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 { function loadIndex(name: string): Index {
@ -155,19 +164,23 @@ function loadIndex(name: string): Index {
const files = fs.readdirSync(dir); const files = fs.readdirSync(dir);
for(const file of files) for(const file of files)
if(file.substr(-4) === '.idx') { if(file.substr(-4) === '.idx') {
const content = fs.readFileSync(path.join(dir, file)); try {
let offset = content.readUInt8(0, noAssert) + 1; const content = fs.readFileSync(path.join(dir, file));
const item: IndexItem = { let offset = content.readUInt8(0, noAssert) + 1;
name: content.toString('utf8', 1, offset), const item: IndexItem = {
index: {}, name: content.toString('utf8', 1, offset),
offsets: new Array(content.length - offset) index: {},
}; offsets: new Array(content.length - offset)
for(; offset < content.length; offset += 7) { };
const key = content.readUInt16LE(offset); for(; offset < content.length; offset += 7) {
item.index[key] = item.offsets.length; const key = content.readUInt16LE(offset);
item.offsets.push(content.readUIntLE(offset + 2, 5, noAssert)); 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; return index;
} }
@ -190,18 +203,24 @@ export class Logs implements Logging {
let count = 20; let count = 20;
let messages = new Array<Conversation.Message>(count); let messages = new Array<Conversation.Message>(count);
const fd = fs.openSync(file, 'r'); const fd = fs.openSync(file, 'r');
let pos = fs.fstatSync(fd).size; try {
const buffer = Buffer.allocUnsafe(65536); let pos = fs.fstatSync(fd).size;
while(pos > 0 && count > 0) { const buffer = Buffer.allocUnsafe(65536);
fs.readSync(fd, buffer, 0, 2, pos - 2); while(pos > 0 && count > 0) {
const length = buffer.readUInt16LE(0); fs.readSync(fd, buffer, 0, 2, pos - 2);
pos = pos - length - 2; const length = buffer.readUInt16LE(0);
fs.readSync(fd, buffer, 0, length, pos); pos = pos - length - 2;
messages[--count] = deserializeMessage(buffer).message; 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 { private getIndex(name: string): Index {
@ -229,18 +248,25 @@ export class Logs implements Logging {
const messages: Conversation.Message[] = []; const messages: Conversation.Message[] = [];
const pos = index.offsets[dateOffset]; const pos = index.offsets[dateOffset];
const fd = fs.openSync(getLogFile(character, key), 'r'); const fd = fs.openSync(getLogFile(character, key), 'r');
const end = dateOffset + 1 < index.offsets.length ? index.offsets[dateOffset + 1] : (fs.fstatSync(fd)).size; try {
const length = end - pos; const end = dateOffset + 1 < index.offsets.length ? index.offsets[dateOffset + 1] : (fs.fstatSync(fd)).size;
const buffer = Buffer.allocUnsafe(length); const length = end - pos;
fs.readSync(fd, buffer, 0, length, pos); const buffer = Buffer.allocUnsafe(length);
fs.closeSync(fd); await read(fd, buffer, 0, length, pos);
let offset = 0; fs.closeSync(fd);
while(offset < length) { let offset = 0;
const deserialized = deserializeMessage(buffer, offset); while(offset < length) {
messages.push(deserialized.message); const deserialized = deserializeMessage(buffer, offset);
offset += deserialized.size; 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 { logMessage(conversation: {key: string, name: string}, message: Message): void {
@ -262,6 +288,7 @@ export class Logs implements Logging {
async getAvailableCharacters(): Promise<ReadonlyArray<string>> { async getAvailableCharacters(): Promise<ReadonlyArray<string>> {
const baseDir = core.state.generalSettings!.logDirectory; const baseDir = core.state.generalSettings!.logDirectory;
mkdir(baseDir);
return (fs.readdirSync(baseDir)).filter((x) => fs.lstatSync(path.join(baseDir, x)).isDirectory()); 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 * as electron from 'electron';
import log from 'electron-log'; //tslint:disable-line:match-default-export-name import log from 'electron-log'; //tslint:disable-line:match-default-export-name
import {autoUpdater} from 'electron-updater';
import * as fs from 'fs'; import * as fs from 'fs';
import * as path from 'path'; import * as path from 'path';
import * as url from 'url'; import * as url from 'url';
@ -53,12 +52,12 @@ let tabCount = 0;
const baseDir = app.getPath('userData'); const baseDir = app.getPath('userData');
mkdir(baseDir); mkdir(baseDir);
autoUpdater.logger = log; let shouldImportSettings = false;
log.transports.file.level = 'debug';
log.transports.console.level = 'debug'; const settingsDir = path.join(baseDir, 'data');
log.transports.file.maxSize = 5 * 1024 * 1024; mkdir(settingsDir);
log.transports.file.file = path.join(baseDir, 'log.txt'); const settingsFile = path.join(settingsDir, 'settings');
log.info('Starting application.'); const settings = new GeneralSettings();
async function setDictionary(lang: string | undefined): Promise<void> { async function setDictionary(lang: string | undefined): Promise<void> {
if(lang !== undefined) await ensureDictionary(lang); if(lang !== undefined) await ensureDictionary(lang);
@ -66,19 +65,6 @@ async function setDictionary(lang: string | undefined): Promise<void> {
setGeneralSettings(settings); 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 { function setGeneralSettings(value: GeneralSettings): void {
fs.writeFileSync(path.join(settingsDir, 'settings'), JSON.stringify(value)); fs.writeFileSync(path.join(settingsDir, 'settings'), JSON.stringify(value));
for(const w of electron.webContents.getAllWebContents()) w.send('settings', settings); for(const w of electron.webContents.getAllWebContents()) w.send('settings', settings);
@ -150,7 +136,21 @@ function showPatchNotes(): void {
} }
function onReady(): 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); app.on('open-file', createWindow);
if(settings.version !== app.getVersion()) { if(settings.version !== app.getVersion()) {
@ -159,11 +159,12 @@ function onReady(): void {
setGeneralSettings(settings); setGeneralSettings(settings);
} }
const updaterUrl = `https://client.f-list.net/${process.platform}`;
if(process.env.NODE_ENV === 'production') { if(process.env.NODE_ENV === 'production') {
autoUpdater.channel = settings.beta ? 'beta' : 'latest'; electron.autoUpdater.setFeedURL({url: updaterUrl + (settings.beta ? '?channel=beta' : ''), serverType: 'json'});
autoUpdater.checkForUpdates(); //tslint:disable-line:no-floating-promises setTimeout(() => electron.autoUpdater.checkForUpdates(), 10000);
const updateTimer = setInterval(async() => autoUpdater.checkForUpdates(), 3600000); const updateTimer = setInterval(() => electron.autoUpdater.checkForUpdates(), 3600000);
autoUpdater.on('update-downloaded', () => { electron.autoUpdater.on('update-downloaded', () => {
clearInterval(updateTimer); clearInterval(updateTimer);
const menu = electron.Menu.getApplicationMenu()!; const menu = electron.Menu.getApplicationMenu()!;
const item = menu.getMenuItemById('update') as MenuItem | null; const item = menu.getMenuItemById('update') as MenuItem | null;
@ -175,7 +176,7 @@ function onReady(): void {
label: l('action.update'), label: l('action.update'),
click: () => { click: () => {
for(const w of windows) w.webContents.send('quit'); for(const w of windows) w.webContents.send('quit');
autoUpdater.quitAndInstall(false, true); electron.autoUpdater.quitAndInstall();
} }
}, { }, {
label: l('help.changelog'), label: l('help.changelog'),
@ -186,12 +187,12 @@ function onReady(): void {
electron.Menu.setApplicationMenu(menu); electron.Menu.setApplicationMenu(menu);
for(const w of windows) w.webContents.send('update-available', true); for(const w of windows) w.webContents.send('update-available', true);
}); });
autoUpdater.on('update-not-available', () => { electron.autoUpdater.on('update-not-available', () => {
(<any>autoUpdater).downloadedUpdateHelper.clear(); //tslint:disable-line:no-any no-unsafe-any
for(const w of windows) w.webContents.send('update-available', false); for(const w of windows) w.webContents.send('update-available', false);
const item = electron.Menu.getApplicationMenu()!.getMenuItemById('update') as MenuItem | null; const item = electron.Menu.getApplicationMenu()!.getMenuItemById('update') as MenuItem | null;
if(item !== null) item.visible = false; if(item !== null) item.visible = false;
}); });
electron.autoUpdater.on('error', (e) => log.error(e));
} }
const viewItem = { const viewItem = {
@ -275,8 +276,8 @@ function onReady(): void {
click: async(item: Electron.MenuItem) => { click: async(item: Electron.MenuItem) => {
settings.beta = item.checked; settings.beta = item.checked;
setGeneralSettings(settings); setGeneralSettings(settings);
autoUpdater.channel = item.checked ? 'beta' : 'latest'; electron.autoUpdater.setFeedURL({url: updaterUrl + (item.checked ? '?channel=beta' : ''), serverType: 'json'});
return autoUpdater.checkForUpdates(); return electron.autoUpdater.checkForUpdates();
} }
}, { }, {
label: l('fixLogs.action'), label: l('fixLogs.action'),
@ -360,6 +361,15 @@ function onReady(): void {
else characters.push(character); else characters.push(character);
e.returnValue = true; 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)); electron.ipcMain.on('disconnect', (_: Event, character: string) => characters.splice(characters.indexOf(character), 1));
const emptyBadge = electron.nativeImage.createEmpty(); const emptyBadge = electron.nativeImage.createEmpty();
//tslint:disable-next-line:no-require-imports //tslint:disable-next-line:no-require-imports
@ -372,7 +382,7 @@ function onReady(): void {
createWindow(); createWindow();
} }
const running = process.env.NODE_ENV === 'production' && app.makeSingleInstance(createWindow); const isSquirrelStart = require('electron-squirrel-startup'); //tslint:disable-line:no-require-imports
if(running) app.quit(); if(isSquirrelStart || process.env.NODE_ENV === 'production' && app.makeSingleInstance(createWindow)) app.quit();
else app.on('ready', onReady); else app.on('ready', onReady);
app.on('window-all-closed', () => app.quit()); app.on('window-all-closed', () => app.quit());

View File

@ -9,7 +9,7 @@ const browserWindow = remote.getCurrentWindow();
export default class Notifications extends BaseNotifications { export default class Notifications extends BaseNotifications {
async notify(conversation: Conversation, title: string, body: string, icon: string, sound: string): Promise<void> { async notify(conversation: Conversation, title: string, body: string, icon: string, sound: string): Promise<void> {
if(!this.shouldNotify(conversation)) return; if(!this.shouldNotify(conversation)) return;
await this.playSound(sound); this.playSound(sound);
browserWindow.flashFrame(true); browserWindow.flashFrame(true);
if(core.state.settings.notifications) { if(core.state.settings.notifications) {
const notification = new Notification(title, this.getOptions(conversation, body, icon)); 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", "name": "fchat",
"version": "3.0.0", "version": "3.0.7",
"author": "The F-List Team", "author": "The F-List Team",
"description": "F-List.net Chat Client", "description": "F-List.net Chat Client",
"main": "main.js", "main": "main.js",
"license": "MIT", "id": "fchat",
"scripts": { "license": "MIT",
"build": "node ../webpack development", "scripts": {
"build:dist": "node ../webpack production", "build": "node ../webpack development",
"watch": "node ../webpack watch", "build:dist": "node ../webpack production",
"start": "../node_modules/.bin/electron app" "watch": "node ../webpack watch",
}, "start": "../node_modules/.bin/electron app",
"build": { "pack": "node ./pack"
"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/"
}
}
} }

View File

@ -6,7 +6,7 @@ const OptimizeCssAssetsPlugin = require('optimize-css-assets-webpack-plugin');
const VueLoaderPlugin = require('vue-loader/lib/plugin'); const VueLoaderPlugin = require('vue-loader/lib/plugin');
const mainConfig = { 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: { output: {
path: __dirname + '/app', path: __dirname + '/app',
filename: 'main.js' filename: 'main.js'
@ -23,7 +23,7 @@ const mainConfig = {
transpileOnly: true 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]'} {test: /\.(png|html)$/, loader: 'file-loader?name=[name].[ext]'}
] ]
}, },
@ -45,7 +45,7 @@ const mainConfig = {
}, rendererConfig = { }, rendererConfig = {
entry: { entry: {
chat: [path.join(__dirname, 'chat.ts'), path.join(__dirname, 'index.html')], 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: { output: {
path: __dirname + '/app', path: __dirname + '/app',

View File

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

View File

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

View File

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

View File

@ -30,9 +30,7 @@
* @see {@link https://github.com/f-list/exported|GitHub repo} * @see {@link https://github.com/f-list/exported|GitHub repo}
*/ */
import Axios from 'axios'; import Axios from 'axios';
import * as Raven from 'raven-js'; import {setupRaven} from '../chat/vue-raven';
import Vue from 'vue';
import VueRaven from '../chat/vue-raven';
import Index from './Index.vue'; import Index from './Index.vue';
const version = (<{version: string}>require('./package.json')).version; //tslint:disable-line:no-require-imports 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}` }; Axios.defaults.params = { __fchat: `mobile-${platform}/${version}` };
}; };
if(process.env.NODE_ENV === 'production') { if(process.env.NODE_ENV === 'production')
Raven.config('https://a9239b17b0a14f72ba85e8729b9d1612@sentry.f-list.net/2', { setupRaven('https://a9239b17b0a14f72ba85e8729b9d1612@sentry.f-list.net/2', `mobile-${version}`);
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);
};
}
new Index({ //tslint:disable-line:no-unused-expression new Index({ //tslint:disable-line:no-unused-expression
el: '#app' el: '#app'

View File

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

View File

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

View File

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

View File

@ -1,46 +1,54 @@
{ {
"name": "flist-exported", "name": "flist-exported",
"version": "1.0.0", "version": "1.0.0",
"author": "The F-List Team", "author": "The F-List Team",
"description": "F-List Exported", "description": "F-List Exported",
"license": "MIT", "license": "MIT",
"devDependencies": { "devDependencies": {
"@fortawesome/fontawesome-free-webfonts": "^1.0.6", "@fortawesome/fontawesome-free-webfonts": "^1.0.6",
"@types/node": "^10.3.3", "@types/lodash": "^4.14.116",
"@types/sortablejs": "^1.3.31", "@types/node": "^10.5.6",
"axios": "^0.18.0", "@types/sortablejs": "^1.3.31",
"bootstrap": "^4.1.0", "axios": "^0.18.0",
"css-loader": "^0.28.11", "bootstrap": "^4.1.3",
"date-fns": "^1.28.5", "css-loader": "^1.0.0",
"electron": "^2.0.2", "date-fns": "^1.28.5",
"electron-builder": "^20.8.1", "electron": "2.0.2",
"electron-log": "^2.2.9", "electron-log": "^2.2.16",
"electron-updater": "^2.21.4", "electron-packager": "^12.1.0",
"extract-text-webpack-plugin": "4.0.0-beta.0", "electron-rebuild": "^1.8.2",
"file-loader": "^1.1.10", "extract-text-webpack-plugin": "4.0.0-beta.0",
"fork-ts-checker-webpack-plugin": "^0.4.1", "file-loader": "^1.1.10",
"lodash": "^4.16.4", "fork-ts-checker-webpack-plugin": "^0.4.4",
"node-sass": "^4.8.3", "lodash": "^4.16.4",
"optimize-css-assets-webpack-plugin": "^4.0.0", "node-sass": "^4.8.3",
"qs": "^6.5.1", "optimize-css-assets-webpack-plugin": "^5.0.0",
"raven-js": "^3.24.1", "qs": "^6.5.1",
"sass-loader": "^7.0.1", "raven-js": "^3.26.4",
"sortablejs": "^1.6.0", "sass-loader": "^7.1.0",
"ts-loader": "^4.2.0", "sortablejs": "^1.6.0",
"tslib": "^1.7.1", "style-loader": "^0.21.0",
"tslint": "^5.7.0", "ts-loader": "^4.2.0",
"typescript": "^2.8.1", "tslib": "^1.7.1",
"vue": "^2.5.16", "tslint": "^5.7.0",
"vue-class-component": "^6.0.0", "typescript": "^3.0.1",
"vue-loader": "^15.2.4", "vue": "^2.5.17",
"vue-property-decorator": "^6.0.0", "vue-class-component": "^6.0.0",
"vue-template-compiler": "^2.5.16", "vue-loader": "^15.2.6",
"webpack": "^4.5.0" "vue-property-decorator": "^7.0.0",
}, "vue-template-compiler": "^2.5.17",
"dependencies": { "webpack": "^4.16.4"
"@types/lodash": "^4.14.107", },
"keytar": "^4.2.1", "dependencies": {
"spellchecker": "^3.4.3", "keytar": "^4.2.1",
"style-loader": "^0.21.0" "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) - 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. - Change into the `electron` directory.
- Run `yarn build`/`yarn watch` to build assets. They are placed into the `app` 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. - Run `yarn start` to start the app in debug mode. Use `Ctrl+Shift+I` to open the Chromium debugger.
### Packaging ### Packaging
See https://electron.atom.io/docs/tutorial/application-distribution/ See https://electron.atom.io/docs/tutorial/application-distribution/
- Run `yarn build:dist` to create a minified production build. - 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 ## Building for Mobile
- Change into the `mobile` directory. - Change into the `mobile` directory.

View File

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

View File

@ -1,5 +1,5 @@
$blue-color: #06f; $blue-color: #06f;
.blackText { .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: $gray-100;
$text-background-color-disabled: $gray-200; $text-background-color-disabled: $gray-200;
// Dark theme helpers
$theme-is-dark: true;
@import "invert"; @import "invert";

View File

@ -1,9 +1,9 @@
.purpleText { .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 { .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; $blue-color: #06f;

View File

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

View File

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

View File

@ -3,14 +3,14 @@
"@fortawesome/fontawesome-free-webfonts@^1.0.3": "@fortawesome/fontawesome-free-webfonts@^1.0.3":
version "1.0.4" version "1.0.9"
resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-free-webfonts/-/fontawesome-free-webfonts-1.0.4.tgz#bac5d89755bf3bc2d2b4deee47d92febf641bb1f" resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-free-webfonts/-/fontawesome-free-webfonts-1.0.9.tgz#72f2c10453422aba0d338fa6a9cb761b50ba24d5"
abbrev@1: abbrev@1:
version "1.1.1" version "1.1.1"
resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8" 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" version "5.5.2"
resolved "https://registry.yarnpkg.com/ajv/-/ajv-5.5.2.tgz#73b5eeca3fab653e3d3f9422b341ad42205dc965" resolved "https://registry.yarnpkg.com/ajv/-/ajv-5.5.2.tgz#73b5eeca3fab653e3d3f9422b341ad42205dc965"
dependencies: dependencies:
@ -27,6 +27,10 @@ ansi-regex@^2.0.0:
version "2.1.1" version "2.1.1"
resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df" 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: ansi-styles@^2.2.1:
version "2.2.1" version "2.2.1"
resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-2.2.1.tgz#b432dd3358b634cf75e1e4664368240533c1ddbe" 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" resolved "https://registry.yarnpkg.com/aproba/-/aproba-1.2.0.tgz#6802e6264efd18c790a1b0d517f0f2627bf2c94a"
are-we-there-yet@~1.1.2: are-we-there-yet@~1.1.2:
version "1.1.4" version "1.1.5"
resolved "https://registry.yarnpkg.com/are-we-there-yet/-/are-we-there-yet-1.1.4.tgz#bb5dca382bb94f05e15194373d16fd3ba1ca110d" resolved "https://registry.yarnpkg.com/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz#4b35c2944f062a8bfcda66410760350fe9ddfc21"
dependencies: dependencies:
delegates "^1.0.0" delegates "^1.0.0"
readable-stream "^2.0.6" 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" resolved "https://registry.yarnpkg.com/array-find-index/-/array-find-index-1.0.2.tgz#df010aa1287e164bbda6f9723b0a96a1ec4187a1"
asn1@~0.2.3: asn1@~0.2.3:
version "0.2.3" version "0.2.4"
resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.3.tgz#dac8787713c9966849fc8180777ebe9c1ddf3b86" 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: assert-plus@1.0.0, assert-plus@^1.0.0:
version "1.0.0" version "1.0.0"
resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-1.0.0.tgz#f12e0f3c5d77b0b1cdd9146942e4e96c1e4dd525" 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: async-foreach@^0.1.3:
version "0.1.3" version "0.1.3"
resolved "https://registry.yarnpkg.com/async-foreach/-/async-foreach-0.1.3.tgz#36121f845c0578172de419a97dbeb1d16ec34542" 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" version "0.4.0"
resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" 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: aws-sign2@~0.7.0:
version "0.7.0" version "0.7.0"
resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.7.0.tgz#b46e890934a9591f2d2f6f86d7e6a9f1b3fe76a8" resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.7.0.tgz#b46e890934a9591f2d2f6f86d7e6a9f1b3fe76a8"
aws4@^1.2.1, aws4@^1.6.0: aws4@^1.6.0, aws4@^1.8.0:
version "1.6.0" version "1.8.0"
resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.6.0.tgz#83ef5ca860b2b32e4a0deedee8c771b9db57471e" resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.8.0.tgz#f0e003d9ca9e7f59c7a508945d7b2ef9a04a542f"
balanced-match@^1.0.0: balanced-match@^1.0.0:
version "1.0.0" version "1.0.0"
resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767"
bcrypt-pbkdf@^1.0.0: bcrypt-pbkdf@^1.0.0:
version "1.0.1" version "1.0.2"
resolved "https://registry.yarnpkg.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.1.tgz#63bc5dcb61331b92bc05fd528953c33462a06f8d" resolved "https://registry.yarnpkg.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz#a4301d389b6a43f9b67ff3ca11a3f6637e360e9e"
dependencies: dependencies:
tweetnacl "^0.14.3" tweetnacl "^0.14.3"
@ -94,27 +92,9 @@ block-stream@*:
dependencies: dependencies:
inherits "~2.0.0" 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: bootstrap@^4.0.0:
version "4.0.0" version "4.1.3"
resolved "https://registry.yarnpkg.com/bootstrap/-/bootstrap-4.0.0.tgz#ceb03842c145fcc1b9b4e15da2a05656ba68469a" resolved "https://registry.yarnpkg.com/bootstrap/-/bootstrap-4.1.3.tgz#0eb371af2c8448e8c210411d0cb824a6409a12be"
brace-expansion@^1.1.7: brace-expansion@^1.1.7:
version "1.1.11" version "1.1.11"
@ -142,10 +122,6 @@ camelcase@^3.0.0:
version "3.0.0" version "3.0.0"
resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-3.0.0.tgz#32fc4b9fcdaf845fcdf7e73bb97cac2261f0ab0a" 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: caseless@~0.12.0:
version "0.12.0" version "0.12.0"
resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc" 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" version "1.1.0"
resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77" 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" version "1.0.6"
resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.6.tgz#723e7df6e801ac5613113a7e445a9b69cb632818" resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.6.tgz#723e7df6e801ac5613113a7e445a9b69cb632818"
dependencies: dependencies:
delayed-stream "~1.0.0" 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: concat-map@0.0.1:
version "0.0.1" version "0.0.1"
resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" 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" lru-cache "^4.0.1"
which "^1.2.9" 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: currently-unhandled@^0.4.1:
version "0.4.1" version "0.4.1"
resolved "https://registry.yarnpkg.com/currently-unhandled/-/currently-unhandled-0.4.1.tgz#988df33feab191ef799a61369dd76c17adf957ea" 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" resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a"
ecc-jsbn@~0.1.1: ecc-jsbn@~0.1.1:
version "0.1.1" version "0.1.2"
resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.1.tgz#0fc73a9ed5f0d53c38193398523ef7e543777505" resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz#3a83a904e54353287874c564b7549386849a98c9"
dependencies: dependencies:
jsbn "~0.1.0" jsbn "~0.1.0"
safer-buffer "^2.1.0"
error-ex@^1.2.0: error-ex@^1.2.0:
version "1.3.1" version "1.3.2"
resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.1.tgz#f855a86ce61adc4e8621c3cda21e7a7612c3a8dc" resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf"
dependencies: dependencies:
is-arrayish "^0.2.1" is-arrayish "^0.2.1"
@ -257,9 +218,9 @@ escape-string-regexp@^1.0.2:
version "1.0.5" version "1.0.5"
resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4"
extend@~3.0.0, extend@~3.0.1: extend@~3.0.1, extend@~3.0.2:
version "3.0.1" version "3.0.2"
resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.1.tgz#a755ea7bc1adfcc5a31ce7e762dbaadc5e636444" resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa"
extsprintf@1.3.0: extsprintf@1.3.0:
version "1.3.0" version "1.3.0"
@ -288,15 +249,7 @@ forever-agent@~0.6.1:
version "0.6.1" version "0.6.1"
resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91" resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91"
form-data@~2.1.1: form-data@~2.3.1, form-data@~2.3.2:
version "2.1.4"
resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.1.4.tgz#33c183acf193276ecaa98143a69e94bfee1750d1"
dependencies:
asynckit "^0.4.0"
combined-stream "^1.0.5"
mime-types "^2.1.12"
form-data@~2.3.1:
version "2.3.2" version "2.3.2"
resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.3.2.tgz#4970498be604c20c005d4f5c23aecd21d6b49099" resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.3.2.tgz#4970498be604c20c005d4f5c23aecd21d6b49099"
dependencies: dependencies:
@ -331,24 +284,14 @@ gauge@~2.7.3:
wide-align "^1.1.0" wide-align "^1.1.0"
gaze@^1.0.0: gaze@^1.0.0:
version "1.1.2" version "1.1.3"
resolved "https://registry.yarnpkg.com/gaze/-/gaze-1.1.2.tgz#847224677adb8870d679257ed3388fdb61e40105" resolved "https://registry.yarnpkg.com/gaze/-/gaze-1.1.3.tgz#c441733e13b927ac8c0ff0b4c3b033f28812924a"
dependencies: dependencies:
globule "^1.0.0" 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: get-caller-file@^1.0.1:
version "1.0.2" version "1.0.3"
resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-1.0.2.tgz#f702e63127e7e231c160a80c1554acb70d5047e5" resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-1.0.3.tgz#f978fa4c90d1dfe7ff2d6beda2a515e713bdcf4a"
get-stdin@^4.0.1: get-stdin@^4.0.1:
version "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" path-is-absolute "^1.0.0"
globule@^1.0.0: globule@^1.0.0:
version "1.2.0" version "1.2.1"
resolved "https://registry.yarnpkg.com/globule/-/globule-1.2.0.tgz#1dc49c6822dd9e8a2fa00ba2a295006e8664bd09" resolved "https://registry.yarnpkg.com/globule/-/globule-1.2.1.tgz#5dffb1b191f22d20797a9369b49eab4e9839696d"
dependencies: dependencies:
glob "~7.1.1" glob "~7.1.1"
lodash "~4.17.4" lodash "~4.17.10"
minimatch "~3.0.2" minimatch "~3.0.2"
graceful-fs@^4.1.2: graceful-fs@^4.1.2:
@ -397,15 +340,6 @@ har-schema@^2.0.0:
version "2.0.0" version "2.0.0"
resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-2.0.0.tgz#a94c2224ebcac04782a0d9035521f24735b7ec92" 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: har-validator@~5.0.3:
version "5.0.3" version "5.0.3"
resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-5.0.3.tgz#ba402c266194f15956ef15e0fcf242993f6a7dfd" 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" ajv "^5.1.0"
har-schema "^2.0.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: has-ansi@^2.0.0:
version "2.0.0" version "2.0.0"
resolved "https://registry.yarnpkg.com/has-ansi/-/has-ansi-2.0.0.tgz#34f5049ce1ecdf2b0649af3ef24e45ed35416d91" 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" version "2.0.1"
resolved "https://registry.yarnpkg.com/has-unicode/-/has-unicode-2.0.1.tgz#e0e6fe6a28cf51138855e086d1691e771de2a8b9" 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: hosted-git-info@^2.1.4:
version "2.5.0" version "2.7.1"
resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.5.0.tgz#6d60e34b3abbc8313062c3b798ef8d901a07af3c" resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.7.1.tgz#97f236977bd6e125408930ff6de3eec6281ec047"
http-signature@~1.1.0:
version "1.1.1"
resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.1.1.tgz#df72e267066cd0ac67fb76adf8e134a8fbcf91bf"
dependencies:
assert-plus "^0.2.0"
jsprim "^1.2.2"
sshpk "^1.7.0"
http-signature@~1.2.0: http-signature@~1.2.0:
version "1.2.0" version "1.2.0"
@ -516,23 +423,9 @@ is-fullwidth-code-point@^1.0.0:
dependencies: dependencies:
number-is-nan "^1.0.0" number-is-nan "^1.0.0"
is-my-ip-valid@^1.0.0: is-fullwidth-code-point@^2.0.0:
version "1.0.0" version "2.0.0"
resolved "https://registry.yarnpkg.com/is-my-ip-valid/-/is-my-ip-valid-1.0.0.tgz#7b351b8e8edd4d3995d4d066680e664d94696824" resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz#a3b30a5c4f199183167aaab93beefae3ddfb654f"
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-typedarray@~1.0.0: is-typedarray@~1.0.0:
version "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" resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a"
js-base64@^2.1.8: js-base64@^2.1.8:
version "2.4.3" version "2.4.8"
resolved "https://registry.yarnpkg.com/js-base64/-/js-base64-2.4.3.tgz#2e545ec2b0f2957f41356510205214e98fad6582" resolved "https://registry.yarnpkg.com/js-base64/-/js-base64-2.4.8.tgz#57a9b130888f956834aa40c5b165ba59c758f033"
jsbn@~0.1.0: jsbn@~0.1.0:
version "0.1.1" version "0.1.1"
@ -574,10 +467,6 @@ json-stringify-safe@~5.0.1:
version "5.0.1" version "5.0.1"
resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb" 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: jsprim@^1.2.2:
version "1.4.1" version "1.4.1"
resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.1.tgz#313e66bc1e5cc06e438bc1b7499c2e5c56acb6a2" 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" version "4.6.1"
resolved "https://registry.yarnpkg.com/lodash.mergewith/-/lodash.mergewith-4.6.1.tgz#639057e726c3afbdb3e7d42741caa8d6e4335927" resolved "https://registry.yarnpkg.com/lodash.mergewith/-/lodash.mergewith-4.6.1.tgz#639057e726c3afbdb3e7d42741caa8d6e4335927"
lodash@^4.0.0, lodash@~4.17.4: lodash@^4.0.0, lodash@~4.17.10:
version "4.17.5" version "4.17.10"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.5.tgz#99a92d65c0272debe8c96b6057bc8fbfa3bed511" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.10.tgz#1b7793cf7259ea38fb3661d4d38b3260af8ae4e7"
loud-rejection@^1.0.0: loud-rejection@^1.0.0:
version "1.6.0" version "1.6.0"
@ -627,8 +516,8 @@ loud-rejection@^1.0.0:
signal-exit "^3.0.0" signal-exit "^3.0.0"
lru-cache@^4.0.1: lru-cache@^4.0.1:
version "4.1.1" version "4.1.3"
resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-4.1.1.tgz#622e32e82488b49279114a4f9ecf45e7cd6bba55" resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-4.1.3.tgz#a1175cf3496dfc8436c156c334b4955992bce69c"
dependencies: dependencies:
pseudomap "^1.0.2" pseudomap "^1.0.2"
yallist "^2.1.2" yallist "^2.1.2"
@ -652,17 +541,17 @@ meow@^3.7.0:
redent "^1.0.0" redent "^1.0.0"
trim-newlines "^1.0.0" trim-newlines "^1.0.0"
mime-db@~1.33.0: mime-db@~1.35.0:
version "1.33.0" version "1.35.0"
resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.33.0.tgz#a3492050a5cb9b63450541e39d9788d2272783db" 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: mime-types@^2.1.12, mime-types@~2.1.17, mime-types@~2.1.19:
version "2.1.18" version "2.1.19"
resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.18.tgz#6f323f60a83d11146f831ff11fd66e2fe5503bb8" resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.19.tgz#71e464537a7ef81c15f2db9d97e913fc0ff606f0"
dependencies: 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" version "3.0.4"
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083"
dependencies: dependencies:
@ -682,31 +571,30 @@ minimist@^1.1.3:
dependencies: dependencies:
minimist "0.0.8" minimist "0.0.8"
nan@^2.3.2: nan@^2.10.0:
version "2.9.2" version "2.10.0"
resolved "https://registry.yarnpkg.com/nan/-/nan-2.9.2.tgz#f564d75f5f8f36a6d9456cca7a6c4fe488ab7866" resolved "https://registry.yarnpkg.com/nan/-/nan-2.10.0.tgz#96d0cd610ebd58d4b4de9cc0c6828cda99c7548f"
node-gyp@^3.3.1: node-gyp@^3.8.0:
version "3.6.2" version "3.8.0"
resolved "https://registry.yarnpkg.com/node-gyp/-/node-gyp-3.6.2.tgz#9bfbe54562286284838e750eac05295853fa1c60" resolved "https://registry.yarnpkg.com/node-gyp/-/node-gyp-3.8.0.tgz#540304261c330e80d0d5edce253a68cb3964218c"
dependencies: dependencies:
fstream "^1.0.0" fstream "^1.0.0"
glob "^7.0.3" glob "^7.0.3"
graceful-fs "^4.1.2" graceful-fs "^4.1.2"
minimatch "^3.0.2"
mkdirp "^0.5.0" mkdirp "^0.5.0"
nopt "2 || 3" nopt "2 || 3"
npmlog "0 || 1 || 2 || 3 || 4" npmlog "0 || 1 || 2 || 3 || 4"
osenv "0" osenv "0"
request "2" request "^2.87.0"
rimraf "2" rimraf "2"
semver "~5.3.0" semver "~5.3.0"
tar "^2.0.0" tar "^2.0.0"
which "1" which "1"
node-sass@^4.7.2: node-sass@^4.7.2:
version "4.7.2" version "4.9.3"
resolved "https://registry.yarnpkg.com/node-sass/-/node-sass-4.7.2.tgz#9366778ba1469eb01438a9e8592f4262bcb6794e" resolved "https://registry.yarnpkg.com/node-sass/-/node-sass-4.9.3.tgz#f407cf3d66f78308bb1e346b24fa428703196224"
dependencies: dependencies:
async-foreach "^0.1.3" async-foreach "^0.1.3"
chalk "^1.1.1" chalk "^1.1.1"
@ -720,10 +608,10 @@ node-sass@^4.7.2:
lodash.mergewith "^4.6.0" lodash.mergewith "^4.6.0"
meow "^3.7.0" meow "^3.7.0"
mkdirp "^0.5.1" mkdirp "^0.5.1"
nan "^2.3.2" nan "^2.10.0"
node-gyp "^3.3.1" node-gyp "^3.8.0"
npmlog "^4.0.0" npmlog "^4.0.0"
request "~2.79.0" request "2.87.0"
sass-graph "^2.2.4" sass-graph "^2.2.4"
stdout-stream "^1.4.0" stdout-stream "^1.4.0"
"true-case-path" "^1.0.2" "true-case-path" "^1.0.2"
@ -756,10 +644,14 @@ number-is-nan@^1.0.0:
version "1.0.1" version "1.0.1"
resolved "https://registry.yarnpkg.com/number-is-nan/-/number-is-nan-1.0.1.tgz#097b602b53422a522c1afb8790318336941a011d" 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" version "0.8.2"
resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.8.2.tgz#46a6ab7f0aead8deae9ec0565780b7d4efeb9d43" 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: object-assign@^4.0.1, object-assign@^4.1.0:
version "4.1.1" version "4.1.1"
resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" 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" version "1.0.2"
resolved "https://registry.yarnpkg.com/pseudomap/-/pseudomap-1.0.2.tgz#f052a28da70e618917ef0a8ac34c1ae5a68286b3" 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: punycode@^1.4.1:
version "1.4.1" version "1.4.1"
resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e" resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e"
qs@~6.3.0: qs@~6.5.1, qs@~6.5.2:
version "6.3.2" version "6.5.2"
resolved "https://registry.yarnpkg.com/qs/-/qs-6.3.2.tgz#e75bd5f6e268122a2a0e0bda630b2550c166502c" resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36"
qs@~6.5.1:
version "6.5.1"
resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.1.tgz#349cdf6eef89ec45c12d7d5eb3fc0c870343a6d8"
read-pkg-up@^1.0.1: read-pkg-up@^1.0.1:
version "1.0.1" version "1.0.1"
@ -869,15 +761,15 @@ read-pkg@^1.0.0:
path-type "^1.0.0" path-type "^1.0.0"
readable-stream@^2.0.1, readable-stream@^2.0.6: readable-stream@^2.0.1, readable-stream@^2.0.6:
version "2.3.5" version "2.3.6"
resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.5.tgz#b4f85003a938cbb6ecbce2a124fb1012bd1a838d" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.6.tgz#b11c27d88b8ff1fbe070643cf94b0c79ae1b0aaf"
dependencies: dependencies:
core-util-is "~1.0.0" core-util-is "~1.0.0"
inherits "~2.0.3" inherits "~2.0.3"
isarray "~1.0.0" isarray "~1.0.0"
process-nextick-args "~2.0.0" process-nextick-args "~2.0.0"
safe-buffer "~5.1.1" safe-buffer "~5.1.1"
string_decoder "~1.0.3" string_decoder "~1.1.1"
util-deprecate "~1.0.1" util-deprecate "~1.0.1"
redent@^1.0.0: redent@^1.0.0:
@ -893,9 +785,9 @@ repeating@^2.0.0:
dependencies: dependencies:
is-finite "^1.0.0" is-finite "^1.0.0"
request@2: request@2.87.0:
version "2.83.0" version "2.87.0"
resolved "https://registry.yarnpkg.com/request/-/request-2.83.0.tgz#ca0b65da02ed62935887808e6f510381034e3356" resolved "https://registry.yarnpkg.com/request/-/request-2.87.0.tgz#32f00235cd08d482b4d0d68db93a829c0ed5756e"
dependencies: dependencies:
aws-sign2 "~0.7.0" aws-sign2 "~0.7.0"
aws4 "^1.6.0" aws4 "^1.6.0"
@ -905,7 +797,6 @@ request@2:
forever-agent "~0.6.1" forever-agent "~0.6.1"
form-data "~2.3.1" form-data "~2.3.1"
har-validator "~5.0.3" har-validator "~5.0.3"
hawk "~6.0.2"
http-signature "~1.2.0" http-signature "~1.2.0"
is-typedarray "~1.0.0" is-typedarray "~1.0.0"
isstream "~0.1.2" isstream "~0.1.2"
@ -915,35 +806,34 @@ request@2:
performance-now "^2.1.0" performance-now "^2.1.0"
qs "~6.5.1" qs "~6.5.1"
safe-buffer "^5.1.1" safe-buffer "^5.1.1"
stringstream "~0.0.5"
tough-cookie "~2.3.3" tough-cookie "~2.3.3"
tunnel-agent "^0.6.0" tunnel-agent "^0.6.0"
uuid "^3.1.0" uuid "^3.1.0"
request@~2.79.0: request@^2.87.0:
version "2.79.0" version "2.88.0"
resolved "https://registry.yarnpkg.com/request/-/request-2.79.0.tgz#4dfe5bf6be8b8cdc37fcf93e04b65577722710de" resolved "https://registry.yarnpkg.com/request/-/request-2.88.0.tgz#9c2fca4f7d35b592efe57c7f0a55e81052124fef"
dependencies: dependencies:
aws-sign2 "~0.6.0" aws-sign2 "~0.7.0"
aws4 "^1.2.1" aws4 "^1.8.0"
caseless "~0.11.0" caseless "~0.12.0"
combined-stream "~1.0.5" combined-stream "~1.0.6"
extend "~3.0.0" extend "~3.0.2"
forever-agent "~0.6.1" forever-agent "~0.6.1"
form-data "~2.1.1" form-data "~2.3.2"
har-validator "~2.0.6" har-validator "~5.1.0"
hawk "~3.1.3" http-signature "~1.2.0"
http-signature "~1.1.0"
is-typedarray "~1.0.0" is-typedarray "~1.0.0"
isstream "~0.1.2" isstream "~0.1.2"
json-stringify-safe "~5.0.1" json-stringify-safe "~5.0.1"
mime-types "~2.1.7" mime-types "~2.1.19"
oauth-sign "~0.8.1" oauth-sign "~0.9.0"
qs "~6.3.0" performance-now "^2.1.0"
stringstream "~0.0.4" qs "~6.5.2"
tough-cookie "~2.3.0" safe-buffer "^5.1.2"
tunnel-agent "~0.4.1" tough-cookie "~2.4.3"
uuid "^3.0.0" tunnel-agent "^0.6.0"
uuid "^3.3.2"
require-directory@^2.1.1: require-directory@^2.1.1:
version "2.1.1" version "2.1.1"
@ -959,9 +849,13 @@ rimraf@2:
dependencies: dependencies:
glob "^7.0.5" glob "^7.0.5"
safe-buffer@^5.0.1, safe-buffer@^5.1.1, safe-buffer@~5.1.0, safe-buffer@~5.1.1: 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.1" version "5.1.2"
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.1.tgz#893312af69b2123def71f57889001671eeb2c853" 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: sass-graph@^2.2.4:
version "2.2.4" version "2.2.4"
@ -995,18 +889,6 @@ signal-exit@^3.0.0:
version "3.0.2" version "3.0.2"
resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.2.tgz#b5fdc08f1287ea1178628e415e25132b73646c6d" 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: source-map@^0.4.2:
version "0.4.4" version "0.4.4"
resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.4.4.tgz#eba4f5da9c0dc999de68032d8b4f76173652036b" 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" resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-3.0.0.tgz#7a7cd28470cc6d3a1cfe6d66886f6bc430d3ac87"
sshpk@^1.7.0: sshpk@^1.7.0:
version "1.13.1" version "1.14.2"
resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.13.1.tgz#512df6da6287144316dc4c18fe1cf1d940739be3" resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.14.2.tgz#c6fc61648a3d9c4e764fd3fcdf4ea105e492ba98"
dependencies: dependencies:
asn1 "~0.2.3" asn1 "~0.2.3"
assert-plus "^1.0.0" assert-plus "^1.0.0"
dashdash "^1.12.0" dashdash "^1.12.0"
getpass "^0.1.1" getpass "^0.1.1"
safer-buffer "^2.0.2"
optionalDependencies: optionalDependencies:
bcrypt-pbkdf "^1.0.0" bcrypt-pbkdf "^1.0.0"
ecc-jsbn "~0.1.1" 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" is-fullwidth-code-point "^1.0.0"
strip-ansi "^3.0.0" strip-ansi "^3.0.0"
string_decoder@~1.0.3: "string-width@^1.0.2 || 2":
version "1.0.3" version "2.1.1"
resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.0.3.tgz#0fc67d7c141825de94282dd536bec6b9bce860ab" 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: dependencies:
safe-buffer "~5.1.0" 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: strip-ansi@^3.0.0, strip-ansi@^3.0.1:
version "3.0.1" version "3.0.1"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-3.0.1.tgz#6a385fb8853d952d5ff05d0e8aaf94278dc63dcf" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-3.0.1.tgz#6a385fb8853d952d5ff05d0e8aaf94278dc63dcf"
dependencies: dependencies:
ansi-regex "^2.0.0" 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: strip-bom@^2.0.0:
version "2.0.0" version "2.0.0"
resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-2.0.0.tgz#6219a85616520491f35788bdbf1447a99c7e6b0e" 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" fstream "^1.0.2"
inherits "2" inherits "2"
tough-cookie@~2.3.0, tough-cookie@~2.3.3: tough-cookie@~2.3.3:
version "2.3.4" version "2.3.4"
resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.3.4.tgz#ec60cee38ac675063ffc97a5c18970578ee83655" resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.3.4.tgz#ec60cee38ac675063ffc97a5c18970578ee83655"
dependencies: dependencies:
punycode "^1.4.1" 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: trim-newlines@^1.0.0:
version "1.0.0" version "1.0.0"
resolved "https://registry.yarnpkg.com/trim-newlines/-/trim-newlines-1.0.0.tgz#5887966bb582a4503a41eb524f7d35011815a613" resolved "https://registry.yarnpkg.com/trim-newlines/-/trim-newlines-1.0.0.tgz#5887966bb582a4503a41eb524f7d35011815a613"
@ -1125,10 +1024,6 @@ tunnel-agent@^0.6.0:
dependencies: dependencies:
safe-buffer "^5.0.1" 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: tweetnacl@^0.14.3, tweetnacl@~0.14.0:
version "0.14.5" version "0.14.5"
resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64" 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" version "1.0.2"
resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
uuid@^3.0.0, uuid@^3.1.0: uuid@^3.1.0, uuid@^3.3.2:
version "3.2.1" version "3.3.2"
resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.2.1.tgz#12c528bb9d58d0b9265d9a2f6f0fe8be17ff1f14" resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.3.2.tgz#1b4af4955eb3077c501c23872fc6513811587131"
validate-npm-package-license@^3.0.1: validate-npm-package-license@^3.0.1:
version "3.0.3" version "3.0.4"
resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.3.tgz#81643bcbef1bdfecd4623793dc4648948ba98338" resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz#fc91f6b9c7ba15c857f4cb2c5defeec39d4f410a"
dependencies: dependencies:
spdx-correct "^3.0.0" spdx-correct "^3.0.0"
spdx-expression-parse "^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" resolved "https://registry.yarnpkg.com/which-module/-/which-module-1.0.0.tgz#bba63ca861948994ff307736089e3b96026c2a4f"
which@1, which@^1.2.9: which@1, which@^1.2.9:
version "1.3.0" version "1.3.1"
resolved "https://registry.yarnpkg.com/which/-/which-1.3.0.tgz#ff04bdfc010ee547d780bec38e1ac1c2777d253a" resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a"
dependencies: dependencies:
isexe "^2.0.0" isexe "^2.0.0"
wide-align@^1.1.0: wide-align@^1.1.0:
version "1.1.2" version "1.1.3"
resolved "https://registry.yarnpkg.com/wide-align/-/wide-align-1.1.2.tgz#571e0f1b0604636ebc0dfc21b0339bbe31341710" resolved "https://registry.yarnpkg.com/wide-align/-/wide-align-1.1.3.tgz#ae074e6bdc0c14a431e804e624549c633b000457"
dependencies: dependencies:
string-width "^1.0.2" string-width "^1.0.2 || 2"
wrap-ansi@^2.0.0: wrap-ansi@^2.0.0:
version "2.1.0" version "2.1.0"
@ -1183,10 +1078,6 @@ wrappy@1:
version "1.0.2" version "1.0.2"
resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" 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: y18n@^3.2.1:
version "3.2.1" version "3.2.1"
resolved "https://registry.yarnpkg.com/y18n/-/y18n-3.2.1.tgz#6d15fba884c08679c0d77e88e7759e811e07fa41" resolved "https://registry.yarnpkg.com/y18n/-/y18n-3.2.1.tgz#6d15fba884c08679c0d77e88e7759e811e07fa41"

View File

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

View File

@ -30,12 +30,10 @@
* @see {@link https://github.com/f-list/exported|GitHub repo} * @see {@link https://github.com/f-list/exported|GitHub repo}
*/ */
import Axios from 'axios'; import Axios from 'axios';
import * as Raven from 'raven-js';
import Vue from 'vue';
import Chat from '../chat/Chat.vue'; import Chat from '../chat/Chat.vue';
import {init as initCore} from '../chat/core'; import {init as initCore} from '../chat/core';
import l from '../chat/localize'; import l from '../chat/localize';
import VueRaven from '../chat/vue-raven'; import {setupRaven} from '../chat/vue-raven';
import Socket from '../chat/WebSocket'; import Socket from '../chat/WebSocket';
import Connection from '../fchat/connection'; import Connection from '../fchat/connection';
import '../scss/fa.scss'; //tslint:disable-line:no-import-side-effect 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 const version = (<{version: string}>require('./package.json')).version; //tslint:disable-line:no-require-imports
Axios.defaults.params = { __fchat: `web/${version}` }; Axios.defaults.params = { __fchat: `web/${version}` };
if(process.env.NODE_ENV === 'production') { if(process.env.NODE_ENV === 'production')
Raven.config('https://a9239b17b0a14f72ba85e8729b9d1612@sentry.f-list.net/2', { setupRaven('https://a9239b17b0a14f72ba85e8729b9d1612@sentry.f-list.net/2', `web-${version}`);
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);
};
}
declare const chatSettings: {account: string, theme: string, characters: ReadonlyArray<string>, defaultCharacter: string | null}; 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> { async notify(conversation: Conversation, title: string, body: string, icon: string, sound: string): Promise<void> {
if(!this.shouldNotify(conversation)) return; if(!this.shouldNotify(conversation)) return;
try { try {
return super.notify(conversation, title, body, icon, sound); await super.notify(conversation, title, body, icon, sound);
} catch { } catch {
(async() => { //tslint:disable-line:no-floating-promises //tslint:disable-next-line:no-require-imports no-submodule-imports
//tslint:disable-next-line:no-require-imports no-submodule-imports await navigator.serviceWorker.register(<string>require('file-loader!./sw.js'));
await navigator.serviceWorker.register(<string>require('file-loader!./sw.js')); const reg = await navigator.serviceWorker.ready;
const reg = await navigator.serviceWorker.ready; await reg.showNotification(title, this.getOptions(conversation, body, icon));
await reg.showNotification(title, this.getOptions(conversation, body, icon)); navigator.serviceWorker.onmessage = (e) => {
navigator.serviceWorker.onmessage = (e) => { const conv = core.conversations.byKey((<{key: string}>e.data).key);
const conv = core.conversations.byKey((<{key: string}>e.data).key); if(conv !== undefined) conv.show();
if(conv !== undefined) conv.show(); window.focus();
window.focus(); };
};
})();
} }
} }
} }

View File

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

2500
yarn.lock

File diff suppressed because it is too large Load Diff