3.0.13
This commit is contained in:
parent
a5e57cd52c
commit
8b0fe1aaed
|
@ -38,26 +38,25 @@
|
|||
<script lang="ts">
|
||||
import {Component, Hook, Prop, Watch} from '@f-list/vue-ts';
|
||||
import Vue from 'vue';
|
||||
import {BBCodeElement} from '../chat/bbcode';
|
||||
import {getKey} from '../chat/common';
|
||||
import {Keys} from '../keys';
|
||||
import {CoreBBCodeParser, urlRegex} from './core';
|
||||
import {BBCodeElement, CoreBBCodeParser, urlRegex} from './core';
|
||||
import {defaultButtons, EditorButton, EditorSelection} from './editor';
|
||||
import {BBCodeParser} from './parser';
|
||||
|
||||
@Component
|
||||
export default class Editor extends Vue {
|
||||
@Prop()
|
||||
@Prop
|
||||
readonly extras?: EditorButton[];
|
||||
@Prop({default: 1000})
|
||||
readonly maxlength!: number;
|
||||
@Prop()
|
||||
@Prop
|
||||
readonly classes?: string;
|
||||
@Prop()
|
||||
@Prop
|
||||
readonly value?: string;
|
||||
@Prop()
|
||||
@Prop
|
||||
readonly disabled?: boolean;
|
||||
@Prop()
|
||||
@Prop
|
||||
readonly placeholder?: string;
|
||||
@Prop({default: true})
|
||||
readonly hasToolbar!: boolean;
|
||||
|
@ -256,7 +255,7 @@
|
|||
}
|
||||
|
||||
onPaste(e: ClipboardEvent): void {
|
||||
const data = e.clipboardData.getData('text/plain');
|
||||
const data = e.clipboardData!.getData('text/plain');
|
||||
if(!this.isShiftPressed && urlRegex.test(data)) {
|
||||
e.preventDefault();
|
||||
this.applyText(`[url=${data}]`, '[/url]');
|
||||
|
|
|
@ -4,6 +4,8 @@ const urlFormat = '((?:https?|ftps?|irc)://[^\\s/$.?#"\']+\\.[^\\s"]+)';
|
|||
export const findUrlRegex = new RegExp(`(\\[url[=\\]]\\s*)?${urlFormat}`, 'gi');
|
||||
export const urlRegex = new RegExp(`^${urlFormat}$`);
|
||||
|
||||
export type BBCodeElement = HTMLElement & {cleanup?(): void};
|
||||
|
||||
function domain(url: string): string | undefined {
|
||||
const pieces = urlRegex.exec(url);
|
||||
if(pieces === null) return;
|
||||
|
|
|
@ -1 +0,0 @@
|
|||
export const enum InlineDisplayMode {DISPLAY_ALL, DISPLAY_SFW, DISPLAY_NONE}
|
|
@ -185,9 +185,9 @@ export class BBCodeParser {
|
|||
if(parent !== undefined)
|
||||
parent.appendChild(document.createTextNode(input.substring(mark, selfAllowed ? tagStart : i + 1)));
|
||||
return i;
|
||||
} else if(!selfAllowed)
|
||||
return mark - 1;
|
||||
else if(isAllowed(tagKey))
|
||||
}
|
||||
if(!selfAllowed) return mark - 1;
|
||||
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,19 +1,11 @@
|
|||
import {InlineImage} from '../interfaces';
|
||||
import {InlineDisplayMode, InlineImage} from '../interfaces';
|
||||
import * as Utils from '../site/utils';
|
||||
import {CoreBBCodeParser} from './core';
|
||||
import {InlineDisplayMode} from './interfaces';
|
||||
import {BBCodeCustomTag, BBCodeSimpleTag, BBCodeTextTag} from './parser';
|
||||
|
||||
interface StandardParserSettings {
|
||||
siteDomain: string
|
||||
staticDomain: string
|
||||
animatedIcons: boolean
|
||||
inlineDisplayMode: InlineDisplayMode
|
||||
}
|
||||
|
||||
const usernameRegex = /^[a-zA-Z0-9_\-\s]+$/;
|
||||
|
||||
export class StandardBBCodeParser extends CoreBBCodeParser {
|
||||
allowInlines = true;
|
||||
inlines: {[key: string]: InlineImage | undefined} | undefined;
|
||||
|
||||
createInline(inline: InlineImage): HTMLElement {
|
||||
|
@ -23,12 +15,12 @@ export class StandardBBCodeParser extends CoreBBCodeParser {
|
|||
const el = this.createElement('img');
|
||||
el.className = 'inline-image';
|
||||
el.title = el.alt = inline.name;
|
||||
el.src = `${this.settings.staticDomain}images/charinline/${p1}/${p2}/${inline.hash}.${inline.extension}`;
|
||||
el.src = `${Utils.staticDomain}images/charinline/${p1}/${p2}/${inline.hash}.${inline.extension}`;
|
||||
outerEl.appendChild(el);
|
||||
return outerEl;
|
||||
}
|
||||
|
||||
constructor(public settings: StandardParserSettings) {
|
||||
constructor() {
|
||||
super();
|
||||
const hrTag = new BBCodeSimpleTag('hr', 'hr', [], []);
|
||||
hrTag.noClosingTag = true;
|
||||
|
@ -107,7 +99,7 @@ export class StandardBBCodeParser extends CoreBBCodeParser {
|
|||
if(!usernameRegex.test(content))
|
||||
return;
|
||||
const a = parser.createElement('a');
|
||||
a.href = `${this.settings.siteDomain}c/${content}`;
|
||||
a.href = `${Utils.siteDomain}c/${content}`;
|
||||
a.target = '_blank';
|
||||
a.className = 'character-link';
|
||||
a.appendChild(document.createTextNode(content));
|
||||
|
@ -120,10 +112,10 @@ export class StandardBBCodeParser extends CoreBBCodeParser {
|
|||
if(!usernameRegex.test(content))
|
||||
return;
|
||||
const a = parser.createElement('a');
|
||||
a.href = `${this.settings.siteDomain}c/${content}`;
|
||||
a.href = `${Utils.siteDomain}c/${content}`;
|
||||
a.target = '_blank';
|
||||
const img = parser.createElement('img');
|
||||
img.src = `${this.settings.staticDomain}images/avatar/${content.toLowerCase()}.png`;
|
||||
img.src = `${Utils.staticDomain}images/avatar/${content.toLowerCase()}.png`;
|
||||
img.className = 'character-avatar icon';
|
||||
img.title = img.alt = content;
|
||||
a.appendChild(img);
|
||||
|
@ -137,10 +129,10 @@ export class StandardBBCodeParser extends CoreBBCodeParser {
|
|||
if(!usernameRegex.test(content))
|
||||
return;
|
||||
let extension = '.gif';
|
||||
if(!this.settings.animatedIcons)
|
||||
if(!Utils.settings.animateEicons)
|
||||
extension = '.png';
|
||||
const img = parser.createElement('img');
|
||||
img.src = `${this.settings.staticDomain}images/eicon/${content.toLowerCase()}${extension}`;
|
||||
img.src = `${Utils.staticDomain}images/eicon/${content.toLowerCase()}${extension}`;
|
||||
img.className = 'character-avatar icon';
|
||||
img.title = img.alt = content;
|
||||
parent.appendChild(img);
|
||||
|
@ -148,15 +140,11 @@ export class StandardBBCodeParser extends CoreBBCodeParser {
|
|||
}));
|
||||
this.addTag(new BBCodeTextTag('img', (p, parent, param, content) => {
|
||||
const parser = <StandardBBCodeParser>p;
|
||||
if(!this.allowInlines) {
|
||||
parser.warning('Inline images are not allowed here.');
|
||||
return undefined;
|
||||
}
|
||||
if(typeof parser.inlines === 'undefined') {
|
||||
parser.warning('This page does not support inline images.');
|
||||
return undefined;
|
||||
}
|
||||
const displayMode = this.settings.inlineDisplayMode;
|
||||
const displayMode = Utils.settings.inlineDisplayMode;
|
||||
if(!/^\d+$/.test(param)) {
|
||||
parser.warning('img tag parameters must be numbers.');
|
||||
return undefined;
|
||||
|
@ -188,10 +176,4 @@ export class StandardBBCodeParser extends CoreBBCodeParser {
|
|||
return element;
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
export let standardParser: StandardBBCodeParser;
|
||||
|
||||
export function initParser(settings: StandardParserSettings): void {
|
||||
standardParser = new StandardBBCodeParser(settings);
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
import {CreateElement, FunctionalComponentOptions, RenderContext, VNode} from 'vue';
|
||||
import {DefaultProps, RecordPropsDefinition} from 'vue/types/options'; //tslint:disable-line:no-submodule-imports
|
||||
import {BBCodeElement} from './core';
|
||||
import {BBCodeParser} from './parser';
|
||||
|
||||
export const BBCodeView = (parser: BBCodeParser): FunctionalComponentOptions<DefaultProps, RecordPropsDefinition<DefaultProps>> => ({
|
||||
functional: true,
|
||||
render(createElement: CreateElement, context: RenderContext): VNode {
|
||||
/*tslint:disable:no-unsafe-any*///because we're not actually supposed to do any of this
|
||||
context.data.hook = {
|
||||
insert(node: VNode): void {
|
||||
node.elm!.appendChild(parser.parseEverything(
|
||||
context.props.text !== undefined ? context.props.text : context.props.unsafeText));
|
||||
if(context.props.afterInsert !== undefined) context.props.afterInsert(node.elm);
|
||||
},
|
||||
destroy(node: VNode): void {
|
||||
const element = (<BBCodeElement>(<Element>node.elm).firstChild);
|
||||
if(element.cleanup !== undefined) element.cleanup();
|
||||
}
|
||||
};
|
||||
const vnode = createElement('span', context.data);
|
||||
vnode.key = context.props.text;
|
||||
return vnode;
|
||||
//tslint:enable
|
||||
}
|
||||
});
|
|
@ -28,10 +28,10 @@
|
|||
<script lang="ts">
|
||||
import {Component, Hook} from '@f-list/vue-ts';
|
||||
import Axios from 'axios';
|
||||
import {BBCodeView} from '../bbcode/view';
|
||||
import CustomDialog from '../components/custom_dialog';
|
||||
import FilterableSelect from '../components/FilterableSelect.vue';
|
||||
import Modal from '../components/Modal.vue';
|
||||
import {BBCodeView} from './bbcode';
|
||||
import {characterImage} from './common';
|
||||
import core from './core';
|
||||
import {Character, Connection} from './interfaces';
|
||||
|
@ -66,7 +66,7 @@
|
|||
}
|
||||
|
||||
@Component({
|
||||
components: {modal: Modal, user: UserView, 'filterable-select': FilterableSelect, bbcode: BBCodeView}
|
||||
components: {modal: Modal, user: UserView, 'filterable-select': FilterableSelect, bbcode: BBCodeView(core.bbCodeParser)}
|
||||
})
|
||||
export default class CharacterSearch extends CustomDialog {
|
||||
l = l;
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import {InlineDisplayMode} from '../interfaces';
|
||||
<template>
|
||||
<div style="display:flex; flex-direction: column; height:100%; justify-content: center">
|
||||
<div class="card bg-light" style="width:400px;max-width:100%;margin:0 auto" v-if="!connected">
|
||||
|
@ -11,7 +12,7 @@
|
|||
<div class="card-body">
|
||||
<h4 class="card-title">{{l('login.selectCharacter')}}</h4>
|
||||
<select v-model="selectedCharacter" class="form-control custom-select">
|
||||
<option v-for="character in ownCharacters" :value="character">{{character}}</option>
|
||||
<option v-for="character in ownCharacters" :value="character">{{character.name}}</option>
|
||||
</select>
|
||||
<div style="text-align:right;margin-top:10px">
|
||||
<button class="btn btn-primary" @click="connect" :disabled="connecting">
|
||||
|
@ -35,15 +36,14 @@
|
|||
import {Component, Hook, Prop} from '@f-list/vue-ts';
|
||||
import Vue from 'vue';
|
||||
import Modal from '../components/Modal.vue';
|
||||
import Channels from '../fchat/channels';
|
||||
import Characters from '../fchat/characters';
|
||||
import {InlineDisplayMode, SimpleCharacter} from '../interfaces';
|
||||
import {Keys} from '../keys';
|
||||
import ChatView from './ChatView.vue';
|
||||
import {errorToString, getKey} from './common';
|
||||
import Conversations from './conversations';
|
||||
import core from './core';
|
||||
import l from './localize';
|
||||
import Logs from './Logs.vue';
|
||||
import {init as profileApiInit} from './profile_api';
|
||||
|
||||
type BBCodeNode = Node & {bbcodeTag?: string, bbcodeParam?: string};
|
||||
|
||||
|
@ -53,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);
|
||||
|
@ -78,11 +78,12 @@
|
|||
})
|
||||
export default class Chat extends Vue {
|
||||
@Prop({required: true})
|
||||
readonly ownCharacters!: string[];
|
||||
readonly ownCharacters!: SimpleCharacter[];
|
||||
@Prop({required: true})
|
||||
readonly defaultCharacter!: string | undefined;
|
||||
selectedCharacter = this.defaultCharacter || this.ownCharacters[0]; //tslint:disable-line:strict-boolean-expressions
|
||||
@Prop()
|
||||
readonly defaultCharacter!: number;
|
||||
//tslint:disable-next-line:strict-boolean-expressions
|
||||
selectedCharacter = this.ownCharacters.find((x) => x.id === this.defaultCharacter) || this.ownCharacters[0];
|
||||
@Prop
|
||||
readonly version?: string;
|
||||
error = '';
|
||||
connecting = false;
|
||||
|
@ -110,7 +111,7 @@
|
|||
} 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];
|
||||
e.clipboardData.setData('text/plain', copyNode(startValue, start, end, range, {}));
|
||||
e.clipboardData!.setData('text/plain', copyNode(startValue, start, end, range, {}));
|
||||
e.preventDefault();
|
||||
}) as EventListener);
|
||||
window.addEventListener('keydown', (e) => {
|
||||
|
@ -120,10 +121,7 @@
|
|||
e.preventDefault();
|
||||
}
|
||||
});
|
||||
core.register('characters', Characters(core.connection));
|
||||
core.register('channels', Channels(core.connection, core.characters));
|
||||
core.register('conversations', Conversations());
|
||||
core.connection.onEvent('closed', async(isReconnect) => {
|
||||
core.connection.onEvent('closed', (isReconnect) => {
|
||||
if(isReconnect) (<Modal>this.$refs['reconnecting']).show(true);
|
||||
if(this.connected) core.notifications.playSound('logout');
|
||||
this.connected = false;
|
||||
|
@ -132,9 +130,13 @@
|
|||
});
|
||||
core.connection.onEvent('connecting', async() => {
|
||||
this.connecting = true;
|
||||
profileApiInit({
|
||||
defaultCharacter: this.defaultCharacter, animateEicons: core.state.settings.animatedEicons, fuzzyDates: true,
|
||||
inlineDisplayMode: InlineDisplayMode.DISPLAY_ALL
|
||||
}, this.ownCharacters);
|
||||
if(core.state.settings.notifications) await core.notifications.requestPermission();
|
||||
});
|
||||
core.connection.onEvent('connected', async() => {
|
||||
core.connection.onEvent('connected', () => {
|
||||
(<Modal>this.$refs['reconnecting']).hide();
|
||||
this.error = '';
|
||||
this.connecting = false;
|
||||
|
@ -165,7 +167,7 @@
|
|||
async connect(): Promise<void> {
|
||||
this.connecting = true;
|
||||
await core.notifications.initSounds(['attention', 'login', 'logout', 'modalert', 'newnote']);
|
||||
core.connection.connect(this.selectedCharacter);
|
||||
core.connection.connect(this.selectedCharacter.name);
|
||||
}
|
||||
}
|
||||
</script>
|
|
@ -29,7 +29,8 @@
|
|||
<div class="list-group conversation-nav" ref="privateConversations">
|
||||
<a v-for="conversation in conversations.privateConversations" href="#" @click.prevent="conversation.show()"
|
||||
:class="getClasses(conversation)" :data-character="conversation.character.name" data-touch="false"
|
||||
class="list-group-item list-group-item-action item-private" :key="conversation.key" @click.middle="conversation.close()">
|
||||
class="list-group-item list-group-item-action item-private" :key="conversation.key"
|
||||
@click.middle.prevent="conversation.close()">
|
||||
<img :src="characterImage(conversation.character.name)" v-if="showAvatars"/>
|
||||
<div class="name">
|
||||
<span>{{conversation.character.name}}</span>
|
||||
|
@ -39,8 +40,8 @@
|
|||
:class="{'fa-comment-dots': conversation.typingStatus == 'typing', 'fa-comment': conversation.typingStatus == 'paused'}"
|
||||
></span>
|
||||
<span style="flex:1"></span>
|
||||
<span class="pin fas fa-thumbtack" :class="{'active': conversation.isPinned}" @mousedown.prevent
|
||||
@click.stop="conversation.isPinned = !conversation.isPinned" :aria-label="l('chat.pinTab')"></span>
|
||||
<span class="pin fas fa-thumbtack" :class="{'active': conversation.isPinned}"
|
||||
@click="conversation.isPinned = !conversation.isPinned" :aria-label="l('chat.pinTab')"></span>
|
||||
<span class="fas fa-times leave" @click.stop="conversation.close()" :aria-label="l('chat.closeTab')"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -51,7 +52,7 @@
|
|||
<div class="list-group conversation-nav" ref="channelConversations">
|
||||
<a v-for="conversation in conversations.channelConversations" href="#" @click.prevent="conversation.show()"
|
||||
:class="getClasses(conversation)" class="list-group-item list-group-item-action item-channel" :key="conversation.key"
|
||||
@click.middle="conversation.close()">
|
||||
@click.middle.prevent="conversation.close()">
|
||||
<span class="name">{{conversation.name}}</span>
|
||||
<span>
|
||||
<span class="pin fas fa-thumbtack" :class="{'active': conversation.isPinned}" :aria-label="l('chat.pinTab')"
|
||||
|
@ -145,6 +146,7 @@
|
|||
this.setFontSize(core.state.settings.fontSize);
|
||||
Sortable.create(<HTMLElement>this.$refs['privateConversations'], {
|
||||
animation: 50,
|
||||
fallbackTolerance: 5,
|
||||
onEnd: async(e) => {
|
||||
if(e.oldIndex === e.newIndex) return;
|
||||
return core.conversations.privateConversations[e.oldIndex!].sort(e.newIndex!);
|
||||
|
@ -152,6 +154,7 @@
|
|||
});
|
||||
Sortable.create(<HTMLElement>this.$refs['channelConversations'], {
|
||||
animation: 50,
|
||||
fallbackTolerance: 5,
|
||||
onEnd: async(e) => {
|
||||
if(e.oldIndex === e.newIndex) return;
|
||||
return core.conversations.channelConversations[e.oldIndex!].sort(e.newIndex!);
|
||||
|
@ -256,13 +259,8 @@
|
|||
overrideEl.id = 'overrideFontSize';
|
||||
document.body.appendChild(overrideEl);
|
||||
const sheet = <CSSStyleSheet>overrideEl.sheet;
|
||||
const selectorList = ['#chatView', '.btn', '.form-control'];
|
||||
for(const selector of selectorList)
|
||||
sheet.insertRule(`${selector} { font-size: ${fontSize}px; }`, sheet.cssRules.length);
|
||||
|
||||
const lineHeight = 1.428571429;
|
||||
sheet.insertRule(`.form-control { line-height: ${lineHeight} }`, sheet.cssRules.length);
|
||||
sheet.insertRule(`select.form-control { line-height: ${lineHeight} }`, sheet.cssRules.length);
|
||||
sheet.insertRule(`#chatView, .btn, .form-control, .custom-select { font-size: ${fontSize}px; }`, sheet.cssRules.length);
|
||||
sheet.insertRule(`.form-control, select.form-control { line-height: 1.428571429 }`, sheet.cssRules.length);
|
||||
}
|
||||
|
||||
logOut(): void {
|
||||
|
@ -419,6 +417,7 @@
|
|||
#sidebar {
|
||||
.body a.btn {
|
||||
padding: 2px 0;
|
||||
text-align: left;
|
||||
}
|
||||
@media (min-width: breakpoint-min(md)) {
|
||||
.sidebar {
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
<div style="height:100%;display:flex;flex-direction:column;flex:1;margin:0 5px;position:relative" id="conversation">
|
||||
<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 style="flex:1;position:relative;display:flex;flex-direction:column;user-select:text">
|
||||
<div>
|
||||
<user :character="conversation.character"></user>
|
||||
<a href="#" @click.prevent="showLogs()" class="btn">
|
||||
|
@ -14,7 +14,7 @@
|
|||
<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">
|
||||
<div style="overflow:auto;overflow-x:hidden;max-height:50px;user-select:text">
|
||||
{{l('status.' + conversation.character.status)}}
|
||||
<span v-show="conversation.character.statusText"> – <bbcode :text="conversation.character.statusText"></bbcode></span>
|
||||
</div>
|
||||
|
@ -129,9 +129,10 @@
|
|||
import {Component, Hook, Prop, Watch} from '@f-list/vue-ts';
|
||||
import Vue from 'vue';
|
||||
import {EditorButton, EditorSelection} from '../bbcode/editor';
|
||||
import {BBCodeView} from '../bbcode/view';
|
||||
import {isShowing as anyDialogsShown} from '../components/Modal.vue';
|
||||
import {Keys} from '../keys';
|
||||
import {BBCodeView, Editor} from './bbcode';
|
||||
import {Editor} from './bbcode';
|
||||
import CommandHelp from './CommandHelp.vue';
|
||||
import {characterImage, getByteLength, getKey} from './common';
|
||||
import ConversationSettings from './ConversationSettings.vue';
|
||||
|
@ -148,7 +149,7 @@
|
|||
@Component({
|
||||
components: {
|
||||
user: UserView, 'bbcode-editor': Editor, 'manage-channel': ManageChannel, settings: ConversationSettings,
|
||||
logs: Logs, 'message-view': MessageView, bbcode: BBCodeView, 'command-help': CommandHelp
|
||||
logs: Logs, 'message-view': MessageView, bbcode: BBCodeView(core.bbCodeParser), 'command-help': CommandHelp
|
||||
}
|
||||
})
|
||||
export default class ConversationView extends Vue {
|
||||
|
@ -281,8 +282,13 @@
|
|||
if(this.messageView.scrollTop < 20) {
|
||||
if(!this.scrolledUp) {
|
||||
const firstMessage = this.messageView.firstElementChild;
|
||||
if(this.conversation.loadMore() && firstMessage !== null)
|
||||
this.$nextTick(() => setTimeout(() => this.messageView.scrollTop = (<HTMLElement>firstMessage).offsetTop, 0));
|
||||
if(this.conversation.loadMore() && firstMessage !== null) {
|
||||
this.messageView.style.overflow = 'hidden';
|
||||
this.$nextTick(() => {
|
||||
this.messageView.scrollTop = (<HTMLElement>firstMessage).offsetTop;
|
||||
this.messageView.style.overflow = 'auto';
|
||||
});
|
||||
}
|
||||
}
|
||||
this.scrolledUp = true;
|
||||
} else this.scrolledUp = false;
|
||||
|
|
|
@ -85,7 +85,7 @@
|
|||
components: {modal: Modal, 'message-view': MessageView, 'filterable-select': FilterableSelect}
|
||||
})
|
||||
export default class Logs extends CustomDialog {
|
||||
@Prop()
|
||||
@Prop
|
||||
readonly conversation?: Conversation;
|
||||
conversations: LogInterface.Conversation[] = [];
|
||||
selectedConversation: LogInterface.Conversation | undefined;
|
||||
|
|
|
@ -16,9 +16,10 @@
|
|||
|
||||
<script lang="ts">
|
||||
import {Component, Hook} from '@f-list/vue-ts';
|
||||
import {BBCodeElement} from '../bbcode/core';
|
||||
import CustomDialog from '../components/custom_dialog';
|
||||
import Modal from '../components/Modal.vue';
|
||||
import BBCodeParser, {BBCodeElement} from './bbcode';
|
||||
import BBCodeParser from './bbcode';
|
||||
import {errorToString, messageToString} from './common';
|
||||
import core from './core';
|
||||
import {Character, Conversation} from './interfaces';
|
||||
|
|
|
@ -20,9 +20,9 @@
|
|||
|
||||
@Component
|
||||
export default class Sidebar extends Vue {
|
||||
@Prop()
|
||||
@Prop
|
||||
readonly right?: true;
|
||||
@Prop()
|
||||
@Prop
|
||||
readonly label?: string;
|
||||
@Prop({required: true})
|
||||
readonly icon!: string;
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
<modal :action="l('chat.setStatus')" @submit="setStatus" @close="reset" dialogClass="w-100 modal-lg">
|
||||
<div class="form-group" id="statusSelector">
|
||||
<label class="control-label">{{l('chat.setStatus.status')}}</label>
|
||||
<dropdown>
|
||||
<dropdown linkClass="custom-select">
|
||||
<span slot="title"><span class="fa fa-fw" :class="getStatusIcon(status)"></span>{{l('status.' + status)}}</span>
|
||||
<a href="#" class="dropdown-item" v-for="item in statuses" @click.prevent="status = item">
|
||||
<span class="fa fa-fw" :class="getStatusIcon(item)"></span>{{l('status.' + item)}}
|
||||
|
|
|
@ -42,8 +42,8 @@
|
|||
<script lang="ts">
|
||||
import {Component, Prop} from '@f-list/vue-ts';
|
||||
import Vue from 'vue';
|
||||
import {BBCodeView} from '../bbcode/view';
|
||||
import Modal from '../components/Modal.vue';
|
||||
import {BBCodeView} from './bbcode';
|
||||
import {characterImage, errorToString, getByteLength, profileLink} from './common';
|
||||
import core from './core';
|
||||
import {Channel, Character} from './interfaces';
|
||||
|
@ -51,7 +51,7 @@
|
|||
import ReportDialog from './ReportDialog.vue';
|
||||
|
||||
@Component({
|
||||
components: {bbcode: BBCodeView, modal: Modal}
|
||||
components: {bbcode: BBCodeView(core.bbCodeParser), modal: Modal}
|
||||
})
|
||||
export default class UserMenu extends Vue {
|
||||
@Prop({required: true})
|
||||
|
@ -156,7 +156,7 @@
|
|||
node = node.parentElement!;
|
||||
}
|
||||
if(node.dataset['touch'] === 'false' && e.type !== 'contextmenu') return;
|
||||
if(node.character === undefined)
|
||||
if(!node.character)
|
||||
if(node.dataset['character'] !== undefined) node.character = core.characters.get(node.dataset['character']!);
|
||||
else {
|
||||
this.showContextMenu = false;
|
||||
|
@ -166,7 +166,7 @@
|
|||
switch(e.type) {
|
||||
case 'click':
|
||||
if(node.dataset['character'] === undefined)
|
||||
if(node === this.touchedElement) this.openMenu(touch, node.character, node.channel);
|
||||
if(node === this.touchedElement) this.openMenu(touch, node.character, node.channel || undefined);
|
||||
else this.onClick(node.character);
|
||||
e.preventDefault();
|
||||
break;
|
||||
|
@ -174,7 +174,7 @@
|
|||
this.touchedElement = node;
|
||||
break;
|
||||
case 'contextmenu':
|
||||
this.openMenu(touch, node.character, node.channel);
|
||||
this.openMenu(touch, node.character, node.channel || undefined);
|
||||
e.preventDefault();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import {WebSocketConnection} from '../fchat';
|
||||
|
||||
export default class Socket implements WebSocketConnection {
|
||||
static host = 'wss://chat.f-list.net:9799';
|
||||
static host = 'wss://chat.f-list.net/chat2';
|
||||
private socket: WebSocket;
|
||||
private lastHandler: Promise<void> = Promise.resolve();
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import Vue, {Component, CreateElement, RenderContext, VNode} from 'vue';
|
||||
import {CoreBBCodeParser} from '../bbcode/core';
|
||||
import Vue from 'vue';
|
||||
import {BBCodeElement, CoreBBCodeParser} from '../bbcode/core';
|
||||
//tslint:disable-next-line:match-default-export-name
|
||||
import BaseEditor from '../bbcode/Editor.vue';
|
||||
import {BBCodeTextTag} from '../bbcode/parser';
|
||||
|
@ -9,34 +9,10 @@ import core from './core';
|
|||
import {Character} from './interfaces';
|
||||
import UserView from './user_view';
|
||||
|
||||
export const BBCodeView: Component = {
|
||||
functional: true,
|
||||
render(createElement: CreateElement, context: RenderContext): VNode {
|
||||
/*tslint:disable:no-unsafe-any*///because we're not actually supposed to do any of this
|
||||
context.data.hook = {
|
||||
insert(node: VNode): void {
|
||||
node.elm!.appendChild(core.bbCodeParser.parseEverything(
|
||||
context.props.text !== undefined ? context.props.text : context.props.unsafeText));
|
||||
if(context.props.afterInsert !== undefined) context.props.afterInsert(node.elm);
|
||||
},
|
||||
destroy(node: VNode): void {
|
||||
const element = (<BBCodeElement>(<Element>node.elm).firstChild);
|
||||
if(element.cleanup !== undefined) element.cleanup();
|
||||
}
|
||||
};
|
||||
const vnode = createElement('span', context.data);
|
||||
vnode.key = context.props.text;
|
||||
return vnode;
|
||||
//tslint:enable
|
||||
}
|
||||
};
|
||||
|
||||
export class Editor extends BaseEditor {
|
||||
parser = core.bbCodeParser;
|
||||
}
|
||||
|
||||
export type BBCodeElement = HTMLElement & {cleanup?(): void};
|
||||
|
||||
export default class BBCodeParser extends CoreBBCodeParser {
|
||||
cleanup: Vue[] = [];
|
||||
|
||||
|
|
|
@ -79,6 +79,7 @@ abstract class Conversation implements Interfaces.Conversation {
|
|||
}
|
||||
}
|
||||
|
||||
//tslint:disable-next-line:no-async-without-await
|
||||
abstract async addMessage(message: Interfaces.Message): Promise<void>;
|
||||
|
||||
loadLastSent(): void {
|
||||
|
@ -182,7 +183,8 @@ class PrivateConversation extends Conversation implements Interfaces.PrivateConv
|
|||
if(this.character.status === 'offline') {
|
||||
this.errorText = l('chat.errorOffline', this.character.name);
|
||||
return;
|
||||
} else if(this.character.isIgnored) {
|
||||
}
|
||||
if(this.character.isIgnored) {
|
||||
this.errorText = l('chat.errorIgnored', this.character.name);
|
||||
return;
|
||||
}
|
||||
|
@ -402,6 +404,7 @@ class State implements Interfaces.State {
|
|||
}
|
||||
|
||||
show(conversation: Conversation): void {
|
||||
if(conversation === this.selectedConversation) return;
|
||||
this.selectedConversation.onHide();
|
||||
conversation.unread = Interfaces.UnreadState.None;
|
||||
this.selectedConversation = conversation;
|
||||
|
@ -419,12 +422,9 @@ class State implements Interfaces.State {
|
|||
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;
|
||||
settings[key] = Object.assign(new ConversationSettings(), settings[key]);
|
||||
const conv = this.byKey(key);
|
||||
if(conv !== undefined) conv._settings = settingsItem;
|
||||
if(conv !== undefined) conv._settings = settings[key];
|
||||
}
|
||||
this.settings = settings;
|
||||
//tslint:enable
|
||||
|
@ -443,11 +443,6 @@ function isOfInterest(this: void, character: Character): boolean {
|
|||
return character.isFriend || character.isBookmarked || state.privateMap[character.name.toLowerCase()] !== undefined;
|
||||
}
|
||||
|
||||
function isOp(conv: ChannelConversation): boolean {
|
||||
const ownChar = core.characters.ownCharacter;
|
||||
return ownChar.isChatOp || conv.channel.members[ownChar.name]!.rank > Channel.Rank.Member;
|
||||
}
|
||||
|
||||
export default function(this: void): Interfaces.State {
|
||||
state = new State();
|
||||
window.addEventListener('focus', () => {
|
||||
|
@ -523,7 +518,7 @@ export default function(this: void): Interfaces.State {
|
|||
const char = core.characters.get(data.character);
|
||||
const conversation = state.channelMap[data.channel.toLowerCase()];
|
||||
if(conversation === undefined) return core.channels.leave(data.channel);
|
||||
if(char.isIgnored && !isOp(conversation)) return;
|
||||
if(char.isIgnored) return;
|
||||
const message = createMessage(MessageType.Message, char, decodeHTML(data.message), time);
|
||||
await conversation.addMessage(message);
|
||||
|
||||
|
@ -552,7 +547,7 @@ export default function(this: void): Interfaces.State {
|
|||
const char = core.characters.get(data.character);
|
||||
const conv = state.channelMap[data.channel.toLowerCase()];
|
||||
if(conv === undefined) return core.channels.leave(data.channel);
|
||||
if((char.isIgnored || core.state.hiddenUsers.indexOf(char.name) !== -1) && !isOp(conv)) return;
|
||||
if(char.isIgnored || core.state.hiddenUsers.indexOf(char.name) !== -1) return;
|
||||
await conv.addMessage(new Message(MessageType.Ad, char, decodeHTML(data.message), time));
|
||||
});
|
||||
connection.onMessage('RLL', async(data, time) => {
|
||||
|
@ -569,7 +564,7 @@ export default function(this: void): Interfaces.State {
|
|||
const channel = (<{channel: string}>data).channel.toLowerCase();
|
||||
const conversation = state.channelMap[channel];
|
||||
if(conversation === undefined) return core.channels.leave(channel);
|
||||
if(sender.isIgnored && !isOp(conversation)) return;
|
||||
if(sender.isIgnored) return;
|
||||
if(data.type === 'bottle' && data.target === core.connection.character) {
|
||||
await core.notifications.notify(conversation, conversation.name, messageToString(message),
|
||||
characterImage(data.character), 'attention');
|
||||
|
|
24
chat/core.ts
24
chat/core.ts
|
@ -1,6 +1,8 @@
|
|||
import Vue, {WatchHandler} from 'vue';
|
||||
import {Channels, Characters} from '../fchat';
|
||||
import BBCodeParser from './bbcode';
|
||||
import {Settings as SettingsImpl} from './common';
|
||||
import Conversations from './conversations';
|
||||
import {Channel, Character, Connection, Conversation, Logs, Notifications, Settings, State as StateInterface} from './interfaces';
|
||||
|
||||
function createBBCodeParser(): BBCodeParser {
|
||||
|
@ -44,8 +46,9 @@ const vue = <Vue & VueState>new Vue({
|
|||
state
|
||||
},
|
||||
watch: {
|
||||
'state.hiddenUsers': async(newValue: string[]) => {
|
||||
if(data.settingsStore !== undefined) await data.settingsStore.set('hiddenUsers', newValue);
|
||||
'state.hiddenUsers': async(newValue: string[], oldValue: string[]) => {
|
||||
if(data.settingsStore !== undefined && newValue !== oldValue)
|
||||
await data.settingsStore.set('hiddenUsers', newValue);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@ -60,20 +63,15 @@ const data = {
|
|||
channels: <Channel.State | undefined>undefined,
|
||||
characters: <Character.State | undefined>undefined,
|
||||
notifications: <Notifications | undefined>undefined,
|
||||
register(this: void | never, module: 'characters' | 'conversations' | 'channels',
|
||||
subState: Channel.State | Character.State | Conversation.State): void {
|
||||
register<K extends 'characters' | 'conversations' | 'channels'>(module: K, subState: VueState[K]): void {
|
||||
Vue.set(vue, module, subState);
|
||||
data[module] = subState;
|
||||
(<VueState[K]>data[module]) = subState;
|
||||
},
|
||||
watch<T>(getter: (this: VueState) => T, callback: WatchHandler<T>): void {
|
||||
vue.$watch(getter, callback);
|
||||
},
|
||||
async reloadSettings(): Promise<void> {
|
||||
const settings = new SettingsImpl();
|
||||
const loadedSettings = <SettingsImpl | undefined>await core.settingsStore.get('settings');
|
||||
if(loadedSettings !== undefined)
|
||||
for(const key in loadedSettings) settings[<keyof Settings>key] = loadedSettings[<keyof Settings>key];
|
||||
state._settings = settings;
|
||||
state._settings = Object.assign(new SettingsImpl(), await core.settingsStore.get('settings'));
|
||||
const hiddenUsers = await core.settingsStore.get('hiddenUsers');
|
||||
state.hiddenUsers = hiddenUsers !== undefined ? hiddenUsers : [];
|
||||
}
|
||||
|
@ -85,6 +83,9 @@ export function init(this: void, connection: Connection, logsClass: new() => Log
|
|||
data.logs = new logsClass();
|
||||
data.settingsStore = new settingsClass();
|
||||
data.notifications = new notificationsClass();
|
||||
data.register('characters', Characters(connection));
|
||||
data.register('channels', Channels(connection, core.characters));
|
||||
data.register('conversations', Conversations());
|
||||
connection.onEvent('connecting', async() => {
|
||||
await data.reloadSettings();
|
||||
data.bbCodeParser = createBBCodeParser();
|
||||
|
@ -101,9 +102,6 @@ export interface Core {
|
|||
readonly channels: Channel.State
|
||||
readonly bbCodeParser: BBCodeParser
|
||||
readonly notifications: Notifications
|
||||
register(module: 'conversations', state: Conversation.State): void
|
||||
register(module: 'channels', state: Channel.State): void
|
||||
register(module: 'characters', state: Character.State): void
|
||||
watch<T>(getter: (this: VueState) => T, callback: WatchHandler<T>): void
|
||||
}
|
||||
|
||||
|
|
|
@ -321,10 +321,6 @@ Once this process has started, do not interrupt it or your logs will get corrupt
|
|||
'commands.roll.param0.help': 'Syntax: [1-9]d[1-100]. Addition and subtraction of rolls and fixed numbers is also possible. Example: /roll 1d6+1d20-5',
|
||||
'commands.bottle': 'Spin the bottle',
|
||||
'commands.bottle.help': 'Spins a bottle, randomly selecting a member of the current tab and displaying it to all.',
|
||||
'commands.ad': 'Post as ad',
|
||||
'commands.ad.help': 'A quick way to post an ad in the current channel. You may receive an error if ads are not allowed in that channel.',
|
||||
'commands.ad.param0': 'Message',
|
||||
'commands.ad.param0.help': 'The message to post as an ad.',
|
||||
'commands.me': 'Post as action',
|
||||
'commands.me.help': 'This will cause your message to be formatted differently, as an action your character is performing.',
|
||||
'commands.me.param0': 'Message',
|
||||
|
@ -427,9 +423,10 @@ Any existing FChat 3.0 data for this character will be overwritten.`,
|
|||
export default function l(key: string, ...args: (string | number)[]): string {
|
||||
let i = args.length;
|
||||
let str = strings[key];
|
||||
if(str === undefined)
|
||||
if(str === undefined) {
|
||||
if(process.env.NODE_ENV !== 'production') throw new Error(`String ${key} does not exist.`);
|
||||
else return '';
|
||||
return '';
|
||||
}
|
||||
while(i-- > 0)
|
||||
str = str.replace(new RegExp(`\\{${i}\\}`, 'igm'), args[i].toString());
|
||||
return str;
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import {Component, Prop} from '@f-list/vue-ts';
|
||||
import {CreateElement, default as Vue, VNode, VNodeChildrenArrayContents} from 'vue';
|
||||
import {BBCodeView} from '../bbcode/view';
|
||||
import {Channel} from '../fchat';
|
||||
import {BBCodeView} from './bbcode';
|
||||
import {formatTime} from './common';
|
||||
import core from './core';
|
||||
import {Conversation} from './interfaces';
|
||||
|
@ -29,7 +29,8 @@ const userPostfix: {[key: number]: string | undefined} = {
|
|||
if(message.isHighlight) classes += ' message-highlight';
|
||||
}
|
||||
const isAd = message.type === Conversation.Message.Type.Ad && !this.logs;
|
||||
children.push(createElement(BBCodeView, {props: {unsafeText: message.text, afterInsert: isAd ? (elm: HTMLElement) => {
|
||||
children.push(createElement(BBCodeView(core.bbCodeParser),
|
||||
{props: {unsafeText: message.text, afterInsert: isAd ? (elm: HTMLElement) => {
|
||||
setImmediate(() => {
|
||||
elm = elm.parentElement!;
|
||||
if(elm.scrollHeight > elm.offsetHeight) {
|
||||
|
|
|
@ -1,32 +1,23 @@
|
|||
import Axios from 'axios';
|
||||
import Vue from 'vue';
|
||||
import Editor from '../bbcode/Editor.vue';
|
||||
import {InlineDisplayMode} from '../bbcode/interfaces';
|
||||
import {initParser, standardParser} from '../bbcode/standard';
|
||||
import {StandardBBCodeParser} from '../bbcode/standard';
|
||||
import {BBCodeView} from '../bbcode/view';
|
||||
import CharacterLink from '../components/character_link.vue';
|
||||
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
|
||||
Character as CharacterInfo, CharacterImage, CharacterImageOld, CharacterInfotag, CharacterSettings, Infotag, InfotagGroup, Kink,
|
||||
KinkChoice, KinkGroup, ListItem, Settings, SimpleCharacter
|
||||
} from '../interfaces';
|
||||
import {registerMethod, Store} from '../site/character_page/data_store';
|
||||
import {
|
||||
Character, CharacterFriend, CharacterKink, Friend, FriendRequest, FriendsByCharacter, GuestbookState, KinkChoiceFull,
|
||||
SharedKinks
|
||||
Character, CharacterKink, Friend, FriendRequest, FriendsByCharacter, Guestbook, GuestbookPost, KinkChoiceFull
|
||||
} from '../site/character_page/interfaces';
|
||||
import '../site/directives/vue-select'; //tslint:disable-line:no-import-side-effect
|
||||
import * as Utils from '../site/utils';
|
||||
import core from './core';
|
||||
|
||||
const parserSettings = {
|
||||
siteDomain: 'https://www.f-list.net/',
|
||||
staticDomain: 'https://static.f-list.net/',
|
||||
animatedIcons: true,
|
||||
inlineDisplayMode: InlineDisplayMode.DISPLAY_ALL
|
||||
};
|
||||
|
||||
async function characterData(name: string | undefined): Promise<Character> {
|
||||
const data = await core.connection.queryApi<CharacterInfo & {
|
||||
badges: string[]
|
||||
|
@ -47,7 +38,7 @@ async function characterData(name: string | undefined): Promise<Character> {
|
|||
}>('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]);
|
||||
newKinks[key] = <KinkChoiceFull>(data.kinks[key] === 'fave' ? 'favorite' : data.kinks[key]);
|
||||
for(const key in data.custom_kinks) {
|
||||
const custom = data.custom_kinks[key];
|
||||
if((<'fave'>custom.choice) === 'fave') custom.choice = 'favorite';
|
||||
|
@ -59,12 +50,12 @@ async function characterData(name: string | undefined): Promise<Character> {
|
|||
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;
|
||||
const infotag = Store.shared.infotags[key];
|
||||
if(!infotag) continue;
|
||||
newInfotags[key] = infotag.type === 'list' ? {list: parseInt(characterInfotag, 10)} : {string: characterInfotag};
|
||||
}
|
||||
parserSettings.inlineDisplayMode = data.current_user.inline_mode;
|
||||
parserSettings.animatedIcons = core.state.settings.animatedEicons;
|
||||
Utils.settings.inlineDisplayMode = data.current_user.inline_mode;
|
||||
Utils.settings.animateEicons = core.state.settings.animatedEicons;
|
||||
return {
|
||||
is_self: false,
|
||||
character: {
|
||||
|
@ -98,13 +89,22 @@ function contactMethodIconUrl(name: string): string {
|
|||
}
|
||||
|
||||
async function fieldsGet(): Promise<void> {
|
||||
if(Store.kinks !== undefined) return; //tslint:disable-line:strict-type-predicates
|
||||
if(Store.shared !== undefined) return; //tslint:disable-line:strict-type-predicates
|
||||
try {
|
||||
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}}
|
||||
const fields = (await (Axios.get(`${Utils.siteDomain}json/api/mapping-list.php`))).data as {
|
||||
listitems: {[key: string]: ListItem}
|
||||
kinks: {[key: string]: Kink & {group_id: number}}
|
||||
kink_groups: {[key: string]: KinkGroup}
|
||||
infotags: {[key: string]: Infotag & {list: string, group_id: string}}
|
||||
infotag_groups: {[key: string]: InfotagGroup & {id: string}}
|
||||
};
|
||||
const kinks: SharedKinks = {kinks: {}, kink_groups: {}, infotags: {}, infotag_groups: {}, listitems: {}};
|
||||
const kinks: {
|
||||
listItems: {[key: string]: ListItem}
|
||||
kinks: {[key: string]: Kink}
|
||||
kinkGroups: {[key: string]: KinkGroup}
|
||||
infotags: {[key: string]: Infotag}
|
||||
infotagGroups: {[key: string]: InfotagGroup}
|
||||
} = {kinks: {}, kinkGroups: {}, infotags: {}, infotagGroups: {}, listItems: {}};
|
||||
for(const id in fields.kinks) {
|
||||
const oldKink = fields.kinks[id];
|
||||
kinks.kinks[oldKink.id] = {
|
||||
|
@ -115,8 +115,8 @@ async function fieldsGet(): Promise<void> {
|
|||
};
|
||||
}
|
||||
for(const id in fields.kink_groups) {
|
||||
const oldGroup = fields.kink_groups[id]!;
|
||||
kinks.kink_groups[oldGroup.id] = {
|
||||
const oldGroup = fields.kink_groups[id];
|
||||
kinks.kinkGroups[oldGroup.id] = {
|
||||
id: oldGroup.id,
|
||||
name: oldGroup.name,
|
||||
description: '',
|
||||
|
@ -132,12 +132,12 @@ async function fieldsGet(): Promise<void> {
|
|||
validator: oldInfotag.list,
|
||||
search_field: '',
|
||||
allow_legacy: true,
|
||||
infotag_group: oldInfotag.group_id
|
||||
infotag_group: parseInt(oldInfotag.group_id, 10)
|
||||
};
|
||||
}
|
||||
for(const id in fields.listitems) {
|
||||
const oldListItem = fields.listitems[id]!;
|
||||
kinks.listitems[oldListItem.id] = {
|
||||
const oldListItem = fields.listitems[id];
|
||||
kinks.listItems[oldListItem.id] = {
|
||||
id: oldListItem.id,
|
||||
name: oldListItem.name,
|
||||
value: oldListItem.value,
|
||||
|
@ -145,31 +145,33 @@ async function fieldsGet(): Promise<void> {
|
|||
};
|
||||
}
|
||||
for(const id in fields.infotag_groups) {
|
||||
const oldGroup = fields.infotag_groups[id]!;
|
||||
kinks.infotag_groups[oldGroup.id] = {
|
||||
id: oldGroup.id,
|
||||
const oldGroup = fields.infotag_groups[id];
|
||||
kinks.infotagGroups[oldGroup.id] = {
|
||||
id: parseInt(oldGroup.id, 10),
|
||||
name: oldGroup.name,
|
||||
description: oldGroup.description,
|
||||
sort_order: oldGroup.id
|
||||
};
|
||||
}
|
||||
Store.kinks = kinks;
|
||||
Store.shared = kinks;
|
||||
} catch(e) {
|
||||
Utils.ajaxError(e, 'Error loading character fields');
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
async function friendsGet(id: number): Promise<CharacterFriend[]> {
|
||||
return (await core.connection.queryApi<{friends: CharacterFriend[]}>('character-friends.php', {id})).friends;
|
||||
async function friendsGet(id: number): Promise<SimpleCharacter[]> {
|
||||
return (await core.connection.queryApi<{friends: SimpleCharacter[]}>('character-friends.php', {id})).friends;
|
||||
}
|
||||
|
||||
async function imagesGet(id: number): Promise<CharacterImage[]> {
|
||||
return (await core.connection.queryApi<{images: CharacterImage[]}>('character-images.php', {id})).images;
|
||||
}
|
||||
|
||||
async function guestbookGet(id: number, page: number): Promise<GuestbookState> {
|
||||
return core.connection.queryApi<GuestbookState>('character-guestbook.php', {id, page: page - 1});
|
||||
async function guestbookGet(id: number, offset: number): Promise<Guestbook> {
|
||||
const data = await core.connection.queryApi<{nextPage: boolean, posts: GuestbookPost[]}>('character-guestbook.php',
|
||||
{id, page: offset / 10});
|
||||
return {posts: data.posts, total: data.nextPage ? offset + 100 : offset};
|
||||
}
|
||||
|
||||
async function kinksGet(id: number): Promise<CharacterKink[]> {
|
||||
|
@ -180,23 +182,18 @@ async function kinksGet(id: number): Promise<CharacterKink[]> {
|
|||
});
|
||||
}
|
||||
|
||||
export function init(characters: {[key: string]: number}): void {
|
||||
Utils.setDomains(parserSettings.siteDomain, parserSettings.staticDomain);
|
||||
initParser(parserSettings);
|
||||
export function init(settings: Settings, characters: SimpleCharacter[]): void {
|
||||
Utils.setDomains('https://www.f-list.net/', 'https://static.f-list.net/');
|
||||
|
||||
Vue.component('character-select', CharacterSelect);
|
||||
Vue.component('character-link', CharacterLink);
|
||||
Vue.component('date-display', DateDisplay);
|
||||
Vue.component('simple-pager', SimplePager);
|
||||
Vue.component('bbcode', BBCodeView(new StandardBBCodeParser()));
|
||||
Vue.component('bbcode-editor', Editor);
|
||||
setCharacters(Object.keys(characters).map((name) => ({name, id: characters[name]})));
|
||||
Utils.init(settings, characters);
|
||||
core.connection.onEvent('connecting', () => {
|
||||
Utils.Settings.defaultCharacter = characters[core.connection.character];
|
||||
});
|
||||
Vue.directive('bbcode', (el, binding) => {
|
||||
while(el.firstChild !== null)
|
||||
el.removeChild(el.firstChild);
|
||||
el.appendChild(standardParser.parseEverything(<string>binding.value));
|
||||
Utils.settings.defaultCharacter = characters.find((x) => x.name === core.connection.character)!.id;
|
||||
});
|
||||
registerMethod('characterData', characterData);
|
||||
registerMethod('contactMethodIconUrl', contactMethodIconUrl);
|
||||
|
@ -209,12 +206,10 @@ export function init(characters: {[key: string]: number}): void {
|
|||
registerMethod('imageUrl', (image: CharacterImageOld) => image.url);
|
||||
registerMethod('memoUpdate', async(id: number, memo: string) => {
|
||||
await core.connection.queryApi('character-memo-save.php', {target: id, note: memo});
|
||||
return {id, memo, updated_at: Date.now() / 1000};
|
||||
});
|
||||
registerMethod('imageThumbUrl', (image: CharacterImage) => `${Utils.staticDomain}images/charthumb/${image.id}.${image.extension}`);
|
||||
registerMethod('bookmarkUpdate', async(id: number, state: boolean) => {
|
||||
await core.connection.queryApi(`bookmark-${state ? 'add' : 'remove'}.php`, {id});
|
||||
return state;
|
||||
});
|
||||
registerMethod('characterFriends', async(id: number) =>
|
||||
core.connection.queryApi<FriendsByCharacter>('character-friend-list.php', {id}));
|
||||
|
|
|
@ -37,16 +37,18 @@ export function parse(this: void | never, input: string, context: CommandContext
|
|||
for(let i = 0; i < command.params.length; ++i) {
|
||||
while(args[index] === ' ') ++index;
|
||||
const param = command.params[i];
|
||||
if(index === -1)
|
||||
if(index === -1) {
|
||||
if(param.optional !== undefined) continue;
|
||||
else return l('commands.tooFewParams');
|
||||
return l('commands.tooFewParams');
|
||||
}
|
||||
let delimiter = param.delimiter !== undefined ? param.delimiter : defaultDelimiters[param.type];
|
||||
if(delimiter === undefined) delimiter = ' ';
|
||||
const endIndex = delimiter.length > 0 ? args.indexOf(delimiter, index) : args.length;
|
||||
const value = args.substring(index, endIndex !== -1 ? endIndex : undefined);
|
||||
if(value.length === 0)
|
||||
if(value.length === 0) {
|
||||
if(param.optional !== undefined) continue;
|
||||
else return l('commands.tooFewParams');
|
||||
return l('commands.tooFewParams');
|
||||
}
|
||||
values[i] = value;
|
||||
switch(param.type) {
|
||||
case ParamType.String:
|
||||
|
@ -142,12 +144,6 @@ const commands: {readonly [key: string]: Command | undefined} = {
|
|||
exec: (_, status: Character.Status, statusmsg: string = '') => core.connection.send('STA', {status, statusmsg}),
|
||||
params: [{type: ParamType.Enum, options: userStatuses}, {type: ParamType.String, optional: true}]
|
||||
},
|
||||
ad: {
|
||||
exec: (conv: ChannelConversation, message: string) =>
|
||||
core.connection.send('LRP', {channel: conv.channel.id, message}),
|
||||
context: CommandContext.Channel,
|
||||
params: [{type: ParamType.String}]
|
||||
},
|
||||
roll: {
|
||||
exec: (conv: ChannelConversation | PrivateConversation, dice: string) => {
|
||||
if(Conversation.isChannel(conv)) core.connection.send('RLL', {channel: conv.channel.id, dice});
|
||||
|
|
|
@ -47,6 +47,7 @@ function VueRaven(this: void, raven: Raven.RavenStatic): Raven.RavenStatic {
|
|||
//tslint:enable
|
||||
|
||||
export function setupRaven(dsn: string, version: string): void {
|
||||
return; //TODO sentry temporarily disabled
|
||||
Raven.config(dsn, {
|
||||
release: version,
|
||||
dataCallback: (data: {culprit?: string, exception?: {values: {stacktrace: {frames: {filename: string}[]}}[]}}) => {
|
||||
|
|
|
@ -1,13 +1,10 @@
|
|||
<template>
|
||||
<div class="dropdown">
|
||||
<a class="form-control custom-select" aria-haspopup="true" :aria-expanded="isOpen" @click="isOpen = true"
|
||||
@blur="isOpen = false" style="width:100%;text-align:left;display:flex;align-items:center" role="button" tabindex="-1">
|
||||
<div style="flex:1">
|
||||
<slot name="title" style="flex:1"></slot>
|
||||
</div>
|
||||
<div class="dropdown" @focusout="blur">
|
||||
<a :class="linkClass" aria-haspopup="true" :aria-expanded="isOpen" @click.prevent="isOpen = !isOpen" href="#"
|
||||
style="width:100%;text-align:left;align-items:center" role="button" tabindex="-1" ref="button">
|
||||
<slot name="title">{{title}}</slot>
|
||||
</a>
|
||||
<div class="dropdown-menu" :style="open ? {display: 'block'} : undefined" @mousedown.stop.prevent @click="isOpen = false"
|
||||
ref="menu">
|
||||
<div class="dropdown-menu" ref="menu" @mousedown.prevent.stop @click.prevent.stop="menuClick()">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -20,33 +17,39 @@
|
|||
@Component
|
||||
export default class Dropdown extends Vue {
|
||||
isOpen = false;
|
||||
@Prop()
|
||||
@Prop({default: 'btn btn-secondary dropdown-toggle'})
|
||||
readonly linkClass!: string;
|
||||
@Prop
|
||||
readonly keepOpen?: boolean;
|
||||
@Prop
|
||||
readonly title?: string;
|
||||
|
||||
get open(): boolean {
|
||||
return this.keepOpen || this.isOpen;
|
||||
}
|
||||
|
||||
@Watch('open')
|
||||
@Watch('isOpen')
|
||||
onToggle(): void {
|
||||
const menu = this.$refs['menu'] as HTMLElement;
|
||||
if(!this.isOpen) {
|
||||
menu.style.cssText = '';
|
||||
return;
|
||||
}
|
||||
let element = <HTMLElement | null>this.$el;
|
||||
while(element !== null) {
|
||||
if(getComputedStyle(element).position === 'fixed') {
|
||||
menu.style.display = 'block';
|
||||
const offset = menu.getBoundingClientRect();
|
||||
menu.style.position = 'fixed';
|
||||
menu.style.left = `${offset.left}px`;
|
||||
menu.style.top = (offset.bottom < window.innerHeight) ? menu.style.top = `${offset.top}px` :
|
||||
`${this.$el.getBoundingClientRect().top - offset.bottom + offset.top}px`;
|
||||
break;
|
||||
}
|
||||
element = element.parentElement;
|
||||
menu.style.display = 'block';
|
||||
const offset = menu.getBoundingClientRect();
|
||||
menu.style.position = 'fixed';
|
||||
menu.style.left = offset.right < window.innerWidth ? `${offset.left}px` : `${window.innerWidth - offset.width}px`;
|
||||
menu.style.top = (offset.bottom < window.innerHeight) ? `${offset.top}px` :
|
||||
`${offset.top - offset.height - (<HTMLElement>this.$el).offsetHeight}px`;
|
||||
}
|
||||
|
||||
blur(event: FocusEvent): void {
|
||||
let elm = <HTMLElement | null>event.relatedTarget;
|
||||
while(elm) {
|
||||
if(elm === this.$refs['menu']) return;
|
||||
elm = elm.parentElement;
|
||||
}
|
||||
this.isOpen = false;
|
||||
}
|
||||
|
||||
menuClick(): void {
|
||||
if(!this.keepOpen) this.isOpen = false;
|
||||
}
|
||||
}
|
||||
</script>
|
|
@ -1,11 +1,10 @@
|
|||
<template>
|
||||
<dropdown class="filterable-select" :keepOpen="keepOpen">
|
||||
<dropdown class="filterable-select" linkClass="custom-select" :keepOpen="true">
|
||||
<template slot="title" v-if="multiple">{{label}}</template>
|
||||
<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/>
|
||||
</div>
|
||||
<div class="dropdown-items">
|
||||
<template v-if="multiple">
|
||||
|
@ -32,21 +31,20 @@
|
|||
components: {dropdown: Dropdown}
|
||||
})
|
||||
export default class FilterableSelect extends Vue {
|
||||
@Prop()
|
||||
@Prop
|
||||
readonly placeholder?: string;
|
||||
@Prop({required: true})
|
||||
readonly options!: object[];
|
||||
@Prop({default: () => ((filter: RegExp, value: string) => filter.test(value))})
|
||||
readonly filterFunc!: (filter: RegExp, value: object) => boolean;
|
||||
@Prop()
|
||||
@Prop
|
||||
readonly multiple?: true;
|
||||
@Prop()
|
||||
@Prop
|
||||
readonly value?: object | object[];
|
||||
@Prop()
|
||||
@Prop
|
||||
readonly title?: string;
|
||||
filter = '';
|
||||
selected: object | object[] | undefined = this.value !== undefined ? this.value : (this.multiple !== undefined ? [] : undefined);
|
||||
keepOpen = false;
|
||||
|
||||
@Watch('value')
|
||||
watchValue(newValue: object | object[] | undefined): void {
|
||||
|
@ -59,10 +57,8 @@
|
|||
const index = selected.indexOf(item);
|
||||
if(index === -1) selected.push(item);
|
||||
else selected.splice(index, 1);
|
||||
} else {
|
||||
this.keepOpen = false;
|
||||
} else
|
||||
this.selected = item;
|
||||
}
|
||||
this.$emit('input', this.selected);
|
||||
}
|
||||
|
||||
|
@ -91,6 +87,7 @@
|
|||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
button {
|
||||
display: flex;
|
||||
text-align: left
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<template>
|
||||
<span v-show="isShown">
|
||||
<div class="modal" @click.self="hideWithCheck()" style="display:flex;justify-content:center">
|
||||
<div class="modal" @mousedown.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">
|
||||
|
@ -49,17 +49,17 @@
|
|||
export default class Modal extends Vue {
|
||||
@Prop({default: ''})
|
||||
readonly action!: string;
|
||||
@Prop()
|
||||
@Prop
|
||||
readonly dialogClass?: {string: boolean};
|
||||
@Prop({default: true})
|
||||
readonly buttons!: boolean;
|
||||
@Prop({default: () => ({'btn-primary': true})})
|
||||
readonly buttonClass!: {string: boolean};
|
||||
@Prop()
|
||||
@Prop
|
||||
readonly disabled?: boolean;
|
||||
@Prop({default: true})
|
||||
readonly showCancel!: boolean;
|
||||
@Prop()
|
||||
@Prop
|
||||
readonly buttonText?: string;
|
||||
isShown = false;
|
||||
keepOpen = false;
|
||||
|
|
|
@ -1,19 +1,22 @@
|
|||
<template>
|
||||
<span :class="linkClasses" v-if="character">
|
||||
<slot v-if="deleted">[Deleted] {{ name }}</slot>
|
||||
<a :href="characterUrl" class="characterLinkLink" v-else><slot>{{ name }}</slot></a>
|
||||
<a :href="characterUrl" class="characterLinkLink" v-else :target="target"><slot>{{ name }}</slot></a>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {Component, Prop} from '@f-list/vue-ts';
|
||||
import Vue from 'vue';
|
||||
import {SimpleCharacter} from '../interfaces';
|
||||
import * as Utils from '../site/utils';
|
||||
|
||||
@Component
|
||||
export default class CharacterLink extends Vue {
|
||||
@Prop({required: true})
|
||||
readonly character!: {name: string, id: number, deleted: boolean} | string;
|
||||
readonly character!: SimpleCharacter | string;
|
||||
@Prop({default: '_blank'})
|
||||
readonly target!: string;
|
||||
|
||||
get deleted(): boolean {
|
||||
return typeof(this.character) === 'string' ? false : this.character.deleted;
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<template>
|
||||
<select class="form-control" :value="value" @change="emit">
|
||||
<option v-for="o in characters" :value="o.value" v-once>{{o.text}}</option>
|
||||
<select :value="value" @change="emit">
|
||||
<option v-for="character in characters" :value="character.id">{{character.name}}</option>
|
||||
<slot></slot>
|
||||
</select>
|
||||
</template>
|
||||
|
@ -8,24 +8,16 @@
|
|||
<script lang="ts">
|
||||
import {Component, Prop} from '@f-list/vue-ts';
|
||||
import Vue from 'vue';
|
||||
import {getCharacters} from './character_select/character_list';
|
||||
|
||||
interface SelectItem {
|
||||
value: number
|
||||
text: string
|
||||
}
|
||||
import {SimpleCharacter} from '../interfaces';
|
||||
import * as Utils from '../site/utils';
|
||||
|
||||
@Component
|
||||
export default class CharacterSelect extends Vue {
|
||||
@Prop({required: true})
|
||||
readonly value!: number;
|
||||
|
||||
get characters(): SelectItem[] {
|
||||
const characterList = getCharacters();
|
||||
const characters: SelectItem[] = [];
|
||||
for(const character of characterList)
|
||||
characters.push({value: character.id, text: character.name});
|
||||
return characters;
|
||||
get characters(): SimpleCharacter[] {
|
||||
return Utils.characters;
|
||||
}
|
||||
|
||||
emit(evt: Event): void {
|
||||
|
|
|
@ -1,14 +0,0 @@
|
|||
export interface CharacterItem {
|
||||
readonly name: string
|
||||
readonly id: number
|
||||
}
|
||||
|
||||
let characterList: ReadonlyArray<CharacterItem> = [];
|
||||
|
||||
export function setCharacters(characters: ReadonlyArray<CharacterItem>): void {
|
||||
characterList = characters;
|
||||
}
|
||||
|
||||
export function getCharacters(): ReadonlyArray<CharacterItem> {
|
||||
return characterList;
|
||||
}
|
|
@ -0,0 +1,45 @@
|
|||
<template>
|
||||
<div class="card bg-light">
|
||||
<div class="card-header" @click="toggle()" style="cursor:pointer" :class="headerClass">
|
||||
<h4>{{title}} <span class="fas" :class="'fa-chevron-' + (collapsed ? 'down' : 'up')"></span></h4>
|
||||
</div>
|
||||
<div :style="style" style="overflow:hidden">
|
||||
<div class="card-body" ref="content">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import {Component, Prop} from '@f-list/vue-ts';
|
||||
|
||||
@Component
|
||||
export default class Collapse extends Vue {
|
||||
@Prop({required: true})
|
||||
readonly title!: string;
|
||||
@Prop
|
||||
readonly headerClass?: string;
|
||||
collapsed = true;
|
||||
timeout = 0;
|
||||
style = {height: <string | undefined>'0', transition: 'height .2s'};
|
||||
|
||||
toggle(state?: boolean) {
|
||||
clearTimeout(this.timeout);
|
||||
this.collapsed = state !== undefined ? state : !this.collapsed;
|
||||
this.$emit(this.collapsed ? 'close' : 'open');
|
||||
if(this.collapsed) {
|
||||
this.style.transition = 'initial';
|
||||
this.style.height = `${(<HTMLElement>this.$refs['content']).scrollHeight}px`;
|
||||
setTimeout(() => {
|
||||
this.style.transition = 'height .2s';
|
||||
this.style.height = '0';
|
||||
}, 0);
|
||||
} else {
|
||||
this.style.height = `${(<HTMLElement>this.$refs['content']).scrollHeight}px`;
|
||||
this.timeout = window.setTimeout(() => this.style.height = undefined, 200);
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
|
@ -2,7 +2,9 @@ import {Component} from '@f-list/vue-ts';
|
|||
import Vue from 'vue';
|
||||
import Modal from './Modal.vue';
|
||||
|
||||
@Component
|
||||
@Component({
|
||||
components: {Modal}
|
||||
})
|
||||
export default class CustomDialog extends Vue {
|
||||
protected get dialog(): Modal {
|
||||
return <Modal>this.$children[0];
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
import {Component, Hook, Prop, Watch} from '@f-list/vue-ts';
|
||||
import {distanceInWordsToNow, format} from 'date-fns';
|
||||
import Vue from 'vue';
|
||||
import {Settings} from '../site/utils';
|
||||
import {settings} from '../site/utils';
|
||||
|
||||
@Component
|
||||
export default class DateDisplay extends Vue {
|
||||
|
@ -23,7 +23,7 @@
|
|||
const date = isNaN(+this.time) ? new Date(`${this.time}+00:00`) : new Date(+this.time * 1000);
|
||||
const absolute = format(date, 'YYYY-MM-DD HH:mm');
|
||||
const relative = distanceInWordsToNow(date, {addSuffix: true});
|
||||
if(Settings.fuzzyDates) {
|
||||
if(settings.fuzzyDates) {
|
||||
this.primary = relative;
|
||||
this.secondary = absolute;
|
||||
} else {
|
||||
|
|
|
@ -23,13 +23,13 @@
|
|||
readonly field!: string;
|
||||
@Prop({required: true})
|
||||
readonly errors!: {[key: string]: ReadonlyArray<string> | undefined};
|
||||
@Prop()
|
||||
@Prop
|
||||
readonly label?: string;
|
||||
@Prop()
|
||||
@Prop
|
||||
readonly id?: string;
|
||||
@Prop({default: false})
|
||||
readonly valid!: boolean;
|
||||
@Prop()
|
||||
@Prop
|
||||
readonly helptext?: string;
|
||||
|
||||
get hasErrors(): boolean {
|
||||
|
|
|
@ -26,13 +26,13 @@
|
|||
readonly field!: string;
|
||||
@Prop({required: true})
|
||||
readonly errors!: {[key: string]: ReadonlyArray<string> | undefined};
|
||||
@Prop()
|
||||
@Prop
|
||||
readonly label?: string;
|
||||
@Prop()
|
||||
@Prop
|
||||
readonly id?: string;
|
||||
@Prop({default: false})
|
||||
readonly valid!: boolean;
|
||||
@Prop()
|
||||
@Prop
|
||||
readonly helptext?: string;
|
||||
|
||||
get hasErrors(): boolean {
|
||||
|
|
|
@ -46,7 +46,7 @@
|
|||
readonly prev!: boolean;
|
||||
@Prop({default: false})
|
||||
readonly routed!: boolean;
|
||||
@Prop({default: () => ({})})
|
||||
@Prop({default(this: Vue & {$route: RouteParams}): RouteParams { return this.$route; }})
|
||||
readonly route!: RouteParams;
|
||||
@Prop({default: 'page'})
|
||||
readonly paramName!: string;
|
||||
|
|
|
@ -22,7 +22,8 @@ const Tabs = Vue.extend({
|
|||
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) }
|
||||
attrs: {href: '#'},
|
||||
staticClass: 'nav-link', class: {active: this._v === key}, on: {click: () => this.$emit('input', key)}
|
||||
}, [children[key]!])])))]);
|
||||
}
|
||||
});
|
||||
|
|
|
@ -3,25 +3,33 @@
|
|||
<div v-html="styling"></div>
|
||||
<div v-if="!characters" style="display:flex; align-items:center; justify-content:center; height: 100%;">
|
||||
<div class="card bg-light" style="width: 400px;">
|
||||
<h3 class="card-header" style="margin-top:0">{{l('title')}}</h3>
|
||||
<h3 class="card-header" style="margin-top:0;display:flex">
|
||||
{{l('title')}}
|
||||
<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>
|
||||
<div class="card-body">
|
||||
<div class="alert alert-danger" v-show="error">
|
||||
{{error}}
|
||||
</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"/>
|
||||
<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>
|
||||
|
@ -61,6 +69,7 @@
|
|||
</select>
|
||||
</div>
|
||||
</modal>
|
||||
<logs ref="logsDialog"></logs>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
@ -77,18 +86,17 @@
|
|||
import Vue from 'vue';
|
||||
import Chat from '../chat/Chat.vue';
|
||||
import {getKey, Settings} from '../chat/common';
|
||||
import core, {init as initCore} from '../chat/core';
|
||||
import core from '../chat/core';
|
||||
import l from '../chat/localize';
|
||||
import {init as profileApiInit} from '../chat/profile_api';
|
||||
import Logs from '../chat/Logs.vue';
|
||||
import Socket from '../chat/WebSocket';
|
||||
import Modal from '../components/Modal.vue';
|
||||
import Connection from '../fchat/connection';
|
||||
import {SimpleCharacter} from '../interfaces';
|
||||
import {Keys} from '../keys';
|
||||
import CharacterPage from '../site/character_page/character_page.vue';
|
||||
import {defaultHost, GeneralSettings, nativeRequire} from './common';
|
||||
import {fixLogs, Logs, SettingsStore} from './filesystem';
|
||||
import {fixLogs} from './filesystem';
|
||||
import * as SlimcatImporter from './importer';
|
||||
import Notifications from './notifications';
|
||||
|
||||
const webContents = electron.remote.getCurrentWebContents();
|
||||
const parent = electron.remote.getCurrentWindow().webContents;
|
||||
|
@ -106,17 +114,17 @@
|
|||
log.info('Loaded keytar.');
|
||||
|
||||
@Component({
|
||||
components: {chat: Chat, modal: Modal, characterPage: CharacterPage}
|
||||
components: {chat: Chat, modal: Modal, characterPage: CharacterPage, logs: Logs}
|
||||
})
|
||||
export default class Index extends Vue {
|
||||
showAdvanced = false;
|
||||
saveLogin = false;
|
||||
loggingIn = false;
|
||||
password = '';
|
||||
character: string | undefined;
|
||||
characters: string[] | undefined;
|
||||
character?: string;
|
||||
characters?: SimpleCharacter[];
|
||||
error = '';
|
||||
defaultCharacter: string | undefined;
|
||||
defaultCharacter?: number;
|
||||
l = l;
|
||||
settings!: GeneralSettings;
|
||||
importProgress = 0;
|
||||
|
@ -125,7 +133,7 @@
|
|||
fixCharacter = '';
|
||||
|
||||
@Hook('created')
|
||||
async created(): Promise<void> {
|
||||
created(): void {
|
||||
if(this.settings.account.length > 0) this.saveLogin = true;
|
||||
keyStore.getPassword(this.settings.account)
|
||||
.then((value: string) => this.password = value, (err: Error) => this.error = err.message);
|
||||
|
@ -140,7 +148,7 @@
|
|||
profileViewer.show();
|
||||
});
|
||||
electron.ipcRenderer.on('fix-logs', async() => {
|
||||
this.fixCharacters = await new SettingsStore().getAvailableCharacters();
|
||||
this.fixCharacters = await core.settingsStore.getAvailableCharacters();
|
||||
this.fixCharacter = this.fixCharacters[0];
|
||||
(<Modal>this.$refs['fixLogsModal']).show();
|
||||
});
|
||||
|
@ -169,15 +177,14 @@
|
|||
await keyStore.setPassword(this.settings.account, this.password);
|
||||
}
|
||||
Socket.host = this.settings.host;
|
||||
const connection = new Connection(`F-Chat 3.0 (${process.platform})`, electron.remote.app.getVersion(), Socket,
|
||||
this.settings.account, this.password);
|
||||
connection.onEvent('connecting', async() => {
|
||||
|
||||
core.connection.onEvent('connecting', async() => {
|
||||
if(!electron.ipcRenderer.sendSync('connect', core.connection.character) && process.env.NODE_ENV === 'production') {
|
||||
alert(l('login.alreadyLoggedIn'));
|
||||
return core.connection.close();
|
||||
}
|
||||
parent.send('connect', webContents.id, core.connection.character);
|
||||
this.character = connection.character;
|
||||
this.character = core.connection.character;
|
||||
if((await core.settingsStore.get('settings')) === undefined &&
|
||||
SlimcatImporter.canImportCharacter(core.connection.character)) {
|
||||
if(!confirm(l('importer.importGeneral'))) return core.settingsStore.set('settings', new Settings());
|
||||
|
@ -186,22 +193,21 @@
|
|||
(<Modal>this.$refs['importModal']).hide();
|
||||
}
|
||||
});
|
||||
connection.onEvent('connected', () => {
|
||||
core.connection.onEvent('connected', () => {
|
||||
core.watch(() => core.conversations.hasNew, (newValue) => parent.send('has-new', webContents.id, newValue));
|
||||
Raven.setUserContext({username: core.connection.character});
|
||||
});
|
||||
connection.onEvent('closed', () => {
|
||||
core.connection.onEvent('closed', () => {
|
||||
if(this.character === undefined) return;
|
||||
electron.ipcRenderer.send('disconnect', this.character);
|
||||
this.character = undefined;
|
||||
parent.send('disconnect', webContents.id);
|
||||
Raven.setUserContext();
|
||||
});
|
||||
initCore(connection, Logs, SettingsStore, Notifications);
|
||||
const charNames = Object.keys(data.characters);
|
||||
this.characters = charNames.sort();
|
||||
this.defaultCharacter = charNames.find((x) => data.characters[x] === data.default_character)!;
|
||||
profileApiInit(data.characters);
|
||||
core.connection.setCredentials(this.settings.account, this.password);
|
||||
this.characters = Object.keys(data.characters).map((name) => ({name, id: data.characters[name], deleted: false}))
|
||||
.sort((x, y) => x.name.localeCompare(y.name));
|
||||
this.defaultCharacter = data.default_character;
|
||||
} catch(e) {
|
||||
this.error = l('login.error');
|
||||
if(process.env.NODE_ENV !== 'production') throw e;
|
||||
|
@ -244,8 +250,8 @@
|
|||
preview.style.display = 'none';
|
||||
}
|
||||
|
||||
openProfileInBrowser(): void {
|
||||
electron.remote.shell.openExternal(`https://www.f-list.net/c/${this.profileName}`);
|
||||
async openProfileInBrowser(): Promise<void> {
|
||||
return electron.remote.shell.openExternal(`https://www.f-list.net/c/${this.profileName}`);
|
||||
}
|
||||
|
||||
get styling(): string {
|
||||
|
@ -259,6 +265,10 @@
|
|||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
showLogs(): void {
|
||||
(<Logs>this.$refs['logsDialog']).show();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
|
|
|
@ -75,7 +75,7 @@
|
|||
tabs: Tab[] = [];
|
||||
activeTab: Tab | undefined;
|
||||
tabMap: {[key: number]: Tab} = {};
|
||||
isMaximized = browserWindow.isMaximized();
|
||||
isMaximized = false;
|
||||
canOpenTab = true;
|
||||
l = l;
|
||||
hasUpdate = false;
|
||||
|
@ -83,8 +83,8 @@
|
|||
lockTab = false;
|
||||
|
||||
@Hook('mounted')
|
||||
mounted(): void {
|
||||
this.addTab();
|
||||
async mounted(): Promise<void> {
|
||||
await this.addTab();
|
||||
electron.ipcRenderer.on('settings', (_: Event, settings: GeneralSettings) => this.settings = settings);
|
||||
electron.ipcRenderer.on('allow-new-tabs', (_: Event, allow: boolean) => this.canOpenTab = allow);
|
||||
electron.ipcRenderer.on('open-tab', () => this.addTab());
|
||||
|
@ -153,6 +153,7 @@
|
|||
browserWindow.hide();
|
||||
return false;
|
||||
};
|
||||
this.isMaximized = browserWindow.isMaximized();
|
||||
}
|
||||
|
||||
destroyAllTabs(): void {
|
||||
|
@ -186,12 +187,12 @@
|
|||
];
|
||||
}
|
||||
|
||||
addTab(): void {
|
||||
async addTab(): Promise<void> {
|
||||
if(this.lockTab) return;
|
||||
const tray = new electron.remote.Tray(trayIcon);
|
||||
tray.setToolTip(l('title'));
|
||||
tray.on('click', (_) => this.trayClicked(tab));
|
||||
const view = new electron.remote.BrowserView();
|
||||
const view = new electron.remote.BrowserView({webPreferences: {nodeIntegration: true}});
|
||||
view.setAutoResize({width: true, height: true});
|
||||
electron.ipcRenderer.send('tab-added', view.webContents.id);
|
||||
const tab = {active: false, view, user: undefined, hasNew: false, tray};
|
||||
|
@ -200,13 +201,14 @@
|
|||
this.tabMap[view.webContents.id] = tab;
|
||||
this.show(tab);
|
||||
this.lockTab = true;
|
||||
view.webContents.loadURL(url.format({
|
||||
await view.webContents.loadURL(url.format({
|
||||
pathname: path.join(__dirname, 'index.html'),
|
||||
protocol: 'file:',
|
||||
slashes: true,
|
||||
query: {settings: JSON.stringify(this.settings)}
|
||||
}));
|
||||
view.webContents.on('did-stop-loading', () => this.lockTab = false);
|
||||
tab.view.setBounds(getWindowBounds());
|
||||
this.lockTab = false;
|
||||
}
|
||||
|
||||
show(tab: Tab): void {
|
||||
|
@ -259,6 +261,7 @@
|
|||
align-items: center;
|
||||
line-height: 1;
|
||||
-webkit-app-region: no-drag;
|
||||
flex-grow: 0;
|
||||
}
|
||||
|
||||
.btn-default {
|
||||
|
|
|
@ -35,12 +35,17 @@ import * as electron from 'electron';
|
|||
import * as path from 'path';
|
||||
import * as qs from 'querystring';
|
||||
import {getKey} from '../chat/common';
|
||||
import {init as initCore} from '../chat/core';
|
||||
import l from '../chat/localize';
|
||||
import {setupRaven} from '../chat/vue-raven';
|
||||
import Socket from '../chat/WebSocket';
|
||||
import Connection from '../fchat/connection';
|
||||
import {Keys} from '../keys';
|
||||
import {GeneralSettings, nativeRequire} from './common';
|
||||
import {Logs, SettingsStore} from './filesystem';
|
||||
import * as SlimcatImporter from './importer';
|
||||
import Index from './Index.vue';
|
||||
import Notifications from './notifications';
|
||||
|
||||
document.addEventListener('keydown', (e: KeyboardEvent) => {
|
||||
if(e.ctrlKey && e.shiftKey && getKey(e) === Keys.KeyI)
|
||||
|
@ -79,7 +84,7 @@ function openIncognito(url: string): void {
|
|||
} catch(e) {
|
||||
console.error(e);
|
||||
}
|
||||
const commands = {
|
||||
const commands = {
|
||||
chrome: 'chrome.exe -incognito', firefox: 'firefox.exe -private-window', vivaldi: 'vivaldi.exe -incognito',
|
||||
opera: 'opera.exe -private'
|
||||
};
|
||||
|
@ -99,7 +104,7 @@ webContents.on('context-menu', (_, props) => {
|
|||
menuTemplate.push({
|
||||
id: 'copy',
|
||||
label: l('action.copy'),
|
||||
role: can('Copy') ? 'copy' : '',
|
||||
role: can('Copy') ? 'copy' : undefined,
|
||||
accelerator: 'CmdOrCtrl+C',
|
||||
enabled: can('Copy')
|
||||
});
|
||||
|
@ -107,13 +112,13 @@ webContents.on('context-menu', (_, props) => {
|
|||
menuTemplate.push({
|
||||
id: 'cut',
|
||||
label: l('action.cut'),
|
||||
role: can('Cut') ? 'cut' : '',
|
||||
role: can('Cut') ? 'cut' : undefined,
|
||||
accelerator: 'CmdOrCtrl+X',
|
||||
enabled: can('Cut')
|
||||
}, {
|
||||
id: 'paste',
|
||||
label: l('action.paste'),
|
||||
role: props.editFlags.canPaste ? 'paste' : '',
|
||||
role: props.editFlags.canPaste ? 'paste' : undefined,
|
||||
accelerator: 'CmdOrCtrl+V',
|
||||
enabled: props.editFlags.canPaste
|
||||
});
|
||||
|
@ -165,7 +170,7 @@ webContents.on('context-menu', (_, props) => {
|
|||
let dictDir = path.join(electron.remote.app.getPath('userData'), 'spellchecker');
|
||||
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)});
|
||||
electron.webFrame.setSpellCheckProvider('', {spellCheck: (words, callback) => callback(words.filter((x) => spellchecker.isMisspelled(x)))});
|
||||
|
||||
function onSettings(s: GeneralSettings): void {
|
||||
settings = s;
|
||||
|
@ -187,6 +192,10 @@ if(params['import'] !== undefined)
|
|||
alert(l('importer.error'));
|
||||
}
|
||||
onSettings(settings);
|
||||
|
||||
const connection = new Connection(`F-Chat 3.0 (${process.platform})`, electron.remote.app.getVersion(), Socket);
|
||||
initCore(connection, Logs, SettingsStore, Notifications);
|
||||
|
||||
//tslint:disable-next-line:no-unused-expression
|
||||
new Index({
|
||||
el: '#app',
|
||||
|
|
|
@ -1,8 +1,7 @@
|
|||
import * as electron from 'electron';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
export const defaultHost = 'wss://chat.f-list.net:9799';
|
||||
export const defaultHost = 'wss://chat.f-list.net/chat2';
|
||||
|
||||
export class GeneralSettings {
|
||||
account = '';
|
||||
|
@ -18,30 +17,6 @@ export class GeneralSettings {
|
|||
hwAcceleration = true;
|
||||
}
|
||||
|
||||
export function mkdir(dir: string): void {
|
||||
try {
|
||||
fs.mkdirSync(dir);
|
||||
} catch(e) {
|
||||
if(!(e instanceof Error)) throw e;
|
||||
switch((<Error & {code: string}>e).code) {
|
||||
case 'ENOENT':
|
||||
const dirname = path.dirname(dir);
|
||||
if(dirname === dir) throw e;
|
||||
mkdir(dirname);
|
||||
mkdir(dir);
|
||||
break;
|
||||
default:
|
||||
try {
|
||||
const stat = fs.statSync(dir);
|
||||
if(stat.isDirectory()) return;
|
||||
} catch(e) {
|
||||
console.log(e);
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//tslint:disable
|
||||
const Module = require('module');
|
||||
|
||||
|
|
|
@ -4,11 +4,9 @@ import log from 'electron-log'; //tslint:disable-line:match-default-export-name
|
|||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import {promisify} from 'util';
|
||||
import {mkdir} from './common';
|
||||
|
||||
const dictDir = path.join(electron.app.getPath('userData'), 'spellchecker');
|
||||
mkdir(dictDir);
|
||||
const requestConfig = {responseType: 'arraybuffer'};
|
||||
fs.mkdirSync(dictDir, {recursive: true});
|
||||
|
||||
const downloadedPath = path.join(dictDir, 'downloaded.json');
|
||||
const downloadUrl = 'https://client.f-list.net/dicts/';
|
||||
|
@ -40,7 +38,8 @@ export async function ensureDictionary(lang: string): Promise<void> {
|
|||
const filePath = path.join(dictDir, `${lang}.${type}`);
|
||||
const downloaded = downloadedDictionaries[file.name];
|
||||
if(downloaded === undefined || downloaded.hash !== file.hash || !fs.existsSync(filePath)) {
|
||||
await writeFile(filePath, Buffer.from((await Axios.get<string>(`${downloadUrl}${file.name}`, requestConfig)).data));
|
||||
const dictionary = (await Axios.get<string>(`${downloadUrl}${file.name}`, {responseType: 'arraybuffer'})).data;
|
||||
await writeFile(filePath, Buffer.from(dictionary));
|
||||
downloadedDictionaries[file.name] = file;
|
||||
await writeFile(downloadedPath, JSON.stringify(downloadedDictionaries));
|
||||
}
|
||||
|
|
|
@ -6,7 +6,7 @@ import {Message as MessageImpl} from '../chat/common';
|
|||
import core from '../chat/core';
|
||||
import {Character, Conversation, Logs as Logging, Settings} from '../chat/interfaces';
|
||||
import l from '../chat/localize';
|
||||
import {GeneralSettings, mkdir} from './common';
|
||||
import {GeneralSettings} from './common';
|
||||
|
||||
declare module '../chat/interfaces' {
|
||||
interface State {
|
||||
|
@ -16,7 +16,6 @@ declare module '../chat/interfaces' {
|
|||
|
||||
const dayMs = 86400000;
|
||||
const read = promisify(fs.read);
|
||||
const noAssert = process.env.NODE_ENV === 'production';
|
||||
|
||||
function writeFile(p: fs.PathLike | number, data: string | object | number,
|
||||
options?: {encoding?: string | null; mode?: number | string; flag?: string} | string | null): void {
|
||||
|
@ -46,7 +45,7 @@ interface Index {
|
|||
|
||||
export function getLogDir(this: void, character: string): string {
|
||||
const dir = path.join(core.state.generalSettings!.logDirectory, character, 'logs');
|
||||
mkdir(dir);
|
||||
fs.mkdirSync(dir, {recursive: true});
|
||||
return dir;
|
||||
}
|
||||
|
||||
|
@ -66,15 +65,15 @@ export function checkIndex(this: void, index: Index, message: Message, key: stri
|
|||
index[key] = item = {name, index: {}, offsets: []};
|
||||
const nameLength = Buffer.byteLength(name);
|
||||
buffer = Buffer.allocUnsafe(nameLength + 8);
|
||||
buffer.writeUInt8(nameLength, 0, noAssert);
|
||||
buffer.writeUInt8(nameLength, 0);
|
||||
buffer.write(name, 1);
|
||||
offset = nameLength + 1;
|
||||
}
|
||||
const newValue = typeof size === 'function' ? size() : size;
|
||||
item.index[date] = item.offsets.length;
|
||||
item.offsets.push(newValue);
|
||||
buffer.writeUInt16LE(date, offset, noAssert);
|
||||
buffer.writeUIntLE(newValue, offset + 2, 5, noAssert);
|
||||
buffer.writeUInt16LE(date, offset);
|
||||
buffer.writeUIntLE(newValue, offset + 2, 5);
|
||||
return buffer;
|
||||
}
|
||||
|
||||
|
@ -83,25 +82,25 @@ export function serializeMessage(message: Message): {serialized: Buffer, size: n
|
|||
const senderLength = Buffer.byteLength(name);
|
||||
const messageLength = Buffer.byteLength(message.text);
|
||||
const buffer = Buffer.allocUnsafe(senderLength + messageLength + 10);
|
||||
buffer.writeUInt32LE(message.time.getTime() / 1000, 0, noAssert);
|
||||
buffer.writeUInt8(message.type, 4, noAssert);
|
||||
buffer.writeUInt8(senderLength, 5, noAssert);
|
||||
buffer.writeUInt32LE(message.time.getTime() / 1000, 0);
|
||||
buffer.writeUInt8(message.type, 4);
|
||||
buffer.writeUInt8(senderLength, 5);
|
||||
buffer.write(name, 6);
|
||||
let offset = senderLength + 6;
|
||||
buffer.writeUInt16LE(messageLength, offset, noAssert);
|
||||
buffer.writeUInt16LE(messageLength, offset);
|
||||
buffer.write(message.text, offset += 2);
|
||||
buffer.writeUInt16LE(offset += messageLength, offset, noAssert);
|
||||
buffer.writeUInt16LE(offset += messageLength, offset);
|
||||
return {serialized: buffer, size: offset + 2};
|
||||
}
|
||||
|
||||
function deserializeMessage(buffer: Buffer, offset: number = 0,
|
||||
characterGetter: (name: string) => Character = (name) => core.characters.get(name),
|
||||
unsafe: boolean = noAssert): {size: number, message: Conversation.Message} {
|
||||
const time = buffer.readUInt32LE(offset, unsafe);
|
||||
const type = buffer.readUInt8(offset += 4, unsafe);
|
||||
const senderLength = buffer.readUInt8(offset += 1, unsafe);
|
||||
characterGetter: (name: string) => Character = (name) => core.characters.get(name)
|
||||
): {size: number, message: Conversation.Message} {
|
||||
const time = buffer.readUInt32LE(offset);
|
||||
const type = buffer.readUInt8(offset += 4);
|
||||
const senderLength = buffer.readUInt8(offset += 1);
|
||||
const sender = buffer.toString('utf8', offset += 1, offset += senderLength);
|
||||
const messageLength = buffer.readUInt16LE(offset, unsafe);
|
||||
const messageLength = buffer.readUInt16LE(offset);
|
||||
const text = buffer.toString('utf8', offset += 2, offset + messageLength);
|
||||
const message = new MessageImpl(type, characterGetter(sender), text, new Date(time * 1000));
|
||||
return {message, size: senderLength + messageLength + 10};
|
||||
|
@ -126,7 +125,7 @@ export function fixLogs(character: string): void {
|
|||
const indexFd = fs.openSync(indexPath, 'r+');
|
||||
fs.readSync(indexFd, buffer, 0, 1, 0);
|
||||
let pos = 0, lastDay = 0;
|
||||
const nameEnd = buffer.readUInt8(0, noAssert) + 1;
|
||||
const nameEnd = buffer.readUInt8(0) + 1;
|
||||
fs.ftruncateSync(indexFd, nameEnd);
|
||||
fs.readSync(indexFd, buffer, 0, nameEnd, null); //tslint:disable-line:no-null-keyword
|
||||
const size = (fs.fstatSync(fd)).size;
|
||||
|
@ -137,12 +136,12 @@ export function fixLogs(character: string): void {
|
|||
const deserialized = deserializeMessage(buffer, 0, (name) => ({
|
||||
gender: 'None', status: 'online', statusText: '', isFriend: false, isBookmarked: false, isChatOp: false,
|
||||
isIgnored: false, name
|
||||
}), false);
|
||||
}));
|
||||
const time = deserialized.message.time;
|
||||
const day = Math.floor(time.getTime() / dayMs - time.getTimezoneOffset() / 1440);
|
||||
if(day > lastDay) {
|
||||
buffer.writeUInt16LE(day, 0, noAssert);
|
||||
buffer.writeUIntLE(pos, 2, 5, noAssert);
|
||||
buffer.writeUInt16LE(day, 0);
|
||||
buffer.writeUIntLE(pos, 2, 5);
|
||||
fs.writeSync(indexFd, buffer, 0, 7);
|
||||
lastDay = day;
|
||||
}
|
||||
|
@ -166,7 +165,7 @@ function loadIndex(name: string): Index {
|
|||
if(file.substr(-4) === '.idx')
|
||||
try {
|
||||
const content = fs.readFileSync(path.join(dir, file));
|
||||
let offset = content.readUInt8(0, noAssert) + 1;
|
||||
let offset = content.readUInt8(0) + 1;
|
||||
const item: IndexItem = {
|
||||
name: content.toString('utf8', 1, offset),
|
||||
index: {},
|
||||
|
@ -175,7 +174,7 @@ function loadIndex(name: string): Index {
|
|||
for(; offset < content.length; offset += 7) {
|
||||
const key = content.readUInt16LE(offset);
|
||||
item.index[key] = item.offsets.length;
|
||||
item.offsets.push(content.readUIntLE(offset + 2, 5, noAssert));
|
||||
item.offsets.push(content.readUIntLE(offset + 2, 5));
|
||||
}
|
||||
index[file.slice(0, -4).toLowerCase()] = item;
|
||||
} catch(e) {
|
||||
|
@ -289,14 +288,14 @@ export class Logs implements Logging {
|
|||
|
||||
async getAvailableCharacters(): Promise<ReadonlyArray<string>> {
|
||||
const baseDir = core.state.generalSettings!.logDirectory;
|
||||
mkdir(baseDir);
|
||||
fs.mkdirSync(baseDir, {recursive: true});
|
||||
return (fs.readdirSync(baseDir)).filter((x) => fs.statSync(path.join(baseDir, x)).isDirectory());
|
||||
}
|
||||
}
|
||||
|
||||
function getSettingsDir(character: string = core.connection.character): string {
|
||||
const dir = path.join(core.state.generalSettings!.logDirectory, character, 'settings');
|
||||
mkdir(dir);
|
||||
fs.mkdirSync(dir, {recursive: true});
|
||||
return dir;
|
||||
}
|
||||
|
||||
|
@ -315,6 +314,7 @@ export class SettingsStore implements Settings.Store {
|
|||
return (fs.readdirSync(baseDir)).filter((x) => fs.statSync(path.join(baseDir, x)).isDirectory());
|
||||
}
|
||||
|
||||
//tslint:disable-next-line:no-async-without-await
|
||||
async set<K extends keyof Settings.Keys>(key: K, value: Settings.Keys[K]): Promise<void> {
|
||||
writeFile(path.join(getSettingsDir(), key), JSON.stringify(value));
|
||||
}
|
||||
|
|
|
@ -231,7 +231,8 @@ export async function importCharacter(ownCharacter: string, progress: (progress:
|
|||
}
|
||||
++index;
|
||||
continue;
|
||||
} else if(char === '\r' || char === '\n') {
|
||||
}
|
||||
if(char === '\r' || char === '\n') {
|
||||
const nextLine = content.substr(index + (char === '\r' ? 2 : 1), 29);
|
||||
if(logRegex.test(nextLine) || content.length - index <= 2) {
|
||||
const line = content.substring(start, index);
|
||||
|
|
|
@ -35,7 +35,7 @@ import * as fs from 'fs';
|
|||
import * as path from 'path';
|
||||
import * as url from 'url';
|
||||
import l from '../chat/localize';
|
||||
import {GeneralSettings, mkdir} from './common';
|
||||
import {defaultHost, GeneralSettings} from './common';
|
||||
import {ensureDictionary, getAvailableDictionaries} from './dictionaries';
|
||||
import * as windowState from './window_state';
|
||||
import BrowserWindow = Electron.BrowserWindow;
|
||||
|
@ -51,11 +51,11 @@ const characters: string[] = [];
|
|||
let tabCount = 0;
|
||||
|
||||
const baseDir = app.getPath('userData');
|
||||
mkdir(baseDir);
|
||||
fs.mkdirSync(baseDir, {recursive: true});
|
||||
let shouldImportSettings = false;
|
||||
|
||||
const settingsDir = path.join(baseDir, 'data');
|
||||
mkdir(settingsDir);
|
||||
fs.mkdirSync(settingsDir, {recursive: true});
|
||||
const settingsFile = path.join(settingsDir, 'settings');
|
||||
const settings = new GeneralSettings();
|
||||
|
||||
|
@ -107,7 +107,7 @@ function setUpWebContents(webContents: Electron.WebContents): void {
|
|||
e.preventDefault();
|
||||
const profileMatch = linkUrl.match(/^https?:\/\/(www\.)?f-list.net\/c\/([^/#]+)\/?#?/);
|
||||
if(profileMatch !== null && settings.profileViewer) webContents.send('open-profile', decodeURIComponent(profileMatch[2]));
|
||||
else electron.shell.openExternal(linkUrl);
|
||||
else return electron.shell.openExternal(linkUrl);
|
||||
};
|
||||
|
||||
webContents.on('will-navigate', openLinkExternally);
|
||||
|
@ -118,14 +118,14 @@ function createWindow(): Electron.BrowserWindow | undefined {
|
|||
if(tabCount >= 3) return;
|
||||
const lastState = windowState.getSavedWindowState();
|
||||
const windowProperties: Electron.BrowserWindowConstructorOptions & {maximized: boolean} = {
|
||||
...lastState, center: lastState.x === undefined, show: false
|
||||
...lastState, center: lastState.x === undefined, show: false, webPreferences: {nodeIntegration: true}
|
||||
};
|
||||
if(process.platform === 'darwin') windowProperties.titleBarStyle = 'hiddenInset';
|
||||
else windowProperties.frame = false;
|
||||
const window = new electron.BrowserWindow(windowProperties);
|
||||
windows.push(window);
|
||||
|
||||
window.loadURL(url.format({
|
||||
window.loadURL(url.format({ //tslint:disable-line:no-floating-promises
|
||||
pathname: path.join(__dirname, 'window.html'),
|
||||
protocol: 'file:',
|
||||
slashes: true,
|
||||
|
@ -145,7 +145,7 @@ function createWindow(): Electron.BrowserWindow | undefined {
|
|||
}
|
||||
|
||||
function showPatchNotes(): void {
|
||||
electron.shell.openExternal('https://wiki.f-list.net/F-Chat_3.0#Changelog');
|
||||
electron.shell.openExternal('https://wiki.f-list.net/F-Chat_3.0#Changelog'); //tslint:disable-line:no-floating-promises
|
||||
}
|
||||
|
||||
function onReady(): void {
|
||||
|
@ -160,6 +160,8 @@ function onReady(): void {
|
|||
|
||||
if(settings.version !== app.getVersion()) {
|
||||
showPatchNotes();
|
||||
if(settings.host === 'wss://chat.f-list.net:9799')
|
||||
settings.host = defaultHost;
|
||||
settings.version = app.getVersion();
|
||||
setGeneralSettings(settings);
|
||||
}
|
||||
|
@ -235,8 +237,8 @@ function onReady(): void {
|
|||
{
|
||||
label: l('settings.logDir'),
|
||||
click: (_, window: BrowserWindow) => {
|
||||
const dir = <string[] | undefined>electron.dialog.showOpenDialog(
|
||||
{defaultPath: new GeneralSettings().logDirectory, properties: ['openDirectory']});
|
||||
const dir = electron.dialog.showOpenDialog(
|
||||
{defaultPath: settings.logDirectory, properties: ['openDirectory']});
|
||||
if(dir !== undefined) {
|
||||
if(dir[0].startsWith(path.dirname(app.getPath('exe'))))
|
||||
return electron.dialog.showErrorBox(l('settings.logDir'), l('settings.logDir.inAppDir'));
|
||||
|
@ -369,7 +371,7 @@ function onReady(): void {
|
|||
});
|
||||
electron.ipcMain.on('connect', (e: Event & {sender: Electron.WebContents}, character: string) => {
|
||||
if(characters.indexOf(character) !== -1) return e.returnValue = false;
|
||||
else characters.push(character);
|
||||
characters.push(character);
|
||||
e.returnValue = true;
|
||||
});
|
||||
electron.ipcMain.on('dictionary-add', (_: Event, word: string) => {
|
||||
|
|
|
@ -4,36 +4,12 @@ const pkg = require(path.join(__dirname, 'package.json'));
|
|||
const fs = require('fs');
|
||||
const child_process = require('child_process');
|
||||
|
||||
function mkdir(dir) {
|
||||
try {
|
||||
fs.mkdirSync(dir);
|
||||
} catch(e) {
|
||||
if(!(e instanceof Error)) throw e;
|
||||
switch(e.code) {
|
||||
case 'ENOENT':
|
||||
const dirname = path.dirname(dir);
|
||||
if(dirname === dir) throw e;
|
||||
mkdir(dirname);
|
||||
mkdir(dir);
|
||||
break;
|
||||
default:
|
||||
try {
|
||||
const stat = fs.statSync(dir);
|
||||
if(stat.isDirectory()) return;
|
||||
} catch(e) {
|
||||
console.log(e);
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const distDir = path.join(__dirname, 'dist');
|
||||
const isBeta = pkg.version.indexOf('beta') !== -1;
|
||||
const spellcheckerPath = 'spellchecker/build/Release/spellchecker.node', keytarPath = 'keytar/build/Release/keytar.node';
|
||||
const modules = path.join(__dirname, 'app', 'node_modules');
|
||||
mkdir(path.dirname(path.join(modules, spellcheckerPath)));
|
||||
mkdir(path.dirname(path.join(modules, keytarPath)));
|
||||
fs.mkdirSync(path.dirname(path.join(modules, spellcheckerPath)), {recursive: true});
|
||||
fs.mkdirSync(path.dirname(path.join(modules, keytarPath)), {recursive: true});
|
||||
fs.copyFileSync(require.resolve(spellcheckerPath), path.join(modules, spellcheckerPath));
|
||||
fs.copyFileSync(require.resolve(keytarPath), path.join(modules, keytarPath));
|
||||
|
||||
|
@ -69,7 +45,7 @@ require('electron-packager')({
|
|||
setupExe: setupName,
|
||||
remoteReleases: 'https://client.f-list.net/win32/' + (isBeta ? '?channel=beta' : ''),
|
||||
signWithParams: process.argv.length > 3 ? `/a /f ${process.argv[2]} /p ${process.argv[3]} /fd sha256 /tr http://timestamp.digicert.com /td sha256` : undefined
|
||||
}).catch((e) => console.log(`Error while creating installer: ${e.message}`));
|
||||
}).catch((e) => console.error(`Error while creating installer: ${e.message}`));
|
||||
} else if(process.platform === 'darwin') {
|
||||
console.log('Creating Mac DMG');
|
||||
const target = path.join(distDir, `F-Chat.dmg`);
|
||||
|
@ -104,7 +80,7 @@ require('electron-packager')({
|
|||
fs.renameSync(path.join(appPaths[0], 'F-Chat'), path.join(appPaths[0], 'AppRun'));
|
||||
fs.copyFileSync(path.join(__dirname, 'build', 'icon.png'), path.join(appPaths[0], 'icon.png'));
|
||||
const libDir = path.join(appPaths[0], 'usr', 'lib'), libSource = path.join(__dirname, 'build', 'linux-libs');
|
||||
mkdir(libDir);
|
||||
fs.mkdirSync(libDir, {recursive: true});
|
||||
for(const file of fs.readdirSync(libSource))
|
||||
fs.copyFileSync(path.join(libSource, file), path.join(libDir, file));
|
||||
fs.symlinkSync(path.join(appPaths[0], 'icon.png'), path.join(appPaths[0], '.DirIcon'));
|
||||
|
@ -114,13 +90,13 @@ require('electron-packager')({
|
|||
const stream = fs.createWriteStream(downloaded);
|
||||
res.data.pipe(stream);
|
||||
stream.on('close', () => {
|
||||
const args = [appPaths[0], 'fchat.AppImage', '-u', 'zsync|httpos://client.f-list.net/fchat.AppImage.zsync'];
|
||||
const args = [appPaths[0], 'fchat.AppImage', '-u', 'zsync|https://client.f-list.net/fchat.AppImage.zsync'];
|
||||
if(process.argv.length > 2) args.push('-s', '--sign-key', process.argv[2]);
|
||||
else console.warn('Warning: Creating unsigned AppImage');
|
||||
if(process.argv.length > 3) args.push('--sign-args', `--passphrase=${process.argv[3]}`);
|
||||
if(process.argv.length > 3) args.push('--sign-args', `--no-tty --passphrase=${process.argv[3]}`);
|
||||
fs.chmodSync(downloaded, 0o755);
|
||||
child_process.spawn(downloaded, ['--appimage-extract'], {cwd: distDir}).on('close', () => {
|
||||
const child = child_process.spawn(path.join(distDir, 'squashfs-root', 'AppRun'), args, {cwd: distDir});
|
||||
const child = child_process.spawn(path.join(distDir, 'squashfs-root', 'AppRun'), args, {cwd: distDir, env: {ARCH: 'x86_64'}});
|
||||
child.stdout.on('data', (data) => console.log(data.toString()));
|
||||
child.stderr.on('data', (data) => console.error(data.toString()));
|
||||
});
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "fchat",
|
||||
"version": "3.0.10",
|
||||
"version": "3.0.13",
|
||||
"author": "The F-List Team",
|
||||
"description": "F-List.net Chat Client",
|
||||
"main": "main.js",
|
||||
|
|
|
@ -7,8 +7,8 @@
|
|||
"allowJs": true,
|
||||
"noEmitHelpers": true,
|
||||
"importHelpers": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"strict": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true
|
||||
},
|
||||
|
|
|
@ -33,7 +33,6 @@ const mainConfig = {
|
|||
},
|
||||
plugins: [
|
||||
new ForkTsCheckerWebpackPlugin({
|
||||
workers: 2,
|
||||
async: false,
|
||||
tslint: path.join(__dirname, '../tslint.json'),
|
||||
tsconfig: './tsconfig-main.json'
|
||||
|
@ -90,7 +89,6 @@ const mainConfig = {
|
|||
},
|
||||
plugins: [
|
||||
new ForkTsCheckerWebpackPlugin({
|
||||
workers: 2,
|
||||
async: false,
|
||||
tslint: path.join(__dirname, '../tslint.json'),
|
||||
tsconfig: './tsconfig-renderer.json',
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import Axios, {AxiosError, AxiosResponse} from 'axios';
|
||||
import * as qs from 'qs';
|
||||
import {Connection as Interfaces, WebSocketConnection} from './interfaces';
|
||||
import ReadyState = WebSocketConnection.ReadyState;
|
||||
|
||||
const fatalErrors = [2, 3, 4, 9, 30, 31, 33, 39, 40, 62, -4];
|
||||
const dieErrors = [9, 30, 31, 39, 40];
|
||||
|
@ -11,25 +12,32 @@ async function queryApi(this: void, endpoint: string, data: object): Promise<Axi
|
|||
|
||||
export default class Connection implements Interfaces.Connection {
|
||||
character = '';
|
||||
vars: Interfaces.Vars & {[key: string]: string} = <any>{}; //tslint:disable-line:no-any
|
||||
vars: Interfaces.Vars = <any>{}; //tslint:disable-line:no-any
|
||||
protected socket: WebSocketConnection | undefined = undefined;
|
||||
private messageHandlers: {[key in keyof Interfaces.ServerCommands]?: Interfaces.CommandHandler<key>[]} = {};
|
||||
private connectionHandlers: {[key in Interfaces.EventType]?: Interfaces.EventHandler[]} = {};
|
||||
//tslint:disable-next-line:no-object-literal-type-assertion
|
||||
private messageHandlers = <{ [key in keyof Interfaces.ServerCommands]: Interfaces.CommandHandler<key>[] }>{};
|
||||
private connectionHandlers: { [key in Interfaces.EventType]?: Interfaces.EventHandler[] } = {};
|
||||
private errorHandlers: ((error: Error) => void)[] = [];
|
||||
private ticket = '';
|
||||
private cleanClose = false;
|
||||
private reconnectTimer: NodeJS.Timer | undefined;
|
||||
private readonly ticketProvider: Interfaces.TicketProvider;
|
||||
private account = '';
|
||||
private ticketProvider?: Interfaces.TicketProvider;
|
||||
private reconnectDelay = 0;
|
||||
private isReconnect = false;
|
||||
private pinTimeout?: NodeJS.Timer;
|
||||
|
||||
constructor(private readonly clientName: string, private readonly version: string,
|
||||
private readonly socketProvider: new() => WebSocketConnection, private readonly account: string,
|
||||
ticketProvider: Interfaces.TicketProvider | string) {
|
||||
private readonly socketProvider: new() => WebSocketConnection) {
|
||||
}
|
||||
|
||||
setCredentials(account: string, ticketProvider: Interfaces.TicketProvider | string): void {
|
||||
this.account = account;
|
||||
this.ticketProvider = typeof ticketProvider === 'string' ? async() => this.getTicket(ticketProvider) : ticketProvider;
|
||||
}
|
||||
|
||||
async connect(character: string): Promise<void> {
|
||||
if(!this.ticketProvider) throw new Error('No credentials set!');
|
||||
this.cleanClose = false;
|
||||
if(this.character !== character) this.isReconnect = false;
|
||||
this.character = character;
|
||||
|
@ -67,6 +75,7 @@ export default class Connection implements Interfaces.Connection {
|
|||
method: 'ticket',
|
||||
ticket: this.ticket
|
||||
});
|
||||
this.resetPinTimeout();
|
||||
});
|
||||
this.socket.onMessage(async(msg: string) => {
|
||||
const type = <keyof Interfaces.ServerCommands>msg.substr(0, 3);
|
||||
|
@ -74,6 +83,7 @@ export default class Connection implements Interfaces.Connection {
|
|||
return this.handleMessage(type, data);
|
||||
});
|
||||
this.socket.onClose(async() => {
|
||||
if(this.pinTimeout) clearTimeout(this.pinTimeout);
|
||||
if(!this.cleanClose) this.reconnect();
|
||||
this.socket = undefined;
|
||||
await this.invokeHandlers('closed', !this.cleanClose);
|
||||
|
@ -95,10 +105,11 @@ export default class Connection implements Interfaces.Connection {
|
|||
}
|
||||
|
||||
get isOpen(): boolean {
|
||||
return this.socket !== undefined;
|
||||
return this.socket !== undefined && this.socket.readyState === ReadyState.OPEN;
|
||||
}
|
||||
|
||||
async queryApi<T = object>(endpoint: string, data?: {account?: string, ticket?: string}): Promise<T> {
|
||||
if(!this.ticketProvider) throw new Error('No credentials set!');
|
||||
if(data === undefined) data = {};
|
||||
data.account = this.account;
|
||||
data.ticket = this.ticket;
|
||||
|
@ -156,10 +167,11 @@ export default class Connection implements Interfaces.Connection {
|
|||
for(const handler of handlers) await handler(data, time);
|
||||
switch(type) {
|
||||
case 'VAR':
|
||||
this.vars[data.variable] = data.value;
|
||||
this.vars[<keyof Interfaces.Vars>data.variable] = data.value;
|
||||
break;
|
||||
case 'PIN':
|
||||
this.send('PIN');
|
||||
this.resetPinTimeout();
|
||||
break;
|
||||
case 'ERR':
|
||||
if(fatalErrors.indexOf(data.number) !== -1) {
|
||||
|
@ -198,4 +210,9 @@ export default class Connection implements Interfaces.Connection {
|
|||
if(request) (<Error & {request: true}>error).request = true;
|
||||
for(const handler of this.errorHandlers) handler(error);
|
||||
}
|
||||
|
||||
private resetPinTimeout(): void {
|
||||
if(this.pinTimeout) clearTimeout(this.pinTimeout);
|
||||
this.pinTimeout = setTimeout(() => this.socket!.close(), 90000);
|
||||
}
|
||||
}
|
|
@ -122,21 +122,22 @@ export namespace Connection {
|
|||
export type EventHandler = (isReconnect: boolean) => Promise<void> | void;
|
||||
|
||||
export interface Vars {
|
||||
readonly chat_max: number
|
||||
readonly priv_max: number
|
||||
readonly lfrp_max: number
|
||||
readonly cds_max: number
|
||||
readonly lfrp_flood: number
|
||||
readonly msg_flood: number
|
||||
readonly sta_flood: number
|
||||
readonly permissions: number
|
||||
readonly icon_blacklist: ReadonlyArray<string>
|
||||
chat_max: number
|
||||
priv_max: number
|
||||
lfrp_max: number
|
||||
cds_max: number
|
||||
lfrp_flood: number
|
||||
msg_flood: number
|
||||
sta_flood: number
|
||||
permissions: number
|
||||
icon_blacklist: ReadonlyArray<string>
|
||||
}
|
||||
|
||||
export interface Connection {
|
||||
readonly character: string
|
||||
readonly vars: Vars
|
||||
readonly vars: Readonly<Vars>
|
||||
readonly isOpen: boolean
|
||||
setCredentials(account: string, ticketProvider: TicketProvider | string): void
|
||||
connect(character: string): void
|
||||
close(keepState?: boolean): void
|
||||
onMessage<K extends keyof ServerCommands>(type: K, handler: CommandHandler<K>): void
|
||||
|
|
|
@ -46,10 +46,9 @@ export interface Infotag {
|
|||
name: string
|
||||
type: InfotagType
|
||||
search_field: string
|
||||
validator: string
|
||||
validator?: string
|
||||
allow_legacy: boolean
|
||||
infotag_group: string
|
||||
list?: number
|
||||
infotag_group: number
|
||||
}
|
||||
|
||||
export interface Character extends SimpleCharacter {
|
||||
|
@ -57,7 +56,7 @@ export interface Character extends SimpleCharacter {
|
|||
name: string
|
||||
title: string
|
||||
description: string
|
||||
kinks: {[key: string]: KinkChoice | number | undefined}
|
||||
kinks: {[key: number]: KinkChoice | number | undefined}
|
||||
inlines: {[key: string]: InlineImage}
|
||||
customs: {[key: string]: CustomKink | undefined}
|
||||
infotags: {[key: number]: CharacterInfotag | undefined}
|
||||
|
@ -96,4 +95,42 @@ export interface CustomKink {
|
|||
name: string
|
||||
choice: KinkChoice
|
||||
description: string
|
||||
}
|
||||
|
||||
export interface KinkGroup {
|
||||
id: number
|
||||
name: string
|
||||
description: string
|
||||
sort_order: number
|
||||
}
|
||||
|
||||
export interface InfotagGroup {
|
||||
id: number
|
||||
name: string
|
||||
description: string
|
||||
sort_order: number
|
||||
}
|
||||
|
||||
export interface ListItem {
|
||||
id: number
|
||||
name: string
|
||||
value: string
|
||||
sort_order: number
|
||||
}
|
||||
|
||||
export const enum InlineDisplayMode {DISPLAY_ALL, DISPLAY_SFW, DISPLAY_NONE}
|
||||
|
||||
export interface Settings {
|
||||
animateEicons: boolean
|
||||
inlineDisplayMode: InlineDisplayMode
|
||||
defaultCharacter: number
|
||||
fuzzyDates: boolean
|
||||
}
|
||||
|
||||
export interface SharedDefinitions {
|
||||
readonly listItems: {readonly [key: string]: Readonly<ListItem>}
|
||||
readonly kinks: {readonly [key: string]: Readonly<Kink>}
|
||||
readonly kinkGroups: {readonly [key: string]: Readonly<KinkGroup>}
|
||||
readonly infotags: {readonly [key: string]: Readonly<Infotag>}
|
||||
readonly infotagGroups: {readonly [key: string]: Readonly<InfotagGroup>}
|
||||
}
|
|
@ -63,15 +63,13 @@
|
|||
import * as Raven from 'raven-js';
|
||||
import Vue from 'vue';
|
||||
import Chat from '../chat/Chat.vue';
|
||||
import core, {init as initCore} from '../chat/core';
|
||||
import core from '../chat/core';
|
||||
import l from '../chat/localize';
|
||||
import {init as profileApiInit} from '../chat/profile_api';
|
||||
import Socket from '../chat/WebSocket';
|
||||
import Modal from '../components/Modal.vue';
|
||||
import Connection from '../fchat/connection';
|
||||
import {SimpleCharacter} from '../interfaces';
|
||||
import CharacterPage from '../site/character_page/character_page.vue';
|
||||
import {appVersion, GeneralSettings, getGeneralSettings, Logs, setGeneralSettings, SettingsStore} from './filesystem';
|
||||
import Notifications from './notifications';
|
||||
import {appVersion, GeneralSettings, getGeneralSettings, setGeneralSettings, SettingsStore} from './filesystem';
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
|
@ -97,9 +95,9 @@
|
|||
showAdvanced = false;
|
||||
saveLogin = false;
|
||||
loggingIn = false;
|
||||
characters?: ReadonlyArray<string>;
|
||||
characters?: ReadonlyArray<SimpleCharacter>;
|
||||
error = '';
|
||||
defaultCharacter?: string;
|
||||
defaultCharacter?: number;
|
||||
settingsStore = new SettingsStore();
|
||||
l = l;
|
||||
settings!: GeneralSettings;
|
||||
|
@ -147,27 +145,20 @@
|
|||
}
|
||||
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);
|
||||
connection.onEvent('connected', () => {
|
||||
core.connection.setCredentials(this.settings.account, this.settings.password);
|
||||
core.connection.onEvent('connected', () => {
|
||||
Raven.setUserContext({username: core.connection.character});
|
||||
document.addEventListener('backbutton', confirmBack);
|
||||
NativeBackground.start();
|
||||
});
|
||||
connection.onEvent('closed', () => {
|
||||
core.connection.onEvent('closed', () => {
|
||||
Raven.setUserContext();
|
||||
document.removeEventListener('backbutton', confirmBack);
|
||||
NativeBackground.stop();
|
||||
});
|
||||
initCore(connection, Logs, SettingsStore, Notifications);
|
||||
const charNames = Object.keys(data.characters);
|
||||
this.characters = charNames.sort();
|
||||
for(const character of charNames)
|
||||
if(data.characters[character] === data.default_character) {
|
||||
this.defaultCharacter = character;
|
||||
break;
|
||||
}
|
||||
profileApiInit(data.characters);
|
||||
this.characters = Object.keys(data.characters).map((name) => ({name, id: data.characters[name], deleted: false}))
|
||||
.sort((x, y) => x.name.localeCompare(y.name));
|
||||
this.defaultCharacter = data.default_character;
|
||||
} catch(e) {
|
||||
this.error = l('login.error');
|
||||
if(process.env.NODE_ENV !== 'production') throw e;
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_1_8" project-jdk-name="1.8" project-jdk-type="JavaSDK" />
|
||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_1_7" project-jdk-name="1.8" project-jdk-type="JavaSDK" />
|
||||
</project>
|
|
@ -8,8 +8,8 @@ android {
|
|||
applicationId "net.f_list.fchat"
|
||||
minSdkVersion 21
|
||||
targetSdkVersion 27
|
||||
versionCode 21
|
||||
versionName "3.0.10"
|
||||
versionCode 25
|
||||
versionName "3.0.12"
|
||||
}
|
||||
buildTypes {
|
||||
release {
|
||||
|
|
|
@ -2,19 +2,24 @@ package net.f_list.fchat
|
|||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.PowerManager
|
||||
import android.webkit.JavascriptInterface
|
||||
|
||||
class Background(private val ctx: Context) {
|
||||
private val serviceIntent: Intent by lazy { Intent(ctx, BackgroundService::class.java) }
|
||||
|
||||
private val powerManager: PowerManager by lazy { ctx.getSystemService(Context.POWER_SERVICE) as PowerManager }
|
||||
private var wakeLock: PowerManager.WakeLock? = null
|
||||
|
||||
@JavascriptInterface
|
||||
fun start() {
|
||||
wakeLock = powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "fchat")
|
||||
wakeLock!!.acquire()
|
||||
ctx.startService(serviceIntent)
|
||||
}
|
||||
|
||||
@JavascriptInterface
|
||||
fun stop() {
|
||||
if(wakeLock != null && wakeLock!!.isHeld) wakeLock!!.release()
|
||||
ctx.stopService(serviceIntent)
|
||||
}
|
||||
}
|
|
@ -13,6 +13,7 @@ import android.os.Bundle
|
|||
import android.os.Environment
|
||||
import android.os.Handler
|
||||
import android.view.KeyEvent
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.webkit.JsResult
|
||||
import android.webkit.WebChromeClient
|
||||
|
@ -96,7 +97,7 @@ class MainActivity : Activity() {
|
|||
"n.getCharacters=function(){return JSON.parse(n.getCharactersN())}})(NativeLogs)", null)
|
||||
}
|
||||
}
|
||||
|
||||
debugHandler.postDelayed(keepAlive, 10000);
|
||||
}
|
||||
|
||||
private fun addFolder(folder: java.io.File, out: ZipOutputStream, path: String) {
|
||||
|
@ -109,6 +110,13 @@ class MainActivity : Activity() {
|
|||
}
|
||||
}
|
||||
|
||||
val keepAlive = object : Runnable {
|
||||
override fun run() {
|
||||
webView.dispatchWindowVisibilityChanged(View.VISIBLE);
|
||||
debugHandler.postDelayed(this, 10000)
|
||||
}
|
||||
}
|
||||
|
||||
val debug = Runnable {
|
||||
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
val permission = checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE)
|
||||
|
|
|
@ -30,8 +30,13 @@
|
|||
* @see {@link https://github.com/f-list/exported|GitHub repo}
|
||||
*/
|
||||
import Axios from 'axios';
|
||||
import {init as initCore} from '../chat/core';
|
||||
import {setupRaven} from '../chat/vue-raven';
|
||||
import Socket from '../chat/WebSocket';
|
||||
import Connection from '../fchat/connection';
|
||||
import {appVersion, Logs, SettingsStore} from './filesystem';
|
||||
import Index from './Index.vue';
|
||||
import Notifications from './notifications';
|
||||
|
||||
const version = (<{version: string}>require('./package.json')).version; //tslint:disable-line:no-require-imports
|
||||
(<any>window)['setupPlatform'] = (platform: string) => { //tslint:disable-line:no-any
|
||||
|
@ -41,6 +46,9 @@ const version = (<{version: string}>require('./package.json')).version; //tslint
|
|||
if(process.env.NODE_ENV === 'production')
|
||||
setupRaven('https://a9239b17b0a14f72ba85e8729b9d1612@sentry.f-list.net/2', `mobile-${version}`);
|
||||
|
||||
const connection = new Connection('F-Chat 3.0 (Mobile)', appVersion, Socket);
|
||||
initCore(connection, Logs, SettingsStore, Notifications);
|
||||
|
||||
new Index({ //tslint:disable-line:no-unused-expression
|
||||
el: '#app'
|
||||
});
|
|
@ -31,7 +31,7 @@ export const appVersion = (<{version: string}>require('./package.json')).version
|
|||
export class GeneralSettings {
|
||||
account = '';
|
||||
password = '';
|
||||
host = 'wss://chat.f-list.net:9799';
|
||||
host = 'wss://chat.f-list.net/chat2';
|
||||
theme = 'default';
|
||||
version = appVersion;
|
||||
}
|
||||
|
@ -135,7 +135,9 @@ export class Logs implements Logging {
|
|||
export async function getGeneralSettings(): Promise<GeneralSettings | undefined> {
|
||||
const file = await NativeFile.read('!settings');
|
||||
if(file === undefined) return undefined;
|
||||
return <GeneralSettings>JSON.parse(file);
|
||||
const settings = <GeneralSettings>JSON.parse(file);
|
||||
if(settings.host === 'wss://chat.f-list.net:9799') settings.host = 'wss://chat.f-list.net/chat2';
|
||||
return settings;
|
||||
}
|
||||
|
||||
export async function setGeneralSettings(value: GeneralSettings): Promise<void> {
|
||||
|
|
|
@ -121,7 +121,7 @@ class Logs: NSObject, WKScriptMessageHandler {
|
|||
indexItem = IndexItem(conversation as String)
|
||||
index![key] = indexItem
|
||||
let cstring = conversation.utf8String
|
||||
var length = strlen(cstring)
|
||||
var length = strlen(cstring!)
|
||||
write(indexFd.fileDescriptor, &length, 1)
|
||||
write(indexFd.fileDescriptor, cstring, length)
|
||||
}
|
||||
|
@ -135,11 +135,11 @@ class Logs: NSObject, WKScriptMessageHandler {
|
|||
write(fd.fileDescriptor, &time, 4)
|
||||
write(fd.fileDescriptor, &type, 1)
|
||||
var cstring = sender.utf8String
|
||||
var length = strlen(cstring)
|
||||
var length = strlen(cstring!)
|
||||
write(fd.fileDescriptor, &length, 1)
|
||||
write(fd.fileDescriptor, cstring, length)
|
||||
cstring = text.utf8String
|
||||
length = strlen(cstring)
|
||||
length = strlen(cstring!)
|
||||
write(fd.fileDescriptor, &length, 2)
|
||||
write(fd.fileDescriptor, cstring, length)
|
||||
var size = fd.offsetInFile - start
|
||||
|
|
|
@ -20,7 +20,11 @@ class ViewController: UIViewController, WKNavigationDelegate, WKUIDelegate {
|
|||
controller.add(Logs(), name: "Logs")
|
||||
config.userContentController = controller
|
||||
config.mediaTypesRequiringUserActionForPlayback = [.video]
|
||||
config.setValue(true, forKey: "_alwaysRunsAtForegroundPriority")
|
||||
if #available(iOS 12.2, *) {
|
||||
config.setValue(true, forKey: "alwaysRunsAtForegroundPriority")
|
||||
} else {
|
||||
config.setValue(true, forKey: "_alwaysRunsAtForegroundPriority")
|
||||
}
|
||||
webView = WKWebView(frame: UIApplication.shared.windows[0].frame, configuration: config)
|
||||
webView.uiDelegate = self
|
||||
webView.navigationDelegate = self
|
||||
|
|
|
@ -22,6 +22,6 @@ export default class Notifications extends BaseNotifications {
|
|||
}
|
||||
|
||||
async requestPermission(): Promise<void> {
|
||||
NativeNotification.requestPermission();
|
||||
return NativeNotification.requestPermission();
|
||||
}
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "net.f_list.fchat",
|
||||
"version": "3.0.10",
|
||||
"version": "3.0.12",
|
||||
"displayName": "F-Chat",
|
||||
"author": "The F-List Team",
|
||||
"description": "F-List.net Chat Client",
|
||||
|
|
|
@ -45,7 +45,7 @@ const config = {
|
|||
]
|
||||
},
|
||||
plugins: [
|
||||
new ForkTsCheckerWebpackPlugin({workers: 2, async: false, vue: true, tslint: path.join(__dirname, '../tslint.json')}),
|
||||
new ForkTsCheckerWebpackPlugin({async: false, vue: true, tslint: path.join(__dirname, '../tslint.json')}),
|
||||
new VueLoaderPlugin()
|
||||
],
|
||||
resolve: {
|
||||
|
|
56
package.json
56
package.json
|
@ -5,48 +5,48 @@
|
|||
"description": "F-List Exported",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@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": "^2.0.1",
|
||||
"@f-list/fork-ts-checker-webpack-plugin": "^1.3.7",
|
||||
"@f-list/vue-ts": "^1.0.3",
|
||||
"@fortawesome/fontawesome-free": "^5.9.0",
|
||||
"@types/lodash": "^4.14.134",
|
||||
"@types/sortablejs": "^1.7.2",
|
||||
"axios": "^0.19.0",
|
||||
"bootstrap": "^4.3.1",
|
||||
"css-loader": "^3.0.0",
|
||||
"date-fns": "^1.30.1",
|
||||
"electron": "3.0.13",
|
||||
"electron-log": "^2.2.17",
|
||||
"electron-packager": "^13.0.1",
|
||||
"electron-rebuild": "^1.8.2",
|
||||
"electron": "^5.0.4",
|
||||
"electron-log": "^3.0.1",
|
||||
"electron-packager": "^14.0.0",
|
||||
"electron-rebuild": "^1.8.4",
|
||||
"extract-loader": "^3.1.0",
|
||||
"file-loader": "^2.0.0",
|
||||
"file-loader": "^4.0.0",
|
||||
"lodash": "^4.17.11",
|
||||
"node-sass": "^4.11.0",
|
||||
"optimize-css-assets-webpack-plugin": "^5.0.1",
|
||||
"qs": "^6.6.0",
|
||||
"raven-js": "^3.27.0",
|
||||
"raven-js": "^3.27.2",
|
||||
"sass-loader": "^7.1.0",
|
||||
"sortablejs": "^1.8.0-rc1",
|
||||
"sortablejs": "~1.9.0",
|
||||
"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-template-compiler": "^2.5.21",
|
||||
"webpack": "^4.27.1"
|
||||
"ts-loader": "^6.0.3",
|
||||
"tslib": "^1.10.0",
|
||||
"tslint": "^5.17.0",
|
||||
"typescript": "^3.5.2",
|
||||
"vue": "^2.6.8",
|
||||
"vue-loader": "^15.7.0",
|
||||
"vue-template-compiler": "^2.6.8",
|
||||
"webpack": "^4.35.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"keytar": "^4.3.0",
|
||||
"spellchecker": "^3.5.0"
|
||||
"keytar": "^4.10.0",
|
||||
"spellchecker": "^3.6.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"appdmg": "^0.5.2",
|
||||
"appdmg": "^0.6.0",
|
||||
"electron-squirrel-startup": "^1.0.0",
|
||||
"electron-winstaller": "^2.7.0"
|
||||
"electron-winstaller": "^3.0.4"
|
||||
},
|
||||
"scripts": {
|
||||
"postinstall": "electron-rebuild -o spellchecker,keytar"
|
||||
"postinstall": "electron-rebuild -fo spellchecker,keytar"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -113,7 +113,7 @@ div.indentText {
|
|||
}
|
||||
}
|
||||
|
||||
.styledText, .bbcode {
|
||||
.bbcode {
|
||||
@include force-word-wrapping;
|
||||
max-width: 100%;
|
||||
a {
|
||||
|
|
|
@ -83,6 +83,7 @@
|
|||
border-color: theme-color("secondary");
|
||||
border-top-right-radius: 0;
|
||||
border-top-left-radius: 0;
|
||||
white-space: nowrap;
|
||||
@media (min-width: breakpoint-min(sm)) {
|
||||
.name {
|
||||
display: none;
|
||||
|
|
|
@ -54,5 +54,6 @@
|
|||
|
||||
* {
|
||||
min-height: 0;
|
||||
min-width: 0;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
|
@ -59,4 +59,12 @@ select {
|
|||
@extend .custom-select;
|
||||
-webkit-appearance: none;
|
||||
-moz-appearance: none;
|
||||
}
|
||||
|
||||
a.btn {
|
||||
color: $link-color;
|
||||
|
||||
&:hover {
|
||||
color: $link-hover-color;
|
||||
}
|
||||
}
|
|
@ -1,4 +0,0 @@
|
|||
.kink-editor-panel {
|
||||
position: fixed;
|
||||
width: 55%;
|
||||
}
|
|
@ -1,24 +1,15 @@
|
|||
.note-folder {
|
||||
clear: both;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
.note-folder-total, .note-folder-unread {
|
||||
width: 3.5rem;
|
||||
display: inline-block;
|
||||
float: left;
|
||||
text-align: center;
|
||||
}
|
||||
.note-folder-unread {
|
||||
font-weight: bold;
|
||||
float: right;
|
||||
}
|
||||
.note-folder-name {
|
||||
width: auto;
|
||||
display: inline-block;
|
||||
float: left;
|
||||
}
|
||||
.note-folder-icon {
|
||||
display: inline-block;
|
||||
float: right;
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -5,9 +5,9 @@
|
|||
"description": "F-Chat Themes",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-free-webfonts": "^1.0.3",
|
||||
"bootstrap": "^4.0.0",
|
||||
"node-sass": "^4.7.2"
|
||||
"@fortawesome/fontawesome-free": "^5.6.3",
|
||||
"bootstrap": "^4.2.1",
|
||||
"node-sass": "^4.11.0"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "node-sass --importer=./importer --output css"
|
||||
|
|
|
@ -9,6 +9,5 @@
|
|||
@import "../tickets";
|
||||
@import "../notes";
|
||||
@import "../threads";
|
||||
@import "../kink_editor";
|
||||
@import "../flist_overrides";
|
||||
@import "../tag_input";
|
||||
|
|
|
@ -30,6 +30,7 @@ $body-bg: $gray-100;
|
|||
$link-color: $gray-800;
|
||||
$link-hover-color: lighten($link-color, 15%);
|
||||
$dropdown-bg: $gray-200;
|
||||
$dropdown-divider-bg: $gray-300;
|
||||
|
||||
// Modal Dialogs
|
||||
$modal-backdrop-bg: $white;
|
||||
|
|
471
scss/yarn.lock
471
scss/yarn.lock
File diff suppressed because it is too large
Load Diff
|
@ -5,7 +5,7 @@
|
|||
<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>
|
||||
<sidebar :character="character" @memo="memo" :oldApi="oldApi"></sidebar>
|
||||
</div>
|
||||
<div class="col-md-8 col-lg-9 col-xl-10 profile-body" v-if="!loading && character">
|
||||
<div id="characterView">
|
||||
|
@ -34,28 +34,26 @@
|
|||
</tabs>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="tab-content">
|
||||
<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">
|
||||
<character-infotags :character="character" ref="tab1"></character-infotags>
|
||||
</div>
|
||||
<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'}">
|
||||
<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'}"
|
||||
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">
|
||||
<character-friends :character="character" ref="tab5"></character-friends>
|
||||
<div role="tabpanel" v-show="tab === '0'">
|
||||
<div style="margin-bottom:10px">
|
||||
<bbcode :text="character.character.description"></bbcode>
|
||||
</div>
|
||||
<character-kinks :character="character" :oldApi="oldApi" ref="tab0"></character-kinks>
|
||||
</div>
|
||||
<div role="tabpanel" v-show="tab === '1'">
|
||||
<character-infotags :character="character" ref="tab1"></character-infotags>
|
||||
</div>
|
||||
<div role="tabpanel" v-show="tab === '2'" v-if="!oldApi">
|
||||
<character-groups :character="character" ref="tab2"></character-groups>
|
||||
</div>
|
||||
<div role="tabpanel" v-show="tab === '3'">
|
||||
<character-images :character="character" ref="tab3" :use-preview="imagePreview"></character-images>
|
||||
</div>
|
||||
<div v-if="character.settings.guestbook" role="tabpanel" v-show="tab === '4'">
|
||||
<character-guestbook :character="character" :oldApi="oldApi" ref="tab4"></character-guestbook>
|
||||
</div>
|
||||
<div v-if="character.is_self || character.settings.show_friends" role="tabpanel" v-show="tab === '5'">
|
||||
<character-friends :character="character" ref="tab5"></character-friends>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -68,18 +66,18 @@
|
|||
<script lang="ts">
|
||||
import {Component, Hook, Prop, Watch} from '@f-list/vue-ts';
|
||||
import Vue from 'vue';
|
||||
import {standardParser} from '../../bbcode/standard';
|
||||
import * as Utils from '../utils';
|
||||
import {methods, Store} from './data_store';
|
||||
import {Character, SharedStore} from './interfaces';
|
||||
|
||||
import {StandardBBCodeParser} from '../../bbcode/standard';
|
||||
import {BBCodeView} from '../../bbcode/view';
|
||||
import DateDisplay from '../../components/date_display.vue';
|
||||
import Tabs from '../../components/tabs';
|
||||
import * as Utils from '../utils';
|
||||
import {methods, Store} from './data_store';
|
||||
import FriendsView from './friends.vue';
|
||||
import GroupsView from './groups.vue';
|
||||
import GuestbookView from './guestbook.vue';
|
||||
import ImagesView from './images.vue';
|
||||
import InfotagsView from './infotags.vue';
|
||||
import {Character, SharedStore} from './interfaces';
|
||||
import CharacterKinksView from './kinks.vue';
|
||||
import Sidebar from './sidebar.vue';
|
||||
|
||||
|
@ -87,28 +85,25 @@
|
|||
show?(): void
|
||||
}
|
||||
|
||||
const standardParser = new StandardBBCodeParser();
|
||||
|
||||
@Component({
|
||||
components: {
|
||||
sidebar: Sidebar,
|
||||
date: DateDisplay, tabs: Tabs,
|
||||
'character-friends': FriendsView,
|
||||
'character-guestbook': GuestbookView,
|
||||
'character-groups': GroupsView,
|
||||
'character-infotags': InfotagsView,
|
||||
'character-images': ImagesView,
|
||||
'character-kinks': CharacterKinksView
|
||||
sidebar: Sidebar, date: DateDisplay, 'character-friends': FriendsView, 'character-guestbook': GuestbookView,
|
||||
'character-groups': GroupsView, 'character-infotags': InfotagsView, 'character-images': ImagesView, tabs: Tabs,
|
||||
'character-kinks': CharacterKinksView, bbcode: BBCodeView(standardParser)
|
||||
}
|
||||
})
|
||||
export default class CharacterPage extends Vue {
|
||||
@Prop()
|
||||
@Prop
|
||||
readonly name?: string;
|
||||
@Prop()
|
||||
readonly characterid?: number;
|
||||
@Prop
|
||||
readonly id?: number;
|
||||
@Prop({required: true})
|
||||
readonly authenticated!: boolean;
|
||||
@Prop()
|
||||
@Prop
|
||||
readonly oldApi?: true;
|
||||
@Prop()
|
||||
@Prop
|
||||
readonly imagePreview?: true;
|
||||
shared: SharedStore = Store;
|
||||
character: Character | undefined;
|
||||
|
@ -143,20 +138,15 @@
|
|||
Vue.set(this.character!, 'memo', memo);
|
||||
}
|
||||
|
||||
bookmarked(state: boolean): void {
|
||||
Vue.set(this.character!, 'bookmarked', state);
|
||||
}
|
||||
|
||||
private async _getCharacter(): Promise<void> {
|
||||
this.error = '';
|
||||
this.character = undefined;
|
||||
if(this.name === undefined || this.name.length === 0)
|
||||
if((this.name === undefined || this.name.length === 0) && !this.id)
|
||||
return;
|
||||
try {
|
||||
this.loading = true;
|
||||
await methods.fieldsGet();
|
||||
this.character = await methods.characterData(this.name, this.characterid);
|
||||
standardParser.allowInlines = true;
|
||||
this.character = await methods.characterData(this.name, this.id);
|
||||
standardParser.inlines = this.character.character.inlines;
|
||||
} catch(e) {
|
||||
this.error = Utils.isJSONError(e) ? <string>e.response.data.error : (<Error>e).message;
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<div class="contact-method" :title="altText">
|
||||
<div class="contact-method" :title="infotag.name">
|
||||
<span v-if="contactLink" class="contact-link">
|
||||
<a :href="contactLink" target="_blank" rel="nofollow noreferrer noopener">
|
||||
<img :src="iconUrl"><span class="contact-value">{{value}}</span>
|
||||
|
@ -14,39 +14,27 @@
|
|||
<script lang="ts">
|
||||
import {Component, Prop} from '@f-list/vue-ts';
|
||||
import Vue from 'vue';
|
||||
import {CharacterInfotag, Infotag} from '../../interfaces';
|
||||
import {formatContactLink, formatContactValue} from './contact_utils';
|
||||
import {methods, Store} from './data_store';
|
||||
|
||||
interface DisplayContactMethod {
|
||||
id: number
|
||||
value: string
|
||||
}
|
||||
import {methods} from './data_store';
|
||||
|
||||
@Component
|
||||
export default class ContactMethodView extends Vue {
|
||||
@Prop({required: true})
|
||||
private readonly method!: DisplayContactMethod;
|
||||
readonly infotag!: Infotag;
|
||||
@Prop({required: true})
|
||||
readonly data!: CharacterInfotag;
|
||||
|
||||
get iconUrl(): string {
|
||||
const infotag = Store.kinks.infotags[this.method.id];
|
||||
if(typeof infotag === 'undefined')
|
||||
return 'Unknown Infotag';
|
||||
return methods.contactMethodIconUrl(infotag.name);
|
||||
return methods.contactMethodIconUrl(this.infotag.name);
|
||||
}
|
||||
|
||||
get value(): string {
|
||||
return formatContactValue(this.method.id, this.method.value);
|
||||
}
|
||||
|
||||
get altText(): string {
|
||||
const infotag = Store.kinks.infotags[this.method.id];
|
||||
if(typeof infotag === 'undefined')
|
||||
return '';
|
||||
return infotag.name;
|
||||
return formatContactValue(this.infotag, this.data.string!);
|
||||
}
|
||||
|
||||
get contactLink(): string | undefined {
|
||||
return formatContactLink(this.method.id, this.method.value);
|
||||
return formatContactLink(this.infotag, this.data.string!);
|
||||
}
|
||||
}
|
||||
</script>
|
|
@ -1,5 +1,5 @@
|
|||
import {urlRegex as websitePattern} from '../../bbcode/core';
|
||||
import {Store} from './data_store';
|
||||
import {Infotag} from '../../interfaces';
|
||||
|
||||
const daUsernamePattern = /^([a-z0-9_\-]+)$/i;
|
||||
const daSitePattern = /^https?:\/\/([a-z0-9_\-]+)\.deviantart\.com\//i;
|
||||
|
@ -30,10 +30,7 @@ function normalizeSiteUsernamePair(site: RegExp, username: RegExp): (value: stri
|
|||
};
|
||||
}
|
||||
|
||||
export function formatContactValue(id: number, value: string): string {
|
||||
const infotag = Store.kinks.infotags[id];
|
||||
if(typeof infotag === 'undefined')
|
||||
return value;
|
||||
export function formatContactValue(infotag: Infotag, value: string): string {
|
||||
const methodName = infotag.name.toLowerCase();
|
||||
const formatters: {[key: string]: (() => string | undefined) | undefined} = {
|
||||
deviantart(): string | undefined {
|
||||
|
@ -56,10 +53,7 @@ export function formatContactValue(id: number, value: string): string {
|
|||
return value;
|
||||
}
|
||||
|
||||
export function formatContactLink(id: number, value: string): string | undefined {
|
||||
const infotag = Store.kinks.infotags[id];
|
||||
if(typeof infotag === 'undefined')
|
||||
return;
|
||||
export function formatContactLink(infotag: Infotag, value: string): string | undefined {
|
||||
const methodName = infotag.name.toLowerCase();
|
||||
const formatters: {[key: string]: (() => string | undefined) | undefined} = {
|
||||
deviantart(): string | undefined {
|
||||
|
|
|
@ -38,7 +38,7 @@
|
|||
name = '';
|
||||
description = '';
|
||||
choice: KinkChoice = 'favorite';
|
||||
target = Utils.Settings.defaultCharacter;
|
||||
target = Utils.settings.defaultCharacter;
|
||||
formErrors = {};
|
||||
submitting = false;
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@ import {Component} from 'vue';
|
|||
import {SharedStore, StoreMethods} from './interfaces';
|
||||
|
||||
export let Store: SharedStore = {
|
||||
kinks: <any>undefined, //tslint:disable-line:no-any
|
||||
shared: undefined!,
|
||||
authenticated: false
|
||||
};
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<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>
|
||||
|
|
|
@ -46,8 +46,8 @@
|
|||
async checkName(): Promise<boolean> {
|
||||
try {
|
||||
this.checking = true;
|
||||
const result = await methods.characterNameCheck(this.newName);
|
||||
this.valid = result.valid;
|
||||
await methods.characterNameCheck(this.newName);
|
||||
this.valid = true;
|
||||
this.errors = {};
|
||||
return true;
|
||||
} catch(e) {
|
||||
|
@ -64,9 +64,8 @@
|
|||
async duplicate(): Promise<void> {
|
||||
try {
|
||||
this.duplicating = true;
|
||||
const result = await methods.characterDuplicate(this.character.character.id, this.newName);
|
||||
await methods.characterDuplicate(this.character.character.id, this.newName);
|
||||
this.hide();
|
||||
window.location.assign(result.next);
|
||||
} catch(e) {
|
||||
Utils.ajaxError(e, 'Unable to duplicate character');
|
||||
this.valid = false;
|
||||
|
|
|
@ -93,7 +93,7 @@
|
|||
@Prop({required: true})
|
||||
readonly character!: Character;
|
||||
|
||||
ourCharacter = Utils.Settings.defaultCharacter;
|
||||
ourCharacter = Utils.settings.defaultCharacter;
|
||||
|
||||
incoming: FriendRequest[] = [];
|
||||
pending: FriendRequest[] = [];
|
||||
|
@ -113,7 +113,12 @@
|
|||
try {
|
||||
this.requesting = true;
|
||||
const newRequest = await methods.friendRequest(this.character.character.id, this.ourCharacter);
|
||||
this.pending.push(newRequest);
|
||||
if(typeof newRequest === 'number')
|
||||
this.pending.push({
|
||||
id: newRequest, source: Utils.characters.find((x) => x.id === this.ourCharacter)!, target: this.character.character,
|
||||
createdAt: Date.now() / 1000
|
||||
});
|
||||
else this.existing.push(newRequest);
|
||||
} catch(e) {
|
||||
if(Utils.isJSONError(e))
|
||||
this.error = <string>e.response.data.error;
|
||||
|
|
|
@ -15,14 +15,15 @@
|
|||
import Vue from 'vue';
|
||||
import * as Utils from '../utils';
|
||||
import {methods} from './data_store';
|
||||
import {Character, CharacterFriend} from './interfaces';
|
||||
import {Character} from './interfaces';
|
||||
import {SimpleCharacter} from '../../interfaces';
|
||||
|
||||
@Component
|
||||
export default class FriendsView extends Vue {
|
||||
@Prop({required: true})
|
||||
private readonly character!: Character;
|
||||
private shown = false;
|
||||
friends: CharacterFriend[] = [];
|
||||
friends: SimpleCharacter[] = [];
|
||||
loading = true;
|
||||
error = '';
|
||||
|
||||
|
|
|
@ -5,11 +5,12 @@
|
|||
<label v-show="canEdit" class="control-label">Unapproved only:
|
||||
<input type="checkbox" v-model="unapprovedOnly"/>
|
||||
</label>
|
||||
<simple-pager :next="hasNextPage" :prev="page > 1" @next="nextPage" @prev="previousPage"></simple-pager>
|
||||
<simple-pager :next="hasNextPage" :prev="page > 1" @next="++page" @prev="--page"></simple-pager>
|
||||
</div>
|
||||
<template v-if="!loading">
|
||||
<div class="alert alert-info" v-show="posts.length === 0">No guestbook posts.</div>
|
||||
<guestbook-post :post="post" :can-edit="canEdit" v-for="post in posts" :key="post.id" @reload="getPage"></guestbook-post>
|
||||
<guestbook-post :character="character" :post="post" :can-edit="canEdit" v-for="post in posts" :key="post.id"
|
||||
@reload="getPage"></guestbook-post>
|
||||
<div v-if="authenticated && !oldApi" class="form-horizontal">
|
||||
<bbcode-editor v-model="newPost.message" :maxlength="5000" classes="form-control"></bbcode-editor>
|
||||
<input type="checkbox" id="guestbookPostPrivate" v-model="newPost.privatePost"/>
|
||||
|
@ -20,7 +21,7 @@
|
|||
</div>
|
||||
</template>
|
||||
<div class="guestbook-controls">
|
||||
<simple-pager :next="hasNextPage" :prev="page > 1" @next="nextPage" @prev="previousPage"></simple-pager>
|
||||
<simple-pager :next="hasNextPage" :prev="page > 1" @next="++page" @prev="--page"></simple-pager>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
@ -40,14 +41,12 @@
|
|||
export default class GuestbookView extends Vue {
|
||||
@Prop({required: true})
|
||||
readonly character!: Character;
|
||||
@Prop()
|
||||
@Prop
|
||||
readonly oldApi?: true;
|
||||
loading = true;
|
||||
error = '';
|
||||
authenticated = Store.authenticated;
|
||||
|
||||
posts: GuestbookPost[] = [];
|
||||
|
||||
unapprovedOnly = false;
|
||||
page = 1;
|
||||
hasNextPage = false;
|
||||
|
@ -55,28 +54,18 @@
|
|||
newPost = {
|
||||
posting: false,
|
||||
privatePost: false,
|
||||
character: Utils.Settings.defaultCharacter,
|
||||
character: Utils.settings.defaultCharacter,
|
||||
message: ''
|
||||
};
|
||||
|
||||
async nextPage(): Promise<void> {
|
||||
this.page += 1;
|
||||
return this.getPage();
|
||||
}
|
||||
|
||||
async previousPage(): Promise<void> {
|
||||
this.page -= 1;
|
||||
return this.getPage();
|
||||
}
|
||||
|
||||
@Watch('unapprovedOnly')
|
||||
@Watch('page')
|
||||
async getPage(): Promise<void> {
|
||||
try {
|
||||
this.loading = true;
|
||||
const guestbookState = await methods.guestbookPageGet(this.character.character.id, this.page, this.unapprovedOnly);
|
||||
this.posts = guestbookState.posts;
|
||||
this.hasNextPage = guestbookState.nextPage;
|
||||
this.canEdit = guestbookState.canEdit;
|
||||
const guestbook = await methods.guestbookPageGet(this.character.character.id, (this.page - 1) * 10, 10, this.unapprovedOnly);
|
||||
this.posts = guestbook.posts;
|
||||
this.hasNextPage = (this.page + 1) * 10 < guestbook.total;
|
||||
} catch(e) {
|
||||
this.posts = [];
|
||||
this.hasNextPage = false;
|
||||
|
|
|
@ -19,16 +19,15 @@
|
|||
{{ (post.approved) ? 'Unapprove' : 'Approve' }}
|
||||
</button>
|
||||
</div>
|
||||
<button class="btn btn-danger" v-show="!post.deleted && (canEdit || post.canEdit)"
|
||||
@click="deletePost" :disabled="deleting">Delete
|
||||
</button>
|
||||
<!-- TODO proper permission handling -->
|
||||
<button class="btn btn-danger" v-show="!post.deleted && canEdit" @click="deletePost" :disabled="deleting">Delete</button>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<div class="bbcode guestbook-message" v-bbcode="post.message"></div>
|
||||
<bbcode class="bbcode guestbook-message" :text="post.message"></bbcode>
|
||||
<div v-if="post.reply && !replyBox" class="guestbook-reply">
|
||||
<date-display v-if="post.repliedAt" :time="post.repliedAt"></date-display>
|
||||
<div class="reply-message" v-bbcode="post.reply"></div>
|
||||
<bbcode class="reply-message" :text="post.reply"></bbcode>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -54,12 +53,14 @@
|
|||
import DateDisplay from '../../components/date_display.vue';
|
||||
import * as Utils from '../utils';
|
||||
import {methods} from './data_store';
|
||||
import {GuestbookPost} from './interfaces';
|
||||
import {Character, GuestbookPost} from './interfaces';
|
||||
|
||||
@Component({
|
||||
components: {'date-display': DateDisplay, 'character-link': CharacterLink}
|
||||
})
|
||||
export default class GuestbookPostView extends Vue {
|
||||
@Prop({required: true})
|
||||
readonly character!: Character;
|
||||
@Prop({required: true})
|
||||
readonly post!: GuestbookPost;
|
||||
@Prop({required: true})
|
||||
|
@ -79,7 +80,7 @@
|
|||
async deletePost(): Promise<void> {
|
||||
try {
|
||||
this.deleting = true;
|
||||
await methods.guestbookPostDelete(this.post.id);
|
||||
await methods.guestbookPostDelete(this.character.character.id, this.post.id);
|
||||
Vue.set(this.post, 'deleted', true);
|
||||
this.$emit('reload');
|
||||
} catch(e) {
|
||||
|
@ -92,7 +93,7 @@
|
|||
async approve(): Promise<void> {
|
||||
try {
|
||||
this.approving = true;
|
||||
await methods.guestbookPostApprove(this.post.id, !this.post.approved);
|
||||
await methods.guestbookPostApprove(this.character.character.id, this.post.id, !this.post.approved);
|
||||
this.post.approved = !this.post.approved;
|
||||
} catch(e) {
|
||||
Utils.ajaxError(e, 'Unable to change post approval.');
|
||||
|
@ -104,9 +105,9 @@
|
|||
async postReply(): Promise<void> {
|
||||
try {
|
||||
this.replying = true;
|
||||
const replyData = await methods.guestbookPostReply(this.post.id, this.replyMessage);
|
||||
this.post.reply = replyData.reply;
|
||||
this.post.repliedAt = replyData.repliedAt;
|
||||
await methods.guestbookPostReply(this.character.character.id, this.post.id, this.replyMessage);
|
||||
this.post.reply = this.replyMessage;
|
||||
this.post.repliedAt = Date.now() / 1000;
|
||||
this.replyBox = false;
|
||||
} catch(e) {
|
||||
Utils.ajaxError(e, 'Unable to post guestbook reply.');
|
||||
|
|
|
@ -28,7 +28,7 @@
|
|||
export default class ImagesView extends Vue {
|
||||
@Prop({required: true})
|
||||
private readonly character!: Character;
|
||||
@Prop()
|
||||
@Prop
|
||||
private readonly usePreview?: boolean;
|
||||
private shown = false;
|
||||
previewImage = '';
|
||||
|
|
|
@ -1,50 +1,45 @@
|
|||
<template>
|
||||
<div class="infotag">
|
||||
<span class="infotag-label">{{label}}: </span>
|
||||
<span v-if="!contactLink" class="infotag-value">{{value}}</span>
|
||||
<span v-if="contactLink" class="infotag-value"><a :href="contactLink">{{value}}</a></span>
|
||||
<span class="infotag-label">{{infotag.name}}: </span>
|
||||
<span v-if="infotag.infotag_group !== contactGroupId" class="infotag-value">{{value}}</span>
|
||||
<span v-else class="infotag-value"><a :href="contactLink">{{contactValue}}</a></span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {Component, Prop} from '@f-list/vue-ts';
|
||||
import Vue from 'vue';
|
||||
import {CharacterInfotag, Infotag, ListItem} from '../../interfaces';
|
||||
import {formatContactLink, formatContactValue} from './contact_utils';
|
||||
import {Store} from './data_store';
|
||||
import {DisplayInfotag} from './interfaces';
|
||||
import {CONTACT_GROUP_ID} from './interfaces';
|
||||
|
||||
@Component
|
||||
export default class InfotagView extends Vue {
|
||||
@Prop({required: true})
|
||||
private readonly infotag!: DisplayInfotag;
|
||||
|
||||
get label(): string {
|
||||
const infotag = Store.kinks.infotags[this.infotag.id];
|
||||
if(typeof infotag === 'undefined')
|
||||
return 'Unknown Infotag';
|
||||
return infotag.name;
|
||||
}
|
||||
readonly infotag!: Infotag;
|
||||
@Prop({required: true})
|
||||
readonly data!: CharacterInfotag;
|
||||
readonly contactGroupId = CONTACT_GROUP_ID;
|
||||
|
||||
get contactLink(): string | undefined {
|
||||
if(this.infotag.isContact)
|
||||
return formatContactLink(this.infotag.id, this.infotag.string!);
|
||||
return formatContactLink(this.infotag, this.data.string!);
|
||||
}
|
||||
|
||||
get contactValue(): string {
|
||||
return formatContactValue(this.infotag, this.data.string!);
|
||||
}
|
||||
|
||||
get value(): string {
|
||||
const infotag = Store.kinks.infotags[this.infotag.id];
|
||||
if(typeof infotag === 'undefined')
|
||||
return '';
|
||||
if(this.infotag.isContact)
|
||||
return formatContactValue(this.infotag.id, this.infotag.string!);
|
||||
switch(infotag.type) {
|
||||
switch(this.infotag.type) {
|
||||
case 'text':
|
||||
return this.infotag.string!;
|
||||
return this.data.string!;
|
||||
case 'number':
|
||||
if(infotag.allow_legacy && this.infotag.number === null)
|
||||
return this.infotag.string !== undefined ? this.infotag.string : '';
|
||||
return this.infotag.number!.toPrecision();
|
||||
if(this.infotag.allow_legacy && !this.data.number)
|
||||
return this.data.string !== undefined ? this.data.string : '';
|
||||
return this.data.number!.toPrecision();
|
||||
}
|
||||
const listitem = Store.kinks.listitems[this.infotag.list!];
|
||||
const listitem = <ListItem | undefined>Store.shared.listItems[this.data.list!];
|
||||
if(typeof listitem === 'undefined')
|
||||
return '';
|
||||
return listitem.value;
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
<template>
|
||||
<div class="infotags row">
|
||||
<div class="infotag-group col-sm-3" v-for="group in groupedInfotags" :key="group.id" style="margin-top:5px">
|
||||
<div class="infotag-group col-sm-3" v-for="group in groups" :key="group.id" style="margin-top:5px">
|
||||
<div class="infotag-title">{{group.name}}</div>
|
||||
<hr>
|
||||
<infotag :infotag="infotag" v-for="infotag in group.infotags" :key="infotag.id"></infotag>
|
||||
<infotag v-for="infotag in getInfotags(group.id)" :key="infotag.id" :infotag="infotag"
|
||||
:data="character.character.infotags[infotag.id]"></infotag>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
@ -11,66 +12,25 @@
|
|||
<script lang="ts">
|
||||
import {Component, Prop} from '@f-list/vue-ts';
|
||||
import Vue from 'vue';
|
||||
import * as Utils from '../utils';
|
||||
import {Infotag, InfotagGroup} from '../../interfaces';
|
||||
import {Store} from './data_store';
|
||||
import {Character, CONTACT_GROUP_ID, DisplayInfotag} from './interfaces';
|
||||
|
||||
import InfotagView from './infotag.vue';
|
||||
|
||||
interface DisplayInfotagGroup {
|
||||
id: number
|
||||
name: string
|
||||
sortOrder: number
|
||||
infotags: DisplayInfotag[]
|
||||
}
|
||||
import {Character} from './interfaces';
|
||||
|
||||
@Component({
|
||||
components: {infotag: InfotagView}
|
||||
})
|
||||
export default class InfotagsView extends Vue {
|
||||
@Prop({required: true})
|
||||
private readonly character!: Character;
|
||||
readonly character!: Character;
|
||||
|
||||
get groupedInfotags(): DisplayInfotagGroup[] {
|
||||
const groups = Store.kinks.infotag_groups;
|
||||
const infotags = Store.kinks.infotags;
|
||||
const characterTags = this.character.character.infotags;
|
||||
const outputGroups: DisplayInfotagGroup[] = [];
|
||||
const groupedTags = Utils.groupObjectBy(infotags, 'infotag_group');
|
||||
for(const groupId in groups) {
|
||||
const group = groups[groupId]!;
|
||||
const groupedInfotags = groupedTags[groupId];
|
||||
if(groupedInfotags === undefined) continue;
|
||||
const collectedTags: DisplayInfotag[] = [];
|
||||
for(const infotag of groupedInfotags) {
|
||||
const characterInfotag = characterTags[infotag.id];
|
||||
if(typeof characterInfotag === 'undefined')
|
||||
continue;
|
||||
const newInfotag: DisplayInfotag = {
|
||||
id: infotag.id,
|
||||
isContact: infotag.infotag_group === CONTACT_GROUP_ID,
|
||||
string: characterInfotag.string,
|
||||
number: characterInfotag.number,
|
||||
list: characterInfotag.list
|
||||
};
|
||||
collectedTags.push(newInfotag);
|
||||
}
|
||||
collectedTags.sort((a, b): number => {
|
||||
const infotagA = infotags[a.id]!;
|
||||
const infotagB = infotags[b.id]!;
|
||||
return infotagA.name < infotagB.name ? -1 : 1;
|
||||
});
|
||||
outputGroups.push({
|
||||
id: group.id,
|
||||
name: group.name,
|
||||
sortOrder: group.sort_order,
|
||||
infotags: collectedTags
|
||||
});
|
||||
}
|
||||
get groups(): {readonly [key: string]: Readonly<InfotagGroup>} {
|
||||
return Store.shared.infotagGroups;
|
||||
}
|
||||
|
||||
outputGroups.sort((a, b) => a.sortOrder < b.sortOrder ? -1 : 1);
|
||||
|
||||
return outputGroups.filter((a) => a.infotags.length > 0);
|
||||
getInfotags(group: number): Infotag[] {
|
||||
return Object.keys(Store.shared.infotags).map((x) => Store.shared.infotags[x])
|
||||
.filter((x) => x.infotag_group === group && this.character.character.infotags[x.id] !== undefined);
|
||||
}
|
||||
}
|
||||
</script>
|
|
@ -1,33 +1,23 @@
|
|||
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
|
||||
}
|
||||
|
||||
export interface SelectItem {
|
||||
text: string
|
||||
value: string | number
|
||||
}
|
||||
import {
|
||||
Character as CharacterInfo, CharacterImage, CharacterSettings, KinkChoice, SharedDefinitions, SimpleCharacter
|
||||
} from '../../interfaces';
|
||||
|
||||
export interface SharedStore {
|
||||
kinks: SharedKinks
|
||||
shared: SharedDefinitions
|
||||
authenticated: boolean
|
||||
}
|
||||
|
||||
export interface StoreMethods {
|
||||
bookmarkUpdate(id: number, state: boolean): Promise<boolean>
|
||||
bookmarkUpdate(id: number, state: boolean): Promise<void>
|
||||
|
||||
characterBlock?(id: number, block: boolean, reason?: string): Promise<void>
|
||||
characterCustomKinkAdd(id: number, name: string, description: string, choice: KinkChoice): Promise<void>
|
||||
characterData(name: string | undefined, id: number | undefined): Promise<Character>
|
||||
characterDelete(id: number): Promise<void>
|
||||
characterDuplicate(id: number, name: string): Promise<DuplicateResult>
|
||||
characterDuplicate(id: number, name: string): Promise<void>
|
||||
characterFriends(id: number): Promise<FriendsByCharacter>
|
||||
characterNameCheck(name: string): Promise<CharacterNameCheckResult>
|
||||
characterRename?(id: number, name: string, renamedFor?: string): Promise<RenameResult>
|
||||
characterNameCheck(name: string): Promise<void>
|
||||
characterRename?(id: number, name: string, renamedFor?: string): Promise<void>
|
||||
characterReport(reportData: CharacterReportData): Promise<void>
|
||||
|
||||
contactMethodIconUrl(name: string): string
|
||||
|
@ -36,20 +26,20 @@ export interface StoreMethods {
|
|||
fieldsGet(): Promise<void>
|
||||
|
||||
friendDissolve(friend: Friend): Promise<void>
|
||||
friendRequest(target: number, source: number): Promise<FriendRequest>
|
||||
friendRequest(target: number, source: number): Promise<Friend | number>
|
||||
friendRequestAccept(request: FriendRequest): Promise<Friend>
|
||||
friendRequestIgnore(request: FriendRequest): Promise<void>
|
||||
friendRequestCancel(request: FriendRequest): Promise<void>
|
||||
|
||||
friendsGet(id: number): Promise<CharacterFriend[]>
|
||||
friendsGet(id: number): Promise<SimpleCharacter[]>
|
||||
|
||||
groupsGet(id: number): Promise<CharacterGroup[]>
|
||||
|
||||
guestbookPageGet(id: number, page: number, unapproved: boolean): Promise<GuestbookState>
|
||||
guestbookPostApprove(id: number, approval: boolean): Promise<void>
|
||||
guestbookPostDelete(id: number): Promise<void>
|
||||
guestbookPageGet(character: number, offset?: number, limit?: number, unapproved_only?: boolean): Promise<Guestbook>
|
||||
guestbookPostApprove(character: number, id: number, approval: boolean): Promise<void>
|
||||
guestbookPostDelete(character: number, id: number): Promise<void>
|
||||
guestbookPostPost(target: number, source: number, message: string, privatePost: boolean): Promise<void>
|
||||
guestbookPostReply(id: number, reply: string | null): Promise<GuestbookReply>
|
||||
guestbookPostReply(character: number, id: number, reply: string | null): Promise<void>
|
||||
|
||||
hasPermission?(permission: string): boolean
|
||||
|
||||
|
@ -59,20 +49,12 @@ export interface StoreMethods {
|
|||
|
||||
kinksGet(id: number): Promise<CharacterKink[]>
|
||||
|
||||
memoUpdate(id: number, memo: string): Promise<MemoReply>
|
||||
}
|
||||
|
||||
export interface SharedKinks {
|
||||
kinks: {[key: string]: Kink | undefined}
|
||||
kink_groups: {[key: string]: KinkGroup | undefined}
|
||||
infotags: {[key: string]: Infotag | undefined}
|
||||
infotag_groups: {[key: string]: InfotagGroup | undefined}
|
||||
listitems: {[key: string]: ListItem | undefined}
|
||||
memoUpdate(id: number, memo: string): Promise<void>
|
||||
}
|
||||
|
||||
export type SiteDate = number | string | null;
|
||||
export type KinkChoiceFull = KinkChoice | number;
|
||||
export const CONTACT_GROUP_ID = '1';
|
||||
export const CONTACT_GROUP_ID = 1;
|
||||
|
||||
export interface DisplayKink {
|
||||
id: number
|
||||
|
@ -84,40 +66,7 @@ export interface DisplayKink {
|
|||
hasSubkinks: boolean
|
||||
subkinks: DisplayKink[]
|
||||
ignore: boolean
|
||||
}
|
||||
|
||||
export interface DisplayInfotag {
|
||||
id: number
|
||||
isContact: boolean
|
||||
string?: string
|
||||
number?: number | null
|
||||
list?: number
|
||||
}
|
||||
|
||||
export interface KinkGroup {
|
||||
id: number
|
||||
name: string
|
||||
description: string
|
||||
sort_order: number
|
||||
}
|
||||
|
||||
export interface InfotagGroup {
|
||||
id: number
|
||||
name: string
|
||||
description: string
|
||||
sort_order: number
|
||||
}
|
||||
|
||||
export interface ListItem {
|
||||
id: number
|
||||
name: string
|
||||
value: string
|
||||
sort_order: number
|
||||
}
|
||||
|
||||
export interface CharacterFriend {
|
||||
id: number
|
||||
name: string
|
||||
key: string
|
||||
}
|
||||
|
||||
export interface CharacterKink {
|
||||
|
@ -125,14 +74,6 @@ export interface CharacterKink {
|
|||
choice: KinkChoice
|
||||
}
|
||||
|
||||
export type CharacterName = string | CharacterNameDetails;
|
||||
|
||||
export interface CharacterNameDetails {
|
||||
id: number
|
||||
name: string
|
||||
deleted: boolean
|
||||
}
|
||||
|
||||
export type ThreadOrderMode = 'post' | 'explicit';
|
||||
|
||||
export interface GroupPermissions {
|
||||
|
@ -151,7 +92,7 @@ export interface CharacterGroup {
|
|||
orderMode: ThreadOrderMode
|
||||
createdAt: SiteDate
|
||||
myPermissions: GroupPermissions
|
||||
character: CharacterName
|
||||
character: SimpleCharacter
|
||||
owner: boolean
|
||||
}
|
||||
|
||||
|
@ -178,7 +119,7 @@ export interface Character {
|
|||
|
||||
export interface GuestbookPost {
|
||||
readonly id: number
|
||||
readonly character: CharacterNameDetails
|
||||
readonly character: SimpleCharacter
|
||||
approved: boolean
|
||||
readonly private: boolean
|
||||
postedAt: SiteDate
|
||||
|
@ -189,33 +130,9 @@ export interface GuestbookPost {
|
|||
deleted?: boolean
|
||||
}
|
||||
|
||||
export interface GuestbookReply {
|
||||
readonly reply: string
|
||||
readonly postId: number
|
||||
readonly repliedAt: SiteDate
|
||||
}
|
||||
|
||||
export interface GuestbookState {
|
||||
posts: GuestbookPost[]
|
||||
readonly nextPage: boolean
|
||||
readonly canEdit: boolean
|
||||
}
|
||||
|
||||
export interface MemoReply {
|
||||
readonly id: number
|
||||
readonly memo: string
|
||||
readonly updated_at: SiteDate
|
||||
}
|
||||
|
||||
export interface DuplicateResult {
|
||||
// Url to redirect user to when duplication is complete.
|
||||
readonly next: string
|
||||
}
|
||||
|
||||
export type RenameResult = DuplicateResult;
|
||||
|
||||
export interface CharacterNameCheckResult {
|
||||
valid: boolean
|
||||
export interface Guestbook {
|
||||
readonly posts: GuestbookPost[]
|
||||
readonly total: number
|
||||
}
|
||||
|
||||
export interface CharacterReportData {
|
||||
|
@ -229,8 +146,8 @@ export interface CharacterReportData {
|
|||
|
||||
export interface Friend {
|
||||
id: number
|
||||
source: CharacterNameDetails
|
||||
target: CharacterNameDetails
|
||||
source: SimpleCharacter
|
||||
target: SimpleCharacter
|
||||
createdAt: SiteDate
|
||||
}
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<div class="character-kink" :class="kinkClasses" :id="kinkId" @click="toggleSubkinks" :data-custom="customId"
|
||||
<div class="character-kink" :class="kinkClasses" :id="kink.key" @click="toggleSubkinks"
|
||||
@mouseover.stop="showTooltip = true" @mouseout.stop="showTooltip = false">
|
||||
<i v-show="kink.hasSubkinks" class="fa" :class="{'fa-minus': !listClosed, 'fa-plus': listClosed}"></i>
|
||||
<i v-show="!kink.hasSubkinks && kink.isCustom" class="far fa-dot-circle custom-kink-icon"></i>
|
||||
|
@ -42,10 +42,6 @@
|
|||
this.listClosed = !this.listClosed;
|
||||
}
|
||||
|
||||
get kinkId(): number {
|
||||
return this.kink.isCustom ? -this.kink.id : this.kink.id;
|
||||
}
|
||||
|
||||
get kinkClasses(): {[key: string]: boolean} {
|
||||
const classes: {[key: string]: boolean} = {
|
||||
'stock-kink': !this.kink.isCustom,
|
||||
|
@ -53,7 +49,7 @@
|
|||
highlighted: !this.kink.isCustom && this.highlights[this.kink.id],
|
||||
subkink: this.kink.hasSubkinks
|
||||
};
|
||||
classes[`kink-id-${this.kinkId}`] = true;
|
||||
classes[`kink-id-${this.kink.key}`] = true;
|
||||
classes[`kink-group-${this.kink.group}`] = true;
|
||||
if(!this.kink.isCustom && typeof this.comparisons[this.kink.id] !== 'undefined')
|
||||
classes[`comparison-${this.comparisons[this.kink.id]}`] = true;
|
||||
|
|
|
@ -21,7 +21,7 @@
|
|||
<h4>Favorites</h4>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<kink v-for="kink in groupedKinks['favorite']" :kink="kink" :key="kink.id" :highlights="highlighting"
|
||||
<kink v-for="kink in groupedKinks['favorite']" :kink="kink" :key="kink.key" :highlights="highlighting"
|
||||
:comparisons="comparison"></kink>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -32,7 +32,7 @@
|
|||
<h4>Yes</h4>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<kink v-for="kink in groupedKinks['yes']" :kink="kink" :key="kink.id" :highlights="highlighting"
|
||||
<kink v-for="kink in groupedKinks['yes']" :kink="kink" :key="kink.key" :highlights="highlighting"
|
||||
:comparisons="comparison"></kink>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -43,7 +43,7 @@
|
|||
<h4>Maybe</h4>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<kink v-for="kink in groupedKinks['maybe']" :kink="kink" :key="kink.id" :highlights="highlighting"
|
||||
<kink v-for="kink in groupedKinks['maybe']" :kink="kink" :key="kink.key" :highlights="highlighting"
|
||||
:comparisons="comparison"></kink>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -54,7 +54,7 @@
|
|||
<h4>No</h4>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<kink v-for="kink in groupedKinks['no']" :kink="kink" :key="kink.id" :highlights="highlighting"
|
||||
<kink v-for="kink in groupedKinks['no']" :kink="kink" :key="kink.key" :highlights="highlighting"
|
||||
:comparisons="comparison"></kink>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -67,11 +67,11 @@
|
|||
<script lang="ts">
|
||||
import {Component, Prop, Watch} from '@f-list/vue-ts';
|
||||
import Vue from 'vue';
|
||||
import {Kink, KinkChoice} from '../../interfaces';
|
||||
import {Kink, KinkChoice, KinkGroup} from '../../interfaces';
|
||||
import * as Utils from '../utils';
|
||||
import CopyCustomMenu from './copy_custom_menu.vue';
|
||||
import {methods, Store} from './data_store';
|
||||
import {Character, DisplayKink, KinkGroup} from './interfaces';
|
||||
import {Character, DisplayKink} from './interfaces';
|
||||
import KinkView from './kink.vue';
|
||||
|
||||
@Component({
|
||||
|
@ -80,10 +80,10 @@
|
|||
export default class CharacterKinksView extends Vue {
|
||||
@Prop({required: true})
|
||||
readonly character!: Character;
|
||||
@Prop()
|
||||
@Prop
|
||||
readonly oldApi?: true;
|
||||
shared = Store;
|
||||
characterToCompare = Utils.Settings.defaultCharacter;
|
||||
characterToCompare = Utils.settings.defaultCharacter;
|
||||
highlightGroup: number | undefined;
|
||||
|
||||
loading = false;
|
||||
|
@ -120,8 +120,8 @@
|
|||
this.highlighting = {};
|
||||
if(group === null) return;
|
||||
const toAssign: {[key: string]: boolean} = {};
|
||||
for(const kinkId in this.shared.kinks.kinks) {
|
||||
const kink = this.shared.kinks.kinks[kinkId]!;
|
||||
for(const kinkId in Store.shared.kinks) {
|
||||
const kink = Store.shared.kinks[kinkId];
|
||||
if(kink.kink_group === group)
|
||||
toAssign[kinkId] = true;
|
||||
}
|
||||
|
@ -129,7 +129,7 @@
|
|||
}
|
||||
|
||||
get kinkGroups(): {[key: string]: KinkGroup | undefined} {
|
||||
return this.shared.kinks.kink_groups;
|
||||
return Store.shared.kinkGroups;
|
||||
}
|
||||
|
||||
get compareButtonText(): string {
|
||||
|
@ -139,7 +139,7 @@
|
|||
}
|
||||
|
||||
get groupedKinks(): {[key in KinkChoice]: DisplayKink[]} {
|
||||
const kinks = this.shared.kinks.kinks;
|
||||
const kinks = Store.shared.kinks;
|
||||
const characterKinks = this.character.character.kinks;
|
||||
const characterCustoms = this.character.character.customs;
|
||||
const displayCustoms: {[key: string]: DisplayKink | undefined} = {};
|
||||
|
@ -152,7 +152,8 @@
|
|||
isCustom: false,
|
||||
hasSubkinks: false,
|
||||
ignore: false,
|
||||
subkinks: []
|
||||
subkinks: [],
|
||||
key: kink.id.toString()
|
||||
});
|
||||
const kinkSorter = (a: DisplayKink, b: DisplayKink) => {
|
||||
if(this.character.settings.customs_first && a.isCustom !== b.isCustom)
|
||||
|
@ -174,13 +175,14 @@
|
|||
isCustom: true,
|
||||
hasSubkinks: false,
|
||||
ignore: false,
|
||||
subkinks: []
|
||||
subkinks: [],
|
||||
key: `c${custom.id}`
|
||||
};
|
||||
}
|
||||
|
||||
for(const kinkId in characterKinks) {
|
||||
const kinkChoice = characterKinks[kinkId]!;
|
||||
const kink = kinks[kinkId];
|
||||
const kink = <Kink | undefined>kinks[kinkId];
|
||||
if(kink === undefined) continue;
|
||||
const newKink = makeKink(kink);
|
||||
if(typeof kinkChoice === 'number' && typeof displayCustoms[kinkChoice] !== 'undefined') {
|
||||
|
|
|
@ -33,7 +33,7 @@
|
|||
export default class MemoDialog extends CustomDialog {
|
||||
@Prop({required: true})
|
||||
readonly character!: {id: number, name: string};
|
||||
@Prop()
|
||||
@Prop
|
||||
readonly memo?: Memo;
|
||||
message = '';
|
||||
editing = false;
|
||||
|
|
|
@ -2,12 +2,27 @@
|
|||
<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>
|
||||
<select v-model="type" class="form-control">
|
||||
<option>None</option>
|
||||
<option value="profile">Profile Violation</option>
|
||||
<option value="name_request">Name Request</option>
|
||||
<option value="takedown">Art Takedown</option>
|
||||
<option value="other">Other</option>
|
||||
</select>
|
||||
</div>
|
||||
<div v-if="type !== 'takedown'">
|
||||
<div class="form-group" v-if="type === 'profile'">
|
||||
<label>Violation Type</label>
|
||||
<select v-select="violationTypes" v-model="violation" class="form-control"></select>
|
||||
<select v-model="violation" class="form-control">
|
||||
<option>Real life images on underage character</option>
|
||||
<option>Real life animal images on sexual character</option>
|
||||
<option>Amateur/farmed real life images</option>
|
||||
<option>Defamation</option>
|
||||
<option>OOC Kinks</option>
|
||||
<option>Real life contact information</option>
|
||||
<option>Solicitation for real life contact</option>
|
||||
<option>Other</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Your Character</label>
|
||||
|
@ -30,7 +45,7 @@
|
|||
import Modal from '../../components/Modal.vue';
|
||||
import * as Utils from '../utils';
|
||||
import {methods} from './data_store';
|
||||
import {Character, SelectItem} from './interfaces';
|
||||
import {Character} from './interfaces';
|
||||
|
||||
@Component({
|
||||
components: {modal: Modal}
|
||||
|
@ -38,34 +53,13 @@
|
|||
export default class ReportDialog extends CustomDialog {
|
||||
@Prop({required: true})
|
||||
readonly character!: Character;
|
||||
|
||||
ourCharacter = Utils.Settings.defaultCharacter;
|
||||
ourCharacter = Utils.settings.defaultCharacter;
|
||||
type = '';
|
||||
violation = '';
|
||||
message = '';
|
||||
|
||||
submitting = false;
|
||||
|
||||
ticketUrl = `${Utils.siteDomain}tickets/new`;
|
||||
|
||||
validTypes: ReadonlyArray<SelectItem> = [
|
||||
{text: 'None', value: ''},
|
||||
{text: 'Profile Violation', value: 'profile'},
|
||||
{text: 'Name Request', value: 'name_request'},
|
||||
{text: 'Art Takedown', value: 'takedown'},
|
||||
{text: 'Other', value: 'other'}
|
||||
];
|
||||
violationTypes: ReadonlyArray<string> = [
|
||||
'Real life images on underage character',
|
||||
'Real life animal images on sexual character',
|
||||
'Amateur/farmed real life images',
|
||||
'Defamation',
|
||||
'OOC Kinks',
|
||||
'Real life contact information',
|
||||
'Solicitation for real life contact',
|
||||
'Other'
|
||||
];
|
||||
|
||||
get name(): string {
|
||||
return this.character.character.name;
|
||||
}
|
||||
|
|
|
@ -16,8 +16,8 @@
|
|||
</template>
|
||||
<template v-else>
|
||||
<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">
|
||||
<a @click.prevent="toggleBookmark()" href="#" class="btn"
|
||||
:class="{bookmarked: character.bookmarked, unbookmarked: !character.bookmarked}">
|
||||
<i class="fa fa-fw" :class="{'fa-minus': character.bookmarked, 'fa-plus': !character.bookmarked}"></i>Bookmark
|
||||
</a>
|
||||
<span v-if="character.settings.block_bookmarks" class="prevents-bookmarks">!</span>
|
||||
|
@ -39,11 +39,13 @@
|
|||
<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>
|
||||
<contact-method v-for="method in contactMethods" :infotag="method" :key="method.id"
|
||||
:data="character.character.infotags[method.id]"></contact-method>
|
||||
</div>
|
||||
|
||||
<div class="quick-info-block">
|
||||
<infotag-item v-for="infotag in quickInfoItems" :infotag="infotag" :key="infotag.id"></infotag-item>
|
||||
<infotag-item v-for="id in quickInfoIds" v-if="character.character.infotags[id]" :infotag="getInfotag(id)"
|
||||
:data="character.character.infotags[id]" :key="id"></infotag-item>
|
||||
<div class="quick-info">
|
||||
<span class="quick-info-label">Created: </span>
|
||||
<span class="quick-info-value"><date :time="character.character.created_at"></date></span>
|
||||
|
@ -99,7 +101,7 @@
|
|||
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 {Character, CONTACT_GROUP_ID} from './interfaces';
|
||||
import MemoDialog from './memo_dialog.vue';
|
||||
import ReportDialog from './report_dialog.vue';
|
||||
|
||||
|
@ -139,9 +141,8 @@
|
|||
export default class Sidebar extends Vue {
|
||||
@Prop({required: true})
|
||||
readonly character!: Character;
|
||||
@Prop()
|
||||
@Prop
|
||||
readonly oldApi?: true;
|
||||
readonly shared: SharedStore = Store;
|
||||
readonly quickInfoIds: ReadonlyArray<number> = [1, 3, 2, 49, 9, 29, 15, 41, 25]; // Do not sort these.
|
||||
readonly avatarUrl = Utils.avatarURL;
|
||||
|
||||
|
@ -209,20 +210,16 @@
|
|||
}
|
||||
|
||||
async toggleBookmark(): Promise<void> {
|
||||
const previousState = this.character.bookmarked;
|
||||
try {
|
||||
const state = !this.character.bookmarked;
|
||||
this.$emit('bookmarked', state);
|
||||
const actualState = await methods.bookmarkUpdate(this.character.character.id, state);
|
||||
this.$emit('bookmarked', actualState);
|
||||
await methods.bookmarkUpdate(this.character.character.id, !this.character.bookmarked);
|
||||
this.character.bookmarked = !this.character.bookmarked;
|
||||
} catch(e) {
|
||||
this.$emit('bookmarked', previousState);
|
||||
Utils.ajaxError(e, 'Unable to change bookmark state.');
|
||||
}
|
||||
}
|
||||
|
||||
get editUrl(): string {
|
||||
return `${Utils.siteDomain}character/${this.character.character.id}/`;
|
||||
return `${Utils.siteDomain}character/${this.character.character.id}/edit`;
|
||||
}
|
||||
|
||||
get noteUrl(): string {
|
||||
|
@ -230,33 +227,13 @@
|
|||
}
|
||||
|
||||
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 = [];
|
||||
for(const infotag of contactInfotags[CONTACT_GROUP_ID]!) {
|
||||
const charTag = this.character.character.infotags[infotag.id];
|
||||
if(charTag === undefined) continue;
|
||||
contactMethods.push({
|
||||
id: infotag.id,
|
||||
value: charTag.string
|
||||
});
|
||||
}
|
||||
return contactMethods;
|
||||
return Object.keys(Store.shared.infotags).map((x) => Store.shared.infotags[x])
|
||||
.filter((x) => x.infotag_group === CONTACT_GROUP_ID && this.character.character.infotags[x.id] !== undefined)
|
||||
.sort((a, b) => a.name < b.name ? -1 : 1);
|
||||
}
|
||||
|
||||
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];
|
||||
if(infotag === undefined) continue;
|
||||
quickItems.push({
|
||||
id,
|
||||
string: infotag.string,
|
||||
list: infotag.list,
|
||||
number: infotag.number
|
||||
});
|
||||
}
|
||||
return quickItems;
|
||||
getInfotag(id: number): Infotag {
|
||||
return Store.shared.infotags[id];
|
||||
}
|
||||
|
||||
get authenticated(): boolean {
|
||||
|
|
|
@ -1,60 +0,0 @@
|
|||
import Vue, {VNodeDirective} from 'vue';
|
||||
//tslint:disable:strict-boolean-expressions
|
||||
type Option = { value: string | null, disabled: boolean, text: string, label: string, options: Option[]} | string | number;
|
||||
|
||||
function rebuild(e: HTMLElement, binding: VNodeDirective): void {
|
||||
const el = <HTMLSelectElement>e;
|
||||
if(binding.oldValue === binding.value) return;
|
||||
if(!binding.value) console.error('Must provide a value');
|
||||
const value = <Option[]>binding.value;
|
||||
|
||||
function _isObject(val: any): val is object { //tslint:disable-line:no-any
|
||||
return val !== null && typeof val === 'object';
|
||||
}
|
||||
|
||||
function clearOptions(): void {
|
||||
let i = el.options.length;
|
||||
while(i--) {
|
||||
const opt = el.options[i];
|
||||
const parent = opt.parentNode!;
|
||||
if(parent === el) parent.removeChild(opt);
|
||||
else {
|
||||
el.removeChild(parent);
|
||||
i = el.options.length;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function buildOptions(parent: HTMLElement, options: Option[]): void {
|
||||
let newEl: (HTMLOptionElement & {'_value'?: string | null});
|
||||
for(let i = 0, l = options.length; i < l; i++) {
|
||||
const op = options[i];
|
||||
if(!_isObject(op) || !op.options) {
|
||||
newEl = document.createElement('option');
|
||||
if(typeof op === 'string' || typeof op === 'number')
|
||||
newEl.text = newEl.value = op as string;
|
||||
else {
|
||||
if(op.value !== null && !_isObject(op.value))
|
||||
newEl.value = op.value;
|
||||
newEl['_value'] = op.value;
|
||||
newEl.text = op.text || '';
|
||||
if(op.disabled)
|
||||
newEl.disabled = true;
|
||||
}
|
||||
} else {
|
||||
newEl = <any>document.createElement('optgroup'); //tslint:disable-line:no-any
|
||||
newEl.label = op.label;
|
||||
buildOptions(newEl, op.options);
|
||||
}
|
||||
parent.appendChild(newEl);
|
||||
}
|
||||
}
|
||||
|
||||
clearOptions();
|
||||
buildOptions(el, value);
|
||||
}
|
||||
|
||||
export default Vue.directive('select', {
|
||||
inserted: rebuild,
|
||||
update: rebuild
|
||||
});
|
|
@ -1,18 +1,14 @@
|
|||
import Axios, {AxiosError, AxiosResponse} from 'axios';
|
||||
import {InlineDisplayMode} from '../bbcode/interfaces';
|
||||
import {InlineDisplayMode, Settings, SimpleCharacter} from '../interfaces';
|
||||
|
||||
interface Dictionary<T> {
|
||||
[key: string]: T | undefined;
|
||||
}
|
||||
type FlashMessageType = 'info' | 'success' | 'warning' | 'danger';
|
||||
type FlashMessageImpl = (type: FlashMessageType, message: string) => void;
|
||||
|
||||
type flashMessageType = 'info' | 'success' | 'warning' | 'danger';
|
||||
type flashMessageImpl = (type: flashMessageType, message: string) => void;
|
||||
|
||||
let flashImpl: flashMessageImpl = (type: flashMessageType, message: string) => {
|
||||
let flashImpl: FlashMessageImpl = (type: FlashMessageType, message: string) => {
|
||||
console.log(`${type}: ${message}`);
|
||||
};
|
||||
|
||||
export function setFlashMessageImplementation(impl: flashMessageImpl): void {
|
||||
export function setFlashMessageImplementation(impl: FlashMessageImpl): void {
|
||||
flashImpl = impl;
|
||||
}
|
||||
|
||||
|
@ -28,32 +24,6 @@ export function characterURL(name: string): string {
|
|||
return `${siteDomain}c/${name}`;
|
||||
}
|
||||
|
||||
export function groupObjectBy<K extends string, T extends {[k in K]: string}>(obj: Dictionary<T>, key: K): Dictionary<T[]> {
|
||||
const newObject: Dictionary<T[]> = {};
|
||||
for(const objkey in obj) {
|
||||
if(!(objkey in obj)) continue;
|
||||
const realItem = <T>obj[objkey];
|
||||
const newKey = realItem[key];
|
||||
if(newObject[<string>newKey] === undefined) newObject[newKey] = [];
|
||||
newObject[<string>newKey]!.push(realItem);
|
||||
}
|
||||
return newObject;
|
||||
}
|
||||
|
||||
export function groupArrayBy<K extends string, T extends {[k in K]: string}>(arr: T[], key: K): Dictionary<T[]> {
|
||||
const newObject: Dictionary<T[]> = {};
|
||||
arr.map((item) => {
|
||||
const newKey = item[key];
|
||||
if(newObject[<string>newKey] === undefined) newObject[newKey] = [];
|
||||
newObject[<string>newKey]!.push(item);
|
||||
});
|
||||
return newObject;
|
||||
}
|
||||
|
||||
export function filterOut<K extends string, V, T extends {[key in K]: V}>(arr: ReadonlyArray<T>, field: K, value: V): T[] {
|
||||
return arr.filter((item) => item[field] !== value);
|
||||
}
|
||||
|
||||
//tslint:disable-next-line:no-any
|
||||
export function isJSONError(error: any): error is Error & {response: AxiosResponse<{[key: string]: object | string | number}>} {
|
||||
return (<AxiosError>error).response !== undefined && typeof (<AxiosError>error).response!.data === 'object';
|
||||
|
@ -75,6 +45,7 @@ export function ajaxError(error: any, prefix: string, showFlashMessage: boolean
|
|||
message = (<Error & {response?: AxiosResponse}>error).response !== undefined ?
|
||||
(<Error & {response: AxiosResponse}>error).response.statusText : error.name;
|
||||
} else message = <string>error;
|
||||
console.error(error);
|
||||
if(showFlashMessage) flashError(`[ERROR] ${prefix}: ${message}`);
|
||||
}
|
||||
|
||||
|
@ -86,35 +57,28 @@ export function flashSuccess(message: string): void {
|
|||
flashMessage('success', message);
|
||||
}
|
||||
|
||||
export function flashMessage(type: flashMessageType, message: string): void {
|
||||
export function flashMessage(type: FlashMessageType, message: string): void {
|
||||
flashImpl(type, message);
|
||||
}
|
||||
|
||||
export let siteDomain = '';
|
||||
export let staticDomain = '';
|
||||
|
||||
interface Settings {
|
||||
animatedIcons: boolean
|
||||
inlineDisplayMode: InlineDisplayMode
|
||||
defaultCharacter: number
|
||||
fuzzyDates: boolean
|
||||
}
|
||||
|
||||
export let Settings: Settings = {
|
||||
animatedIcons: false,
|
||||
export let settings: Settings = {
|
||||
animateEicons: true,
|
||||
inlineDisplayMode: InlineDisplayMode.DISPLAY_ALL,
|
||||
defaultCharacter: -1,
|
||||
fuzzyDates: true
|
||||
};
|
||||
|
||||
export let characters: SimpleCharacter[] = [];
|
||||
|
||||
export function setDomains(site: string, stat: string): void {
|
||||
siteDomain = site;
|
||||
staticDomain = stat;
|
||||
}
|
||||
|
||||
export function copySettings(settings: Settings): void {
|
||||
Settings.animatedIcons = settings.animatedIcons;
|
||||
Settings.inlineDisplayMode = settings.inlineDisplayMode;
|
||||
Settings.defaultCharacter = settings.defaultCharacter;
|
||||
Settings.fuzzyDates = settings.fuzzyDates;
|
||||
export function init(s: Settings, c: SimpleCharacter[]): void {
|
||||
settings = s;
|
||||
characters = c;
|
||||
}
|
|
@ -119,6 +119,7 @@
|
|||
true,
|
||||
"allow-declarations"
|
||||
],
|
||||
"ordered-imports": true,
|
||||
"prefer-function-over-method": [
|
||||
true,
|
||||
"allow-public"
|
||||
|
@ -138,7 +139,8 @@
|
|||
true,
|
||||
"never"
|
||||
],
|
||||
"strict-boolean-expressions": [true, "allow-boolean-or-undefined"],
|
||||
"strict-boolean-expressions": false,
|
||||
"strict-comparisons": false,
|
||||
"switch-default": false,
|
||||
"trailing-comma": [
|
||||
true,
|
||||
|
|
|
@ -36,6 +36,7 @@ import l from '../chat/localize';
|
|||
import {setupRaven} from '../chat/vue-raven';
|
||||
import Socket from '../chat/WebSocket';
|
||||
import Connection from '../fchat/connection';
|
||||
import {SimpleCharacter} from '../interfaces';
|
||||
import '../scss/fa.scss'; //tslint:disable-line:no-import-side-effect
|
||||
import {Logs, SettingsStore} from './logs';
|
||||
import Notifications from './notifications';
|
||||
|
@ -49,7 +50,7 @@ Axios.defaults.params = { __fchat: `web/${version}` };
|
|||
if(process.env.NODE_ENV === 'production')
|
||||
setupRaven('https://a9239b17b0a14f72ba85e8729b9d1612@sentry.f-list.net/2', `web-${version}`);
|
||||
|
||||
declare const chatSettings: {account: string, theme: string, characters: ReadonlyArray<string>, defaultCharacter: string | null};
|
||||
declare const chatSettings: {account: string, theme: string, characters: ReadonlyArray<SimpleCharacter>, defaultCharacter: number | null};
|
||||
|
||||
const ticketProvider = async() => {
|
||||
const data = (await Axios.post<{ticket?: string, error: string}>(
|
||||
|
@ -58,7 +59,8 @@ const ticketProvider = async() => {
|
|||
throw new Error(data.error);
|
||||
};
|
||||
|
||||
const connection = new Connection('F-Chat 3.0 (Web)', version, Socket, chatSettings.account, ticketProvider);
|
||||
const connection = new Connection('F-Chat 3.0 (Web)', version, Socket);
|
||||
connection.setCredentials(chatSettings.account, ticketProvider);
|
||||
initCore(connection, Logs, SettingsStore, Notifications);
|
||||
|
||||
window.addEventListener('beforeunload', (e) => {
|
||||
|
|
|
@ -120,7 +120,7 @@ export class Logs implements Logging {
|
|||
if(character === this.loadedCharacter) return this.loadedIndex!;
|
||||
if(character === core.connection.character) {
|
||||
this.loadedDb = this.db;
|
||||
this.loadedIndex = this.index !== undefined ? this.index : {};
|
||||
this.loadedIndex = this.index;
|
||||
} else
|
||||
try {
|
||||
this.loadedDb = await openDatabase(character);
|
||||
|
@ -130,7 +130,7 @@ export class Logs implements Logging {
|
|||
return {};
|
||||
}
|
||||
this.loadedCharacter = character;
|
||||
return this.loadedIndex;
|
||||
return this.loadedIndex!;
|
||||
}
|
||||
|
||||
async getConversations(character: string): Promise<ReadonlyArray<{key: string, name: string}>> {
|
||||
|
@ -179,7 +179,7 @@ export class SettingsStore implements Settings.Store {
|
|||
const pinned: Settings.Keys['pinned'] = {channels: [], private: []};
|
||||
pinned.channels = tabs.filter((x) => x.type === 'channel').map((x) => x.id.toLowerCase());
|
||||
pinned.private = tabs.filter((x) => x.type === 'user').map((x) => x.title);
|
||||
return pinned;
|
||||
return pinned as Settings.Keys[K];
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
|
@ -203,7 +203,7 @@ export class SettingsStore implements Settings.Store {
|
|||
settings.playSound = old.html5Audio;
|
||||
settings.joinMessages = old.joinLeaveAlerts;
|
||||
settings.clickOpensMessage = !old.leftClickOpensFlist;
|
||||
return settings;
|
||||
return settings as unknown as Settings.Keys[K];
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue