0.2.1 - Open beta

This commit is contained in:
MayaWolf 2017-10-17 01:58:57 +02:00
parent 878389f717
commit 51d8ba1680
29 changed files with 313 additions and 122 deletions

View File

@ -12,10 +12,15 @@
</filterable-select>
<div v-show="!data.kinks.length" class="alert alert-warning">{{l('characterSearch.kinkNotice')}}</div>
</div>
<div v-else-if="results">
<h5>{{l('characterSearch.results')}}</h5>
<div v-for="character in results">
<user :character="character"></user>
<div v-else-if="results" class="results">
<h4>{{l('characterSearch.results')}}</h4>
<div v-for="character in results" :key="character.name" :class="'status-' + character.status">
<template v-if="character.status === 'looking'" v-once>
<img :src="characterImage(character.name)" v-if="showAvatars"/>
<user :character="character" :showStatus="true"></user>
<bbcode :text="character.statusText"></bbcode>
</template>
<user v-else :character="character" :showStatus="true" v-once></user>
</div>
</div>
</modal>
@ -27,6 +32,8 @@
import CustomDialog from '../components/custom_dialog';
import FilterableSelect from '../components/FilterableSelect.vue';
import Modal from '../components/Modal.vue';
import {BBCodeView} from './bbcode';
import {characterImage} from './common';
import core from './core';
import {Character, Connection} from './interfaces';
import l from './localize';
@ -41,8 +48,16 @@
type Kink = {id: number, name: string, description: string};
function sort(x: Character, y: Character): number {
if(x.status === 'looking' && y.status !== 'looking') return -1;
if(x.status !== 'looking' && y.status === 'looking') return 1;
if(x.name < y.name) return -1;
if(x.name > y.name) return 1;
return 0;
}
@Component({
components: {modal: Modal, user: UserView, 'filterable-select': FilterableSelect}
components: {modal: Modal, user: UserView, 'filterable-select': FilterableSelect, bbcode: BBCodeView}
})
export default class CharacterSearch extends CustomDialog {
//tslint:disable:no-null-keyword
@ -50,6 +65,7 @@
kinksFilter = '';
error = '';
results: Character[] | null = null;
characterImage = characterImage;
options: {
kinks: Kink[]
genders: string[]
@ -82,7 +98,6 @@
roles: options.listitems.filter((x) => x.name === 'subdom').map((x) => x.value),
positions: options.listitems.filter((x) => x.name === 'position').map((x) => x.value)
};
this.$nextTick(() => (<Modal>this.$children[0]).fixDropdowns());
}
mounted(): void {
@ -98,7 +113,8 @@
this.error = l('characterSearch.error.tooManyResults');
}
});
core.connection.onMessage('FKS', (data) => this.results = data.characters.map((x: string) => core.characters.get(x)));
core.connection.onMessage('FKS', (data) => this.results = data.characters.map((x: string) => core.characters.get(x)).sort(sort));
(<Modal>this.$children[0]).fixDropdowns();
}
filterKink(filter: RegExp, kink: Kink): boolean {
@ -107,6 +123,10 @@
return filter.test(kink.name);
}
get showAvatars(): boolean {
return core.state.settings.showAvatars;
}
submit(): void {
if(this.results !== null) {
this.results = null;
@ -122,8 +142,25 @@
}
</script>
<style>
.character-search .dropdown {
margin-bottom: 10px;
<style lang="less">
.character-search {
.dropdown {
margin-bottom: 10px;
}
.results {
.user-view {
display: block;
}
& > .status-looking {
margin-bottom: 5px;
min-height: 50px;
}
img {
float: left;
margin-right: 5px;
width: 50px;
}
}
}
</style>

View File

@ -84,7 +84,12 @@
connect(): void {
this.connecting = true;
core.connection.connect(this.selectedCharacter);
try {
core.connection.connect(this.selectedCharacter);
} catch(e) {
if(e.request !== undefined) this.error = l('login.connectError'); //catch axios network errors
else throw e;
}
}
}
</script>

View File

@ -169,6 +169,12 @@
core.connection.send('STA', {status: 'idle', statusmsg: ownCharacter.statusText});
}, core.state.settings.idleTimer * 60000);
};
core.connection.onEvent('closed', () => {
if(idleTimer !== undefined) {
window.clearTimeout(idleTimer);
idleTimer = undefined;
}
});
}
logOut(): void {

View File

@ -42,7 +42,7 @@
</li>
</ul>
</div>
<div style="z-index:5; position:absolute; left:0; right:32px; max-height:60%; overflow:auto;"
<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">
<bbcode :text="conversation.channel.description"></bbcode>
</div>
@ -53,21 +53,18 @@
</div>
<div class="border-top messages" :class="'messages-' + conversation.mode" style="flex:1;overflow:auto;margin-top:2px"
ref="messages" @scroll="onMessagesScroll">
<template v-if="!isConsoleTab">
<message-view v-for="message in conversation.messages" :message="message" :channel="conversation.channel"
:classes="message == conversation.lastRead ? 'last-read' : ''" :key="message.id">
<template v-for="message in conversation.messages">
<message-view :message="message" :channel="conversation.channel" :key="message.id"
:classes="message == conversation.lastRead ? 'last-read' : ''">
</message-view>
</template>
<template v-else>
<div v-for="message in conversation.messages" :key="message.id">
<message-view :message="message"></message-view>
<span v-if="message.sfc && message.sfc.action == 'report'">
<a :href="'https://www.f-list.net/fchat/getLog.php?log=' + message.sfc.logid">{{l('events.report.viewLog')}}</a>
<span v-show="!message.sfc.confirmed">
| <a href="#" @click.prevent="acceptReport(message.sfc)">{{l('events.report.confirm')}}</a>
</span>
<span v-if="message.sfc && message.sfc.action == 'report'" :key="message.id">
<a :href="'https://www.f-list.net/fchat/getLog.php?log=' + message.sfc.logid"
v-if="message.sfc.logid">{{l('events.report.viewLog')}}</a>
<span v-else>{{l('events.report.noLog')}}</span>
<span v-show="!message.sfc.confirmed">
| <a href="#" @click.prevent="acceptReport(message.sfc)">{{l('events.report.confirm')}}</a>
</span>
</div>
</span>
</template>
</div>
<div>
@ -185,8 +182,8 @@
}
onMessagesScroll(): void {
const messageView = <HTMLElement>this.$refs['messages'];
if(messageView.scrollTop < 50) this.conversation.loadMore();
const messageView = <HTMLElement | undefined>this.$refs['messages'];
if(messageView !== undefined && messageView.scrollTop < 50) this.conversation.loadMore();
}
@Watch('conversation.errorText')

View File

@ -69,7 +69,7 @@
character: Character | null = null;
position = {left: '', top: ''};
characterImage: string | null = null;
touchTimer: number;
touchTimer: number | undefined;
channel: Channel | null = null;
memo = '';
memoId: number;
@ -145,8 +145,7 @@
}
handleEvent(e: MouseEvent | TouchEvent): void {
if(e.type === 'touchend') return clearTimeout(this.touchTimer);
const touch = e instanceof TouchEvent ? e.touches[0] : e;
const touch = e instanceof TouchEvent ? e.changedTouches[0] : e;
let node = <Node & {character?: Character, channel?: Channel}>touch.target;
while(node !== document.body) {
if(node.character !== undefined || node.parentNode === null) break;
@ -158,13 +157,20 @@
}
switch(e.type) {
case 'click':
this.character = node.character;
if(core.state.settings.clickOpensMessage) this.openConversation(true);
else window.open(this.profileLink);
this.showContextMenu = false;
this.onClick(node.character);
break;
case 'touchstart':
this.touchTimer = window.setTimeout(() => this.openMenu(touch, node.character!, node.channel), 500);
this.touchTimer = window.setTimeout(() => {
this.openMenu(touch, node.character!, node.channel);
this.touchTimer = undefined;
}, 500);
break;
case 'touchend':
if(this.touchTimer !== undefined) {
clearTimeout(this.touchTimer);
this.touchTimer = undefined;
this.onClick(node.character);
}
break;
case 'contextmenu':
this.openMenu(touch, node.character, node.channel);
@ -172,6 +178,13 @@
e.preventDefault();
}
private onClick(character: Character): void {
this.character = character;
if(core.state.settings.clickOpensMessage) this.openConversation(true);
else window.open(this.profileLink);
this.showContextMenu = false;
}
private openMenu(touch: MouseEvent | Touch, character: Character, channel: Channel | undefined): void {
this.channel = channel !== undefined ? channel : null;
this.character = character;

View File

@ -73,7 +73,7 @@ export function errorToString(e: any): string {
//tslint:enable
export async function requestNotificationsPermission(): Promise<void> {
if(<object | undefined>Notification !== undefined) await Notification.requestPermission();
if((<Window & {Notification: Notification | undefined}>window).Notification !== undefined) await Notification.requestPermission();
}
let messageId = 0;
@ -84,6 +84,7 @@ export class Message implements Conversation.ChatMessage {
constructor(readonly type: Conversation.Message.Type, readonly sender: Character, readonly text: string,
readonly time: Date = new Date()) {
if(Conversation.Message.Type[type] === undefined) throw new Error('Unknown type'); /*tslint:disable-line*/ //TODO debug code
}
}

View File

@ -7,6 +7,7 @@ import {Channel, Character, Connection, Conversation as Interfaces} from './inte
import l from './localize';
import {CommandContext, isCommand, parse as parseCommand} from './slash_commands';
import MessageType = Interfaces.Message.Type;
function createMessage(this: void, type: MessageType, sender: Character, text: string, time?: Date): Message {
if(type === MessageType.Message && text.match(/^\/me\b/) !== null) {
type = MessageType.Action;
@ -179,7 +180,7 @@ class PrivateConversation extends Conversation implements Interfaces.PrivateConv
core.connection.send('PRI', {recipient: this.name, message: this.enteredText});
const message = createMessage(MessageType.Message, core.characters.ownCharacter, this.enteredText);
this.safeAddMessage(message);
core.logs.logMessage(this, message);
if(core.state.settings.logMessages) this.logPromise.then(() => core.logs.logMessage(this, message));
this.enteredText = '';
}
@ -205,7 +206,7 @@ class ChannelConversation extends Conversation implements Interfaces.ChannelConv
this.chat.unshift(...this.both.filter((x) => x.type !== MessageType.Ad));
this.ads.unshift(...this.both.filter((x) => x.type === MessageType.Ad));
this.lastRead = this.messages[this.messages.length - 1];
this.mode = this.channel.mode;
this.messages = this.allMessages.slice(-this.maxMessages);
});
constructor(readonly channel: Channel) {
@ -218,6 +219,7 @@ class ChannelConversation extends Conversation implements Interfaces.ChannelConv
this.mode = value;
if(value !== 'both') this.isSendingAds = value === 'ads';
});
this.mode = this.channel.mode;
}
get maxMessageLength(): number {
@ -552,7 +554,8 @@ export default function(this: void): Interfaces.State {
});
connection.onMessage('HLO', (data, time) => addEventMessage(new EventMessage(data.message, time)));
connection.onMessage('BRO', (data, time) => {
const text = l('events.broadcast', `[user]${data.character}[/user]`, decodeHTML(data.message.substr(data.character.length + 23)));
const text = data.character === undefined ? decodeHTML(data.message) :
l('events.broadcast', `[user]${data.character}[/user]`, decodeHTML(data.message.substr(data.character.length + 23)));
addEventMessage(new EventMessage(text, time));
});
connection.onMessage('CIU', (data, time) => {

View File

@ -12,6 +12,8 @@ const strings: {[key: string]: string | undefined} = {
'action.updateAvailable': 'UPDATE AVAILABLE',
'action.update': 'Restart now!',
'action.cancel': 'Cancel',
'consoleWarning.head': 'THIS IS THE DANGER ZONE.',
'consoleWarning.body': `ANYTHING YOU WRITE OR PASTE IN HERE COULD BE USED TO STEAL YOUR PASSWORDS OR TAKE OVER YOUR ENTIRE COMPUTER. This is where happiness goes to die. If you aren't a developer or a special kind of daredevil, please get out of here!`,
'help.fchat': 'FChat 3.0 Help and Changelog',
'help.rules': 'F-List Rules',
'help.faq': 'F-List FAQ',
@ -183,6 +185,7 @@ Are you sure?`,
'events.report.confirmed': '{0} is handling {1}\'s report.',
'events.report.confirm': 'Confirm report',
'events.report.viewLog': 'View log',
'events.report.noLog': 'No log available',
'events.status': '{0} is now {1}.',
'events.status.message': '{0} is now {1}: {2}',
'events.status.own': 'You are now {0}.',

View File

@ -106,5 +106,10 @@
display: flex;
text-align: left
}
input[type=checkbox] {
vertical-align: text-bottom;
margin-right: 5px;
}
}
</style>

View File

@ -51,10 +51,11 @@
import Vue from 'vue';
import Component from 'vue-class-component';
import Chat from '../chat/Chat.vue';
import Connection from '../chat/connection';
import core, {init as initCore} from '../chat/core';
import l from '../chat/localize';
import Socket from '../chat/WebSocket';
import Modal from '../components/Modal.vue';
import Connection from '../fchat/connection';
import {GeneralSettings, getGeneralSettings, Logs, setGeneralSettings, SettingsStore} from './filesystem';
import Notifications from './notifications';
@ -91,18 +92,23 @@
this.loggingIn = true;
try {
const data = <{ticket?: string, error: string, characters: string[], default_character: string}>
(await Axios.post('https://www.f-list.net/json/getApiTicket.php',
qs.stringify({account: this.settings!.account, password: this.settings!.password, no_friends: true, no_bookmarks: true})
)).data;
(await Axios.post('https://www.f-list.net/json/getApiTicket.php', qs.stringify(
{account: this.settings!.account, password: this.settings!.password, no_friends: true, no_bookmarks: true})
)).data;
if(data.error !== '') {
this.error = data.error;
return;
}
if(this.saveLogin)
await setGeneralSettings(this.settings!);
const connection = new Connection(this.settings!.host, this.settings!.account, this.getTicket.bind(this));
connection.onEvent('connected', () => Raven.setUserContext({username: core.connection.character}));
connection.onEvent('closed', () => Raven.setUserContext());
Socket.host = this.settings!.host;
const connection = new Connection(Socket, this.settings!.account, this.getTicket.bind(this));
connection.onEvent('connected', () => {
Raven.setUserContext({username: core.connection.character});
});
connection.onEvent('closed', () => {
Raven.setUserContext();
});
initCore(connection, Logs, SettingsStore, Notifications);
this.characters = data.characters.sort();
this.defaultCharacter = data.default_character;

View File

@ -38,7 +38,7 @@ import {init as fsInit} from './filesystem';
import Index from './Index.vue';
if(process.env.NODE_ENV === 'production') {
Raven.config('https://af3e6032460e418cb794b1799e536f37@sentry.newtsin.space/2', {
Raven.config('https://a9239b17b0a14f72ba85e8729b9d1612@sentry.f-list.net/2', {
release: `android-${require('./package.json').version}`, //tslint:disable-line:no-require-imports no-unsafe-any
dataCallback: (data: {culprit: string, exception: {values: {stacktrace: {frames: {filename: string}[]}}[]}}) => {
data.culprit = `~${data.culprit.substr(data.culprit.lastIndexOf('/'))}`;

View File

@ -1,6 +1,6 @@
{
"name": "fchat",
"version": "0.1.0",
"version": "0.2.0",
"author": "The F-List Team",
"description": "F-List.net Chat Client",
"main": "main.js",

View File

@ -67,7 +67,10 @@ module.exports = function(env) {
}));
if(dist) {
config.devtool = 'source-map';
config.plugins.push(new UglifyPlugin({sourceMap: true}));
config.plugins.push(
new UglifyPlugin({sourceMap: true}),
new webpack.LoaderOptionsPlugin({minimize: true})
);
}
return config;
};

View File

@ -88,10 +88,12 @@
{label: l('action.open'), click: () => mainWindow!.show()},
{
label: l('action.quit'),
role: 'quit',
click: () => {
isClosing = true;
mainWindow!.close();
mainWindow = undefined;
electron.remote.app.quit();
}
}
];
@ -100,7 +102,7 @@
let isClosing = false;
let mainWindow: Electron.BrowserWindow | undefined = electron.remote.getCurrentWindow(); //TODO
//tslint:disable-next-line:no-require-imports
const tray = new electron.remote.Tray(path.join(__dirname, <string>require('./build/icon.png')));
const tray = new electron.remote.Tray(path.join(__dirname, <string>require('./build/tray.png')));
tray.setToolTip(l('title'));
tray.on('click', (_) => mainWindow!.show());
tray.setContextMenu(trayMenu);
@ -199,13 +201,7 @@
},
{type: 'separator'},
{role: 'minimize'},
{
label: l('action.quit'),
click(): void {
isClosing = true;
mainWindow!.close();
}
}
{role: 'quit'}
];
electron.remote.Menu.setApplicationMenu(electron.remote.Menu.buildFromTemplate(appMenu));

View File

@ -1,6 +1,6 @@
{
"name": "fchat",
"version": "0.1.29",
"version": "0.2.1",
"author": "The F-List Team",
"description": "F-List.net Chat Client",
"main": "main.js",

BIN
electron/build/tray.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

View File

@ -34,11 +34,13 @@ import 'bootstrap/js/modal.js';
import * as electron from 'electron';
import * as Raven from 'raven-js';
import Vue from 'vue';
import {getKey} from '../chat/common';
import l from '../chat/localize';
import VueRaven from '../chat/vue-raven';
import Index from './Index.vue';
if(process.env.NODE_ENV === 'production') {
Raven.config('https://af3e6032460e418cb794b1799e536f37@sentry.newtsin.space/2', {
Raven.config('https://a9239b17b0a14f72ba85e8729b9d1612@sentry.f-list.net/2', {
release: electron.remote.app.getVersion(),
dataCallback(data: {culprit: string, exception: {values: {stacktrace: {frames: {filename: string}[]}}[]}}): void {
data.culprit = `~${data.culprit.substr(data.culprit.lastIndexOf('/'))}`;
@ -52,6 +54,15 @@ if(process.env.NODE_ENV === 'production') {
(<Window & {onunhandledrejection(e: PromiseRejectionEvent): void}>window).onunhandledrejection = (e: PromiseRejectionEvent) => {
Raven.captureException(<Error>e.reason);
};
document.addEventListener('keydown', (e: KeyboardEvent) => {
if(e.ctrlKey && e.shiftKey && getKey(e) === 'I')
electron.remote.getCurrentWebContents().toggleDevTools();
});
electron.remote.getCurrentWebContents().on('devtools-opened', () => {
console.log(`%c${l('consoleWarning.head')}`, 'background: red; color: yellow; font-size: 30pt');
console.log(`%c${l('consoleWarning.body')}`, 'font-size: 16pt; color:red');
});
}
//tslint:disable-next-line:no-unused-expression
@ -59,9 +70,4 @@ new Index({
el: '#app'
});
electron.ipcRenderer.on('focus', (_: Event, message: boolean) => message ? window.focus() : window.blur());
document.addEventListener('keydown', (e: KeyboardEvent) => {
if(e.which === 123)
electron.remote.getCurrentWebContents().toggleDevTools();
});
electron.ipcRenderer.on('focus', (_: Event, message: boolean) => message ? window.focus() : window.blur());

View File

@ -8,7 +8,9 @@ export function mkdir(dir: string): void {
if(!(e instanceof Error)) throw e;
switch((<Error & {code: string}>e).code) {
case 'ENOENT':
mkdir(path.dirname(dir));
const dirname = path.dirname(dir);
if(dirname === dir) throw e;
mkdir(dirname);
mkdir(dir);
break;
default:

View File

@ -39,7 +39,7 @@
},
"publish": {
"provider": "generic",
"url": "https://toys.in.newtsin.space/chat-updater",
"url": "https://client.f-list.net/",
"channel": "latest"
}
}

View File

@ -85,9 +85,8 @@ module.exports = function(env) {
config.devtool = 'source-map';
config.plugins.push(
new UglifyPlugin({sourceMap: true}),
new webpack.DefinePlugin({
'process.env.NODE_ENV': JSON.stringify('production')
})
new webpack.DefinePlugin({'process.env.NODE_ENV': JSON.stringify('production')}),
new webpack.LoaderOptionsPlugin({minimize: true})
);
} else {
//config.devtool = 'cheap-module-eval-source-map';

View File

@ -23,8 +23,8 @@ function mapToScreen(state: SavedWindowState): SavedWindowState {
x /= primaryDisplay.scaleFactor;
y /= primaryDisplay.scaleFactor;
}
state.x = x > 0 ? x : undefined;
state.y = y > 0 ? y : undefined;
state.x = x !== 0 ? x : undefined;
state.y = y !== 0 ? y : undefined;
return state;
}

View File

@ -72,8 +72,8 @@ class State implements Interfaces.State {
officialChannels: {readonly [key: string]: ListItem | undefined} = {};
openRooms: {readonly [key: string]: ListItem | undefined} = {};
joinedChannels: Channel[] = [];
joinedMap: {[key: string]: Channel | undefined} = {};
handlers: Interfaces.EventHandler[] = [];
joinedKeys: {[key: string]: number | undefined} = {};
constructor(private connection: Connection) {
}
@ -86,18 +86,6 @@ class State implements Interfaces.State {
this.connection.send('LCH', {channel});
}
addChannel(channel: Channel): void {
this.joinedKeys[channel.id] = this.joinedChannels.length;
this.joinedChannels.push(channel);
for(const handler of this.handlers) handler('join', channel);
}
removeChannel(channel: Channel): void {
this.joinedChannels.splice(this.joinedKeys[channel.id]!, 1);
delete this.joinedKeys[channel.id];
for(const handler of this.handlers) handler('leave', channel);
}
getChannelItem(id: string): ListItem | undefined {
id = id.toLowerCase();
return (id.substr(0, 4) === 'adh-' ? this.openRooms : this.officialChannels)[id];
@ -108,8 +96,7 @@ class State implements Interfaces.State {
}
getChannel(id: string): Channel | undefined {
const key = this.joinedKeys[id.toLowerCase()];
return key !== undefined ? this.joinedChannels[key] : undefined;
return this.joinedMap[id.toLowerCase()];
}
}
@ -120,7 +107,7 @@ export default function(this: void, connection: Connection, characters: Characte
let getChannelTimer: NodeJS.Timer | undefined;
connection.onEvent('connecting', () => {
state.joinedChannels = [];
state.joinedKeys = {};
state.joinedMap = {};
});
connection.onEvent('connected', (isReconnect) => {
if(isReconnect) queuedJoin(Object.keys(state.joinedChannels));
@ -132,13 +119,16 @@ export default function(this: void, connection: Connection, characters: Characte
if(getChannelTimer !== undefined) clearInterval(getChannelTimer);
getChannelTimer = setInterval(getChannels, 60000);
});
connection.onEvent('closed', () => {
if(getChannelTimer !== undefined) clearInterval(getChannelTimer);
});
connection.onMessage('CHA', (data) => {
const channels: {[key: string]: ListItem} = {};
for(const channel of data.channels) {
const id = channel.name.toLowerCase();
const item = new ListItem(id, channel.name, channel.characters);
if(state.joinedKeys[id] !== undefined) item.isJoined = true;
if(state.joinedMap[id] !== undefined) item.isJoined = true;
channels[id] = item;
}
state.officialChannels = channels;
@ -148,7 +138,7 @@ export default function(this: void, connection: Connection, characters: Characte
for(const channel of data.channels) {
const id = channel.name.toLowerCase();
const item = new ListItem(id, decodeHTML(channel.title), channel.characters);
if(state.joinedKeys[id] !== undefined) item.isJoined = true;
if(state.joinedMap[id] !== undefined) item.isJoined = true;
channels[id] = item;
}
state.openRooms = channels;
@ -156,7 +146,9 @@ export default function(this: void, connection: Connection, characters: Characte
connection.onMessage('JCH', (data) => {
const item = state.getChannelItem(data.channel);
if(data.character.identity === connection.character) {
state.addChannel(new Channel(data.channel.toLowerCase(), decodeHTML(data.title)));
const id = data.channel.toLowerCase();
const channel = state.joinedMap[id] = new Channel(id, decodeHTML(data.title));
state.joinedChannels.push(channel);
if(item !== undefined) item.isJoined = true;
} else {
const channel = state.getChannel(data.channel)!;
@ -179,6 +171,7 @@ export default function(this: void, connection: Connection, characters: Characte
channel.sortedMembers = sorted;
const item = state.getChannelItem(data.channel);
if(item !== undefined) item.memberCount = data.users.length;
for(const handler of state.handlers) handler('join', channel);
});
connection.onMessage('CDS', (data) => state.getChannel(data.channel)!.description = decodeHTML(data.description));
connection.onMessage('LCH', (data) => {
@ -186,7 +179,9 @@ export default function(this: void, connection: Connection, characters: Characte
if(channel === undefined) return;
const item = state.getChannelItem(data.channel);
if(data.character === connection.character) {
state.removeChannel(channel);
state.joinedChannels.splice(state.joinedChannels.indexOf(channel), 1);
delete state.joinedMap[channel.id];
for(const handler of state.handlers) handler('leave', channel);
if(item !== undefined) item.isJoined = false;
} else {
channel.removeMember(data.character);
@ -230,13 +225,13 @@ export default function(this: void, connection: Connection, characters: Characte
});
connection.onMessage('RMO', (data) => state.getChannel(data.channel)!.mode = data.mode);
connection.onMessage('FLN', (data) => {
for(const key in state.joinedKeys)
state.getChannel(key)!.removeMember(data.character);
for(const key in state.joinedMap)
state.joinedMap[key]!.removeMember(data.character);
});
const globalHandler = (data: Connection.ServerCommands['AOP'] | Connection.ServerCommands['DOP']) => {
//tslint:disable-next-line:forin
for(const key in state.joinedKeys) {
const channel = state.getChannel(key)!;
for(const key in state.joinedMap) {
const channel = state.joinedMap[key]!;
const member = channel.members[data.character];
if(member !== undefined) channel.reSortMember(member);
}

View File

@ -77,7 +77,11 @@ export default class Connection implements Interfaces.Connection {
data.ticket = this.ticket = await this.ticketProvider();
res = <{error: string}>(await queryApi(endpoint, data)).data;
}
if(res.error !== '') throw new Error(res.error);
if(res.error !== '') {
const error = new Error(res.error);
(<Error & {request: true}>error).request = true;
throw error;
}
return res;
}

View File

@ -55,7 +55,7 @@ export namespace Connection {
export type ServerCommands = {
ADL: {ops: ReadonlyArray<string>},
AOP: {character: string},
BRO: {message: string, character: string},
BRO: {message: string, character?: string},
CBU: {operator: string, channel: string, character: string},
CDS: {channel: string, description: string},
CHA: {channels: ReadonlyArray<{name: string, mode: Channel.Mode, characters: number}>},

View File

@ -1,11 +1,15 @@
@import "../variables/default.less";
.message-own {
background-color: @gray-lighter;
.nav-tabs > li > a:hover {
background-color: @gray-darker;
}
.whiteText {
text-shadow: 1px 1px @gray;
.modal .nav-tabs > li.active > a {
background-color: @gray-dark;
}
.message-own {
background-color: @gray-darker;
}
// Apply variables to theme.
@ -13,7 +17,7 @@
* {
&::-webkit-scrollbar-track {
box-shadow: inset 0 0 8px @gray;
box-shadow: inset 0 0 8px @panel-default-border;
border-radius: 10px;
}
@ -24,13 +28,13 @@
&::-webkit-scrollbar-thumb {
border-radius: 10px;
box-shadow: inset 0 0 4px rgba(0, 0, 0, 0.8);
background-color: @gray-lighter;
background-color: @gray-dark;
&:hover {
background-color: @gray-light;
background-color: @gray;
}
&:active {
background-color: @gray;
background-color: @gray-light;
}
}
}

View File

@ -13,7 +13,7 @@
* {
&::-webkit-scrollbar-track {
box-shadow: inset 0 0 8px @gray-light;
box-shadow: inset 0 0 8px @gray;
border-radius: 10px;
}

View File

@ -2,23 +2,22 @@
@import "~bootstrap/less/variables.less";
@import "../../flist_variables.less";
@gray-base: #080810;
@gray-darker: lighten(@gray-base, 15%);
@gray-dark: lighten(@gray-base, 25%);
@gray-base: #000000;
@gray-darker: lighten(@gray-base, 4%);
@gray-dark: lighten(@gray-base, 20%);
@gray: lighten(@gray-base, 55%);
@gray-light: lighten(@gray-base, 76.7%);
@gray-lighter: lighten(@gray-base, 93.5%);
@gray-light: lighten(@gray-base, 85%);
@gray-lighter: lighten(@gray-base, 95%);
// @body-bg: #262626;
@body-bg: darken(@text-background-color-disabled, 3%);
@body-bg: @gray-darker;
@text-color: @gray-lighter;
@text-color-disabled: @gray;
@link-color: darken(@gray-lighter, 15%);
@brand-warning: #c26c00;
@brand-danger: #930300;
@brand-success: #009900;
@brand-info: #0447af;
@brand-warning: #a50;
@brand-danger: #800;
@brand-success: #080;
@brand-info: #13b;
@brand-primary: @brand-info;
@state-info-bg: darken(@brand-info, 15%);

View File

@ -2,7 +2,113 @@
@import "~bootstrap/less/variables.less";
@import "../../flist_variables.less";
@gray-base: #080810;
@gray-darker: lighten(@gray-base, 15%);
@gray-dark: lighten(@gray-base, 25%);
@gray: lighten(@gray-base, 55%);
@gray-light: lighten(@gray-base, 76.7%);
@gray-lighter: lighten(@gray-base, 93.5%);
// Update variables here.
// @body-bg: #00ff00;
@hr-border: @text-color;
// @body-bg: #262626;
@body-bg: darken(@text-background-color-disabled, 3%);
@text-color: @gray-lighter;
@text-color-disabled: @gray;
@link-color: darken(@gray-lighter, 15%);
@brand-warning: #c26c00;
@brand-danger: #930300;
@brand-success: #009900;
@brand-info: #0447af;
@brand-primary: @brand-info;
@state-info-bg: darken(@brand-info, 15%);
@state-info-text: lighten(@brand-info, 30%);
@state-success-bg: darken(@brand-success, 15%);
@state-success-text: lighten(@brand-success, 30%);
@state-warning-bg: darken(@brand-warning, 15%);
@state-warning-text: lighten(@brand-warning, 30%);
@state-danger-bg: darken(@brand-danger, 15%);
@state-danger-text: lighten(@brand-danger, 30%);
@text-background-color: @gray-dark;
@text-background-color-disabled: @gray-darker;
@border-color: lighten(spin(@text-background-color, -10), 15%);
@border-color-active: lighten(spin(@text-background-color, -10), 25%);
@border-color-disabled: darken(spin(@text-background-color-disabled, -10), 8%);
@hover-bg: lighten(@gray-dark, 15%);
@hr-border: @text-color;
@panel-bg: @text-background-color;
@panel-default-heading-bg: @gray;
@panel-default-border: @border-color;
@input-color: @gray-light;
@input-bg: @text-background-color;
@input-bg-disabled: @text-background-color-disabled;
@input-border: @border-color;
@input-border-focus: @gray;
@dropdown-bg: @text-background-color;
@dropdown-color: @text-color;
@dropdown-link-color: @link-color;
@dropdown-link-hover-color: @gray-dark;
@dropdown-link-hover-bg: @gray-light;
@navbar-default-bg: @text-background-color;
@navbar-default-color: @text-color;
@navbar-default-link-color: @link-color;
@navbar-default-link-hover-color: @link-hover-color;
@nav-link-hover-bg: @gray-light;
@nav-link-hover-color: @gray-dark;
@nav-tabs-border-color: @border-color;
@nav-tabs-link-hover-border-color: @border-color;
@nav-tabs-active-link-hover-bg: @body-bg;
@nav-tabs-active-link-hover-color: @text-color;
@nav-tabs-active-link-hover-border-color: @border-color;
@component-active-color: @gray-dark;
@component-active-bg: @gray-light;
@list-group-bg: @gray-darker;
@list-group-border: @gray-dark;
@list-group-link-color: @text-color;
@list-group-hover-bg: @gray-dark;
@btn-default-bg: @text-background-color;
@btn-default-color: @text-color;
@btn-default-border: @border-color;
@pagination-bg: @text-background-color;
@pagination-color: @text-color;
@pagination-border: @border-color;
@pagination-disabled-bg: @text-background-color-disabled;
@pagination-disabled-color: @text-color-disabled;
@pagination-disabled-border: @border-color-disabled;
@pagination-active-bg: @gray;
@pagination-active-color: @gray-lighter;
@pagination-active-border: @border-color-active;
@modal-content-bg: @text-background-color;
@modal-footer-border-color: lighten(spin(@modal-content-bg, -10), 15%);
@modal-header-border-color: @modal-footer-border-color;
@badge-color: @gray-darker;
@close-color: saturate(@text-color, 10%);
@close-text-shadow: 0 1px 0 @text-color;
@well-bg: @text-background-color;
@well-border: @border-color;
@blockquote-border-color: @border-color-active;
@collapse-border: desaturate(@well-border, 20%);
@collapse-header-bg: desaturate(@well-bg, 20%);
@white-color: @text-color;
@purple-color: @gray-light;

View File

@ -4,4 +4,5 @@
// Update variables here.
// @body-bg: #00ff00;
@hr-border: @text-color;
@hr-border: @text-color;
@body-bg: #fafafa;