3.0.7 - electron-builder removed
This commit is contained in:
		
							parent
							
								
									4d8f6c3670
								
							
						
					
					
						commit
						7151bf916e
					
				@ -124,7 +124,7 @@
 | 
			
		||||
            core.register('conversations', Conversations());
 | 
			
		||||
            core.connection.onEvent('closed', async(isReconnect) => {
 | 
			
		||||
                if(isReconnect) (<Modal>this.$refs['reconnecting']).show(true);
 | 
			
		||||
                if(this.connected) await core.notifications.playSound('logout');
 | 
			
		||||
                if(this.connected) core.notifications.playSound('logout');
 | 
			
		||||
                this.connected = false;
 | 
			
		||||
                this.connecting = false;
 | 
			
		||||
                document.title = l('title');
 | 
			
		||||
@ -138,7 +138,7 @@
 | 
			
		||||
                this.error = '';
 | 
			
		||||
                this.connecting = false;
 | 
			
		||||
                this.connected = true;
 | 
			
		||||
                await core.notifications.playSound('login');
 | 
			
		||||
                core.notifications.playSound('login');
 | 
			
		||||
                document.title = l('title.connected', core.connection.character);
 | 
			
		||||
            });
 | 
			
		||||
            core.watch(() => core.conversations.hasNew, (hasNew) => {
 | 
			
		||||
 | 
			
		||||
@ -4,7 +4,7 @@
 | 
			
		||||
        @touchend="$refs['userMenu'].handleEvent($event)">
 | 
			
		||||
        <sidebar id="sidebar" :label="l('chat.menu')" icon="fa-bars">
 | 
			
		||||
            <img :src="characterImage(ownCharacter.name)" v-if="showAvatars" style="float:left;margin-right:5px;width:60px"/>
 | 
			
		||||
            <a href="#" target="_blank" :href="ownCharacterLink" class="btn" style="margin-right:5px">{{ownCharacter.name}}</a>
 | 
			
		||||
            <a target="_blank" :href="ownCharacterLink" class="btn" style="margin-right:5px">{{ownCharacter.name}}</a>
 | 
			
		||||
            <a href="#" @click.prevent="logOut" class="btn"><i class="fas fa-sign-out-alt"></i>{{l('chat.logout')}}</a><br/>
 | 
			
		||||
            <div>
 | 
			
		||||
                {{l('chat.status')}}
 | 
			
		||||
 | 
			
		||||
@ -26,7 +26,7 @@
 | 
			
		||||
                    <span v-show="conversation.channel.id.substr(0, 4) !== 'adh-'" class="fa fa-star" :title="l('channel.official')"
 | 
			
		||||
                        style="margin-right:5px;vertical-align:sub"></span>
 | 
			
		||||
                    <h5 style="margin:0;display:inline;vertical-align:middle">{{conversation.name}}</h5>
 | 
			
		||||
                    <a @click="descriptionExpanded = !descriptionExpanded" class="btn">
 | 
			
		||||
                    <a href="#" @click.prevent="descriptionExpanded = !descriptionExpanded" class="btn">
 | 
			
		||||
                        <span class="fa" :class="{'fa-chevron-down': !descriptionExpanded, 'fa-chevron-up': descriptionExpanded}"></span>
 | 
			
		||||
                        <span class="btn-text">{{l('channel.description')}}</span>
 | 
			
		||||
                    </a>
 | 
			
		||||
@ -69,13 +69,13 @@
 | 
			
		||||
            <a class="btn btn-sm btn-light" style="position:absolute;right:5px;top:50%;transform:translateY(-50%);line-height:0;z-index:10"
 | 
			
		||||
                @click="hideSearch"><i class="fas fa-times"></i></a>
 | 
			
		||||
        </div>
 | 
			
		||||
        <div class="border-top messages" :class="'messages-' + conversation.mode" style="flex:1;overflow:auto;margin-top:2px"
 | 
			
		||||
            ref="messages" @scroll="onMessagesScroll">
 | 
			
		||||
        <div class="border-top messages" :class="'messages-' + conversation.mode" ref="messages" @scroll="onMessagesScroll"
 | 
			
		||||
            style="flex:1;overflow:auto;margin-top:2px;position:relative">
 | 
			
		||||
            <template v-for="message in messages">
 | 
			
		||||
                <message-view :message="message" :channel="conversation.channel" :key="message.id"
 | 
			
		||||
                    :classes="message == conversation.lastRead ? 'last-read' : ''">
 | 
			
		||||
                </message-view>
 | 
			
		||||
                <span v-if="message.sfc && message.sfc.action == 'report'" :key="message.id">
 | 
			
		||||
                <span v-if="message.sfc && message.sfc.action == 'report'" :key="'r' + message.id">
 | 
			
		||||
                    <a :href="'https://www.f-list.net/fchat/getLog.php?log=' + message.sfc.logid"
 | 
			
		||||
                        v-if="message.sfc.logid" target="_blank">{{l('events.report.viewLog')}}</a>
 | 
			
		||||
                    <span v-else>{{l('events.report.noLog')}}</span>
 | 
			
		||||
@ -174,6 +174,8 @@
 | 
			
		||||
        keypressHandler!: EventListener;
 | 
			
		||||
        scrolledDown = true;
 | 
			
		||||
        scrolledUp = false;
 | 
			
		||||
        adCountdown = 0;
 | 
			
		||||
        adsMode = l('channel.mode.ads');
 | 
			
		||||
 | 
			
		||||
        mounted(): void {
 | 
			
		||||
            this.extraButtons = [{
 | 
			
		||||
@ -203,6 +205,21 @@
 | 
			
		||||
                    this.search = this.searchInput;
 | 
			
		||||
            }, 500);
 | 
			
		||||
            this.messageView = <HTMLElement>this.$refs['messages'];
 | 
			
		||||
            this.$watch('conversation.nextAd', (value: number) => {
 | 
			
		||||
                const setAdCountdown = () => {
 | 
			
		||||
                    const diff = ((<Conversation.ChannelConversation>this.conversation).nextAd - Date.now()) / 1000;
 | 
			
		||||
                    if(diff <= 0) {
 | 
			
		||||
                        if(this.adCountdown !== 0) window.clearInterval(this.adCountdown);
 | 
			
		||||
                        this.adCountdown = 0;
 | 
			
		||||
                        this.adsMode = l('channel.mode.ads');
 | 
			
		||||
                    } else this.adsMode = l('channel.mode.ads.countdown', Math.floor(diff / 60), Math.floor(diff % 60));
 | 
			
		||||
                };
 | 
			
		||||
                if(Date.now() < value) {
 | 
			
		||||
                    if(this.adCountdown === 0)
 | 
			
		||||
                        this.adCountdown = window.setInterval(setAdCountdown, 1000);
 | 
			
		||||
                    setAdCountdown();
 | 
			
		||||
                }
 | 
			
		||||
            });
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        destroyed(): void {
 | 
			
		||||
@ -252,9 +269,13 @@
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        onMessagesScroll(): void {
 | 
			
		||||
            if(this.messageView.scrollTop < 50 && !this.scrolledUp) {
 | 
			
		||||
            if(this.messageView.scrollTop < 20) {
 | 
			
		||||
                if(!this.scrolledUp) {
 | 
			
		||||
                    const firstMessage = this.messageView.firstElementChild;
 | 
			
		||||
                    if(this.conversation.loadMore() && firstMessage !== null)
 | 
			
		||||
                        this.$nextTick(() => setTimeout(() => this.messageView.scrollTop = (<HTMLElement>firstMessage).offsetTop, 0));
 | 
			
		||||
                }
 | 
			
		||||
                this.scrolledUp = true;
 | 
			
		||||
                this.conversation.loadMore();
 | 
			
		||||
            } else this.scrolledUp = false;
 | 
			
		||||
            this.scrolledDown = this.messageView.scrollTop + this.messageView.offsetHeight >= this.messageView.scrollHeight - 15;
 | 
			
		||||
        }
 | 
			
		||||
@ -313,7 +334,7 @@
 | 
			
		||||
                else if(getKey(e) === Keys.Enter) {
 | 
			
		||||
                    if(e.shiftKey === this.settings.enterSend) return;
 | 
			
		||||
                    e.preventDefault();
 | 
			
		||||
                    await this.conversation.send();
 | 
			
		||||
                    setImmediate(async() => this.conversation.send());
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
@ -335,13 +356,6 @@
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        get adsMode(): string | undefined {
 | 
			
		||||
            if(!Conversation.isChannel(this.conversation)) return;
 | 
			
		||||
            if(this.conversation.adCountdown <= 0) return l('channel.mode.ads');
 | 
			
		||||
            else return l('channel.mode.ads.countdown',
 | 
			
		||||
                Math.floor(this.conversation.adCountdown / 60).toString(), (this.conversation.adCountdown % 60).toString());
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        get characterImage(): string {
 | 
			
		||||
            return characterImage(this.conversation.name);
 | 
			
		||||
        }
 | 
			
		||||
@ -390,9 +404,9 @@
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .chat-info-text {
 | 
			
		||||
        display:flex;
 | 
			
		||||
        align-items:center;
 | 
			
		||||
        flex:1 51%;
 | 
			
		||||
        display: flex;
 | 
			
		||||
        align-items: center;
 | 
			
		||||
        flex: 1 51%;
 | 
			
		||||
        @media (max-width: breakpoint-max(xs)) {
 | 
			
		||||
            flex-basis: 100%;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
@ -246,9 +246,11 @@
 | 
			
		||||
                this.dates[this.dateOffset++]);
 | 
			
		||||
            this.messages = messages.concat(this.messages);
 | 
			
		||||
            const noOverflow = list.offsetHeight === list.scrollHeight;
 | 
			
		||||
            const firstMessage = <HTMLElement>list.firstElementChild!;
 | 
			
		||||
            this.$nextTick(() => {
 | 
			
		||||
                if(list.offsetHeight === list.scrollHeight) return this.onMessagesScroll();
 | 
			
		||||
                else if(noOverflow) list.scrollTop = list.scrollHeight;
 | 
			
		||||
                else if(noOverflow) setTimeout(() => list.scrollTop = list.scrollHeight, 0);
 | 
			
		||||
                else setTimeout(() => list.scrollTop = firstMessage.offsetTop, 0);
 | 
			
		||||
            });
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -39,7 +39,7 @@
 | 
			
		||||
            </div>
 | 
			
		||||
            <div class="form-group">
 | 
			
		||||
                <label class="control-label" for="idleTimer">{{l('settings.idleTimer')}}</label>
 | 
			
		||||
                <input id="idleTimer" class="form-control" type="number" v-model="idleTimer"/>
 | 
			
		||||
                <input id="idleTimer" class="form-control" type="number" v-model="idleTimer" min="0" max="1440"/>
 | 
			
		||||
            </div>
 | 
			
		||||
            <div class="form-group">
 | 
			
		||||
                <label class="control-label" for="messageSeparators">
 | 
			
		||||
@ -160,7 +160,7 @@
 | 
			
		||||
        alwaysNotify!: boolean;
 | 
			
		||||
        logMessages!: boolean;
 | 
			
		||||
        logAds!: boolean;
 | 
			
		||||
        fontSize!: number;
 | 
			
		||||
        fontSize!: string;
 | 
			
		||||
        showNeedsReply!: boolean;
 | 
			
		||||
        enterSend!: boolean;
 | 
			
		||||
        colorBookmarks!: boolean;
 | 
			
		||||
@ -192,7 +192,7 @@
 | 
			
		||||
            this.alwaysNotify = settings.alwaysNotify;
 | 
			
		||||
            this.logMessages = settings.logMessages;
 | 
			
		||||
            this.logAds = settings.logAds;
 | 
			
		||||
            this.fontSize = settings.fontSize;
 | 
			
		||||
            this.fontSize = settings.fontSize.toString();
 | 
			
		||||
            this.showNeedsReply = settings.showNeedsReply;
 | 
			
		||||
            this.enterSend = settings.enterSend;
 | 
			
		||||
            this.colorBookmarks = settings.colorBookmarks;
 | 
			
		||||
@ -215,6 +215,8 @@
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        async submit(): Promise<void> {
 | 
			
		||||
            const idleTimer = parseInt(this.idleTimer, 10);
 | 
			
		||||
            const fontSize = parseInt(this.fontSize, 10);
 | 
			
		||||
            core.state.settings = {
 | 
			
		||||
                playSound: this.playSound,
 | 
			
		||||
                clickOpensMessage: this.clickOpensMessage,
 | 
			
		||||
@ -224,14 +226,14 @@
 | 
			
		||||
                highlightWords: this.highlightWords.split(',').map((x) => x.trim()).filter((x) => x.length),
 | 
			
		||||
                showAvatars: this.showAvatars,
 | 
			
		||||
                animatedEicons: this.animatedEicons,
 | 
			
		||||
                idleTimer: this.idleTimer.length > 0 ? parseInt(this.idleTimer, 10) : 0,
 | 
			
		||||
                idleTimer: isNaN(idleTimer) ? 0 : idleTimer < 0 ? 0 : idleTimer > 1440 ? 1440 : idleTimer,
 | 
			
		||||
                messageSeparators: this.messageSeparators,
 | 
			
		||||
                eventMessages: this.eventMessages,
 | 
			
		||||
                joinMessages: this.joinMessages,
 | 
			
		||||
                alwaysNotify: this.alwaysNotify,
 | 
			
		||||
                logMessages: this.logMessages,
 | 
			
		||||
                logAds: this.logAds,
 | 
			
		||||
                fontSize: isNaN(this.fontSize) ? 14 : this.fontSize < 10 ? 10 : this.fontSize > 24 ? 24 : this.fontSize,
 | 
			
		||||
                fontSize: isNaN(fontSize) ? 14 : fontSize < 10 ? 10 : fontSize > 24 ? 24 : fontSize,
 | 
			
		||||
                showNeedsReply: this.showNeedsReply,
 | 
			
		||||
                enterSend: this.enterSend,
 | 
			
		||||
                colorBookmarks: this.colorBookmarks,
 | 
			
		||||
 | 
			
		||||
@ -3,7 +3,6 @@ import {WebSocketConnection} from '../fchat';
 | 
			
		||||
export default class Socket implements WebSocketConnection {
 | 
			
		||||
    static host = 'wss://chat.f-list.net:9799';
 | 
			
		||||
    private socket: WebSocket;
 | 
			
		||||
    private errorHandler: ((error: Error) => void) | undefined;
 | 
			
		||||
    private lastHandler: Promise<void> = Promise.resolve();
 | 
			
		||||
 | 
			
		||||
    constructor() {
 | 
			
		||||
@ -16,7 +15,10 @@ export default class Socket implements WebSocketConnection {
 | 
			
		||||
 | 
			
		||||
    onMessage(handler: (message: string) => void): void {
 | 
			
		||||
        this.socket.addEventListener('message', (e) => {
 | 
			
		||||
            this.lastHandler = this.lastHandler.then(() => handler(<string>e.data), this.errorHandler);
 | 
			
		||||
            this.lastHandler = this.lastHandler.then(() => handler(<string>e.data), (err) => {
 | 
			
		||||
                window.requestAnimationFrame(() => { throw err; });
 | 
			
		||||
                handler(<string>e.data);
 | 
			
		||||
            });
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -29,7 +31,6 @@ export default class Socket implements WebSocketConnection {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    onError(handler: (error: Error) => void): void {
 | 
			
		||||
        this.errorHandler = handler;
 | 
			
		||||
        this.socket.addEventListener('error', () => handler(new Error()));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -81,10 +81,11 @@ abstract class Conversation implements Interfaces.Conversation {
 | 
			
		||||
        this.enteredText = this.lastSent;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    loadMore(): void {
 | 
			
		||||
        if(this.messages.length >= this.allMessages.length) return;
 | 
			
		||||
    loadMore(): boolean {
 | 
			
		||||
        if(this.messages.length >= this.allMessages.length) return false;
 | 
			
		||||
        this.maxMessages += 50;
 | 
			
		||||
        this.messages = this.allMessages.slice(-this.maxMessages);
 | 
			
		||||
        return true;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    show(): void {
 | 
			
		||||
@ -198,7 +199,7 @@ class ChannelConversation extends Conversation implements Interfaces.ChannelConv
 | 
			
		||||
    readonly context = CommandContext.Channel;
 | 
			
		||||
    readonly name = this.channel.name;
 | 
			
		||||
    isSendingAds = this.channel.mode === 'ads';
 | 
			
		||||
    adCountdown = 0;
 | 
			
		||||
    nextAd = 0;
 | 
			
		||||
    private chat: Interfaces.Message[] = [];
 | 
			
		||||
    private ads: Interfaces.Message[] = [];
 | 
			
		||||
    private both: Interfaces.Message[] = [];
 | 
			
		||||
@ -284,6 +285,13 @@ class ChannelConversation extends Conversation implements Interfaces.ChannelConv
 | 
			
		||||
        this.addModeMessage('both', message);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    clear(): void {
 | 
			
		||||
        this.messages = [];
 | 
			
		||||
        this.chat.length = 0;
 | 
			
		||||
        this.ads.length = 0;
 | 
			
		||||
        this.both.length = 0;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    close(): void {
 | 
			
		||||
        core.connection.send('LCH', {channel: this.channel.id});
 | 
			
		||||
    }
 | 
			
		||||
@ -296,17 +304,13 @@ class ChannelConversation extends Conversation implements Interfaces.ChannelConv
 | 
			
		||||
 | 
			
		||||
    protected async doSend(): Promise<void> {
 | 
			
		||||
        const isAd = this.isSendingAds;
 | 
			
		||||
        if(isAd && this.adCountdown > 0) return;
 | 
			
		||||
        if(isAd && Date.now() < this.nextAd) return;
 | 
			
		||||
        core.connection.send(isAd ? 'LRP' : 'MSG', {channel: this.channel.id, message: this.enteredText});
 | 
			
		||||
        await this.addMessage(
 | 
			
		||||
            createMessage(isAd ? MessageType.Ad : MessageType.Message, core.characters.ownCharacter, this.enteredText, new Date()));
 | 
			
		||||
        if(isAd) {
 | 
			
		||||
            this.adCountdown = core.connection.vars.lfrp_flood;
 | 
			
		||||
            const interval = setInterval(() => {
 | 
			
		||||
                this.adCountdown -= 1;
 | 
			
		||||
                if(this.adCountdown === 0) clearInterval(interval);
 | 
			
		||||
            }, 1000);
 | 
			
		||||
        } else this.enteredText = '';
 | 
			
		||||
        if(isAd)
 | 
			
		||||
            this.nextAd = Date.now() + core.connection.vars.lfrp_flood * 1000;
 | 
			
		||||
        else this.enteredText = '';
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -63,7 +63,7 @@ export namespace Conversation {
 | 
			
		||||
    export interface ChannelConversation extends TabConversation {
 | 
			
		||||
        readonly channel: Channel
 | 
			
		||||
        mode: Channel.Mode
 | 
			
		||||
        readonly adCountdown: number
 | 
			
		||||
        readonly nextAd: number
 | 
			
		||||
        isSendingAds: boolean
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -116,7 +116,7 @@ export namespace Conversation {
 | 
			
		||||
        clear(): void
 | 
			
		||||
        loadLastSent(): void
 | 
			
		||||
        show(): void
 | 
			
		||||
        loadMore(): void
 | 
			
		||||
        loadMore(): boolean
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -181,7 +181,7 @@ export type Settings = Settings.Settings;
 | 
			
		||||
export interface Notifications {
 | 
			
		||||
    isInBackground: boolean
 | 
			
		||||
    notify(conversation: Conversation, title: string, body: string, icon: string, sound: string): Promise<void>
 | 
			
		||||
    playSound(sound: string): Promise<void>
 | 
			
		||||
    playSound(sound: string): void
 | 
			
		||||
    requestPermission(): Promise<void>
 | 
			
		||||
    initSounds(sounds: ReadonlyArray<string>): Promise<void>
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -87,6 +87,10 @@ const strings: {[key: string]: string | undefined} = {
 | 
			
		||||
    'logs.selectCharacter': 'Select a character...',
 | 
			
		||||
    'logs.selectConversation': 'Select a conversation...',
 | 
			
		||||
    'logs.allDates': 'Show all',
 | 
			
		||||
    'logs.corruption.desktop': 'Log corruption has been detected. This is usually caused by a crash/force close or power loss mid-write. Please use the "Fix corrupted logs" option for this character to restore proper functionality.',
 | 
			
		||||
    'logs.corruption.mobile': 'Log corruption has been detected. This is usually caused by a crash/force close or power loss mid-write. Will now attempt to fix corrupted logs.',
 | 
			
		||||
    'logs.corruption.mobile.success': 'Your logs have been fixed.',
 | 
			
		||||
    'logs.corruption.mobile.error': 'Unable to fix corrupted logs. Please clear the application data or reinstall the app.',
 | 
			
		||||
    'user.profile': 'Profile',
 | 
			
		||||
    'user.message': 'Open conversation',
 | 
			
		||||
    'user.messageJump': 'View conversation',
 | 
			
		||||
@ -384,6 +388,10 @@ Once this process has started, do not interrupt it or your logs will get corrupt
 | 
			
		||||
    'commands.gop.help': 'Promotes a character to global chat OP.',
 | 
			
		||||
    'commands.gdeop': 'Demote from Chat OP',
 | 
			
		||||
    'commands.gdeop.help': 'Demotes a character from global chat OP.',
 | 
			
		||||
    'commands.scop': 'Promote to Super COP',
 | 
			
		||||
    'commands.scop.help': 'Promotes a character to super channel operator, making them an operator in all public channels.',
 | 
			
		||||
    'commands.scdeop': 'Demote from Super COP',
 | 
			
		||||
    'commands.scdeop.help': 'Demotes a character from super channel operator.',
 | 
			
		||||
    'commands.reloadconfig': 'Reload config',
 | 
			
		||||
    'commands.reloadconfig.help': 'Reload server-side config from disk.',
 | 
			
		||||
    'commands.reloadconfig.param0': 'Save?',
 | 
			
		||||
@ -412,13 +420,13 @@ Any existing FChat 3.0 data for this character will be overwritten.`,
 | 
			
		||||
    'importer.error': 'There was an error importing your settings. The defaults will be used.'
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default function l(key: string, ...args: string[]): string {
 | 
			
		||||
export default function l(key: string, ...args: (string | number)[]): string {
 | 
			
		||||
    let i = args.length;
 | 
			
		||||
    let str = strings[key];
 | 
			
		||||
    if(str === undefined)
 | 
			
		||||
        if(process.env.NODE_ENV !== 'production') throw new Error(`String ${key} does not exist.`);
 | 
			
		||||
        else return '';
 | 
			
		||||
    while(i-- > 0)
 | 
			
		||||
        str = str.replace(new RegExp(`\\{${i}\\}`, 'igm'), args[i]);
 | 
			
		||||
        str = str.replace(new RegExp(`\\{${i}\\}`, 'igm'), args[i].toString());
 | 
			
		||||
    return str;
 | 
			
		||||
}
 | 
			
		||||
@ -13,17 +13,15 @@ export default class Notifications implements Interface {
 | 
			
		||||
 | 
			
		||||
    async notify(conversation: Conversation, title: string, body: string, icon: string, sound: string): Promise<void> {
 | 
			
		||||
        if(!this.shouldNotify(conversation)) return;
 | 
			
		||||
        await this.playSound(sound);
 | 
			
		||||
        this.playSound(sound);
 | 
			
		||||
        if(core.state.settings.notifications && (<any>Notification).permission === 'granted') { //tslint:disable-line:no-any
 | 
			
		||||
            const notification = new Notification(title, this.getOptions(conversation, body, icon));
 | 
			
		||||
            notification.onclick = () => {
 | 
			
		||||
                conversation.show();
 | 
			
		||||
                window.focus();
 | 
			
		||||
                notification.close();
 | 
			
		||||
                if('close' in notification) notification.close();
 | 
			
		||||
            };
 | 
			
		||||
            window.setTimeout(() => {
 | 
			
		||||
                notification.close();
 | 
			
		||||
            }, 5000);
 | 
			
		||||
            if('close' in notification) window.setTimeout(() => notification.close(), 5000);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -36,20 +34,22 @@ export default class Notifications implements Interface {
 | 
			
		||||
        };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async playSound(sound: string): Promise<void> {
 | 
			
		||||
    playSound(sound: string): void {
 | 
			
		||||
        if(!core.state.settings.playSound) return;
 | 
			
		||||
        const audio = <HTMLAudioElement>document.getElementById(`soundplayer-${sound}`);
 | 
			
		||||
        audio.volume = 1;
 | 
			
		||||
        audio.muted = false;
 | 
			
		||||
        return audio.play();
 | 
			
		||||
        const promise = audio.play();
 | 
			
		||||
        if(promise instanceof Promise) promise.catch((e) => console.error(e));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    initSounds(sounds: ReadonlyArray<string>): Promise<void> { //tslint:disable-line:promise-function-async
 | 
			
		||||
    async initSounds(sounds: ReadonlyArray<string>): Promise<void> {
 | 
			
		||||
        const promises = [];
 | 
			
		||||
        for(const sound of sounds) {
 | 
			
		||||
            const id = `soundplayer-${sound}`;
 | 
			
		||||
            if(document.getElementById(id) !== null) continue;
 | 
			
		||||
            const audio = document.createElement('audio');
 | 
			
		||||
            audio.preload = 'auto';
 | 
			
		||||
            audio.id = id;
 | 
			
		||||
            for(const name in codecs) {
 | 
			
		||||
                const src = document.createElement('source');
 | 
			
		||||
@ -63,7 +63,7 @@ export default class Notifications implements Interface {
 | 
			
		||||
            audio.muted = true;
 | 
			
		||||
            const promise = audio.play();
 | 
			
		||||
            if(promise instanceof Promise)
 | 
			
		||||
                promises.push(promise);
 | 
			
		||||
                promises.push(promise.catch((e) => console.error(e)));
 | 
			
		||||
        }
 | 
			
		||||
        return <any>Promise.all(promises); //tslint:disable-line:no-any
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -269,6 +269,16 @@ const commands: {readonly [key: string]: Command | undefined} = {
 | 
			
		||||
        context: CommandContext.Channel,
 | 
			
		||||
        params: [{type: ParamType.Character}]
 | 
			
		||||
    },
 | 
			
		||||
    scop: {
 | 
			
		||||
        exec: (_, character: string) => core.connection.send('SCP', {action: 'add', character}),
 | 
			
		||||
        permission: Permission.Admin,
 | 
			
		||||
        params: [{type: ParamType.Character}]
 | 
			
		||||
    },
 | 
			
		||||
    scdeop: {
 | 
			
		||||
        exec: (_, character: string) => core.connection.send('SCP', {action: 'remove', character}),
 | 
			
		||||
        permission: Permission.Admin,
 | 
			
		||||
        params: [{type: ParamType.Character}]
 | 
			
		||||
    },
 | 
			
		||||
    oplist: {
 | 
			
		||||
        exec: (conv: ChannelConversation) => core.connection.send('COL', {channel: conv.channel.id}),
 | 
			
		||||
        context: CommandContext.Channel
 | 
			
		||||
 | 
			
		||||
@ -1,4 +1,4 @@
 | 
			
		||||
import {RavenStatic} from 'raven-js';
 | 
			
		||||
import * as Raven from 'raven-js';
 | 
			
		||||
import Vue from 'vue';
 | 
			
		||||
 | 
			
		||||
/*tslint:disable:no-unsafe-any no-any*///hack
 | 
			
		||||
@ -13,7 +13,7 @@ function formatComponentName(vm: any): string {
 | 
			
		||||
//tslint:enable
 | 
			
		||||
 | 
			
		||||
/*tslint:disable:no-unbound-method strict-type-predicates*///hack
 | 
			
		||||
export default function VueRaven(this: void, raven: RavenStatic): RavenStatic {
 | 
			
		||||
function VueRaven(this: void, raven: Raven.RavenStatic): Raven.RavenStatic {
 | 
			
		||||
    if(typeof Vue.config !== 'object') return raven;
 | 
			
		||||
    const oldOnError = Vue.config.errorHandler;
 | 
			
		||||
    Vue.config.errorHandler = (error: Error, vm: Vue, info: string): void => {
 | 
			
		||||
@ -44,4 +44,27 @@ export default function VueRaven(this: void, raven: RavenStatic): RavenStatic {
 | 
			
		||||
 | 
			
		||||
    return raven;
 | 
			
		||||
}
 | 
			
		||||
//tslint:enable
 | 
			
		||||
//tslint:enable
 | 
			
		||||
 | 
			
		||||
export function setupRaven(dsn: string, version: string): void {
 | 
			
		||||
    Raven.config(dsn, {
 | 
			
		||||
        release: version,
 | 
			
		||||
        dataCallback: (data: {culprit?: string, exception?: {values: {stacktrace: {frames: {filename: string}[]}}[]}}) => {
 | 
			
		||||
            if(data.culprit !== undefined) {
 | 
			
		||||
                const end = data.culprit.lastIndexOf('?');
 | 
			
		||||
                data.culprit = `~${data.culprit.substring(data.culprit.lastIndexOf('/'), end === -1 ? undefined : end)}`;
 | 
			
		||||
            }
 | 
			
		||||
            if(data.exception !== undefined)
 | 
			
		||||
                for(const ex of data.exception.values)
 | 
			
		||||
                    for(const frame of ex.stacktrace.frames) {
 | 
			
		||||
                        const index = frame.filename.lastIndexOf('/');
 | 
			
		||||
                        const endIndex = frame.filename.lastIndexOf('?');
 | 
			
		||||
                        frame.filename =
 | 
			
		||||
                            `~${frame.filename.substring(index !== -1 ? index : 0, endIndex === -1 ? undefined : endIndex)}`;
 | 
			
		||||
                    }
 | 
			
		||||
        }
 | 
			
		||||
    }).addPlugin(VueRaven, Vue).install();
 | 
			
		||||
    (<Window & {onunhandledrejection(e: PromiseRejectionEvent): void}>window).onunhandledrejection = (e: PromiseRejectionEvent) => {
 | 
			
		||||
        Raven.captureException(<Error>e.reason);
 | 
			
		||||
    };
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										13
									
								
								chat/zip.ts
									
									
									
									
									
								
							
							
						
						
									
										13
									
								
								chat/zip.ts
									
									
									
									
									
								
							@ -1,8 +1,10 @@
 | 
			
		||||
import {getByteLength} from './common';
 | 
			
		||||
 | 
			
		||||
let crcTable!: number[];
 | 
			
		||||
 | 
			
		||||
export default class Zip {
 | 
			
		||||
    private blob: (object | string)[] = [];
 | 
			
		||||
    private files: {header: object[], offset: number, name: string}[] = [];
 | 
			
		||||
    private blob: BlobPart[] = [];
 | 
			
		||||
    private files: {header: BlobPart[], offset: number, name: string}[] = [];
 | 
			
		||||
    private offset = 0;
 | 
			
		||||
 | 
			
		||||
    constructor() {
 | 
			
		||||
@ -19,6 +21,7 @@ export default class Zip {
 | 
			
		||||
    addFile(name: string, content: string): void {
 | 
			
		||||
        let crc = -1;
 | 
			
		||||
        let length = 0;
 | 
			
		||||
        const nameLength = getByteLength(name);
 | 
			
		||||
        for(let i = 0, strlen = content.length; i < strlen; ++i) {
 | 
			
		||||
            let c = content.charCodeAt(i);
 | 
			
		||||
            if(c > 0xD800 && c < 0xD8FF) //surrogate pairs
 | 
			
		||||
@ -35,13 +38,13 @@ export default class Zip {
 | 
			
		||||
        }
 | 
			
		||||
        crc = (crc ^ (-1)) >>> 0;
 | 
			
		||||
        const file = {
 | 
			
		||||
            header: [Uint16Array.of(0, 0, 0, 0, 0), Uint32Array.of(crc, length, length), Uint16Array.of(name.length, 0)],
 | 
			
		||||
            header: [Uint16Array.of(0, 0, 0, 0, 0), Uint32Array.of(crc, length, length), Uint16Array.of(nameLength, 0)],
 | 
			
		||||
            offset: this.offset, name
 | 
			
		||||
        };
 | 
			
		||||
        this.blob.push(Uint32Array.of(0x04034B50));
 | 
			
		||||
        this.blob.push(...file.header);
 | 
			
		||||
        this.blob.push(name, content);
 | 
			
		||||
        this.offset += name.length + length + 30;
 | 
			
		||||
        this.offset += nameLength + length + 30;
 | 
			
		||||
        this.files.push(file);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -51,7 +54,7 @@ export default class Zip {
 | 
			
		||||
            this.blob.push(Uint16Array.of(0x4B50, 0x0201, 0));
 | 
			
		||||
            this.blob.push(...file.header);
 | 
			
		||||
            this.blob.push(Uint16Array.of(0, 0, 0, 0, 0), Uint32Array.of(file.offset), file.name);
 | 
			
		||||
            this.offset += file.name.length + 46;
 | 
			
		||||
            this.offset += getByteLength(file.name) + 46;
 | 
			
		||||
        }
 | 
			
		||||
        this.blob.push(Uint16Array.of(0x4B50, 0x0605, 0, 0, this.files.length, this.files.length),
 | 
			
		||||
            Uint32Array.of(this.offset - start, start), Uint16Array.of(0));
 | 
			
		||||
 | 
			
		||||
@ -266,4 +266,9 @@
 | 
			
		||||
    html, body, #page {
 | 
			
		||||
        height: 100%;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    *:not([draggable]), *::after, *::before {
 | 
			
		||||
        -webkit-user-drag: none;
 | 
			
		||||
        -webkit-app-region: no-drag;
 | 
			
		||||
    }
 | 
			
		||||
</style>
 | 
			
		||||
@ -81,6 +81,7 @@
 | 
			
		||||
        l = l;
 | 
			
		||||
        hasUpdate = false;
 | 
			
		||||
        platform = process.platform;
 | 
			
		||||
        lockTab = false;
 | 
			
		||||
 | 
			
		||||
        mounted(): void {
 | 
			
		||||
            this.addTab();
 | 
			
		||||
@ -193,26 +194,30 @@
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        addTab(): void {
 | 
			
		||||
            if(this.lockTab) return;
 | 
			
		||||
            const tray = new electron.remote.Tray(trayIcon);
 | 
			
		||||
            tray.setToolTip(l('title'));
 | 
			
		||||
            tray.on('click', (_) => this.trayClicked(tab));
 | 
			
		||||
            const view = new electron.remote.BrowserView();
 | 
			
		||||
            view.setAutoResize({width: true, height: true});
 | 
			
		||||
            view.webContents.loadURL(url.format({
 | 
			
		||||
                pathname: path.join(__dirname, 'index.html'),
 | 
			
		||||
                protocol: 'file:',
 | 
			
		||||
                slashes: true,
 | 
			
		||||
                query: {settings: JSON.stringify(this.settings)}
 | 
			
		||||
            }));
 | 
			
		||||
            electron.ipcRenderer.send('tab-added', view.webContents.id);
 | 
			
		||||
            const tab = {active: false, view, user: undefined, hasNew: false, tray};
 | 
			
		||||
            tray.setContextMenu(electron.remote.Menu.buildFromTemplate(this.createTrayMenu(tab)));
 | 
			
		||||
            this.tabs.push(tab);
 | 
			
		||||
            this.tabMap[view.webContents.id] = tab;
 | 
			
		||||
            this.show(tab);
 | 
			
		||||
            this.lockTab = true;
 | 
			
		||||
            view.webContents.loadURL(url.format({
 | 
			
		||||
                pathname: path.join(__dirname, 'index.html'),
 | 
			
		||||
                protocol: 'file:',
 | 
			
		||||
                slashes: true,
 | 
			
		||||
                query: {settings: JSON.stringify(this.settings)}
 | 
			
		||||
            }));
 | 
			
		||||
            view.webContents.on('did-stop-loading', () => this.lockTab = false);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        show(tab: Tab): void {
 | 
			
		||||
            if(this.lockTab) return;
 | 
			
		||||
            this.activeTab = tab;
 | 
			
		||||
            browserWindow.setBrowserView(tab.view);
 | 
			
		||||
            tab.view.setBounds(getWindowBounds());
 | 
			
		||||
@ -313,7 +318,7 @@
 | 
			
		||||
 | 
			
		||||
        #window-tabs {
 | 
			
		||||
            h4 {
 | 
			
		||||
                margin: 0 34px 0 77px;
 | 
			
		||||
                margin: 0 15px 0 77px;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            .btn, li a {
 | 
			
		||||
 | 
			
		||||
@ -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
									
								
							
							
						
						
									
										
											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
									
								
							
							
						
						
									
										
											BIN
										
									
								
								electron/build/dmg@2x.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| 
		 After Width: | Height: | Size: 267 KiB  | 
@ -32,14 +32,11 @@
 | 
			
		||||
import Axios from 'axios';
 | 
			
		||||
import {exec, execSync} from 'child_process';
 | 
			
		||||
import * as electron from 'electron';
 | 
			
		||||
import * as fs from 'fs';
 | 
			
		||||
import * as path from 'path';
 | 
			
		||||
import * as qs from 'querystring';
 | 
			
		||||
import * as Raven from 'raven-js';
 | 
			
		||||
import Vue from 'vue';
 | 
			
		||||
import {getKey} from '../chat/common';
 | 
			
		||||
import l from '../chat/localize';
 | 
			
		||||
import VueRaven from '../chat/vue-raven';
 | 
			
		||||
import {setupRaven} from '../chat/vue-raven';
 | 
			
		||||
import {Keys} from '../keys';
 | 
			
		||||
import {GeneralSettings, nativeRequire} from './common';
 | 
			
		||||
import * as SlimcatImporter from './importer';
 | 
			
		||||
@ -67,21 +64,7 @@ const spellchecker = new sc.Spellchecker();
 | 
			
		||||
Axios.defaults.params = { __fchat: `desktop/${electron.remote.app.getVersion()}` };
 | 
			
		||||
 | 
			
		||||
if(process.env.NODE_ENV === 'production') {
 | 
			
		||||
    Raven.config('https://a9239b17b0a14f72ba85e8729b9d1612@sentry.f-list.net/2', {
 | 
			
		||||
        release: electron.remote.app.getVersion(),
 | 
			
		||||
        dataCallback(data: {culprit: string, exception?: {values: {stacktrace: {frames: {filename: string}[]}}[]}}): void {
 | 
			
		||||
            data.culprit = `~${data.culprit.substr(data.culprit.lastIndexOf('/'))}`;
 | 
			
		||||
            if(data.exception !== undefined)
 | 
			
		||||
                for(const ex of data.exception.values)
 | 
			
		||||
                    for(const frame of ex.stacktrace.frames) {
 | 
			
		||||
                        const index = frame.filename.lastIndexOf('/');
 | 
			
		||||
                        frame.filename = index !== -1 ? `~${frame.filename.substr(index)}` : frame.filename;
 | 
			
		||||
                    }
 | 
			
		||||
        }
 | 
			
		||||
    }).addPlugin(VueRaven, Vue).install();
 | 
			
		||||
    (<Window & {onunhandledrejection(e: PromiseRejectionEvent): void}>window).onunhandledrejection = (e: PromiseRejectionEvent) => {
 | 
			
		||||
        Raven.captureException(<Error>e.reason);
 | 
			
		||||
    };
 | 
			
		||||
    setupRaven('https://a9239b17b0a14f72ba85e8729b9d1612@sentry.f-list.net/2', electron.remote.app.getVersion());
 | 
			
		||||
 | 
			
		||||
    electron.remote.getCurrentWebContents().on('devtools-opened', () => {
 | 
			
		||||
        console.log(`%c${l('consoleWarning.head')}`, 'background: red; color: yellow; font-size: 30pt');
 | 
			
		||||
@ -171,12 +154,7 @@ webContents.on('context-menu', (_, props) => {
 | 
			
		||||
        const corrections = spellchecker.getCorrectionsForMisspelling(props.misspelledWord);
 | 
			
		||||
        menuTemplate.unshift({
 | 
			
		||||
            label: l('spellchecker.add'),
 | 
			
		||||
            click: () => {
 | 
			
		||||
                if(customDictionary.indexOf(props.misspelledWord) !== -1) return;
 | 
			
		||||
                spellchecker.add(props.misspelledWord);
 | 
			
		||||
                customDictionary.push(props.misspelledWord);
 | 
			
		||||
                fs.writeFile(customDictionaryPath, JSON.stringify(customDictionary), () => {/**/});
 | 
			
		||||
            }
 | 
			
		||||
            click: () => electron.ipcRenderer.send('dictionary-add', props.misspelledWord)
 | 
			
		||||
        }, {type: 'separator'});
 | 
			
		||||
        if(corrections.length > 0)
 | 
			
		||||
            menuTemplate.unshift(...corrections.map((correction: string) => ({
 | 
			
		||||
@ -184,14 +162,10 @@ webContents.on('context-menu', (_, props) => {
 | 
			
		||||
                click: () => webContents.replaceMisspelling(correction)
 | 
			
		||||
            })));
 | 
			
		||||
        else menuTemplate.unshift({enabled: false, label: l('spellchecker.noCorrections')});
 | 
			
		||||
    } else if(customDictionary.indexOf(props.selectionText) !== -1)
 | 
			
		||||
    } else if(settings.customDictionary.indexOf(props.selectionText) !== -1)
 | 
			
		||||
        menuTemplate.unshift({
 | 
			
		||||
            label: l('spellchecker.remove'),
 | 
			
		||||
            click: () => {
 | 
			
		||||
                spellchecker.remove(props.selectionText);
 | 
			
		||||
                customDictionary.splice(customDictionary.indexOf(props.selectionText), 1);
 | 
			
		||||
                fs.writeFile(customDictionaryPath, JSON.stringify(customDictionary), () => {/**/});
 | 
			
		||||
            }
 | 
			
		||||
            click: () => electron.ipcRenderer.send('dictionary-remove', props.selectionText)
 | 
			
		||||
        }, {type: 'separator'});
 | 
			
		||||
 | 
			
		||||
    if(menuTemplate.length > 0) electron.remote.Menu.buildFromTemplate(menuTemplate).popup({});
 | 
			
		||||
@ -201,10 +175,14 @@ let dictDir = path.join(electron.remote.app.getPath('userData'), 'spellchecker')
 | 
			
		||||
if(process.platform === 'win32')
 | 
			
		||||
   exec(`for /d %I in ("${dictDir}") do @echo %~sI`, (_, stdout) => { dictDir = stdout.trim(); });
 | 
			
		||||
electron.webFrame.setSpellCheckProvider('', false, {spellCheck: (text) => !spellchecker.isMisspelled(text)});
 | 
			
		||||
electron.ipcRenderer.on('settings', async(_: Event, s: GeneralSettings) => spellchecker.setDictionary(s.spellcheckLang, dictDir));
 | 
			
		||||
electron.ipcRenderer.on('settings', async(_: Event, s: GeneralSettings) => {
 | 
			
		||||
    settings = s;
 | 
			
		||||
    spellchecker.setDictionary(s.spellcheckLang, dictDir);
 | 
			
		||||
    for(const word of s.customDictionary) spellchecker.add(word);
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const params = <{[key: string]: string | undefined}>qs.parse(window.location.search.substr(1));
 | 
			
		||||
const settings = <GeneralSettings>JSON.parse(params['settings']!);
 | 
			
		||||
let settings = <GeneralSettings>JSON.parse(params['settings']!);
 | 
			
		||||
if(params['import'] !== undefined)
 | 
			
		||||
    try {
 | 
			
		||||
        if(SlimcatImporter.canImportGeneral() && confirm(l('importer.importGeneral'))) {
 | 
			
		||||
@ -214,11 +192,6 @@ if(params['import'] !== undefined)
 | 
			
		||||
    } catch {
 | 
			
		||||
        alert(l('importer.error'));
 | 
			
		||||
    }
 | 
			
		||||
spellchecker.setDictionary(settings.spellcheckLang, dictDir);
 | 
			
		||||
 | 
			
		||||
const customDictionaryPath = path.join(settings.logDirectory, 'words');
 | 
			
		||||
const customDictionary = fs.existsSync(customDictionaryPath) ? <string[]>JSON.parse(fs.readFileSync(customDictionaryPath, 'utf8')) : [];
 | 
			
		||||
for(const word of customDictionary) spellchecker.add(word);
 | 
			
		||||
 | 
			
		||||
//tslint:disable-next-line:no-unused-expression
 | 
			
		||||
new Index({
 | 
			
		||||
 | 
			
		||||
@ -14,6 +14,7 @@ export class GeneralSettings {
 | 
			
		||||
    theme = 'default';
 | 
			
		||||
    version = electron.app.getVersion();
 | 
			
		||||
    beta = false;
 | 
			
		||||
    customDictionary: string[] = [];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function mkdir(dir: string): void {
 | 
			
		||||
 | 
			
		||||
@ -40,7 +40,7 @@ export async function ensureDictionary(lang: string): Promise<void> {
 | 
			
		||||
        const filePath = path.join(dictDir, `${lang}.${type}`);
 | 
			
		||||
        const downloaded = downloadedDictionaries[file.name];
 | 
			
		||||
        if(downloaded === undefined || downloaded.hash !== file.hash || !fs.existsSync(filePath)) {
 | 
			
		||||
            await writeFile(filePath, new Buffer((await Axios.get<string>(`${downloadUrl}${file.name}`, requestConfig)).data));
 | 
			
		||||
            await writeFile(filePath, Buffer.from((await Axios.get<string>(`${downloadUrl}${file.name}`, requestConfig)).data));
 | 
			
		||||
            downloadedDictionaries[file.name] = file;
 | 
			
		||||
            await writeFile(downloadedPath, JSON.stringify(downloadedDictionaries));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
@ -1,6 +1,7 @@
 | 
			
		||||
import * as electron from 'electron';
 | 
			
		||||
import * as fs from 'fs';
 | 
			
		||||
import * as path from 'path';
 | 
			
		||||
import {promisify} from 'util';
 | 
			
		||||
import {Message as MessageImpl} from '../chat/common';
 | 
			
		||||
import core from '../chat/core';
 | 
			
		||||
import {Character, Conversation, Logs as Logging, Settings} from '../chat/interfaces';
 | 
			
		||||
@ -14,7 +15,7 @@ declare module '../chat/interfaces' {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const dayMs = 86400000;
 | 
			
		||||
 | 
			
		||||
const read = promisify(fs.read);
 | 
			
		||||
const noAssert = process.env.NODE_ENV === 'production';
 | 
			
		||||
 | 
			
		||||
function writeFile(p: fs.PathLike | number, data: string | object | number,
 | 
			
		||||
@ -110,43 +111,51 @@ export function fixLogs(character: string): void {
 | 
			
		||||
    const dir = getLogDir(character);
 | 
			
		||||
    const files = fs.readdirSync(dir);
 | 
			
		||||
    const buffer = Buffer.allocUnsafe(50100);
 | 
			
		||||
    for(const file of files)
 | 
			
		||||
        if(file.substr(-4) !== '.idx') {
 | 
			
		||||
            const fd = fs.openSync(path.join(dir, file), 'r+');
 | 
			
		||||
            const indexFd = fs.openSync(path.join(dir, `${file}.idx`), 'r+');
 | 
			
		||||
            fs.readSync(indexFd, buffer, 0, 1, 0);
 | 
			
		||||
            let pos = 0, lastDay = 0;
 | 
			
		||||
            const nameEnd = buffer.readUInt8(0, noAssert) + 1;
 | 
			
		||||
            fs.readSync(indexFd, buffer, 0, nameEnd, null); //tslint:disable-line:no-null-keyword
 | 
			
		||||
            buffer.toString('utf8', 1, nameEnd);
 | 
			
		||||
            fs.ftruncateSync(indexFd, nameEnd);
 | 
			
		||||
            const size = (fs.fstatSync(fd)).size;
 | 
			
		||||
            try {
 | 
			
		||||
                while(pos < size) {
 | 
			
		||||
                    buffer.fill(-1);
 | 
			
		||||
                    fs.readSync(fd, buffer, 0, 50100, pos);
 | 
			
		||||
                    const deserialized = deserializeMessage(buffer, 0, (name) => ({
 | 
			
		||||
                        gender: 'None', status: 'online', statusText: '', isFriend: false, isBookmarked: false, isChatOp: false,
 | 
			
		||||
                        isIgnored: false, name
 | 
			
		||||
                    }), false);
 | 
			
		||||
                    const time = deserialized.message.time;
 | 
			
		||||
                    const day = Math.floor(time.getTime() / dayMs - time.getTimezoneOffset() / 1440);
 | 
			
		||||
                    if(day > lastDay) {
 | 
			
		||||
                        buffer.writeUInt16LE(day, 0, noAssert);
 | 
			
		||||
                        buffer.writeUIntLE(pos, 2, 5, noAssert);
 | 
			
		||||
                        fs.writeSync(indexFd, buffer, 0, 7);
 | 
			
		||||
                        lastDay = day;
 | 
			
		||||
                    }
 | 
			
		||||
                    if(buffer.readUInt16LE(deserialized.size - 2) !== deserialized.size - 2) throw new Error();
 | 
			
		||||
                    pos += deserialized.size;
 | 
			
		||||
                }
 | 
			
		||||
            } catch {
 | 
			
		||||
                fs.ftruncateSync(fd, pos);
 | 
			
		||||
            } finally {
 | 
			
		||||
                fs.closeSync(fd);
 | 
			
		||||
                fs.closeSync(indexFd);
 | 
			
		||||
            }
 | 
			
		||||
    for(const file of files) {
 | 
			
		||||
        const full = path.join(dir, file);
 | 
			
		||||
        if(file.substr(-4) === '.idx') {
 | 
			
		||||
            if(!fs.existsSync(full.slice(0, -4))) fs.unlinkSync(full);
 | 
			
		||||
            continue;
 | 
			
		||||
        }
 | 
			
		||||
        const fd = fs.openSync(full, 'r+');
 | 
			
		||||
        const indexPath = path.join(dir, `${file}.idx`);
 | 
			
		||||
        if(!fs.existsSync(indexPath)) {
 | 
			
		||||
            fs.unlinkSync(full);
 | 
			
		||||
            continue;
 | 
			
		||||
        }
 | 
			
		||||
        const indexFd = fs.openSync(indexPath, 'r+');
 | 
			
		||||
        fs.readSync(indexFd, buffer, 0, 1, 0);
 | 
			
		||||
        let pos = 0, lastDay = 0;
 | 
			
		||||
        const nameEnd = buffer.readUInt8(0, noAssert) + 1;
 | 
			
		||||
        fs.ftruncateSync(indexFd, nameEnd);
 | 
			
		||||
        fs.readSync(indexFd, buffer, 0, nameEnd, null); //tslint:disable-line:no-null-keyword
 | 
			
		||||
        const size = (fs.fstatSync(fd)).size;
 | 
			
		||||
        try {
 | 
			
		||||
            while(pos < size) {
 | 
			
		||||
                buffer.fill(-1);
 | 
			
		||||
                fs.readSync(fd, buffer, 0, 50100, pos);
 | 
			
		||||
                const deserialized = deserializeMessage(buffer, 0, (name) => ({
 | 
			
		||||
                    gender: 'None', status: 'online', statusText: '', isFriend: false, isBookmarked: false, isChatOp: false,
 | 
			
		||||
                    isIgnored: false, name
 | 
			
		||||
                }), false);
 | 
			
		||||
                const time = deserialized.message.time;
 | 
			
		||||
                const day = Math.floor(time.getTime() / dayMs - time.getTimezoneOffset() / 1440);
 | 
			
		||||
                if(day > lastDay) {
 | 
			
		||||
                    buffer.writeUInt16LE(day, 0, noAssert);
 | 
			
		||||
                    buffer.writeUIntLE(pos, 2, 5, noAssert);
 | 
			
		||||
                    fs.writeSync(indexFd, buffer, 0, 7);
 | 
			
		||||
                    lastDay = day;
 | 
			
		||||
                }
 | 
			
		||||
                if(buffer.readUInt16LE(deserialized.size - 2) !== deserialized.size - 2) throw new Error();
 | 
			
		||||
                pos += deserialized.size;
 | 
			
		||||
            }
 | 
			
		||||
        } catch {
 | 
			
		||||
            fs.ftruncateSync(fd, pos);
 | 
			
		||||
        } finally {
 | 
			
		||||
            fs.closeSync(fd);
 | 
			
		||||
            fs.closeSync(indexFd);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function loadIndex(name: string): Index {
 | 
			
		||||
@ -155,19 +164,23 @@ function loadIndex(name: string): Index {
 | 
			
		||||
    const files = fs.readdirSync(dir);
 | 
			
		||||
    for(const file of files)
 | 
			
		||||
        if(file.substr(-4) === '.idx') {
 | 
			
		||||
            const content = fs.readFileSync(path.join(dir, file));
 | 
			
		||||
            let offset = content.readUInt8(0, noAssert) + 1;
 | 
			
		||||
            const item: IndexItem = {
 | 
			
		||||
                name: content.toString('utf8', 1, offset),
 | 
			
		||||
                index: {},
 | 
			
		||||
                offsets: new Array(content.length - offset)
 | 
			
		||||
            };
 | 
			
		||||
            for(; offset < content.length; offset += 7) {
 | 
			
		||||
                const key = content.readUInt16LE(offset);
 | 
			
		||||
                item.index[key] = item.offsets.length;
 | 
			
		||||
                item.offsets.push(content.readUIntLE(offset + 2, 5, noAssert));
 | 
			
		||||
            try {
 | 
			
		||||
                const content = fs.readFileSync(path.join(dir, file));
 | 
			
		||||
                let offset = content.readUInt8(0, noAssert) + 1;
 | 
			
		||||
                const item: IndexItem = {
 | 
			
		||||
                    name: content.toString('utf8', 1, offset),
 | 
			
		||||
                    index: {},
 | 
			
		||||
                    offsets: new Array(content.length - offset)
 | 
			
		||||
                };
 | 
			
		||||
                for(; offset < content.length; offset += 7) {
 | 
			
		||||
                    const key = content.readUInt16LE(offset);
 | 
			
		||||
                    item.index[key] = item.offsets.length;
 | 
			
		||||
                    item.offsets.push(content.readUIntLE(offset + 2, 5, noAssert));
 | 
			
		||||
                }
 | 
			
		||||
                index[file.slice(0, -4).toLowerCase()] = item;
 | 
			
		||||
            } catch {
 | 
			
		||||
                alert(l('logs.corruption.desktop'));
 | 
			
		||||
            }
 | 
			
		||||
            index[file.slice(0, -4).toLowerCase()] = item;
 | 
			
		||||
        }
 | 
			
		||||
    return index;
 | 
			
		||||
}
 | 
			
		||||
@ -190,18 +203,24 @@ export class Logs implements Logging {
 | 
			
		||||
        let count = 20;
 | 
			
		||||
        let messages = new Array<Conversation.Message>(count);
 | 
			
		||||
        const fd = fs.openSync(file, 'r');
 | 
			
		||||
        let pos = fs.fstatSync(fd).size;
 | 
			
		||||
        const buffer = Buffer.allocUnsafe(65536);
 | 
			
		||||
        while(pos > 0 && count > 0) {
 | 
			
		||||
            fs.readSync(fd, buffer, 0, 2, pos - 2);
 | 
			
		||||
            const length = buffer.readUInt16LE(0);
 | 
			
		||||
            pos = pos - length - 2;
 | 
			
		||||
            fs.readSync(fd, buffer, 0, length, pos);
 | 
			
		||||
            messages[--count] = deserializeMessage(buffer).message;
 | 
			
		||||
        try {
 | 
			
		||||
            let pos = fs.fstatSync(fd).size;
 | 
			
		||||
            const buffer = Buffer.allocUnsafe(65536);
 | 
			
		||||
            while(pos > 0 && count > 0) {
 | 
			
		||||
                fs.readSync(fd, buffer, 0, 2, pos - 2);
 | 
			
		||||
                const length = buffer.readUInt16LE(0);
 | 
			
		||||
                pos = pos - length - 2;
 | 
			
		||||
                fs.readSync(fd, buffer, 0, length, pos);
 | 
			
		||||
                messages[--count] = deserializeMessage(buffer).message;
 | 
			
		||||
            }
 | 
			
		||||
            if(count !== 0) messages = messages.slice(count);
 | 
			
		||||
            return messages;
 | 
			
		||||
        } catch {
 | 
			
		||||
            alert(l('logs.corruption.desktop'));
 | 
			
		||||
            return [];
 | 
			
		||||
        } finally {
 | 
			
		||||
            fs.closeSync(fd);
 | 
			
		||||
        }
 | 
			
		||||
        if(count !== 0) messages = messages.slice(count);
 | 
			
		||||
        fs.closeSync(fd);
 | 
			
		||||
        return messages;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private getIndex(name: string): Index {
 | 
			
		||||
@ -229,18 +248,25 @@ export class Logs implements Logging {
 | 
			
		||||
        const messages: Conversation.Message[] = [];
 | 
			
		||||
        const pos = index.offsets[dateOffset];
 | 
			
		||||
        const fd = fs.openSync(getLogFile(character, key), 'r');
 | 
			
		||||
        const end = dateOffset + 1 < index.offsets.length ? index.offsets[dateOffset + 1] : (fs.fstatSync(fd)).size;
 | 
			
		||||
        const length = end - pos;
 | 
			
		||||
        const buffer = Buffer.allocUnsafe(length);
 | 
			
		||||
        fs.readSync(fd, buffer, 0, length, pos);
 | 
			
		||||
        fs.closeSync(fd);
 | 
			
		||||
        let offset = 0;
 | 
			
		||||
        while(offset < length) {
 | 
			
		||||
            const deserialized = deserializeMessage(buffer, offset);
 | 
			
		||||
            messages.push(deserialized.message);
 | 
			
		||||
            offset += deserialized.size;
 | 
			
		||||
        try {
 | 
			
		||||
            const end = dateOffset + 1 < index.offsets.length ? index.offsets[dateOffset + 1] : (fs.fstatSync(fd)).size;
 | 
			
		||||
            const length = end - pos;
 | 
			
		||||
            const buffer = Buffer.allocUnsafe(length);
 | 
			
		||||
            await read(fd, buffer, 0, length, pos);
 | 
			
		||||
            fs.closeSync(fd);
 | 
			
		||||
            let offset = 0;
 | 
			
		||||
            while(offset < length) {
 | 
			
		||||
                const deserialized = deserializeMessage(buffer, offset);
 | 
			
		||||
                messages.push(deserialized.message);
 | 
			
		||||
                offset += deserialized.size;
 | 
			
		||||
            }
 | 
			
		||||
            return messages;
 | 
			
		||||
        } catch {
 | 
			
		||||
            alert(l('logs.corruption.desktop'));
 | 
			
		||||
            return [];
 | 
			
		||||
        } finally {
 | 
			
		||||
            fs.closeSync(fd);
 | 
			
		||||
        }
 | 
			
		||||
        return messages;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    logMessage(conversation: {key: string, name: string}, message: Message): void {
 | 
			
		||||
@ -262,6 +288,7 @@ export class Logs implements Logging {
 | 
			
		||||
 | 
			
		||||
    async getAvailableCharacters(): Promise<ReadonlyArray<string>> {
 | 
			
		||||
        const baseDir = core.state.generalSettings!.logDirectory;
 | 
			
		||||
        mkdir(baseDir);
 | 
			
		||||
        return (fs.readdirSync(baseDir)).filter((x) => fs.lstatSync(path.join(baseDir, x)).isDirectory());
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -31,7 +31,6 @@
 | 
			
		||||
 */
 | 
			
		||||
import * as electron from 'electron';
 | 
			
		||||
import log from 'electron-log'; //tslint:disable-line:match-default-export-name
 | 
			
		||||
import {autoUpdater} from 'electron-updater';
 | 
			
		||||
import * as fs from 'fs';
 | 
			
		||||
import * as path from 'path';
 | 
			
		||||
import * as url from 'url';
 | 
			
		||||
@ -53,12 +52,12 @@ let tabCount = 0;
 | 
			
		||||
 | 
			
		||||
const baseDir = app.getPath('userData');
 | 
			
		||||
mkdir(baseDir);
 | 
			
		||||
autoUpdater.logger = log;
 | 
			
		||||
log.transports.file.level = 'debug';
 | 
			
		||||
log.transports.console.level = 'debug';
 | 
			
		||||
log.transports.file.maxSize = 5 * 1024 * 1024;
 | 
			
		||||
log.transports.file.file = path.join(baseDir, 'log.txt');
 | 
			
		||||
log.info('Starting application.');
 | 
			
		||||
let shouldImportSettings = false;
 | 
			
		||||
 | 
			
		||||
const settingsDir = path.join(baseDir, 'data');
 | 
			
		||||
mkdir(settingsDir);
 | 
			
		||||
const settingsFile = path.join(settingsDir, 'settings');
 | 
			
		||||
const settings = new GeneralSettings();
 | 
			
		||||
 | 
			
		||||
async function setDictionary(lang: string | undefined): Promise<void> {
 | 
			
		||||
    if(lang !== undefined) await ensureDictionary(lang);
 | 
			
		||||
@ -66,19 +65,6 @@ async function setDictionary(lang: string | undefined): Promise<void> {
 | 
			
		||||
    setGeneralSettings(settings);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const settingsDir = path.join(electron.app.getPath('userData'), 'data');
 | 
			
		||||
mkdir(settingsDir);
 | 
			
		||||
const settingsFile = path.join(settingsDir, 'settings');
 | 
			
		||||
const settings = new GeneralSettings();
 | 
			
		||||
let shouldImportSettings = false;
 | 
			
		||||
if(!fs.existsSync(settingsFile)) shouldImportSettings = true;
 | 
			
		||||
else
 | 
			
		||||
    try {
 | 
			
		||||
        Object.assign(settings, <GeneralSettings>JSON.parse(fs.readFileSync(settingsFile, 'utf8')));
 | 
			
		||||
    } catch(e) {
 | 
			
		||||
        log.error(`Error loading settings: ${e}`);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
function setGeneralSettings(value: GeneralSettings): void {
 | 
			
		||||
    fs.writeFileSync(path.join(settingsDir, 'settings'), JSON.stringify(value));
 | 
			
		||||
    for(const w of electron.webContents.getAllWebContents()) w.send('settings', settings);
 | 
			
		||||
@ -150,7 +136,21 @@ function showPatchNotes(): void {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function onReady(): void {
 | 
			
		||||
    app.setAppUserModelId('net.f-list.f-chat');
 | 
			
		||||
    log.transports.file.level = 'debug';
 | 
			
		||||
    log.transports.console.level = 'debug';
 | 
			
		||||
    log.transports.file.maxSize = 5 * 1024 * 1024;
 | 
			
		||||
    log.transports.file.file = path.join(baseDir, 'log.txt');
 | 
			
		||||
    log.info('Starting application.');
 | 
			
		||||
 | 
			
		||||
    if(!fs.existsSync(settingsFile)) shouldImportSettings = true;
 | 
			
		||||
    else
 | 
			
		||||
        try {
 | 
			
		||||
            Object.assign(settings, <GeneralSettings>JSON.parse(fs.readFileSync(settingsFile, 'utf8')));
 | 
			
		||||
        } catch(e) {
 | 
			
		||||
            log.error(`Error loading settings: ${e}`);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
    app.setAppUserModelId('com.squirrel.fchat.F-Chat');
 | 
			
		||||
    app.on('open-file', createWindow);
 | 
			
		||||
 | 
			
		||||
    if(settings.version !== app.getVersion()) {
 | 
			
		||||
@ -159,11 +159,12 @@ function onReady(): void {
 | 
			
		||||
        setGeneralSettings(settings);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const updaterUrl = `https://client.f-list.net/${process.platform}`;
 | 
			
		||||
    if(process.env.NODE_ENV === 'production') {
 | 
			
		||||
        autoUpdater.channel = settings.beta ? 'beta' : 'latest';
 | 
			
		||||
        autoUpdater.checkForUpdates(); //tslint:disable-line:no-floating-promises
 | 
			
		||||
        const updateTimer = setInterval(async() => autoUpdater.checkForUpdates(), 3600000);
 | 
			
		||||
        autoUpdater.on('update-downloaded', () => {
 | 
			
		||||
        electron.autoUpdater.setFeedURL({url: updaterUrl + (settings.beta ? '?channel=beta' : ''), serverType: 'json'});
 | 
			
		||||
        setTimeout(() => electron.autoUpdater.checkForUpdates(), 10000);
 | 
			
		||||
        const updateTimer = setInterval(() => electron.autoUpdater.checkForUpdates(), 3600000);
 | 
			
		||||
        electron.autoUpdater.on('update-downloaded', () => {
 | 
			
		||||
            clearInterval(updateTimer);
 | 
			
		||||
            const menu = electron.Menu.getApplicationMenu()!;
 | 
			
		||||
            const item = menu.getMenuItemById('update') as MenuItem | null;
 | 
			
		||||
@ -175,7 +176,7 @@ function onReady(): void {
 | 
			
		||||
                        label: l('action.update'),
 | 
			
		||||
                        click: () => {
 | 
			
		||||
                            for(const w of windows) w.webContents.send('quit');
 | 
			
		||||
                            autoUpdater.quitAndInstall(false, true);
 | 
			
		||||
                            electron.autoUpdater.quitAndInstall();
 | 
			
		||||
                        }
 | 
			
		||||
                    }, {
 | 
			
		||||
                        label: l('help.changelog'),
 | 
			
		||||
@ -186,12 +187,12 @@ function onReady(): void {
 | 
			
		||||
            electron.Menu.setApplicationMenu(menu);
 | 
			
		||||
            for(const w of windows) w.webContents.send('update-available', true);
 | 
			
		||||
        });
 | 
			
		||||
        autoUpdater.on('update-not-available', () => {
 | 
			
		||||
            (<any>autoUpdater).downloadedUpdateHelper.clear(); //tslint:disable-line:no-any no-unsafe-any
 | 
			
		||||
        electron.autoUpdater.on('update-not-available', () => {
 | 
			
		||||
            for(const w of windows) w.webContents.send('update-available', false);
 | 
			
		||||
            const item = electron.Menu.getApplicationMenu()!.getMenuItemById('update') as MenuItem | null;
 | 
			
		||||
            if(item !== null) item.visible = false;
 | 
			
		||||
        });
 | 
			
		||||
        electron.autoUpdater.on('error', (e) => log.error(e));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const viewItem = {
 | 
			
		||||
@ -275,8 +276,8 @@ function onReady(): void {
 | 
			
		||||
                    click: async(item: Electron.MenuItem) => {
 | 
			
		||||
                        settings.beta = item.checked;
 | 
			
		||||
                        setGeneralSettings(settings);
 | 
			
		||||
                        autoUpdater.channel = item.checked ? 'beta' : 'latest';
 | 
			
		||||
                        return autoUpdater.checkForUpdates();
 | 
			
		||||
                        electron.autoUpdater.setFeedURL({url: updaterUrl + (item.checked ? '?channel=beta' : ''), serverType: 'json'});
 | 
			
		||||
                        return electron.autoUpdater.checkForUpdates();
 | 
			
		||||
                    }
 | 
			
		||||
                }, {
 | 
			
		||||
                    label: l('fixLogs.action'),
 | 
			
		||||
@ -360,6 +361,15 @@ function onReady(): void {
 | 
			
		||||
        else characters.push(character);
 | 
			
		||||
        e.returnValue = true;
 | 
			
		||||
    });
 | 
			
		||||
    electron.ipcMain.on('dictionary-add', (_: Event, word: string) => {
 | 
			
		||||
        if(settings.customDictionary.indexOf(word) !== -1) return;
 | 
			
		||||
        settings.customDictionary.push(word);
 | 
			
		||||
        setGeneralSettings(settings);
 | 
			
		||||
    });
 | 
			
		||||
    electron.ipcMain.on('dictionary-remove', (_: Event, word: string) => {
 | 
			
		||||
        settings.customDictionary.splice(settings.customDictionary.indexOf(word), 1);
 | 
			
		||||
        setGeneralSettings(settings);
 | 
			
		||||
    });
 | 
			
		||||
    electron.ipcMain.on('disconnect', (_: Event, character: string) => characters.splice(characters.indexOf(character), 1));
 | 
			
		||||
    const emptyBadge = electron.nativeImage.createEmpty();
 | 
			
		||||
    //tslint:disable-next-line:no-require-imports
 | 
			
		||||
@ -372,7 +382,7 @@ function onReady(): void {
 | 
			
		||||
    createWindow();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const running = process.env.NODE_ENV === 'production' && app.makeSingleInstance(createWindow);
 | 
			
		||||
if(running) app.quit();
 | 
			
		||||
const isSquirrelStart = require('electron-squirrel-startup'); //tslint:disable-line:no-require-imports
 | 
			
		||||
if(isSquirrelStart || process.env.NODE_ENV === 'production' && app.makeSingleInstance(createWindow)) app.quit();
 | 
			
		||||
else app.on('ready', onReady);
 | 
			
		||||
app.on('window-all-closed', () => app.quit());
 | 
			
		||||
@ -9,7 +9,7 @@ const browserWindow = remote.getCurrentWindow();
 | 
			
		||||
export default class Notifications extends BaseNotifications {
 | 
			
		||||
    async notify(conversation: Conversation, title: string, body: string, icon: string, sound: string): Promise<void> {
 | 
			
		||||
        if(!this.shouldNotify(conversation)) return;
 | 
			
		||||
        await this.playSound(sound);
 | 
			
		||||
        this.playSound(sound);
 | 
			
		||||
        browserWindow.flashFrame(true);
 | 
			
		||||
        if(core.state.settings.notifications) {
 | 
			
		||||
            const notification = new Notification(title, this.getOptions(conversation, body, icon));
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										122
									
								
								electron/pack.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										122
									
								
								electron/pack.js
									
									
									
									
									
										Normal 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}`));
 | 
			
		||||
@ -1,38 +1,16 @@
 | 
			
		||||
{
 | 
			
		||||
    "name": "fchat",
 | 
			
		||||
    "version": "3.0.0",
 | 
			
		||||
    "author": "The F-List Team",
 | 
			
		||||
    "description": "F-List.net Chat Client",
 | 
			
		||||
    "main": "main.js",
 | 
			
		||||
    "license": "MIT",
 | 
			
		||||
    "scripts": {
 | 
			
		||||
        "build": "node ../webpack development",
 | 
			
		||||
        "build:dist": "node ../webpack production",
 | 
			
		||||
        "watch": "node ../webpack watch",
 | 
			
		||||
        "start": "../node_modules/.bin/electron app"
 | 
			
		||||
    },
 | 
			
		||||
    "build": {
 | 
			
		||||
        "appId": "net.f-list.f-chat",
 | 
			
		||||
        "productName": "F-Chat",
 | 
			
		||||
        "files": [
 | 
			
		||||
            "*",
 | 
			
		||||
            "sounds",
 | 
			
		||||
            "themes",
 | 
			
		||||
            "!**/*.map",
 | 
			
		||||
            "!node_modules/",
 | 
			
		||||
            "node_modules/**/*.node"
 | 
			
		||||
        ],
 | 
			
		||||
        "asar": false,
 | 
			
		||||
        "nsis": {
 | 
			
		||||
            "oneClick": false,
 | 
			
		||||
            "allowToChangeInstallationDirectory": true
 | 
			
		||||
        },
 | 
			
		||||
        "linux": {
 | 
			
		||||
            "category": "Network"
 | 
			
		||||
        },
 | 
			
		||||
        "publish": {
 | 
			
		||||
            "provider": "generic",
 | 
			
		||||
            "url": "https://client.f-list.net/"
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
  "name": "fchat",
 | 
			
		||||
  "version": "3.0.7",
 | 
			
		||||
  "author": "The F-List Team",
 | 
			
		||||
  "description": "F-List.net Chat Client",
 | 
			
		||||
  "main": "main.js",
 | 
			
		||||
  "id": "fchat",
 | 
			
		||||
  "license": "MIT",
 | 
			
		||||
  "scripts": {
 | 
			
		||||
    "build": "node ../webpack development",
 | 
			
		||||
    "build:dist": "node ../webpack production",
 | 
			
		||||
    "watch": "node ../webpack watch",
 | 
			
		||||
    "start": "../node_modules/.bin/electron app",
 | 
			
		||||
    "pack": "node ./pack"
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -6,7 +6,7 @@ const OptimizeCssAssetsPlugin = require('optimize-css-assets-webpack-plugin');
 | 
			
		||||
const VueLoaderPlugin = require('vue-loader/lib/plugin');
 | 
			
		||||
 | 
			
		||||
const mainConfig = {
 | 
			
		||||
    entry: [path.join(__dirname, 'main.ts'), path.join(__dirname, 'application.json')],
 | 
			
		||||
    entry: [path.join(__dirname, 'main.ts'), path.join(__dirname, 'package.json')],
 | 
			
		||||
    output: {
 | 
			
		||||
        path: __dirname + '/app',
 | 
			
		||||
        filename: 'main.js'
 | 
			
		||||
@ -23,7 +23,7 @@ const mainConfig = {
 | 
			
		||||
                    transpileOnly: true
 | 
			
		||||
                }
 | 
			
		||||
            },
 | 
			
		||||
            {test: path.join(__dirname, 'application.json'), loader: 'file-loader?name=package.json', type: 'javascript/auto'},
 | 
			
		||||
            {test: path.join(__dirname, 'package.json'), loader: 'file-loader?name=package.json', type: 'javascript/auto'},
 | 
			
		||||
            {test: /\.(png|html)$/, loader: 'file-loader?name=[name].[ext]'}
 | 
			
		||||
        ]
 | 
			
		||||
    },
 | 
			
		||||
@ -45,7 +45,7 @@ const mainConfig = {
 | 
			
		||||
}, rendererConfig = {
 | 
			
		||||
    entry: {
 | 
			
		||||
        chat: [path.join(__dirname, 'chat.ts'), path.join(__dirname, 'index.html')],
 | 
			
		||||
        window: [path.join(__dirname, 'window.ts'), path.join(__dirname, 'window.html')]
 | 
			
		||||
        window: [path.join(__dirname, 'window.ts'), path.join(__dirname, 'window.html'), path.join(__dirname, 'build', 'tray@2x.png')]
 | 
			
		||||
    },
 | 
			
		||||
    output: {
 | 
			
		||||
        path: __dirname + '/app',
 | 
			
		||||
 | 
			
		||||
@ -42,6 +42,7 @@ export namespace Connection {
 | 
			
		||||
        RLL: {channel: string, dice: 'bottle' | string} | {recipient: string, dice: 'bottle' | string},
 | 
			
		||||
        RMO: {channel: string, mode: Channel.Mode},
 | 
			
		||||
        RST: {channel: string, status: 'public' | 'private'},
 | 
			
		||||
        SCP: {action: 'add' | 'remove', character: string}
 | 
			
		||||
        RWD: {character: string},
 | 
			
		||||
        SFC: {action: 'report', report: string, tab?: string, logid: number} | {action: 'confirm', callid: number},
 | 
			
		||||
        STA: {status: Character.Status, statusmsg: string},
 | 
			
		||||
 | 
			
		||||
@ -8,12 +8,12 @@ android {
 | 
			
		||||
		applicationId "net.f_list.fchat"
 | 
			
		||||
		minSdkVersion 19
 | 
			
		||||
		targetSdkVersion 27
 | 
			
		||||
		versionCode 17
 | 
			
		||||
		versionName "3.0.6"
 | 
			
		||||
		versionCode 18
 | 
			
		||||
		versionName "3.0.7"
 | 
			
		||||
	}
 | 
			
		||||
	buildTypes {
 | 
			
		||||
		release {
 | 
			
		||||
			minifyEnabled false
 | 
			
		||||
			minifyEnabled true
 | 
			
		||||
			proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
@ -1,19 +1,21 @@
 | 
			
		||||
package net.f_list.fchat
 | 
			
		||||
 | 
			
		||||
import android.content.Context
 | 
			
		||||
import android.util.SparseArray
 | 
			
		||||
import android.webkit.JavascriptInterface
 | 
			
		||||
import org.json.JSONArray
 | 
			
		||||
import org.json.JSONStringer
 | 
			
		||||
import java.io.File
 | 
			
		||||
import java.io.FileInputStream
 | 
			
		||||
import java.io.FileOutputStream
 | 
			
		||||
import java.io.RandomAccessFile
 | 
			
		||||
import java.nio.ByteBuffer
 | 
			
		||||
import java.nio.ByteOrder
 | 
			
		||||
import java.nio.CharBuffer
 | 
			
		||||
import java.util.*
 | 
			
		||||
 | 
			
		||||
class Logs(private val ctx: Context) {
 | 
			
		||||
	data class IndexItem(val name: String, val index: MutableMap<Int, Long> = HashMap(), val dates: MutableList<Int> = LinkedList())
 | 
			
		||||
	data class IndexItem(val name: String, val index: MutableMap<Int, Int> = LinkedHashMap(), val offsets: MutableList<Long> = ArrayList())
 | 
			
		||||
 | 
			
		||||
	private var index: MutableMap<String, IndexItem>? = null
 | 
			
		||||
	private var loadedIndex: MutableMap<String, IndexItem>? = null
 | 
			
		||||
@ -42,8 +44,8 @@ class Logs(private val ctx: Context) {
 | 
			
		||||
				buffer.limit(read)
 | 
			
		||||
				while(buffer.position() < buffer.limit()) {
 | 
			
		||||
					val key = buffer.short.toInt()
 | 
			
		||||
					indexItem.index[key] = buffer.int.toLong() or (buffer.get().toLong() shl 32)
 | 
			
		||||
					indexItem.dates.add(key)
 | 
			
		||||
					indexItem.index[key] = indexItem.offsets.size
 | 
			
		||||
					indexItem.offsets.add(buffer.int.toLong() or (buffer.get().toLong() shl 32))
 | 
			
		||||
				}
 | 
			
		||||
				index[file.nameWithoutExtension] = indexItem
 | 
			
		||||
			}
 | 
			
		||||
@ -60,7 +62,7 @@ class Logs(private val ctx: Context) {
 | 
			
		||||
		loadedIndex = index
 | 
			
		||||
		val json = JSONStringer().`object`()
 | 
			
		||||
		for(item in index!!)
 | 
			
		||||
			json.key(item.key).`object`().key("name").value(item.value.name).key("dates").value(JSONArray(item.value.dates)).endObject()
 | 
			
		||||
			json.key(item.key).`object`().key("name").value(item.value.name).key("dates").value(JSONArray(item.value.index.keys)).endObject()
 | 
			
		||||
		return json.endObject().toString()
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
@ -70,7 +72,7 @@ class Logs(private val ctx: Context) {
 | 
			
		||||
		val file = File(baseDir, key)
 | 
			
		||||
		buffer.clear()
 | 
			
		||||
		if(!index!!.containsKey(key)) {
 | 
			
		||||
			index!![key] = IndexItem(conversation, HashMap())
 | 
			
		||||
			index!![key] = IndexItem(conversation)
 | 
			
		||||
			buffer.position(1)
 | 
			
		||||
			encoder.encode(CharBuffer.wrap(conversation), buffer, true)
 | 
			
		||||
			buffer.put(0, (buffer.position() - 1).toByte())
 | 
			
		||||
@ -79,10 +81,9 @@ class Logs(private val ctx: Context) {
 | 
			
		||||
		if(!item.index.containsKey(day)) {
 | 
			
		||||
			buffer.putShort(day.toShort())
 | 
			
		||||
			val size = file.length()
 | 
			
		||||
			item.index[day] = size
 | 
			
		||||
			item.dates.add(day)
 | 
			
		||||
			buffer.putInt((size and 0xffffffffL).toInt())
 | 
			
		||||
			buffer.put((size shr 32).toByte())
 | 
			
		||||
			item.index[day] = item.offsets.size
 | 
			
		||||
			item.offsets.add(size)
 | 
			
		||||
			buffer.putInt((size and 0xffffffffL).toInt()).put((size shr 32).toByte())
 | 
			
		||||
			FileOutputStream(File(baseDir, "$key.idx"), true).use { file ->
 | 
			
		||||
				buffer.flip()
 | 
			
		||||
				file.channel.write(buffer)
 | 
			
		||||
@ -141,20 +142,22 @@ class Logs(private val ctx: Context) {
 | 
			
		||||
 | 
			
		||||
	@JavascriptInterface
 | 
			
		||||
	fun getLogsN(character: String, key: String, date: Int): String {
 | 
			
		||||
		val offset = loadedIndex!![key]?.index?.get(date) ?: return "[]"
 | 
			
		||||
		val indexItem = loadedIndex!![key] ?: return "[]"
 | 
			
		||||
		val dateKey = indexItem.index[date] ?: return "[]"
 | 
			
		||||
		val json = JSONStringer()
 | 
			
		||||
		json.array()
 | 
			
		||||
		FileInputStream(File(ctx.filesDir, "$character/logs/$key")).use { stream ->
 | 
			
		||||
			val channel = stream.channel
 | 
			
		||||
			channel.position(offset)
 | 
			
		||||
			while(channel.position() < channel.size()) {
 | 
			
		||||
				buffer.clear()
 | 
			
		||||
				val oldPosition = channel.position()
 | 
			
		||||
				channel.read(buffer)
 | 
			
		||||
				buffer.rewind()
 | 
			
		||||
				deserializeMessage(buffer, json, date)
 | 
			
		||||
				if(buffer.position() == 0) break
 | 
			
		||||
				channel.position(oldPosition + buffer.position() + 2)
 | 
			
		||||
			val start = indexItem.offsets[dateKey]
 | 
			
		||||
			val end = if(dateKey >= indexItem.offsets.size - 1) channel.size() else indexItem.offsets[dateKey + 1]
 | 
			
		||||
			channel.position(start)
 | 
			
		||||
			val buffer = ByteBuffer.allocateDirect((end - start).toInt()).order(ByteOrder.LITTLE_ENDIAN)
 | 
			
		||||
			channel.read(buffer)
 | 
			
		||||
			buffer.rewind()
 | 
			
		||||
			while(buffer.position() < buffer.limit()) {
 | 
			
		||||
				deserializeMessage(buffer, json)
 | 
			
		||||
				buffer.limit(buffer.capacity())
 | 
			
		||||
				buffer.position(buffer.position() + 2)
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		return json.endArray().toString()
 | 
			
		||||
@ -165,7 +168,7 @@ class Logs(private val ctx: Context) {
 | 
			
		||||
		loadedIndex = if(character == this.character) this.index else this.loadIndex(character)
 | 
			
		||||
		val json = JSONStringer().`object`()
 | 
			
		||||
		for(item in loadedIndex!!)
 | 
			
		||||
			json.key(item.key).`object`().key("name").value(item.value.name).key("dates").value(JSONArray(item.value.dates)).endObject()
 | 
			
		||||
			json.key(item.key).`object`().key("name").value(item.value.name).key("dates").value(JSONArray(item.value.index.keys)).endObject()
 | 
			
		||||
		return json.endObject().toString()
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
@ -174,22 +177,83 @@ class Logs(private val ctx: Context) {
 | 
			
		||||
		return JSONArray(ctx.filesDir.listFiles().filter { it.isDirectory }.map { it.name }).toString()
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	private fun deserializeMessage(buffer: ByteBuffer, json: JSONStringer, checkDate: Int = -1) {
 | 
			
		||||
		val date = buffer.int
 | 
			
		||||
		if(checkDate != -1 && date / 86400 != checkDate) return
 | 
			
		||||
	@JavascriptInterface
 | 
			
		||||
	fun repair() {
 | 
			
		||||
		val files = baseDir.listFiles()
 | 
			
		||||
		val indexBuffer = ByteBuffer.allocateDirect(7).order(ByteOrder.LITTLE_ENDIAN)
 | 
			
		||||
		for(entry in files) {
 | 
			
		||||
			if(entry.name.endsWith(".idx")) continue
 | 
			
		||||
			RandomAccessFile("$entry.idx", "rw").use { idx ->
 | 
			
		||||
				buffer.clear()
 | 
			
		||||
				buffer.limit(1)
 | 
			
		||||
				idx.channel.read(buffer)
 | 
			
		||||
				idx.channel.truncate((buffer.get(0) + 1).toLong())
 | 
			
		||||
				idx.channel.position(idx.channel.size())
 | 
			
		||||
				RandomAccessFile(entry, "rw").use { file ->
 | 
			
		||||
 					var lastDay = 0
 | 
			
		||||
					val size = file.channel.size()
 | 
			
		||||
					var pos = 0L
 | 
			
		||||
					try {
 | 
			
		||||
						while(file.channel.position() < size) {
 | 
			
		||||
							buffer.clear()
 | 
			
		||||
							pos = file.channel.position()
 | 
			
		||||
							val read = file.channel.read(buffer)
 | 
			
		||||
							var success = false
 | 
			
		||||
							buffer.flip()
 | 
			
		||||
							while(buffer.remaining() > 10) {
 | 
			
		||||
								val offset = buffer.position()
 | 
			
		||||
								val day = buffer.int / 86400
 | 
			
		||||
								buffer.get()
 | 
			
		||||
								val senderLength = buffer.get()
 | 
			
		||||
								if(buffer.remaining() < senderLength + 4) break
 | 
			
		||||
								buffer.limit(buffer.position() + senderLength)
 | 
			
		||||
								decoder.decode(buffer)
 | 
			
		||||
								buffer.limit(read)
 | 
			
		||||
								val textLength = buffer.short.toInt()
 | 
			
		||||
								if(buffer.remaining() < textLength + 2) break
 | 
			
		||||
								buffer.limit(buffer.position() + textLength)
 | 
			
		||||
								decoder.decode(buffer)
 | 
			
		||||
								buffer.limit(read)
 | 
			
		||||
								val messageSize = buffer.position() - offset
 | 
			
		||||
								if(messageSize != buffer.short.toInt()) throw Exception()
 | 
			
		||||
 | 
			
		||||
								if(day > lastDay) {
 | 
			
		||||
									lastDay = day
 | 
			
		||||
									indexBuffer.position(0)
 | 
			
		||||
									indexBuffer.putShort(day.toShort())
 | 
			
		||||
									indexBuffer.putInt((pos and 0xffffffffL).toInt()).put((pos shr 32).toByte())
 | 
			
		||||
									indexBuffer.position(0)
 | 
			
		||||
									idx.channel.write(indexBuffer)
 | 
			
		||||
								}
 | 
			
		||||
								pos += messageSize + 2
 | 
			
		||||
								success = true
 | 
			
		||||
							}
 | 
			
		||||
							if(!success) throw Exception()
 | 
			
		||||
							file.channel.position(pos)
 | 
			
		||||
						}
 | 
			
		||||
					} catch(e: Exception) {
 | 
			
		||||
						file.channel.truncate(pos)
 | 
			
		||||
					}
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	private fun deserializeMessage(buffer: ByteBuffer, json: JSONStringer) {
 | 
			
		||||
		val start = buffer.position()
 | 
			
		||||
		json.`object`()
 | 
			
		||||
		json.key("time")
 | 
			
		||||
		json.value(date)
 | 
			
		||||
		json.value(buffer.int)
 | 
			
		||||
		json.key("type")
 | 
			
		||||
		json.value(buffer.get())
 | 
			
		||||
		json.key("sender")
 | 
			
		||||
		val senderLength = buffer.get()
 | 
			
		||||
		buffer.limit(6 + senderLength)
 | 
			
		||||
		buffer.limit(start + 6 + senderLength)
 | 
			
		||||
		json.value(decoder.decode(buffer))
 | 
			
		||||
		buffer.limit(buffer.capacity())
 | 
			
		||||
		val textLength = buffer.short.toInt() and 0xffff
 | 
			
		||||
		json.key("text")
 | 
			
		||||
		buffer.limit(8 + senderLength + textLength)
 | 
			
		||||
		buffer.limit(start + 8 + senderLength + textLength)
 | 
			
		||||
		json.value(decoder.decode(buffer))
 | 
			
		||||
		json.endObject()
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
@ -30,9 +30,7 @@
 | 
			
		||||
 * @see {@link https://github.com/f-list/exported|GitHub repo}
 | 
			
		||||
 */
 | 
			
		||||
import Axios from 'axios';
 | 
			
		||||
import * as Raven from 'raven-js';
 | 
			
		||||
import Vue from 'vue';
 | 
			
		||||
import VueRaven from '../chat/vue-raven';
 | 
			
		||||
import {setupRaven} from '../chat/vue-raven';
 | 
			
		||||
import Index from './Index.vue';
 | 
			
		||||
 | 
			
		||||
const version = (<{version: string}>require('./package.json')).version; //tslint:disable-line:no-require-imports
 | 
			
		||||
@ -40,23 +38,8 @@ const version = (<{version: string}>require('./package.json')).version; //tslint
 | 
			
		||||
    Axios.defaults.params = { __fchat: `mobile-${platform}/${version}` };
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
if(process.env.NODE_ENV === 'production') {
 | 
			
		||||
    Raven.config('https://a9239b17b0a14f72ba85e8729b9d1612@sentry.f-list.net/2', {
 | 
			
		||||
        release: `mobile-${version}`,
 | 
			
		||||
        dataCallback: (data: {culprit: string, exception?: {values: {stacktrace: {frames: {filename: string}[]}}[]}}) => {
 | 
			
		||||
            data.culprit = `~${data.culprit.substr(data.culprit.lastIndexOf('/'))}`;
 | 
			
		||||
            if(data.exception !== undefined)
 | 
			
		||||
                for(const ex of data.exception.values)
 | 
			
		||||
                    for(const frame of ex.stacktrace.frames) {
 | 
			
		||||
                        const index = frame.filename.lastIndexOf('/');
 | 
			
		||||
                        frame.filename = index !== -1 ? `~${frame.filename.substr(index)}` : frame.filename;
 | 
			
		||||
                    }
 | 
			
		||||
        }
 | 
			
		||||
    }).addPlugin(VueRaven, Vue).install();
 | 
			
		||||
    (<Window & {onunhandledrejection(e: PromiseRejectionEvent): void}>window).onunhandledrejection = (e: PromiseRejectionEvent) => {
 | 
			
		||||
        Raven.captureException(<Error>e.reason);
 | 
			
		||||
    };
 | 
			
		||||
}
 | 
			
		||||
if(process.env.NODE_ENV === 'production')
 | 
			
		||||
    setupRaven('https://a9239b17b0a14f72ba85e8729b9d1612@sentry.f-list.net/2', `mobile-${version}`);
 | 
			
		||||
 | 
			
		||||
new Index({ //tslint:disable-line:no-unused-expression
 | 
			
		||||
    el: '#app'
 | 
			
		||||
 | 
			
		||||
@ -1,6 +1,7 @@
 | 
			
		||||
import {Message as MessageImpl} from '../chat/common';
 | 
			
		||||
import core from '../chat/core';
 | 
			
		||||
import {Conversation, Logs as Logging, Settings} from '../chat/interfaces';
 | 
			
		||||
import l from '../chat/localize';
 | 
			
		||||
 | 
			
		||||
declare global {
 | 
			
		||||
    const NativeFile: {
 | 
			
		||||
@ -20,6 +21,7 @@ declare global {
 | 
			
		||||
                   message: string): Promise<void>;
 | 
			
		||||
        getBacklog(key: string): Promise<ReadonlyArray<NativeMessage>>;
 | 
			
		||||
        getLogs(character: string, key: string, date: number): Promise<ReadonlyArray<NativeMessage>>
 | 
			
		||||
        repair(character: string): Promise<void>
 | 
			
		||||
    };
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -41,10 +43,16 @@ export class Logs implements Logging {
 | 
			
		||||
    private index: Index = {};
 | 
			
		||||
    private loadedIndex?: Index;
 | 
			
		||||
    private loadedCharacter?: string;
 | 
			
		||||
    attemptedFix = false;
 | 
			
		||||
 | 
			
		||||
    constructor() {
 | 
			
		||||
        core.connection.onEvent('connecting', async() => {
 | 
			
		||||
            this.index = await NativeLogs.init(core.connection.character);
 | 
			
		||||
            this.attemptedFix = false;
 | 
			
		||||
            try {
 | 
			
		||||
                this.index = await NativeLogs.init(core.connection.character);
 | 
			
		||||
            } catch {
 | 
			
		||||
                await this.fixLogs(core.connection.character);
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -59,20 +67,35 @@ export class Logs implements Logging {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async getBacklog(conversation: Conversation): Promise<ReadonlyArray<Conversation.Message>> {
 | 
			
		||||
        return (await NativeLogs.getBacklog(conversation.key))
 | 
			
		||||
            .map((x) => new MessageImpl(x.type, core.characters.get(x.sender), x.text, new Date(x.time * 1000)));
 | 
			
		||||
        try {
 | 
			
		||||
            return (await NativeLogs.getBacklog(conversation.key))
 | 
			
		||||
                .map((x) => new MessageImpl(x.type, core.characters.get(x.sender), x.text, new Date(x.time * 1000)));
 | 
			
		||||
        } catch {
 | 
			
		||||
            await this.fixLogs(this.loadedCharacter!);
 | 
			
		||||
            return [];
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private async getIndex(name: string): Promise<Index> {
 | 
			
		||||
        if(this.loadedCharacter === name) return this.loadedIndex!;
 | 
			
		||||
        this.loadedCharacter = name;
 | 
			
		||||
        return this.loadedIndex = name === core.connection.character ? this.index : await NativeLogs.loadIndex(name);
 | 
			
		||||
        try {
 | 
			
		||||
            return this.loadedIndex = name === core.connection.character ? this.index : await NativeLogs.loadIndex(name);
 | 
			
		||||
        } catch {
 | 
			
		||||
            await this.fixLogs(name);
 | 
			
		||||
            return {};
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async getLogs(character: string, key: string, date: Date): Promise<ReadonlyArray<Conversation.Message>> {
 | 
			
		||||
        await NativeLogs.loadIndex(character);
 | 
			
		||||
        return (await NativeLogs.getLogs(character, key, Math.floor(date.getTime() / dayMs - date.getTimezoneOffset() / 1440)))
 | 
			
		||||
            .map((x) => new MessageImpl(x.type, core.characters.get(x.sender), x.text, new Date(x.time * 1000)));
 | 
			
		||||
        try {
 | 
			
		||||
            await NativeLogs.loadIndex(character);
 | 
			
		||||
            return (await NativeLogs.getLogs(character, key, Math.floor(date.getTime() / dayMs - date.getTimezoneOffset() / 1440)))
 | 
			
		||||
                .map((x) => new MessageImpl(x.type, core.characters.get(x.sender), x.text, new Date(x.time * 1000)));
 | 
			
		||||
        } catch {
 | 
			
		||||
            await this.fixLogs(character);
 | 
			
		||||
            return [];
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async getLogDates(character: string, key: string): Promise<ReadonlyArray<Date>> {
 | 
			
		||||
@ -94,6 +117,19 @@ export class Logs implements Logging {
 | 
			
		||||
    async getAvailableCharacters(): Promise<ReadonlyArray<string>> {
 | 
			
		||||
        return NativeLogs.getCharacters();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async fixLogs(character: string): Promise<void> {
 | 
			
		||||
        if(this.attemptedFix) return alert(l('logs.corruption.mobile.error'));
 | 
			
		||||
        this.attemptedFix = true;
 | 
			
		||||
        alert(l('logs.corruption.mobile'));
 | 
			
		||||
        try {
 | 
			
		||||
            await NativeLogs.repair(character);
 | 
			
		||||
            this.index = await NativeLogs.init(core.connection.character);
 | 
			
		||||
            alert(l('logs.corruption.mobile.success'));
 | 
			
		||||
        } catch {
 | 
			
		||||
            alert(l('logs.corruption.mobile.error'));
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export async function getGeneralSettings(): Promise<GeneralSettings | undefined> {
 | 
			
		||||
 | 
			
		||||
@ -3,13 +3,18 @@ import WebKit
 | 
			
		||||
 | 
			
		||||
class IndexItem: Encodable {
 | 
			
		||||
    let name: String
 | 
			
		||||
    var index = NSMutableOrderedSet()
 | 
			
		||||
    var dates = [UInt16]()
 | 
			
		||||
    var dates = NSMutableOrderedSet()
 | 
			
		||||
    var offsets = [UInt64]()
 | 
			
		||||
    init(_ name: String) {
 | 
			
		||||
        self.name = name
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    func encode(to encoder: Encoder) throws {
 | 
			
		||||
        var container = encoder.container(keyedBy: CodingKeys.self)
 | 
			
		||||
        try container.encode(name, forKey: .name)
 | 
			
		||||
        try container.encode(dates.array as! [UInt16], forKey: .dates)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private enum CodingKeys: String, CodingKey {
 | 
			
		||||
        case name
 | 
			
		||||
        case dates
 | 
			
		||||
@ -19,7 +24,7 @@ class IndexItem: Encodable {
 | 
			
		||||
class Logs: NSObject, WKScriptMessageHandler {
 | 
			
		||||
    let fm = FileManager.default;
 | 
			
		||||
    let baseDir = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask).first!
 | 
			
		||||
    var buffer = UnsafeMutableRawPointer.allocate(bytes: 51000, alignedTo: 1)
 | 
			
		||||
    var buffer = UnsafeMutableRawPointer.allocate(byteCount: 51000, alignment: 1)
 | 
			
		||||
    var logDir: URL!
 | 
			
		||||
    var character: String?
 | 
			
		||||
    var index: [String: IndexItem]!
 | 
			
		||||
@ -43,6 +48,8 @@ class Logs: NSObject, WKScriptMessageHandler {
 | 
			
		||||
                result = try getBacklog(data["key"] as! String)
 | 
			
		||||
            case "getLogs":
 | 
			
		||||
                result = try getLogs(data["character"] as! String, data["key"] as! String, (data["date"] as! NSNumber).uint16Value)
 | 
			
		||||
            case "repair":
 | 
			
		||||
                try repair(data["character"] as! String)
 | 
			
		||||
            default:
 | 
			
		||||
                message.webView!.evaluateJavaScript("nativeError('\(key)',new Error('Unknown message type'))")
 | 
			
		||||
                return
 | 
			
		||||
@ -65,13 +72,13 @@ class Logs: NSObject, WKScriptMessageHandler {
 | 
			
		||||
            let name = String(data: data.subdata(with: NSMakeRange(1, nameLength)), encoding: .utf8)!
 | 
			
		||||
            var offset = nameLength + 1
 | 
			
		||||
            let indexItem = IndexItem(name)
 | 
			
		||||
            if (data.length - offset) % 7 != 0 { throw NSError(domain: "Log corruption", code: 0) }
 | 
			
		||||
            while offset < data.length {
 | 
			
		||||
                var date: UInt16 = 0
 | 
			
		||||
                data.getBytes(&date, range: NSMakeRange(offset, 2))
 | 
			
		||||
                indexItem.dates.append(date)
 | 
			
		||||
                var o: UInt64 = 0
 | 
			
		||||
                data.getBytes(&o, range: NSMakeRange(offset + 2, 5))
 | 
			
		||||
                indexItem.index.add(date)
 | 
			
		||||
                indexItem.dates.add(date)
 | 
			
		||||
                indexItem.offsets.append(o)
 | 
			
		||||
                offset += 7
 | 
			
		||||
            }
 | 
			
		||||
@ -85,6 +92,7 @@ class Logs: NSObject, WKScriptMessageHandler {
 | 
			
		||||
        try fm.createDirectory(at: logDir, withIntermediateDirectories: true, attributes: nil)
 | 
			
		||||
        index = try getIndex(name)
 | 
			
		||||
        loadedIndex = index
 | 
			
		||||
        character = name
 | 
			
		||||
        return String(data: try JSONEncoder().encode(index), encoding: .utf8)!
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -104,7 +112,7 @@ class Logs: NSObject, WKScriptMessageHandler {
 | 
			
		||||
        if(indexItem == nil) { fm.createFile(atPath: url.path, contents: nil) }
 | 
			
		||||
        let fd = try FileHandle(forWritingTo: url)
 | 
			
		||||
        fd.seekToEndOfFile()
 | 
			
		||||
        if(!(indexItem?.index.contains(day) ?? false)) {
 | 
			
		||||
        if(!(indexItem?.dates.contains(day) ?? false)) {
 | 
			
		||||
            let indexFile = url.appendingPathExtension("idx")
 | 
			
		||||
            if(indexItem == nil) { fm.createFile(atPath: indexFile.path, contents: nil) }
 | 
			
		||||
            let indexFd = try FileHandle(forWritingTo: indexFile)
 | 
			
		||||
@ -120,9 +128,8 @@ class Logs: NSObject, WKScriptMessageHandler {
 | 
			
		||||
            write(indexFd.fileDescriptor, &day, 2)
 | 
			
		||||
            var offset = fd.offsetInFile
 | 
			
		||||
            write(indexFd.fileDescriptor, &offset, 5)
 | 
			
		||||
            indexItem!.index.add(indexItem!.offsets.count)
 | 
			
		||||
            indexItem!.dates.add(day)
 | 
			
		||||
            indexItem!.offsets.append(offset)
 | 
			
		||||
            indexItem!.dates.append(day)
 | 
			
		||||
        }
 | 
			
		||||
        let start = fd.offsetInFile
 | 
			
		||||
        write(fd.fileDescriptor, &time, 4)
 | 
			
		||||
@ -150,6 +157,7 @@ class Logs: NSObject, WKScriptMessageHandler {
 | 
			
		||||
            file.seek(toFileOffset: file.offsetInFile - 2)
 | 
			
		||||
            read(file.fileDescriptor, buffer, 2)
 | 
			
		||||
            let length = buffer.load(as: UInt16.self)
 | 
			
		||||
            if(length > file.offsetInFile - 2) { throw NSError(domain: "Log corruption", code: 0) }
 | 
			
		||||
            let newOffset = file.offsetInFile - UInt64(length + 2)
 | 
			
		||||
            file.seek(toFileOffset: newOffset)
 | 
			
		||||
            read(file.fileDescriptor, buffer, Int(length))
 | 
			
		||||
@ -161,14 +169,14 @@ class Logs: NSObject, WKScriptMessageHandler {
 | 
			
		||||
 | 
			
		||||
    func getLogs(_ character: String, _ key: String, _ date: UInt16) throws -> String {
 | 
			
		||||
        let index = loadedIndex![key]
 | 
			
		||||
        guard let indexKey = index?.index.index(of: date) else { return "[]" }
 | 
			
		||||
        guard let indexKey = index?.dates.index(of: date) else { return "[]" }
 | 
			
		||||
        let url = baseDir.appendingPathComponent("\(character)/logs/\(key)", isDirectory: false)
 | 
			
		||||
        let file = try FileHandle(forReadingFrom: url)
 | 
			
		||||
        let start = index!.offsets[indexKey]
 | 
			
		||||
        let end = indexKey >= index!.offsets.count - 1 ? file.seekToEndOfFile() : index!.offsets[indexKey + 1]
 | 
			
		||||
        file.seek(toFileOffset: start)
 | 
			
		||||
        let length = Int(end - start)
 | 
			
		||||
        let buffer = UnsafeMutableRawPointer.allocate(bytes: length, alignedTo: 1)
 | 
			
		||||
        let buffer = UnsafeMutableRawPointer.allocate(byteCount: length, alignment: 1)
 | 
			
		||||
        read(file.fileDescriptor, buffer, length)
 | 
			
		||||
        var json = "["
 | 
			
		||||
        var offset = 0
 | 
			
		||||
@ -185,19 +193,67 @@ class Logs: NSObject, WKScriptMessageHandler {
 | 
			
		||||
        return String(data: try JSONEncoder().encode(loadedIndex), encoding: .utf8)!
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    func decodeString(_ buffer: UnsafeMutableRawPointer, _ offset: Int, _ length: Int) -> String? {
 | 
			
		||||
        return String(bytesNoCopy: buffer.advanced(by: offset), length: length, encoding: .utf8, freeWhenDone: false)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    func deserializeMessage(_ buffer: UnsafeMutableRawPointer, _ o: Int) throws -> (String, Int) {
 | 
			
		||||
        var offset = o
 | 
			
		||||
        let date = buffer.advanced(by: offset).bindMemory(to: UInt32.self, capacity: 1).pointee
 | 
			
		||||
        let type = buffer.load(fromByteOffset: offset + 4, as: UInt8.self)
 | 
			
		||||
        let senderLength = Int(buffer.load(fromByteOffset: offset + 5, as: UInt8.self))
 | 
			
		||||
        guard let sender = String(bytesNoCopy: buffer.advanced(by: offset + 6), length: senderLength, encoding: .utf8, freeWhenDone: false) else {
 | 
			
		||||
        guard let sender = decodeString(buffer, offset + 6, senderLength) else {
 | 
			
		||||
            throw NSError(domain: "Log corruption", code: 0)
 | 
			
		||||
        }
 | 
			
		||||
        offset += senderLength + 6
 | 
			
		||||
        let textLength = Int(buffer.advanced(by: offset).bindMemory(to: UInt16.self, capacity: 1).pointee)
 | 
			
		||||
        guard let text = String(bytesNoCopy: buffer.advanced(by: offset + 2), length: textLength, encoding: .utf8, freeWhenDone: false) else {
 | 
			
		||||
        guard let text = decodeString(buffer, offset + 2, textLength) else {
 | 
			
		||||
            throw NSError(domain: "Log corruption", code: 0)
 | 
			
		||||
        }
 | 
			
		||||
        return ("{\"time\":\(date),\"type\":\(type),\"sender\":\(File.escape(sender)),\"text\":\(File.escape(text))}", offset + textLength + 2)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    func repair(_ character: String) throws {
 | 
			
		||||
        let files = try fm.contentsOfDirectory(at: baseDir.appendingPathComponent("\(character)/logs", isDirectory: true), includingPropertiesForKeys: nil, options: [.skipsHiddenFiles])
 | 
			
		||||
        for file in files {
 | 
			
		||||
            if(file.lastPathComponent.hasSuffix(".idx")) { continue }
 | 
			
		||||
            let indexFd = try FileHandle(forUpdating: file.appendingPathExtension("idx"))
 | 
			
		||||
            read(indexFd.fileDescriptor, buffer, 1)
 | 
			
		||||
            indexFd.truncateFile(atOffset: UInt64(buffer.load(as: UInt8.self) + 1))
 | 
			
		||||
            let fd = try FileHandle(forUpdating: file)
 | 
			
		||||
            let size = fd.seekToEndOfFile()
 | 
			
		||||
            fd.seek(toFileOffset: 0)
 | 
			
		||||
            var lastDay = 0, pos = UInt64(0)
 | 
			
		||||
            do {
 | 
			
		||||
                while fd.offsetInFile < size {
 | 
			
		||||
                    pos = fd.offsetInFile
 | 
			
		||||
                    let max = read(fd.fileDescriptor, buffer, 51000)
 | 
			
		||||
                    var offset = 0
 | 
			
		||||
                    while offset + 10 < max {
 | 
			
		||||
                        let day = buffer.advanced(by: offset).bindMemory(to: UInt32.self, capacity: 1).pointee / 86400
 | 
			
		||||
                        let senderLength = Int(buffer.load(fromByteOffset: offset + 5, as: UInt8.self))
 | 
			
		||||
                        if offset + senderLength + 10 > max { break }
 | 
			
		||||
                        let sender = decodeString(buffer, offset + 6, senderLength)
 | 
			
		||||
                        let textLength = Int(buffer.advanced(by: offset + senderLength + 6).bindMemory(to: UInt16.self, capacity: 1).pointee)
 | 
			
		||||
                        if(offset + senderLength + textLength + 10 > max) { break }
 | 
			
		||||
                        let text = decodeString(buffer, offset + senderLength + 8, textLength)
 | 
			
		||||
                        let mark = senderLength + textLength + 8
 | 
			
		||||
                        let size = buffer.advanced(by: offset + mark).bindMemory(to: UInt16.self, capacity: 1).pointee
 | 
			
		||||
                        if(size != mark || sender == nil || text == nil) { throw NSError(domain: "", code: 0) }
 | 
			
		||||
                        if(day > lastDay) {
 | 
			
		||||
                            lastDay = Int(day)
 | 
			
		||||
                            write(indexFd.fileDescriptor, &lastDay, 2)
 | 
			
		||||
                            write(indexFd.fileDescriptor, &pos, 5)
 | 
			
		||||
                        }
 | 
			
		||||
                        offset = offset + mark + 2
 | 
			
		||||
                        pos = pos + UInt64(mark + 2)
 | 
			
		||||
                    }
 | 
			
		||||
                    if(offset == 0) { throw NSError(domain: "", code: 0) }
 | 
			
		||||
                    fd.seek(toFileOffset: pos)
 | 
			
		||||
                }
 | 
			
		||||
            } catch {
 | 
			
		||||
                fd.truncateFile(atOffset: pos)
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@ -77,5 +77,8 @@ window.NativeLogs = {
 | 
			
		||||
    },
 | 
			
		||||
    getCharacters: function() {
 | 
			
		||||
        return sendMessage('Logs', 'getCharacters', {});
 | 
			
		||||
    },
 | 
			
		||||
    repair: function(character) {
 | 
			
		||||
        return sendMessage('Logs', 'repair', {character: character});
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
@ -1,6 +1,6 @@
 | 
			
		||||
{
 | 
			
		||||
  "name": "net.f_list.fchat",
 | 
			
		||||
  "version": "3.0.6",
 | 
			
		||||
  "version": "3.0.7",
 | 
			
		||||
  "displayName": "F-Chat",
 | 
			
		||||
  "author": "The F-List Team",
 | 
			
		||||
  "description": "F-List.net Chat Client",
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										96
									
								
								package.json
									
									
									
									
									
								
							
							
						
						
									
										96
									
								
								package.json
									
									
									
									
									
								
							@ -1,46 +1,54 @@
 | 
			
		||||
{
 | 
			
		||||
    "name": "flist-exported",
 | 
			
		||||
    "version": "1.0.0",
 | 
			
		||||
    "author": "The F-List Team",
 | 
			
		||||
    "description": "F-List Exported",
 | 
			
		||||
    "license": "MIT",
 | 
			
		||||
    "devDependencies": {
 | 
			
		||||
        "@fortawesome/fontawesome-free-webfonts": "^1.0.6",
 | 
			
		||||
        "@types/node": "^10.3.3",
 | 
			
		||||
        "@types/sortablejs": "^1.3.31",
 | 
			
		||||
        "axios": "^0.18.0",
 | 
			
		||||
        "bootstrap": "^4.1.0",
 | 
			
		||||
        "css-loader": "^0.28.11",
 | 
			
		||||
        "date-fns": "^1.28.5",
 | 
			
		||||
        "electron": "^2.0.2",
 | 
			
		||||
        "electron-builder": "^20.8.1",
 | 
			
		||||
        "electron-log": "^2.2.9",
 | 
			
		||||
        "electron-updater": "^2.21.4",
 | 
			
		||||
        "extract-text-webpack-plugin": "4.0.0-beta.0",
 | 
			
		||||
        "file-loader": "^1.1.10",
 | 
			
		||||
        "fork-ts-checker-webpack-plugin": "^0.4.1",
 | 
			
		||||
        "lodash": "^4.16.4",
 | 
			
		||||
        "node-sass": "^4.8.3",
 | 
			
		||||
        "optimize-css-assets-webpack-plugin": "^4.0.0",
 | 
			
		||||
        "qs": "^6.5.1",
 | 
			
		||||
        "raven-js": "^3.24.1",
 | 
			
		||||
        "sass-loader": "^7.0.1",
 | 
			
		||||
        "sortablejs": "^1.6.0",
 | 
			
		||||
        "ts-loader": "^4.2.0",
 | 
			
		||||
        "tslib": "^1.7.1",
 | 
			
		||||
        "tslint": "^5.7.0",
 | 
			
		||||
        "typescript": "^2.8.1",
 | 
			
		||||
        "vue": "^2.5.16",
 | 
			
		||||
        "vue-class-component": "^6.0.0",
 | 
			
		||||
        "vue-loader": "^15.2.4",
 | 
			
		||||
        "vue-property-decorator": "^6.0.0",
 | 
			
		||||
        "vue-template-compiler": "^2.5.16",
 | 
			
		||||
        "webpack": "^4.5.0"
 | 
			
		||||
    },
 | 
			
		||||
    "dependencies": {
 | 
			
		||||
        "@types/lodash": "^4.14.107",
 | 
			
		||||
        "keytar": "^4.2.1",
 | 
			
		||||
        "spellchecker": "^3.4.3",
 | 
			
		||||
        "style-loader": "^0.21.0"
 | 
			
		||||
    }
 | 
			
		||||
  "name": "flist-exported",
 | 
			
		||||
  "version": "1.0.0",
 | 
			
		||||
  "author": "The F-List Team",
 | 
			
		||||
  "description": "F-List Exported",
 | 
			
		||||
  "license": "MIT",
 | 
			
		||||
  "devDependencies": {
 | 
			
		||||
    "@fortawesome/fontawesome-free-webfonts": "^1.0.6",
 | 
			
		||||
    "@types/lodash": "^4.14.116",
 | 
			
		||||
    "@types/node": "^10.5.6",
 | 
			
		||||
    "@types/sortablejs": "^1.3.31",
 | 
			
		||||
    "axios": "^0.18.0",
 | 
			
		||||
    "bootstrap": "^4.1.3",
 | 
			
		||||
    "css-loader": "^1.0.0",
 | 
			
		||||
    "date-fns": "^1.28.5",
 | 
			
		||||
    "electron": "2.0.2",
 | 
			
		||||
    "electron-log": "^2.2.16",
 | 
			
		||||
    "electron-packager": "^12.1.0",
 | 
			
		||||
    "electron-rebuild": "^1.8.2",
 | 
			
		||||
    "extract-text-webpack-plugin": "4.0.0-beta.0",
 | 
			
		||||
    "file-loader": "^1.1.10",
 | 
			
		||||
    "fork-ts-checker-webpack-plugin": "^0.4.4",
 | 
			
		||||
    "lodash": "^4.16.4",
 | 
			
		||||
    "node-sass": "^4.8.3",
 | 
			
		||||
    "optimize-css-assets-webpack-plugin": "^5.0.0",
 | 
			
		||||
    "qs": "^6.5.1",
 | 
			
		||||
    "raven-js": "^3.26.4",
 | 
			
		||||
    "sass-loader": "^7.1.0",
 | 
			
		||||
    "sortablejs": "^1.6.0",
 | 
			
		||||
    "style-loader": "^0.21.0",
 | 
			
		||||
    "ts-loader": "^4.2.0",
 | 
			
		||||
    "tslib": "^1.7.1",
 | 
			
		||||
    "tslint": "^5.7.0",
 | 
			
		||||
    "typescript": "^3.0.1",
 | 
			
		||||
    "vue": "^2.5.17",
 | 
			
		||||
    "vue-class-component": "^6.0.0",
 | 
			
		||||
    "vue-loader": "^15.2.6",
 | 
			
		||||
    "vue-property-decorator": "^7.0.0",
 | 
			
		||||
    "vue-template-compiler": "^2.5.17",
 | 
			
		||||
    "webpack": "^4.16.4"
 | 
			
		||||
  },
 | 
			
		||||
  "dependencies": {
 | 
			
		||||
    "keytar": "^4.2.1",
 | 
			
		||||
    "spellchecker": "^3.4.3"
 | 
			
		||||
  },
 | 
			
		||||
  "optionalDependencies": {
 | 
			
		||||
    "appdmg": "^0.5.2",
 | 
			
		||||
    "electron-squirrel-startup": "^1.0.0",
 | 
			
		||||
    "electron-winstaller": "^2.6.4"
 | 
			
		||||
  },
 | 
			
		||||
  "scripts": {
 | 
			
		||||
    "postinstall": "electron-rebuild -o spellchecker,keytar"
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -12,13 +12,15 @@ All necessary files to build F-Chat 3.0 as an Electron, mobile or web applicatio
 | 
			
		||||
 - To build native Node assets, you will need to install Python 2.7 and the Visual C++ 2015 Build tools. [More information can be found in the node-gyp docs.](https://github.com/nodejs/node-gyp#installation)
 | 
			
		||||
 - Change into the `electron` directory.
 | 
			
		||||
 - Run `yarn build`/`yarn watch` to build assets. They are placed into the `app` directory.
 | 
			
		||||
 - You will probably need to rebuild the native dependencies (`spellchecker` and `keytar`) for electron. To do so, run `npm rebuild {NAME} --target={ELECTRON_VERSION} --arch=x64 --dist-url=https://atom.io/download/electron`. [See the electron documentation for more info.](https://github.com/electron/electron/blob/master/docs/tutorial/using-native-node-modules.md)
 | 
			
		||||
 - Run `yarn start` to start the app in debug mode. Use `Ctrl+Shift+I` to open the Chromium debugger.
 | 
			
		||||
 | 
			
		||||
### Packaging
 | 
			
		||||
See https://electron.atom.io/docs/tutorial/application-distribution/
 | 
			
		||||
 - Run `yarn build:dist` to create a minified production build.
 | 
			
		||||
 - Run `./node_modules/.bin/electron-builder` with [options specifying the platform you want to build for](https://www.electron.build/cli).
 | 
			
		||||
 - Run `yarn pack`. The generated installer is placed into the `dist` directory.
 | 
			
		||||
   - On Windows you can add the path to and password for a code signing certificate as arguments.
 | 
			
		||||
   - On Mac you can add your code signing identity as an argument. `zip` is required to be installed.
 | 
			
		||||
   - On Linux you can add a GPG key for signing as an argument. `mksquashfs` and `zsyncmake` are required to be installed.
 | 
			
		||||
 | 
			
		||||
## Building for Mobile
 | 
			
		||||
 - Change into the `mobile` directory.
 | 
			
		||||
 | 
			
		||||
@ -280,4 +280,8 @@ $genders: (
 | 
			
		||||
  border-radius: 100%;
 | 
			
		||||
  line-height: 0;
 | 
			
		||||
  box-shadow: 0 1px 4px #000;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.nav-link {
 | 
			
		||||
  cursor: pointer;
 | 
			
		||||
}
 | 
			
		||||
@ -1,5 +1,5 @@
 | 
			
		||||
$blue-color: #06f;
 | 
			
		||||
 | 
			
		||||
.blackText {
 | 
			
		||||
  text-shadow: $gray-600 1px 1px 1px, $gray-600 -1px 1px 1px, $gray-600 1px -1px 1px, $gray-600 -1px -1px 1px;
 | 
			
		||||
  text-shadow: $gray-600 1px 1px, $gray-600 -1px 1px, $gray-600 1px -1px, $gray-600 -1px -1px;
 | 
			
		||||
}
 | 
			
		||||
@ -59,7 +59,4 @@ $pagination-active-color: $link-color;
 | 
			
		||||
$text-background-color: $gray-100;
 | 
			
		||||
$text-background-color-disabled: $gray-200;
 | 
			
		||||
 | 
			
		||||
// Dark theme helpers
 | 
			
		||||
$theme-is-dark: true;
 | 
			
		||||
 | 
			
		||||
@import "invert";
 | 
			
		||||
@ -1,9 +1,9 @@
 | 
			
		||||
.purpleText {
 | 
			
		||||
  text-shadow: #306 1px 1px 1px, #306 -1px 1px 1px, #306 1px -1px 1px, #306 -1px -1px 1px;
 | 
			
		||||
  text-shadow: #306 1px 1px, #306 -1px 1px, #306 1px -1px, #306 -1px -1px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.blackText {
 | 
			
		||||
  text-shadow: $gray-600 1px 1px 1px, $gray-600 -1px 1px 1px, $gray-600 1px -1px 1px, $gray-600 -1px -1px 1px;
 | 
			
		||||
  text-shadow: $gray-600 1px 1px, $gray-600 -1px 1px, $gray-600 1px -1px, $gray-600 -1px -1px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
$blue-color: #06f;
 | 
			
		||||
@ -57,7 +57,4 @@ $pagination-active-color: $link-color;
 | 
			
		||||
$text-background-color: $gray-200;
 | 
			
		||||
$text-background-color-disabled: $gray-100;
 | 
			
		||||
 | 
			
		||||
// Dark theme helpers
 | 
			
		||||
$theme-is-dark: true;
 | 
			
		||||
 | 
			
		||||
@import "invert";
 | 
			
		||||
@ -15,6 +15,5 @@
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Alert color levels
 | 
			
		||||
$alert-bg-level: 7;
 | 
			
		||||
$alert-border-level: 6;
 | 
			
		||||
$alert-color-level: -8;
 | 
			
		||||
$alert-border-level: 4;
 | 
			
		||||
$theme-is-dark: true;
 | 
			
		||||
							
								
								
									
										423
									
								
								scss/yarn.lock
									
									
									
									
									
								
							
							
						
						
									
										423
									
								
								scss/yarn.lock
									
									
									
									
									
								
							@ -3,14 +3,14 @@
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
"@fortawesome/fontawesome-free-webfonts@^1.0.3":
 | 
			
		||||
  version "1.0.4"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-free-webfonts/-/fontawesome-free-webfonts-1.0.4.tgz#bac5d89755bf3bc2d2b4deee47d92febf641bb1f"
 | 
			
		||||
  version "1.0.9"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-free-webfonts/-/fontawesome-free-webfonts-1.0.9.tgz#72f2c10453422aba0d338fa6a9cb761b50ba24d5"
 | 
			
		||||
 | 
			
		||||
abbrev@1:
 | 
			
		||||
  version "1.1.1"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8"
 | 
			
		||||
 | 
			
		||||
ajv@^5.1.0:
 | 
			
		||||
ajv@^5.1.0, ajv@^5.3.0:
 | 
			
		||||
  version "5.5.2"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/ajv/-/ajv-5.5.2.tgz#73b5eeca3fab653e3d3f9422b341ad42205dc965"
 | 
			
		||||
  dependencies:
 | 
			
		||||
@ -27,6 +27,10 @@ ansi-regex@^2.0.0:
 | 
			
		||||
  version "2.1.1"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df"
 | 
			
		||||
 | 
			
		||||
ansi-regex@^3.0.0:
 | 
			
		||||
  version "3.0.0"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-3.0.0.tgz#ed0317c322064f79466c02966bddb605ab37d998"
 | 
			
		||||
 | 
			
		||||
ansi-styles@^2.2.1:
 | 
			
		||||
  version "2.2.1"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-2.2.1.tgz#b432dd3358b634cf75e1e4664368240533c1ddbe"
 | 
			
		||||
@ -36,8 +40,8 @@ aproba@^1.0.3:
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/aproba/-/aproba-1.2.0.tgz#6802e6264efd18c790a1b0d517f0f2627bf2c94a"
 | 
			
		||||
 | 
			
		||||
are-we-there-yet@~1.1.2:
 | 
			
		||||
  version "1.1.4"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/are-we-there-yet/-/are-we-there-yet-1.1.4.tgz#bb5dca382bb94f05e15194373d16fd3ba1ca110d"
 | 
			
		||||
  version "1.1.5"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz#4b35c2944f062a8bfcda66410760350fe9ddfc21"
 | 
			
		||||
  dependencies:
 | 
			
		||||
    delegates "^1.0.0"
 | 
			
		||||
    readable-stream "^2.0.6"
 | 
			
		||||
@ -47,17 +51,15 @@ array-find-index@^1.0.1:
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/array-find-index/-/array-find-index-1.0.2.tgz#df010aa1287e164bbda6f9723b0a96a1ec4187a1"
 | 
			
		||||
 | 
			
		||||
asn1@~0.2.3:
 | 
			
		||||
  version "0.2.3"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.3.tgz#dac8787713c9966849fc8180777ebe9c1ddf3b86"
 | 
			
		||||
  version "0.2.4"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.4.tgz#8d2475dfab553bb33e77b54e59e880bb8ce23136"
 | 
			
		||||
  dependencies:
 | 
			
		||||
    safer-buffer "~2.1.0"
 | 
			
		||||
 | 
			
		||||
assert-plus@1.0.0, assert-plus@^1.0.0:
 | 
			
		||||
  version "1.0.0"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-1.0.0.tgz#f12e0f3c5d77b0b1cdd9146942e4e96c1e4dd525"
 | 
			
		||||
 | 
			
		||||
assert-plus@^0.2.0:
 | 
			
		||||
  version "0.2.0"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-0.2.0.tgz#d74e1b87e7affc0db8aadb7021f3fe48101ab234"
 | 
			
		||||
 | 
			
		||||
async-foreach@^0.1.3:
 | 
			
		||||
  version "0.1.3"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/async-foreach/-/async-foreach-0.1.3.tgz#36121f845c0578172de419a97dbeb1d16ec34542"
 | 
			
		||||
@ -66,25 +68,21 @@ asynckit@^0.4.0:
 | 
			
		||||
  version "0.4.0"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79"
 | 
			
		||||
 | 
			
		||||
aws-sign2@~0.6.0:
 | 
			
		||||
  version "0.6.0"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.6.0.tgz#14342dd38dbcc94d0e5b87d763cd63612c0e794f"
 | 
			
		||||
 | 
			
		||||
aws-sign2@~0.7.0:
 | 
			
		||||
  version "0.7.0"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.7.0.tgz#b46e890934a9591f2d2f6f86d7e6a9f1b3fe76a8"
 | 
			
		||||
 | 
			
		||||
aws4@^1.2.1, aws4@^1.6.0:
 | 
			
		||||
  version "1.6.0"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.6.0.tgz#83ef5ca860b2b32e4a0deedee8c771b9db57471e"
 | 
			
		||||
aws4@^1.6.0, aws4@^1.8.0:
 | 
			
		||||
  version "1.8.0"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.8.0.tgz#f0e003d9ca9e7f59c7a508945d7b2ef9a04a542f"
 | 
			
		||||
 | 
			
		||||
balanced-match@^1.0.0:
 | 
			
		||||
  version "1.0.0"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767"
 | 
			
		||||
 | 
			
		||||
bcrypt-pbkdf@^1.0.0:
 | 
			
		||||
  version "1.0.1"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.1.tgz#63bc5dcb61331b92bc05fd528953c33462a06f8d"
 | 
			
		||||
  version "1.0.2"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz#a4301d389b6a43f9b67ff3ca11a3f6637e360e9e"
 | 
			
		||||
  dependencies:
 | 
			
		||||
    tweetnacl "^0.14.3"
 | 
			
		||||
 | 
			
		||||
@ -94,27 +92,9 @@ block-stream@*:
 | 
			
		||||
  dependencies:
 | 
			
		||||
    inherits "~2.0.0"
 | 
			
		||||
 | 
			
		||||
boom@2.x.x:
 | 
			
		||||
  version "2.10.1"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/boom/-/boom-2.10.1.tgz#39c8918ceff5799f83f9492a848f625add0c766f"
 | 
			
		||||
  dependencies:
 | 
			
		||||
    hoek "2.x.x"
 | 
			
		||||
 | 
			
		||||
boom@4.x.x:
 | 
			
		||||
  version "4.3.1"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/boom/-/boom-4.3.1.tgz#4f8a3005cb4a7e3889f749030fd25b96e01d2e31"
 | 
			
		||||
  dependencies:
 | 
			
		||||
    hoek "4.x.x"
 | 
			
		||||
 | 
			
		||||
boom@5.x.x:
 | 
			
		||||
  version "5.2.0"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/boom/-/boom-5.2.0.tgz#5dd9da6ee3a5f302077436290cb717d3f4a54e02"
 | 
			
		||||
  dependencies:
 | 
			
		||||
    hoek "4.x.x"
 | 
			
		||||
 | 
			
		||||
bootstrap@^4.0.0:
 | 
			
		||||
  version "4.0.0"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/bootstrap/-/bootstrap-4.0.0.tgz#ceb03842c145fcc1b9b4e15da2a05656ba68469a"
 | 
			
		||||
  version "4.1.3"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/bootstrap/-/bootstrap-4.1.3.tgz#0eb371af2c8448e8c210411d0cb824a6409a12be"
 | 
			
		||||
 | 
			
		||||
brace-expansion@^1.1.7:
 | 
			
		||||
  version "1.1.11"
 | 
			
		||||
@ -142,10 +122,6 @@ camelcase@^3.0.0:
 | 
			
		||||
  version "3.0.0"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-3.0.0.tgz#32fc4b9fcdaf845fcdf7e73bb97cac2261f0ab0a"
 | 
			
		||||
 | 
			
		||||
caseless@~0.11.0:
 | 
			
		||||
  version "0.11.0"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.11.0.tgz#715b96ea9841593cc33067923f5ec60ebda4f7d7"
 | 
			
		||||
 | 
			
		||||
caseless@~0.12.0:
 | 
			
		||||
  version "0.12.0"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc"
 | 
			
		||||
@ -176,16 +152,12 @@ code-point-at@^1.0.0:
 | 
			
		||||
  version "1.1.0"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77"
 | 
			
		||||
 | 
			
		||||
combined-stream@1.0.6, combined-stream@^1.0.5, combined-stream@~1.0.5:
 | 
			
		||||
combined-stream@1.0.6, combined-stream@~1.0.5, combined-stream@~1.0.6:
 | 
			
		||||
  version "1.0.6"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.6.tgz#723e7df6e801ac5613113a7e445a9b69cb632818"
 | 
			
		||||
  dependencies:
 | 
			
		||||
    delayed-stream "~1.0.0"
 | 
			
		||||
 | 
			
		||||
commander@^2.9.0:
 | 
			
		||||
  version "2.14.1"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/commander/-/commander-2.14.1.tgz#2235123e37af8ca3c65df45b026dbd357b01b9aa"
 | 
			
		||||
 | 
			
		||||
concat-map@0.0.1:
 | 
			
		||||
  version "0.0.1"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
 | 
			
		||||
@ -205,18 +177,6 @@ cross-spawn@^3.0.0:
 | 
			
		||||
    lru-cache "^4.0.1"
 | 
			
		||||
    which "^1.2.9"
 | 
			
		||||
 | 
			
		||||
cryptiles@2.x.x:
 | 
			
		||||
  version "2.0.5"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/cryptiles/-/cryptiles-2.0.5.tgz#3bdfecdc608147c1c67202fa291e7dca59eaa3b8"
 | 
			
		||||
  dependencies:
 | 
			
		||||
    boom "2.x.x"
 | 
			
		||||
 | 
			
		||||
cryptiles@3.x.x:
 | 
			
		||||
  version "3.1.2"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/cryptiles/-/cryptiles-3.1.2.tgz#a89fbb220f5ce25ec56e8c4aa8a4fd7b5b0d29fe"
 | 
			
		||||
  dependencies:
 | 
			
		||||
    boom "5.x.x"
 | 
			
		||||
 | 
			
		||||
currently-unhandled@^0.4.1:
 | 
			
		||||
  version "0.4.1"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/currently-unhandled/-/currently-unhandled-0.4.1.tgz#988df33feab191ef799a61369dd76c17adf957ea"
 | 
			
		||||
@ -242,14 +202,15 @@ delegates@^1.0.0:
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a"
 | 
			
		||||
 | 
			
		||||
ecc-jsbn@~0.1.1:
 | 
			
		||||
  version "0.1.1"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.1.tgz#0fc73a9ed5f0d53c38193398523ef7e543777505"
 | 
			
		||||
  version "0.1.2"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz#3a83a904e54353287874c564b7549386849a98c9"
 | 
			
		||||
  dependencies:
 | 
			
		||||
    jsbn "~0.1.0"
 | 
			
		||||
    safer-buffer "^2.1.0"
 | 
			
		||||
 | 
			
		||||
error-ex@^1.2.0:
 | 
			
		||||
  version "1.3.1"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.1.tgz#f855a86ce61adc4e8621c3cda21e7a7612c3a8dc"
 | 
			
		||||
  version "1.3.2"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf"
 | 
			
		||||
  dependencies:
 | 
			
		||||
    is-arrayish "^0.2.1"
 | 
			
		||||
 | 
			
		||||
@ -257,9 +218,9 @@ escape-string-regexp@^1.0.2:
 | 
			
		||||
  version "1.0.5"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4"
 | 
			
		||||
 | 
			
		||||
extend@~3.0.0, extend@~3.0.1:
 | 
			
		||||
  version "3.0.1"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.1.tgz#a755ea7bc1adfcc5a31ce7e762dbaadc5e636444"
 | 
			
		||||
extend@~3.0.1, extend@~3.0.2:
 | 
			
		||||
  version "3.0.2"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa"
 | 
			
		||||
 | 
			
		||||
extsprintf@1.3.0:
 | 
			
		||||
  version "1.3.0"
 | 
			
		||||
@ -288,15 +249,7 @@ forever-agent@~0.6.1:
 | 
			
		||||
  version "0.6.1"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91"
 | 
			
		||||
 | 
			
		||||
form-data@~2.1.1:
 | 
			
		||||
  version "2.1.4"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.1.4.tgz#33c183acf193276ecaa98143a69e94bfee1750d1"
 | 
			
		||||
  dependencies:
 | 
			
		||||
    asynckit "^0.4.0"
 | 
			
		||||
    combined-stream "^1.0.5"
 | 
			
		||||
    mime-types "^2.1.12"
 | 
			
		||||
 | 
			
		||||
form-data@~2.3.1:
 | 
			
		||||
form-data@~2.3.1, form-data@~2.3.2:
 | 
			
		||||
  version "2.3.2"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.3.2.tgz#4970498be604c20c005d4f5c23aecd21d6b49099"
 | 
			
		||||
  dependencies:
 | 
			
		||||
@ -331,24 +284,14 @@ gauge@~2.7.3:
 | 
			
		||||
    wide-align "^1.1.0"
 | 
			
		||||
 | 
			
		||||
gaze@^1.0.0:
 | 
			
		||||
  version "1.1.2"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/gaze/-/gaze-1.1.2.tgz#847224677adb8870d679257ed3388fdb61e40105"
 | 
			
		||||
  version "1.1.3"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/gaze/-/gaze-1.1.3.tgz#c441733e13b927ac8c0ff0b4c3b033f28812924a"
 | 
			
		||||
  dependencies:
 | 
			
		||||
    globule "^1.0.0"
 | 
			
		||||
 | 
			
		||||
generate-function@^2.0.0:
 | 
			
		||||
  version "2.0.0"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/generate-function/-/generate-function-2.0.0.tgz#6858fe7c0969b7d4e9093337647ac79f60dfbe74"
 | 
			
		||||
 | 
			
		||||
generate-object-property@^1.1.0:
 | 
			
		||||
  version "1.2.0"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/generate-object-property/-/generate-object-property-1.2.0.tgz#9c0e1c40308ce804f4783618b937fa88f99d50d0"
 | 
			
		||||
  dependencies:
 | 
			
		||||
    is-property "^1.0.0"
 | 
			
		||||
 | 
			
		||||
get-caller-file@^1.0.1:
 | 
			
		||||
  version "1.0.2"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-1.0.2.tgz#f702e63127e7e231c160a80c1554acb70d5047e5"
 | 
			
		||||
  version "1.0.3"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-1.0.3.tgz#f978fa4c90d1dfe7ff2d6beda2a515e713bdcf4a"
 | 
			
		||||
 | 
			
		||||
get-stdin@^4.0.1:
 | 
			
		||||
  version "4.0.1"
 | 
			
		||||
@ -382,11 +325,11 @@ glob@^7.0.0, glob@^7.0.3, glob@^7.0.5, glob@~7.1.1:
 | 
			
		||||
    path-is-absolute "^1.0.0"
 | 
			
		||||
 | 
			
		||||
globule@^1.0.0:
 | 
			
		||||
  version "1.2.0"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/globule/-/globule-1.2.0.tgz#1dc49c6822dd9e8a2fa00ba2a295006e8664bd09"
 | 
			
		||||
  version "1.2.1"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/globule/-/globule-1.2.1.tgz#5dffb1b191f22d20797a9369b49eab4e9839696d"
 | 
			
		||||
  dependencies:
 | 
			
		||||
    glob "~7.1.1"
 | 
			
		||||
    lodash "~4.17.4"
 | 
			
		||||
    lodash "~4.17.10"
 | 
			
		||||
    minimatch "~3.0.2"
 | 
			
		||||
 | 
			
		||||
graceful-fs@^4.1.2:
 | 
			
		||||
@ -397,15 +340,6 @@ har-schema@^2.0.0:
 | 
			
		||||
  version "2.0.0"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-2.0.0.tgz#a94c2224ebcac04782a0d9035521f24735b7ec92"
 | 
			
		||||
 | 
			
		||||
har-validator@~2.0.6:
 | 
			
		||||
  version "2.0.6"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-2.0.6.tgz#cdcbc08188265ad119b6a5a7c8ab70eecfb5d27d"
 | 
			
		||||
  dependencies:
 | 
			
		||||
    chalk "^1.1.1"
 | 
			
		||||
    commander "^2.9.0"
 | 
			
		||||
    is-my-json-valid "^2.12.4"
 | 
			
		||||
    pinkie-promise "^2.0.0"
 | 
			
		||||
 | 
			
		||||
har-validator@~5.0.3:
 | 
			
		||||
  version "5.0.3"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-5.0.3.tgz#ba402c266194f15956ef15e0fcf242993f6a7dfd"
 | 
			
		||||
@ -413,6 +347,13 @@ har-validator@~5.0.3:
 | 
			
		||||
    ajv "^5.1.0"
 | 
			
		||||
    har-schema "^2.0.0"
 | 
			
		||||
 | 
			
		||||
har-validator@~5.1.0:
 | 
			
		||||
  version "5.1.0"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-5.1.0.tgz#44657f5688a22cfd4b72486e81b3a3fb11742c29"
 | 
			
		||||
  dependencies:
 | 
			
		||||
    ajv "^5.3.0"
 | 
			
		||||
    har-schema "^2.0.0"
 | 
			
		||||
 | 
			
		||||
has-ansi@^2.0.0:
 | 
			
		||||
  version "2.0.0"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/has-ansi/-/has-ansi-2.0.0.tgz#34f5049ce1ecdf2b0649af3ef24e45ed35416d91"
 | 
			
		||||
@ -423,43 +364,9 @@ has-unicode@^2.0.0:
 | 
			
		||||
  version "2.0.1"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/has-unicode/-/has-unicode-2.0.1.tgz#e0e6fe6a28cf51138855e086d1691e771de2a8b9"
 | 
			
		||||
 | 
			
		||||
hawk@~3.1.3:
 | 
			
		||||
  version "3.1.3"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/hawk/-/hawk-3.1.3.tgz#078444bd7c1640b0fe540d2c9b73d59678e8e1c4"
 | 
			
		||||
  dependencies:
 | 
			
		||||
    boom "2.x.x"
 | 
			
		||||
    cryptiles "2.x.x"
 | 
			
		||||
    hoek "2.x.x"
 | 
			
		||||
    sntp "1.x.x"
 | 
			
		||||
 | 
			
		||||
hawk@~6.0.2:
 | 
			
		||||
  version "6.0.2"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/hawk/-/hawk-6.0.2.tgz#af4d914eb065f9b5ce4d9d11c1cb2126eecc3038"
 | 
			
		||||
  dependencies:
 | 
			
		||||
    boom "4.x.x"
 | 
			
		||||
    cryptiles "3.x.x"
 | 
			
		||||
    hoek "4.x.x"
 | 
			
		||||
    sntp "2.x.x"
 | 
			
		||||
 | 
			
		||||
hoek@2.x.x:
 | 
			
		||||
  version "2.16.3"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/hoek/-/hoek-2.16.3.tgz#20bb7403d3cea398e91dc4710a8ff1b8274a25ed"
 | 
			
		||||
 | 
			
		||||
hoek@4.x.x:
 | 
			
		||||
  version "4.2.1"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/hoek/-/hoek-4.2.1.tgz#9634502aa12c445dd5a7c5734b572bb8738aacbb"
 | 
			
		||||
 | 
			
		||||
hosted-git-info@^2.1.4:
 | 
			
		||||
  version "2.5.0"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.5.0.tgz#6d60e34b3abbc8313062c3b798ef8d901a07af3c"
 | 
			
		||||
 | 
			
		||||
http-signature@~1.1.0:
 | 
			
		||||
  version "1.1.1"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.1.1.tgz#df72e267066cd0ac67fb76adf8e134a8fbcf91bf"
 | 
			
		||||
  dependencies:
 | 
			
		||||
    assert-plus "^0.2.0"
 | 
			
		||||
    jsprim "^1.2.2"
 | 
			
		||||
    sshpk "^1.7.0"
 | 
			
		||||
  version "2.7.1"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.7.1.tgz#97f236977bd6e125408930ff6de3eec6281ec047"
 | 
			
		||||
 | 
			
		||||
http-signature@~1.2.0:
 | 
			
		||||
  version "1.2.0"
 | 
			
		||||
@ -516,23 +423,9 @@ is-fullwidth-code-point@^1.0.0:
 | 
			
		||||
  dependencies:
 | 
			
		||||
    number-is-nan "^1.0.0"
 | 
			
		||||
 | 
			
		||||
is-my-ip-valid@^1.0.0:
 | 
			
		||||
  version "1.0.0"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/is-my-ip-valid/-/is-my-ip-valid-1.0.0.tgz#7b351b8e8edd4d3995d4d066680e664d94696824"
 | 
			
		||||
 | 
			
		||||
is-my-json-valid@^2.12.4:
 | 
			
		||||
  version "2.17.2"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/is-my-json-valid/-/is-my-json-valid-2.17.2.tgz#6b2103a288e94ef3de5cf15d29dd85fc4b78d65c"
 | 
			
		||||
  dependencies:
 | 
			
		||||
    generate-function "^2.0.0"
 | 
			
		||||
    generate-object-property "^1.1.0"
 | 
			
		||||
    is-my-ip-valid "^1.0.0"
 | 
			
		||||
    jsonpointer "^4.0.0"
 | 
			
		||||
    xtend "^4.0.0"
 | 
			
		||||
 | 
			
		||||
is-property@^1.0.0:
 | 
			
		||||
  version "1.0.2"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/is-property/-/is-property-1.0.2.tgz#57fe1c4e48474edd65b09911f26b1cd4095dda84"
 | 
			
		||||
is-fullwidth-code-point@^2.0.0:
 | 
			
		||||
  version "2.0.0"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz#a3b30a5c4f199183167aaab93beefae3ddfb654f"
 | 
			
		||||
 | 
			
		||||
is-typedarray@~1.0.0:
 | 
			
		||||
  version "1.0.0"
 | 
			
		||||
@ -555,8 +448,8 @@ isstream@~0.1.2:
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a"
 | 
			
		||||
 | 
			
		||||
js-base64@^2.1.8:
 | 
			
		||||
  version "2.4.3"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/js-base64/-/js-base64-2.4.3.tgz#2e545ec2b0f2957f41356510205214e98fad6582"
 | 
			
		||||
  version "2.4.8"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/js-base64/-/js-base64-2.4.8.tgz#57a9b130888f956834aa40c5b165ba59c758f033"
 | 
			
		||||
 | 
			
		||||
jsbn@~0.1.0:
 | 
			
		||||
  version "0.1.1"
 | 
			
		||||
@ -574,10 +467,6 @@ json-stringify-safe@~5.0.1:
 | 
			
		||||
  version "5.0.1"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb"
 | 
			
		||||
 | 
			
		||||
jsonpointer@^4.0.0:
 | 
			
		||||
  version "4.0.1"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/jsonpointer/-/jsonpointer-4.0.1.tgz#4fd92cb34e0e9db3c89c8622ecf51f9b978c6cb9"
 | 
			
		||||
 | 
			
		||||
jsprim@^1.2.2:
 | 
			
		||||
  version "1.4.1"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.1.tgz#313e66bc1e5cc06e438bc1b7499c2e5c56acb6a2"
 | 
			
		||||
@ -615,9 +504,9 @@ lodash.mergewith@^4.6.0:
 | 
			
		||||
  version "4.6.1"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/lodash.mergewith/-/lodash.mergewith-4.6.1.tgz#639057e726c3afbdb3e7d42741caa8d6e4335927"
 | 
			
		||||
 | 
			
		||||
lodash@^4.0.0, lodash@~4.17.4:
 | 
			
		||||
  version "4.17.5"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.5.tgz#99a92d65c0272debe8c96b6057bc8fbfa3bed511"
 | 
			
		||||
lodash@^4.0.0, lodash@~4.17.10:
 | 
			
		||||
  version "4.17.10"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.10.tgz#1b7793cf7259ea38fb3661d4d38b3260af8ae4e7"
 | 
			
		||||
 | 
			
		||||
loud-rejection@^1.0.0:
 | 
			
		||||
  version "1.6.0"
 | 
			
		||||
@ -627,8 +516,8 @@ loud-rejection@^1.0.0:
 | 
			
		||||
    signal-exit "^3.0.0"
 | 
			
		||||
 | 
			
		||||
lru-cache@^4.0.1:
 | 
			
		||||
  version "4.1.1"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-4.1.1.tgz#622e32e82488b49279114a4f9ecf45e7cd6bba55"
 | 
			
		||||
  version "4.1.3"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-4.1.3.tgz#a1175cf3496dfc8436c156c334b4955992bce69c"
 | 
			
		||||
  dependencies:
 | 
			
		||||
    pseudomap "^1.0.2"
 | 
			
		||||
    yallist "^2.1.2"
 | 
			
		||||
@ -652,17 +541,17 @@ meow@^3.7.0:
 | 
			
		||||
    redent "^1.0.0"
 | 
			
		||||
    trim-newlines "^1.0.0"
 | 
			
		||||
 | 
			
		||||
mime-db@~1.33.0:
 | 
			
		||||
  version "1.33.0"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.33.0.tgz#a3492050a5cb9b63450541e39d9788d2272783db"
 | 
			
		||||
mime-db@~1.35.0:
 | 
			
		||||
  version "1.35.0"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.35.0.tgz#0569d657466491283709663ad379a99b90d9ab47"
 | 
			
		||||
 | 
			
		||||
mime-types@^2.1.12, mime-types@~2.1.17, mime-types@~2.1.7:
 | 
			
		||||
  version "2.1.18"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.18.tgz#6f323f60a83d11146f831ff11fd66e2fe5503bb8"
 | 
			
		||||
mime-types@^2.1.12, mime-types@~2.1.17, mime-types@~2.1.19:
 | 
			
		||||
  version "2.1.19"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.19.tgz#71e464537a7ef81c15f2db9d97e913fc0ff606f0"
 | 
			
		||||
  dependencies:
 | 
			
		||||
    mime-db "~1.33.0"
 | 
			
		||||
    mime-db "~1.35.0"
 | 
			
		||||
 | 
			
		||||
"minimatch@2 || 3", minimatch@^3.0.2, minimatch@^3.0.4, minimatch@~3.0.2:
 | 
			
		||||
"minimatch@2 || 3", minimatch@^3.0.4, minimatch@~3.0.2:
 | 
			
		||||
  version "3.0.4"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083"
 | 
			
		||||
  dependencies:
 | 
			
		||||
@ -682,31 +571,30 @@ minimist@^1.1.3:
 | 
			
		||||
  dependencies:
 | 
			
		||||
    minimist "0.0.8"
 | 
			
		||||
 | 
			
		||||
nan@^2.3.2:
 | 
			
		||||
  version "2.9.2"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/nan/-/nan-2.9.2.tgz#f564d75f5f8f36a6d9456cca7a6c4fe488ab7866"
 | 
			
		||||
nan@^2.10.0:
 | 
			
		||||
  version "2.10.0"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/nan/-/nan-2.10.0.tgz#96d0cd610ebd58d4b4de9cc0c6828cda99c7548f"
 | 
			
		||||
 | 
			
		||||
node-gyp@^3.3.1:
 | 
			
		||||
  version "3.6.2"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/node-gyp/-/node-gyp-3.6.2.tgz#9bfbe54562286284838e750eac05295853fa1c60"
 | 
			
		||||
node-gyp@^3.8.0:
 | 
			
		||||
  version "3.8.0"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/node-gyp/-/node-gyp-3.8.0.tgz#540304261c330e80d0d5edce253a68cb3964218c"
 | 
			
		||||
  dependencies:
 | 
			
		||||
    fstream "^1.0.0"
 | 
			
		||||
    glob "^7.0.3"
 | 
			
		||||
    graceful-fs "^4.1.2"
 | 
			
		||||
    minimatch "^3.0.2"
 | 
			
		||||
    mkdirp "^0.5.0"
 | 
			
		||||
    nopt "2 || 3"
 | 
			
		||||
    npmlog "0 || 1 || 2 || 3 || 4"
 | 
			
		||||
    osenv "0"
 | 
			
		||||
    request "2"
 | 
			
		||||
    request "^2.87.0"
 | 
			
		||||
    rimraf "2"
 | 
			
		||||
    semver "~5.3.0"
 | 
			
		||||
    tar "^2.0.0"
 | 
			
		||||
    which "1"
 | 
			
		||||
 | 
			
		||||
node-sass@^4.7.2:
 | 
			
		||||
  version "4.7.2"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/node-sass/-/node-sass-4.7.2.tgz#9366778ba1469eb01438a9e8592f4262bcb6794e"
 | 
			
		||||
  version "4.9.3"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/node-sass/-/node-sass-4.9.3.tgz#f407cf3d66f78308bb1e346b24fa428703196224"
 | 
			
		||||
  dependencies:
 | 
			
		||||
    async-foreach "^0.1.3"
 | 
			
		||||
    chalk "^1.1.1"
 | 
			
		||||
@ -720,10 +608,10 @@ node-sass@^4.7.2:
 | 
			
		||||
    lodash.mergewith "^4.6.0"
 | 
			
		||||
    meow "^3.7.0"
 | 
			
		||||
    mkdirp "^0.5.1"
 | 
			
		||||
    nan "^2.3.2"
 | 
			
		||||
    node-gyp "^3.3.1"
 | 
			
		||||
    nan "^2.10.0"
 | 
			
		||||
    node-gyp "^3.8.0"
 | 
			
		||||
    npmlog "^4.0.0"
 | 
			
		||||
    request "~2.79.0"
 | 
			
		||||
    request "2.87.0"
 | 
			
		||||
    sass-graph "^2.2.4"
 | 
			
		||||
    stdout-stream "^1.4.0"
 | 
			
		||||
    "true-case-path" "^1.0.2"
 | 
			
		||||
@ -756,10 +644,14 @@ number-is-nan@^1.0.0:
 | 
			
		||||
  version "1.0.1"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/number-is-nan/-/number-is-nan-1.0.1.tgz#097b602b53422a522c1afb8790318336941a011d"
 | 
			
		||||
 | 
			
		||||
oauth-sign@~0.8.1, oauth-sign@~0.8.2:
 | 
			
		||||
oauth-sign@~0.8.2:
 | 
			
		||||
  version "0.8.2"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.8.2.tgz#46a6ab7f0aead8deae9ec0565780b7d4efeb9d43"
 | 
			
		||||
 | 
			
		||||
oauth-sign@~0.9.0:
 | 
			
		||||
  version "0.9.0"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.9.0.tgz#47a7b016baa68b5fa0ecf3dee08a85c679ac6455"
 | 
			
		||||
 | 
			
		||||
object-assign@^4.0.1, object-assign@^4.1.0:
 | 
			
		||||
  version "4.1.1"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863"
 | 
			
		||||
@ -841,17 +733,17 @@ pseudomap@^1.0.2:
 | 
			
		||||
  version "1.0.2"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/pseudomap/-/pseudomap-1.0.2.tgz#f052a28da70e618917ef0a8ac34c1ae5a68286b3"
 | 
			
		||||
 | 
			
		||||
psl@^1.1.24:
 | 
			
		||||
  version "1.1.29"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/psl/-/psl-1.1.29.tgz#60f580d360170bb722a797cc704411e6da850c67"
 | 
			
		||||
 | 
			
		||||
punycode@^1.4.1:
 | 
			
		||||
  version "1.4.1"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e"
 | 
			
		||||
 | 
			
		||||
qs@~6.3.0:
 | 
			
		||||
  version "6.3.2"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/qs/-/qs-6.3.2.tgz#e75bd5f6e268122a2a0e0bda630b2550c166502c"
 | 
			
		||||
 | 
			
		||||
qs@~6.5.1:
 | 
			
		||||
  version "6.5.1"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.1.tgz#349cdf6eef89ec45c12d7d5eb3fc0c870343a6d8"
 | 
			
		||||
qs@~6.5.1, qs@~6.5.2:
 | 
			
		||||
  version "6.5.2"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36"
 | 
			
		||||
 | 
			
		||||
read-pkg-up@^1.0.1:
 | 
			
		||||
  version "1.0.1"
 | 
			
		||||
@ -869,15 +761,15 @@ read-pkg@^1.0.0:
 | 
			
		||||
    path-type "^1.0.0"
 | 
			
		||||
 | 
			
		||||
readable-stream@^2.0.1, readable-stream@^2.0.6:
 | 
			
		||||
  version "2.3.5"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.5.tgz#b4f85003a938cbb6ecbce2a124fb1012bd1a838d"
 | 
			
		||||
  version "2.3.6"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.6.tgz#b11c27d88b8ff1fbe070643cf94b0c79ae1b0aaf"
 | 
			
		||||
  dependencies:
 | 
			
		||||
    core-util-is "~1.0.0"
 | 
			
		||||
    inherits "~2.0.3"
 | 
			
		||||
    isarray "~1.0.0"
 | 
			
		||||
    process-nextick-args "~2.0.0"
 | 
			
		||||
    safe-buffer "~5.1.1"
 | 
			
		||||
    string_decoder "~1.0.3"
 | 
			
		||||
    string_decoder "~1.1.1"
 | 
			
		||||
    util-deprecate "~1.0.1"
 | 
			
		||||
 | 
			
		||||
redent@^1.0.0:
 | 
			
		||||
@ -893,9 +785,9 @@ repeating@^2.0.0:
 | 
			
		||||
  dependencies:
 | 
			
		||||
    is-finite "^1.0.0"
 | 
			
		||||
 | 
			
		||||
request@2:
 | 
			
		||||
  version "2.83.0"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/request/-/request-2.83.0.tgz#ca0b65da02ed62935887808e6f510381034e3356"
 | 
			
		||||
request@2.87.0:
 | 
			
		||||
  version "2.87.0"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/request/-/request-2.87.0.tgz#32f00235cd08d482b4d0d68db93a829c0ed5756e"
 | 
			
		||||
  dependencies:
 | 
			
		||||
    aws-sign2 "~0.7.0"
 | 
			
		||||
    aws4 "^1.6.0"
 | 
			
		||||
@ -905,7 +797,6 @@ request@2:
 | 
			
		||||
    forever-agent "~0.6.1"
 | 
			
		||||
    form-data "~2.3.1"
 | 
			
		||||
    har-validator "~5.0.3"
 | 
			
		||||
    hawk "~6.0.2"
 | 
			
		||||
    http-signature "~1.2.0"
 | 
			
		||||
    is-typedarray "~1.0.0"
 | 
			
		||||
    isstream "~0.1.2"
 | 
			
		||||
@ -915,35 +806,34 @@ request@2:
 | 
			
		||||
    performance-now "^2.1.0"
 | 
			
		||||
    qs "~6.5.1"
 | 
			
		||||
    safe-buffer "^5.1.1"
 | 
			
		||||
    stringstream "~0.0.5"
 | 
			
		||||
    tough-cookie "~2.3.3"
 | 
			
		||||
    tunnel-agent "^0.6.0"
 | 
			
		||||
    uuid "^3.1.0"
 | 
			
		||||
 | 
			
		||||
request@~2.79.0:
 | 
			
		||||
  version "2.79.0"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/request/-/request-2.79.0.tgz#4dfe5bf6be8b8cdc37fcf93e04b65577722710de"
 | 
			
		||||
request@^2.87.0:
 | 
			
		||||
  version "2.88.0"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/request/-/request-2.88.0.tgz#9c2fca4f7d35b592efe57c7f0a55e81052124fef"
 | 
			
		||||
  dependencies:
 | 
			
		||||
    aws-sign2 "~0.6.0"
 | 
			
		||||
    aws4 "^1.2.1"
 | 
			
		||||
    caseless "~0.11.0"
 | 
			
		||||
    combined-stream "~1.0.5"
 | 
			
		||||
    extend "~3.0.0"
 | 
			
		||||
    aws-sign2 "~0.7.0"
 | 
			
		||||
    aws4 "^1.8.0"
 | 
			
		||||
    caseless "~0.12.0"
 | 
			
		||||
    combined-stream "~1.0.6"
 | 
			
		||||
    extend "~3.0.2"
 | 
			
		||||
    forever-agent "~0.6.1"
 | 
			
		||||
    form-data "~2.1.1"
 | 
			
		||||
    har-validator "~2.0.6"
 | 
			
		||||
    hawk "~3.1.3"
 | 
			
		||||
    http-signature "~1.1.0"
 | 
			
		||||
    form-data "~2.3.2"
 | 
			
		||||
    har-validator "~5.1.0"
 | 
			
		||||
    http-signature "~1.2.0"
 | 
			
		||||
    is-typedarray "~1.0.0"
 | 
			
		||||
    isstream "~0.1.2"
 | 
			
		||||
    json-stringify-safe "~5.0.1"
 | 
			
		||||
    mime-types "~2.1.7"
 | 
			
		||||
    oauth-sign "~0.8.1"
 | 
			
		||||
    qs "~6.3.0"
 | 
			
		||||
    stringstream "~0.0.4"
 | 
			
		||||
    tough-cookie "~2.3.0"
 | 
			
		||||
    tunnel-agent "~0.4.1"
 | 
			
		||||
    uuid "^3.0.0"
 | 
			
		||||
    mime-types "~2.1.19"
 | 
			
		||||
    oauth-sign "~0.9.0"
 | 
			
		||||
    performance-now "^2.1.0"
 | 
			
		||||
    qs "~6.5.2"
 | 
			
		||||
    safe-buffer "^5.1.2"
 | 
			
		||||
    tough-cookie "~2.4.3"
 | 
			
		||||
    tunnel-agent "^0.6.0"
 | 
			
		||||
    uuid "^3.3.2"
 | 
			
		||||
 | 
			
		||||
require-directory@^2.1.1:
 | 
			
		||||
  version "2.1.1"
 | 
			
		||||
@ -959,9 +849,13 @@ rimraf@2:
 | 
			
		||||
  dependencies:
 | 
			
		||||
    glob "^7.0.5"
 | 
			
		||||
 | 
			
		||||
safe-buffer@^5.0.1, safe-buffer@^5.1.1, safe-buffer@~5.1.0, safe-buffer@~5.1.1:
 | 
			
		||||
  version "5.1.1"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.1.tgz#893312af69b2123def71f57889001671eeb2c853"
 | 
			
		||||
safe-buffer@^5.0.1, safe-buffer@^5.1.1, safe-buffer@^5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1:
 | 
			
		||||
  version "5.1.2"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d"
 | 
			
		||||
 | 
			
		||||
safer-buffer@^2.0.2, safer-buffer@^2.1.0, safer-buffer@~2.1.0:
 | 
			
		||||
  version "2.1.2"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a"
 | 
			
		||||
 | 
			
		||||
sass-graph@^2.2.4:
 | 
			
		||||
  version "2.2.4"
 | 
			
		||||
@ -995,18 +889,6 @@ signal-exit@^3.0.0:
 | 
			
		||||
  version "3.0.2"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.2.tgz#b5fdc08f1287ea1178628e415e25132b73646c6d"
 | 
			
		||||
 | 
			
		||||
sntp@1.x.x:
 | 
			
		||||
  version "1.0.9"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/sntp/-/sntp-1.0.9.tgz#6541184cc90aeea6c6e7b35e2659082443c66198"
 | 
			
		||||
  dependencies:
 | 
			
		||||
    hoek "2.x.x"
 | 
			
		||||
 | 
			
		||||
sntp@2.x.x:
 | 
			
		||||
  version "2.1.0"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/sntp/-/sntp-2.1.0.tgz#2c6cec14fedc2222739caf9b5c3d85d1cc5a2cc8"
 | 
			
		||||
  dependencies:
 | 
			
		||||
    hoek "4.x.x"
 | 
			
		||||
 | 
			
		||||
source-map@^0.4.2:
 | 
			
		||||
  version "0.4.4"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.4.4.tgz#eba4f5da9c0dc999de68032d8b4f76173652036b"
 | 
			
		||||
@ -1036,13 +918,14 @@ spdx-license-ids@^3.0.0:
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-3.0.0.tgz#7a7cd28470cc6d3a1cfe6d66886f6bc430d3ac87"
 | 
			
		||||
 | 
			
		||||
sshpk@^1.7.0:
 | 
			
		||||
  version "1.13.1"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.13.1.tgz#512df6da6287144316dc4c18fe1cf1d940739be3"
 | 
			
		||||
  version "1.14.2"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.14.2.tgz#c6fc61648a3d9c4e764fd3fcdf4ea105e492ba98"
 | 
			
		||||
  dependencies:
 | 
			
		||||
    asn1 "~0.2.3"
 | 
			
		||||
    assert-plus "^1.0.0"
 | 
			
		||||
    dashdash "^1.12.0"
 | 
			
		||||
    getpass "^0.1.1"
 | 
			
		||||
    safer-buffer "^2.0.2"
 | 
			
		||||
  optionalDependencies:
 | 
			
		||||
    bcrypt-pbkdf "^1.0.0"
 | 
			
		||||
    ecc-jsbn "~0.1.1"
 | 
			
		||||
@ -1063,22 +946,31 @@ string-width@^1.0.1, string-width@^1.0.2:
 | 
			
		||||
    is-fullwidth-code-point "^1.0.0"
 | 
			
		||||
    strip-ansi "^3.0.0"
 | 
			
		||||
 | 
			
		||||
string_decoder@~1.0.3:
 | 
			
		||||
  version "1.0.3"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.0.3.tgz#0fc67d7c141825de94282dd536bec6b9bce860ab"
 | 
			
		||||
"string-width@^1.0.2 || 2":
 | 
			
		||||
  version "2.1.1"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/string-width/-/string-width-2.1.1.tgz#ab93f27a8dc13d28cac815c462143a6d9012ae9e"
 | 
			
		||||
  dependencies:
 | 
			
		||||
    is-fullwidth-code-point "^2.0.0"
 | 
			
		||||
    strip-ansi "^4.0.0"
 | 
			
		||||
 | 
			
		||||
string_decoder@~1.1.1:
 | 
			
		||||
  version "1.1.1"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8"
 | 
			
		||||
  dependencies:
 | 
			
		||||
    safe-buffer "~5.1.0"
 | 
			
		||||
 | 
			
		||||
stringstream@~0.0.4, stringstream@~0.0.5:
 | 
			
		||||
  version "0.0.5"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/stringstream/-/stringstream-0.0.5.tgz#4e484cd4de5a0bbbee18e46307710a8a81621878"
 | 
			
		||||
 | 
			
		||||
strip-ansi@^3.0.0, strip-ansi@^3.0.1:
 | 
			
		||||
  version "3.0.1"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-3.0.1.tgz#6a385fb8853d952d5ff05d0e8aaf94278dc63dcf"
 | 
			
		||||
  dependencies:
 | 
			
		||||
    ansi-regex "^2.0.0"
 | 
			
		||||
 | 
			
		||||
strip-ansi@^4.0.0:
 | 
			
		||||
  version "4.0.0"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-4.0.0.tgz#a8479022eb1ac368a871389b635262c505ee368f"
 | 
			
		||||
  dependencies:
 | 
			
		||||
    ansi-regex "^3.0.0"
 | 
			
		||||
 | 
			
		||||
strip-bom@^2.0.0:
 | 
			
		||||
  version "2.0.0"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-2.0.0.tgz#6219a85616520491f35788bdbf1447a99c7e6b0e"
 | 
			
		||||
@ -1103,12 +995,19 @@ tar@^2.0.0:
 | 
			
		||||
    fstream "^1.0.2"
 | 
			
		||||
    inherits "2"
 | 
			
		||||
 | 
			
		||||
tough-cookie@~2.3.0, tough-cookie@~2.3.3:
 | 
			
		||||
tough-cookie@~2.3.3:
 | 
			
		||||
  version "2.3.4"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.3.4.tgz#ec60cee38ac675063ffc97a5c18970578ee83655"
 | 
			
		||||
  dependencies:
 | 
			
		||||
    punycode "^1.4.1"
 | 
			
		||||
 | 
			
		||||
tough-cookie@~2.4.3:
 | 
			
		||||
  version "2.4.3"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.4.3.tgz#53f36da3f47783b0925afa06ff9f3b165280f781"
 | 
			
		||||
  dependencies:
 | 
			
		||||
    psl "^1.1.24"
 | 
			
		||||
    punycode "^1.4.1"
 | 
			
		||||
 | 
			
		||||
trim-newlines@^1.0.0:
 | 
			
		||||
  version "1.0.0"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/trim-newlines/-/trim-newlines-1.0.0.tgz#5887966bb582a4503a41eb524f7d35011815a613"
 | 
			
		||||
@ -1125,10 +1024,6 @@ tunnel-agent@^0.6.0:
 | 
			
		||||
  dependencies:
 | 
			
		||||
    safe-buffer "^5.0.1"
 | 
			
		||||
 | 
			
		||||
tunnel-agent@~0.4.1:
 | 
			
		||||
  version "0.4.3"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.4.3.tgz#6373db76909fe570e08d73583365ed828a74eeeb"
 | 
			
		||||
 | 
			
		||||
tweetnacl@^0.14.3, tweetnacl@~0.14.0:
 | 
			
		||||
  version "0.14.5"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64"
 | 
			
		||||
@ -1137,13 +1032,13 @@ util-deprecate@~1.0.1:
 | 
			
		||||
  version "1.0.2"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
 | 
			
		||||
 | 
			
		||||
uuid@^3.0.0, uuid@^3.1.0:
 | 
			
		||||
  version "3.2.1"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.2.1.tgz#12c528bb9d58d0b9265d9a2f6f0fe8be17ff1f14"
 | 
			
		||||
uuid@^3.1.0, uuid@^3.3.2:
 | 
			
		||||
  version "3.3.2"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.3.2.tgz#1b4af4955eb3077c501c23872fc6513811587131"
 | 
			
		||||
 | 
			
		||||
validate-npm-package-license@^3.0.1:
 | 
			
		||||
  version "3.0.3"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.3.tgz#81643bcbef1bdfecd4623793dc4648948ba98338"
 | 
			
		||||
  version "3.0.4"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz#fc91f6b9c7ba15c857f4cb2c5defeec39d4f410a"
 | 
			
		||||
  dependencies:
 | 
			
		||||
    spdx-correct "^3.0.0"
 | 
			
		||||
    spdx-expression-parse "^3.0.0"
 | 
			
		||||
@ -1161,16 +1056,16 @@ which-module@^1.0.0:
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/which-module/-/which-module-1.0.0.tgz#bba63ca861948994ff307736089e3b96026c2a4f"
 | 
			
		||||
 | 
			
		||||
which@1, which@^1.2.9:
 | 
			
		||||
  version "1.3.0"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/which/-/which-1.3.0.tgz#ff04bdfc010ee547d780bec38e1ac1c2777d253a"
 | 
			
		||||
  version "1.3.1"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a"
 | 
			
		||||
  dependencies:
 | 
			
		||||
    isexe "^2.0.0"
 | 
			
		||||
 | 
			
		||||
wide-align@^1.1.0:
 | 
			
		||||
  version "1.1.2"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/wide-align/-/wide-align-1.1.2.tgz#571e0f1b0604636ebc0dfc21b0339bbe31341710"
 | 
			
		||||
  version "1.1.3"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/wide-align/-/wide-align-1.1.3.tgz#ae074e6bdc0c14a431e804e624549c633b000457"
 | 
			
		||||
  dependencies:
 | 
			
		||||
    string-width "^1.0.2"
 | 
			
		||||
    string-width "^1.0.2 || 2"
 | 
			
		||||
 | 
			
		||||
wrap-ansi@^2.0.0:
 | 
			
		||||
  version "2.1.0"
 | 
			
		||||
@ -1183,10 +1078,6 @@ wrappy@1:
 | 
			
		||||
  version "1.0.2"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
 | 
			
		||||
 | 
			
		||||
xtend@^4.0.0:
 | 
			
		||||
  version "4.0.1"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.1.tgz#a5c6d532be656e23db820efb943a1f04998d63af"
 | 
			
		||||
 | 
			
		||||
y18n@^3.2.1:
 | 
			
		||||
  version "3.2.1"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/y18n/-/y18n-3.2.1.tgz#6d15fba884c08679c0d77e88e7759e811e07fa41"
 | 
			
		||||
 | 
			
		||||
@ -63,6 +63,7 @@
 | 
			
		||||
    ],
 | 
			
		||||
    "cyclomatic-complexity": false,
 | 
			
		||||
    "eofline": false,
 | 
			
		||||
    "file-name-casing": false,
 | 
			
		||||
    "forin": false,
 | 
			
		||||
    "interface-name": false,
 | 
			
		||||
    "interface-over-type-literal": false,
 | 
			
		||||
 | 
			
		||||
@ -30,12 +30,10 @@
 | 
			
		||||
 * @see {@link https://github.com/f-list/exported|GitHub repo}
 | 
			
		||||
 */
 | 
			
		||||
import Axios from 'axios';
 | 
			
		||||
import * as Raven from 'raven-js';
 | 
			
		||||
import Vue from 'vue';
 | 
			
		||||
import Chat from '../chat/Chat.vue';
 | 
			
		||||
import {init as initCore} from '../chat/core';
 | 
			
		||||
import l from '../chat/localize';
 | 
			
		||||
import VueRaven from '../chat/vue-raven';
 | 
			
		||||
import {setupRaven} from '../chat/vue-raven';
 | 
			
		||||
import Socket from '../chat/WebSocket';
 | 
			
		||||
import Connection from '../fchat/connection';
 | 
			
		||||
import '../scss/fa.scss'; //tslint:disable-line:no-import-side-effect
 | 
			
		||||
@ -49,27 +47,8 @@ if(typeof window.Promise !== 'function' || typeof window.Notification !== 'funct
 | 
			
		||||
const version = (<{version: string}>require('./package.json')).version; //tslint:disable-line:no-require-imports
 | 
			
		||||
Axios.defaults.params = { __fchat: `web/${version}` };
 | 
			
		||||
 | 
			
		||||
if(process.env.NODE_ENV === 'production') {
 | 
			
		||||
    Raven.config('https://a9239b17b0a14f72ba85e8729b9d1612@sentry.f-list.net/2', {
 | 
			
		||||
        release: `web-${version}`,
 | 
			
		||||
        dataCallback: (data: {culprit?: string, exception?: {values: {stacktrace: {frames: {filename: string}[]}}[]}}) => {
 | 
			
		||||
            if(data.culprit !== undefined) {
 | 
			
		||||
                const end = data.culprit.lastIndexOf('?');
 | 
			
		||||
                data.culprit = `~${data.culprit.substring(data.culprit.lastIndexOf('/'), end === -1 ? undefined : end)}`;
 | 
			
		||||
            }
 | 
			
		||||
            if(data.exception !== undefined)
 | 
			
		||||
                for(const ex of data.exception.values)
 | 
			
		||||
                    for(const frame of ex.stacktrace.frames) {
 | 
			
		||||
                        const index = frame.filename.lastIndexOf('/');
 | 
			
		||||
                        const endIndex = frame.filename.lastIndexOf('?');
 | 
			
		||||
                        frame.filename = `~${frame.filename.substring(index !== -1 ? index : 0, endIndex === -1 ? undefined : endIndex)}`;
 | 
			
		||||
                    }
 | 
			
		||||
        }
 | 
			
		||||
    }).addPlugin(VueRaven, Vue).install();
 | 
			
		||||
    (<Window & {onunhandledrejection(e: PromiseRejectionEvent): void}>window).onunhandledrejection = (e: PromiseRejectionEvent) => {
 | 
			
		||||
        Raven.captureException(<Error>e.reason);
 | 
			
		||||
    };
 | 
			
		||||
}
 | 
			
		||||
if(process.env.NODE_ENV === 'production')
 | 
			
		||||
    setupRaven('https://a9239b17b0a14f72ba85e8729b9d1612@sentry.f-list.net/2', `web-${version}`);
 | 
			
		||||
 | 
			
		||||
declare const chatSettings: {account: string, theme: string, characters: ReadonlyArray<string>, defaultCharacter: string | null};
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -7,19 +7,17 @@ export default class Notifications extends BaseNotifications {
 | 
			
		||||
    async notify(conversation: Conversation, title: string, body: string, icon: string, sound: string): Promise<void> {
 | 
			
		||||
        if(!this.shouldNotify(conversation)) return;
 | 
			
		||||
        try {
 | 
			
		||||
            return super.notify(conversation, title, body, icon, sound);
 | 
			
		||||
            await super.notify(conversation, title, body, icon, sound);
 | 
			
		||||
        } catch {
 | 
			
		||||
            (async() => { //tslint:disable-line:no-floating-promises
 | 
			
		||||
                //tslint:disable-next-line:no-require-imports no-submodule-imports
 | 
			
		||||
                await navigator.serviceWorker.register(<string>require('file-loader!./sw.js'));
 | 
			
		||||
                const reg = await navigator.serviceWorker.ready;
 | 
			
		||||
                await reg.showNotification(title, this.getOptions(conversation, body, icon));
 | 
			
		||||
                navigator.serviceWorker.onmessage = (e) => {
 | 
			
		||||
                    const conv = core.conversations.byKey((<{key: string}>e.data).key);
 | 
			
		||||
                    if(conv !== undefined) conv.show();
 | 
			
		||||
                    window.focus();
 | 
			
		||||
                };
 | 
			
		||||
            })();
 | 
			
		||||
            //tslint:disable-next-line:no-require-imports no-submodule-imports
 | 
			
		||||
            await navigator.serviceWorker.register(<string>require('file-loader!./sw.js'));
 | 
			
		||||
            const reg = await navigator.serviceWorker.ready;
 | 
			
		||||
            await reg.showNotification(title, this.getOptions(conversation, body, icon));
 | 
			
		||||
            navigator.serviceWorker.onmessage = (e) => {
 | 
			
		||||
                const conv = core.conversations.byKey((<{key: string}>e.data).key);
 | 
			
		||||
                if(conv !== undefined) conv.show();
 | 
			
		||||
                window.focus();
 | 
			
		||||
            };
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@ -1,6 +1,6 @@
 | 
			
		||||
{
 | 
			
		||||
  "name": "net.f_list.fchat",
 | 
			
		||||
  "version": "3.0.6",
 | 
			
		||||
  "version": "3.0.7",
 | 
			
		||||
  "displayName": "F-Chat",
 | 
			
		||||
  "author": "The F-List Team",
 | 
			
		||||
  "description": "F-List.net Chat Client",
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user