3.0.6
This commit is contained in:
parent
128c638ad4
commit
4d8f6c3670
|
@ -1,11 +1,12 @@
|
|||
<template>
|
||||
<div class="bbcode-editor">
|
||||
<div class="bbcode-editor" style="display:flex;flex-wrap:wrap;justify-content:flex-end">
|
||||
<slot></slot>
|
||||
<a tabindex="0" class="btn btn-light bbcode-btn btn-sm" role="button" @click="showToolbar = true" @blur="showToolbar = false"
|
||||
style="border-bottom-left-radius:0;border-bottom-right-radius:0">
|
||||
style="border-bottom-left-radius:0;border-bottom-right-radius:0" v-if="hasToolbar">
|
||||
<i class="fa fa-code"></i>
|
||||
</a>
|
||||
<div class="bbcode-toolbar btn-toolbar" role="toolbar" :style="showToolbar ? 'display:flex' : ''" @mousedown.stop.prevent>
|
||||
<div class="bbcode-toolbar btn-toolbar" role="toolbar" :style="showToolbar ? 'display:flex' : ''" @mousedown.stop.prevent
|
||||
v-if="hasToolbar" style="flex:1 51%">
|
||||
<div class="btn-group" style="flex-wrap:wrap">
|
||||
<div class="btn btn-light btn-sm" v-for="button in buttons" :title="button.title" @click.prevent.stop="apply(button)">
|
||||
<i :class="(button.class ? button.class : 'fa ') + button.icon"></i>
|
||||
|
@ -17,10 +18,10 @@
|
|||
</div>
|
||||
<button type="button" class="close" aria-label="Close" style="margin-left:10px" @click="showToolbar = false">×</button>
|
||||
</div>
|
||||
<div class="bbcode-editor-text-area">
|
||||
<textarea ref="input" v-model="text" @input="onInput" v-show="!preview" :maxlength="maxlength"
|
||||
:class="finalClasses" @keyup="onKeyUp" :disabled="disabled" @paste="onPaste" style="border-top-left-radius:0"
|
||||
:placeholder="placeholder" @keypress="$emit('keypress', $event)" @keydown="onKeyDown"></textarea>
|
||||
<div class="bbcode-editor-text-area" style="order:100;width:100%;">
|
||||
<textarea ref="input" v-model="text" @input="onInput" v-show="!preview" :maxlength="maxlength" :placeholder="placeholder"
|
||||
:class="finalClasses" @keyup="onKeyUp" :disabled="disabled" @paste="onPaste" @keypress="$emit('keypress', $event)"
|
||||
:style="hasToolbar ? 'border-top-left-radius:0' : ''"@keydown="onKeyDown"></textarea>
|
||||
<textarea ref="sizer"></textarea>
|
||||
<div class="bbcode-preview" v-show="preview">
|
||||
<div class="bbcode-preview-warnings">
|
||||
|
@ -59,6 +60,8 @@
|
|||
readonly disabled?: boolean;
|
||||
@Prop()
|
||||
readonly placeholder?: string;
|
||||
@Prop({default: true})
|
||||
readonly hasToolbar!: boolean;
|
||||
@Prop({default: false, type: Boolean})
|
||||
readonly invalid!: boolean;
|
||||
preview = false;
|
||||
|
@ -191,6 +194,7 @@
|
|||
button.startText = `[${button.tag}]`;
|
||||
if(button.endText === undefined)
|
||||
button.endText = `[/${button.tag}]`;
|
||||
if(this.text.length + button.startText.length + button.endText.length > this.maxlength) return;
|
||||
this.applyText(button.startText, button.endText);
|
||||
this.lastInput = Date.now();
|
||||
}
|
||||
|
|
|
@ -88,7 +88,7 @@
|
|||
if(options === undefined)
|
||||
options = <Options | undefined>(await Axios.get('https://www.f-list.net/json/api/mapping-list.php')).data;
|
||||
if(options === undefined) return;
|
||||
this.options = {
|
||||
this.options = Object.freeze({
|
||||
kinks: options.kinks.sort((x, y) => (x.name < y.name ? -1 : (x.name > y.name ? 1 : 0))),
|
||||
genders: options.listitems.filter((x) => x.name === 'gender').map((x) => x.value),
|
||||
orientations: options.listitems.filter((x) => x.name === 'orientation').map((x) => x.value),
|
||||
|
@ -96,7 +96,7 @@
|
|||
furryprefs: options.listitems.filter((x) => x.name === 'furrypref').map((x) => x.value),
|
||||
roles: options.listitems.filter((x) => x.name === 'subdom').map((x) => x.value),
|
||||
positions: options.listitems.filter((x) => x.name === 'position').map((x) => x.value)
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
mounted(): void {
|
||||
|
|
|
@ -69,7 +69,7 @@
|
|||
else if(node instanceof HTMLImageElement) str += node.alt;
|
||||
if(node.firstChild !== null && !flags.endFound) str += scanNode(node.firstChild, end, range, flags, hide);
|
||||
if(node.bbcodeTag !== undefined) str += `[/${node.bbcodeTag}]`;
|
||||
if(node instanceof HTMLElement && getComputedStyle(node).display === 'block') str += '\r\n';
|
||||
if(node instanceof HTMLElement && getComputedStyle(node).display === 'block' && !flags.endFound) str += '\r\n';
|
||||
if(node.nextSibling !== null && !flags.endFound) str += scanNode(node.nextSibling, end, range, flags, hide);
|
||||
return hide ? '' : str;
|
||||
}
|
||||
|
@ -108,7 +108,7 @@
|
|||
startValue = start instanceof HTMLImageElement ? start.alt : scanNode(start.firstChild!, end, range, {});
|
||||
} else
|
||||
startValue = start.nodeValue!.substring(range.startOffset, start === range.endContainer ? range.endOffset : undefined);
|
||||
if(end instanceof HTMLElement) end = end.childNodes[range.endOffset - 1];
|
||||
if(end instanceof HTMLElement && range.endOffset > 0) end = end.childNodes[range.endOffset - 1];
|
||||
e.clipboardData.setData('text/plain', copyNode(startValue, start, end, range, {}));
|
||||
e.preventDefault();
|
||||
}) as EventListener);
|
||||
|
@ -122,9 +122,9 @@
|
|||
core.register('characters', Characters(core.connection));
|
||||
core.register('channels', Channels(core.connection, core.characters));
|
||||
core.register('conversations', Conversations());
|
||||
core.connection.onEvent('closed', (isReconnect) => {
|
||||
core.connection.onEvent('closed', async(isReconnect) => {
|
||||
if(isReconnect) (<Modal>this.$refs['reconnecting']).show(true);
|
||||
if(this.connected) core.notifications.playSound('logout');
|
||||
if(this.connected) await core.notifications.playSound('logout');
|
||||
this.connected = false;
|
||||
this.connecting = false;
|
||||
document.title = l('title');
|
||||
|
@ -133,12 +133,12 @@
|
|||
this.connecting = true;
|
||||
if(core.state.settings.notifications) await core.notifications.requestPermission();
|
||||
});
|
||||
core.connection.onEvent('connected', () => {
|
||||
core.connection.onEvent('connected', async() => {
|
||||
(<Modal>this.$refs['reconnecting']).hide();
|
||||
this.error = '';
|
||||
this.connecting = false;
|
||||
this.connected = true;
|
||||
core.notifications.playSound('login');
|
||||
await core.notifications.playSound('login');
|
||||
document.title = l('title.connected', core.connection.character);
|
||||
});
|
||||
core.watch(() => core.conversations.hasNew, (hasNew) => {
|
||||
|
@ -157,8 +157,9 @@
|
|||
(<Modal>this.$refs['reconnecting']).hide();
|
||||
}
|
||||
|
||||
connect(): void {
|
||||
async connect(): Promise<void> {
|
||||
this.connecting = true;
|
||||
await core.notifications.initSounds(['attention', 'login', 'logout', 'modalert', 'newnote']);
|
||||
core.connection.connect(this.selectedCharacter);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -30,16 +30,18 @@
|
|||
<div class="list-group conversation-nav" ref="privateConversations">
|
||||
<a v-for="conversation in conversations.privateConversations" href="#" @click.prevent="conversation.show()"
|
||||
:class="getClasses(conversation)" :data-character="conversation.character.name" data-touch="false"
|
||||
class="list-group-item list-group-item-action item-private" :key="conversation.key">
|
||||
class="list-group-item list-group-item-action item-private" :key="conversation.key" @click.middle="conversation.close()">
|
||||
<img :src="characterImage(conversation.character.name)" v-if="showAvatars"/>
|
||||
<div class="name">
|
||||
<span>{{conversation.character.name}}</span>
|
||||
<div style="line-height:0;display:flex">
|
||||
<span class="fas fa-reply" v-show="needsReply(conversation)"></span><span class="fas"
|
||||
<span class="fas fa-reply" v-show="needsReply(conversation)"></span>
|
||||
<span class="fas"
|
||||
:class="{'fa-comment-dots': conversation.typingStatus == 'typing', 'fa-comment': conversation.typingStatus == 'paused'}"
|
||||
></span><span style="flex:1"></span>
|
||||
></span>
|
||||
<span style="flex:1"></span>
|
||||
<span class="pin fas fa-thumbtack" :class="{'active': conversation.isPinned}" @mousedown.prevent
|
||||
@click.stop="conversation.isPinned = !conversation.isPinned" :aria-label="l('chat.pinTab')"></span>
|
||||
@click.stop="conversation.isPinned = !conversation.isPinned" :aria-label="l('chat.pinTab')"></span>
|
||||
<span class="fas fa-times leave" @click.stop="conversation.close()" :aria-label="l('chat.closeTab')"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -49,11 +51,14 @@
|
|||
{{l('chat.channels')}}</a>
|
||||
<div class="list-group conversation-nav" ref="channelConversations">
|
||||
<a v-for="conversation in conversations.channelConversations" href="#" @click.prevent="conversation.show()"
|
||||
:class="getClasses(conversation)" class="list-group-item list-group-item-action item-channel"
|
||||
:key="conversation.key"><span class="name">{{conversation.name}}</span><span><span class="pin fas fa-thumbtack"
|
||||
:class="{'active': conversation.isPinned}" @click.stop="conversation.isPinned = !conversation.isPinned"
|
||||
:aria-label="l('chat.pinTab')" @mousedown.prevent></span><span class="fas fa-times leave"
|
||||
@click.stop="conversation.close()" :aria-label="l('chat.closeTab')"></span></span>
|
||||
:class="getClasses(conversation)" class="list-group-item list-group-item-action item-channel" :key="conversation.key"
|
||||
@click.middle="conversation.close()">
|
||||
<span class="name">{{conversation.name}}</span>
|
||||
<span>
|
||||
<span class="pin fas fa-thumbtack" :class="{'active': conversation.isPinned}" :aria-label="l('chat.pinTab')"
|
||||
@click.stop="conversation.isPinned = !conversation.isPinned" @mousedown.prevent></span>
|
||||
<span class="fas fa-times leave" @click.stop="conversation.close()" :aria-label="l('chat.closeTab')"></span>
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
</sidebar>
|
||||
|
@ -131,6 +136,8 @@
|
|||
conversations = core.conversations;
|
||||
getStatusIcon = getStatusIcon;
|
||||
keydownListener!: (e: KeyboardEvent) => void;
|
||||
focusListener!: () => void;
|
||||
blurListener!: () => void;
|
||||
|
||||
mounted(): void {
|
||||
this.keydownListener = (e: KeyboardEvent) => this.onKeyDown(e);
|
||||
|
@ -152,7 +159,7 @@
|
|||
});
|
||||
const ownCharacter = core.characters.ownCharacter;
|
||||
let idleTimer: number | undefined, idleStatus: Connection.ClientCommands['STA'] | undefined, lastUpdate = 0;
|
||||
window.addEventListener('focus', () => {
|
||||
window.addEventListener('focus', this.focusListener = () => {
|
||||
core.notifications.isInBackground = false;
|
||||
if(idleTimer !== undefined) {
|
||||
clearTimeout(idleTimer);
|
||||
|
@ -161,11 +168,11 @@
|
|||
if(idleStatus !== undefined) {
|
||||
const status = idleStatus;
|
||||
window.setTimeout(() => core.connection.send('STA', status),
|
||||
Math.max(lastUpdate + 5 /*core.connection.vars.sta_flood*/ * 1000 + 1000 - Date.now(), 0));
|
||||
Math.max(lastUpdate + core.connection.vars.sta_flood * 1000 + 1000 - Date.now(), 0));
|
||||
idleStatus = undefined;
|
||||
}
|
||||
});
|
||||
window.addEventListener('blur', () => {
|
||||
window.addEventListener('blur', this.blurListener = () => {
|
||||
core.notifications.isInBackground = true;
|
||||
if(idleTimer !== undefined) clearTimeout(idleTimer);
|
||||
if(core.state.settings.idleTimer > 0)
|
||||
|
@ -190,6 +197,8 @@
|
|||
|
||||
destroyed(): void {
|
||||
window.removeEventListener('keydown', this.keydownListener);
|
||||
window.removeEventListener('focus', this.focusListener);
|
||||
window.removeEventListener('blur', this.blurListener);
|
||||
}
|
||||
|
||||
needsReply(conversation: Conversation): boolean {
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
<template>
|
||||
<modal :action="l('conversationSettings.action', conversation.name)" @submit="submit" ref="dialog" @close="init()" dialogClass="w-100">
|
||||
<modal :action="l('conversationSettings.action', conversation.name)" @submit="submit" ref="dialog" @close="init()" dialogClass="w-100"
|
||||
:buttonText="l('conversationSettings.save')">
|
||||
<div class="form-group">
|
||||
<label class="control-label" :for="'notify' + conversation.key">{{l('conversationSettings.notify')}}</label>
|
||||
<select class="form-control" :id="'notify' + conversation.key" v-model="notify">
|
||||
|
@ -82,7 +83,7 @@
|
|||
this.conversation.settings = {
|
||||
notify: this.notify,
|
||||
highlight: this.highlight,
|
||||
highlightWords: this.highlightWords.split(',').filter((x) => x.length),
|
||||
highlightWords: this.highlightWords.split(',').map((x) => x.trim()).filter((x) => x.length),
|
||||
joinMessages: this.joinMessages,
|
||||
defaultHighlights: this.defaultHighlights
|
||||
};
|
||||
|
|
|
@ -30,7 +30,9 @@
|
|||
<span class="fa" :class="{'fa-chevron-down': !descriptionExpanded, 'fa-chevron-up': descriptionExpanded}"></span>
|
||||
<span class="btn-text">{{l('channel.description')}}</span>
|
||||
</a>
|
||||
<manage-channel :channel="conversation.channel" v-if="isChannelMod"></manage-channel>
|
||||
<a href="#" @click.prevent="$refs['manageDialog'].show()" v-show="isChannelMod" class="btn">
|
||||
<span class="fa fa-edit"></span> <span class="btn-text">{{l('manageChannel.open')}}</span>
|
||||
</a>
|
||||
<a href="#" @click.prevent="$refs['logsDialog'].show()" class="btn">
|
||||
<span class="fa fa-file-alt"></span> <span class="btn-text">{{l('logs.title')}}</span>
|
||||
</a>
|
||||
|
@ -83,45 +85,42 @@
|
|||
</span>
|
||||
</template>
|
||||
</div>
|
||||
<div>
|
||||
<span v-if="conversation.typingStatus && conversation.typingStatus !== 'clear'">
|
||||
<bbcode-editor v-model="conversation.enteredText" @keydown="onKeyDown" :extras="extraButtons" @input="keepScroll"
|
||||
:classes="'form-control chat-text-box' + (conversation.isSendingAds ? ' ads-text-box' : '')" :hasToolbar="settings.bbCodeBar"
|
||||
ref="textBox" style="position:relative;margin-top:5px" :maxlength="conversation.maxMessageLength">
|
||||
<span v-if="conversation.typingStatus && conversation.typingStatus !== 'clear'" class="chat-info-text">
|
||||
{{l('chat.typing.' + conversation.typingStatus, conversation.name)}}
|
||||
</span>
|
||||
<div v-show="conversation.infoText" style="display:flex;align-items:center">
|
||||
<div v-show="conversation.infoText" class="chat-info-text">
|
||||
<span class="fa fa-times" style="cursor:pointer" @click.stop="conversation.infoText = ''"></span>
|
||||
<span style="flex:1;margin-left:5px">{{conversation.infoText}}</span>
|
||||
</div>
|
||||
<div v-show="conversation.errorText" style="display:flex;align-items:center">
|
||||
<div v-show="conversation.errorText" class="chat-info-text">
|
||||
<span class="fa fa-times" style="cursor:pointer" @click.stop="conversation.errorText = ''"></span>
|
||||
<span class="redText" style="flex:1;margin-left:5px">{{conversation.errorText}}</span>
|
||||
</div>
|
||||
<div style="position:relative;margin-top:5px">
|
||||
<bbcode-editor v-model="conversation.enteredText" @keydown="onKeyDown" :extras="extraButtons" @input="keepScroll"
|
||||
:classes="'form-control chat-text-box' + (conversation.isSendingAds ? ' ads-text-box' : '')"
|
||||
ref="textBox" style="position:relative" :maxlength="conversation.maxMessageLength">
|
||||
<div style="float:right;text-align:right;display:flex;align-items:center">
|
||||
<div v-show="conversation.maxMessageLength" style="margin-right:5px">
|
||||
{{getByteLength(conversation.enteredText)}} / {{conversation.maxMessageLength}}
|
||||
</div>
|
||||
<ul class="nav nav-pills send-ads-switcher" v-if="conversation.channel"
|
||||
style="position:relative;z-index:10;margin-right:5px">
|
||||
<li class="nav-item">
|
||||
<a href="#" :class="{active: !conversation.isSendingAds, disabled: conversation.channel.mode != 'both'}"
|
||||
class="nav-link" @click.prevent="setSendingAds(false)">{{l('channel.mode.chat')}}</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="#" :class="{active: conversation.isSendingAds, disabled: conversation.channel.mode != 'both'}"
|
||||
class="nav-link" @click.prevent="setSendingAds(true)">{{adsMode}}</a>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="btn btn-sm btn-primary" v-show="!settings.enterSend" @click="sendButton">{{l('chat.send')}}</div>
|
||||
</div>
|
||||
</bbcode-editor>
|
||||
<div class="bbcode-editor-controls">
|
||||
<div v-show="conversation.maxMessageLength" style="margin-right:5px">
|
||||
{{getByteLength(conversation.enteredText)}} / {{conversation.maxMessageLength}}
|
||||
</div>
|
||||
<ul class="nav nav-pills send-ads-switcher" v-if="conversation.channel"
|
||||
style="position:relative;z-index:10;margin-right:5px">
|
||||
<li class="nav-item">
|
||||
<a href="#" :class="{active: !conversation.isSendingAds, disabled: conversation.channel.mode != 'both'}"
|
||||
class="nav-link" @click.prevent="setSendingAds(false)">{{l('channel.mode.chat')}}</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="#" :class="{active: conversation.isSendingAds, disabled: conversation.channel.mode != 'both'}"
|
||||
class="nav-link" @click.prevent="setSendingAds(true)">{{adsMode}}</a>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="btn btn-sm btn-primary" v-show="!settings.enterSend" @click="sendButton">{{l('chat.send')}}</div>
|
||||
</div>
|
||||
</div>
|
||||
</bbcode-editor>
|
||||
<command-help ref="helpDialog"></command-help>
|
||||
<settings ref="settingsDialog" :conversation="conversation"></settings>
|
||||
<logs ref="logsDialog" :conversation="conversation"></logs>
|
||||
<manage-channel ref="manageDialog" :channel="conversation.channel" v-if="conversation.channel"></manage-channel>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
@ -130,6 +129,7 @@
|
|||
import Component from 'vue-class-component';
|
||||
import {Prop, Watch} from 'vue-property-decorator';
|
||||
import {EditorButton, EditorSelection} from '../bbcode/editor';
|
||||
import {isShowing as anyDialogsShown} from '../components/Modal.vue';
|
||||
import {Keys} from '../keys';
|
||||
import {BBCodeView, Editor} from './bbcode';
|
||||
import CommandHelp from './CommandHelp.vue';
|
||||
|
@ -168,23 +168,30 @@
|
|||
lastSearchInput = 0;
|
||||
messageCount = 0;
|
||||
searchTimer = 0;
|
||||
windowHeight = window.innerHeight;
|
||||
resizeHandler = () => {
|
||||
const messageView = <HTMLElement>this.$refs['messages'];
|
||||
if(this.windowHeight - window.innerHeight + messageView.scrollTop + messageView.offsetHeight >= messageView.scrollHeight - 15)
|
||||
messageView.scrollTop = messageView.scrollHeight - messageView.offsetHeight;
|
||||
this.windowHeight = window.innerHeight;
|
||||
}
|
||||
messageView!: HTMLElement;
|
||||
resizeHandler!: EventListener;
|
||||
keydownHandler!: EventListener;
|
||||
keypressHandler!: EventListener;
|
||||
scrolledDown = true;
|
||||
scrolledUp = false;
|
||||
|
||||
created(): void {
|
||||
mounted(): void {
|
||||
this.extraButtons = [{
|
||||
title: 'Help\n\nClick this button for a quick overview of slash commands.',
|
||||
tag: '?',
|
||||
icon: 'fa-question',
|
||||
handler: () => (<CommandHelp>this.$refs['helpDialog']).show()
|
||||
}];
|
||||
window.addEventListener('resize', this.resizeHandler);
|
||||
window.addEventListener('resize', this.resizeHandler = () => {
|
||||
if(this.scrolledDown)
|
||||
this.messageView.scrollTop = this.messageView.scrollHeight - this.messageView.offsetHeight;
|
||||
this.onMessagesScroll();
|
||||
});
|
||||
window.addEventListener('keypress', this.keypressHandler = () => {
|
||||
if(document.getSelection().isCollapsed && !anyDialogsShown &&
|
||||
(document.activeElement === document.body || document.activeElement.tagName === 'A'))
|
||||
(<Editor>this.$refs['textBox']).focus();
|
||||
});
|
||||
window.addEventListener('keydown', this.keydownHandler = ((e: KeyboardEvent) => {
|
||||
if(getKey(e) === Keys.KeyF && (e.ctrlKey || e.metaKey) && !e.shiftKey && !e.altKey) {
|
||||
this.showSearch = true;
|
||||
|
@ -195,11 +202,13 @@
|
|||
if(Date.now() - this.lastSearchInput > 500 && this.search !== this.searchInput)
|
||||
this.search = this.searchInput;
|
||||
}, 500);
|
||||
this.messageView = <HTMLElement>this.$refs['messages'];
|
||||
}
|
||||
|
||||
destroyed(): void {
|
||||
window.removeEventListener('resize', this.resizeHandler);
|
||||
window.removeEventListener('keydown', this.keydownHandler);
|
||||
window.removeEventListener('keypress', this.keypressHandler);
|
||||
clearInterval(this.searchTimer);
|
||||
}
|
||||
|
||||
|
@ -224,29 +233,30 @@
|
|||
|
||||
@Watch('conversation')
|
||||
conversationChanged(): void {
|
||||
(<Editor>this.$refs['textBox']).focus();
|
||||
if(!anyDialogsShown) (<Editor>this.$refs['textBox']).focus();
|
||||
setTimeout(() => this.messageView.scrollTop = this.messageView.scrollHeight - this.messageView.offsetHeight);
|
||||
this.scrolledDown = true;
|
||||
}
|
||||
|
||||
@Watch('conversation.messages')
|
||||
messageAdded(newValue: Conversation.Message[]): void {
|
||||
const messageView = <HTMLElement>this.$refs['messages'];
|
||||
if(!this.keepScroll() && newValue.length === this.messageCount)
|
||||
messageView.scrollTop -= (<HTMLElement>messageView.firstElementChild).clientHeight;
|
||||
this.keepScroll();
|
||||
if(!this.scrolledDown && newValue.length === this.messageCount)
|
||||
this.messageView.scrollTop -= (this.messageView.firstElementChild!).clientHeight;
|
||||
this.messageCount = newValue.length;
|
||||
}
|
||||
|
||||
keepScroll(): boolean {
|
||||
const messageView = <HTMLElement>this.$refs['messages'];
|
||||
if(messageView.scrollTop + messageView.offsetHeight >= messageView.scrollHeight - 15) {
|
||||
this.$nextTick(() => setTimeout(() => messageView.scrollTop = messageView.scrollHeight, 0));
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
keepScroll(): void {
|
||||
if(this.scrolledDown)
|
||||
this.$nextTick(() => setTimeout(() => this.messageView.scrollTop = this.messageView.scrollHeight, 0));
|
||||
}
|
||||
|
||||
onMessagesScroll(): void {
|
||||
const messageView = <HTMLElement | undefined>this.$refs['messages'];
|
||||
if(messageView !== undefined && messageView.scrollTop < 50) this.conversation.loadMore();
|
||||
if(this.messageView.scrollTop < 50 && !this.scrolledUp) {
|
||||
this.scrolledUp = true;
|
||||
this.conversation.loadMore();
|
||||
} else this.scrolledUp = false;
|
||||
this.scrolledDown = this.messageView.scrollTop + this.messageView.offsetHeight >= this.messageView.scrollHeight - 15;
|
||||
}
|
||||
|
||||
@Watch('conversation.errorText')
|
||||
|
@ -378,4 +388,13 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
.chat-info-text {
|
||||
display:flex;
|
||||
align-items:center;
|
||||
flex:1 51%;
|
||||
@media (max-width: breakpoint-max(xs)) {
|
||||
flex-basis: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -38,7 +38,7 @@
|
|||
<label for="date" class="col-sm-2 col-form-label">{{l('logs.date')}}</label>
|
||||
<div class="col-sm-8 col-10 col-xl-9">
|
||||
<select class="form-control" v-model="selectedDate" id="date" @change="loadMessages">
|
||||
<option :value="null">{{l('logs.selectDate')}}</option>
|
||||
<option :value="null">{{l('logs.allDates')}}</option>
|
||||
<option v-for="date in dates" :value="date.getTime()">{{formatDate(date)}}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
@ -47,7 +47,7 @@
|
|||
class="fa fa-download"></span></button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="messages-both" style="overflow: auto" ref="messages" tabindex="-1">
|
||||
<div class="messages-both" style="overflow:auto" ref="messages" tabindex="-1" @scroll="onMessagesScroll">
|
||||
<message-view v-for="message in filteredMessages" :message="message" :key="message.id"></message-view>
|
||||
</div>
|
||||
<div class="input-group" style="flex-shrink:0">
|
||||
|
@ -102,6 +102,7 @@
|
|||
selectedCharacter = core.connection.character;
|
||||
showFilters = true;
|
||||
canZip = core.logs.canZip;
|
||||
dateOffset = -1;
|
||||
|
||||
get filteredMessages(): ReadonlyArray<Conversation.Message> {
|
||||
if(this.filter.length === 0) return this.messages;
|
||||
|
@ -139,9 +140,16 @@
|
|||
this.dates = this.selectedConversation === null ? [] :
|
||||
(await core.logs.getLogDates(this.selectedCharacter, this.selectedConversation.key)).slice().reverse();
|
||||
this.selectedDate = null;
|
||||
this.dateOffset = -1;
|
||||
this.filter = '';
|
||||
await this.loadMessages();
|
||||
}
|
||||
|
||||
@Watch('filter')
|
||||
onFilterChanged(): void {
|
||||
this.$nextTick(async() => this.onMessagesScroll());
|
||||
}
|
||||
|
||||
download(file: string, logs: string): void {
|
||||
const a = document.createElement('a');
|
||||
a.href = logs;
|
||||
|
@ -189,6 +197,8 @@
|
|||
if(this.selectedCharacter !== '') {
|
||||
this.conversations = (await core.logs.getConversations(this.selectedCharacter)).slice();
|
||||
this.conversations.sort((x, y) => (x.name < y.name ? -1 : (x.name > y.name ? 1 : 0)));
|
||||
this.dates = this.selectedConversation === null ? [] :
|
||||
(await core.logs.getLogDates(this.selectedCharacter, this.selectedConversation.key)).slice().reverse();
|
||||
await this.loadMessages();
|
||||
}
|
||||
this.keyDownListener = (e) => {
|
||||
|
@ -213,10 +223,33 @@
|
|||
}
|
||||
|
||||
async loadMessages(): Promise<ReadonlyArray<Conversation.Message>> {
|
||||
if(this.selectedDate === null || this.selectedConversation === null)
|
||||
if(this.selectedConversation === null)
|
||||
return this.messages = [];
|
||||
return this.messages = await core.logs.getLogs(this.selectedCharacter, this.selectedConversation.key,
|
||||
new Date(this.selectedDate));
|
||||
if(this.selectedDate !== null) {
|
||||
this.dateOffset = -1;
|
||||
return this.messages = await core.logs.getLogs(this.selectedCharacter, this.selectedConversation.key,
|
||||
new Date(this.selectedDate));
|
||||
}
|
||||
if(this.dateOffset === -1) {
|
||||
this.messages = [];
|
||||
this.dateOffset = 0;
|
||||
}
|
||||
this.$nextTick(async() => this.onMessagesScroll());
|
||||
return this.messages;
|
||||
}
|
||||
|
||||
async onMessagesScroll(): Promise<void> {
|
||||
const list = <HTMLElement | undefined>this.$refs['messages'];
|
||||
if(this.selectedConversation === null || this.selectedDate !== null || list === undefined || list.scrollTop > 15
|
||||
|| !this.dialog.isShown || this.dateOffset >= this.dates.length) return;
|
||||
const messages = await core.logs.getLogs(this.selectedCharacter, this.selectedConversation.key,
|
||||
this.dates[this.dateOffset++]);
|
||||
this.messages = messages.concat(this.messages);
|
||||
const noOverflow = list.offsetHeight === list.scrollHeight;
|
||||
this.$nextTick(() => {
|
||||
if(list.offsetHeight === list.scrollHeight) return this.onMessagesScroll();
|
||||
else if(noOverflow) list.scrollTop = list.scrollHeight;
|
||||
});
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -1,51 +1,46 @@
|
|||
<template>
|
||||
<span>
|
||||
<a href="#" @click.prevent="openDialog" class="btn">
|
||||
<span class="fa fa-edit"></span> <span class="btn-text">{{l('manageChannel.open')}}</span>
|
||||
</a>
|
||||
<modal ref="dialog" :action="l('manageChannel.action', channel.name)" :buttonText="l('manageChannel.submit')" @submit="submit"
|
||||
dialogClass="w-100 modal-lg">
|
||||
<div class="form-group" v-show="channel.id.substr(0, 4) === 'adh-'">
|
||||
<label class="control-label" for="isPublic">
|
||||
<input type="checkbox" id="isPublic" v-model="isPublic"/>
|
||||
{{l('manageChannel.isPublic')}}
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label" for="mode">{{l('manageChannel.mode')}}</label>
|
||||
<select v-model="mode" class="form-control" id="mode">
|
||||
<option v-for="mode in modes" :value="mode">{{l('channel.mode.' + mode)}}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>{{l('manageChannel.description')}}</label>
|
||||
<bbcode-editor classes="form-control" id="description" v-model="description" style="position:relative" :maxlength="50000">
|
||||
<div style="float:right;text-align:right;">
|
||||
{{getByteLength(description)}} / {{maxLength}}
|
||||
</div>
|
||||
</bbcode-editor>
|
||||
</div>
|
||||
<div v-if="isChannelOwner">
|
||||
<h4>{{l('manageChannel.mods')}}</h4>
|
||||
<div v-for="(mod, index) in opList">
|
||||
<a href="#" @click.prevent="opList.splice(index, 1)" class="btn" style="padding:0;vertical-align:baseline">
|
||||
<i class="fas fa-times"></i>
|
||||
</a>
|
||||
{{mod}}
|
||||
</div>
|
||||
<div style="display:flex;margin-top:5px">
|
||||
<input :placeholder="l('manageChannel.modAddName')" v-model="modAddName" class="form-control"/>
|
||||
<button class="btn btn-secondary" @click="modAdd" :disabled="!modAddName">{{l('manageChannel.modAdd')}}</button>
|
||||
<modal ref="dialog" :action="l('manageChannel.action', channel.name)" :buttonText="l('manageChannel.submit')" @submit="submit"
|
||||
dialogClass="w-100 modal-lg" @open="onOpen">
|
||||
<div class="form-group" v-show="isChannelOwner && channel.id.substr(0, 4) === 'adh-'">
|
||||
<label class="control-label" for="isPublic">
|
||||
<input type="checkbox" id="isPublic" v-model="isPublic"/>
|
||||
{{l('manageChannel.isPublic')}}
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-group" v-show="isChannelOwner">
|
||||
<label class="control-label" for="mode">{{l('manageChannel.mode')}}</label>
|
||||
<select v-model="mode" class="form-control" id="mode">
|
||||
<option v-for="mode in modes" :value="mode">{{l('channel.mode.' + mode)}}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>{{l('manageChannel.description')}}</label>
|
||||
<bbcode-editor classes="form-control" id="description" v-model="description" style="position:relative" :maxlength="50000">
|
||||
<div class="bbcode-editor-controls">
|
||||
{{getByteLength(description)}} / {{maxLength}}
|
||||
</div>
|
||||
</bbcode-editor>
|
||||
</div>
|
||||
<template v-if="isChannelOwner">
|
||||
<h4>{{l('manageChannel.mods')}}</h4>
|
||||
<div v-for="(mod, index) in opList">
|
||||
<a href="#" @click.prevent="opList.splice(index, 1)" class="btn" style="padding:0;vertical-align:baseline">
|
||||
<i class="fas fa-times"></i>
|
||||
</a>
|
||||
{{mod}}
|
||||
</div>
|
||||
</modal>
|
||||
</span>
|
||||
<div style="display:flex;margin-top:5px">
|
||||
<input :placeholder="l('manageChannel.modAddName')" v-model="modAddName" class="form-control" style="margin-right:5px"/>
|
||||
<button class="btn btn-secondary" @click="modAdd" :disabled="!modAddName">{{l('manageChannel.modAdd')}}</button>
|
||||
</div>
|
||||
</template>
|
||||
</modal>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import Component from 'vue-class-component';
|
||||
import {Prop, Watch} from 'vue-property-decorator';
|
||||
import {Prop} from 'vue-property-decorator';
|
||||
import CustomDialog from '../components/custom_dialog';
|
||||
import Modal from '../components/Modal.vue';
|
||||
import {Editor} from './bbcode';
|
||||
import {getByteLength} from './common';
|
||||
|
@ -56,7 +51,7 @@
|
|||
@Component({
|
||||
components: {modal: Modal, 'bbcode-editor': Editor}
|
||||
})
|
||||
export default class ManageChannel extends Vue {
|
||||
export default class ManageChannel extends CustomDialog {
|
||||
@Prop({required: true})
|
||||
readonly channel!: Channel;
|
||||
modes = channelModes;
|
||||
|
@ -66,14 +61,14 @@
|
|||
l = l;
|
||||
getByteLength = getByteLength;
|
||||
modAddName = '';
|
||||
opList: string[] = [];
|
||||
maxLength = 50000; //core.connection.vars.cds_max;
|
||||
opList = this.channel.opList.slice();
|
||||
maxLength = core.connection.vars.cds_max;
|
||||
|
||||
@Watch('channel')
|
||||
channelChanged(): void {
|
||||
onOpen(): void {
|
||||
this.mode = this.channel.mode;
|
||||
this.isPublic = this.channelIsPublic;
|
||||
this.description = this.channel.description;
|
||||
this.opList = this.channel.opList.slice();
|
||||
}
|
||||
|
||||
get channelIsPublic(): boolean {
|
||||
|
@ -90,14 +85,13 @@
|
|||
}
|
||||
|
||||
submit(): void {
|
||||
if(this.isPublic !== this.channelIsPublic) {
|
||||
core.connection.send('RST', {channel: this.channel.id, status: this.isPublic ? 'public' : 'private'});
|
||||
core.connection.send('ORS');
|
||||
}
|
||||
if(this.mode !== this.channel.mode)
|
||||
core.connection.send('RMO', {channel: this.channel.id, mode: this.mode});
|
||||
if(this.description !== this.channel.description)
|
||||
core.connection.send('CDS', {channel: this.channel.id, description: this.description});
|
||||
if(!this.isChannelOwner) return;
|
||||
if(this.isPublic !== this.channelIsPublic)
|
||||
core.connection.send('RST', {channel: this.channel.id, status: this.isPublic ? 'public' : 'private'});
|
||||
if(this.mode !== this.channel.mode)
|
||||
core.connection.send('RMO', {channel: this.channel.id, mode: this.mode});
|
||||
for(const op of this.channel.opList) {
|
||||
const index = this.opList.indexOf(op);
|
||||
if(index !== -1) this.opList.splice(index, 1);
|
||||
|
@ -105,10 +99,5 @@
|
|||
}
|
||||
for(const op of this.opList) core.connection.send('COA', {channel: this.channel.id, character: op});
|
||||
}
|
||||
|
||||
openDialog(): void {
|
||||
(<Modal>this.$refs['dialog']).show();
|
||||
this.opList = this.channel.opList.slice();
|
||||
}
|
||||
}
|
||||
</script>
|
|
@ -47,6 +47,12 @@
|
|||
{{l('settings.messageSeparators')}}
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label" for="bbCodeBar">
|
||||
<input type="checkbox" id="bbCodeBar" v-model="bbCodeBar"/>
|
||||
{{l('settings.bbCodeBar')}}
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label" for="logMessages">
|
||||
<input type="checkbox" id="logMessages" v-model="logMessages"/>
|
||||
|
@ -158,6 +164,7 @@
|
|||
showNeedsReply!: boolean;
|
||||
enterSend!: boolean;
|
||||
colorBookmarks!: boolean;
|
||||
bbCodeBar!: boolean;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
@ -189,6 +196,7 @@
|
|||
this.showNeedsReply = settings.showNeedsReply;
|
||||
this.enterSend = settings.enterSend;
|
||||
this.colorBookmarks = settings.colorBookmarks;
|
||||
this.bbCodeBar = settings.bbCodeBar;
|
||||
};
|
||||
|
||||
async doImport(): Promise<void> {
|
||||
|
@ -226,7 +234,8 @@
|
|||
fontSize: isNaN(this.fontSize) ? 14 : this.fontSize < 10 ? 10 : this.fontSize > 24 ? 24 : this.fontSize,
|
||||
showNeedsReply: this.showNeedsReply,
|
||||
enterSend: this.enterSend,
|
||||
colorBookmarks: this.colorBookmarks
|
||||
colorBookmarks: this.colorBookmarks,
|
||||
bbCodeBar: this.bbCodeBar
|
||||
};
|
||||
if(this.notifications) await core.notifications.requestPermission();
|
||||
}
|
||||
|
|
|
@ -11,8 +11,8 @@
|
|||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label">{{l('chat.setStatus.message')}}</label>
|
||||
<editor id="text" v-model="text" classes="form-control" maxlength="255" style="position:relative;">
|
||||
<div style="float:right;text-align:right;">
|
||||
<editor id="text" v-model="text" classes="form-control" maxlength="255">
|
||||
<div class="bbcode-editor-controls">
|
||||
{{getByteLength(text)}} / 255
|
||||
</div>
|
||||
</editor>
|
||||
|
|
|
@ -30,7 +30,7 @@
|
|||
<a tabindex="-1" href="#" @click.prevent="channelKick" class="list-group-item list-group-item-action" v-show="isChannelMod">
|
||||
<span class="fa fa-fw fa-ban"></span>{{l('user.channelKick')}}</a>
|
||||
<a tabindex="-1" href="#" @click.prevent="chatKick" style="color:#f00" class="list-group-item list-group-item-action"
|
||||
v-show="isChatOp"><span class="far fa-fw fa-trash"></span>{{l('user.chatKick')}}</a>
|
||||
v-show="isChatOp"><span class="fas fa-fw fa-trash"></span>{{l('user.chatKick')}}</a>
|
||||
</div>
|
||||
<modal :action="l('user.memo.action')" ref="memo" :disabled="memoLoading" @submit="updateMemo" dialogClass="w-100">
|
||||
<div style="float:right;text-align:right;">{{getByteLength(memo)}} / 1000</div>
|
||||
|
|
Binary file not shown.
After Width: | Height: | Size: 2.9 KiB |
|
@ -41,6 +41,7 @@ export class Settings implements ISettings {
|
|||
showNeedsReply = false;
|
||||
enterSend = true;
|
||||
colorBookmarks = false;
|
||||
bbCodeBar = true;
|
||||
}
|
||||
|
||||
export class ConversationSettings implements Conversation.Settings {
|
||||
|
|
|
@ -152,7 +152,7 @@ class PrivateConversation extends Conversation implements Interfaces.PrivateConv
|
|||
if(message.type !== Interfaces.Message.Type.Event) {
|
||||
if(core.state.settings.logMessages) await core.logs.logMessage(this, message);
|
||||
if(this.settings.notify !== Interfaces.Setting.False && message.sender !== core.characters.ownCharacter)
|
||||
core.notifications.notify(this, message.sender.name, message.text, characterImage(message.sender.name), 'attention');
|
||||
await core.notifications.notify(this, message.sender.name, message.text, characterImage(message.sender.name), 'attention');
|
||||
if(this !== state.selectedConversation || !state.windowFocused)
|
||||
this.unread = Interfaces.UnreadState.Mention;
|
||||
this.typingStatus = 'clear';
|
||||
|
@ -525,19 +525,21 @@ export default function(this: void): Interfaces.State {
|
|||
const message = createMessage(MessageType.Message, char, decodeHTML(data.message), time);
|
||||
await conversation.addMessage(message);
|
||||
|
||||
const words = conversation.settings.highlightWords.map((w) => w.replace(/[^\w]/gi, '\\$&'));
|
||||
const words = conversation.settings.highlightWords.slice();
|
||||
if(conversation.settings.defaultHighlights) words.push(...core.state.settings.highlightWords);
|
||||
if(conversation.settings.highlight === Interfaces.Setting.Default && core.state.settings.highlight ||
|
||||
conversation.settings.highlight === Interfaces.Setting.True) words.push(core.connection.character);
|
||||
for(let i = 0; i < words.length; ++i)
|
||||
words[i] = words[i].replace(/[^\w]/gi, '\\$&');
|
||||
//tslint:disable-next-line:no-null-keyword
|
||||
const results = words.length > 0 ? message.text.match(new RegExp(`\\b(${words.join('|')})\\b`, 'i')) : null;
|
||||
if(results !== null) {
|
||||
core.notifications.notify(conversation, data.character, l('chat.highlight', results[0], conversation.name, message.text),
|
||||
await core.notifications.notify(conversation, data.character, l('chat.highlight', results[0], conversation.name, message.text),
|
||||
characterImage(data.character), 'attention');
|
||||
if(conversation !== state.selectedConversation || !state.windowFocused) conversation.unread = Interfaces.UnreadState.Mention;
|
||||
message.isHighlight = true;
|
||||
} else if(conversation.settings.notify === Interfaces.Setting.True) {
|
||||
core.notifications.notify(conversation, conversation.name, messageToString(message),
|
||||
await core.notifications.notify(conversation, conversation.name, messageToString(message),
|
||||
characterImage(data.character), 'attention');
|
||||
if(conversation !== state.selectedConversation || !state.windowFocused) conversation.unread = Interfaces.UnreadState.Mention;
|
||||
}
|
||||
|
@ -565,7 +567,7 @@ export default function(this: void): Interfaces.State {
|
|||
if(conversation === undefined) return core.channels.leave(channel);
|
||||
if(sender.isIgnored && !isOp(conversation)) return;
|
||||
if(data.type === 'bottle' && data.target === core.connection.character) {
|
||||
core.notifications.notify(conversation, conversation.name, messageToString(message),
|
||||
await core.notifications.notify(conversation, conversation.name, messageToString(message),
|
||||
characterImage(data.character), 'attention');
|
||||
if(conversation !== state.selectedConversation || !state.windowFocused)
|
||||
conversation.unread = Interfaces.UnreadState.Mention;
|
||||
|
@ -648,13 +650,13 @@ export default function(this: void): Interfaces.State {
|
|||
url += `newspost/${data.target_id}/#Comment${data.id}`;
|
||||
break;
|
||||
case 'bugreport':
|
||||
url += `view_bugreport.php?id=/${data.target_id}/#${data.id}`;
|
||||
url += `view_bugreport.php?id=${data.target_id}/#${data.id}`;
|
||||
break;
|
||||
case 'changelog':
|
||||
url += `log.php?id=/${data.target_id}/#${data.id}`;
|
||||
url += `log.php?id=${data.target_id}/#${data.id}`;
|
||||
break;
|
||||
case 'feature':
|
||||
url += `vote.php?id=/${data.target_id}/#${data.id}`;
|
||||
url += `vote.php?id=${data.target_id}/#${data.id}`;
|
||||
}
|
||||
const key = `events.rtbComment${(data.parent_id !== 0 ? 'Reply' : '')}`;
|
||||
text = l(key, `[user]${data.name}[/user]`, l(`events.rtbComment_${data.target_type}`), `[url=${url}]${data.target}[/url]`);
|
||||
|
@ -691,7 +693,7 @@ export default function(this: void): Interfaces.State {
|
|||
}
|
||||
await addEventMessage(new EventMessage(text, time));
|
||||
if(data.type === 'note')
|
||||
core.notifications.notify(state.consoleTab, character, text, characterImage(character), 'newnote');
|
||||
await core.notifications.notify(state.consoleTab, character, text, characterImage(character), 'newnote');
|
||||
});
|
||||
type SFCMessage = (Interfaces.Message & {sfc: Connection.ServerCommands['SFC'] & {confirmed?: true}});
|
||||
const sfcList: SFCMessage[] = [];
|
||||
|
@ -699,7 +701,8 @@ export default function(this: void): Interfaces.State {
|
|||
let text: string, message: Interfaces.Message;
|
||||
if(data.action === 'report') {
|
||||
text = l('events.report', `[user]${data.character}[/user]`, decodeHTML(data.tab), decodeHTML(data.report));
|
||||
core.notifications.notify(state.consoleTab, data.character, text, characterImage(data.character), 'modalert');
|
||||
if(!data.old)
|
||||
await core.notifications.notify(state.consoleTab, data.character, text, characterImage(data.character), 'modalert');
|
||||
message = new EventMessage(text, time);
|
||||
safeAddMessage(sfcList, message, 500);
|
||||
(<SFCMessage>message).sfc = data;
|
||||
|
|
|
@ -172,6 +172,7 @@ export namespace Settings {
|
|||
readonly showNeedsReply: boolean;
|
||||
readonly enterSend: boolean;
|
||||
readonly colorBookmarks: boolean;
|
||||
readonly bbCodeBar: boolean;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -179,9 +180,10 @@ export type Settings = Settings.Settings;
|
|||
|
||||
export interface Notifications {
|
||||
isInBackground: boolean
|
||||
notify(conversation: Conversation, title: string, body: string, icon: string, sound: string): void
|
||||
playSound(sound: string): void
|
||||
notify(conversation: Conversation, title: string, body: string, icon: string, sound: string): Promise<void>
|
||||
playSound(sound: string): Promise<void>
|
||||
requestPermission(): Promise<void>
|
||||
initSounds(sounds: ReadonlyArray<string>): Promise<void>
|
||||
}
|
||||
|
||||
export interface State {
|
||||
|
|
|
@ -86,7 +86,7 @@ const strings: {[key: string]: string | undefined} = {
|
|||
'logs.date': 'Date',
|
||||
'logs.selectCharacter': 'Select a character...',
|
||||
'logs.selectConversation': 'Select a conversation...',
|
||||
'logs.selectDate': 'Select a date...',
|
||||
'logs.allDates': 'Show all',
|
||||
'user.profile': 'Profile',
|
||||
'user.message': 'Open conversation',
|
||||
'user.messageJump': 'View conversation',
|
||||
|
@ -172,6 +172,7 @@ Current log location: {1}`,
|
|||
'settings.defaultHighlights': 'Use global highlight words',
|
||||
'settings.colorBookmarks': 'Show friends/bookmarks in a different colour',
|
||||
'settings.beta': 'Opt-in to test unstable prerelease updates',
|
||||
'settings.bbCodeBar': 'Show BBCode formatting bar',
|
||||
'fixLogs.action': 'Fix corrupted logs',
|
||||
'fixLogs.text': `There are a few reason log files can become corrupted - log files from old versions with bugs that have since been fixed or incomplete file operations caused by computer crashes are the most common.
|
||||
If one of your log files is corrupted, you may get an "Unknown Type" error when you log in or when you open a specific tab. You may also experience other issues.
|
||||
|
@ -182,6 +183,7 @@ Once this process has started, do not interrupt it or your logs will get corrupt
|
|||
'fixLogs.success': 'Your logs have been fixed. If you experience any more issues, please ask in for further assistance in the Helpdesk channel.',
|
||||
'conversationSettings.title': 'Tab Settings',
|
||||
'conversationSettings.action': 'Edit settings for {0}',
|
||||
'conversationSettings.save': 'Save settings',
|
||||
'conversationSettings.default': 'Default',
|
||||
'conversationSettings.true': 'Yes',
|
||||
'conversationSettings.false': 'No',
|
||||
|
@ -286,7 +288,7 @@ Once this process has started, do not interrupt it or your logs will get corrupt
|
|||
'commands.status': 'Set status',
|
||||
'commands.status.help': 'Sets your status along with an optional message.',
|
||||
'commands.status.param0': 'Status',
|
||||
'commands.status.param0.help': 'A valid status, namely "online", "busy", "looking", "away", "dnd" or "busy".',
|
||||
'commands.status.param0.help': 'A valid status, namely "online", "busy", "looking", "away" or "dnd".',
|
||||
'commands.status.param1': 'Message',
|
||||
'commands.status.param1.help': 'An optional status message of up to 255 bytes.',
|
||||
'commands.priv': 'Open conversation',
|
||||
|
|
|
@ -11,26 +11,45 @@ export default class Notifications implements Interface {
|
|||
conversation !== core.conversations.selectedConversation || core.state.settings.alwaysNotify);
|
||||
}
|
||||
|
||||
notify(conversation: Conversation, title: string, body: string, icon: string, sound: string): void {
|
||||
async notify(conversation: Conversation, title: string, body: string, icon: string, sound: string): Promise<void> {
|
||||
if(!this.shouldNotify(conversation)) return;
|
||||
this.playSound(sound);
|
||||
if(core.state.settings.notifications) {
|
||||
/*tslint:disable-next-line:no-object-literal-type-assertion*///false positive
|
||||
const notification = new Notification(title, <NotificationOptions & {silent: boolean}>{body, icon, silent: true});
|
||||
await 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();
|
||||
};
|
||||
window.setTimeout(() => {
|
||||
notification.close();
|
||||
}, 5000);
|
||||
}
|
||||
}
|
||||
|
||||
playSound(sound: string): void {
|
||||
getOptions(conversation: Conversation, body: string, icon: string):
|
||||
NotificationOptions & {badge: string, silent: boolean, renotify: boolean} {
|
||||
const badge = <string>require(`./assets/ic_notification.png`); //tslint:disable-line:no-require-imports
|
||||
return {
|
||||
body, icon: core.state.settings.showAvatars ? icon : undefined, badge, silent: true, data: {key: conversation.key},
|
||||
tag: conversation.key, renotify: true
|
||||
};
|
||||
}
|
||||
|
||||
async playSound(sound: string): Promise<void> {
|
||||
if(!core.state.settings.playSound) return;
|
||||
const id = `soundplayer-${sound}`;
|
||||
let audio = <HTMLAudioElement | null>document.getElementById(id);
|
||||
if(audio === null) {
|
||||
audio = document.createElement('audio');
|
||||
const audio = <HTMLAudioElement>document.getElementById(`soundplayer-${sound}`);
|
||||
audio.volume = 1;
|
||||
audio.muted = false;
|
||||
return audio.play();
|
||||
}
|
||||
|
||||
initSounds(sounds: ReadonlyArray<string>): Promise<void> { //tslint:disable-line:promise-function-async
|
||||
const promises = [];
|
||||
for(const sound of sounds) {
|
||||
const id = `soundplayer-${sound}`;
|
||||
if(document.getElementById(id) !== null) continue;
|
||||
const audio = document.createElement('audio');
|
||||
audio.id = id;
|
||||
for(const name in codecs) {
|
||||
const src = document.createElement('source');
|
||||
|
@ -39,9 +58,14 @@ export default class Notifications implements Interface {
|
|||
src.src = <string>require(`./assets/${sound}.${codecs[name]}`);
|
||||
audio.appendChild(src);
|
||||
}
|
||||
document.body.appendChild(audio);
|
||||
audio.volume = 0;
|
||||
audio.muted = true;
|
||||
const promise = audio.play();
|
||||
if(promise instanceof Promise)
|
||||
promises.push(promise);
|
||||
}
|
||||
//tslint:disable-next-line:no-floating-promises
|
||||
audio.play();
|
||||
return <any>Promise.all(promises); //tslint:disable-line:no-any
|
||||
}
|
||||
|
||||
async requestPermission(): Promise<void> {
|
||||
|
|
|
@ -40,7 +40,7 @@ async function characterData(name: string | undefined): Promise<Character> {
|
|||
};
|
||||
const newKinks: {[key: string]: KinkChoiceFull} = {};
|
||||
for(const key in data.kinks)
|
||||
newKinks[key] = <KinkChoiceFull>(data.kinks[key] === 'fave' ? 'favorite' : data.kinks[key]);
|
||||
newKinks[key] = <KinkChoiceFull>(<string>data.kinks[key] === 'fave' ? 'favorite' : data.kinks[key]);
|
||||
const newCustoms: CharacterCustom[] = [];
|
||||
for(const key in data.custom_kinks) {
|
||||
const custom = data.custom_kinks[key];
|
||||
|
|
|
@ -281,18 +281,12 @@ const commands: {readonly [key: string]: Command | undefined} = {
|
|||
params: [{type: ParamType.Character}]
|
||||
},
|
||||
closeroom: {
|
||||
exec: (conv: ChannelConversation) => {
|
||||
core.connection.send('RST', {channel: conv.channel.id, status: 'private'});
|
||||
core.connection.send('ORS');
|
||||
},
|
||||
exec: (conv: ChannelConversation) => core.connection.send('RST', {channel: conv.channel.id, status: 'private'}),
|
||||
permission: Permission.RoomOwner,
|
||||
context: CommandContext.Channel
|
||||
},
|
||||
openroom: {
|
||||
exec: (conv: ChannelConversation) => {
|
||||
core.connection.send('RST', {channel: conv.channel.id, status: 'public'});
|
||||
core.connection.send('ORS');
|
||||
},
|
||||
exec: (conv: ChannelConversation) => core.connection.send('RST', {channel: conv.channel.id, status: 'public'}),
|
||||
permission: Permission.RoomOwner,
|
||||
context: CommandContext.Channel
|
||||
},
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
<template>
|
||||
<div class="dropdown">
|
||||
<button class="form-control custom-select" aria-haspopup="true" :aria-expanded="isOpen" @click="isOpen = true"
|
||||
<a class="form-control custom-select" aria-haspopup="true" :aria-expanded="isOpen" @click="isOpen = true"
|
||||
@blur="isOpen = false" style="width:100%;text-align:left;display:flex;align-items:center" role="button" tabindex="-1">
|
||||
<div style="flex:1">
|
||||
<slot name="title" style="flex:1"></slot>
|
||||
</div>
|
||||
</button>
|
||||
</a>
|
||||
<div class="dropdown-menu" :style="open ? 'display:block' : ''" @mousedown.stop.prevent @click="isOpen = false"
|
||||
ref="menu">
|
||||
<slot></slot>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<template>
|
||||
<span v-show="isShown">
|
||||
<div tabindex="-1" class="modal" @click.self="hideWithCheck" style="display:flex">
|
||||
<div class="modal" @click.self="hideWithCheck" style="display:flex">
|
||||
<div class="modal-dialog" :class="dialogClass" style="display:flex;align-items:center">
|
||||
<div class="modal-content" style="max-height:100%">
|
||||
<div class="modal-header" style="flex-shrink:0">
|
||||
|
@ -9,7 +9,7 @@
|
|||
</h4>
|
||||
<button type="button" class="close" @click="hide" aria-label="Close" v-show="!keepOpen">×</button>
|
||||
</div>
|
||||
<div class="modal-body" style="overflow:auto" tabindex="-1">
|
||||
<div class="modal-body" style="overflow:auto;-webkit-overflow-scrolling:auto" tabindex="-1">
|
||||
<slot></slot>
|
||||
</div>
|
||||
<div class="modal-footer" v-if="buttons">
|
||||
|
@ -40,10 +40,12 @@
|
|||
if(dialogStack.length > 0) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
dialogStack.pop()!.isShown = false;
|
||||
dialogStack[dialogStack.length - 1].hide();
|
||||
}
|
||||
}, true);
|
||||
|
||||
export let isShowing = false;
|
||||
|
||||
@Component
|
||||
export default class Modal extends Vue {
|
||||
@Prop({default: ''})
|
||||
|
@ -72,18 +74,20 @@
|
|||
if(!e.defaultPrevented) this.hideWithCheck();
|
||||
}
|
||||
|
||||
/*tslint:disable-next-line:typedef*///https://github.com/palantir/tslint/issues/711
|
||||
show(keepOpen = false): void {
|
||||
this.isShown = true;
|
||||
show(keepOpen: boolean = false): void {
|
||||
this.keepOpen = keepOpen;
|
||||
if(this.isShown) return;
|
||||
this.isShown = true;
|
||||
dialogStack.push(this);
|
||||
this.$emit('open');
|
||||
isShowing = true;
|
||||
}
|
||||
|
||||
hide(): void {
|
||||
this.isShown = false;
|
||||
this.$emit('close');
|
||||
dialogStack.pop();
|
||||
if(dialogStack.length === 0) isShowing = false;
|
||||
}
|
||||
|
||||
hideWithCheck(): void {
|
||||
|
|
|
@ -18,7 +18,7 @@
|
|||
|
||||
@Component
|
||||
export default class CharacterSelect extends Vue {
|
||||
@Prop({required: true, type: Number})
|
||||
@Prop({required: true})
|
||||
readonly value!: number;
|
||||
|
||||
get characters(): SelectItem[] {
|
||||
|
|
|
@ -2,11 +2,15 @@ import Vue from 'vue';
|
|||
import Modal from './Modal.vue';
|
||||
|
||||
export default class CustomDialog extends Vue {
|
||||
protected get dialog(): Modal {
|
||||
return <Modal>this.$children[0];
|
||||
}
|
||||
|
||||
show(): void {
|
||||
(<Modal>this.$children[0]).show();
|
||||
this.dialog.show();
|
||||
}
|
||||
|
||||
hide(): void {
|
||||
(<Modal>this.$children[0]).hide();
|
||||
this.dialog.hide();
|
||||
}
|
||||
}
|
|
@ -7,11 +7,11 @@
|
|||
<i class="fa fa-cog"></i>
|
||||
</div>
|
||||
<ul class="nav nav-tabs" style="border-bottom:0;margin-bottom:-2px" ref="tabs">
|
||||
<li v-for="tab in tabs" :key="tab.view.id" class="nav-item" @auxclick="remove(tab)">
|
||||
<li v-for="tab in tabs" :key="tab.view.id" class="nav-item" @click.middle="remove(tab)">
|
||||
<a href="#" @click.prevent="show(tab)" class="nav-link"
|
||||
:class="{active: tab === activeTab, hasNew: tab.hasNew && tab !== activeTab}">
|
||||
<img v-if="tab.user" :src="'https://static.f-list.net/images/avatar/' + tab.user.toLowerCase() + '.png'"/>
|
||||
{{tab.user || l('window.newTab')}}
|
||||
<span class="d-sm-inline d-none">{{tab.user || l('window.newTab')}}</span>
|
||||
<a href="#" class="btn" :aria-label="l('action.close')" style="margin-left:10px;padding:0;color:inherit"
|
||||
@click.stop="remove(tab)"><i class="fa fa-times"></i>
|
||||
</a>
|
||||
|
@ -245,7 +245,7 @@
|
|||
}
|
||||
|
||||
openMenu(): void {
|
||||
electron.remote.Menu.getApplicationMenu()!.popup();
|
||||
electron.remote.Menu.getApplicationMenu()!.popup({});
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
{
|
||||
"name": "fchat",
|
||||
"version": "3.0.3",
|
||||
"version": "3.0.6",
|
||||
"author": "The F-List Team",
|
||||
"description": "F-List.net Chat Client",
|
||||
"main": "main.js",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"electron": "^1.8.4"
|
||||
"electron": "^2.0.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"keytar": "^4.2.1",
|
||||
|
|
|
@ -69,13 +69,14 @@ 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 {
|
||||
dataCallback(data: {culprit: string, exception?: {values: {stacktrace: {frames: {filename: string}[]}}[]}}): void {
|
||||
data.culprit = `~${data.culprit.substr(data.culprit.lastIndexOf('/'))}`;
|
||||
for(const ex of data.exception.values)
|
||||
for(const frame of ex.stacktrace.frames) {
|
||||
const index = frame.filename.lastIndexOf('/');
|
||||
frame.filename = index !== -1 ? `~${frame.filename.substr(index)}` : frame.filename;
|
||||
}
|
||||
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) => {
|
||||
|
@ -103,6 +104,12 @@ function openIncognito(url: string): void {
|
|||
case 'ChromeHTML':
|
||||
exec(`start chrome.exe -incognito ${url}`);
|
||||
break;
|
||||
case 'VivaldiHTM':
|
||||
exec(`start vivaldi.exe -incognito ${url}`);
|
||||
break;
|
||||
case 'OperaStable':
|
||||
exec(`start opera.exe -private ${url}`);
|
||||
break;
|
||||
default:
|
||||
exec(`start iexplore.exe -private ${url}`);
|
||||
}
|
||||
|
@ -187,7 +194,7 @@ webContents.on('context-menu', (_, props) => {
|
|||
}
|
||||
}, {type: 'separator'});
|
||||
|
||||
if(menuTemplate.length > 0) electron.remote.Menu.buildFromTemplate(menuTemplate).popup();
|
||||
if(menuTemplate.length > 0) electron.remote.Menu.buildFromTemplate(menuTemplate).popup({});
|
||||
});
|
||||
|
||||
let dictDir = path.join(electron.remote.app.getPath('userData'), 'spellchecker');
|
||||
|
|
|
@ -7,17 +7,12 @@ import BaseNotifications from '../chat/notifications';
|
|||
const browserWindow = remote.getCurrentWindow();
|
||||
|
||||
export default class Notifications extends BaseNotifications {
|
||||
notify(conversation: Conversation, title: string, body: string, icon: string, sound: string): void {
|
||||
async notify(conversation: Conversation, title: string, body: string, icon: string, sound: string): Promise<void> {
|
||||
if(!this.shouldNotify(conversation)) return;
|
||||
this.playSound(sound);
|
||||
await this.playSound(sound);
|
||||
browserWindow.flashFrame(true);
|
||||
if(core.state.settings.notifications) {
|
||||
/*tslint:disable-next-line:no-object-literal-type-assertion*///false positive
|
||||
const notification = new Notification(title, <NotificationOptions & {silent: boolean}>{
|
||||
body,
|
||||
icon: core.state.settings.showAvatars ? icon : undefined,
|
||||
silent: true
|
||||
});
|
||||
const notification = new Notification(title, this.getOptions(conversation, body, icon));
|
||||
notification.onclick = () => {
|
||||
browserWindow.webContents.send('show-tab', remote.getCurrentWebContents().id);
|
||||
conversation.show();
|
||||
|
|
|
@ -3,6 +3,7 @@ const ExtractTextPlugin = require('extract-text-webpack-plugin');
|
|||
const fs = require('fs');
|
||||
const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin');
|
||||
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')],
|
||||
|
@ -58,8 +59,9 @@ const mainConfig = {
|
|||
test: /\.vue$/,
|
||||
loader: 'vue-loader',
|
||||
options: {
|
||||
preserveWhitespace: false,
|
||||
cssSourceMap: false
|
||||
compilerOptions: {
|
||||
preserveWhitespace: false
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
|
@ -76,7 +78,9 @@ const mainConfig = {
|
|||
{test: /\.ttf(\?v=\d+\.\d+\.\d+)?$/, loader: 'file-loader'},
|
||||
{test: /\.svg(\?v=\d+\.\d+\.\d+)?$/, loader: 'file-loader'},
|
||||
{test: /\.(wav|mp3|ogg)$/, loader: 'file-loader?name=sounds/[name].[ext]'},
|
||||
{test: /\.(png|html)$/, loader: 'file-loader?name=[name].[ext]'}
|
||||
{test: /\.(png|html)$/, loader: 'file-loader?name=[name].[ext]'},
|
||||
{test: /\.vue\.scss/, loader: ['vue-style-loader','css-loader','sass-loader']},
|
||||
{test: /\.vue\.css/, loader: ['vue-style-loader','css-loader']},
|
||||
]
|
||||
},
|
||||
node: {
|
||||
|
@ -90,7 +94,8 @@ const mainConfig = {
|
|||
tslint: path.join(__dirname, '../tslint.json'),
|
||||
tsconfig: './tsconfig-renderer.json',
|
||||
vue: true
|
||||
})
|
||||
}),
|
||||
new VueLoaderPlugin()
|
||||
],
|
||||
resolve: {
|
||||
extensions: ['.ts', '.js', '.vue', '.css'],
|
||||
|
@ -111,13 +116,13 @@ module.exports = function(mode) {
|
|||
rendererConfig.entry.chat.push(absPath);
|
||||
const plugin = new ExtractTextPlugin('themes/' + theme.slice(0, -5) + '.css');
|
||||
rendererConfig.plugins.push(plugin);
|
||||
rendererConfig.module.rules.push({test: absPath, use: plugin.extract(cssOptions)});
|
||||
rendererConfig.module.rules.unshift({test: absPath, use: plugin.extract(cssOptions)});
|
||||
}
|
||||
const faPath = path.join(themesDir, '../../fa.scss');
|
||||
rendererConfig.entry.chat.push(faPath);
|
||||
const faPlugin = new ExtractTextPlugin('./fa.css');
|
||||
rendererConfig.plugins.push(faPlugin);
|
||||
rendererConfig.module.rules.push({test: faPath, use: faPlugin.extract(cssOptions)});
|
||||
rendererConfig.module.rules.unshift({test: faPath, use: faPlugin.extract(cssOptions)});
|
||||
if(mode === 'production') {
|
||||
process.env.NODE_ENV = 'production';
|
||||
mainConfig.devtool = rendererConfig.devtool = 'source-map';
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import Axios, {AxiosResponse} from 'axios';
|
||||
import Axios, {AxiosError, AxiosResponse} from 'axios';
|
||||
import * as qs from 'qs';
|
||||
import {Connection as Interfaces, WebSocketConnection} from './interfaces';
|
||||
|
||||
|
@ -36,7 +36,9 @@ export default class Connection implements Interfaces.Connection {
|
|||
try {
|
||||
this.ticket = await this.ticketProvider();
|
||||
} catch(e) {
|
||||
if(this.reconnectTimer !== undefined) this.reconnect();
|
||||
if(this.reconnectTimer !== undefined)
|
||||
if((<AxiosError>e).request !== undefined) this.reconnect();
|
||||
else await this.invokeHandlers('closed', false);
|
||||
return this.invokeErrorHandlers(<Error>e, true);
|
||||
}
|
||||
try {
|
||||
|
|
|
@ -104,8 +104,9 @@ export namespace Connection {
|
|||
type: 'grouprequest' | 'bugreport' | 'helpdeskticket' | 'helpdeskreply' | 'featurerequest',
|
||||
name: string, id: number, title?: string
|
||||
} | {type: 'trackadd' | 'trackrem' | 'friendadd' | 'friendremove' | 'friendrequest', name: string},
|
||||
SFC: {action: 'confirm', moderator: string, character: string, timestamp: string, tab: string, logid: number} |
|
||||
{callid: number, action: 'report', report: string, timestamp: string, character: string, tab: string, logid: number},
|
||||
SFC: {action: 'confirm', moderator: string, character: string, timestamp: string, tab: string, logid: number} | {
|
||||
callid: number, action: 'report', report: string, timestamp: string, character: string, tab: string, logid: number, old?: true
|
||||
},
|
||||
STA: {status: Character.Status, character: string, statusmsg: string},
|
||||
SYS: {message: string, channel?: string},
|
||||
TPN: {character: string, status: Character.TypingStatus},
|
||||
|
@ -123,10 +124,10 @@ export namespace Connection {
|
|||
readonly chat_max: number
|
||||
readonly priv_max: number
|
||||
readonly lfrp_max: number
|
||||
//readonly cds_max: number
|
||||
readonly cds_max: number
|
||||
readonly lfrp_flood: number
|
||||
readonly msg_flood: number
|
||||
//readonly sta_flood: number
|
||||
readonly sta_flood: number
|
||||
readonly permissions: number
|
||||
readonly icon_blacklist: ReadonlyArray<string>
|
||||
}
|
||||
|
|
|
@ -1 +1,2 @@
|
|||
/build
|
||||
/release
|
|
@ -8,8 +8,8 @@ android {
|
|||
applicationId "net.f_list.fchat"
|
||||
minSdkVersion 19
|
||||
targetSdkVersion 27
|
||||
versionCode 14
|
||||
versionName "3.0.3"
|
||||
versionCode 17
|
||||
versionName "3.0.6"
|
||||
}
|
||||
buildTypes {
|
||||
release {
|
||||
|
|
|
@ -24,7 +24,7 @@ import java.util.*
|
|||
|
||||
class MainActivity : Activity() {
|
||||
private lateinit var webView: WebView
|
||||
private val profileRegex = Regex("^https?://(www\\.)?f-list.net/c/(.+)/?#?")
|
||||
private val profileRegex = Regex("^https?://(www\\.)?f-list.net/c/([^/#]+)/?#?")
|
||||
private val backgroundPlugin = Background(this)
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
|
|
|
@ -4,9 +4,10 @@ buildscript {
|
|||
ext.kotlin_version = '1.2.30'
|
||||
repositories {
|
||||
jcenter()
|
||||
google()
|
||||
}
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:2.3.2'
|
||||
classpath 'com.android.tools.build:gradle:3.1.3'
|
||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||
|
||||
// NOTE: Do not place your application dependencies here; they belong
|
||||
|
@ -17,6 +18,7 @@ buildscript {
|
|||
allprojects {
|
||||
repositories {
|
||||
jcenter()
|
||||
google()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -43,13 +43,14 @@ const version = (<{version: string}>require('./package.json')).version; //tslint
|
|||
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}[]}}[]}}) => {
|
||||
dataCallback: (data: {culprit: string, exception?: {values: {stacktrace: {frames: {filename: string}[]}}[]}}) => {
|
||||
data.culprit = `~${data.culprit.substr(data.culprit.lastIndexOf('/'))}`;
|
||||
for(const ex of data.exception.values)
|
||||
for(const frame of ex.stacktrace.frames) {
|
||||
const index = frame.filename.lastIndexOf('/');
|
||||
frame.filename = index !== -1 ? `~${frame.filename.substr(index)}` : frame.filename;
|
||||
}
|
||||
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) => {
|
||||
|
|
|
@ -66,7 +66,7 @@ export class Logs implements Logging {
|
|||
private async getIndex(name: string): Promise<Index> {
|
||||
if(this.loadedCharacter === name) return this.loadedIndex!;
|
||||
this.loadedCharacter = name;
|
||||
return this.loadedIndex = await NativeLogs.loadIndex(name);
|
||||
return this.loadedIndex = name === core.connection.character ? this.index : await NativeLogs.loadIndex(name);
|
||||
}
|
||||
|
||||
async getLogs(character: string, key: string, date: Date): Promise<ReadonlyArray<Conversation.Message>> {
|
||||
|
|
|
@ -3,8 +3,9 @@ import WebKit
|
|||
|
||||
class IndexItem: Encodable {
|
||||
let name: String
|
||||
var index = [UInt16: UInt64]()
|
||||
var index = NSMutableOrderedSet()
|
||||
var dates = [UInt16]()
|
||||
var offsets = [UInt64]()
|
||||
init(_ name: String) {
|
||||
self.name = name
|
||||
}
|
||||
|
@ -70,7 +71,8 @@ class Logs: NSObject, WKScriptMessageHandler {
|
|||
indexItem.dates.append(date)
|
||||
var o: UInt64 = 0
|
||||
data.getBytes(&o, range: NSMakeRange(offset + 2, 5))
|
||||
indexItem.index[date] = o
|
||||
indexItem.index.add(date)
|
||||
indexItem.offsets.append(o)
|
||||
offset += 7
|
||||
}
|
||||
index[file.deletingPathExtension().lastPathComponent] = indexItem
|
||||
|
@ -102,7 +104,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[day] == nil) {
|
||||
if(!(indexItem?.index.contains(day) ?? false)) {
|
||||
let indexFile = url.appendingPathExtension("idx")
|
||||
if(indexItem == nil) { fm.createFile(atPath: indexFile.path, contents: nil) }
|
||||
let indexFd = try FileHandle(forWritingTo: indexFile)
|
||||
|
@ -118,7 +120,8 @@ class Logs: NSObject, WKScriptMessageHandler {
|
|||
write(indexFd.fileDescriptor, &day, 2)
|
||||
var offset = fd.offsetInFile
|
||||
write(indexFd.fileDescriptor, &offset, 5)
|
||||
indexItem!.index[day] = offset
|
||||
indexItem!.index.add(indexItem!.offsets.count)
|
||||
indexItem!.offsets.append(offset)
|
||||
indexItem!.dates.append(day)
|
||||
}
|
||||
let start = fd.offsetInFile
|
||||
|
@ -150,25 +153,29 @@ class Logs: NSObject, WKScriptMessageHandler {
|
|||
let newOffset = file.offsetInFile - UInt64(length + 2)
|
||||
file.seek(toFileOffset: newOffset)
|
||||
read(file.fileDescriptor, buffer, Int(length))
|
||||
strings.append(deserializeMessage().0)
|
||||
strings.append(try deserializeMessage(buffer, 0).0)
|
||||
file.seek(toFileOffset: newOffset)
|
||||
}
|
||||
return "[" + strings.reversed().joined(separator: ",") + "]"
|
||||
}
|
||||
|
||||
func getLogs(_ character: String, _ key: String, _ date: UInt16) throws -> String {
|
||||
guard let offset = loadedIndex![key]?.index[date] else { return "[]" }
|
||||
let index = loadedIndex![key]
|
||||
guard let indexKey = index?.index.index(of: date) else { return "[]" }
|
||||
let url = baseDir.appendingPathComponent("\(character)/logs/\(key)", isDirectory: false)
|
||||
let file = try FileHandle(forReadingFrom: url)
|
||||
let size = file.seekToEndOfFile()
|
||||
file.seek(toFileOffset: offset)
|
||||
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)
|
||||
read(file.fileDescriptor, buffer, length)
|
||||
var json = "["
|
||||
while file.offsetInFile < size {
|
||||
read(file.fileDescriptor, buffer, 51000)
|
||||
let deserialized = deserializeMessage(date)
|
||||
if(deserialized.1 == 0) { break }
|
||||
var offset = 0
|
||||
while offset < length {
|
||||
let deserialized = try deserializeMessage(buffer, offset)
|
||||
offset = deserialized.1 + 2
|
||||
json += deserialized.0 + ","
|
||||
file.seek(toFileOffset: file.offsetInFile + UInt64(deserialized.1 + 2))
|
||||
}
|
||||
return json + "]"
|
||||
}
|
||||
|
@ -178,14 +185,19 @@ class Logs: NSObject, WKScriptMessageHandler {
|
|||
return String(data: try JSONEncoder().encode(loadedIndex), encoding: .utf8)!
|
||||
}
|
||||
|
||||
func deserializeMessage(_ checkDate: UInt16 = 0) -> (String, Int) {
|
||||
let date = buffer.load(as: UInt32.self)
|
||||
if(checkDate != 0 && date / 86400 != checkDate) { return ("", 0) }
|
||||
let type = buffer.load(fromByteOffset: 4, as: UInt8.self)
|
||||
let senderLength = Int(buffer.load(fromByteOffset: 5, as: UInt8.self))
|
||||
let sender = String(bytesNoCopy: buffer.advanced(by: 6), length: senderLength, encoding: .utf8, freeWhenDone: false)!
|
||||
let textLength = Int(buffer.advanced(by: 6 + senderLength).bindMemory(to: UInt16.self, capacity: 1).pointee)
|
||||
let text = String(bytesNoCopy: buffer.advanced(by: 6 + senderLength + 2), length: textLength, encoding: .utf8, freeWhenDone: false)!
|
||||
return ("{\"time\":\(date),\"type\":\(type),\"sender\":\(File.escape(sender)),\"text\":\(File.escape(text))}", senderLength + textLength + 8)
|
||||
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 {
|
||||
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 {
|
||||
throw NSError(domain: "Log corruption", code: 0)
|
||||
}
|
||||
return ("{\"time\":\(date),\"type\":\(type),\"sender\":\(File.escape(sender)),\"text\":\(File.escape(text))}", offset + textLength + 2)
|
||||
}
|
||||
}
|
|
@ -4,7 +4,7 @@ import AVFoundation
|
|||
|
||||
class ViewController: UIViewController, WKNavigationDelegate, WKUIDelegate {
|
||||
var webView: WKWebView!
|
||||
let profileRegex = try! NSRegularExpression(pattern: "^https?://(www\\.)?f-list.net/c/(.+)/?#?", options: [.caseInsensitive])
|
||||
let profileRegex = try! NSRegularExpression(pattern: "^https?://(www\\.)?f-list.net/c/([^/#]+)/?#?", options: [.caseInsensitive])
|
||||
|
||||
override func loadView() {
|
||||
let config = WKWebViewConfiguration()
|
||||
|
|
|
@ -15,7 +15,7 @@ document.addEventListener('notification-clicked', (e: Event) => {
|
|||
});
|
||||
|
||||
export default class Notifications extends BaseNotifications {
|
||||
notify(conversation: Conversation, title: string, body: string, icon: string, sound: string): void {
|
||||
async notify(conversation: Conversation, title: string, body: string, icon: string, sound: string): Promise<void> {
|
||||
if(!this.shouldNotify(conversation)) return;
|
||||
NativeNotification.notify(core.state.settings.notifications && this.isInBackground, title, body, icon,
|
||||
core.state.settings.playSound ? sound : null, conversation.key); //tslint:disable-line:no-null-keyword
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "net.f_list.fchat",
|
||||
"version": "3.0.3",
|
||||
"version": "3.0.6",
|
||||
"displayName": "F-Chat",
|
||||
"author": "The F-List Team",
|
||||
"description": "F-List.net Chat Client",
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
const path = require('path');
|
||||
const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin');
|
||||
const VueLoaderPlugin = require('vue-loader/lib/plugin');
|
||||
|
||||
const config = {
|
||||
entry: {
|
||||
|
@ -25,8 +26,9 @@ const config = {
|
|||
test: /\.vue$/,
|
||||
loader: 'vue-loader',
|
||||
options: {
|
||||
preserveWhitespace: false,
|
||||
cssSourceMap: false
|
||||
compilerOptions: {
|
||||
preserveWhitespace: false
|
||||
}
|
||||
}
|
||||
},
|
||||
{test: /\.eot(\?v=\d+\.\d+\.\d+)?$/, loader: 'file-loader'},
|
||||
|
@ -35,11 +37,14 @@ const config = {
|
|||
{test: /\.svg(\?v=\d+\.\d+\.\d+)?$/, loader: 'file-loader'},
|
||||
{test: /\.(wav|mp3|ogg)$/, loader: 'file-loader?name=sounds/[name].[ext]'},
|
||||
{test: /\.(png|html)$/, loader: 'file-loader?name=[name].[ext]'},
|
||||
{test: /\.scss/, use: ['css-loader', 'sass-loader']}
|
||||
{test: /(?<!\.vue)\.scss/, use: ['css-loader', 'sass-loader']},
|
||||
{test: /\.vue\.scss/, loader: ['vue-style-loader','css-loader','sass-loader']},
|
||||
{test: /\.vue\.css/, loader: ['vue-style-loader','css-loader']},
|
||||
]
|
||||
},
|
||||
plugins: [
|
||||
new ForkTsCheckerWebpackPlugin({workers: 2, async: false, vue: true, tslint: path.join(__dirname, '../tslint.json')})
|
||||
new ForkTsCheckerWebpackPlugin({workers: 2, async: false, vue: true, tslint: path.join(__dirname, '../tslint.json')}),
|
||||
new VueLoaderPlugin()
|
||||
],
|
||||
resolve: {
|
||||
'extensions': ['.ts', '.js', '.vue', '.scss']
|
||||
|
|
10
package.json
10
package.json
|
@ -6,18 +6,18 @@
|
|||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@fortawesome/fontawesome-free-webfonts": "^1.0.6",
|
||||
"@types/node": "^9.6.5",
|
||||
"@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": "^1.8.4",
|
||||
"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.11",
|
||||
"file-loader": "^1.1.10",
|
||||
"fork-ts-checker-webpack-plugin": "^0.4.1",
|
||||
"lodash": "^4.16.4",
|
||||
"node-sass": "^4.8.3",
|
||||
|
@ -32,7 +32,7 @@
|
|||
"typescript": "^2.8.1",
|
||||
"vue": "^2.5.16",
|
||||
"vue-class-component": "^6.0.0",
|
||||
"vue-loader": "^14.2.2",
|
||||
"vue-loader": "^15.2.4",
|
||||
"vue-property-decorator": "^6.0.0",
|
||||
"vue-template-compiler": "^2.5.16",
|
||||
"webpack": "^4.5.0"
|
||||
|
@ -41,6 +41,6 @@
|
|||
"@types/lodash": "^4.14.107",
|
||||
"keytar": "^4.2.1",
|
||||
"spellchecker": "^3.4.3",
|
||||
"style-loader": "^0.20.3"
|
||||
"style-loader": "^0.21.0"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -40,4 +40,15 @@
|
|||
@media (min-width: breakpoint-min(sm)) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.bbcode-editor-controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
float: right;
|
||||
order: 1;
|
||||
justify-content: flex-end;
|
||||
@media (max-width: breakpoint-max(xs)) {
|
||||
flex: 1 49%;
|
||||
}
|
||||
}
|
|
@ -241,7 +241,7 @@ $genders: (
|
|||
}
|
||||
}
|
||||
|
||||
.user-bookmark {
|
||||
.user-bookmark, .message-event .user-bookmark {
|
||||
color: #66CC33;
|
||||
}
|
||||
|
||||
|
|
|
@ -55,6 +55,6 @@
|
|||
}
|
||||
|
||||
* {
|
||||
-webkit-overflow-scrolling: touch;
|
||||
min-height: 0;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
|
@ -50,6 +50,11 @@ $text-background-color-disabled: $gray-800 !default;
|
|||
cursor: default;
|
||||
}
|
||||
|
||||
.custom-select:hover {
|
||||
text-decoration: none;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
select {
|
||||
@extend .custom-select;
|
||||
-webkit-appearance: none;
|
||||
|
|
|
@ -4,13 +4,21 @@ hr {
|
|||
}
|
||||
|
||||
.modal-dialog.modal-wide {
|
||||
width: 95%;
|
||||
max-width: 95%;
|
||||
}
|
||||
|
||||
.card-title {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.nav-link {
|
||||
cursor: pointer;
|
||||
}
|
||||
$theme-is-dark: false !default;
|
||||
|
||||
// HACK: Bootstrap offers no way to override these by default, and they are SUPER bright.
|
||||
// The level numbers have been changed to make them work for dark themes.
|
||||
@if $theme-is-dark {
|
||||
@each $color, $value in $theme-colors {
|
||||
@include table-row-variant($color, theme-color-level($color, 5));
|
||||
}
|
||||
}
|
||||
|
||||
@include table-row-variant(active, $table-active-bg);
|
|
@ -22,9 +22,6 @@
|
|||
}
|
||||
}
|
||||
|
||||
.note-folder-create {
|
||||
}
|
||||
|
||||
.conversation-from-me, .conversation-from-them {
|
||||
margin-bottom: 5px;
|
||||
max-width: percentage((($grid-columns - 4) / $grid-columns));
|
||||
|
|
|
@ -18,6 +18,7 @@
|
|||
|
||||
.tag-input {
|
||||
background-color: $input-bg;
|
||||
color: $input-color;
|
||||
border: none;
|
||||
width: auto;
|
||||
&:focus {
|
||||
|
@ -32,11 +33,11 @@
|
|||
}
|
||||
|
||||
.tag-error {
|
||||
border: 1px solid theme-color-border(danger);
|
||||
background-color: theme-color-bg(danger);
|
||||
border: 1px solid theme-color-level(danger, $alert-border-level);
|
||||
background-color: theme-color-level(danger, $alert-bg-level);
|
||||
.tag-input {
|
||||
text-color: theme-color-level(danger, 6);
|
||||
background-color: theme-color-bg(danger);
|
||||
text-color: theme-color-level(danger, $alert-color-level);
|
||||
background-color: theme-color-level(danger, $alert-bg-level);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -59,4 +59,7 @@ $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";
|
|
@ -57,4 +57,7 @@ $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";
|
|
@ -1,9 +1,9 @@
|
|||
@function lighten($color, $amount) {
|
||||
@return hsl(hue($color), saturation($color), lightness($color) - $amount);
|
||||
@return hsla(hue($color), saturation($color), lightness($color) - $amount, alpha($color));
|
||||
}
|
||||
|
||||
@function darken($color, $amount) {
|
||||
@return hsl(hue($color), saturation($color), lightness($color) + $amount);
|
||||
@return hsla(hue($color), saturation($color), lightness($color) + $amount, alpha($color));
|
||||
}
|
||||
|
||||
@function theme-color-level($color-name: "primary", $level: 0) {
|
||||
|
@ -12,4 +12,9 @@
|
|||
$level: abs($level);
|
||||
|
||||
@return mix($color-base, $color, $level * $theme-color-interval);
|
||||
}
|
||||
}
|
||||
|
||||
// Alert color levels
|
||||
$alert-bg-level: 7;
|
||||
$alert-border-level: 6;
|
||||
$alert-color-level: -8;
|
|
@ -35,12 +35,12 @@ import Vue from 'vue';
|
|||
import Chat from '../chat/Chat.vue';
|
||||
import {init as initCore} from '../chat/core';
|
||||
import l from '../chat/localize';
|
||||
import Notifications from '../chat/notifications';
|
||||
import VueRaven 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
|
||||
import {Logs, SettingsStore} from './logs';
|
||||
import Notifications from './notifications';
|
||||
|
||||
//@ts-ignore
|
||||
if(typeof window.Promise !== 'function' || typeof window.Notification !== 'function') //tslint:disable-line:strict-type-predicates
|
||||
|
@ -52,15 +52,18 @@ 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}[]}}[]}}) => {
|
||||
const end = data.culprit.lastIndexOf('?');
|
||||
data.culprit = `~${data.culprit.substring(data.culprit.lastIndexOf('/'), end === -1 ? undefined : end)}`;
|
||||
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)}`;
|
||||
}
|
||||
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) => {
|
||||
|
@ -77,7 +80,7 @@ const ticketProvider = async() => {
|
|||
throw new Error(data.error);
|
||||
};
|
||||
|
||||
const connection = new Connection('F-Chat 3.0 (Web)', '3.0', Socket, chatSettings.account, ticketProvider);
|
||||
const connection = new Connection('F-Chat 3.0 (Web)', version, Socket, chatSettings.account, ticketProvider);
|
||||
initCore(connection, Logs, SettingsStore, Notifications);
|
||||
|
||||
window.addEventListener('beforeunload', (e) => {
|
||||
|
|
|
@ -0,0 +1,25 @@
|
|||
import core from '../chat/core';
|
||||
import {Conversation} from '../chat/interfaces';
|
||||
//tslint:disable-next-line:match-default-export-name
|
||||
import BaseNotifications from '../chat/notifications';
|
||||
|
||||
export default class Notifications extends BaseNotifications {
|
||||
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);
|
||||
} 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();
|
||||
};
|
||||
})();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "net.f_list.fchat",
|
||||
"version": "3.0.3",
|
||||
"version": "3.0.6",
|
||||
"displayName": "F-Chat",
|
||||
"author": "The F-List Team",
|
||||
"description": "F-List.net Chat Client",
|
||||
|
|
|
@ -0,0 +1,15 @@
|
|||
let client;
|
||||
|
||||
self.addEventListener('install', function(event) {
|
||||
self.skipWaiting();
|
||||
});
|
||||
|
||||
self.addEventListener('activate', function(event){
|
||||
event.waitUntil(clients.claim());
|
||||
client = clients.matchAll().then(x => client = x[0]);
|
||||
});
|
||||
|
||||
self.addEventListener('notificationclick', function(event) {
|
||||
event.notification.close();
|
||||
client.postMessage(event.notification.data);
|
||||
});
|
|
@ -1,5 +1,6 @@
|
|||
const path = require('path');
|
||||
const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin');
|
||||
const VueLoaderPlugin = require('vue-loader/lib/plugin');
|
||||
|
||||
const config = {
|
||||
entry: __dirname + '/chat.ts',
|
||||
|
@ -22,8 +23,9 @@ const config = {
|
|||
test: /\.vue$/,
|
||||
loader: 'vue-loader',
|
||||
options: {
|
||||
preserveWhitespace: false,
|
||||
cssSourceMap: false
|
||||
compilerOptions: {
|
||||
preserveWhitespace: false
|
||||
}
|
||||
}
|
||||
},
|
||||
{test: /\.eot(\?v=\d+\.\d+\.\d+)?$/, loader: 'file-loader'},
|
||||
|
@ -31,11 +33,14 @@ const config = {
|
|||
{test: /\.ttf(\?v=\d+\.\d+\.\d+)?$/, loader: 'file-loader'},
|
||||
{test: /\.svg(\?v=\d+\.\d+\.\d+)?$/, loader: 'file-loader'},
|
||||
{test: /\.(wav|mp3|ogg)$/, loader: 'file-loader?name=sounds/[name].[ext]'},
|
||||
{test: /\.scss/, use: ['style-loader', 'css-loader', 'sass-loader']}
|
||||
{test: /\.(png|html)$/, loader: 'file-loader?name=[name].[ext]'},
|
||||
{test: /\.scss/, use: ['vue-style-loader', 'css-loader', 'sass-loader']},
|
||||
{test: /\.css/, use: ['vue-style-loader', 'css-loader']}
|
||||
]
|
||||
},
|
||||
plugins: [
|
||||
new ForkTsCheckerWebpackPlugin({workers: 2, async: false, vue: true, tslint: path.join(__dirname, '../tslint.json')})
|
||||
new ForkTsCheckerWebpackPlugin({workers: 2, async: false, vue: true, tslint: path.join(__dirname, '../tslint.json')}),
|
||||
new VueLoaderPlugin()
|
||||
],
|
||||
resolve: {
|
||||
'extensions': ['.ts', '.js', '.vue', '.scss']
|
||||
|
|
Loading…
Reference in New Issue