3.0.10
This commit is contained in:
parent
8810b29552
commit
a5e57cd52c
|
@ -5,7 +5,7 @@
|
|||
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'} : undefined" @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)">
|
||||
|
@ -21,7 +21,7 @@
|
|||
<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>
|
||||
:style="hasToolbar ? {'border-top-left-radius': 0} : undefined" @keydown="onKeyDown"></textarea>
|
||||
<textarea ref="sizer"></textarea>
|
||||
<div class="bbcode-preview" v-show="preview">
|
||||
<div class="bbcode-preview-warnings">
|
||||
|
@ -36,9 +36,8 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {Component, Hook, Prop, Watch} from '@f-list/vue-ts';
|
||||
import Vue from 'vue';
|
||||
import Component from 'vue-class-component';
|
||||
import {Prop, Watch} from 'vue-property-decorator';
|
||||
import {BBCodeElement} from '../chat/bbcode';
|
||||
import {getKey} from '../chat/common';
|
||||
import {Keys} from '../keys';
|
||||
|
@ -82,6 +81,7 @@
|
|||
//tslint:disable:strict-boolean-expressions
|
||||
private resizeListener!: () => void;
|
||||
|
||||
@Hook('created')
|
||||
created(): void {
|
||||
this.parser = new CoreBBCodeParser();
|
||||
this.resizeListener = () => {
|
||||
|
@ -91,6 +91,7 @@
|
|||
};
|
||||
}
|
||||
|
||||
@Hook('mounted')
|
||||
mounted(): void {
|
||||
this.element = <HTMLTextAreaElement>this.$refs['input'];
|
||||
const styles = getComputedStyle(this.element);
|
||||
|
@ -113,8 +114,10 @@
|
|||
this.resize();
|
||||
window.addEventListener('resize', this.resizeListener);
|
||||
}
|
||||
|
||||
//tslint:enable
|
||||
|
||||
@Hook('destroyed')
|
||||
destroyed(): void {
|
||||
window.removeEventListener('resize', this.resizeListener);
|
||||
}
|
||||
|
@ -189,7 +192,7 @@
|
|||
// Allow emitted variations for custom buttons.
|
||||
this.$once('insert', (startText: string, endText: string) => this.applyText(startText, endText));
|
||||
if(button.handler !== undefined)
|
||||
return <void>button.handler.call(this, this);
|
||||
return button.handler.call(this, this);
|
||||
if(button.startText === undefined)
|
||||
button.startText = `[${button.tag}]`;
|
||||
if(button.endText === undefined)
|
||||
|
|
|
@ -72,9 +72,8 @@ export class CoreBBCodeParser extends BBCodeParser {
|
|||
a.textContent = display;
|
||||
element.appendChild(a);
|
||||
const span = document.createElement('span');
|
||||
span.className = 'link-domain';
|
||||
span.className = 'link-domain bbcode-pseudo';
|
||||
span.textContent = ` [${domain(url)}]`;
|
||||
(<HTMLElement & {bbcodeHide: true}>span).bbcodeHide = true;
|
||||
element.appendChild(span);
|
||||
return element;
|
||||
}));
|
||||
|
|
|
@ -166,10 +166,14 @@ export class BBCodeParser {
|
|||
if(tag instanceof BBCodeTextTag) {
|
||||
i = this.parse(input, i + 1, tag, undefined, isAllowed);
|
||||
element = tag.createElement(this, parent, param, input.substring(mark, input.lastIndexOf('[', i)));
|
||||
if(element === undefined) parent.appendChild(document.createTextNode(input.substring(tagStart, i + 1)));
|
||||
} else {
|
||||
element = tag.createElement(this, parent, param, '');
|
||||
if(element === undefined) parent.appendChild(document.createTextNode(input.substring(tagStart, i + 1)));
|
||||
if(!tag.noClosingTag)
|
||||
i = this.parse(input, i + 1, tag, element !== undefined ? element : parent, isAllowed);
|
||||
if(element === undefined)
|
||||
parent.appendChild(document.createTextNode(input.substring(input.lastIndexOf('[', i), i + 1)));
|
||||
}
|
||||
mark = i + 1;
|
||||
this._currentTag = currentTag;
|
||||
|
@ -182,7 +186,7 @@ export class BBCodeParser {
|
|||
parent.appendChild(document.createTextNode(input.substring(mark, selfAllowed ? tagStart : i + 1)));
|
||||
return i;
|
||||
} else if(!selfAllowed)
|
||||
return tagStart - 1;
|
||||
return mark - 1;
|
||||
else if(isAllowed(tagKey))
|
||||
this.warning(`Unexpected closing ${tagKey} tag. Needed ${self} tag instead.`);
|
||||
} else if(isAllowed(tagKey)) this.warning(`Found closing ${tagKey} tag that was never opened.`);
|
||||
|
|
|
@ -1,15 +1,8 @@
|
|||
import {InlineImage} from '../interfaces';
|
||||
import {CoreBBCodeParser} from './core';
|
||||
import {InlineDisplayMode} from './interfaces';
|
||||
import {BBCodeCustomTag, BBCodeSimpleTag, BBCodeTextTag} from './parser';
|
||||
|
||||
interface InlineImage {
|
||||
id: number
|
||||
hash: string
|
||||
extension: string
|
||||
nsfw: boolean
|
||||
name?: string
|
||||
}
|
||||
|
||||
interface StandardParserSettings {
|
||||
siteDomain: string
|
||||
staticDomain: string
|
||||
|
@ -29,7 +22,7 @@ export class StandardBBCodeParser extends CoreBBCodeParser {
|
|||
const outerEl = this.createElement('div');
|
||||
const el = this.createElement('img');
|
||||
el.className = 'inline-image';
|
||||
el.title = el.alt = inline.name!;
|
||||
el.title = el.alt = inline.name;
|
||||
el.src = `${this.settings.staticDomain}images/charinline/${p1}/${p2}/${inline.hash}.${inline.extension}`;
|
||||
outerEl.appendChild(el);
|
||||
return outerEl;
|
||||
|
|
|
@ -12,7 +12,7 @@
|
|||
<span class="fa fa-2x" :class="{'fa-sort-amount-down': sortCount, 'fa-sort-alpha-down': !sortCount}"></span>
|
||||
</a>
|
||||
</div>
|
||||
<div style="overflow: auto;" v-show="tab == 0">
|
||||
<div style="overflow: auto;" v-show="tab === '0'">
|
||||
<div v-for="channel in officialChannels" :key="channel.id">
|
||||
<label :for="channel.id">
|
||||
<input type="checkbox" :checked="channel.isJoined" :id="channel.id" @click.prevent="setJoined(channel)"/>
|
||||
|
@ -20,7 +20,7 @@
|
|||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div style="overflow: auto;" v-show="tab == 1">
|
||||
<div style="overflow: auto;" v-show="tab === '1'">
|
||||
<div v-for="channel in openRooms" :key="channel.id">
|
||||
<label :for="channel.id">
|
||||
<input type="checkbox" :checked="channel.isJoined" :id="channel.id" @click.prevent="setJoined(channel)"/>
|
||||
|
@ -42,7 +42,7 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Component from 'vue-class-component';
|
||||
import {Component} from '@f-list/vue-ts';
|
||||
import CustomDialog from '../components/custom_dialog';
|
||||
import Modal from '../components/Modal.vue';
|
||||
import Tabs from '../components/tabs';
|
||||
|
|
|
@ -1,11 +1,14 @@
|
|||
<template>
|
||||
<a href="#" @click.prevent="joinChannel" :disabled="channel && channel.isJoined"><span class="fa fa-hashtag"></span>{{displayText}}</a>
|
||||
<a href="#" @click.prevent="joinChannel()" :disabled="channel && channel.isJoined">
|
||||
<span class="fa fa-hashtag"></span>
|
||||
<template v-if="channel">{{channel.name}}<span class="bbcode-pseudo"> ({{channel.memberCount}})</span></template>
|
||||
<template v-else>{{text}}</template>
|
||||
</a>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {Component, Hook, Prop} from '@f-list/vue-ts';
|
||||
import Vue from 'vue';
|
||||
import Component from 'vue-class-component';
|
||||
import {Prop} from 'vue-property-decorator';
|
||||
import core from './core';
|
||||
import {Channel} from './interfaces';
|
||||
|
||||
|
@ -16,6 +19,7 @@
|
|||
@Prop({required: true})
|
||||
readonly text!: string;
|
||||
|
||||
@Hook('mounted')
|
||||
mounted(): void {
|
||||
core.channels.requestChannelsIfNeeded(300000);
|
||||
}
|
||||
|
@ -23,10 +27,8 @@
|
|||
joinChannel(): void {
|
||||
if(this.channel === undefined || !this.channel.isJoined)
|
||||
core.channels.join(this.id);
|
||||
}
|
||||
|
||||
get displayText(): string {
|
||||
return this.channel !== undefined ? `${this.channel.name} (${this.channel.memberCount})` : this.text;
|
||||
const channel = core.conversations.byKey(`#${this.id}`);
|
||||
if(channel !== undefined) channel.show();
|
||||
}
|
||||
|
||||
get channel(): Channel.ListItem | undefined {
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<modal :action="l('characterSearch.action')" @submit.prevent="submit" dialogClass="w-100"
|
||||
<modal :action="l('characterSearch.action')" @submit.prevent="submit()" dialogClass="w-100"
|
||||
:buttonText="results ? l('characterSearch.again') : undefined" class="character-search">
|
||||
<div v-if="options && !results">
|
||||
<div v-show="error" class="alert alert-danger">{{error}}</div>
|
||||
|
@ -7,7 +7,7 @@
|
|||
:title="l('characterSearch.kinks')" :filterFunc="filterKink" :options="options.kinks">
|
||||
<template slot-scope="s">{{s.option.name}}</template>
|
||||
</filterable-select>
|
||||
<filterable-select v-for="item in ['genders', 'orientations', 'languages', 'furryprefs', 'roles', 'positions']" :multiple="true"
|
||||
<filterable-select v-for="item in listItems" :multiple="true"
|
||||
v-model="data[item]" :placeholder="l('filter')" :title="l('characterSearch.' + item)" :options="options[item]" :key="item">
|
||||
</filterable-select>
|
||||
</div>
|
||||
|
@ -26,8 +26,8 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {Component, Hook} from '@f-list/vue-ts';
|
||||
import Axios from 'axios';
|
||||
import Component from 'vue-class-component';
|
||||
import CustomDialog from '../components/custom_dialog';
|
||||
import FilterableSelect from '../components/FilterableSelect.vue';
|
||||
import Modal from '../components/Modal.vue';
|
||||
|
@ -39,7 +39,7 @@
|
|||
import UserView from './user_view';
|
||||
|
||||
type Options = {
|
||||
kinks: {id: number, name: string, description: string}[],
|
||||
kinks: Kink[],
|
||||
listitems: {id: string, name: string, value: string}[]
|
||||
};
|
||||
|
||||
|
@ -55,35 +55,30 @@
|
|||
return 0;
|
||||
}
|
||||
|
||||
interface Data {
|
||||
kinks: Kink[]
|
||||
genders: string[]
|
||||
orientations: string[]
|
||||
languages: string[]
|
||||
furryprefs: string[]
|
||||
roles: string[]
|
||||
positions: string[]
|
||||
}
|
||||
|
||||
@Component({
|
||||
components: {modal: Modal, user: UserView, 'filterable-select': FilterableSelect, bbcode: BBCodeView}
|
||||
})
|
||||
export default class CharacterSearch extends CustomDialog {
|
||||
//tslint:disable:no-null-keyword
|
||||
l = l;
|
||||
kinksFilter = '';
|
||||
error = '';
|
||||
results: Character[] | null = null;
|
||||
results: Character[] | undefined;
|
||||
characterImage = characterImage;
|
||||
options: {
|
||||
kinks: Kink[]
|
||||
genders: string[]
|
||||
orientations: string[]
|
||||
languages: string[]
|
||||
furryprefs: string[]
|
||||
roles: string[]
|
||||
positions: string[]
|
||||
} | null = null;
|
||||
data: {[key: string]: (string | Kink)[]} = {
|
||||
kinks: <Kink[]>[],
|
||||
genders: <string[]>[],
|
||||
orientations: <string[]>[],
|
||||
languages: <string[]>[],
|
||||
furryprefs: <string[]>[],
|
||||
roles: <string[]>[],
|
||||
positions: <string[]>[]
|
||||
};
|
||||
options!: Data;
|
||||
data: Data = {kinks: [], genders: [], orientations: [], languages: [], furryprefs: [], roles: [], positions: []};
|
||||
listItems: ReadonlyArray<keyof Data> = ['genders', 'orientations', 'languages', 'furryprefs', 'roles', 'positions'];
|
||||
|
||||
@Hook('created')
|
||||
async created(): Promise<void> {
|
||||
if(options === undefined)
|
||||
options = <Options | undefined>(await Axios.get('https://www.f-list.net/json/api/mapping-list.php')).data;
|
||||
|
@ -99,6 +94,7 @@
|
|||
});
|
||||
}
|
||||
|
||||
@Hook('mounted')
|
||||
mounted(): void {
|
||||
core.connection.onMessage('ERR', (data) => {
|
||||
switch(data.number) {
|
||||
|
@ -129,15 +125,17 @@
|
|||
}
|
||||
|
||||
submit(): void {
|
||||
if(this.results !== null) {
|
||||
this.results = null;
|
||||
if(this.results !== undefined) {
|
||||
this.results = undefined;
|
||||
return;
|
||||
}
|
||||
this.error = '';
|
||||
const data: Connection.ClientCommands['FKS'] & {[key: string]: (string | number)[]} = {kinks: []};
|
||||
for(const key in this.data)
|
||||
if(this.data[key].length > 0)
|
||||
data[key] = key === 'kinks' ? (<Kink[]>this.data[key]).map((x) => x.id) : (<string[]>this.data[key]);
|
||||
for(const key in this.data) {
|
||||
const item = this.data[<keyof Data>key];
|
||||
if(item.length > 0)
|
||||
data[key] = key === 'kinks' ? (<Kink[]>item).map((x) => x.id) : (<string[]>item);
|
||||
}
|
||||
core.connection.send('FKS', data);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
<div class="alert alert-danger" v-show="error">{{error}}</div>
|
||||
<h3 class="card-header" style="margin-top:0;display:flex">
|
||||
{{l('title')}}
|
||||
<a href="#" @click.prevent="$refs['logsDialog'].show()" class="btn" style="flex:1;text-align:right">
|
||||
<a href="#" @click.prevent="showLogs()" class="btn" style="flex:1;text-align:right">
|
||||
<span class="fa fa-file-alt"></span> <span class="btn-text">{{l('logs.title')}}</span>
|
||||
</a>
|
||||
</h3>
|
||||
|
@ -32,9 +32,8 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {Component, Hook, Prop} from '@f-list/vue-ts';
|
||||
import Vue from 'vue';
|
||||
import Component from 'vue-class-component';
|
||||
import {Prop} from 'vue-property-decorator';
|
||||
import Modal from '../components/Modal.vue';
|
||||
import Channels from '../fchat/channels';
|
||||
import Characters from '../fchat/characters';
|
||||
|
@ -46,7 +45,7 @@
|
|||
import l from './localize';
|
||||
import Logs from './Logs.vue';
|
||||
|
||||
type BBCodeNode = Node & {bbcodeTag?: string, bbcodeParam?: string, bbcodeHide?: boolean};
|
||||
type BBCodeNode = Node & {bbcodeTag?: string, bbcodeParam?: string};
|
||||
|
||||
function copyNode(str: string, node: BBCodeNode, end: Node, range: Range, flags: {endFound?: true}): string {
|
||||
if(node === end) flags.endFound = true;
|
||||
|
@ -54,7 +53,7 @@
|
|||
str = `[${node.bbcodeTag}${node.bbcodeParam !== undefined ? `=${node.bbcodeParam}` : ''}]${str}[/${node.bbcodeTag}]`;
|
||||
if(node.nextSibling !== null && !flags.endFound) {
|
||||
if(node instanceof HTMLElement && getComputedStyle(node).display === 'block') str += '\r\n';
|
||||
str += scanNode(node.nextSibling!, end, range, flags);
|
||||
str += scanNode(node.nextSibling, end, range, flags);
|
||||
}
|
||||
if(node.parentElement === null) return str;
|
||||
return copyNode(str, node.parentNode!, end, range, flags);
|
||||
|
@ -62,7 +61,7 @@
|
|||
|
||||
function scanNode(node: BBCodeNode, end: Node, range: Range, flags: {endFound?: true}, hide?: boolean): string {
|
||||
let str = '';
|
||||
hide = hide || node.bbcodeHide;
|
||||
hide = hide || node instanceof HTMLElement && node.classList.contains('bbcode-pseudo');
|
||||
if(node === end) flags.endFound = true;
|
||||
if(node.bbcodeTag !== undefined) str += `[${node.bbcodeTag}${node.bbcodeParam !== undefined ? `=${node.bbcodeParam}` : ''}]`;
|
||||
if(node instanceof Text) str += node === range.endContainer ? node.nodeValue!.substr(0, range.endOffset) : node.nodeValue;
|
||||
|
@ -91,6 +90,7 @@
|
|||
l = l;
|
||||
copyPlain = false;
|
||||
|
||||
@Hook('mounted')
|
||||
mounted(): void {
|
||||
document.title = l('title', core.connection.character);
|
||||
document.addEventListener('copy', ((e: ClipboardEvent) => {
|
||||
|
@ -102,10 +102,11 @@
|
|||
if(selection === null || selection.isCollapsed) return;
|
||||
const range = selection.getRangeAt(0);
|
||||
let start = range.startContainer, end = range.endContainer;
|
||||
let startValue: string;
|
||||
let startValue = '';
|
||||
if(start instanceof HTMLElement) {
|
||||
start = start.childNodes[range.startOffset];
|
||||
startValue = start instanceof HTMLImageElement ? start.alt : scanNode(start.firstChild!, end, range, {});
|
||||
if(<Node | undefined>start === undefined) start = range.startContainer;
|
||||
else 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 && range.endOffset > 0) end = end.childNodes[range.endOffset - 1];
|
||||
|
@ -157,6 +158,10 @@
|
|||
(<Modal>this.$refs['reconnecting']).hide();
|
||||
}
|
||||
|
||||
showLogs(): void {
|
||||
(<Logs>this.$refs['logsDialog']).show();
|
||||
}
|
||||
|
||||
async connect(): Promise<void> {
|
||||
this.connecting = true;
|
||||
await core.notifications.initSounds(['attention', 'login', 'logout', 'modalert', 'newnote']);
|
||||
|
|
|
@ -1,24 +1,23 @@
|
|||
<template>
|
||||
<div style="height:100%; display: flex; position: relative;" id="chatView" @click="$refs['userMenu'].handleEvent($event)"
|
||||
@contextmenu="$refs['userMenu'].handleEvent($event)" @touchstart.passive="$refs['userMenu'].handleEvent($event)"
|
||||
@touchend="$refs['userMenu'].handleEvent($event)">
|
||||
<div style="height:100%; display: flex; position: relative;" id="chatView" @click="userMenuHandle" @contextmenu="userMenuHandle" @touchstart.passive="userMenuHandle"
|
||||
@touchend="userMenuHandle">
|
||||
<sidebar id="sidebar" :label="l('chat.menu')" icon="fa-bars">
|
||||
<img :src="characterImage(ownCharacter.name)" v-if="showAvatars" style="float:left;margin-right:5px;width:60px"/>
|
||||
<a target="_blank" :href="ownCharacterLink" class="btn" style="margin-right:5px">{{ownCharacter.name}}</a>
|
||||
<a href="#" @click.prevent="logOut" class="btn"><i class="fas fa-sign-out-alt"></i>{{l('chat.logout')}}</a><br/>
|
||||
<a href="#" @click.prevent="logOut()" class="btn"><i class="fas fa-sign-out-alt"></i>{{l('chat.logout')}}</a><br/>
|
||||
<div>
|
||||
{{l('chat.status')}}
|
||||
<a href="#" @click.prevent="$refs['statusDialog'].show()" class="btn">
|
||||
<a href="#" @click.prevent="showStatus()" class="btn">
|
||||
<span class="fas fa-fw" :class="getStatusIcon(ownCharacter.status)"></span>{{l('status.' + ownCharacter.status)}}
|
||||
</a>
|
||||
</div>
|
||||
<div style="clear:both">
|
||||
<a href="#" @click.prevent="$refs['searchDialog'].show()" class="btn"><span class="fas fa-search"></span>
|
||||
<a href="#" @click.prevent="showSearch()" class="btn"><span class="fas fa-search"></span>
|
||||
{{l('characterSearch.open')}}</a>
|
||||
</div>
|
||||
<div><a href="#" @click.prevent="$refs['settingsDialog'].show()" class="btn"><span class="fas fa-cog"></span>
|
||||
<div><a href="#" @click.prevent="showSettings()" class="btn"><span class="fas fa-cog"></span>
|
||||
{{l('settings.open')}}</a></div>
|
||||
<div><a href="#" @click.prevent="$refs['recentDialog'].show()" class="btn"><span class="fas fa-history"></span>
|
||||
<div><a href="#" @click.prevent="showRecent()" class="btn"><span class="fas fa-history"></span>
|
||||
{{l('chat.recentConversations')}}</a></div>
|
||||
<div class="list-group conversation-nav">
|
||||
<a :class="getClasses(conversations.consoleTab)" href="#" @click.prevent="conversations.consoleTab.show()"
|
||||
|
@ -47,7 +46,7 @@
|
|||
</div>
|
||||
</a>
|
||||
</div>
|
||||
<a href="#" @click.prevent="$refs['channelsDialog'].show()" class="btn"><span class="fas fa-list"></span>
|
||||
<a href="#" @click.prevent="showChannels()" class="btn"><span class="fas fa-list"></span>
|
||||
{{l('chat.channels')}}</a>
|
||||
<div class="list-group conversation-nav" ref="channelConversations">
|
||||
<a v-for="conversation in conversations.channelConversations" href="#" @click.prevent="conversation.show()"
|
||||
|
@ -62,7 +61,7 @@
|
|||
</a>
|
||||
</div>
|
||||
</sidebar>
|
||||
<div style="width: 100%; display:flex; flex-direction:column;">
|
||||
<div style="display:flex;flex-direction:column;flex:1;min-width:0">
|
||||
<div id="quick-switcher" class="list-group">
|
||||
<a :class="getClasses(conversations.consoleTab)" href="#" @click.prevent="conversations.consoleTab.show()"
|
||||
class="list-group-item list-group-item-action">
|
||||
|
@ -95,10 +94,10 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {Component, Hook} from '@f-list/vue-ts';
|
||||
//tslint:disable-next-line:no-require-imports
|
||||
import Sortable = require('sortablejs');
|
||||
import Vue from 'vue';
|
||||
import Component from 'vue-class-component';
|
||||
import {Keys} from '../keys';
|
||||
import ChannelList from './ChannelList.vue';
|
||||
import CharacterSearch from './CharacterSearch.vue';
|
||||
|
@ -139,22 +138,23 @@
|
|||
focusListener!: () => void;
|
||||
blurListener!: () => void;
|
||||
|
||||
@Hook('mounted')
|
||||
mounted(): void {
|
||||
this.keydownListener = (e: KeyboardEvent) => this.onKeyDown(e);
|
||||
window.addEventListener('keydown', this.keydownListener);
|
||||
this.setFontSize(core.state.settings.fontSize);
|
||||
Sortable.create(this.$refs['privateConversations'], {
|
||||
Sortable.create(<HTMLElement>this.$refs['privateConversations'], {
|
||||
animation: 50,
|
||||
onEnd: async(e: {oldIndex: number, newIndex: number}) => {
|
||||
onEnd: async(e) => {
|
||||
if(e.oldIndex === e.newIndex) return;
|
||||
return core.conversations.privateConversations[e.oldIndex].sort(e.newIndex);
|
||||
return core.conversations.privateConversations[e.oldIndex!].sort(e.newIndex!);
|
||||
}
|
||||
});
|
||||
Sortable.create(this.$refs['channelConversations'], {
|
||||
Sortable.create(<HTMLElement>this.$refs['channelConversations'], {
|
||||
animation: 50,
|
||||
onEnd: async(e: {oldIndex: number, newIndex: number}) => {
|
||||
onEnd: async(e) => {
|
||||
if(e.oldIndex === e.newIndex) return;
|
||||
return core.conversations.channelConversations[e.oldIndex].sort(e.newIndex);
|
||||
return core.conversations.channelConversations[e.oldIndex!].sort(e.newIndex!);
|
||||
}
|
||||
});
|
||||
const ownCharacter = core.characters.ownCharacter;
|
||||
|
@ -175,7 +175,7 @@
|
|||
window.addEventListener('blur', this.blurListener = () => {
|
||||
core.notifications.isInBackground = true;
|
||||
if(idleTimer !== undefined) clearTimeout(idleTimer);
|
||||
if(core.state.settings.idleTimer > 0)
|
||||
if(core.state.settings.idleTimer > 0 && core.characters.ownCharacter.status !== 'dnd')
|
||||
idleTimer = window.setTimeout(() => {
|
||||
lastUpdate = Date.now();
|
||||
idleStatus = {status: ownCharacter.status, statusmsg: ownCharacter.statusText};
|
||||
|
@ -195,6 +195,7 @@
|
|||
});
|
||||
}
|
||||
|
||||
@Hook('destroyed')
|
||||
destroyed(): void {
|
||||
window.removeEventListener('keydown', this.keydownListener);
|
||||
window.removeEventListener('focus', this.focusListener);
|
||||
|
@ -204,7 +205,7 @@
|
|||
needsReply(conversation: Conversation): boolean {
|
||||
if(!core.state.settings.showNeedsReply) return false;
|
||||
for(let i = conversation.messages.length - 1; i >= 0; --i) {
|
||||
const sender = conversation.messages[i].sender;
|
||||
const sender = (<Partial<Conversation.ChatMessage>>conversation.messages[i]).sender;
|
||||
if(sender !== undefined)
|
||||
return sender !== core.characters.ownCharacter;
|
||||
}
|
||||
|
@ -268,6 +269,30 @@
|
|||
if(confirm(l('chat.confirmLeave'))) core.connection.close();
|
||||
}
|
||||
|
||||
showSettings(): void {
|
||||
(<SettingsView>this.$refs['settingsDialog']).show();
|
||||
}
|
||||
|
||||
showSearch(): void {
|
||||
(<CharacterSearch>this.$refs['searchDialog']).show();
|
||||
}
|
||||
|
||||
showRecent(): void {
|
||||
(<RecentConversations>this.$refs['recentDialog']).show();
|
||||
}
|
||||
|
||||
showChannels(): void {
|
||||
(<ChannelList>this.$refs['channelsDialog']).show();
|
||||
}
|
||||
|
||||
showStatus(): void {
|
||||
(<StatusSwitcher>this.$refs['statusDialog']).show();
|
||||
}
|
||||
|
||||
userMenuHandle(e: MouseEvent | TouchEvent): void {
|
||||
(<UserMenu>this.$refs['userMenu']).handleEvent(e);
|
||||
}
|
||||
|
||||
get showAvatars(): boolean {
|
||||
return core.state.settings.showAvatars;
|
||||
}
|
||||
|
|
|
@ -25,7 +25,7 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Component from 'vue-class-component';
|
||||
import {Component, Hook} from '@f-list/vue-ts';
|
||||
import CustomDialog from '../components/custom_dialog';
|
||||
import Modal from '../components/Modal.vue';
|
||||
import core from './core';
|
||||
|
@ -55,6 +55,7 @@
|
|||
return this.commands.filter((x) => filter.test(x.name));
|
||||
}
|
||||
|
||||
@Hook('mounted')
|
||||
mounted(): void {
|
||||
const permissions = core.connection.vars.permissions;
|
||||
for(const key in commands) {
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<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" @open="load()" dialogClass="w-100"
|
||||
:buttonText="l('conversationSettings.save')">
|
||||
<div class="form-group">
|
||||
<label class="control-label" :for="'notify' + conversation.key">{{l('conversationSettings.notify')}}</label>
|
||||
|
@ -39,8 +39,7 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Component from 'vue-class-component';
|
||||
import {Prop, Watch} from 'vue-property-decorator';
|
||||
import {Component, Prop} from '@f-list/vue-ts';
|
||||
import CustomDialog from '../components/custom_dialog';
|
||||
import Modal from '../components/Modal.vue';
|
||||
import {Conversation} from './interfaces';
|
||||
|
@ -60,23 +59,13 @@
|
|||
joinMessages!: Conversation.Setting;
|
||||
defaultHighlights!: boolean;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.init();
|
||||
}
|
||||
|
||||
init = function(this: ConversationSettings): void {
|
||||
load(): void {
|
||||
const settings = this.conversation.settings;
|
||||
this.notify = settings.notify;
|
||||
this.highlight = settings.highlight;
|
||||
this.highlightWords = settings.highlightWords.join(',');
|
||||
this.joinMessages = settings.joinMessages;
|
||||
this.defaultHighlights = settings.defaultHighlights;
|
||||
};
|
||||
|
||||
@Watch('conversation')
|
||||
conversationChanged(): void {
|
||||
this.init();
|
||||
}
|
||||
|
||||
submit(): void {
|
||||
|
|
|
@ -1,17 +1,17 @@
|
|||
<template>
|
||||
<div style="height:100%;display:flex;flex-direction:column;flex:1;margin:0 5px;position:relative" id="conversation">
|
||||
<div style="display:flex" v-if="conversation.character" class="header">
|
||||
<div style="display:flex" v-if="isPrivate(conversation)" class="header">
|
||||
<img :src="characterImage" style="height:60px;width:60px;margin-right:10px" v-if="settings.showAvatars"/>
|
||||
<div style="flex:1;position:relative;display:flex;flex-direction:column">
|
||||
<div>
|
||||
<user :character="conversation.character"></user>
|
||||
<a href="#" @click.prevent="$refs['logsDialog'].show()" class="btn">
|
||||
<a href="#" @click.prevent="showLogs()" class="btn">
|
||||
<span class="fa fa-file-alt"></span> <span class="btn-text">{{l('logs.title')}}</span>
|
||||
</a>
|
||||
<a href="#" @click.prevent="$refs['settingsDialog'].show()" class="btn">
|
||||
<a href="#" @click.prevent="showSettings()" class="btn">
|
||||
<span class="fa fa-cog"></span> <span class="btn-text">{{l('conversationSettings.title')}}</span>
|
||||
</a>
|
||||
<a href="#" @click.prevent="reportDialog.report();" class="btn">
|
||||
<a href="#" @click.prevent="reportDialog.report()" class="btn">
|
||||
<span class="fa fa-exclamation-triangle"></span><span class="btn-text">{{l('chat.report')}}</span></a>
|
||||
</div>
|
||||
<div style="overflow:auto;max-height:50px">
|
||||
|
@ -20,7 +20,7 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else-if="conversation.channel" class="header">
|
||||
<div v-else-if="isChannel(conversation)" class="header">
|
||||
<div style="display: flex; align-items: center;">
|
||||
<div style="flex: 1;">
|
||||
<span v-show="conversation.channel.id.substr(0, 4) !== 'adh-'" class="fa fa-star" :title="l('channel.official')"
|
||||
|
@ -30,33 +30,33 @@
|
|||
<span class="fa" :class="{'fa-chevron-down': !descriptionExpanded, 'fa-chevron-up': descriptionExpanded}"></span>
|
||||
<span class="btn-text">{{l('channel.description')}}</span>
|
||||
</a>
|
||||
<a href="#" @click.prevent="$refs['manageDialog'].show()" v-show="isChannelMod" class="btn">
|
||||
<a href="#" @click.prevent="showManage()" 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">
|
||||
<a href="#" @click.prevent="showLogs()" class="btn">
|
||||
<span class="fa fa-file-alt"></span> <span class="btn-text">{{l('logs.title')}}</span>
|
||||
</a>
|
||||
<a href="#" @click.prevent="$refs['settingsDialog'].show()" class="btn">
|
||||
<a href="#" @click.prevent="showSettings()" class="btn">
|
||||
<span class="fa fa-cog"></span> <span class="btn-text">{{l('conversationSettings.title')}}</span>
|
||||
</a>
|
||||
<a href="#" @click.prevent="reportDialog.report();" class="btn">
|
||||
<a href="#" @click.prevent="reportDialog.report()" class="btn">
|
||||
<span class="fa fa-exclamation-triangle"></span><span class="btn-text">{{l('chat.report')}}</span></a>
|
||||
</div>
|
||||
<ul class="nav nav-pills mode-switcher">
|
||||
<li v-for="mode in modes" class="nav-item">
|
||||
<a :class="{active: conversation.mode == mode, disabled: conversation.channel.mode != 'both'}"
|
||||
<a :class="isChannel(conversation) ? {active: conversation.mode == mode, disabled: conversation.channel.mode != 'both'} : undefined"
|
||||
class="nav-link" href="#" @click.prevent="setMode(mode)">{{l('channel.mode.' + mode)}}</a>
|
||||
</li>
|
||||
</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 border-bottom">
|
||||
:style="{display: descriptionExpanded ? 'block' : 'none'}" class="bg-solid-text border-bottom">
|
||||
<bbcode :text="conversation.channel.description"></bbcode>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="header" style="display:flex;align-items:center">
|
||||
<h4>{{l('chat.consoleTab')}}</h4>
|
||||
<a href="#" @click.prevent="$refs['logsDialog'].show()" class="btn">
|
||||
<a href="#" @click.prevent="showLogs()" class="btn">
|
||||
<span class="fa fa-file-alt"></span> <span class="btn-text">{{l('logs.title')}}</span>
|
||||
</a>
|
||||
</div>
|
||||
|
@ -64,31 +64,32 @@
|
|||
<div class="input-group-prepend">
|
||||
<div class="input-group-text"><span class="fas fa-search"></span></div>
|
||||
</div>
|
||||
<input v-model="searchInput" @keydown.esc="hideSearch" @keypress="lastSearchInput = Date.now()"
|
||||
<input v-model="searchInput" @keydown.esc="hideSearch()" @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;z-index:10"
|
||||
@click="hideSearch"><i class="fas fa-times"></i></a>
|
||||
</div>
|
||||
<div class="border-top messages" :class="'messages-' + conversation.mode" ref="messages" @scroll="onMessagesScroll"
|
||||
style="flex:1;overflow:auto;margin-top:2px;position:relative">
|
||||
<div class="border-top messages" :class="isChannel(conversation) ? 'messages-' + conversation.mode : undefined" ref="messages"
|
||||
@scroll="onMessagesScroll" style="flex:1;overflow:auto;margin-top:2px">
|
||||
<template v-for="message in messages">
|
||||
<message-view :message="message" :channel="conversation.channel" :key="message.id"
|
||||
<message-view :message="message" :channel="isChannel(conversation) ? conversation.channel : undefined" :key="message.id"
|
||||
:classes="message == conversation.lastRead ? 'last-read' : ''">
|
||||
</message-view>
|
||||
<span v-if="message.sfc && message.sfc.action == 'report'" :key="'r' + message.id">
|
||||
<span v-if="hasSFC(message) && message.sfc.action === 'report'" :key="'r' + message.id">
|
||||
<a :href="'https://www.f-list.net/fchat/getLog.php?log=' + message.sfc.logid"
|
||||
v-if="message.sfc.logid" target="_blank">{{l('events.report.viewLog')}}</a>
|
||||
<span v-else>{{l('events.report.noLog')}}</span>
|
||||
<span v-show="!message.sfc.confirmed">
|
||||
| <a href="#" @click.prevent="acceptReport(message.sfc)">{{l('events.report.confirm')}}</a>
|
||||
| <a href="#" @click.prevent="message.sfc.action === 'report' && acceptReport(message.sfc)">{{l('events.report.confirm')}}</a>
|
||||
</span>
|
||||
</span>
|
||||
</template>
|
||||
</div>
|
||||
<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">
|
||||
:classes="'form-control chat-text-box' + (isChannel(conversation) && conversation.isSendingAds ? ' ads-text-box' : '')"
|
||||
:hasToolbar="settings.bbCodeBar" ref="textBox" style="position:relative;margin-top:5px"
|
||||
:maxlength="isChannel(conversation) || isPrivate(conversation) ? conversation.maxMessageLength : undefined">
|
||||
<span v-if="isPrivate(conversation) && conversation.typingStatus !== 'clear'" class="chat-info-text">
|
||||
{{l('chat.typing.' + conversation.typingStatus, conversation.name)}}
|
||||
</span>
|
||||
<div v-show="conversation.infoText" class="chat-info-text">
|
||||
|
@ -100,10 +101,10 @@
|
|||
<span class="redText" style="flex:1;margin-left:5px">{{conversation.errorText}}</span>
|
||||
</div>
|
||||
<div class="bbcode-editor-controls">
|
||||
<div v-show="conversation.maxMessageLength" style="margin-right:5px">
|
||||
<div v-if="isChannel(conversation) || isPrivate(conversation)" style="margin-right:5px">
|
||||
{{getByteLength(conversation.enteredText)}} / {{conversation.maxMessageLength}}
|
||||
</div>
|
||||
<ul class="nav nav-pills send-ads-switcher" v-if="conversation.channel"
|
||||
<ul class="nav nav-pills send-ads-switcher" v-if="isChannel(conversation)"
|
||||
style="position:relative;z-index:10;margin-right:5px">
|
||||
<li class="nav-item">
|
||||
<a href="#" :class="{active: !conversation.isSendingAds, disabled: conversation.channel.mode != 'both'}"
|
||||
|
@ -120,14 +121,13 @@
|
|||
<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>
|
||||
<manage-channel ref="manageDialog" v-if="isChannel(conversation)" :channel="conversation.channel"></manage-channel>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {Component, Hook, Prop, Watch} from '@f-list/vue-ts';
|
||||
import Vue from 'vue';
|
||||
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';
|
||||
|
@ -177,7 +177,10 @@
|
|||
ignoreScroll = false;
|
||||
adCountdown = 0;
|
||||
adsMode = l('channel.mode.ads');
|
||||
isChannel = Conversation.isChannel;
|
||||
isPrivate = Conversation.isPrivate;
|
||||
|
||||
@Hook('mounted')
|
||||
mounted(): void {
|
||||
this.extraButtons = [{
|
||||
title: 'Help\n\nClick this button for a quick overview of slash commands.',
|
||||
|
@ -218,6 +221,7 @@
|
|||
});
|
||||
}
|
||||
|
||||
@Hook('destroyed')
|
||||
destroyed(): void {
|
||||
window.removeEventListener('resize', this.resizeHandler);
|
||||
window.removeEventListener('keydown', this.keydownHandler);
|
||||
|
@ -234,7 +238,7 @@
|
|||
return core.conversations.selectedConversation;
|
||||
}
|
||||
|
||||
get messages(): ReadonlyArray<Conversation.Message> {
|
||||
get messages(): ReadonlyArray<Conversation.Message | Conversation.SFCMessage> {
|
||||
if(this.search === '') return this.conversation.messages;
|
||||
const filter = new RegExp(this.search.replace(/[^\w]/gi, '\\$&'), 'i');
|
||||
return this.conversation.messages.filter((x) => filter.test(x.text));
|
||||
|
@ -361,6 +365,22 @@
|
|||
}
|
||||
}
|
||||
|
||||
showLogs(): void {
|
||||
(<Logs>this.$refs['logsDialog']).show();
|
||||
}
|
||||
|
||||
showSettings(): void {
|
||||
(<ConversationSettings>this.$refs['settingsDialog']).show();
|
||||
}
|
||||
|
||||
showManage(): void {
|
||||
(<ManageChannel>this.$refs['manageDialog']).show();
|
||||
}
|
||||
|
||||
hasSFC(message: Conversation.Message): message is Conversation.SFCMessage {
|
||||
return (<Partial<Conversation.SFCMessage>>message).sfc !== undefined;
|
||||
}
|
||||
|
||||
get characterImage(): string {
|
||||
return characterImage(this.conversation.name);
|
||||
}
|
||||
|
|
168
chat/Logs.vue
168
chat/Logs.vue
|
@ -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.allDates')}}</option>
|
||||
<option :value="undefined">{{l('logs.allDates')}}</option>
|
||||
<option v-for="date in dates" :value="date.getTime()">{{formatDate(date)}}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
@ -47,8 +47,8 @@
|
|||
class="fa fa-download"></span></button>
|
||||
</div>
|
||||
</div>
|
||||
<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 class="messages messages-both" style="overflow:auto;overscroll-behavior:none;" ref="messages" tabindex="-1" @scroll="onMessagesScroll">
|
||||
<message-view v-for="message in displayedMessages" :message="message" :key="message.id" :logs="true"></message-view>
|
||||
</div>
|
||||
<div class="input-group" style="flex-shrink:0">
|
||||
<div class="input-group-prepend">
|
||||
|
@ -60,9 +60,8 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {Component, Hook, Prop, Watch} from '@f-list/vue-ts';
|
||||
import {format} from 'date-fns';
|
||||
import Component from 'vue-class-component';
|
||||
import {Prop, Watch} from 'vue-property-decorator';
|
||||
import CustomDialog from '../components/custom_dialog';
|
||||
import FilterableSelect from '../components/FilterableSelect.vue';
|
||||
import Modal from '../components/Modal.vue';
|
||||
|
@ -86,13 +85,12 @@
|
|||
components: {modal: Modal, 'message-view': MessageView, 'filterable-select': FilterableSelect}
|
||||
})
|
||||
export default class Logs extends CustomDialog {
|
||||
//tslint:disable:no-null-keyword
|
||||
@Prop()
|
||||
readonly conversation?: Conversation;
|
||||
selectedConversation: LogInterface.Conversation | null = null;
|
||||
dates: ReadonlyArray<Date> = [];
|
||||
selectedDate: string | null = null;
|
||||
conversations: LogInterface.Conversation[] = [];
|
||||
selectedConversation: LogInterface.Conversation | undefined;
|
||||
dates: ReadonlyArray<Date> = [];
|
||||
selectedDate: string | undefined;
|
||||
l = l;
|
||||
filter = '';
|
||||
messages: ReadonlyArray<Conversation.Message> = [];
|
||||
|
@ -103,6 +101,14 @@
|
|||
showFilters = true;
|
||||
canZip = core.logs.canZip;
|
||||
dateOffset = -1;
|
||||
windowStart = 0;
|
||||
windowEnd = 0;
|
||||
resizeListener = async() => this.onMessagesScroll();
|
||||
|
||||
get displayedMessages(): ReadonlyArray<Conversation.Message> {
|
||||
if(this.selectedDate !== undefined) return this.filteredMessages;
|
||||
return this.filteredMessages.slice(this.windowStart, this.windowEnd);
|
||||
}
|
||||
|
||||
get filteredMessages(): ReadonlyArray<Conversation.Message> {
|
||||
if(this.filter.length === 0) return this.messages;
|
||||
|
@ -111,35 +117,42 @@
|
|||
(x) => filter.test(x.text) || x.type !== Conversation.Message.Type.Event && filter.test(x.sender.name));
|
||||
}
|
||||
|
||||
@Hook('mounted')
|
||||
async mounted(): Promise<void> {
|
||||
this.characters = await core.logs.getAvailableCharacters();
|
||||
await this.loadCharacter();
|
||||
return this.conversationChanged();
|
||||
window.addEventListener('resize', this.resizeListener);
|
||||
}
|
||||
|
||||
@Hook('beforeDestroy')
|
||||
beforeDestroy(): void {
|
||||
window.removeEventListener('resize', this.resizeListener);
|
||||
}
|
||||
|
||||
async loadCharacter(): Promise<void> {
|
||||
this.selectedConversation = undefined;
|
||||
return this.loadConversations();
|
||||
}
|
||||
|
||||
async loadConversations(): Promise<void> {
|
||||
if(this.selectedCharacter === '') return;
|
||||
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.selectedConversation = null;
|
||||
}
|
||||
|
||||
filterConversation(filter: RegExp, conversation: {key: string, name: string}): boolean {
|
||||
async loadDates(): Promise<void> {
|
||||
this.dates = this.selectedConversation === undefined ? [] :
|
||||
(await core.logs.getLogDates(this.selectedCharacter, this.selectedConversation.key)).slice().reverse();
|
||||
}
|
||||
|
||||
filterConversation(filter: RegExp, conversation: LogInterface.Conversation): boolean {
|
||||
return filter.test(conversation.name);
|
||||
}
|
||||
|
||||
@Watch('conversation')
|
||||
async conversationChanged(): Promise<void> {
|
||||
if(this.conversation === undefined) return;
|
||||
//tslint:disable-next-line:strict-boolean-expressions
|
||||
this.selectedConversation = this.conversations.filter((x) => x.key === this.conversation!.key)[0] || null;
|
||||
}
|
||||
|
||||
@Watch('selectedConversation')
|
||||
async conversationSelected(): Promise<void> {
|
||||
this.dates = this.selectedConversation === null ? [] :
|
||||
(await core.logs.getLogDates(this.selectedCharacter, this.selectedConversation.key)).slice().reverse();
|
||||
this.selectedDate = null;
|
||||
async conversationSelected(oldValue: Conversation | undefined, newValue: Conversation | undefined): Promise<void> {
|
||||
if(oldValue !== undefined && newValue !== undefined && oldValue.key === newValue.key) return;
|
||||
await this.loadDates();
|
||||
this.selectedDate = undefined;
|
||||
this.dateOffset = -1;
|
||||
this.filter = '';
|
||||
await this.loadMessages();
|
||||
|
@ -147,9 +160,18 @@
|
|||
|
||||
@Watch('filter')
|
||||
onFilterChanged(): void {
|
||||
if(this.selectedDate === undefined) {
|
||||
this.windowEnd = this.filteredMessages.length;
|
||||
this.windowStart = this.windowEnd - 50;
|
||||
}
|
||||
this.$nextTick(async() => this.onMessagesScroll());
|
||||
}
|
||||
|
||||
@Watch('showFilters')
|
||||
async onFilterToggle(): Promise<void> {
|
||||
return this.onMessagesScroll();
|
||||
}
|
||||
|
||||
download(file: string, logs: string): void {
|
||||
const a = document.createElement('a');
|
||||
a.href = logs;
|
||||
|
@ -164,13 +186,13 @@
|
|||
}
|
||||
|
||||
downloadDay(): void {
|
||||
if(this.selectedConversation === null || this.selectedDate === null || this.messages.length === 0) return;
|
||||
if(this.selectedConversation === undefined || this.selectedDate === undefined || this.messages.length === 0) return;
|
||||
const name = `${this.selectedConversation.name}-${formatDate(new Date(this.selectedDate))}.txt`;
|
||||
this.download(name, `data:${encodeURIComponent(name)},${encodeURIComponent(getLogs(this.messages))}`);
|
||||
}
|
||||
|
||||
async downloadConversation(): Promise<void> {
|
||||
if(this.selectedConversation === null) return;
|
||||
if(this.selectedConversation === undefined) return;
|
||||
const zip = new Zip();
|
||||
for(const date of this.dates) {
|
||||
const messages = await core.logs.getLogs(this.selectedCharacter, this.selectedConversation.key, date);
|
||||
|
@ -195,14 +217,17 @@
|
|||
|
||||
async onOpen(): Promise<void> {
|
||||
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();
|
||||
await this.loadConversations();
|
||||
if(this.conversation !== undefined)
|
||||
this.selectedConversation = this.conversations.filter((x) => x.key === this.conversation!.key)[0];
|
||||
else {
|
||||
await this.loadDates();
|
||||
await this.loadMessages();
|
||||
}
|
||||
}
|
||||
this.keyDownListener = (e) => {
|
||||
if(getKey(e) === Keys.KeyA && (e.ctrlKey || e.metaKey) && !e.altKey && !e.shiftKey) {
|
||||
if((<HTMLElement>e.target).tagName.toLowerCase() === 'input') return;
|
||||
e.preventDefault();
|
||||
const selection = document.getSelection();
|
||||
if(selection === null) return;
|
||||
|
@ -223,36 +248,69 @@
|
|||
window.removeEventListener('keydown', this.keyDownListener!);
|
||||
}
|
||||
|
||||
async loadMessages(): Promise<ReadonlyArray<Conversation.Message>> {
|
||||
if(this.selectedConversation === null)
|
||||
return this.messages = [];
|
||||
if(this.selectedDate !== null) {
|
||||
async loadMessages(): Promise<void> {
|
||||
if(this.selectedConversation === undefined) this.messages = [];
|
||||
else if(this.selectedDate !== undefined) {
|
||||
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 = await core.logs.getLogs(this.selectedCharacter, this.selectedConversation.key, new Date(this.selectedDate));
|
||||
} else if(this.dateOffset === -1) {
|
||||
this.messages = [];
|
||||
this.dateOffset = 0;
|
||||
}
|
||||
this.$nextTick(async() => this.onMessagesScroll());
|
||||
return this.messages;
|
||||
this.windowStart = 0;
|
||||
this.windowEnd = 0;
|
||||
this.lastScroll = -1;
|
||||
this.lockScroll = false;
|
||||
this.$nextTick(async() => this.onMessagesScroll());
|
||||
} else return this.onMessagesScroll();
|
||||
}
|
||||
|
||||
async onMessagesScroll(): Promise<void> {
|
||||
lockScroll = false;
|
||||
lastScroll = -1;
|
||||
|
||||
async onMessagesScroll(ev?: Event): 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;
|
||||
const firstMessage = <HTMLElement>list.firstElementChild!;
|
||||
this.$nextTick(() => {
|
||||
if(list.offsetHeight === list.scrollHeight) return this.onMessagesScroll();
|
||||
else if(noOverflow) setTimeout(() => list.scrollTop = list.scrollHeight, 0);
|
||||
else setTimeout(() => list.scrollTop = firstMessage.offsetTop, 0);
|
||||
});
|
||||
if(this.lockScroll) return;
|
||||
if(list === undefined || ev !== undefined && Math.abs(list.scrollTop - this.lastScroll) < 50) return;
|
||||
this.lockScroll = true;
|
||||
function getTop(index: number): number {
|
||||
return (<HTMLElement>list!.children[index]).offsetTop;
|
||||
}
|
||||
while(this.selectedConversation !== undefined && this.selectedDate === undefined && this.dialog.isShown) {
|
||||
const oldHeight = list.scrollHeight, oldTop = list.scrollTop;
|
||||
const oldFirst = this.displayedMessages[0];
|
||||
const oldEnd = this.windowEnd;
|
||||
const length = this.displayedMessages.length;
|
||||
const oldTotal = this.filteredMessages.length;
|
||||
let loaded = false;
|
||||
if(length <= 20 || getTop(20) > list.scrollTop)
|
||||
this.windowStart -= 50;
|
||||
else if(length > 100 && getTop(100) < list.scrollTop)
|
||||
this.windowStart += 50;
|
||||
else if(length >= 100 && getTop(length - 100) > list.scrollTop + list.offsetHeight)
|
||||
this.windowEnd -= 50;
|
||||
else if(getTop(length - 20) < list.scrollTop + list.offsetHeight)
|
||||
this.windowEnd += 50;
|
||||
if(this.windowStart <= 0 && this.dateOffset < this.dates.length) {
|
||||
const messages = await core.logs.getLogs(this.selectedCharacter, this.selectedConversation.key,
|
||||
this.dates[this.dateOffset++]);
|
||||
this.messages = messages.concat(this.messages);
|
||||
const addedTotal = this.filteredMessages.length - oldTotal;
|
||||
this.windowStart += addedTotal;
|
||||
this.windowEnd += addedTotal;
|
||||
loaded = true;
|
||||
}
|
||||
this.windowStart = Math.max(this.windowStart, 0);
|
||||
this.windowEnd = Math.min(this.windowEnd, this.filteredMessages.length);
|
||||
if(this.displayedMessages[0] !== oldFirst) {
|
||||
list.style.overflow = 'hidden';
|
||||
await this.$nextTick();
|
||||
list.scrollTop = oldTop + list.scrollHeight - oldHeight;
|
||||
list.style.overflow = 'auto';
|
||||
} else if(this.windowEnd === oldEnd && !loaded) break;
|
||||
else await this.$nextTick();
|
||||
}
|
||||
this.lastScroll = list.scrollTop;
|
||||
this.lockScroll = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -38,8 +38,7 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Component from 'vue-class-component';
|
||||
import {Prop} from 'vue-property-decorator';
|
||||
import {Component, Prop} from '@f-list/vue-ts';
|
||||
import CustomDialog from '../components/custom_dialog';
|
||||
import Modal from '../components/Modal.vue';
|
||||
import {Editor} from './bbcode';
|
||||
|
|
|
@ -1,18 +1,25 @@
|
|||
<template>
|
||||
<modal :buttons="false" :action="l('chat.recentConversations')" dialogClass="w-100 modal-lg">
|
||||
<div style="display:flex; flex-direction:column; max-height:500px; flex-wrap:wrap;">
|
||||
<div v-for="recent in recentConversations" style="margin: 3px;">
|
||||
<user-view v-if="recent.character" :character="getCharacter(recent.character)"></user-view>
|
||||
<channel-view v-else :id="recent.channel" :text="recent.name"></channel-view>
|
||||
<tabs style="flex-shrink:0;margin-bottom:10px" v-model="selectedTab"
|
||||
:tabs="[l('chat.pms'), l('chat.channels')]"></tabs>
|
||||
<div>
|
||||
<div v-show="selectedTab === '0'" class="recent-conversations">
|
||||
<user-view v-for="recent in recentPrivate" v-if="recent.character"
|
||||
:key="recent.character" :character="getCharacter(recent.character)"></user-view>
|
||||
</div>
|
||||
<div v-show="selectedTab === '1'" class="recent-conversations">
|
||||
<channel-view v-for="recent in recentChannels" :key="recent.channel" :id="recent.channel"
|
||||
:text="recent.name"></channel-view>
|
||||
</div>
|
||||
</div>
|
||||
</modal>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Component from 'vue-class-component';
|
||||
import {Component} from '@f-list/vue-ts';
|
||||
import CustomDialog from '../components/custom_dialog';
|
||||
import Modal from '../components/Modal.vue';
|
||||
import Tabs from '../components/tabs';
|
||||
import ChannelView from './ChannelTagView.vue';
|
||||
import core from './core';
|
||||
import {Character, Conversation} from './interfaces';
|
||||
|
@ -20,17 +27,34 @@
|
|||
import UserView from './user_view';
|
||||
|
||||
@Component({
|
||||
components: {'user-view': UserView, 'channel-view': ChannelView, modal: Modal}
|
||||
components: {'user-view': UserView, 'channel-view': ChannelView, modal: Modal, tabs: Tabs}
|
||||
})
|
||||
export default class RecentConversations extends CustomDialog {
|
||||
l = l;
|
||||
selectedTab = '0';
|
||||
|
||||
get recentConversations(): ReadonlyArray<Conversation.RecentConversation> {
|
||||
get recentPrivate(): ReadonlyArray<Conversation.RecentPrivateConversation> {
|
||||
return core.conversations.recent;
|
||||
}
|
||||
|
||||
get recentChannels(): ReadonlyArray<Conversation.RecentChannelConversation> {
|
||||
return core.conversations.recentChannels;
|
||||
}
|
||||
|
||||
getCharacter(name: string): Character {
|
||||
return core.characters.get(name);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.recent-conversations {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-height: 500px;
|
||||
flex-wrap: wrap;
|
||||
& > * {
|
||||
margin: 3px;
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -1,19 +1,21 @@
|
|||
<template>
|
||||
<modal :action="l('chat.report')" @submit.prevent="submit" :disabled="submitting">
|
||||
<modal :action="l('chat.report')" @submit.prevent="submit()" :disabled="submitting" dialogClass="modal-lg">
|
||||
<div class="alert alert-danger" v-show="error">{{error}}</div>
|
||||
<h4>{{reporting}}</h4>
|
||||
<span v-show="!character">{{l('chat.report.channel.description')}}</span>
|
||||
<div ref="caption"></div>
|
||||
<br/>
|
||||
<div class="form-group">
|
||||
<label>{{l('chat.report.text')}}</label>
|
||||
<h6>{{l('chat.report.conversation')}}</h6>
|
||||
<p>{{conversation}}</p>
|
||||
<h6>{{l('chat.report.reporting')}}</h6>
|
||||
<p>{{character ? character.name : l('chat.report.general')}}</p>
|
||||
<h6>{{l('chat.report.text')}}</h6>
|
||||
<textarea class="form-control" v-model="text"></textarea>
|
||||
</div>
|
||||
</modal>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Component from 'vue-class-component';
|
||||
import {Component, Hook} from '@f-list/vue-ts';
|
||||
import CustomDialog from '../components/custom_dialog';
|
||||
import Modal from '../components/Modal.vue';
|
||||
import BBCodeParser, {BBCodeElement} from './bbcode';
|
||||
|
@ -26,35 +28,31 @@
|
|||
components: {modal: Modal}
|
||||
})
|
||||
export default class ReportDialog extends CustomDialog {
|
||||
//tslint:disable:no-null-keyword
|
||||
character: Character | null = null;
|
||||
character: Character | undefined;
|
||||
text = '';
|
||||
l = l;
|
||||
error = '';
|
||||
submitting = false;
|
||||
|
||||
@Hook('mounted')
|
||||
mounted(): void {
|
||||
(<Element>this.$refs['caption']).appendChild(new BBCodeParser().parseEverything(l('chat.report.description')));
|
||||
}
|
||||
|
||||
@Hook('beforeDestroy')
|
||||
beforeDestroy(): void {
|
||||
(<BBCodeElement>(<Element>this.$refs['caption']).firstChild).cleanup!();
|
||||
}
|
||||
|
||||
get reporting(): string {
|
||||
const conversation = core.conversations.selectedConversation;
|
||||
const isChannel = !Conversation.isPrivate(conversation);
|
||||
if(isChannel && this.character === null) return l('chat.report.channel', conversation.name);
|
||||
if(this.character === null) return '';
|
||||
const key = `chat.report.${(isChannel ? 'channel.user' : 'private')}`;
|
||||
return l(key, this.character.name, conversation.name);
|
||||
get conversation(): string {
|
||||
return core.conversations.selectedConversation.name;
|
||||
}
|
||||
|
||||
report(character?: Character): void {
|
||||
this.error = '';
|
||||
this.text = '';
|
||||
const current = core.conversations.selectedConversation;
|
||||
this.character = character !== undefined ? character : Conversation.isPrivate(current) ? current.character : null;
|
||||
this.character = character !== undefined ? character : Conversation.isPrivate(current) ? current.character : undefined;
|
||||
this.show();
|
||||
}
|
||||
|
||||
|
@ -64,7 +62,7 @@
|
|||
const log = conversation.reportMessages.map((x) => messageToString(x));
|
||||
const tab = (Conversation.isChannel(conversation) ? `${conversation.name} (${conversation.channel.id})`
|
||||
: Conversation.isPrivate(conversation) ? `Conversation with ${conversation.name}` : 'Console');
|
||||
const text = (this.character !== null ? `Reporting user: [user]${this.character.name}[/user] | ` : '') + this.text;
|
||||
const text = (this.character !== undefined ? `Reporting user: [user]${this.character.name}[/user] | ` : '') + this.text;
|
||||
const data = {
|
||||
character: core.connection.character,
|
||||
reportText: this.text,
|
||||
|
@ -73,10 +71,10 @@
|
|||
text: true,
|
||||
reportUser: <string | undefined>undefined
|
||||
};
|
||||
if(this.character !== null) data.reportUser = this.character.name;
|
||||
if(this.character !== undefined) data.reportUser = this.character.name;
|
||||
try {
|
||||
this.submitting = true;
|
||||
const report = <{log_id?: number}>(await core.connection.queryApi('report-submit.php', data));
|
||||
const report = (await core.connection.queryApi<{log_id?: number}>('report-submit.php', data));
|
||||
//tslint:disable-next-line:strict-boolean-expressions
|
||||
if(!report.log_id) return;
|
||||
core.connection.send('SFC', {action: 'report', logid: report.log_id, report: text, tab: conversation.name});
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
<template>
|
||||
<modal :action="l('settings.action')" @submit="submit" @close="init()" id="settings" dialogClass="w-100">
|
||||
<modal :action="l('settings.action')" @submit="submit" @open="load()" id="settings" dialogClass="w-100">
|
||||
<tabs style="flex-shrink:0;margin-bottom:10px" v-model="selectedTab"
|
||||
:tabs="[l('settings.tabs.general'), l('settings.tabs.notifications'), l('settings.tabs.import')]"></tabs>
|
||||
<div v-show="selectedTab == 0">
|
||||
:tabs="[l('settings.tabs.general'), l('settings.tabs.notifications'), l('settings.tabs.hideAds'), l('settings.tabs.import')]"></tabs>
|
||||
<div v-show="selectedTab === '0'">
|
||||
<div class="form-group">
|
||||
<label class="control-label" for="disallowedTags">{{l('settings.disallowedTags')}}</label>
|
||||
<input id="disallowedTags" class="form-control" v-model="disallowedTags"/>
|
||||
|
@ -70,7 +70,7 @@
|
|||
<input id="fontSize" type="number" min="10" max="24" class="form-control" v-model="fontSize"/>
|
||||
</div>
|
||||
</div>
|
||||
<div v-show="selectedTab == 1">
|
||||
<div v-show="selectedTab === '1'">
|
||||
<div class="form-group">
|
||||
<label class="control-label" for="playSound">
|
||||
<input type="checkbox" id="playSound" v-model="playSound"/>
|
||||
|
@ -118,7 +118,16 @@
|
|||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div v-show="selectedTab == 2" style="display:flex;padding-top:10px">
|
||||
<div v-show="selectedTab === '2'">
|
||||
<template v-if="hidden.length">
|
||||
<div v-for="(user, i) in hidden">
|
||||
<span class="fa fa-times" style="cursor:pointer" @click.stop="hidden.splice(i, 1)"></span>
|
||||
{{user}}
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>{{l('settings.hideAds.empty')}}</template>
|
||||
</div>
|
||||
<div v-show="selectedTab === '3'" style="display:flex;padding-top:10px">
|
||||
<select id="import" class="form-control" v-model="importCharacter" style="flex:1;margin-right:10px">
|
||||
<option value="">{{l('settings.import.selectCharacter')}}</option>
|
||||
<option v-for="character in availableImports" :value="character">{{character}}</option>
|
||||
|
@ -129,7 +138,7 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Component from 'vue-class-component';
|
||||
import {Component} from '@f-list/vue-ts';
|
||||
import CustomDialog from '../components/custom_dialog';
|
||||
import Modal from '../components/Modal.vue';
|
||||
import Tabs from '../components/tabs';
|
||||
|
@ -166,16 +175,7 @@
|
|||
colorBookmarks!: boolean;
|
||||
bbCodeBar!: boolean;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.init();
|
||||
}
|
||||
|
||||
async created(): Promise<void> {
|
||||
this.availableImports = (await core.settingsStore.getAvailableCharacters()).filter((x) => x !== core.connection.character);
|
||||
}
|
||||
|
||||
init = function(this: SettingsView): void {
|
||||
async load(): Promise<void> {
|
||||
const settings = core.state.settings;
|
||||
this.playSound = settings.playSound;
|
||||
this.clickOpensMessage = settings.clickOpensMessage;
|
||||
|
@ -197,7 +197,8 @@
|
|||
this.enterSend = settings.enterSend;
|
||||
this.colorBookmarks = settings.colorBookmarks;
|
||||
this.bbCodeBar = settings.bbCodeBar;
|
||||
};
|
||||
this.availableImports = (await core.settingsStore.getAvailableCharacters()).filter((x) => x !== core.connection.character);
|
||||
}
|
||||
|
||||
async doImport(): Promise<void> {
|
||||
if(!confirm(l('settings.import.confirm', this.importCharacter, core.connection.character))) return;
|
||||
|
@ -209,9 +210,11 @@
|
|||
await importKey('pinned');
|
||||
await importKey('modes');
|
||||
await importKey('conversationSettings');
|
||||
this.init();
|
||||
core.reloadSettings();
|
||||
core.conversations.reloadSettings();
|
||||
core.connection.close(false);
|
||||
}
|
||||
|
||||
get hidden(): string[] {
|
||||
return core.state.hiddenUsers;
|
||||
}
|
||||
|
||||
async submit(): Promise<void> {
|
||||
|
|
|
@ -15,9 +15,8 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {Component, Prop, Watch} from '@f-list/vue-ts';
|
||||
import Vue from 'vue';
|
||||
import Component from 'vue-class-component';
|
||||
import {Prop, Watch} from 'vue-property-decorator';
|
||||
|
||||
@Component
|
||||
export default class Sidebar extends Vue {
|
||||
|
|
|
@ -21,7 +21,7 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Component from 'vue-class-component';
|
||||
import {Component} from '@f-list/vue-ts';
|
||||
import CustomDialog from '../components/custom_dialog';
|
||||
import Dropdown from '../components/Dropdown.vue';
|
||||
import Modal from '../components/Modal.vue';
|
||||
|
@ -36,16 +36,15 @@
|
|||
components: {modal: Modal, editor: Editor, dropdown: Dropdown}
|
||||
})
|
||||
export default class StatusSwitcher extends CustomDialog {
|
||||
//tslint:disable:no-null-keyword
|
||||
selectedStatus: Character.Status | null = null;
|
||||
enteredText: string | null = null;
|
||||
selectedStatus: Character.Status | undefined;
|
||||
enteredText: string | undefined;
|
||||
statuses = userStatuses;
|
||||
l = l;
|
||||
getByteLength = getByteLength;
|
||||
getStatusIcon = getStatusIcon;
|
||||
|
||||
get status(): Character.Status {
|
||||
return this.selectedStatus !== null ? this.selectedStatus : this.character.status;
|
||||
return this.selectedStatus !== undefined ? this.selectedStatus : this.character.status;
|
||||
}
|
||||
|
||||
set status(status: Character.Status) {
|
||||
|
@ -53,7 +52,7 @@
|
|||
}
|
||||
|
||||
get text(): string {
|
||||
return this.enteredText !== null ? this.enteredText : this.character.statusText;
|
||||
return this.enteredText !== undefined ? this.enteredText : this.character.statusText;
|
||||
}
|
||||
|
||||
set text(text: string) {
|
||||
|
@ -69,8 +68,8 @@
|
|||
}
|
||||
|
||||
reset(): void {
|
||||
this.selectedStatus = null;
|
||||
this.enteredText = null;
|
||||
this.selectedStatus = undefined;
|
||||
this.enteredText = undefined;
|
||||
}
|
||||
}
|
||||
</script>
|
|
@ -1,7 +1,7 @@
|
|||
<template>
|
||||
<sidebar id="user-list" :label="l('users.title')" icon="fa-users" :right="true" :open="expanded">
|
||||
<tabs style="flex-shrink:0" :tabs="channel ? [l('users.friends'), l('users.members')] : [l('users.friends')]" v-model="tab"></tabs>
|
||||
<div class="users" style="padding-left:10px" v-show="tab == 0">
|
||||
<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" :bookmark="false"></user>
|
||||
|
@ -11,7 +11,7 @@
|
|||
<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">
|
||||
<div v-if="channel" style="padding-left:5px;flex:1;display:flex;flex-direction:column" v-show="tab === '1'">
|
||||
<div class="users" style="flex:1;padding-left:5px">
|
||||
<h4>{{l('users.memberCount', channel.sortedMembers.length)}}</h4>
|
||||
<div v-for="member in filteredMembers" :key="member.character.name">
|
||||
|
@ -29,8 +29,8 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {Component} from '@f-list/vue-ts';
|
||||
import Vue from 'vue';
|
||||
import Component from 'vue-class-component';
|
||||
import Tabs from '../components/tabs';
|
||||
import core from './core';
|
||||
import {Channel, Character, Conversation} from './interfaces';
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
{{l('status.' + character.status)}}
|
||||
</div>
|
||||
<bbcode id="userMenuStatus" :text="character.statusText" v-show="character.statusText" class="list-group-item"
|
||||
style="max-height:200px;overflow:auto;clear:both"></bbcode>
|
||||
style="max-height:200px;overflow:auto;clear:both"></bbcode>
|
||||
<a tabindex="-1" :href="profileLink" target="_blank" v-if="showProfileFirst" class="list-group-item list-group-item-action">
|
||||
<span class="fa fa-fw fa-user"></span>{{l('user.profile')}}</a>
|
||||
<a tabindex="-1" href="#" @click.prevent="openConversation(true)" class="list-group-item list-group-item-action">
|
||||
|
@ -17,19 +17,19 @@
|
|||
<span class="fa fa-fw fa-plus"></span>{{l('user.message')}}</a>
|
||||
<a tabindex="-1" :href="profileLink" target="_blank" v-if="!showProfileFirst" class="list-group-item list-group-item-action">
|
||||
<span class="fa fa-fw fa-user"></span>{{l('user.profile')}}</a>
|
||||
<a tabindex="-1" href="#" @click.prevent="showMemo" class="list-group-item list-group-item-action">
|
||||
<a tabindex="-1" href="#" @click.prevent="showMemo()" class="list-group-item list-group-item-action">
|
||||
<span class="far fa-fw fa-sticky-note"></span>{{l('user.memo')}}</a>
|
||||
<a tabindex="-1" href="#" @click.prevent="setBookmarked" class="list-group-item list-group-item-action">
|
||||
<a tabindex="-1" href="#" @click.prevent="setBookmarked()" class="list-group-item list-group-item-action">
|
||||
<span class="far fa-fw fa-bookmark"></span>{{l('user.' + (character.isBookmarked ? 'unbookmark' : 'bookmark'))}}</a>
|
||||
<a tabindex="-1" href="#" @click.prevent="setIgnored" class="list-group-item list-group-item-action">
|
||||
<span class="fa fa-fw fa-minus-circle"></span>{{l('user.' + (character.isIgnored ? 'unignore' : 'ignore'))}}</a>
|
||||
<a tabindex="-1" href="#" @click.prevent="setHidden" class="list-group-item list-group-item-action" v-show="!isChatOp">
|
||||
<a tabindex="-1" href="#" @click.prevent="setHidden()" class="list-group-item list-group-item-action" v-show="!isChatOp">
|
||||
<span class="fa fa-fw fa-eye-slash"></span>{{l('user.' + (isHidden ? 'unhide' : 'hide'))}}</a>
|
||||
<a tabindex="-1" href="#" @click.prevent="report" class="list-group-item list-group-item-action">
|
||||
<a tabindex="-1" href="#" @click.prevent="report()" class="list-group-item list-group-item-action" style="border-top-width:1px">
|
||||
<span class="fa fa-fw fa-exclamation-triangle"></span>{{l('user.report')}}</a>
|
||||
<a tabindex="-1" href="#" @click.prevent="channelKick" class="list-group-item list-group-item-action" v-show="isChannelMod">
|
||||
<a tabindex="-1" href="#" @click.prevent="setIgnored()" class="list-group-item list-group-item-action">
|
||||
<span class="fa fa-fw fa-minus-circle"></span>{{l('user.' + (character.isIgnored ? 'unignore' : 'ignore'))}}</a>
|
||||
<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"
|
||||
<a tabindex="-1" href="#" @click.prevent="chatKick()" style="color:#f00" class="list-group-item list-group-item-action"
|
||||
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">
|
||||
|
@ -40,9 +40,8 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {Component, Prop} from '@f-list/vue-ts';
|
||||
import Vue from 'vue';
|
||||
import Component from 'vue-class-component';
|
||||
import {Prop} from 'vue-property-decorator';
|
||||
import Modal from '../components/Modal.vue';
|
||||
import {BBCodeView} from './bbcode';
|
||||
import {characterImage, errorToString, getByteLength, profileLink} from './common';
|
||||
|
@ -55,17 +54,16 @@
|
|||
components: {bbcode: BBCodeView, modal: Modal}
|
||||
})
|
||||
export default class UserMenu extends Vue {
|
||||
//tslint:disable:no-null-keyword
|
||||
@Prop({required: true})
|
||||
readonly reportDialog!: ReportDialog;
|
||||
l = l;
|
||||
showContextMenu = false;
|
||||
getByteLength = getByteLength;
|
||||
character: Character | null = null;
|
||||
character: Character | undefined;
|
||||
position = {left: '', top: ''};
|
||||
characterImage: string | null = null;
|
||||
characterImage: string | undefined;
|
||||
touchedElement: HTMLElement | undefined;
|
||||
channel: Channel | null = null;
|
||||
channel: Channel | undefined;
|
||||
memo = '';
|
||||
memoId = 0;
|
||||
memoLoading = false;
|
||||
|
@ -107,7 +105,7 @@
|
|||
this.memo = '';
|
||||
(<Modal>this.$refs['memo']).show();
|
||||
try {
|
||||
const memo = <{note: string | null, id: number}>await core.connection.queryApi('character-memo-get2.php',
|
||||
const memo = await core.connection.queryApi<{note: string | null, id: number}>('character-memo-get2.php',
|
||||
{target: this.character!.name});
|
||||
this.memoId = memo.id;
|
||||
this.memo = memo.note !== null ? memo.note : '';
|
||||
|
@ -123,7 +121,7 @@
|
|||
}
|
||||
|
||||
get isChannelMod(): boolean {
|
||||
if(this.channel === null) return false;
|
||||
if(this.channel === undefined) return false;
|
||||
if(core.characters.ownCharacter.isChatOp) return true;
|
||||
const member = this.channel.members[core.connection.character];
|
||||
return member !== undefined && member.rank > Channel.Rank.Member;
|
||||
|
@ -189,9 +187,9 @@
|
|||
}
|
||||
|
||||
private openMenu(touch: MouseEvent | Touch, character: Character, channel: Channel | undefined): void {
|
||||
this.channel = channel !== undefined ? channel : null;
|
||||
this.channel = channel;
|
||||
this.character = character;
|
||||
this.characterImage = null;
|
||||
this.characterImage = undefined;
|
||||
this.showContextMenu = true;
|
||||
this.position = {left: `${touch.clientX}px`, top: `${touch.clientY}px`};
|
||||
this.$nextTick(() => {
|
||||
|
@ -212,7 +210,7 @@
|
|||
}
|
||||
|
||||
#userMenu .list-group-item-action {
|
||||
border-top: 0;
|
||||
border-top-width: 0;
|
||||
z-index: -1;
|
||||
}
|
||||
</style>
|
|
@ -9,6 +9,10 @@ export default class Socket implements WebSocketConnection {
|
|||
this.socket = new WebSocket(Socket.host);
|
||||
}
|
||||
|
||||
get readyState(): WebSocketConnection.ReadyState {
|
||||
return this.socket.readyState;
|
||||
}
|
||||
|
||||
close(): void {
|
||||
this.socket.close();
|
||||
}
|
||||
|
|
|
@ -24,7 +24,6 @@ export const BBCodeView: Component = {
|
|||
if(element.cleanup !== undefined) element.cleanup();
|
||||
}
|
||||
};
|
||||
context.data.staticClass = `bbcode${context.data.staticClass !== undefined ? ` ${context.data.staticClass}` : ''}`;
|
||||
const vnode = createElement('span', context.data);
|
||||
vnode.key = context.props.text;
|
||||
return vnode;
|
||||
|
@ -84,18 +83,22 @@ export default class BBCodeParser extends CoreBBCodeParser {
|
|||
return img;
|
||||
}));
|
||||
this.addTag(new BBCodeTextTag('session', (parser, parent, param, content) => {
|
||||
const root = parser.createElement('span');
|
||||
const el = parser.createElement('span');
|
||||
parent.appendChild(el);
|
||||
parent.appendChild(root);
|
||||
root.appendChild(el);
|
||||
const view = new ChannelView({el, propsData: {id: content, text: param}});
|
||||
this.cleanup.push(view);
|
||||
return el;
|
||||
return root;
|
||||
}));
|
||||
this.addTag(new BBCodeTextTag('channel', (parser, parent, _, content) => {
|
||||
const root = parser.createElement('span');
|
||||
const el = parser.createElement('span');
|
||||
parent.appendChild(el);
|
||||
parent.appendChild(root);
|
||||
root.appendChild(el);
|
||||
const view = new ChannelView({el, propsData: {id: content, text: content}});
|
||||
this.cleanup.push(view);
|
||||
return el;
|
||||
return root;
|
||||
}));
|
||||
}
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@ import {queuedJoin} from '../fchat/channels';
|
|||
import {decodeHTML} from '../fchat/common';
|
||||
import {characterImage, ConversationSettings, EventMessage, Message, messageToString} from './common';
|
||||
import core from './core';
|
||||
import {Channel, Character, Connection, Conversation as Interfaces} from './interfaces';
|
||||
import {Channel, Character, Conversation as Interfaces} from './interfaces';
|
||||
import l from './localize';
|
||||
import {CommandContext, isAction, isCommand, isWarn, parse as parseCommand} from './slash_commands';
|
||||
import MessageType = Interfaces.Message.Type;
|
||||
|
@ -353,7 +353,8 @@ class State implements Interfaces.State {
|
|||
channelMap: {[key: string]: ChannelConversation | undefined} = {};
|
||||
consoleTab!: ConsoleConversation;
|
||||
selectedConversation: Conversation = this.consoleTab;
|
||||
recent: Interfaces.RecentConversation[] = [];
|
||||
recent: Interfaces.RecentPrivateConversation[] = [];
|
||||
recentChannels: Interfaces.RecentChannelConversation[] = [];
|
||||
pinned!: {channels: string[], private: string[]};
|
||||
settings!: {[key: string]: Interfaces.Settings};
|
||||
modes!: {[key: string]: Channel.Mode | undefined};
|
||||
|
@ -371,13 +372,18 @@ class State implements Interfaces.State {
|
|||
conv = new PrivateConversation(character);
|
||||
this.privateConversations.push(conv);
|
||||
this.privateMap[key] = conv;
|
||||
state.addRecent(conv); //tslint:disable-line:no-floating-promises
|
||||
const index = this.recent.findIndex((c) => c.character === conv!.name);
|
||||
if(index !== -1) this.recent.splice(index, 1);
|
||||
if(this.recent.length >= 50) this.recent.pop();
|
||||
this.recent.unshift({character: conv.name});
|
||||
core.settingsStore.set('recent', this.recent); //tslint:disable-line:no-floating-promises
|
||||
return conv;
|
||||
}
|
||||
|
||||
byKey(key: string): Conversation | undefined {
|
||||
if(key === '_') return this.consoleTab;
|
||||
return (key[0] === '#' ? this.channelMap : this.privateMap)[key];
|
||||
key = key.toLowerCase();
|
||||
return key[0] === '#' ? this.channelMap[key.substr(1)] : this.privateMap[key];
|
||||
}
|
||||
|
||||
async savePinned(): Promise<void> {
|
||||
|
@ -395,25 +401,6 @@ class State implements Interfaces.State {
|
|||
await core.settingsStore.set('conversationSettings', this.settings);
|
||||
}
|
||||
|
||||
async addRecent(conversation: Conversation): Promise<void> {
|
||||
const remove = <T extends Interfaces.RecentConversation>(predicate: (item: T) => boolean) => {
|
||||
for(let i = 0; i < this.recent.length; ++i)
|
||||
if(predicate(<T>this.recent[i])) {
|
||||
this.recent.splice(i, 1);
|
||||
break;
|
||||
}
|
||||
};
|
||||
if(Interfaces.isChannel(conversation)) {
|
||||
remove<Interfaces.RecentChannelConversation>((c) => c.channel === conversation.channel.id);
|
||||
this.recent.unshift({channel: conversation.channel.id, name: conversation.channel.name});
|
||||
} else {
|
||||
remove<Interfaces.RecentPrivateConversation>((c) => c.character === conversation.name);
|
||||
state.recent.unshift({character: conversation.name});
|
||||
}
|
||||
if(this.recent.length >= 50) this.recent.pop();
|
||||
await core.settingsStore.set('recent', this.recent);
|
||||
}
|
||||
|
||||
show(conversation: Conversation): void {
|
||||
this.selectedConversation.onHide();
|
||||
conversation.unread = Interfaces.UnreadState.None;
|
||||
|
@ -429,13 +416,14 @@ class State implements Interfaces.State {
|
|||
for(const conversation of this.privateConversations)
|
||||
conversation._isPinned = this.pinned.private.indexOf(conversation.name) !== -1;
|
||||
this.recent = await core.settingsStore.get('recent') || [];
|
||||
this.recentChannels = await core.settingsStore.get('recentChannels') || [];
|
||||
const settings = <{[key: string]: ConversationSettings}> await core.settingsStore.get('conversationSettings') || {};
|
||||
for(const key in settings) {
|
||||
const settingsItem = new ConversationSettings();
|
||||
for(const itemKey in settings[key])
|
||||
settingsItem[<keyof ConversationSettings>itemKey] = settings[key][<keyof ConversationSettings>itemKey];
|
||||
settings[key] = settingsItem;
|
||||
const conv = (key[0] === '#' ? this.channelMap : this.privateMap)[key];
|
||||
const conv = this.byKey(key);
|
||||
if(conv !== undefined) conv._settings = settingsItem;
|
||||
}
|
||||
this.settings = settings;
|
||||
|
@ -494,7 +482,11 @@ export default function(this: void): Interfaces.State {
|
|||
const conv = new ChannelConversation(channel);
|
||||
state.channelMap[channel.id] = conv;
|
||||
state.channelConversations.push(conv);
|
||||
await state.addRecent(conv);
|
||||
const index = state.recentChannels.findIndex((c) => c.channel === channel.id);
|
||||
if(index !== -1) state.recentChannels.splice(index, 1);
|
||||
if(state.recentChannels.length >= 50) state.recentChannels.pop();
|
||||
state.recentChannels.unshift({channel: channel.id, name: conv.channel.name});
|
||||
core.settingsStore.set('recentChannels', state.recentChannels); //tslint:disable-line:no-floating-promises
|
||||
} else {
|
||||
const conv = state.channelMap[channel.id];
|
||||
if(conv === undefined) return;
|
||||
|
@ -548,6 +540,8 @@ export default function(this: void): Interfaces.State {
|
|||
characterImage(data.character), 'attention');
|
||||
if(conversation !== state.selectedConversation || !state.windowFocused) conversation.unread = Interfaces.UnreadState.Mention;
|
||||
message.isHighlight = true;
|
||||
await state.consoleTab.addMessage(new EventMessage(l('events.highlight', `[user]${data.character}[/user]`, results[0],
|
||||
`[session=${conversation.name}]${data.channel}[/session]`), time));
|
||||
} else if(conversation.settings.notify === Interfaces.Setting.True) {
|
||||
await core.notifications.notify(conversation, conversation.name, messageToString(message),
|
||||
characterImage(data.character), 'attention');
|
||||
|
@ -655,7 +649,9 @@ export default function(this: void): Interfaces.State {
|
|||
|
||||
connection.onMessage('IGN', async(data, time) => {
|
||||
if(data.action !== 'add' && data.action !== 'delete') return;
|
||||
return addEventMessage(new EventMessage(l(`events.ignore_${data.action}`, data.character), time));
|
||||
const text = l(`events.ignore_${data.action}`, data.character);
|
||||
state.selectedConversation.infoText = text;
|
||||
return addEventMessage(new EventMessage(text, time));
|
||||
});
|
||||
connection.onMessage('RTB', async(data, time) => {
|
||||
let url = 'https://www.f-list.net/';
|
||||
|
@ -711,8 +707,7 @@ export default function(this: void): Interfaces.State {
|
|||
if(data.type === 'note')
|
||||
await core.notifications.notify(state.consoleTab, character, text, characterImage(character), 'newnote');
|
||||
});
|
||||
type SFCMessage = (Interfaces.Message & {sfc: Connection.ServerCommands['SFC'] & {confirmed?: true}});
|
||||
const sfcList: SFCMessage[] = [];
|
||||
const sfcList: Interfaces.SFCMessage[] = [];
|
||||
connection.onMessage('SFC', async(data, time) => {
|
||||
let text: string, message: Interfaces.Message;
|
||||
if(data.action === 'report') {
|
||||
|
@ -721,7 +716,7 @@ export default function(this: void): Interfaces.State {
|
|||
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;
|
||||
(<Interfaces.SFCMessage>message).sfc = data;
|
||||
} else {
|
||||
text = l('events.report.confirmed', `[user]${data.moderator}[/user]`, `[user]${data.character}[/user]`);
|
||||
for(const item of sfcList)
|
||||
|
|
|
@ -104,7 +104,6 @@ export interface Core {
|
|||
register(module: 'conversations', state: Conversation.State): void
|
||||
register(module: 'channels', state: Channel.State): void
|
||||
register(module: 'characters', state: Character.State): void
|
||||
reloadSettings(): void
|
||||
watch<T>(getter: (this: VueState) => T, callback: WatchHandler<T>): void
|
||||
}
|
||||
|
||||
|
|
|
@ -1,36 +1,34 @@
|
|||
//tslint:disable:no-shadowed-variable
|
||||
declare global {
|
||||
interface Function {
|
||||
//tslint:disable-next-line:ban-types no-any
|
||||
bind<T extends Function>(this: T, thisArg: any): T;
|
||||
//tslint:disable-next-line:ban-types no-any
|
||||
bind<T, TReturn>(this: (t: T) => TReturn, thisArg: any, arg: T): () => TReturn;
|
||||
}
|
||||
}
|
||||
import {Connection} from '../fchat';
|
||||
|
||||
import {Channel, Character} from '../fchat/interfaces';
|
||||
export {Connection, Channel, Character} from '../fchat/interfaces';
|
||||
export const userStatuses = ['online', 'looking', 'away', 'busy', 'dnd'];
|
||||
export const channelModes = ['chat', 'ads', 'both'];
|
||||
export const userStatuses: ReadonlyArray<Character.Status> = ['online', 'looking', 'away', 'busy', 'dnd'];
|
||||
export const channelModes: ReadonlyArray<Channel.Mode> = ['chat', 'ads', 'both'];
|
||||
|
||||
export namespace Conversation {
|
||||
export interface EventMessage {
|
||||
readonly type: Message.Type.Event,
|
||||
readonly text: string,
|
||||
interface BaseMessage {
|
||||
readonly id: number
|
||||
readonly type: Message.Type
|
||||
readonly text: string
|
||||
readonly time: Date
|
||||
readonly sender?: undefined
|
||||
}
|
||||
|
||||
export interface ChatMessage {
|
||||
readonly type: Message.Type,
|
||||
readonly sender: Character,
|
||||
readonly text: string,
|
||||
readonly time: Date
|
||||
export interface EventMessage extends BaseMessage {
|
||||
readonly type: Message.Type.Event
|
||||
}
|
||||
|
||||
export interface ChatMessage extends BaseMessage {
|
||||
readonly isHighlight: boolean
|
||||
readonly sender: Character
|
||||
}
|
||||
|
||||
export type Message = EventMessage | ChatMessage;
|
||||
|
||||
export interface SFCMessage extends EventMessage {
|
||||
sfc: Connection.ServerCommands['SFC'] & {confirmed?: true}
|
||||
}
|
||||
|
||||
export namespace Message {
|
||||
export enum Type {
|
||||
Message,
|
||||
|
@ -44,7 +42,6 @@ export namespace Conversation {
|
|||
|
||||
export type RecentChannelConversation = {readonly channel: string, readonly name: string};
|
||||
export type RecentPrivateConversation = {readonly character: string};
|
||||
export type RecentConversation = RecentChannelConversation | RecentPrivateConversation;
|
||||
|
||||
export type TypingStatus = 'typing' | 'paused' | 'clear';
|
||||
|
||||
|
@ -79,12 +76,12 @@ export namespace Conversation {
|
|||
readonly privateConversations: ReadonlyArray<PrivateConversation>
|
||||
readonly channelConversations: ReadonlyArray<ChannelConversation>
|
||||
readonly consoleTab: Conversation
|
||||
readonly recent: ReadonlyArray<RecentConversation>
|
||||
readonly recent: ReadonlyArray<RecentPrivateConversation>
|
||||
readonly recentChannels: ReadonlyArray<RecentChannelConversation>
|
||||
readonly selectedConversation: Conversation
|
||||
readonly hasNew: boolean;
|
||||
byKey(key: string): Conversation | undefined
|
||||
getPrivate(character: Character): PrivateConversation
|
||||
reloadSettings(): void
|
||||
}
|
||||
|
||||
export enum Setting {
|
||||
|
@ -142,7 +139,8 @@ export namespace Settings {
|
|||
pinned: {channels: string[], private: string[]},
|
||||
conversationSettings: {[key: string]: Conversation.Settings | undefined}
|
||||
modes: {[key: string]: Channel.Mode | undefined}
|
||||
recent: Conversation.RecentConversation[]
|
||||
recent: Conversation.RecentPrivateConversation[]
|
||||
recentChannels: Conversation.RecentChannelConversation[]
|
||||
hiddenUsers: string[]
|
||||
};
|
||||
|
||||
|
|
|
@ -91,6 +91,7 @@ const strings: {[key: string]: string | undefined} = {
|
|||
'logs.corruption.mobile': 'Log corruption has been detected. This is usually caused by a crash/force close or power loss mid-write. Will now attempt to fix corrupted logs.',
|
||||
'logs.corruption.mobile.success': 'Your logs have been fixed.',
|
||||
'logs.corruption.mobile.error': 'Unable to fix corrupted logs. Please clear the application data or reinstall the app.',
|
||||
'logs.corruption.web': 'Error reading logs from browser storage. If this issue persists, please clear your stored browser data for F-Chat.',
|
||||
'user.profile': 'Profile',
|
||||
'user.message': 'Open conversation',
|
||||
'user.messageJump': 'View conversation',
|
||||
|
@ -111,10 +112,9 @@ const strings: {[key: string]: string | undefined} = {
|
|||
'users.members': 'Members',
|
||||
'users.memberCount': '{0} Members',
|
||||
'chat.report': 'Alert Staff',
|
||||
'chat.report.description': `
|
||||
[color=red]Before you alert the moderators, PLEASE READ:[/color]
|
||||
'chat.report.description': `[color=red]Before you alert the moderators, PLEASE READ:[/color]
|
||||
If you're just having personal trouble with someone, right-click their name and ignore them.
|
||||
Please make sure what you're reporting is a violation of the site's [url=https://wiki.f-list.net/Code_of_Conduct]Code of Conduct[/url] otherwise nothing will be done.
|
||||
Please make sure what you're reporting is a violation of the site's [url=https://wiki.f-list.net/Code_of_Conduct]Code of Conduct[/url], otherwise nothing will be done.
|
||||
|
||||
This tool is intended for chat moderation. If you have a question, please visit our [url=https://wiki.f-list.net/Frequently_Asked_Questions]FAQ[/url] first, and if that doesn't help, join [session=Helpdesk]Helpdesk[/session] and ask your question there.
|
||||
|
||||
|
@ -123,25 +123,26 @@ If your problem lies anywhere outside of the chat, please send in a Ticket inste
|
|||
For a more comprehensive guide as how and when to report another user, please [url=https://wiki.f-list.net/How_to_Report_a_User]consult this page.[/url]
|
||||
|
||||
Please provide a brief summary of your problem and the rules that have been violated.
|
||||
[color=red]DO NOT PASTE LOGS INTO THIS FIELD.
|
||||
[color=red]DO NOT PASTE LOGS INTO THE "REPORT TEXT" FIELD.
|
||||
SELECT THE TAB YOU WISH TO REPORT, LOGS ARE AUTOMATICALLY ATTACHED[/color]`,
|
||||
'chat.report.channel.user': 'Reporting user {0} in channel {1}',
|
||||
'chat.report.channel': 'General report for channel {0}',
|
||||
'chat.report.channel.description': 'If you wish to report a specific user, please right-click them and select "Report".',
|
||||
'chat.report.private': 'Reporting private conversation with user {0}',
|
||||
'chat.report.conversation': 'Reporting tab',
|
||||
'chat.report.reporting': 'Reporting user',
|
||||
'chat.report.general': 'No one in particular. If you wish to report a specific user, please right-click them and select "Report".',
|
||||
'chat.report.text': 'Report text',
|
||||
'chat.recentConversations': 'Recent conversations',
|
||||
'settings.tabs.general': 'General',
|
||||
'settings.tabs.notifications': 'Notifications',
|
||||
'settings.tabs.hideAds': 'Hidden users',
|
||||
'settings.tabs.import': 'Import',
|
||||
'settings.open': 'Settings',
|
||||
'settings.action': 'Change settings',
|
||||
'settings.hideAds.empty': `You aren't currently hiding the ads of any users.`,
|
||||
'settings.import': 'Import settings',
|
||||
'settings.import.selectCharacter': 'Select a character',
|
||||
'settings.import.confirm': `You are importing settings from your character {0}.
|
||||
This will overwrite any and all settings, pinned conversations and conversation settings of character {1}.
|
||||
Logs and recent conversations will not be touched.
|
||||
You may need to log out and back in for some settings to take effect.
|
||||
You will be logged out. Once you log back in, the settings will have been imported.
|
||||
Are you sure?`,
|
||||
'settings.playSound': 'Play notification sounds',
|
||||
'settings.notifications': 'Show desktop/push notifications',
|
||||
|
@ -258,6 +259,7 @@ Once this process has started, do not interrupt it or your logs will get corrupt
|
|||
'events.ignore_add': 'You are now ignoring {0}\'s messages. Should they go around this by any means, please report it using the Alert Staff button.',
|
||||
'events.ignore_delete': '{0} is now allowed to send you messages again.',
|
||||
'events.uptime': 'Server has been running since {0}, with currently {1} channels and {2} users, a total of {3} accepted connections, and {4} maximum users.',
|
||||
'events.highlight': '{0} said "{1}" in {2}',
|
||||
'commands.unknown': 'Unknown command. For a list of valid commands, please click the ? button.',
|
||||
'commands.badContext': 'This command cannot be used here. Please use the Help (click the ? button) if you need further information.',
|
||||
'commands.tooFewParams': 'This command requires more parameters. Please use the Help (click the ? button) if you need further information.',
|
||||
|
|
|
@ -1,58 +1,57 @@
|
|||
import {Component, CreateElement, RenderContext, VNode, VNodeChildrenArrayContents} from 'vue';
|
||||
import {Component, Prop} from '@f-list/vue-ts';
|
||||
import {CreateElement, default as Vue, VNode, VNodeChildrenArrayContents} from 'vue';
|
||||
import {Channel} from '../fchat';
|
||||
import {BBCodeView} from './bbcode';
|
||||
import {formatTime} from './common';
|
||||
import core from './core';
|
||||
import {Conversation} from './interfaces';
|
||||
import UserView from './user_view';
|
||||
// TODO convert this to single-file once Vue supports it for functional components.
|
||||
// template:
|
||||
// <span>[{{formatTime(message.time)}}]</span>
|
||||
// <span v-show="message.type == MessageTypes.Action">*</span>
|
||||
// <span><user :character="message.sender" :reportDialog="$refs['reportDialog']"></user></span>
|
||||
// <span v-show="message.type == MessageTypes.Message">:</span>
|
||||
// <bbcode :text="message.text"></bbcode>
|
||||
|
||||
const userPostfix: {[key: number]: string | undefined} = {
|
||||
[Conversation.Message.Type.Message]: ': ',
|
||||
[Conversation.Message.Type.Ad]: ': ',
|
||||
[Conversation.Message.Type.Action]: ''
|
||||
};
|
||||
//tslint:disable-next-line:variable-name
|
||||
const MessageView: Component = {
|
||||
functional: true,
|
||||
render(createElement: CreateElement,
|
||||
context: RenderContext<{message: Conversation.Message, classes?: string, channel?: Channel}>): VNode {
|
||||
const message = context.props.message;
|
||||
@Component({
|
||||
render(this: MessageView, createElement: CreateElement): VNode {
|
||||
const message = this.message;
|
||||
const children: VNodeChildrenArrayContents =
|
||||
[createElement('span', {staticClass: 'message-time'}, `[${formatTime(message.time)}] `)];
|
||||
const separators = core.connection.isOpen ? core.state.settings.messageSeparators : false;
|
||||
/*tslint:disable-next-line:prefer-template*///unreasonable here
|
||||
let classes = `message message-${Conversation.Message.Type[message.type].toLowerCase()}` + (separators ? ' message-block' : '') +
|
||||
(message.type !== Conversation.Message.Type.Event && message.sender.name === core.connection.character ? ' message-own' : '') +
|
||||
((context.props.classes !== undefined) ? ` ${context.props.classes}` : '');
|
||||
((this.classes !== undefined) ? ` ${this.classes}` : '');
|
||||
if(message.type !== Conversation.Message.Type.Event) {
|
||||
children.push((message.type === Conversation.Message.Type.Action) ? '*' : '',
|
||||
createElement(UserView, {props: {character: message.sender, channel: context.props.channel}}),
|
||||
createElement(UserView, {props: {character: message.sender, channel: this.channel}}),
|
||||
userPostfix[message.type] !== undefined ? userPostfix[message.type]! : ' ');
|
||||
if(message.isHighlight) classes += ' message-highlight';
|
||||
}
|
||||
children.push(createElement(BBCodeView,
|
||||
{props: {unsafeText: message.text, afterInsert: message.type === Conversation.Message.Type.Ad ? (elm: HTMLElement) => {
|
||||
setImmediate(() => {
|
||||
elm = elm.parentElement!;
|
||||
if(elm.scrollHeight > elm.offsetHeight) {
|
||||
const expand = document.createElement('div');
|
||||
expand.className = 'expand fas fa-caret-down';
|
||||
expand.addEventListener('click', function(): void { this.parentElement!.className += ' expanded'; });
|
||||
elm.appendChild(expand);
|
||||
}
|
||||
});
|
||||
} : undefined}}));
|
||||
const isAd = message.type === Conversation.Message.Type.Ad && !this.logs;
|
||||
children.push(createElement(BBCodeView, {props: {unsafeText: message.text, afterInsert: isAd ? (elm: HTMLElement) => {
|
||||
setImmediate(() => {
|
||||
elm = elm.parentElement!;
|
||||
if(elm.scrollHeight > elm.offsetHeight) {
|
||||
const expand = document.createElement('div');
|
||||
expand.className = 'expand fas fa-caret-down';
|
||||
expand.addEventListener('click', function(): void { this.parentElement!.className += ' expanded'; });
|
||||
elm.appendChild(expand);
|
||||
}
|
||||
});
|
||||
} : undefined}}));
|
||||
const node = createElement('div', {attrs: {class: classes}}, children);
|
||||
node.key = context.data.key;
|
||||
node.key = message.id;
|
||||
return node;
|
||||
}
|
||||
};
|
||||
|
||||
export default MessageView;
|
||||
})
|
||||
export default class MessageView extends Vue {
|
||||
@Prop({required: true})
|
||||
readonly message!: Conversation.Message;
|
||||
@Prop
|
||||
readonly classes?: string;
|
||||
@Prop
|
||||
readonly channel?: Channel;
|
||||
@Prop
|
||||
readonly logs?: true;
|
||||
}
|
|
@ -8,10 +8,13 @@ import CharacterSelect from '../components/character_select.vue';
|
|||
import {setCharacters} from '../components/character_select/character_list';
|
||||
import DateDisplay from '../components/date_display.vue';
|
||||
import SimplePager from '../components/simple_pager.vue';
|
||||
import {
|
||||
Character as CharacterInfo, CharacterImage, CharacterImageOld, CharacterInfotag, CharacterSettings, KinkChoice
|
||||
} from '../interfaces';
|
||||
import {registerMethod, Store} from '../site/character_page/data_store';
|
||||
import {
|
||||
Character, CharacterCustom, CharacterFriend, CharacterImage, CharacterImageOld, CharacterInfo, CharacterInfotag, CharacterKink,
|
||||
CharacterSettings, Friend, FriendRequest, FriendsByCharacter, GuestbookState, KinkChoice, KinkChoiceFull, SharedKinks
|
||||
Character, CharacterFriend, CharacterKink, Friend, FriendRequest, FriendsByCharacter, GuestbookState, KinkChoiceFull,
|
||||
SharedKinks
|
||||
} from '../site/character_page/interfaces';
|
||||
import '../site/directives/vue-select'; //tslint:disable-line:no-import-side-effect
|
||||
import * as Utils from '../site/utils';
|
||||
|
@ -25,40 +28,39 @@ const parserSettings = {
|
|||
};
|
||||
|
||||
async function characterData(name: string | undefined): Promise<Character> {
|
||||
const data = await core.connection.queryApi('character-data.php', {name}) as CharacterInfo & {
|
||||
const data = await core.connection.queryApi<CharacterInfo & {
|
||||
badges: string[]
|
||||
customs_first: boolean
|
||||
character_list: {id: number, name: string}[]
|
||||
current_user: {inline_mode: number, animated_icons: boolean}
|
||||
custom_kinks: {[key: number]: {choice: 'fave' | 'yes' | 'maybe' | 'no', name: string, description: string, children: number[]}}
|
||||
custom_kinks: {
|
||||
[key: number]:
|
||||
{id: number, choice: 'favorite' | 'yes' | 'maybe' | 'no', name: string, description: string, children: number[]}
|
||||
}
|
||||
custom_title: string
|
||||
images: CharacterImage[]
|
||||
kinks: {[key: string]: string}
|
||||
infotags: {[key: string]: string}
|
||||
memo?: {id: number, memo: string}
|
||||
settings: CharacterSettings,
|
||||
timezone: number
|
||||
};
|
||||
}>('character-data.php', {name});
|
||||
const newKinks: {[key: string]: KinkChoiceFull} = {};
|
||||
for(const key in data.kinks)
|
||||
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];
|
||||
newCustoms.push({
|
||||
id: parseInt(key, 10),
|
||||
choice: custom.choice === 'fave' ? 'favorite' : custom.choice,
|
||||
name: custom.name,
|
||||
description: custom.description
|
||||
});
|
||||
if((<'fave'>custom.choice) === 'fave') custom.choice = 'favorite';
|
||||
custom.id = parseInt(key, 10);
|
||||
for(const childId of custom.children)
|
||||
newKinks[childId] = parseInt(key, 10);
|
||||
newKinks[childId] = custom.id;
|
||||
}
|
||||
(<any>data.settings).block_bookmarks = (<any>data.settings).prevent_bookmarks; //tslint:disable-line:no-any
|
||||
const newInfotags: {[key: string]: CharacterInfotag} = {};
|
||||
for(const key in data.infotags) {
|
||||
const characterInfotag = data.infotags[key];
|
||||
const infotag = Store.kinks.infotags[key];
|
||||
if(infotag === undefined) continue;
|
||||
|
||||
newInfotags[key] = infotag.type === 'list' ? {list: parseInt(characterInfotag, 10)} : {string: characterInfotag};
|
||||
}
|
||||
parserSettings.inlineDisplayMode = data.current_user.inline_mode;
|
||||
|
@ -73,13 +75,14 @@ async function characterData(name: string | undefined): Promise<Character> {
|
|||
created_at: data.created_at,
|
||||
updated_at: data.updated_at,
|
||||
views: data.views,
|
||||
image_count: data.images!.length,
|
||||
image_count: data.images.length,
|
||||
inlines: data.inlines,
|
||||
kinks: newKinks,
|
||||
customs: newCustoms,
|
||||
customs: data.custom_kinks,
|
||||
infotags: newInfotags,
|
||||
online_chat: false,
|
||||
timezone: data.timezone
|
||||
timezone: data.timezone,
|
||||
deleted: false
|
||||
},
|
||||
memo: data.memo,
|
||||
character_list: data.character_list,
|
||||
|
@ -97,7 +100,7 @@ function contactMethodIconUrl(name: string): string {
|
|||
async function fieldsGet(): Promise<void> {
|
||||
if(Store.kinks !== undefined) return; //tslint:disable-line:strict-type-predicates
|
||||
try {
|
||||
const fields = (await(Axios.get(`${Utils.siteDomain}json/api/mapping-list.php`))).data as SharedKinks & {
|
||||
const fields = (await (Axios.get(`${Utils.siteDomain}json/api/mapping-list.php`))).data as SharedKinks & {
|
||||
kinks: {[key: string]: {group_id: number}}
|
||||
infotags: {[key: string]: {list: string, group_id: string}}
|
||||
};
|
||||
|
@ -221,7 +224,7 @@ export function init(characters: {[key: string]: number}): void {
|
|||
core.connection.queryApi<void>('friend-remove.php', {source_id: friend.source.id, dest_id: friend.target.id}));
|
||||
registerMethod('friendRequestAccept', async(req: FriendRequest) => {
|
||||
await core.connection.queryApi('request-accept.php', {request_id: req.id});
|
||||
return { id: undefined!, source: req.target, target: req.source, createdAt: Date.now() / 1000 };
|
||||
return {id: undefined!, source: req.target, target: req.source, createdAt: Date.now() / 1000};
|
||||
});
|
||||
registerMethod('friendRequestCancel', async(req: FriendRequest) =>
|
||||
core.connection.queryApi<void>('request-cancel.php', {request_id: req.id}));
|
||||
|
|
|
@ -25,7 +25,7 @@ function VueRaven(this: void, raven: Raven.RavenStatic): Raven.RavenStatic {
|
|||
}
|
||||
});
|
||||
|
||||
if(typeof oldOnError === 'function') oldOnError.call(this, error, vm);
|
||||
if(typeof oldOnError === 'function') oldOnError.call(this, error, vm, info);
|
||||
else console.log(error);
|
||||
};
|
||||
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
<slot name="title" style="flex:1"></slot>
|
||||
</div>
|
||||
</a>
|
||||
<div class="dropdown-menu" :style="open ? 'display:block' : ''" @mousedown.stop.prevent @click="isOpen = false"
|
||||
<div class="dropdown-menu" :style="open ? {display: 'block'} : undefined" @mousedown.stop.prevent @click="isOpen = false"
|
||||
ref="menu">
|
||||
<slot></slot>
|
||||
</div>
|
||||
|
@ -14,9 +14,8 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {Component, Prop, Watch} from '@f-list/vue-ts';
|
||||
import Vue from 'vue';
|
||||
import Component from 'vue-class-component';
|
||||
import {Prop, Watch} from 'vue-property-decorator';
|
||||
|
||||
@Component
|
||||
export default class Dropdown extends Vue {
|
||||
|
@ -35,7 +34,7 @@
|
|||
menu.style.cssText = '';
|
||||
return;
|
||||
}
|
||||
let element: HTMLElement | null = this.$el;
|
||||
let element = <HTMLElement | null>this.$el;
|
||||
while(element !== null) {
|
||||
if(getComputedStyle(element).position === 'fixed') {
|
||||
menu.style.display = 'block';
|
||||
|
|
|
@ -4,12 +4,13 @@
|
|||
<slot v-else slot="title" :option="selected">{{label}}</slot>
|
||||
|
||||
<div style="padding:10px;">
|
||||
<input v-model="filter" class="form-control" :placeholder="placeholder" @mousedown.stop @focus="keepOpen = true" @blur="keepOpen = false"/>
|
||||
<input v-model="filter" class="form-control" :placeholder="placeholder" @mousedown.stop @focus="keepOpen = true"
|
||||
@blur="keepOpen = false"/>
|
||||
</div>
|
||||
<div class="dropdown-items">
|
||||
<template v-if="multiple">
|
||||
<a href="#" @click.stop="select(option)" v-for="option in filtered" class="dropdown-item">
|
||||
<input type="checkbox" :checked="selected.indexOf(option) !== -1"/>
|
||||
<input type="checkbox" :checked="isSelected(option)"/>
|
||||
<slot :option="option">{{option}}</slot>
|
||||
</a>
|
||||
</template>
|
||||
|
@ -23,16 +24,14 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {Component, Prop, Watch} from '@f-list/vue-ts';
|
||||
import Vue from 'vue';
|
||||
import Component from 'vue-class-component';
|
||||
import {Prop, Watch} from 'vue-property-decorator';
|
||||
import Dropdown from '../components/Dropdown.vue';
|
||||
|
||||
@Component({
|
||||
components: {dropdown: Dropdown}
|
||||
})
|
||||
export default class FilterableSelect extends Vue {
|
||||
//tslint:disable:no-null-keyword
|
||||
@Prop()
|
||||
readonly placeholder?: string;
|
||||
@Prop({required: true})
|
||||
|
@ -46,11 +45,11 @@
|
|||
@Prop()
|
||||
readonly title?: string;
|
||||
filter = '';
|
||||
selected: object | object[] | null = this.value !== undefined ? this.value : (this.multiple !== undefined ? [] : null);
|
||||
selected: object | object[] | undefined = this.value !== undefined ? this.value : (this.multiple !== undefined ? [] : undefined);
|
||||
keepOpen = false;
|
||||
|
||||
@Watch('value')
|
||||
watchValue(newValue: object | object[] | null): void {
|
||||
watchValue(newValue: object | object[] | undefined): void {
|
||||
this.selected = newValue;
|
||||
}
|
||||
|
||||
|
@ -67,13 +66,17 @@
|
|||
this.$emit('input', this.selected);
|
||||
}
|
||||
|
||||
isSelected(option: object): boolean {
|
||||
return (<object[]>this.selected).indexOf(option) !== -1;
|
||||
}
|
||||
|
||||
get filtered(): object[] {
|
||||
return this.options.filter((x) => this.filterFunc(this.filterRegex, x));
|
||||
}
|
||||
|
||||
get label(): string | undefined {
|
||||
return this.multiple !== undefined ? `${this.title} - ${(<object[]>this.selected).length}` :
|
||||
(this.selected !== null ? this.selected.toString() : this.title);
|
||||
(this.selected !== undefined ? this.selected.toString() : this.title);
|
||||
}
|
||||
|
||||
get filterRegex(): RegExp {
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<template>
|
||||
<span v-show="isShown">
|
||||
<div class="modal" @click.self="hideWithCheck" style="display:flex">
|
||||
<div class="modal-dialog" :class="dialogClass" style="display:flex;align-items:center">
|
||||
<div class="modal" @click.self="hideWithCheck()" style="display:flex;justify-content:center">
|
||||
<div class="modal-dialog" :class="dialogClass" style="display:flex;align-items:center;margin-left:0;margin-right:0">
|
||||
<div class="modal-content" style="max-height:100%">
|
||||
<div class="modal-header" style="flex-shrink:0">
|
||||
<h4 class="modal-title">
|
||||
|
@ -26,9 +26,8 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {Component, Hook, Prop} from '@f-list/vue-ts';
|
||||
import Vue from 'vue';
|
||||
import Component from 'vue-class-component';
|
||||
import {Prop} from 'vue-property-decorator';
|
||||
import {getKey} from '../chat/common';
|
||||
import {Keys} from '../keys';
|
||||
|
||||
|
@ -95,6 +94,7 @@
|
|||
this.hide();
|
||||
}
|
||||
|
||||
@Hook('beforeDestroy')
|
||||
beforeDestroy(): void {
|
||||
if(this.isShown) this.hide();
|
||||
}
|
||||
|
|
|
@ -6,9 +6,8 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {Component, Prop} from '@f-list/vue-ts';
|
||||
import Vue from 'vue';
|
||||
import Component from 'vue-class-component';
|
||||
import {Prop} from 'vue-property-decorator';
|
||||
import * as Utils from '../site/utils';
|
||||
|
||||
@Component
|
||||
|
|
|
@ -6,9 +6,8 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {Component, Prop} from '@f-list/vue-ts';
|
||||
import Vue from 'vue';
|
||||
import Component from 'vue-class-component';
|
||||
import {Prop} from 'vue-property-decorator';
|
||||
import {getCharacters} from './character_select/character_list';
|
||||
|
||||
interface SelectItem {
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
import {Component} from '@f-list/vue-ts';
|
||||
import Vue from 'vue';
|
||||
import Modal from './Modal.vue';
|
||||
|
||||
@Component
|
||||
export default class CustomDialog extends Vue {
|
||||
protected get dialog(): Modal {
|
||||
return <Modal>this.$children[0];
|
||||
|
|
|
@ -3,10 +3,9 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {Component, Hook, Prop, Watch} from '@f-list/vue-ts';
|
||||
import {distanceInWordsToNow, format} from 'date-fns';
|
||||
import Vue, {ComponentOptions} from 'vue';
|
||||
import Component from 'vue-class-component';
|
||||
import {Prop} from 'vue-property-decorator';
|
||||
import Vue from 'vue';
|
||||
import {Settings} from '../site/utils';
|
||||
|
||||
@Component
|
||||
|
@ -16,8 +15,9 @@
|
|||
primary: string | undefined;
|
||||
secondary: string | undefined;
|
||||
|
||||
constructor(options?: ComponentOptions<Vue>) {
|
||||
super(options);
|
||||
@Hook('mounted')
|
||||
@Watch('time')
|
||||
update(): void {
|
||||
if(this.time === null || this.time === 0)
|
||||
return;
|
||||
const date = isNaN(+this.time) ? new Date(`${this.time}+00:00`) : new Date(+this.time * 1000);
|
||||
|
|
|
@ -14,9 +14,8 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {Component, Prop} from '@f-list/vue-ts';
|
||||
import Vue from 'vue';
|
||||
import Component from 'vue-class-component';
|
||||
import {Prop} from 'vue-property-decorator';
|
||||
|
||||
@Component
|
||||
export default class FormGroup extends Vue {
|
||||
|
|
|
@ -17,9 +17,8 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {Component, Prop} from '@f-list/vue-ts';
|
||||
import Vue from 'vue';
|
||||
import Component from 'vue-class-component';
|
||||
import {Prop} from 'vue-property-decorator';
|
||||
|
||||
@Component
|
||||
export default class FormGroupInputgroup extends Vue {
|
||||
|
|
|
@ -2,21 +2,21 @@
|
|||
<div class="d-flex w-100 my-2 justify-content-between">
|
||||
<div>
|
||||
<slot name="previous" v-if="!routed">
|
||||
<a class="btn btn-secondary" :class="{'disabled': !prev}" role="button" @click.prevent="previousPage">
|
||||
<a class="btn btn-secondary" :class="{'disabled': !prev}" role="button" @click.prevent="previousPage()">
|
||||
<span aria-hidden="true">←</span> {{prevLabel}}
|
||||
</a>
|
||||
</slot>
|
||||
<router-link v-if="routed" :to="prevRoute" class="btn btn-secondary" :class="{'disabled': !prev}" role="button">
|
||||
<router-link v-else :to="prevRoute" class="btn btn-secondary" :class="{'disabled': !prev}" role="button">
|
||||
<span aria-hidden="true">←</span> {{prevLabel}}
|
||||
</router-link>
|
||||
</div>
|
||||
<div>
|
||||
<slot name="next" v-if="!routed">
|
||||
<a class="btn btn-secondary" :class="{'disabled': !next}" role="button" @click.prevent="nextPage">
|
||||
<a class="btn btn-secondary" :class="{'disabled': !next}" role="button" @click.prevent="nextPage()">
|
||||
{{nextLabel}} <span aria-hidden="true">→</span>
|
||||
</a>
|
||||
</slot>
|
||||
<router-link v-if="routed" :to="nextRoute" class="btn btn-secondary" :class="{'disabled': !next}" role="button">
|
||||
<router-link v-else :to="nextRoute" class="btn btn-secondary" :class="{'disabled': !next}" role="button">
|
||||
{{nextLabel}} <span aria-hidden="true">→</span>
|
||||
</router-link>
|
||||
</div>
|
||||
|
@ -24,10 +24,9 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {Component, Prop} from '@f-list/vue-ts';
|
||||
import cloneDeep = require('lodash/cloneDeep'); //tslint:disable-line:no-require-imports
|
||||
import Vue from 'vue';
|
||||
import Component from 'vue-class-component';
|
||||
import {Prop} from 'vue-property-decorator';
|
||||
|
||||
type ParamDictionary = {[key: string]: number | undefined};
|
||||
interface RouteParams {
|
||||
|
|
|
@ -6,9 +6,9 @@ const Tabs = Vue.extend({
|
|||
render(this: Vue & {readonly value?: string, _v?: string, selected?: string, tabs: {readonly [key: string]: string}},
|
||||
createElement: CreateElement): VNode {
|
||||
let children: {[key: string]: string | VNode | undefined};
|
||||
if(<VNode[] | undefined>this.$slots['default'] !== undefined) {
|
||||
if(this.$slots['default'] !== undefined) {
|
||||
children = {};
|
||||
this.$slots['default'].forEach((child, i) => {
|
||||
this.$slots['default']!.forEach((child, i) => {
|
||||
if(child.context !== undefined) children[child.key !== undefined ? child.key : i] = child;
|
||||
});
|
||||
} else children = this.tabs;
|
||||
|
@ -19,14 +19,11 @@ const Tabs = Vue.extend({
|
|||
this.$emit('input', this._v = keys[0]);
|
||||
if(this.selected !== this._v && children[this.selected!] !== undefined)
|
||||
this.$emit('input', this._v = this.selected);
|
||||
return createElement('ul', {staticClass: 'nav nav-tabs'}, keys.map((key) => createElement('li', {staticClass: 'nav-item'},
|
||||
[createElement('a', {
|
||||
staticClass: 'nav-link', class: {active: this._v === key}, on: {
|
||||
click: () => {
|
||||
this.$emit('input', key);
|
||||
}
|
||||
}
|
||||
}, [children[key]!])])));
|
||||
return createElement('div', {staticClass: 'nav-tabs-scroll'},
|
||||
[createElement('ul', {staticClass: 'nav nav-tabs'}, keys.map((key) => createElement('li', {staticClass: 'nav-item'},
|
||||
[createElement('a', {
|
||||
staticClass: 'nav-link', class: {active: this._v === key}, on: { click: () => this.$emit('input', key) }
|
||||
}, [children[key]!])])))]);
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -10,18 +10,18 @@
|
|||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label" for="account">{{l('login.account')}}</label>
|
||||
<input class="form-control" id="account" v-model="settings.account" @keypress.enter="login" :disabled="loggingIn"/>
|
||||
<input class="form-control" id="account" v-model="settings.account" @keypress.enter="login()" :disabled="loggingIn"/>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label" for="password">{{l('login.password')}}</label>
|
||||
<input class="form-control" type="password" id="password" v-model="password" @keypress.enter="login" :disabled="loggingIn"/>
|
||||
<input class="form-control" type="password" id="password" v-model="password" @keypress.enter="login()" :disabled="loggingIn"/>
|
||||
</div>
|
||||
<div class="form-group" v-show="showAdvanced">
|
||||
<label class="control-label" for="host">{{l('login.host')}}</label>
|
||||
<div class="input-group">
|
||||
<input class="form-control" id="host" v-model="settings.host" @keypress.enter="login" :disabled="loggingIn"/>
|
||||
<input class="form-control" id="host" v-model="settings.host" @keypress.enter="login()" :disabled="loggingIn"/>
|
||||
<div class="input-group-append">
|
||||
<button class="btn btn-outline-secondary" @click="resetHost"><span class="fas fa-undo-alt"></span></button>
|
||||
<button class="btn btn-outline-secondary" @click="resetHost()"><span class="fas fa-undo-alt"></span></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -65,6 +65,7 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {Component, Hook} from '@f-list/vue-ts';
|
||||
import Axios from 'axios';
|
||||
import * as electron from 'electron';
|
||||
import log from 'electron-log'; //tslint:disable-line:match-default-export-name
|
||||
|
@ -74,7 +75,6 @@
|
|||
import * as Raven from 'raven-js';
|
||||
import {promisify} from 'util';
|
||||
import Vue from 'vue';
|
||||
import Component from 'vue-class-component';
|
||||
import Chat from '../chat/Chat.vue';
|
||||
import {getKey, Settings} from '../chat/common';
|
||||
import core, {init as initCore} from '../chat/core';
|
||||
|
@ -109,15 +109,14 @@
|
|||
components: {chat: Chat, modal: Modal, characterPage: CharacterPage}
|
||||
})
|
||||
export default class Index extends Vue {
|
||||
//tslint:disable:no-null-keyword
|
||||
showAdvanced = false;
|
||||
saveLogin = false;
|
||||
loggingIn = false;
|
||||
password = '';
|
||||
character: string | undefined;
|
||||
characters: string[] | null = null;
|
||||
characters: string[] | undefined;
|
||||
error = '';
|
||||
defaultCharacter: string | null = null;
|
||||
defaultCharacter: string | undefined;
|
||||
l = l;
|
||||
settings!: GeneralSettings;
|
||||
importProgress = 0;
|
||||
|
@ -125,6 +124,7 @@
|
|||
fixCharacters: ReadonlyArray<string> = [];
|
||||
fixCharacter = '';
|
||||
|
||||
@Hook('created')
|
||||
async created(): Promise<void> {
|
||||
if(this.settings.account.length > 0) this.saveLogin = true;
|
||||
keyStore.getPassword(this.settings.account)
|
||||
|
@ -192,8 +192,8 @@
|
|||
});
|
||||
connection.onEvent('closed', () => {
|
||||
if(this.character === undefined) return;
|
||||
electron.ipcRenderer.send('disconnect', this.character);
|
||||
this.character = undefined;
|
||||
electron.ipcRenderer.send('disconnect', connection.character);
|
||||
parent.send('disconnect', webContents.id);
|
||||
Raven.setUserContext();
|
||||
});
|
||||
|
|
|
@ -18,14 +18,14 @@
|
|||
</a>
|
||||
</li>
|
||||
<li v-show="canOpenTab" class="addTab nav-item" id="addTab">
|
||||
<a href="#" @click.prevent="addTab" class="nav-link"><i class="fa fa-plus"></i></a>
|
||||
<a href="#" @click.prevent="addTab()" class="nav-link"><i class="fa fa-plus"></i></a>
|
||||
</li>
|
||||
</ul>
|
||||
<div style="flex:1;display:flex;justify-content:flex-end;-webkit-app-region:drag" class="btn-group"
|
||||
id="windowButtons">
|
||||
<i class="far fa-window-minimize btn btn-light" @click.stop="minimize"></i>
|
||||
<i class="far btn btn-light" :class="'fa-window-' + (isMaximized ? 'restore' : 'maximize')" @click="maximize"></i>
|
||||
<span class="btn btn-light" @click.stop="close">
|
||||
<i class="far fa-window-minimize btn btn-light" @click.stop="minimize()"></i>
|
||||
<i class="far btn btn-light" :class="'fa-window-' + (isMaximized ? 'restore' : 'maximize')" @click="maximize()"></i>
|
||||
<span class="btn btn-light" @click.stop="close()">
|
||||
<i class="fa fa-times fa-lg"></i>
|
||||
</span>
|
||||
</div>
|
||||
|
@ -36,12 +36,12 @@
|
|||
<script lang="ts">
|
||||
import Sortable = require('sortablejs'); //tslint:disable-line:no-require-imports
|
||||
|
||||
import {Component, Hook} from '@f-list/vue-ts';
|
||||
import * as electron from 'electron';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import * as url from 'url';
|
||||
import Vue from 'vue';
|
||||
import Component from 'vue-class-component';
|
||||
import l from '../chat/localize';
|
||||
import {GeneralSettings} from './common';
|
||||
|
||||
|
@ -71,10 +71,9 @@
|
|||
|
||||
@Component
|
||||
export default class Window extends Vue {
|
||||
//tslint:disable:no-null-keyword
|
||||
settings!: GeneralSettings;
|
||||
tabs: Tab[] = [];
|
||||
activeTab: Tab | null = null;
|
||||
activeTab: Tab | undefined;
|
||||
tabMap: {[key: number]: Tab} = {};
|
||||
isMaximized = browserWindow.isMaximized();
|
||||
canOpenTab = true;
|
||||
|
@ -83,6 +82,7 @@
|
|||
platform = process.platform;
|
||||
lockTab = false;
|
||||
|
||||
@Hook('mounted')
|
||||
mounted(): void {
|
||||
this.addTab();
|
||||
electron.ipcRenderer.on('settings', (_: Event, settings: GeneralSettings) => this.settings = settings);
|
||||
|
@ -105,7 +105,6 @@
|
|||
tab.hasNew = false;
|
||||
electron.ipcRenderer.send('has-new', this.tabs.reduce((cur, t) => cur || t.hasNew, false));
|
||||
}
|
||||
electron.ipcRenderer.send('disconnect', tab.user);
|
||||
tab.user = undefined;
|
||||
tab.tray.setToolTip(l('title'));
|
||||
tab.tray.setContextMenu(electron.remote.Menu.buildFromTemplate(this.createTrayMenu(tab)));
|
||||
|
@ -115,14 +114,8 @@
|
|||
tab.hasNew = hasNew;
|
||||
electron.ipcRenderer.send('has-new', this.tabs.reduce((cur, t) => cur || t.hasNew, false));
|
||||
});
|
||||
browserWindow.on('maximize', () => {
|
||||
this.isMaximized = true;
|
||||
this.activeTab!.view.setBounds(getWindowBounds());
|
||||
});
|
||||
browserWindow.on('unmaximize', () => {
|
||||
this.isMaximized = false;
|
||||
this.activeTab!.view.setBounds(getWindowBounds());
|
||||
});
|
||||
browserWindow.on('maximize', () => this.isMaximized = true);
|
||||
browserWindow.on('unmaximize', () => this.isMaximized = false);
|
||||
electron.ipcRenderer.on('switch-tab', (_: Event) => {
|
||||
const index = this.tabs.indexOf(this.activeTab!);
|
||||
this.show(this.tabs[index + 1 === this.tabs.length ? 0 : index + 1]);
|
||||
|
@ -133,12 +126,12 @@
|
|||
document.addEventListener('click', () => this.activeTab!.view.webContents.focus());
|
||||
window.addEventListener('focus', () => this.activeTab!.view.webContents.focus());
|
||||
|
||||
Sortable.create(this.$refs['tabs'], {
|
||||
Sortable.create(<HTMLElement>this.$refs['tabs'], {
|
||||
animation: 50,
|
||||
onEnd: (e: {oldIndex: number, newIndex: number}) => {
|
||||
onEnd: (e) => {
|
||||
if(e.oldIndex === e.newIndex) return;
|
||||
const tab = this.tabs.splice(e.oldIndex, 1)[0];
|
||||
this.tabs.splice(e.newIndex, 0, tab);
|
||||
const tab = this.tabs.splice(e.oldIndex!, 1)[0];
|
||||
this.tabs.splice(e.newIndex!, 0, tab);
|
||||
},
|
||||
onMove: (e: {related: HTMLElement}) => e.related.id !== 'addTab',
|
||||
filter: '.addTab'
|
||||
|
@ -163,7 +156,7 @@
|
|||
}
|
||||
|
||||
destroyAllTabs(): void {
|
||||
browserWindow.setBrowserView(null!);
|
||||
browserWindow.setBrowserView(null!); //tslint:disable-line:no-null-keyword
|
||||
this.tabs.forEach(destroyTab);
|
||||
this.tabs = [];
|
||||
}
|
||||
|
@ -230,7 +223,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!);
|
||||
browserWindow.setBrowserView(null!); //tslint:disable-line:no-null-keyword
|
||||
if(process.env.NODE_ENV === 'production') browserWindow.close();
|
||||
} else if(this.activeTab === tab) this.show(this.tabs[0]);
|
||||
destroyTab(tab);
|
||||
|
|
|
@ -49,19 +49,17 @@ document.addEventListener('keydown', (e: KeyboardEvent) => {
|
|||
|
||||
process.env.SPELLCHECKER_PREFER_HUNSPELL = '1';
|
||||
const sc = nativeRequire<{
|
||||
Spellchecker: {
|
||||
new(): {
|
||||
add(word: string): void
|
||||
remove(word: string): void
|
||||
isMisspelled(x: string): boolean
|
||||
setDictionary(name: string | undefined, dir: string): void
|
||||
getCorrectionsForMisspelling(word: string): ReadonlyArray<string>
|
||||
}
|
||||
Spellchecker: new() => {
|
||||
add(word: string): void
|
||||
remove(word: string): void
|
||||
isMisspelled(x: string): boolean
|
||||
setDictionary(name: string | undefined, dir: string): void
|
||||
getCorrectionsForMisspelling(word: string): ReadonlyArray<string>
|
||||
}
|
||||
}>('spellchecker/build/Release/spellchecker.node');
|
||||
const spellchecker = new sc.Spellchecker();
|
||||
|
||||
Axios.defaults.params = { __fchat: `desktop/${electron.remote.app.getVersion()}` };
|
||||
Axios.defaults.params = {__fchat: `desktop/${electron.remote.app.getVersion()}`};
|
||||
|
||||
if(process.env.NODE_ENV === 'production') {
|
||||
setupRaven('https://a9239b17b0a14f72ba85e8729b9d1612@sentry.f-list.net/2', electron.remote.app.getVersion());
|
||||
|
@ -72,30 +70,23 @@ if(process.env.NODE_ENV === 'production') {
|
|||
});
|
||||
}
|
||||
let browser: string | undefined;
|
||||
|
||||
function openIncognito(url: string): void {
|
||||
if(browser === undefined)
|
||||
try { //tslint:disable-next-line:max-line-length
|
||||
browser = execSync(`FOR /F "skip=2 tokens=3" %A IN ('REG QUERY HKCU\\Software\\Microsoft\\Windows\\Shell\\Associations\\UrlAssociations\\http\\UserChoice /v ProgId') DO @(echo %A)`)
|
||||
.toString().trim();
|
||||
.toString().trim().toLowerCase();
|
||||
} catch(e) {
|
||||
console.error(e);
|
||||
}
|
||||
switch(browser) {
|
||||
case 'FirefoxURL':
|
||||
exec(`start firefox.exe -private-window ${url}`);
|
||||
break;
|
||||
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}`);
|
||||
}
|
||||
const commands = {
|
||||
chrome: 'chrome.exe -incognito', firefox: 'firefox.exe -private-window', vivaldi: 'vivaldi.exe -incognito',
|
||||
opera: 'opera.exe -private'
|
||||
};
|
||||
let start = 'iexplore.exe -private';
|
||||
for(const key in commands)
|
||||
if(browser!.indexOf(key) !== -1) start = commands[<keyof typeof commands>key];
|
||||
exec(`start ${start} ${url}`);
|
||||
}
|
||||
|
||||
const webContents = electron.remote.getCurrentWebContents();
|
||||
|
@ -172,14 +163,16 @@ webContents.on('context-menu', (_, props) => {
|
|||
});
|
||||
|
||||
let dictDir = path.join(electron.remote.app.getPath('userData'), 'spellchecker');
|
||||
if(process.platform === 'win32')
|
||||
exec(`for /d %I in ("${dictDir}") do @echo %~sI`, (_, stdout) => { dictDir = stdout.trim(); });
|
||||
if(process.platform === 'win32') //get the path in DOS (8-character) format as special characters cause problems otherwise
|
||||
exec(`for /d %I in ("${dictDir}") do @echo %~sI`, (_, stdout) => dictDir = stdout.trim());
|
||||
electron.webFrame.setSpellCheckProvider('', false, {spellCheck: (text) => !spellchecker.isMisspelled(text)});
|
||||
|
||||
function onSettings(s: GeneralSettings): void {
|
||||
settings = s;
|
||||
spellchecker.setDictionary(s.spellcheckLang, dictDir);
|
||||
for(const word of s.customDictionary) spellchecker.add(word);
|
||||
}
|
||||
|
||||
electron.ipcRenderer.on('settings', (_: Event, s: GeneralSettings) => onSettings(s));
|
||||
|
||||
const params = <{[key: string]: string | undefined}>qs.parse(window.location.search.substr(1));
|
||||
|
|
|
@ -290,7 +290,7 @@ export class Logs implements Logging {
|
|||
async getAvailableCharacters(): Promise<ReadonlyArray<string>> {
|
||||
const baseDir = core.state.generalSettings!.logDirectory;
|
||||
mkdir(baseDir);
|
||||
return (fs.readdirSync(baseDir)).filter((x) => fs.lstatSync(path.join(baseDir, x)).isDirectory());
|
||||
return (fs.readdirSync(baseDir)).filter((x) => fs.statSync(path.join(baseDir, x)).isDirectory());
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -312,7 +312,7 @@ export class SettingsStore implements Settings.Store {
|
|||
|
||||
async getAvailableCharacters(): Promise<ReadonlyArray<string>> {
|
||||
const baseDir = core.state.generalSettings!.logDirectory;
|
||||
return (fs.readdirSync(baseDir)).filter((x) => fs.lstatSync(path.join(baseDir, x)).isDirectory());
|
||||
return (fs.readdirSync(baseDir)).filter((x) => fs.statSync(path.join(baseDir, x)).isDirectory());
|
||||
}
|
||||
|
||||
async set<K extends keyof Settings.Keys>(key: K, value: Settings.Keys[K]): Promise<void> {
|
||||
|
|
|
@ -184,7 +184,7 @@ export async function importCharacter(ownCharacter: string, progress: (progress:
|
|||
progress(i / subdirs.length);
|
||||
const subdir = subdirs[i];
|
||||
const subdirPath = path.join(dir, subdir);
|
||||
if(subdir === '!Notifications' || subdir === 'Global' || !fs.lstatSync(subdirPath).isDirectory()) continue;
|
||||
if(subdir === '!Notifications' || subdir === 'Global' || !fs.statSync(subdirPath).isDirectory()) continue;
|
||||
|
||||
const channelMarker = subdir.indexOf('(');
|
||||
let key: string, name: string;
|
||||
|
|
|
@ -381,7 +381,10 @@ function onReady(): void {
|
|||
settings.customDictionary.splice(settings.customDictionary.indexOf(word), 1);
|
||||
setGeneralSettings(settings);
|
||||
});
|
||||
electron.ipcMain.on('disconnect', (_: Event, character: string) => characters.splice(characters.indexOf(character), 1));
|
||||
electron.ipcMain.on('disconnect', (_: Event, character: string) => {
|
||||
const index = characters.indexOf(character);
|
||||
if(index !== -1) characters.splice(index, 1);
|
||||
});
|
||||
const emptyBadge = electron.nativeImage.createEmpty();
|
||||
//tslint:disable-next-line:no-require-imports
|
||||
const badge = electron.nativeImage.createFromPath(path.join(__dirname, <string>require('./build/badge.png')));
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "fchat",
|
||||
"version": "3.0.9",
|
||||
"version": "3.0.10",
|
||||
"author": "The F-List Team",
|
||||
"description": "F-List.net Chat Client",
|
||||
"main": "main.js",
|
||||
|
|
|
@ -1,10 +1,9 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "es6",
|
||||
"target": "es2017",
|
||||
"module": "commonjs",
|
||||
"sourceMap": true,
|
||||
"allowJs": true,
|
||||
"outDir": "build",
|
||||
"noEmitHelpers": true,
|
||||
"importHelpers": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
|
|
|
@ -1,11 +1,10 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "es6",
|
||||
"target": "es2017",
|
||||
"module": "commonjs",
|
||||
"sourceMap": true,
|
||||
"experimentalDecorators": true,
|
||||
"allowJs": true,
|
||||
"outDir": "build",
|
||||
"noEmitHelpers": true,
|
||||
"importHelpers": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin');
|
||||
const ForkTsCheckerWebpackPlugin = require('@f-list/fork-ts-checker-webpack-plugin');
|
||||
const OptimizeCssAssetsPlugin = require('optimize-css-assets-webpack-plugin');
|
||||
const VueLoaderPlugin = require('vue-loader/lib/plugin');
|
||||
const vueTransformer = require('@f-list/vue-ts/transform').default;
|
||||
|
||||
const mainConfig = {
|
||||
entry: [path.join(__dirname, 'main.ts'), path.join(__dirname, 'package.json')],
|
||||
|
@ -69,7 +70,8 @@ const mainConfig = {
|
|||
options: {
|
||||
appendTsSuffixTo: [/\.vue$/],
|
||||
configFile: __dirname + '/tsconfig-renderer.json',
|
||||
transpileOnly: true
|
||||
transpileOnly: true,
|
||||
getCustomTransformers: () => ({before: [vueTransformer]})
|
||||
}
|
||||
},
|
||||
{test: /\.eot(\?v=\d+\.\d+\.\d+)?$/, loader: 'file-loader'},
|
||||
|
|
|
@ -59,9 +59,9 @@ export default function(this: void, connection: Connection): Interfaces.State {
|
|||
connection.onEvent('connecting', async(isReconnect) => {
|
||||
state.friends = [];
|
||||
state.bookmarks = [];
|
||||
state.bookmarkList = (<{characters: string[]}>await connection.queryApi('bookmark-list.php')).characters;
|
||||
state.friendList = ((<{friends: {source: string, dest: string, last_online: number}[]}>await connection.queryApi('friend-list.php'))
|
||||
.friends).map((x) => x.dest);
|
||||
state.bookmarkList = (await connection.queryApi<{characters: string[]}>('bookmark-list.php')).characters;
|
||||
state.friendList = (await connection.queryApi<{friends: {source: string, dest: string, last_online: number}[]}>('friend-list.php'))
|
||||
.friends.map((x) => x.dest);
|
||||
if(isReconnect && (<Character | undefined>state.ownCharacter) !== undefined)
|
||||
reconnectStatus = {status: state.ownCharacter.status, statusmsg: state.ownCharacter.statusText};
|
||||
for(const key in state.characters) {
|
||||
|
|
|
@ -86,11 +86,12 @@ export default class Connection implements Interfaces.Connection {
|
|||
this.reconnectDelay = this.reconnectDelay >= 30000 ? 60000 : this.reconnectDelay >= 10000 ? 30000 : 10000;
|
||||
}
|
||||
|
||||
close(): void {
|
||||
close(keepState: boolean = true): void {
|
||||
if(this.reconnectTimer !== undefined) clearTimeout(this.reconnectTimer);
|
||||
this.reconnectTimer = undefined;
|
||||
this.cleanClose = true;
|
||||
if(this.socket !== undefined) this.socket.close();
|
||||
if(!keepState) this.character = '';
|
||||
}
|
||||
|
||||
get isOpen(): boolean {
|
||||
|
@ -143,7 +144,7 @@ export default class Connection implements Interfaces.Connection {
|
|||
}
|
||||
|
||||
send<K extends keyof Interfaces.ClientCommands>(command: K, data?: Interfaces.ClientCommands[K]): void {
|
||||
if(this.socket !== undefined)
|
||||
if(this.socket !== undefined && this.socket.readyState === WebSocketConnection.ReadyState.OPEN)
|
||||
this.socket.send(<string>command + (data !== undefined ? ` ${JSON.stringify(data)}` : ''));
|
||||
}
|
||||
|
||||
|
|
|
@ -138,7 +138,7 @@ export namespace Connection {
|
|||
readonly vars: Vars
|
||||
readonly isOpen: boolean
|
||||
connect(character: string): void
|
||||
close(): void
|
||||
close(keepState?: boolean): void
|
||||
onMessage<K extends keyof ServerCommands>(type: K, handler: CommandHandler<K>): void
|
||||
offMessage<K extends keyof ServerCommands>(type: K, handler: CommandHandler<K>): void
|
||||
onEvent(type: EventType, handler: EventHandler): void
|
||||
|
@ -232,6 +232,10 @@ export namespace Channel {
|
|||
|
||||
export type Channel = Channel.Channel;
|
||||
|
||||
export namespace WebSocketConnection {
|
||||
export enum ReadyState { CONNECTING, OPEN, CLOSING, CLOSED }
|
||||
}
|
||||
|
||||
export interface WebSocketConnection {
|
||||
close(): void
|
||||
onMessage(handler: (message: string) => Promise<void>): void
|
||||
|
@ -239,4 +243,5 @@ export interface WebSocketConnection {
|
|||
onClose(handler: () => void): void
|
||||
onError(handler: (error: Error) => void): void
|
||||
send(message: string): void
|
||||
readyState: WebSocketConnection.ReadyState
|
||||
}
|
|
@ -0,0 +1,99 @@
|
|||
export interface SimpleCharacter {
|
||||
id: number
|
||||
name: string
|
||||
deleted: boolean
|
||||
}
|
||||
|
||||
export interface InlineImage {
|
||||
id: number
|
||||
name: string
|
||||
hash: string
|
||||
extension: string
|
||||
nsfw: boolean
|
||||
}
|
||||
|
||||
export type CharacterImage = CharacterImageOld | CharacterImageNew;
|
||||
|
||||
export interface CharacterImageNew {
|
||||
id: number
|
||||
extension: string
|
||||
description: string
|
||||
hash: string
|
||||
sort_order: number | null
|
||||
}
|
||||
|
||||
export interface CharacterImageOld {
|
||||
id: number
|
||||
extension: string
|
||||
hash: string
|
||||
height: number
|
||||
width: number
|
||||
description: string
|
||||
sort_order: number | null
|
||||
url: string
|
||||
}
|
||||
|
||||
export type InfotagType = 'number' | 'text' | 'list';
|
||||
|
||||
export interface CharacterInfotag {
|
||||
list?: number
|
||||
string?: string
|
||||
number?: number
|
||||
}
|
||||
|
||||
export interface Infotag {
|
||||
id: number
|
||||
name: string
|
||||
type: InfotagType
|
||||
search_field: string
|
||||
validator: string
|
||||
allow_legacy: boolean
|
||||
infotag_group: string
|
||||
list?: number
|
||||
}
|
||||
|
||||
export interface Character extends SimpleCharacter {
|
||||
id: number
|
||||
name: string
|
||||
title: string
|
||||
description: string
|
||||
kinks: {[key: string]: KinkChoice | number | undefined}
|
||||
inlines: {[key: string]: InlineImage}
|
||||
customs: {[key: string]: CustomKink | undefined}
|
||||
infotags: {[key: number]: CharacterInfotag | undefined}
|
||||
created_at: number
|
||||
updated_at: number
|
||||
views: number
|
||||
last_online_at?: number
|
||||
timezone?: number
|
||||
image_count?: number
|
||||
online_chat?: boolean
|
||||
}
|
||||
|
||||
export type KinkChoice = 'favorite' | 'yes' | 'maybe' | 'no';
|
||||
|
||||
export interface CharacterSettings {
|
||||
readonly customs_first: boolean
|
||||
readonly show_friends: boolean
|
||||
readonly show_badges: boolean
|
||||
readonly guestbook: boolean
|
||||
readonly block_bookmarks: boolean
|
||||
readonly public: boolean
|
||||
readonly moderate_guestbook: boolean
|
||||
readonly hide_timezone: boolean
|
||||
readonly hide_contact_details: boolean
|
||||
}
|
||||
|
||||
export interface Kink {
|
||||
id: number
|
||||
name: string
|
||||
description: string
|
||||
kink_group: number
|
||||
}
|
||||
|
||||
export interface CustomKink {
|
||||
id: number
|
||||
name: string
|
||||
choice: KinkChoice
|
||||
description: string
|
||||
}
|
|
@ -10,16 +10,16 @@
|
|||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label" for="account">{{l('login.account')}}</label>
|
||||
<input class="form-control" id="account" v-model="settings.account" @keypress.enter="login" :disabled="loggingIn"/>
|
||||
<input class="form-control" id="account" v-model="settings.account" @keypress.enter="login()" :disabled="loggingIn"/>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label" for="password">{{l('login.password')}}</label>
|
||||
<input class="form-control" type="password" id="password" v-model="settings.password" @keypress.enter="login" :disabled="loggingIn"/>
|
||||
<input class="form-control" type="password" id="password" v-model="settings.password" @keypress.enter="login()" :disabled="loggingIn"/>
|
||||
</div>
|
||||
<div class="form-group" v-show="showAdvanced">
|
||||
<label class="control-label" for="host">{{l('login.host')}}</label>
|
||||
<div class="input-group">
|
||||
<input class="form-control" id="host" v-model="settings.host" @keypress.enter="login" :disabled="loggingIn"/>
|
||||
<input class="form-control" id="host" v-model="settings.host" @keypress.enter="login()" :disabled="loggingIn"/>
|
||||
<div class="input-group-append">
|
||||
<button class="btn btn-outline-secondary" @click="resetHost"><span class="fas fa-undo-alt"></span></button>
|
||||
</div>
|
||||
|
@ -40,7 +40,7 @@
|
|||
<label for="save"><input type="checkbox" id="save" v-model="saveLogin"/> {{l('login.save')}}</label>
|
||||
</div>
|
||||
<div class="form-group" style="text-align:right">
|
||||
<button class="btn btn-primary" @click="login" :disabled="loggingIn">
|
||||
<button class="btn btn-primary" @click="login()" :disabled="loggingIn">
|
||||
{{l(loggingIn ? 'login.working' : 'login.submit')}}
|
||||
</button>
|
||||
</div>
|
||||
|
@ -57,11 +57,11 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {Component, Hook} from '@f-list/vue-ts';
|
||||
import Axios from 'axios';
|
||||
import * as qs from 'qs';
|
||||
import * as Raven from 'raven-js';
|
||||
import Vue from 'vue';
|
||||
import Component from 'vue-class-component';
|
||||
import Chat from '../chat/Chat.vue';
|
||||
import core, {init as initCore} from '../chat/core';
|
||||
import l from '../chat/localize';
|
||||
|
@ -94,18 +94,18 @@
|
|||
components: {chat: Chat, modal: Modal, characterPage: CharacterPage}
|
||||
})
|
||||
export default class Index extends Vue {
|
||||
//tslint:disable:no-null-keyword
|
||||
showAdvanced = false;
|
||||
saveLogin = false;
|
||||
loggingIn = false;
|
||||
characters: ReadonlyArray<string> | null = null;
|
||||
characters?: ReadonlyArray<string>;
|
||||
error = '';
|
||||
defaultCharacter: string | null = null;
|
||||
defaultCharacter?: string;
|
||||
settingsStore = new SettingsStore();
|
||||
l = l;
|
||||
settings: GeneralSettings | null = null;
|
||||
settings!: GeneralSettings;
|
||||
profileName = '';
|
||||
|
||||
@Hook('created')
|
||||
async created(): Promise<void> {
|
||||
document.addEventListener('open-profile', (e: Event) => {
|
||||
const profileViewer = <Modal>this.$refs['profileViewer'];
|
||||
|
@ -123,13 +123,13 @@
|
|||
}
|
||||
|
||||
resetHost(): void {
|
||||
this.settings!.host = new GeneralSettings().host;
|
||||
this.settings.host = new GeneralSettings().host;
|
||||
}
|
||||
|
||||
get styling(): string {
|
||||
if(window.NativeView !== undefined) window.NativeView.setTheme(this.settings!.theme);
|
||||
if(window.NativeView !== undefined) window.NativeView.setTheme(this.settings.theme);
|
||||
//tslint:disable-next-line:no-require-imports
|
||||
return `<style>${require('../scss/fa.scss')}${require(`../scss/themes/chat/${this.settings!.theme}.scss`)}</style>`;
|
||||
return `<style>${require('../scss/fa.scss')}${require(`../scss/themes/chat/${this.settings.theme}.scss`)}</style>`;
|
||||
}
|
||||
|
||||
async login(): Promise<void> {
|
||||
|
@ -138,17 +138,17 @@
|
|||
try {
|
||||
const data = <{ticket?: string, error: string, characters: {[key: string]: number}, default_character: number}>
|
||||
(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,
|
||||
account: this.settings.account, password: this.settings.password, no_friends: true, no_bookmarks: true,
|
||||
new_character_list: true
|
||||
}))).data;
|
||||
if(data.error !== '') {
|
||||
this.error = data.error;
|
||||
return;
|
||||
}
|
||||
if(this.saveLogin) await setGeneralSettings(this.settings!);
|
||||
Socket.host = this.settings!.host;
|
||||
if(this.saveLogin) await setGeneralSettings(this.settings);
|
||||
Socket.host = this.settings.host;
|
||||
const connection = new Connection('F-Chat 3.0 (Mobile)', appVersion, Socket,
|
||||
this.settings!.account, this.settings!.password);
|
||||
this.settings.account, this.settings.password);
|
||||
connection.onEvent('connected', () => {
|
||||
Raven.setUserContext({username: core.connection.character});
|
||||
document.addEventListener('backbutton', confirmBack);
|
||||
|
|
|
@ -1,2 +1,3 @@
|
|||
/build
|
||||
/debug
|
||||
/release
|
|
@ -2,14 +2,14 @@ apply plugin: 'com.android.application'
|
|||
apply plugin: 'kotlin-android'
|
||||
|
||||
android {
|
||||
compileSdkVersion 27
|
||||
buildToolsVersion "27.0.3"
|
||||
compileSdkVersion 28
|
||||
buildToolsVersion "28.0.3"
|
||||
defaultConfig {
|
||||
applicationId "net.f_list.fchat"
|
||||
minSdkVersion 19
|
||||
minSdkVersion 21
|
||||
targetSdkVersion 27
|
||||
versionCode 20
|
||||
versionName "3.0.9"
|
||||
versionCode 21
|
||||
versionName "3.0.10"
|
||||
}
|
||||
buildTypes {
|
||||
release {
|
||||
|
@ -20,7 +20,7 @@ android {
|
|||
}
|
||||
|
||||
dependencies {
|
||||
compile "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
|
||||
}
|
||||
repositories {
|
||||
mavenCentral()
|
||||
|
|
|
@ -5,6 +5,7 @@ import android.webkit.JavascriptInterface
|
|||
import org.json.JSONArray
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
import java.io.RandomAccessFile
|
||||
import java.util.*
|
||||
|
||||
class File(private val ctx: Context) {
|
||||
|
@ -12,7 +13,7 @@ class File(private val ctx: Context) {
|
|||
fun read(name: String): String? {
|
||||
val file = File(ctx.filesDir, name)
|
||||
if(!file.exists()) return null
|
||||
Scanner(file).useDelimiter("\\Z").use { return it.next() }
|
||||
return file.readText()
|
||||
}
|
||||
|
||||
@JavascriptInterface
|
||||
|
|
|
@ -118,7 +118,7 @@ class MainActivity : Activity() {
|
|||
}
|
||||
val view = EditText(this)
|
||||
view.hint = "Enter character name"
|
||||
AlertDialog.Builder(this).setView(view).setPositiveButton("OK", { _, _ ->
|
||||
AlertDialog.Builder(this).setView(view).setPositiveButton("OK") { _, _ ->
|
||||
val file = java.io.File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS), "test.zip")
|
||||
val dest = FileOutputStream(file)
|
||||
val out = ZipOutputStream(dest)
|
||||
|
@ -126,7 +126,7 @@ class MainActivity : Activity() {
|
|||
out.close()
|
||||
val downloadManager = getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
|
||||
downloadManager.addCompletedDownload(file.name, file.name, false, "text/plain", file.absolutePath, file.length(), true)
|
||||
}).setNegativeButton("Cancel", { dialog, _ -> dialog.dismiss() }).setTitle("DEBUG").show()
|
||||
}.setNegativeButton("Cancel") { dialog, _ -> dialog.dismiss() }.setTitle("DEBUG").show()
|
||||
}
|
||||
|
||||
override fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean {
|
||||
|
|
|
@ -22,7 +22,7 @@ class Notifications(private val ctx: Context) {
|
|||
init {
|
||||
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
val manager = ctx.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager;
|
||||
manager.createNotificationChannel(NotificationChannel("messages", ctx.getString(R.string.channel_messages), NotificationManager.IMPORTANCE_DEFAULT))
|
||||
manager.createNotificationChannel(NotificationChannel("messages", ctx.getString(R.string.channel_messages), NotificationManager.IMPORTANCE_HIGH))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -87,7 +87,10 @@ class ViewController: UIViewController, WKNavigationDelegate, WKUIDelegate {
|
|||
let start = str.index(of: ",")!
|
||||
let file = FileManager.default.temporaryDirectory.appendingPathComponent(str[str.index(str.startIndex, offsetBy: 5)..<start].removingPercentEncoding!)
|
||||
try! str.suffix(from: str.index(after: start)).removingPercentEncoding!.write(to: file, atomically: false, encoding: .utf8)
|
||||
self.present(UIActivityViewController(activityItems: [file], applicationActivities: nil), animated: true, completion: nil)
|
||||
let controller = UIActivityViewController(activityItems: [file], applicationActivities: nil)
|
||||
controller.popoverPresentationController?.sourceView = webView
|
||||
controller.popoverPresentationController?.sourceRect = CGRect(origin: webView.bounds.origin, size: CGSize(width: 0, height: 0))
|
||||
self.present(controller, animated: true, completion: nil)
|
||||
return
|
||||
}
|
||||
let match = profileRegex.matches(in: str, range: NSRange(location: 0, length: str.count))
|
||||
|
|
|
@ -1 +0,0 @@
|
|||
../../www
|
|
@ -0,0 +1 @@
|
|||
../../www
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "net.f_list.fchat",
|
||||
"version": "3.0.9",
|
||||
"version": "3.0.10",
|
||||
"displayName": "F-Chat",
|
||||
"author": "The F-List Team",
|
||||
"description": "F-List.net Chat Client",
|
||||
|
|
|
@ -1,16 +1,10 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "es5",
|
||||
"lib": [
|
||||
"dom",
|
||||
"es5",
|
||||
"es2015.promise"
|
||||
],
|
||||
"target": "es6",
|
||||
"module": "commonjs",
|
||||
"sourceMap": true,
|
||||
"experimentalDecorators": true,
|
||||
"allowJs": true,
|
||||
"outDir": "build",
|
||||
"noEmitHelpers": true,
|
||||
"importHelpers": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
|
@ -18,8 +12,5 @@
|
|||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true
|
||||
},
|
||||
"include": ["chat.ts", "../**/*.d.ts"],
|
||||
"exclude": [
|
||||
"node_modules"
|
||||
]
|
||||
"include": ["chat.ts", "../**/*.d.ts"]
|
||||
}
|
|
@ -1,6 +1,7 @@
|
|||
const path = require('path');
|
||||
const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin');
|
||||
const ForkTsCheckerWebpackPlugin = require('@f-list/fork-ts-checker-webpack-plugin');
|
||||
const VueLoaderPlugin = require('vue-loader/lib/plugin');
|
||||
const vueTransformer = require('@f-list/vue-ts/transform').default;
|
||||
|
||||
const config = {
|
||||
entry: {
|
||||
|
@ -19,7 +20,8 @@ const config = {
|
|||
options: {
|
||||
appendTsSuffixTo: [/\.vue$/],
|
||||
configFile: __dirname + '/tsconfig.json',
|
||||
transpileOnly: true
|
||||
transpileOnly: true,
|
||||
getCustomTransformers: () => ({before: [vueTransformer]})
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
46
package.json
46
package.json
|
@ -5,42 +5,40 @@
|
|||
"description": "F-List Exported",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@fortawesome/fontawesome-free-webfonts": "^1.0.6",
|
||||
"@types/lodash": "^4.14.116",
|
||||
"@types/node": "^10.11.2",
|
||||
"@types/sortablejs": "^1.3.31",
|
||||
"@f-list/fork-ts-checker-webpack-plugin": "^0.5.2",
|
||||
"@f-list/vue-ts": "^1.0.2",
|
||||
"@fortawesome/fontawesome-free": "^5.6.1",
|
||||
"@types/lodash": "^4.14.119",
|
||||
"@types/sortablejs": "^1.7.0",
|
||||
"axios": "^0.18.0",
|
||||
"bootstrap": "^4.1.3",
|
||||
"css-loader": "^1.0.0",
|
||||
"date-fns": "^1.28.5",
|
||||
"electron": "^3.0.2",
|
||||
"css-loader": "^2.0.1",
|
||||
"date-fns": "^1.30.1",
|
||||
"electron": "3.0.13",
|
||||
"electron-log": "^2.2.17",
|
||||
"electron-packager": "^12.1.2",
|
||||
"electron-packager": "^13.0.1",
|
||||
"electron-rebuild": "^1.8.2",
|
||||
"extract-loader": "^3.0.0",
|
||||
"extract-loader": "^3.1.0",
|
||||
"file-loader": "^2.0.0",
|
||||
"fork-ts-checker-webpack-plugin": "^0.4.9",
|
||||
"lodash": "^4.17.11",
|
||||
"node-sass": "^4.9.3",
|
||||
"node-sass": "^4.11.0",
|
||||
"optimize-css-assets-webpack-plugin": "^5.0.1",
|
||||
"qs": "^6.5.1",
|
||||
"qs": "^6.6.0",
|
||||
"raven-js": "^3.27.0",
|
||||
"sass-loader": "^7.1.0",
|
||||
"sortablejs": "^1.6.0",
|
||||
"style-loader": "^0.23.0",
|
||||
"ts-loader": "^5.2.1",
|
||||
"tslib": "^1.7.1",
|
||||
"tslint": "^5.7.0",
|
||||
"typescript": "^3.1.1",
|
||||
"vue": "^2.5.17",
|
||||
"vue-class-component": "^6.0.0",
|
||||
"sortablejs": "^1.8.0-rc1",
|
||||
"style-loader": "^0.23.1",
|
||||
"ts-loader": "^5.3.1",
|
||||
"tslib": "^1.9.3",
|
||||
"tslint": "^5.12.0",
|
||||
"typescript": "^3.2.2",
|
||||
"vue": "^2.5.21",
|
||||
"vue-loader": "^15.4.2",
|
||||
"vue-property-decorator": "^7.1.1",
|
||||
"vue-template-compiler": "^2.5.17",
|
||||
"webpack": "^4.20.2"
|
||||
"vue-template-compiler": "^2.5.21",
|
||||
"webpack": "^4.27.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"keytar": "^4.2.1",
|
||||
"keytar": "^4.3.0",
|
||||
"spellchecker": "^3.5.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
|
|
|
@ -165,7 +165,6 @@
|
|||
|
||||
.message {
|
||||
word-wrap: break-word;
|
||||
word-break: break-word;
|
||||
padding-bottom: 1px;
|
||||
}
|
||||
|
||||
|
@ -185,6 +184,10 @@
|
|||
color: color-yiq(theme-color("danger"));
|
||||
}
|
||||
|
||||
.messages {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.messages-both {
|
||||
.message-ad {
|
||||
background-color: theme-color-level("info", -4);
|
||||
|
|
|
@ -46,8 +46,6 @@
|
|||
overflow-wrap: break-word;
|
||||
word-wrap: break-word;
|
||||
|
||||
word-break: break-word; // Non standard form used in some browsers.
|
||||
|
||||
-ms-hyphens: auto;
|
||||
-moz-hyphens: auto;
|
||||
-webkit-hyphens: auto;
|
||||
|
|
|
@ -11,6 +11,28 @@ hr {
|
|||
font-weight: bold;
|
||||
}
|
||||
|
||||
.nav-tabs-scroll {
|
||||
overflow-x: auto;
|
||||
|
||||
.nav-tabs {
|
||||
flex-wrap: nowrap;
|
||||
|
||||
.nav-item {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sub {
|
||||
position: static;
|
||||
vertical-align: sub;
|
||||
}
|
||||
|
||||
sup {
|
||||
position: static;
|
||||
vertical-align: super;
|
||||
}
|
||||
|
||||
$theme-is-dark: false !default;
|
||||
|
||||
// HACK: Bootstrap offers no way to override these by default, and they are SUPER bright.
|
||||
|
|
|
@ -4,4 +4,8 @@
|
|||
|
||||
@function theme-color-border($color-name: "primary") {
|
||||
@return theme-color-level($color-name, -9);
|
||||
}
|
||||
|
||||
@mixin text-outline($color) {
|
||||
text-shadow: $color 1px 0, $color -1px 0, $color 0 1px, $color 0 -1px, $color 1px 1px, $color -1px 1px, $color 1px -1px, $color -1px -1px;
|
||||
}
|
10
scss/fa.scss
10
scss/fa.scss
|
@ -1,5 +1,5 @@
|
|||
$fa-font-path: "~@fortawesome/fontawesome-free-webfonts/webfonts" !default;
|
||||
@import "~@fortawesome/fontawesome-free-webfonts/scss/fontawesome.scss";
|
||||
@import "~@fortawesome/fontawesome-free-webfonts/scss/fa-solid.scss";
|
||||
@import "~@fortawesome/fontawesome-free-webfonts/scss/fa-regular.scss";
|
||||
@import "~@fortawesome/fontawesome-free-webfonts/scss/fa-brands.scss";
|
||||
$fa-font-path: "~@fortawesome/fontawesome-free/webfonts" !default;
|
||||
@import "~@fortawesome/fontawesome-free/scss/fontawesome.scss";
|
||||
@import "~@fortawesome/fontawesome-free/scss/solid.scss";
|
||||
@import "~@fortawesome/fontawesome-free/scss/regular.scss";
|
||||
@import "~@fortawesome/fontawesome-free/scss/brands.scss";
|
|
@ -1,5 +1,5 @@
|
|||
$blue-color: #06f;
|
||||
|
||||
.blackText {
|
||||
text-shadow: $gray-600 1px 1px, $gray-600 -1px 1px, $gray-600 1px -1px, $gray-600 -1px -1px;
|
||||
@include text-outline($gray-600);
|
||||
}
|
|
@ -1,9 +1,9 @@
|
|||
.purpleText {
|
||||
text-shadow: #306 1px 1px, #306 -1px 1px, #306 1px -1px, #306 -1px -1px;
|
||||
@include text-outline(#306);
|
||||
}
|
||||
|
||||
.blackText {
|
||||
text-shadow: $gray-600 1px 1px, $gray-600 -1px 1px, $gray-600 1px -1px, $gray-600 -1px -1px;
|
||||
@include text-outline($gray-600);
|
||||
}
|
||||
|
||||
$blue-color: #06f;
|
|
@ -1,3 +1,3 @@
|
|||
.whiteText {
|
||||
text-shadow: $gray-600 1px 1px 1px, $gray-600 -1px 1px 1px, $gray-600 1px -1px 1px, $gray-600 -1px -1px 1px;
|
||||
@include text-outline($gray-600);
|
||||
}
|
|
@ -1,7 +1,9 @@
|
|||
<template>
|
||||
<div class="row character-page" id="pageBody">
|
||||
<div class="alert alert-info" v-show="loading" style="margin:0 15px;flex:1">Loading character information.</div>
|
||||
<div class="alert alert-danger" v-show="error" style="margin:0 15px;flex:1">{{error}}</div>
|
||||
<div class="col-12" style="min-height:0">
|
||||
<div class="alert alert-info" v-show="loading">Loading character information.</div>
|
||||
<div class="alert alert-danger" v-show="error">{{error}}</div>
|
||||
</div>
|
||||
<div class="col-md-4 col-lg-3 col-xl-2" v-if="!loading && character">
|
||||
<sidebar :character="character" @memo="memo" @bookmarked="bookmarked" :oldApi="oldApi"></sidebar>
|
||||
</div>
|
||||
|
@ -33,25 +35,25 @@
|
|||
</div>
|
||||
<div class="card-body">
|
||||
<div class="tab-content">
|
||||
<div role="tabpanel" class="tab-pane" :class="{active: tab == 0}" id="overview">
|
||||
<div role="tabpanel" class="tab-pane" :class="{active: tab === '0'}" id="overview">
|
||||
<div v-bbcode="character.character.description" style="margin-bottom: 10px"></div>
|
||||
<character-kinks :character="character" :oldApi="oldApi" ref="tab0"></character-kinks>
|
||||
</div>
|
||||
<div role="tabpanel" class="tab-pane" :class="{active: tab == 1}" id="infotags">
|
||||
<div role="tabpanel" class="tab-pane" :class="{active: tab === '1'}" id="infotags">
|
||||
<character-infotags :character="character" ref="tab1"></character-infotags>
|
||||
</div>
|
||||
<div role="tabpanel" class="tab-pane" id="groups" :class="{active: tab == 2}" v-if="!oldApi">
|
||||
<div role="tabpanel" class="tab-pane" id="groups" :class="{active: tab === '2'}" v-if="!oldApi">
|
||||
<character-groups :character="character" ref="tab2"></character-groups>
|
||||
</div>
|
||||
<div role="tabpanel" class="tab-pane" id="images" :class="{active: tab == 3}">
|
||||
<div role="tabpanel" class="tab-pane" id="images" :class="{active: tab === '3'}">
|
||||
<character-images :character="character" ref="tab3" :use-preview="imagePreview"></character-images>
|
||||
</div>
|
||||
<div v-if="character.settings.guestbook" role="tabpanel" class="tab-pane" :class="{active: tab == 4}"
|
||||
<div v-if="character.settings.guestbook" role="tabpanel" class="tab-pane" :class="{active: tab === '4'}"
|
||||
id="guestbook">
|
||||
<character-guestbook :character="character" :oldApi="oldApi" ref="tab4"></character-guestbook>
|
||||
</div>
|
||||
<div v-if="character.is_self || character.settings.show_friends" role="tabpanel" class="tab-pane"
|
||||
:class="{active: tab == 5}" id="friends">
|
||||
:class="{active: tab === '5'}" id="friends">
|
||||
<character-friends :character="character" ref="tab5"></character-friends>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -64,9 +66,8 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {Component, Hook, Prop, Watch} from '@f-list/vue-ts';
|
||||
import Vue from 'vue';
|
||||
import Component from 'vue-class-component';
|
||||
import {Prop, Watch} from 'vue-property-decorator';
|
||||
import {standardParser} from '../../bbcode/standard';
|
||||
import * as Utils from '../utils';
|
||||
import {methods, Store} from './data_store';
|
||||
|
@ -99,29 +100,30 @@
|
|||
}
|
||||
})
|
||||
export default class CharacterPage extends Vue {
|
||||
//tslint:disable:no-null-keyword
|
||||
@Prop()
|
||||
private readonly name?: string;
|
||||
readonly name?: string;
|
||||
@Prop()
|
||||
private readonly characterid?: number;
|
||||
readonly characterid?: number;
|
||||
@Prop({required: true})
|
||||
private readonly authenticated!: boolean;
|
||||
readonly authenticated!: boolean;
|
||||
@Prop()
|
||||
readonly oldApi?: true;
|
||||
@Prop()
|
||||
readonly imagePreview?: true;
|
||||
private shared: SharedStore = Store;
|
||||
private character: Character | null = null;
|
||||
shared: SharedStore = Store;
|
||||
character: Character | undefined;
|
||||
loading = true;
|
||||
error = '';
|
||||
tab = '0';
|
||||
|
||||
@Hook('beforeMount')
|
||||
beforeMount(): void {
|
||||
this.shared.authenticated = this.authenticated;
|
||||
}
|
||||
|
||||
@Hook('mounted')
|
||||
async mounted(): Promise<void> {
|
||||
if(this.character === null) await this._getCharacter();
|
||||
if(this.character === undefined) await this._getCharacter();
|
||||
}
|
||||
|
||||
@Watch('tab')
|
||||
|
@ -147,7 +149,7 @@
|
|||
|
||||
private async _getCharacter(): Promise<void> {
|
||||
this.error = '';
|
||||
this.character = null;
|
||||
this.character = undefined;
|
||||
if(this.name === undefined || this.name.length === 0)
|
||||
return;
|
||||
try {
|
||||
|
|
|
@ -12,9 +12,8 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {Component, Prop} from '@f-list/vue-ts';
|
||||
import Vue from 'vue';
|
||||
import Component from 'vue-class-component';
|
||||
import {Prop} from 'vue-property-decorator';
|
||||
import {formatContactLink, formatContactValue} from './contact_utils';
|
||||
import {methods, Store} from './data_store';
|
||||
|
||||
|
|
|
@ -1,12 +1,13 @@
|
|||
import {Component} from '@f-list/vue-ts';
|
||||
import Vue from 'vue';
|
||||
|
||||
@Component
|
||||
export default abstract class ContextMenu extends Vue {
|
||||
//tslint:disable:no-null-keyword
|
||||
abstract propName: string;
|
||||
showMenu = false;
|
||||
private position = {left: 0, top: 0};
|
||||
private selectedItem: HTMLElement | null = null;
|
||||
private touchTimer = 0;
|
||||
position = {left: 0, top: 0};
|
||||
selectedItem: HTMLElement | undefined;
|
||||
touchTimer = 0;
|
||||
|
||||
abstract itemSelected(element: HTMLElement): void;
|
||||
|
||||
|
@ -16,7 +17,7 @@ export default abstract class ContextMenu extends Vue {
|
|||
|
||||
hideMenu(): void {
|
||||
this.showMenu = false;
|
||||
this.selectedItem = null;
|
||||
this.selectedItem = undefined;
|
||||
}
|
||||
|
||||
bindOffclick(): void {
|
||||
|
@ -40,7 +41,7 @@ export default abstract class ContextMenu extends Vue {
|
|||
this.position = {left, top};
|
||||
}
|
||||
|
||||
protected innerClick(): void {
|
||||
innerClick(): void {
|
||||
this.itemSelected(this.selectedItem!);
|
||||
this.hideMenu();
|
||||
}
|
||||
|
@ -84,8 +85,8 @@ export default abstract class ContextMenu extends Vue {
|
|||
});
|
||||
}
|
||||
|
||||
get positionText(): string {
|
||||
return `left: ${this.position.left}px; top: ${this.position.top}px;`;
|
||||
get positionStyle(): object {
|
||||
return {left: `${this.position.left}px`, top: `${this.position.top}px;`};
|
||||
}
|
||||
|
||||
}
|
|
@ -1,11 +1,11 @@
|
|||
<template>
|
||||
<modal id="copyCustomDialog" action="Copy Custom Kink" :disabled="!valid || submitting" @submit.prevent="copyCustom">
|
||||
<modal id="copyCustomDialog" action="Copy Custom Kink" :disabled="!valid || submitting" @submit.prevent="copyCustom()">
|
||||
<form-group field="name" :errors="formErrors" label="Name" id="copyCustomName">
|
||||
<input type="text" class="form-control" maxlength="30" required v-model="name" id="copyCustomName"
|
||||
slot-scope="props" :class="props.cls"/>
|
||||
<input type="text" class="form-control" maxlength="30" required v-model="name" slot-scope="props" id="copyCustomName"
|
||||
:class="props.cls"/>
|
||||
</form-group>
|
||||
<form-group field="description" :errors="formErrors" label="Description" id="copyCustomDescription">
|
||||
<input type="text" class="form-control" max-length="250" id="copyCustomDescription" v-model="description" required
|
||||
<input type="text" class="form-control" max-length="250" v-model="description" required id="copyCustomDescription"
|
||||
slot-scope="props" :class="props.cls"/>
|
||||
</form-group>
|
||||
<form-group field="choice" :errors="formErrors" label="Choice" id="copyCustomChoice">
|
||||
|
@ -17,28 +17,28 @@
|
|||
</select>
|
||||
</form-group>
|
||||
<form-group field="target" :errors="formErrors" label="Target Character" id="copyCustomTarget">
|
||||
<character-select id="copyCustomTarget" v-model="target" slot-scope="props" :class="props.cls"></character-select>
|
||||
<character-select v-model="target" slot-scope="props" :class="props.cls" id="copyCustomTarget"></character-select>
|
||||
</form-group>
|
||||
</modal>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Component from 'vue-class-component';
|
||||
import {Component} from '@f-list/vue-ts';
|
||||
import CustomDialog from '../../components/custom_dialog';
|
||||
import FormGroup from '../../components/form_group.vue';
|
||||
import Modal from '../../components/Modal.vue';
|
||||
import {KinkChoice} from '../../interfaces';
|
||||
import * as Utils from '../utils';
|
||||
import {methods} from './data_store';
|
||||
import {KinkChoice} from './interfaces';
|
||||
|
||||
@Component({
|
||||
components: {'form-group': FormGroup, modal: Modal}
|
||||
})
|
||||
export default class CopyCustomDialog extends CustomDialog {
|
||||
private name = '';
|
||||
private description = '';
|
||||
private choice: KinkChoice = 'favorite';
|
||||
private target = Utils.Settings.defaultCharacter;
|
||||
name = '';
|
||||
description = '';
|
||||
choice: KinkChoice = 'favorite';
|
||||
target = Utils.Settings.defaultCharacter;
|
||||
formErrors = {};
|
||||
submitting = false;
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<template>
|
||||
<div>
|
||||
<ul class="dropdown-menu" role="menu" @click="innerClick($event)" @touchstart="innerClick($event)" @touchend="innerClick($event)"
|
||||
style="position: fixed; display: block;" :style="positionText" ref="menu" v-show="showMenu">
|
||||
<ul class="dropdown-menu" role="menu" @click="innerClick" @touchstart="innerClick" @touchend="innerClick"
|
||||
style="position: fixed; display: block;" :style="positionStyle" ref="menu" v-show="showMenu">
|
||||
<li><a class="dropdown-item" href="#">Copy Custom</a></li>
|
||||
</ul>
|
||||
<copy-dialog ref="copy-dialog"></copy-dialog>
|
||||
|
@ -9,15 +9,12 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Component from 'vue-class-component';
|
||||
import {Prop} from 'vue-property-decorator';
|
||||
import {Component, Hook, Prop} from '@f-list/vue-ts';
|
||||
import ContextMenu from './context_menu';
|
||||
import CopyCustomDialog from './copy_custom_dialog.vue';
|
||||
|
||||
@Component({
|
||||
components: {
|
||||
'copy-dialog': CopyCustomDialog
|
||||
}
|
||||
components: {'copy-dialog': CopyCustomDialog}
|
||||
})
|
||||
export default class CopyCustomMenu extends ContextMenu {
|
||||
@Prop({required: true})
|
||||
|
@ -35,6 +32,7 @@
|
|||
(<CopyCustomDialog>this.$refs['copy-dialog']).showDialog(name, description);
|
||||
}
|
||||
|
||||
@Hook('mounted')
|
||||
mounted(): void {
|
||||
this.bindOffclick();
|
||||
}
|
||||
|
|
|
@ -1,13 +1,12 @@
|
|||
<template>
|
||||
<modal id="deleteDialog" :action="'Delete character' + name" :disabled="deleting" @submit.prevent="deleteCharacter">
|
||||
<modal id="deleteDialog" :action="'Delete character' + name" :disabled="deleting" @submit.prevent="deleteCharacter()">
|
||||
Are you sure you want to permanently delete {{ name }}?<br/>
|
||||
Character deletion cannot be undone for any reason.
|
||||
</modal>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Component from 'vue-class-component';
|
||||
import {Prop} from 'vue-property-decorator';
|
||||
import {Component, Prop} from '@f-list/vue-ts';
|
||||
import CustomDialog from '../../components/custom_dialog';
|
||||
import Modal from '../../components/Modal.vue';
|
||||
import * as Utils from '../utils';
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<modal id="duplicateDialog" :action="'Duplicate character' + name" :disabled="duplicating || checking" @submit.prevent="duplicate">
|
||||
<modal id="duplicateDialog" :action="'Duplicate character' + name" :disabled="duplicating || checking" @submit.prevent="duplicate()">
|
||||
<p>This will duplicate the character, kinks, infotags, customs, subkinks and images. Guestbook
|
||||
entries, friends, groups, and bookmarks are not duplicated.</p>
|
||||
<div class="form-row mb-2">
|
||||
|
@ -17,8 +17,7 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Component from 'vue-class-component';
|
||||
import {Prop} from 'vue-property-decorator';
|
||||
import {Component, Prop} from '@f-list/vue-ts';
|
||||
import CustomDialog from '../../components/custom_dialog';
|
||||
import FormGroupInputgroup from '../../components/form_group_inputgroup.vue';
|
||||
import Modal from '../../components/Modal.vue';
|
||||
|
@ -31,10 +30,10 @@
|
|||
})
|
||||
export default class DuplicateDialog extends CustomDialog {
|
||||
@Prop({required: true})
|
||||
private readonly character!: Character;
|
||||
readonly character!: Character;
|
||||
|
||||
errors: {[key: string]: string} = {};
|
||||
private newName = '';
|
||||
newName = '';
|
||||
valid = false;
|
||||
|
||||
checking = false;
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<Modal id="memoDialog" :action="'Friends for ' + name" :buttons="false" dialog-class="modal-dialog-centered modal-lg">
|
||||
<Modal :action="'Friends for ' + name" :buttons="false" dialog-class="modal-dialog-centered modal-lg">
|
||||
<div v-show="loading" class="alert alert-info">Loading friend information.</div>
|
||||
<div v-show="error" class="alert alert-danger">{{error}}</div>
|
||||
<template v-if="!loading">
|
||||
|
@ -79,8 +79,7 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Component from 'vue-class-component';
|
||||
import {Prop} from 'vue-property-decorator';
|
||||
import {Component, Prop} from '@f-list/vue-ts';
|
||||
import CustomDialog from '../../components/custom_dialog';
|
||||
import Modal from '../../components/Modal.vue';
|
||||
import * as Utils from '../utils';
|
||||
|
@ -92,13 +91,13 @@
|
|||
})
|
||||
export default class FriendDialog extends CustomDialog {
|
||||
@Prop({required: true})
|
||||
private readonly character!: Character;
|
||||
readonly character!: Character;
|
||||
|
||||
private ourCharacter = Utils.Settings.defaultCharacter;
|
||||
ourCharacter = Utils.Settings.defaultCharacter;
|
||||
|
||||
private incoming: FriendRequest[] = [];
|
||||
private pending: FriendRequest[] = [];
|
||||
private existing: Friend[] = [];
|
||||
incoming: FriendRequest[] = [];
|
||||
pending: FriendRequest[] = [];
|
||||
existing: Friend[] = [];
|
||||
|
||||
requesting = false;
|
||||
loading = true;
|
||||
|
|
|
@ -11,9 +11,8 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {Component, Prop} from '@f-list/vue-ts';
|
||||
import Vue from 'vue';
|
||||
import Component from 'vue-class-component';
|
||||
import {Prop} from 'vue-property-decorator';
|
||||
import * as Utils from '../utils';
|
||||
import {methods} from './data_store';
|
||||
import {Character, CharacterFriend} from './interfaces';
|
||||
|
|
|
@ -11,9 +11,8 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {Component, Prop} from '@f-list/vue-ts';
|
||||
import Vue from 'vue';
|
||||
import Component from 'vue-class-component';
|
||||
import {Prop} from 'vue-property-decorator';
|
||||
import * as Utils from '../utils';
|
||||
import {methods} from './data_store';
|
||||
import {Character, CharacterGroup} from './interfaces';
|
||||
|
|
|
@ -26,9 +26,8 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {Component, Prop, Watch} from '@f-list/vue-ts';
|
||||
import Vue from 'vue';
|
||||
import Component from 'vue-class-component';
|
||||
import {Prop, Watch} from 'vue-property-decorator';
|
||||
import * as Utils from '../utils';
|
||||
import {methods, Store} from './data_store';
|
||||
import {Character, GuestbookPost} from './interfaces';
|
||||
|
@ -36,13 +35,11 @@
|
|||
import GuestbookPostView from './guestbook_post.vue';
|
||||
|
||||
@Component({
|
||||
components: {
|
||||
'guestbook-post': GuestbookPostView
|
||||
}
|
||||
components: {'guestbook-post': GuestbookPostView}
|
||||
})
|
||||
export default class GuestbookView extends Vue {
|
||||
@Prop({required: true})
|
||||
private readonly character!: Character;
|
||||
readonly character!: Character;
|
||||
@Prop()
|
||||
readonly oldApi?: true;
|
||||
loading = true;
|
||||
|
@ -51,11 +48,11 @@
|
|||
|
||||
posts: GuestbookPost[] = [];
|
||||
|
||||
private unapprovedOnly = false;
|
||||
private page = 1;
|
||||
unapprovedOnly = false;
|
||||
page = 1;
|
||||
hasNextPage = false;
|
||||
canEdit = false;
|
||||
private newPost = {
|
||||
newPost = {
|
||||
posting: false,
|
||||
privatePost: false,
|
||||
character: Utils.Settings.defaultCharacter,
|
||||
|
|
|
@ -48,9 +48,8 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {Component, Prop} from '@f-list/vue-ts';
|
||||
import Vue from 'vue';
|
||||
import Component from 'vue-class-component';
|
||||
import {Prop} from 'vue-property-decorator';
|
||||
import CharacterLink from '../../components/character_link.vue';
|
||||
import DateDisplay from '../../components/date_display.vue';
|
||||
import * as Utils from '../utils';
|
||||
|
@ -62,13 +61,13 @@
|
|||
})
|
||||
export default class GuestbookPostView extends Vue {
|
||||
@Prop({required: true})
|
||||
private readonly post!: GuestbookPost;
|
||||
readonly post!: GuestbookPost;
|
||||
@Prop({required: true})
|
||||
readonly canEdit!: boolean;
|
||||
|
||||
replying = false;
|
||||
replyBox = false;
|
||||
private replyMessage = this.post.reply;
|
||||
replyMessage = this.post.reply;
|
||||
|
||||
approving = false;
|
||||
deleting = false;
|
||||
|
|
|
@ -10,19 +10,19 @@
|
|||
</template>
|
||||
<div v-if="!loading && !images.length" class="alert alert-info">No images.</div>
|
||||
<div class="image-preview" v-show="previewImage" @click="previewImage = ''">
|
||||
<img :src="previewImage" />
|
||||
<img :src="previewImage"/>
|
||||
<div class="modal-backdrop show"></div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {Component, Prop} from '@f-list/vue-ts';
|
||||
import Vue from 'vue';
|
||||
import Component from 'vue-class-component';
|
||||
import {Prop} from 'vue-property-decorator';
|
||||
import {CharacterImage} from '../../interfaces';
|
||||
import * as Utils from '../utils';
|
||||
import {methods} from './data_store';
|
||||
import {Character, CharacterImage} from './interfaces';
|
||||
import {Character} from './interfaces';
|
||||
|
||||
@Component
|
||||
export default class ImagesView extends Vue {
|
||||
|
|
|
@ -7,9 +7,8 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {Component, Prop} from '@f-list/vue-ts';
|
||||
import Vue from 'vue';
|
||||
import Component from 'vue-class-component';
|
||||
import {Prop} from 'vue-property-decorator';
|
||||
import {formatContactLink, formatContactValue} from './contact_utils';
|
||||
import {Store} from './data_store';
|
||||
import {DisplayInfotag} from './interfaces';
|
||||
|
|
|
@ -9,9 +9,8 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {Component, Prop} from '@f-list/vue-ts';
|
||||
import Vue from 'vue';
|
||||
import Component from 'vue-class-component';
|
||||
import {Prop} from 'vue-property-decorator';
|
||||
import * as Utils from '../utils';
|
||||
import {Store} from './data_store';
|
||||
import {Character, CONTACT_GROUP_ID, DisplayInfotag} from './interfaces';
|
||||
|
@ -19,15 +18,14 @@
|
|||
import InfotagView from './infotag.vue';
|
||||
|
||||
interface DisplayInfotagGroup {
|
||||
id: number
|
||||
name: string
|
||||
sortOrder: number
|
||||
infotags: DisplayInfotag[]
|
||||
}
|
||||
|
||||
@Component({
|
||||
components: {
|
||||
infotag: InfotagView
|
||||
}
|
||||
components: {infotag: InfotagView}
|
||||
})
|
||||
export default class InfotagsView extends Vue {
|
||||
@Prop({required: true})
|
||||
|
@ -63,6 +61,7 @@
|
|||
return infotagA.name < infotagB.name ? -1 : 1;
|
||||
});
|
||||
outputGroups.push({
|
||||
id: group.id,
|
||||
name: group.name,
|
||||
sortOrder: group.sort_order,
|
||||
infotags: collectedTags
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
import {Character as CharacterInfo, CharacterImage, CharacterSettings, Infotag, Kink, KinkChoice} from '../../interfaces';
|
||||
|
||||
export interface CharacterMenuItem {
|
||||
label: string
|
||||
permission: string
|
||||
link(character: Character): string
|
||||
handleClick?(evt?: MouseEvent): void
|
||||
handleClick?(evt: MouseEvent): void
|
||||
}
|
||||
|
||||
export interface SelectItem {
|
||||
|
@ -69,7 +71,6 @@ export interface SharedKinks {
|
|||
}
|
||||
|
||||
export type SiteDate = number | string | null;
|
||||
export type KinkChoice = 'favorite' | 'yes' | 'maybe' | 'no';
|
||||
export type KinkChoiceFull = KinkChoice | number;
|
||||
export const CONTACT_GROUP_ID = '1';
|
||||
|
||||
|
@ -93,13 +94,6 @@ export interface DisplayInfotag {
|
|||
list?: number
|
||||
}
|
||||
|
||||
export interface Kink {
|
||||
id: number
|
||||
name: string
|
||||
description: string
|
||||
kink_group: number
|
||||
}
|
||||
|
||||
export interface KinkGroup {
|
||||
id: number
|
||||
name: string
|
||||
|
@ -107,16 +101,6 @@ export interface KinkGroup {
|
|||
sort_order: number
|
||||
}
|
||||
|
||||
export interface Infotag {
|
||||
id: number
|
||||
name: string
|
||||
type: 'number' | 'text' | 'list'
|
||||
search_field: string
|
||||
validator: string
|
||||
allow_legacy: boolean
|
||||
infotag_group: string
|
||||
}
|
||||
|
||||
export interface InfotagGroup {
|
||||
id: number
|
||||
name: string
|
||||
|
@ -141,46 +125,6 @@ export interface CharacterKink {
|
|||
choice: KinkChoice
|
||||
}
|
||||
|
||||
export interface CharacterInfotag {
|
||||
list?: number
|
||||
string?: string
|
||||
number?: number
|
||||
}
|
||||
|
||||
export interface CharacterCustom {
|
||||
id: number
|
||||
choice: KinkChoice
|
||||
name: string
|
||||
description: string
|
||||
}
|
||||
|
||||
export interface CharacterInline {
|
||||
id: number
|
||||
hash: string
|
||||
extension: string
|
||||
nsfw: boolean
|
||||
}
|
||||
|
||||
export type CharacterImage = CharacterImageOld | CharacterImageNew;
|
||||
|
||||
export interface CharacterImageNew {
|
||||
id: number
|
||||
extension: string
|
||||
description: string
|
||||
hash: string
|
||||
sort_order: number | null
|
||||
}
|
||||
|
||||
export interface CharacterImageOld {
|
||||
id: number
|
||||
extension: string
|
||||
height: number
|
||||
width: number
|
||||
description: string
|
||||
sort_order: number | null
|
||||
url: string
|
||||
}
|
||||
|
||||
export type CharacterName = string | CharacterNameDetails;
|
||||
|
||||
export interface CharacterNameDetails {
|
||||
|
@ -211,34 +155,6 @@ export interface CharacterGroup {
|
|||
owner: boolean
|
||||
}
|
||||
|
||||
export interface CharacterInfo {
|
||||
readonly id: number
|
||||
readonly name: string
|
||||
readonly description: string
|
||||
readonly title?: string
|
||||
readonly created_at: SiteDate
|
||||
readonly updated_at: SiteDate
|
||||
readonly views: number
|
||||
readonly last_online_at?: SiteDate
|
||||
readonly timezone?: number
|
||||
readonly image_count?: number
|
||||
readonly inlines: {[key: string]: CharacterInline | undefined}
|
||||
images?: CharacterImage[]
|
||||
readonly kinks: {[key: string]: KinkChoiceFull | undefined}
|
||||
readonly customs: CharacterCustom[]
|
||||
readonly infotags: {[key: string]: CharacterInfotag | undefined}
|
||||
readonly online_chat?: boolean
|
||||
}
|
||||
|
||||
export interface CharacterSettings {
|
||||
readonly customs_first: boolean
|
||||
readonly show_friends: boolean
|
||||
readonly badges: boolean
|
||||
readonly guestbook: boolean
|
||||
readonly prevent_bookmarks: boolean
|
||||
readonly public: boolean
|
||||
}
|
||||
|
||||
export interface Character {
|
||||
readonly is_self: boolean
|
||||
character: CharacterInfo
|
||||
|
|
|
@ -19,9 +19,8 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {Component, Prop} from '@f-list/vue-ts';
|
||||
import Vue from 'vue';
|
||||
import Component from 'vue-class-component';
|
||||
import {Prop} from 'vue-property-decorator';
|
||||
import {DisplayKink} from './interfaces';
|
||||
|
||||
@Component({
|
||||
|
|
|
@ -9,8 +9,8 @@
|
|||
</div>
|
||||
<div class="form-inline">
|
||||
<select v-model="highlightGroup" class="form-control">
|
||||
<option :value="null">None</option>
|
||||
<option v-for="group in kinkGroups" :value="group.id" :key="group.id">{{group.name}}</option>
|
||||
<option :value="undefined">None</option>
|
||||
<option v-for="group in kinkGroups" v-if="group" :value="group.id" :key="group.id">{{group.name}}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -65,33 +65,29 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {Component, Prop, Watch} from '@f-list/vue-ts';
|
||||
import Vue from 'vue';
|
||||
import Component from 'vue-class-component';
|
||||
import {Prop, Watch} from 'vue-property-decorator';
|
||||
import {Kink, KinkChoice} from '../../interfaces';
|
||||
import * as Utils from '../utils';
|
||||
import CopyCustomMenu from './copy_custom_menu.vue';
|
||||
import {methods, Store} from './data_store';
|
||||
import {Character, DisplayKink, Kink, KinkChoice, KinkGroup} from './interfaces';
|
||||
import {Character, DisplayKink, KinkGroup} from './interfaces';
|
||||
import KinkView from './kink.vue';
|
||||
|
||||
@Component({
|
||||
components: {
|
||||
'context-menu': CopyCustomMenu,
|
||||
kink: KinkView
|
||||
}
|
||||
components: {'context-menu': CopyCustomMenu, kink: KinkView}
|
||||
})
|
||||
export default class CharacterKinksView extends Vue {
|
||||
//tslint:disable:no-null-keyword
|
||||
@Prop({required: true})
|
||||
private readonly character!: Character;
|
||||
readonly character!: Character;
|
||||
@Prop()
|
||||
readonly oldApi?: true;
|
||||
private shared = Store;
|
||||
shared = Store;
|
||||
characterToCompare = Utils.Settings.defaultCharacter;
|
||||
highlightGroup: number | null = null;
|
||||
highlightGroup: number | undefined;
|
||||
|
||||
private loading = false;
|
||||
private comparing = false;
|
||||
loading = false;
|
||||
comparing = false;
|
||||
highlighting: {[key: string]: boolean} = {};
|
||||
comparison: {[key: string]: KinkChoice} = {};
|
||||
|
||||
|
@ -142,7 +138,7 @@
|
|||
return this.comparing ? 'Clear' : 'Compare';
|
||||
}
|
||||
|
||||
get groupedKinks(): {[key in KinkChoice]: DisplayKink[]} | undefined {
|
||||
get groupedKinks(): {[key in KinkChoice]: DisplayKink[]} {
|
||||
const kinks = this.shared.kinks.kinks;
|
||||
const characterKinks = this.character.character.kinks;
|
||||
const characterCustoms = this.character.character.customs;
|
||||
|
@ -167,8 +163,9 @@
|
|||
return a.name < b.name ? -1 : 1;
|
||||
};
|
||||
|
||||
for(const custom of characterCustoms)
|
||||
displayCustoms[custom.id] = {
|
||||
for(const id in characterCustoms) {
|
||||
const custom = characterCustoms[id]!;
|
||||
displayCustoms[id] = {
|
||||
id: custom.id,
|
||||
name: custom.name,
|
||||
description: custom.description,
|
||||
|
@ -179,6 +176,7 @@
|
|||
ignore: false,
|
||||
subkinks: []
|
||||
};
|
||||
}
|
||||
|
||||
for(const kinkId in characterKinks) {
|
||||
const kinkChoice = characterKinks[kinkId]!;
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<Modal id="memoDialog" :action="'Memo for ' + name" buttonText="Save and Close" @close="onClose" @submit="save" dialog-class="modal-lg modal-dialog-centered">
|
||||
<Modal :action="'Memo for ' + name" buttonText="Save and Close" @close="onClose" @submit="save" dialog-class="modal-lg modal-dialog-centered">
|
||||
<div class="form-group" v-if="editing">
|
||||
<textarea v-model="message" maxlength="1000" class="form-control"></textarea>
|
||||
</div>
|
||||
|
@ -12,33 +12,46 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Component from 'vue-class-component';
|
||||
import {Prop} from 'vue-property-decorator';
|
||||
import {Component, Prop, Watch} from '@f-list/vue-ts';
|
||||
import CustomDialog from '../../components/custom_dialog';
|
||||
import Modal from '../../components/Modal.vue';
|
||||
import {SimpleCharacter} from '../../interfaces';
|
||||
import * as Utils from '../utils';
|
||||
import {methods} from './data_store';
|
||||
import {Character} from './interfaces';
|
||||
|
||||
export interface Memo {
|
||||
id: number
|
||||
memo: string
|
||||
character: SimpleCharacter
|
||||
created_at: number
|
||||
updated_at: number
|
||||
}
|
||||
|
||||
@Component({
|
||||
components: {Modal}
|
||||
})
|
||||
export default class MemoDialog extends CustomDialog {
|
||||
@Prop({required: true})
|
||||
private readonly character!: Character;
|
||||
|
||||
private message = '';
|
||||
readonly character!: {id: number, name: string};
|
||||
@Prop()
|
||||
readonly memo?: Memo;
|
||||
message = '';
|
||||
editing = false;
|
||||
saving = false;
|
||||
|
||||
get name(): string {
|
||||
return this.character.character.name;
|
||||
return this.character.name;
|
||||
}
|
||||
|
||||
show(): void {
|
||||
super.show();
|
||||
if(this.character.memo !== undefined)
|
||||
this.message = this.character.memo.memo;
|
||||
this.setMemo();
|
||||
}
|
||||
|
||||
@Watch('memo')
|
||||
setMemo(): void {
|
||||
if(this.memo !== undefined)
|
||||
this.message = this.memo.memo;
|
||||
}
|
||||
|
||||
onClose(): void {
|
||||
|
@ -48,7 +61,7 @@
|
|||
async save(): Promise<void> {
|
||||
try {
|
||||
this.saving = true;
|
||||
const memoReply = await methods.memoUpdate(this.character.character.id, this.message);
|
||||
const memoReply = await methods.memoUpdate(this.character.id, this.message);
|
||||
this.$emit('memo', this.message !== '' ? memoReply : undefined);
|
||||
this.hide();
|
||||
} catch(e) {
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<modal id="reportDialog" :action="'Report character' + name" :disabled="!dataValid || submitting" @submit.prevent="submitReport">
|
||||
<modal id="reportDialog" :action="'Report character' + name" :disabled="!dataValid || submitting" @submit.prevent="submitReport()">
|
||||
<div class="form-group">
|
||||
<label>Type</label>
|
||||
<select v-select="validTypes" v-model="type" class="form-control"></select>
|
||||
|
@ -25,8 +25,7 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Component from 'vue-class-component';
|
||||
import {Prop} from 'vue-property-decorator';
|
||||
import {Component, Prop} from '@f-list/vue-ts';
|
||||
import CustomDialog from '../../components/custom_dialog';
|
||||
import Modal from '../../components/Modal.vue';
|
||||
import * as Utils from '../utils';
|
||||
|
@ -38,12 +37,12 @@
|
|||
})
|
||||
export default class ReportDialog extends CustomDialog {
|
||||
@Prop({required: true})
|
||||
private readonly character!: Character;
|
||||
readonly character!: Character;
|
||||
|
||||
private ourCharacter = Utils.Settings.defaultCharacter;
|
||||
private type = '';
|
||||
private violation = '';
|
||||
private message = '';
|
||||
ourCharacter = Utils.Settings.defaultCharacter;
|
||||
type = '';
|
||||
violation = '';
|
||||
message = '';
|
||||
|
||||
submitting = false;
|
||||
|
||||
|
|
|
@ -3,7 +3,8 @@
|
|||
<div class="card-header">
|
||||
<span class="character-name">{{ character.character.name }}</span>
|
||||
<div v-if="character.character.title" class="character-title">{{ character.character.title }}</div>
|
||||
<character-action-menu :character="character"></character-action-menu>
|
||||
<character-action-menu :character="character" @rename="showRename()" @delete="showDelete()"
|
||||
@block="showBlock()"></character-action-menu>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<img :src="avatarUrl(character.character.name)" class="character-avatar" style="margin-right:10px">
|
||||
|
@ -11,21 +12,21 @@
|
|||
<template v-if="character.is_self">
|
||||
<a :href="editUrl" class="edit-link"><i class="fa fa-fw fa-pencil-alt"></i>Edit</a>
|
||||
<a @click="showDelete" class="delete-link"><i class="fa fa-fw fa-trash"></i>Delete</a>
|
||||
<a @click="showDuplicate" class="duplicate-link"><i class="fa fa-fw fa-copy"></i>Duplicate</a>
|
||||
<a @click="showDuplicate()" class="duplicate-link"><i class="fa fa-fw fa-copy"></i>Duplicate</a>
|
||||
</template>
|
||||
<template v-else>
|
||||
<span v-if="character.self_staff || character.settings.prevent_bookmarks !== true">
|
||||
<a @click.prevent="toggleBookmark" :class="{bookmarked: character.bookmarked, unbookmarked: !character.bookmarked}"
|
||||
<span v-if="character.self_staff || character.settings.block_bookmarks !== true">
|
||||
<a @click.prevent="toggleBookmark()" :class="{bookmarked: character.bookmarked, unbookmarked: !character.bookmarked}"
|
||||
href="#" class="btn">
|
||||
<i class="fa fa-fw" :class="{'fa-minus': character.bookmarked, 'fa-plus': !character.bookmarked}"></i>Bookmark
|
||||
</a>
|
||||
<span v-if="character.settings.prevent_bookmarks" class="prevents-bookmarks">!</span>
|
||||
<span v-if="character.settings.block_bookmarks" class="prevents-bookmarks">!</span>
|
||||
</span>
|
||||
<a href="#" @click.prevent="showFriends" class="friend-link btn"><i class="fa fa-fw fa-user"></i>Friend</a>
|
||||
<a href="#" v-if="!oldApi" @click.prevent="showReport" class="report-link btn">
|
||||
<a href="#" @click.prevent="showFriends()" class="friend-link btn"><i class="fa fa-fw fa-user"></i>Friend</a>
|
||||
<a href="#" v-if="!oldApi" @click.prevent="showReport()" class="report-link btn">
|
||||
<i class="fa fa-fw fa-exclamation-triangle"></i>Report</a>
|
||||
</template>
|
||||
<a href="#" @click.prevent="showMemo" class="memo-link btn"><i class="far fa-sticky-note fa-fw"></i>Memo</a>
|
||||
<a href="#" @click.prevent="showMemo()" class="memo-link btn"><i class="far fa-sticky-note fa-fw"></i>Memo</a>
|
||||
</div>
|
||||
<div v-if="character.badges && character.badges.length > 0" class="badges-block">
|
||||
<div v-for="badge in character.badges" class="character-badge px-2 py-1" :class="badgeClass(badge)">
|
||||
|
@ -35,7 +36,7 @@
|
|||
|
||||
<a v-if="authenticated && !character.is_self" :href="noteUrl" class="character-page-note-link btn" style="padding:0 4px">
|
||||
<i class="far fa-envelope fa-fw"></i>Send Note</a>
|
||||
<div v-if="character.character.online_chat" @click="showInChat" class="character-page-online-chat">Online In Chat</div>
|
||||
<div v-if="character.character.online_chat" @click="showInChat()" class="character-page-online-chat">Online In Chat</div>
|
||||
|
||||
<div class="contact-block">
|
||||
<contact-method v-for="method in contactMethods" :method="method" :key="method.id"></contact-method>
|
||||
|
@ -67,7 +68,7 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="character-list-block">
|
||||
<div class="character-list-block" v-if="character.character_list">
|
||||
<div v-for="listCharacter in character.character_list">
|
||||
<img :src="avatarUrl(listCharacter.name)" class="character-avatar icon" style="margin-right:5px">
|
||||
<character-link :character="listCharacter.name"></character-link>
|
||||
|
@ -75,7 +76,7 @@
|
|||
</div>
|
||||
</div>
|
||||
<template>
|
||||
<memo-dialog :character="character" ref="memo-dialog" @memo="memo"></memo-dialog>
|
||||
<memo-dialog :character="character.character" :memo="character.memo" ref="memo-dialog" @memo="memo"></memo-dialog>
|
||||
<delete-dialog :character="character" ref="delete-dialog"></delete-dialog>
|
||||
<rename-dialog :character="character" ref="rename-dialog"></rename-dialog>
|
||||
<duplicate-dialog :character="character" ref="duplicate-dialog"></duplicate-dialog>
|
||||
|
@ -87,20 +88,18 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {Component, Prop} from '@f-list/vue-ts';
|
||||
import Vue, {Component as VueComponent, ComponentOptions, CreateElement, VNode} from 'vue';
|
||||
import Component from 'vue-class-component';
|
||||
import {Prop} from 'vue-property-decorator';
|
||||
import * as Utils from '../utils';
|
||||
import {methods, registeredComponents, Store} from './data_store';
|
||||
import {Character, CONTACT_GROUP_ID, Infotag, SharedStore} from './interfaces';
|
||||
|
||||
import DateDisplay from '../../components/date_display.vue';
|
||||
import InfotagView from './infotag.vue';
|
||||
|
||||
import {Infotag} from '../../interfaces';
|
||||
import * as Utils from '../utils';
|
||||
import ContactMethodView from './contact_method.vue';
|
||||
import {methods, registeredComponents, Store} from './data_store';
|
||||
import DeleteDialog from './delete_dialog.vue';
|
||||
import DuplicateDialog from './duplicate_dialog.vue';
|
||||
import FriendDialog from './friend_dialog.vue';
|
||||
import InfotagView from './infotag.vue';
|
||||
import {Character, CONTACT_GROUP_ID, SharedStore} from './interfaces';
|
||||
import MemoDialog from './memo_dialog.vue';
|
||||
import ReportDialog from './report_dialog.vue';
|
||||
|
||||
|
@ -177,6 +176,14 @@
|
|||
return badgeName in badgeMap ? badgeMap[badgeName] : badgeName;
|
||||
}
|
||||
|
||||
showBlock(): void {
|
||||
(<ShowableVueDialog>this.$refs['block-dialog']).show();
|
||||
}
|
||||
|
||||
showRename(): void {
|
||||
(<ShowableVueDialog>this.$refs['rename-dialog']).show();
|
||||
}
|
||||
|
||||
showDelete(): void {
|
||||
(<ShowableVueDialog>this.$refs['delete-dialog']).show();
|
||||
}
|
||||
|
@ -197,6 +204,10 @@
|
|||
(<ShowableVueDialog>this.$refs['friend-dialog']).show();
|
||||
}
|
||||
|
||||
showInChat(): void {
|
||||
//TODO implement this
|
||||
}
|
||||
|
||||
async toggleBookmark(): Promise<void> {
|
||||
const previousState = this.character.bookmarked;
|
||||
try {
|
||||
|
@ -218,7 +229,7 @@
|
|||
return methods.sendNoteUrl(this.character.character);
|
||||
}
|
||||
|
||||
get contactMethods(): object[] {
|
||||
get contactMethods(): {id: number, value?: string}[] {
|
||||
const contactInfotags = Utils.groupObjectBy(Store.kinks.infotags, 'infotag_group');
|
||||
contactInfotags[CONTACT_GROUP_ID]!.sort((a: Infotag, b: Infotag) => a.name < b.name ? -1 : 1);
|
||||
const contactMethods = [];
|
||||
|
@ -233,7 +244,7 @@
|
|||
return contactMethods;
|
||||
}
|
||||
|
||||
get quickInfoItems(): object[] {
|
||||
get quickInfoItems(): {id: number, string?: string, list?: number, number?: number}[] {
|
||||
const quickItems = [];
|
||||
for(const id of this.quickInfoIds) {
|
||||
const infotag = this.character.character.infotags[id];
|
||||
|
@ -256,4 +267,4 @@
|
|||
this.$emit('memo', memo);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</script>
|
||||
|
|
|
@ -8,7 +8,6 @@ interface Dictionary<T> {
|
|||
type flashMessageType = 'info' | 'success' | 'warning' | 'danger';
|
||||
type flashMessageImpl = (type: flashMessageType, message: string) => void;
|
||||
|
||||
|
||||
let flashImpl: flashMessageImpl = (type: flashMessageType, message: string) => {
|
||||
console.log(`${type}: ${message}`);
|
||||
};
|
||||
|
|
|
@ -65,6 +65,7 @@
|
|||
"eofline": false,
|
||||
"file-name-casing": false,
|
||||
"forin": false,
|
||||
"increment-decrement": false,
|
||||
"interface-name": false,
|
||||
"interface-over-type-literal": false,
|
||||
"linebreak-style": false,
|
||||
|
@ -83,10 +84,9 @@
|
|||
"no-angle-bracket-type-assertion": false,
|
||||
"no-bitwise": false,
|
||||
"no-conditional-assignment": false,
|
||||
//disabled for Vue components
|
||||
"no-consecutive-blank-lines": false,
|
||||
"no-console": false,
|
||||
"no-default-export": false,
|
||||
"no-default-import": false,
|
||||
"no-dynamic-delete": false,
|
||||
"no-floating-promises": [true, "AxiosPromise"],
|
||||
"no-implicit-dependencies": false,
|
||||
|
|
|
@ -1,41 +1,36 @@
|
|||
"use strict";
|
||||
exports.__esModule = true;
|
||||
var tslib_1 = require("tslib");
|
||||
var Lint = require("tslint");
|
||||
var ts = require("typescript");
|
||||
var Rule = /** @class */ (function (_super) {
|
||||
tslib_1.__extends(Rule, _super);
|
||||
function Rule() {
|
||||
return _super !== null && _super.apply(this, arguments) || this;
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
const ts = require("typescript");
|
||||
const Lint = require("tslint");
|
||||
class Rule extends Lint.Rules.AbstractRule {
|
||||
apply(sourceFile) {
|
||||
return this.applyWithFunction(sourceFile, walk, undefined);
|
||||
}
|
||||
Rule.prototype.applyWithProgram = function (sourceFile, program) {
|
||||
return this.applyWithFunction(sourceFile, walk, undefined, program.getTypeChecker());
|
||||
};
|
||||
return Rule;
|
||||
}(Lint.Rules.TypedRule));
|
||||
}
|
||||
exports.Rule = Rule;
|
||||
function walk(ctx, checker) {
|
||||
function walk(ctx) {
|
||||
if (ctx.sourceFile.isDeclarationFile)
|
||||
return;
|
||||
return ts.forEachChild(ctx.sourceFile, cb);
|
||||
function cb(node) {
|
||||
if (node.kind !== ts.SyntaxKind.PropertyDeclaration || !node.decorators)
|
||||
if (node.kind !== ts.SyntaxKind.PropertyDeclaration)
|
||||
return ts.forEachChild(node, cb);
|
||||
for (var _i = 0, _a = node.decorators; _i < _a.length; _i++) {
|
||||
var decorator = _a[_i];
|
||||
var call = decorator.expression;
|
||||
var propSymbol = checker.getTypeAtLocation(call.expression).symbol;
|
||||
if (propSymbol.name === 'Prop' &&
|
||||
propSymbol.parent.name.endsWith('node_modules/vue-property-decorator/lib/vue-property-decorator"')) {
|
||||
if (!node.modifiers || !node.modifiers.some(function (x) { return x.kind === ts.SyntaxKind.ReadonlyKeyword; }))
|
||||
ctx.addFailureAtNode(node.name, 'Vue property should be readonly');
|
||||
if (call.arguments.length > 0 && call.arguments[0].properties.map(function (x) { return x.name.getText(); })
|
||||
.some(function (x) { return x === 'default' || x === 'required'; })) {
|
||||
if (node.questionToken !== undefined)
|
||||
ctx.addFailureAtNode(node.name, 'Vue property is required and should not be optional.');
|
||||
if (!node.decorators)
|
||||
return;
|
||||
const property = node;
|
||||
for (const decorator of node.decorators) {
|
||||
const call = decorator.expression.kind == ts.SyntaxKind.CallExpression ? decorator.expression : undefined;
|
||||
const name = call && call.expression.getText() || decorator.expression.getText();
|
||||
if (name === 'Prop') {
|
||||
if (!node.modifiers || !node.modifiers.some((x) => x.kind === ts.SyntaxKind.ReadonlyKeyword))
|
||||
ctx.addFailureAtNode(property.name, 'Vue property should be readonly');
|
||||
if (call && call.arguments.length > 0 &&
|
||||
call.arguments[0].properties.map((x) => x.name.getText()).some((x) => x === 'default' || x === 'required')) {
|
||||
if (property.questionToken !== undefined)
|
||||
ctx.addFailureAtNode(property.name, 'Vue property is required and should not be optional.');
|
||||
}
|
||||
else if (node.questionToken === undefined)
|
||||
ctx.addFailureAtNode(node.name, 'Vue property should be optional - it is not required and has no default value.');
|
||||
else if (property.questionToken === undefined)
|
||||
ctx.addFailureAtNode(property.name, 'Vue property should be optional - it is not required and has no default value.');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue