Stable release!
This commit is contained in:
parent
79d1ee4f48
commit
959cac855a
|
@ -21,7 +21,7 @@
|
|||
<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 ref="sizer"></div>
|
||||
<textarea ref="sizer"></textarea>
|
||||
<div class="bbcode-preview" v-show="preview">
|
||||
<div class="bbcode-preview-warnings">
|
||||
<div class="alert alert-danger" v-show="previewWarnings.length">
|
||||
|
@ -66,7 +66,7 @@
|
|||
previewResult = '';
|
||||
text = this.value !== undefined ? this.value : '';
|
||||
element!: HTMLTextAreaElement;
|
||||
sizer!: HTMLElement;
|
||||
sizer!: HTMLTextAreaElement;
|
||||
maxHeight!: number;
|
||||
minHeight!: number;
|
||||
showToolbar = false;
|
||||
|
@ -99,7 +99,7 @@
|
|||
this.undoStack.unshift(this.text);
|
||||
}
|
||||
}, 500);
|
||||
this.sizer = <HTMLElement>this.$refs['sizer'];
|
||||
this.sizer = <HTMLTextAreaElement>this.$refs['sizer'];
|
||||
this.sizer.style.cssText = styles.cssText;
|
||||
this.sizer.style.height = '0';
|
||||
this.sizer.style.overflow = 'hidden';
|
||||
|
@ -240,7 +240,7 @@
|
|||
this.sizer.style.fontSize = this.element.style.fontSize;
|
||||
this.sizer.style.lineHeight = this.element.style.lineHeight;
|
||||
this.sizer.style.width = `${this.element.offsetWidth}px`;
|
||||
this.sizer.textContent = this.element.value;
|
||||
this.sizer.value = this.element.value;
|
||||
this.element.style.height = `${Math.max(Math.min(this.sizer.scrollHeight, this.maxHeight), this.minHeight)}px`;
|
||||
this.sizer.style.width = '0';
|
||||
}
|
||||
|
|
|
@ -146,8 +146,10 @@
|
|||
document.title = (hasNew ? '💬 ' : '') + l(core.connection.isOpen ? 'title.connected' : 'title', core.connection.character);
|
||||
});
|
||||
core.connection.onError((e) => {
|
||||
this.error = errorToString(e);
|
||||
this.connecting = false;
|
||||
if((<Error & {request?: object}>e).request !== undefined) {//catch axios network errors
|
||||
this.error = l('login.connectError', errorToString(e));
|
||||
this.connecting = false;
|
||||
} else throw e;
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -158,12 +160,7 @@
|
|||
|
||||
connect(): void {
|
||||
this.connecting = true;
|
||||
core.connection.connect(this.selectedCharacter).catch((e: Error) => {
|
||||
if((<Error & {request?: object}>e).request !== undefined) {//catch axios network errors
|
||||
this.error = l('login.connectError', e.message);
|
||||
this.connecting = false;
|
||||
} else throw e;
|
||||
});
|
||||
core.connection.connect(this.selectedCharacter);
|
||||
}
|
||||
}
|
||||
</script>
|
|
@ -158,17 +158,17 @@
|
|||
clearTimeout(idleTimer);
|
||||
idleTimer = undefined;
|
||||
}
|
||||
window.setTimeout(() => {
|
||||
if(idleStatus !== undefined) {
|
||||
core.connection.send('STA', idleStatus);
|
||||
idleStatus = undefined;
|
||||
}
|
||||
}, Math.max(lastUpdate + 5 /*core.connection.vars.sta_flood*/ * 1000 + 1000 - Date.now(), 0));
|
||||
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));
|
||||
idleStatus = undefined;
|
||||
}
|
||||
});
|
||||
window.addEventListener('blur', () => {
|
||||
core.notifications.isInBackground = true;
|
||||
if(idleTimer !== undefined) clearTimeout(idleTimer);
|
||||
if(core.state.settings.idleTimer !== 0)
|
||||
if(core.state.settings.idleTimer > 0)
|
||||
idleTimer = window.setTimeout(() => {
|
||||
lastUpdate = Date.now();
|
||||
idleStatus = {status: ownCharacter.status, statusmsg: ownCharacter.statusText};
|
||||
|
|
|
@ -15,7 +15,12 @@
|
|||
<div v-if="command.permission"><i>{{command.permission}}</i></div>
|
||||
</div>
|
||||
</div>
|
||||
<input class="form-control" v-model="filter" :placeholder="l('filter')"/>
|
||||
<div class="input-group" style="padding:10px 0;flex-shrink:0">
|
||||
<div class="input-group-prepend">
|
||||
<div class="input-group-text"><span class="fas fa-search"></span></div>
|
||||
</div>
|
||||
<input class="form-control" v-model="filter" :placeholder="l('filter')"/>
|
||||
</div>
|
||||
</modal>
|
||||
</template>
|
||||
|
||||
|
|
|
@ -48,7 +48,7 @@
|
|||
</ul>
|
||||
</div>
|
||||
<div style="z-index:5;position:absolute;left:0;right:0;max-height:60%;overflow:auto"
|
||||
:style="'display:' + (descriptionExpanded ? 'block' : 'none')" class="bg-solid-text">
|
||||
:style="'display:' + (descriptionExpanded ? 'block' : 'none')" class="bg-solid-text border-bottom">
|
||||
<bbcode :text="conversation.channel.description"></bbcode>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -64,7 +64,7 @@
|
|||
</div>
|
||||
<input v-model="searchInput" @keydown.esc="showSearch = false; searchInput = ''" @keypress="lastSearchInput = Date.now()"
|
||||
:placeholder="l('chat.search')" ref="searchField" class="form-control"/>
|
||||
<a class="btn btn-sm btn-light" style="position:absolute;right:5px;top:50%;transform:translateY(-50%);line-height:0"
|
||||
<a class="btn btn-sm btn-light" style="position:absolute;right:5px;top:50%;transform:translateY(-50%);line-height:0;z-index:10"
|
||||
@click="showSearch = false"><i class="fas fa-times"></i></a>
|
||||
</div>
|
||||
<div class="border-top messages" :class="'messages-' + conversation.mode" style="flex:1;overflow:auto;margin-top:2px"
|
||||
|
@ -258,6 +258,7 @@
|
|||
async onKeyDown(e: KeyboardEvent): Promise<void> {
|
||||
const editor = <Editor>this.$refs['textBox'];
|
||||
if(getKey(e) === Keys.Tab) {
|
||||
if(e.shiftKey || e.altKey || e.ctrlKey || e.metaKey) return;
|
||||
e.preventDefault();
|
||||
if(this.conversation.enteredText.length === 0 || this.isConsoleTab) return;
|
||||
if(this.tabOptions === undefined) {
|
||||
|
|
|
@ -1,7 +1,12 @@
|
|||
<template>
|
||||
<modal :buttons="false" ref="dialog" id="logs-dialog" :action="l('logs.title')"
|
||||
dialogClass="modal-lg w-100 modal-dialog-centered" @open="onOpen" @close="onClose">
|
||||
<div class="form-group row" style="flex-shrink:0">
|
||||
<modal :buttons="false" ref="dialog" @open="onOpen" @close="onClose" style="width:98%" dialogClass="logs-dialog">
|
||||
<template slot="title">
|
||||
{{l('logs.title')}}
|
||||
<div class="logs-fab btn btn-secondary" slot="title" @click="showFilters = !showFilters">
|
||||
<span class="fas" :class="'fa-chevron-' + (showFilters ? 'up' : 'down')"></span>
|
||||
</div>
|
||||
</template>
|
||||
<div class="form-group row" style="flex-shrink:0" v-show="showFilters">
|
||||
<label for="character" class="col-sm-2 col-form-label">{{l('logs.character')}}</label>
|
||||
<div class="col-sm-10">
|
||||
<select class="form-control" v-model="selectedCharacter" id="character" @change="loadCharacter">
|
||||
|
@ -10,7 +15,7 @@
|
|||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row" style="flex-shrink:0">
|
||||
<div class="form-group row" style="flex-shrink:0" v-show="showFilters">
|
||||
<label class="col-sm-2 col-form-label">{{l('logs.conversation')}}</label>
|
||||
<div class="col-sm-10">
|
||||
<filterable-select v-model="selectedConversation" :options="conversations" :filterFunc="filterConversation"
|
||||
|
@ -20,7 +25,7 @@
|
|||
</filterable-select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row" style="flex-shrink:0">
|
||||
<div class="form-group row" style="flex-shrink:0" v-show="showFilters">
|
||||
<label for="date" class="col-sm-2 col-form-label">{{l('logs.date')}}</label>
|
||||
<div class="col-sm-8 col-10">
|
||||
<select class="form-control" v-model="selectedDate" id="date" @change="loadMessages">
|
||||
|
@ -33,7 +38,7 @@
|
|||
class="fa fa-download"></span></button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="messages-both" style="overflow: auto" ref="messages">
|
||||
<div class="messages-both" style="overflow: auto" ref="messages" tabindex="-1">
|
||||
<message-view v-for="message in filteredMessages" :message="message" :key="message.id"></message-view>
|
||||
</div>
|
||||
<div class="input-group" style="flex-shrink:0">
|
||||
|
@ -85,6 +90,7 @@
|
|||
keyDownListener?: (e: KeyboardEvent) => void;
|
||||
characters: ReadonlyArray<string> = [];
|
||||
selectedCharacter = core.connection.character;
|
||||
showFilters = true;
|
||||
|
||||
get filteredMessages(): ReadonlyArray<Conversation.Message> {
|
||||
if(this.filter.length === 0) return this.messages;
|
||||
|
@ -180,7 +186,12 @@
|
|||
</script>
|
||||
|
||||
<style>
|
||||
#logs-dialog .modal-body {
|
||||
.logs-dialog {
|
||||
max-width: 98% !important;
|
||||
width: 98% !important;
|
||||
}
|
||||
|
||||
.logs-dialog .modal-body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
<modal :action="l('chat.setStatus')" @submit="setStatus" @close="reset" dialogClass="w-100 modal-lg">
|
||||
<div class="form-group" id="statusSelector">
|
||||
<label class="control-label">{{l('chat.setStatus.status')}}</label>
|
||||
<dropdown class="dropdown form-control" style="padding:0">
|
||||
<dropdown>
|
||||
<span slot="title"><span class="fa fa-fw" :class="getStatusIcon(status)"></span>{{l('status.' + status)}}</span>
|
||||
<a href="#" class="dropdown-item" v-for="item in statuses" @click.prevent="status = item">
|
||||
<span class="fa fa-fw" :class="getStatusIcon(item)"></span>{{l('status.' + item)}}
|
||||
|
|
|
@ -4,11 +4,11 @@
|
|||
<div class="users" style="padding-left:10px" v-show="tab == 0">
|
||||
<h4>{{l('users.friends')}}</h4>
|
||||
<div v-for="character in friends" :key="character.name">
|
||||
<user :character="character" :showStatus="true"></user>
|
||||
<user :character="character" :showStatus="true" :bookmark="false"></user>
|
||||
</div>
|
||||
<h4>{{l('users.bookmarks')}}</h4>
|
||||
<div v-for="character in bookmarks" :key="character.name">
|
||||
<user :character="character" :showStatus="true"></user>
|
||||
<user :character="character" :showStatus="true" :bookmark="false"></user>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="channel" style="padding-left:5px;flex:1;display:flex;flex-direction:column" v-show="tab == 1">
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import {WebSocketConnection} from '../fchat';
|
||||
import l from './localize';
|
||||
|
||||
export default class Socket implements WebSocketConnection {
|
||||
static host = 'wss://chat.f-list.net:9799';
|
||||
|
@ -31,7 +30,7 @@ export default class Socket implements WebSocketConnection {
|
|||
|
||||
onError(handler: (error: Error) => void): void {
|
||||
this.errorHandler = handler;
|
||||
this.socket.addEventListener('error', () => handler(new Error(l('login.connectError'))));
|
||||
this.socket.addEventListener('error', () => handler(new Error()));
|
||||
}
|
||||
|
||||
send(message: string): void {
|
||||
|
|
|
@ -6,12 +6,13 @@ const codecs: {[key: string]: string} = {mpeg: 'mp3', wav: 'wav', ogg: 'ogg'};
|
|||
export default class Notifications implements Interface {
|
||||
isInBackground = false;
|
||||
|
||||
protected shouldNotify(conversation: Conversation): boolean {
|
||||
return core.characters.ownCharacter.status !== 'dnd' && (this.isInBackground ||
|
||||
conversation !== core.conversations.selectedConversation || core.state.settings.alwaysNotify);
|
||||
}
|
||||
|
||||
notify(conversation: Conversation, title: string, body: string, icon: string, sound: string): void {
|
||||
if(core.characters.ownCharacter.status === 'dnd') return;
|
||||
if(!this.isInBackground && conversation === core.conversations.selectedConversation) {
|
||||
if(core.state.settings.alwaysNotify) this.playSound(sound);
|
||||
return;
|
||||
}
|
||||
if(!this.shouldNotify(conversation)) return;
|
||||
this.playSound(sound);
|
||||
if(core.state.settings.notifications) {
|
||||
/*tslint:disable-next-line:no-object-literal-type-assertion*///false positive
|
||||
|
|
|
@ -32,7 +32,7 @@ export function getStatusIcon(status: Character.Status): string {
|
|||
const UserView = Vue.extend({
|
||||
functional: true,
|
||||
render(this: void | Vue, createElement: CreateElement, context?: RenderContext): VNode {
|
||||
const props = <{character: Character, channel?: Channel, showStatus?: true}>(
|
||||
const props = <{character: Character, channel?: Channel, showStatus?: true, bookmark?: false}>(
|
||||
context !== undefined ? context.props : (<Vue>this).$options.propsData);
|
||||
const character = props.character;
|
||||
let rankIcon;
|
||||
|
@ -46,7 +46,8 @@ const UserView = Vue.extend({
|
|||
if(props.showStatus !== undefined || character.status === 'crown')
|
||||
children.unshift(createElement('span', {staticClass: `fa-fw ${getStatusIcon(character.status)}`}));
|
||||
const gender = character.gender !== undefined ? character.gender.toLowerCase() : 'none';
|
||||
const isBookmark = core.connection.isOpen && core.state.settings.colorBookmarks && (character.isFriend || character.isBookmarked);
|
||||
const isBookmark = props.bookmark !== false && core.connection.isOpen && core.state.settings.colorBookmarks &&
|
||||
(character.isFriend || character.isBookmarked);
|
||||
return createElement('span', {
|
||||
attrs: {class: `user-view gender-${gender}${isBookmark ? ' user-bookmark' : ''}`},
|
||||
domProps: {character, channel: props.channel, bbcodeTag: 'user'}
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
<template>
|
||||
<div class="dropdown">
|
||||
<a class="btn btn-secondary dropdown-toggle" aria-haspopup="true" :aria-expanded="isOpen" @click="isOpen = true"
|
||||
<button 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>
|
||||
</a>
|
||||
</button>
|
||||
<div class="dropdown-menu" :style="open ? 'display:block' : ''" @mousedown.stop.prevent @click="isOpen = false"
|
||||
ref="menu">
|
||||
<slot></slot>
|
||||
|
|
|
@ -60,7 +60,10 @@
|
|||
const index = selected.indexOf(item);
|
||||
if(index === -1) selected.push(item);
|
||||
else selected.splice(index, 1);
|
||||
} else this.selected = item;
|
||||
} else {
|
||||
this.keepOpen = false;
|
||||
this.selected = item;
|
||||
}
|
||||
this.$emit('input', this.selected);
|
||||
}
|
||||
|
||||
|
|
|
@ -176,6 +176,7 @@
|
|||
alert(l('login.alreadyLoggedIn'));
|
||||
return core.connection.close();
|
||||
}
|
||||
parent.send('connect', webContents.id, core.connection.character);
|
||||
this.character = connection.character;
|
||||
if((await core.settingsStore.get('settings')) === undefined &&
|
||||
SlimcatImporter.canImportCharacter(core.connection.character)) {
|
||||
|
@ -187,7 +188,6 @@
|
|||
});
|
||||
connection.onEvent('connected', () => {
|
||||
core.watch(() => core.conversations.hasNew, (newValue) => parent.send('has-new', webContents.id, newValue));
|
||||
parent.send('connect', webContents.id, core.connection.character);
|
||||
Raven.setUserContext({username: core.connection.character});
|
||||
});
|
||||
connection.onEvent('closed', () => {
|
||||
|
|
|
@ -225,6 +225,7 @@
|
|||
electron.ipcRenderer.send('has-new', this.tabs.reduce((cur, t) => cur || t.hasNew, false));
|
||||
delete this.tabMap[tab.view.webContents.id];
|
||||
if(this.tabs.length === 0) {
|
||||
browserWindow.setBrowserView(null!);
|
||||
if(process.env.NODE_ENV === 'production') browserWindow.close();
|
||||
} else if(this.activeTab === tab) this.show(this.tabs[0]);
|
||||
destroyTab(tab);
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "fchat",
|
||||
"version": "0.2.19",
|
||||
"version": "3.0.0",
|
||||
"author": "The F-List Team",
|
||||
"description": "F-List.net Chat Client",
|
||||
"main": "main.js",
|
||||
|
|
|
@ -214,9 +214,10 @@ export class Logs implements Logging {
|
|||
const entry = this.getIndex(character)[key];
|
||||
if(entry === undefined) return [];
|
||||
const dates = [];
|
||||
const offset = new Date().getTimezoneOffset() * 60000;
|
||||
for(const item in entry.index)
|
||||
dates.push(new Date(parseInt(item, 10) * dayMs + offset));
|
||||
for(const item in entry.index) {
|
||||
const date = new Date(parseInt(item, 10) * dayMs);
|
||||
dates.push(new Date(date.getTime() + date.getTimezoneOffset() * 60000));
|
||||
}
|
||||
return dates;
|
||||
}
|
||||
|
||||
|
|
|
@ -8,7 +8,7 @@ const browserWindow = remote.getCurrentWindow();
|
|||
|
||||
export default class Notifications extends BaseNotifications {
|
||||
notify(conversation: Conversation, title: string, body: string, icon: string, sound: string): void {
|
||||
if(!this.isInBackground && conversation === core.conversations.selectedConversation && !core.state.settings.alwaysNotify) return;
|
||||
if(!this.shouldNotify(conversation)) return;
|
||||
this.playSound(sound);
|
||||
browserWindow.flashFrame(true);
|
||||
if(core.state.settings.notifications) {
|
||||
|
|
|
@ -36,17 +36,26 @@ export default class Connection implements Interfaces.Connection {
|
|||
try {
|
||||
this.ticket = await this.ticketProvider();
|
||||
} catch(e) {
|
||||
(<Error & {request: true}>e).request = true;
|
||||
throw e;
|
||||
return this.invokeErrorHandlers(<Error>e, true);
|
||||
}
|
||||
try {
|
||||
await this.invokeHandlers('connecting', this.isReconnect);
|
||||
} catch(e) {
|
||||
await this.invokeHandlers('closed', false);
|
||||
return this.invokeErrorHandlers(<Error>e);
|
||||
}
|
||||
await this.invokeHandlers('connecting', this.isReconnect);
|
||||
if(this.cleanClose) {
|
||||
this.cleanClose = false;
|
||||
await this.invokeHandlers('closed', false);
|
||||
return;
|
||||
}
|
||||
const socket = this.socket = new this.socketProvider();
|
||||
socket.onOpen(() => {
|
||||
try {
|
||||
this.socket = new this.socketProvider();
|
||||
} catch(e) {
|
||||
await this.invokeHandlers('closed', false);
|
||||
return this.invokeErrorHandlers(<Error>e, true);
|
||||
}
|
||||
this.socket.onOpen(() => {
|
||||
this.send('IDN', {
|
||||
account: this.account,
|
||||
character: this.character,
|
||||
|
@ -56,39 +65,21 @@ export default class Connection implements Interfaces.Connection {
|
|||
ticket: this.ticket
|
||||
});
|
||||
});
|
||||
socket.onMessage(async(msg: string) => {
|
||||
this.socket.onMessage(async(msg: string) => {
|
||||
const type = <keyof Interfaces.ServerCommands>msg.substr(0, 3);
|
||||
const data = msg.length > 6 ? <object>JSON.parse(msg.substr(4)) : undefined;
|
||||
return this.handleMessage(type, data);
|
||||
});
|
||||
socket.onClose(async() => {
|
||||
this.socket.onClose(async() => {
|
||||
if(!this.cleanClose) this.reconnect();
|
||||
this.socket = undefined;
|
||||
await this.invokeHandlers('closed', !this.cleanClose);
|
||||
});
|
||||
socket.onError((error: Error) => {
|
||||
for(const handler of this.errorHandlers) handler(error);
|
||||
});
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
const handler = () => {
|
||||
resolve();
|
||||
this.offEvent('connected', handler);
|
||||
};
|
||||
this.onEvent('connected', handler);
|
||||
this.onError(reject);
|
||||
});
|
||||
this.socket.onError((error: Error) => this.invokeErrorHandlers(error, true));
|
||||
}
|
||||
|
||||
private reconnect(): void {
|
||||
this.reconnectTimer = setTimeout(async() => {
|
||||
try {
|
||||
await this.connect(this.character);
|
||||
} catch(e) {
|
||||
for(const handler of this.errorHandlers) handler(<Error>e);
|
||||
await this.invokeHandlers('closed', true);
|
||||
this.reconnect();
|
||||
}
|
||||
}, this.reconnectDelay);
|
||||
this.reconnectTimer = setTimeout(async() => this.connect(this.character), this.reconnectDelay);
|
||||
this.reconnectDelay = this.reconnectDelay >= 30000 ? 60000 : this.reconnectDelay >= 10000 ? 30000 : 10000;
|
||||
}
|
||||
|
||||
|
@ -167,8 +158,7 @@ export default class Connection implements Interfaces.Connection {
|
|||
break;
|
||||
case 'ERR':
|
||||
if(fatalErrors.indexOf(data.number) !== -1) {
|
||||
const error = new Error(data.message);
|
||||
for(const handler of this.errorHandlers) handler(error);
|
||||
this.invokeErrorHandlers(new Error(data.message), true);
|
||||
if(dieErrors.indexOf(data.number) !== -1) {
|
||||
this.close();
|
||||
this.character = '';
|
||||
|
@ -198,4 +188,9 @@ export default class Connection implements Interfaces.Connection {
|
|||
if(handlers === undefined) return;
|
||||
for(const handler of handlers) await handler(isReconnect);
|
||||
}
|
||||
|
||||
private invokeErrorHandlers(error: Error, request: boolean = false): void {
|
||||
if(request) (<Error & {request: true}>error).request = true;
|
||||
for(const handler of this.errorHandlers) handler(error);
|
||||
}
|
||||
}
|
|
@ -135,7 +135,7 @@ export namespace Connection {
|
|||
readonly character: string
|
||||
readonly vars: Vars
|
||||
readonly isOpen: boolean
|
||||
connect(character: string): Promise<void>
|
||||
connect(character: string): void
|
||||
close(): void
|
||||
onMessage<K extends keyof ServerCommands>(type: K, handler: CommandHandler<K>): void
|
||||
offMessage<K extends keyof ServerCommands>(type: K, handler: CommandHandler<K>): void
|
||||
|
|
|
@ -115,7 +115,6 @@
|
|||
let settings = await getGeneralSettings();
|
||||
if(settings === undefined) settings = new GeneralSettings();
|
||||
if(settings.version !== appVersion) {
|
||||
alert('Your beta version of F-Chat 3.0 has been updated. If you are experiencing any issues after this update, please perform a full reinstall of the application. If the issue persists, please report it.');
|
||||
settings.version = appVersion;
|
||||
await setGeneralSettings(settings);
|
||||
}
|
||||
|
|
|
@ -8,8 +8,8 @@ android {
|
|||
applicationId "net.f_list.fchat"
|
||||
minSdkVersion 19
|
||||
targetSdkVersion 27
|
||||
versionCode 12
|
||||
versionName "0.1.5"
|
||||
versionCode 13
|
||||
versionName "3.0.0"
|
||||
}
|
||||
buildTypes {
|
||||
release {
|
||||
|
|
|
@ -77,8 +77,10 @@ export class Logs implements Logging {
|
|||
async getLogDates(character: string, key: string): Promise<ReadonlyArray<Date>> {
|
||||
const entry = (await this.getIndex(character))[key];
|
||||
if(entry === undefined) return [];
|
||||
const offset = new Date().getTimezoneOffset() * 60000;
|
||||
return entry.dates.map((x) => new Date(x * dayMs + offset));
|
||||
return entry.dates.map((x) => {
|
||||
const date = new Date(x * dayMs);
|
||||
return new Date(date.getTime() + date.getTimezoneOffset() * 60000);
|
||||
});
|
||||
}
|
||||
|
||||
async getConversations(character: string): Promise<ReadonlyArray<{key: string, name: string}>> {
|
||||
|
|
|
@ -16,8 +16,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 {
|
||||
if(!this.isInBackground && conversation === core.conversations.selectedConversation && !core.state.settings.alwaysNotify) return;
|
||||
if(core.characters.ownCharacter.status === 'dnd') return;
|
||||
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": "0.2.18",
|
||||
"version": "3.0.0",
|
||||
"displayName": "F-Chat",
|
||||
"author": "The F-List Team",
|
||||
"description": "F-List.net Chat Client",
|
||||
|
|
|
@ -268,4 +268,16 @@ $genders: (
|
|||
@media (max-width: breakpoint-max(xs)) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.logs-fab {
|
||||
position: absolute;
|
||||
top: 47px;
|
||||
z-index: 10;
|
||||
padding: 12px;
|
||||
left: 50%;
|
||||
margin-left: -20px;
|
||||
border-radius: 100%;
|
||||
line-height: 0;
|
||||
box-shadow: 0 1px 4px #000;
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
import {EventMessage, Message} from '../chat/common';
|
||||
import {EventMessage, Message, Settings as SettingsImpl} from '../chat/common';
|
||||
import core from '../chat/core';
|
||||
import {Conversation, Logs as Logging, Settings} from '../chat/interfaces';
|
||||
|
||||
|
@ -30,7 +30,7 @@ async function iterate<S, T>(request: IDBRequest, map: (stored: S) => T, count:
|
|||
const dayMs = 86400000;
|
||||
const charactersKey = 'fchat.characters';
|
||||
let hasComposite = true;
|
||||
let getComposite: (conv: number, day: number) => string | number[] = (conv, day) => [conv, day];
|
||||
let getComposite: (conv: number, day: number) => string | number[] = (conv, day) => [conv, day];
|
||||
const decode = (str: string) => (str.charCodeAt(0) << 16) + str.charCodeAt(1);
|
||||
try {
|
||||
IDBKeyRange.only([]);
|
||||
|
@ -133,10 +133,11 @@ export class Logs implements Logging {
|
|||
async getLogDates(character: string, key: string): Promise<ReadonlyArray<Date>> {
|
||||
const id = (await this.loadIndex(character))[key]!.id;
|
||||
const trans = this.loadedDb!.transaction(['logs']);
|
||||
const offset = new Date().getTimezoneOffset() * 60000;
|
||||
const bound = IDBKeyRange.bound(getComposite(id, 0), getComposite(id, 1000000));
|
||||
return iterate(trans.objectStore('logs').index('conversation-day').openCursor(bound, 'nextunique'), (value: StoredMessage) =>
|
||||
new Date((hasComposite ? <number>value.day : decode((<string>value.day).substr(2))) * dayMs + offset));
|
||||
return iterate(trans.objectStore('logs').index('conversation-day').openCursor(bound, 'nextunique'), (value: StoredMessage) => {
|
||||
const date = new Date((hasComposite ? <number>value.day : decode((<string>value.day).substr(2))) * dayMs);
|
||||
return new Date(date.getTime() + date.getTimezoneOffset() * 60000);
|
||||
});
|
||||
}
|
||||
|
||||
async getAvailableCharacters(): Promise<ReadonlyArray<string>> {
|
||||
|
@ -148,7 +149,47 @@ export class Logs implements Logging {
|
|||
export class SettingsStore implements Settings.Store {
|
||||
async get<K extends keyof Settings.Keys>(key: K): Promise<Settings.Keys[K] | undefined> {
|
||||
const stored = window.localStorage.getItem(`${core.connection.character}.settings.${key}`);
|
||||
return stored !== null ? JSON.parse(stored) as Settings.Keys[K] : undefined;
|
||||
if(stored === null) {
|
||||
if(key === 'pinned') {
|
||||
const tabs20 = window.localStorage.getItem(`tabs_${core.connection.character.toLowerCase()}`);
|
||||
if(tabs20 !== null)
|
||||
try {
|
||||
const tabs = JSON.parse(tabs20) as {type: string, id: string, title: string}[];
|
||||
const pinned: Settings.Keys['pinned'] = {channels: [], private: []};
|
||||
pinned.channels = tabs.filter((x) => x.type === 'channel').map((x) => x.id.toLowerCase());
|
||||
pinned.private = tabs.filter((x) => x.type === 'user').map((x) => x.title);
|
||||
return pinned;
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
} else if(key === 'settings') {
|
||||
const settings20 = window.localStorage.getItem(`chat_settings`);
|
||||
if(settings20 !== null)
|
||||
try {
|
||||
const old = JSON.parse(settings20) as {
|
||||
animatedIcons: boolean, autoIdle: boolean, autoIdleTime: number, delimit: boolean, disableIconTag: boolean,
|
||||
enableLogging: boolean, highlightMentions: boolean, highlightWords: string[],
|
||||
html5Audio: boolean, joinLeaveAlerts: boolean, leftClickOpensFlist: boolean
|
||||
};
|
||||
const settings = new SettingsImpl();
|
||||
settings.animatedEicons = old.animatedIcons;
|
||||
if(old.autoIdle) settings.idleTimer = old.autoIdleTime / 60000;
|
||||
settings.messageSeparators = old.delimit;
|
||||
if(old.disableIconTag) settings.disallowedTags.push('icon');
|
||||
settings.logMessages = old.enableLogging;
|
||||
settings.highlight = old.highlightMentions;
|
||||
settings.highlightWords = old.highlightWords;
|
||||
settings.playSound = old.html5Audio;
|
||||
settings.joinMessages = old.joinLeaveAlerts;
|
||||
settings.clickOpensMessage = !old.leftClickOpensFlist;
|
||||
return settings;
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
return JSON.parse(stored) as Settings.Keys[K];
|
||||
}
|
||||
|
||||
async set<K extends keyof Settings.Keys>(key: K, value: Settings.Keys[K]): Promise<void> {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "net.f_list.fchat",
|
||||
"version": "0.2.19",
|
||||
"version": "3.0.0",
|
||||
"displayName": "F-Chat",
|
||||
"author": "The F-List Team",
|
||||
"description": "F-List.net Chat Client",
|
||||
|
|
Loading…
Reference in New Issue