Some code was converting the customs object to an array with millions of elements, which caused iteration and ipc transport (json serialize) to hang the client for many minutes at a time. This is probably because incorrect lodash functions were used on the customs. Lodash would convert it to array and then it would get stored and turn up as array every time, corrupting storage and ensuring client denial of service. This fixes that bug by avoiding using incorrect lodash functions that convert things to arrays with millions of elements. It also retroactively fixes profiles that were stored with array customs, so the client won't crash.
401 lines
11 KiB
Vue
401 lines
11 KiB
Vue
<template>
|
|
<div class="character-preview">
|
|
<div v-if="match && character" class="row">
|
|
<div class="col-2">
|
|
<img :src="avatarUrl(character.character.name)" class="character-avatar">
|
|
</div>
|
|
|
|
<div class="col-10">
|
|
<h1 class="user-view">
|
|
<span class="character-name" :class="(statusClasses || {}).userClass">{{ character.character.name }}</span>
|
|
<span v-if="((statusClasses) && (statusClasses.matchScore === 'unicorn'))" :class="(statusClasses || {}).matchClass">Unicorn</span>
|
|
</h1>
|
|
<h3>{{ getOnlineStatus() }}</h3>
|
|
|
|
<div class="summary">
|
|
<span class="uc">
|
|
<span v-if="age" :class="byScore(TagId.Age)">{{age}}-years-old </span>
|
|
<span v-if="sexualOrientation" :class="byScore(TagId.Orientation)">{{sexualOrientation}} </span>
|
|
<span v-if="gender" :class="byScore(TagId.Gender)">{{gender}} </span>
|
|
<span v-if="species" :class="byScore(TagId.Species)">{{species}} </span>
|
|
</span>
|
|
|
|
<span v-if="furryPref" :class="byScore(TagId.FurryPreference)"><br /><span class="uc">{{furryPref}}</span></span>
|
|
<span v-if="subDomRole" :class="byScore(TagId.SubDomRole)"><br /><span class="uc">{{subDomRole}}</span></span>
|
|
</div>
|
|
|
|
<match-tags v-if="match" :match="match"></match-tags>
|
|
|
|
<!-- <div v-if="customs">-->
|
|
<!-- <span v-for="c in customs" :class="Score.getClasses(c.score)">{{c.name}}</span>-->
|
|
<!-- </div>-->
|
|
|
|
<div class="status-message" v-if="statusMessage">
|
|
<h4>Status <span v-if="latestAd && (statusMessage === latestAd.message)">& Latest Ad</span></h4>
|
|
<bbcode :text="statusMessage"></bbcode>
|
|
</div>
|
|
|
|
<div class="latest-ad-message" v-if="latestAd && (latestAd.message !== statusMessage)">
|
|
<h4>Latest Ad <span class="message-time">{{formatTime(latestAd.datePosted)}}</span></h4>
|
|
<bbcode :text="latestAd.message"></bbcode>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div v-else>
|
|
Loading...
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script lang="ts">
|
|
import { Component, Hook, Prop } from '@f-list/vue-ts';
|
|
import Vue from 'vue';
|
|
import core from '../core';
|
|
import { methods } from '../../site/character_page/data_store';
|
|
import {Character as ComplexCharacter} from '../../site/character_page/interfaces';
|
|
import { Matcher, MatchReport, Score } from '../../learn/matcher';
|
|
import { Character as CharacterStatus } from '../../fchat';
|
|
import { getStatusClasses, StatusClasses } from '../UserView.vue';
|
|
import * as _ from 'lodash';
|
|
import { AdCachedPosting } from '../../learn/ad-cache';
|
|
import {formatTime} from '../common';
|
|
import * as Utils from '../../site/utils';
|
|
import MatchTags from './MatchTags.vue';
|
|
import {
|
|
furryPreferenceMapping,
|
|
Gender, kinkMapping,
|
|
Orientation,
|
|
Species,
|
|
SubDomRole,
|
|
TagId
|
|
} from '../../learn/matcher-types';
|
|
import { BBCodeView } from '../../bbcode/view';
|
|
import { EventBus } from './event-bus';
|
|
import { Character, CustomKink } from '../../interfaces';
|
|
|
|
interface CustomKinkWithScore extends CustomKink {
|
|
score: number;
|
|
}
|
|
|
|
|
|
@Component({
|
|
components: {
|
|
'match-tags': MatchTags,
|
|
bbcode: BBCodeView(core.bbCodeParser)
|
|
}
|
|
})
|
|
export default class CharacterPreview extends Vue {
|
|
@Prop
|
|
readonly id?: number;
|
|
|
|
characterName?: string;
|
|
character?: ComplexCharacter;
|
|
match?: MatchReport;
|
|
ownCharacter?: ComplexCharacter;
|
|
onlineCharacter?: CharacterStatus;
|
|
statusClasses?: StatusClasses;
|
|
latestAd?: AdCachedPosting;
|
|
statusMessage?: string;
|
|
|
|
age?: string;
|
|
sexualOrientation?: string;
|
|
species?: string;
|
|
gender?: string;
|
|
furryPref?: string;
|
|
subDomRole?: string;
|
|
|
|
formatTime = formatTime;
|
|
readonly avatarUrl = Utils.avatarURL;
|
|
|
|
TagId = TagId;
|
|
Score = Score;
|
|
|
|
scoreWatcher: ((event: any) => void) | null = null;
|
|
customs?: CustomKinkWithScore[];
|
|
|
|
|
|
@Hook('mounted')
|
|
mounted(): void {
|
|
// tslint:disable-next-line no-unsafe-any no-any
|
|
this.scoreWatcher = (event: {character: Character, score: number}): void => {
|
|
// console.log('scoreWatcher', event);
|
|
|
|
if (
|
|
(event.character)
|
|
&& (this.characterName)
|
|
&& (event.character.name === this.characterName)
|
|
) {
|
|
this.load(this.characterName, true);
|
|
}
|
|
};
|
|
|
|
EventBus.$on(
|
|
'character-score',
|
|
this.scoreWatcher
|
|
);
|
|
}
|
|
|
|
|
|
@Hook('beforeDestroy')
|
|
beforeDestroy(): void {
|
|
if (this.scoreWatcher) {
|
|
EventBus.$off(
|
|
'character-score',
|
|
this.scoreWatcher
|
|
);
|
|
|
|
this.scoreWatcher = null;
|
|
}
|
|
}
|
|
|
|
|
|
load(characterName: string, force: boolean = false): void {
|
|
if (
|
|
(this.characterName === characterName)
|
|
&& (!force)
|
|
&& (this.match)
|
|
&& (this.character)
|
|
&& (this.ownCharacter)
|
|
&& (this.ownCharacter.character.name === core.characters.ownProfile.character.name)
|
|
) {
|
|
this.updateOnlineStatus();
|
|
this.updateAdStatus();
|
|
return;
|
|
}
|
|
|
|
this.characterName = characterName;
|
|
|
|
this.match = undefined;
|
|
this.character = undefined;
|
|
this.customs = undefined;
|
|
this.ownCharacter = core.characters.ownProfile;
|
|
|
|
this.updateOnlineStatus();
|
|
this.updateAdStatus();
|
|
|
|
setTimeout(async () => {
|
|
this.character = await this.getCharacterData(characterName);
|
|
this.match = Matcher.identifyBestMatchReport(this.ownCharacter!.character, this.character!.character);
|
|
|
|
this.updateCustoms();
|
|
this.updateDetails();
|
|
}, 0);
|
|
}
|
|
|
|
updateOnlineStatus(): void {
|
|
this.onlineCharacter = core.characters.get(this.characterName!);
|
|
|
|
if (!this.onlineCharacter) {
|
|
this.statusClasses = undefined;
|
|
return;
|
|
}
|
|
|
|
this.statusMessage = this.onlineCharacter.statusText;
|
|
this.statusClasses = getStatusClasses(this.onlineCharacter, undefined, true, false, true);
|
|
}
|
|
|
|
updateAdStatus(): void {
|
|
const cache = core.cache.adCache.get(this.characterName!);
|
|
|
|
if (
|
|
(!cache)
|
|
|| (cache.posts.length === 0)
|
|
|| (Date.now() - cache.posts[cache.posts.length - 1].datePosted.getTime() > (45 * 60 * 1000))
|
|
) {
|
|
this.latestAd = undefined;
|
|
return;
|
|
}
|
|
|
|
this.latestAd = cache.posts[cache.posts.length - 1];
|
|
}
|
|
|
|
|
|
updateCustoms(): void {
|
|
this.customs = _.orderBy(
|
|
_.map(
|
|
_.reject(Object.values(this.character!.character.customs ?? []), (c) => _.isUndefined(c)) as CustomKink[],
|
|
(c: CustomKink) => _.assign(
|
|
{},
|
|
c,
|
|
{
|
|
score: kinkMapping[c.choice],
|
|
name: c.name.trim().replace(/^\W+/, '').replace(/\W+$/, '')
|
|
}
|
|
)
|
|
),
|
|
['score', 'name'],
|
|
['desc', 'asc']
|
|
);
|
|
}
|
|
|
|
|
|
updateDetails(): void {
|
|
if (!this.match) {
|
|
this.age = undefined;
|
|
this.species = undefined;
|
|
this.gender = undefined;
|
|
this.furryPref = undefined;
|
|
this.subDomRole = undefined;
|
|
this.sexualOrientation = undefined;
|
|
return;
|
|
}
|
|
|
|
const a = this.match.them.yourAnalysis;
|
|
const c = this.match.them.you;
|
|
|
|
const rawSpecies = Matcher.getTagValue(TagId.Species, c);
|
|
const rawAge = Matcher.getTagValue(TagId.Age, c);
|
|
|
|
// if ((a.species) && (!Species[a.species])) {
|
|
// console.log('SPECIES', a.species, rawSpecies);
|
|
// }
|
|
|
|
if ((a.orientation) && (!Orientation[a.orientation])) {
|
|
console.error('Missing Orientation', a.orientation, c.name);
|
|
}
|
|
|
|
this.age = a.age ? this.readable(`${a.age}`) : (rawAge && /[0-9]/.test(rawAge.string || '') && rawAge.string) || undefined;
|
|
this.species = a.species ? this.readable(Species[a.species]) : (rawSpecies && rawSpecies.string) || undefined;
|
|
this.gender = (a.gender && a.gender !== Gender.None) ? this.readable(Gender[a.gender]) : undefined;
|
|
this.furryPref = a.furryPreference ? this.readable(furryPreferenceMapping[a.furryPreference]) : undefined;
|
|
this.subDomRole = a.subDomRole ? this.readable(SubDomRole[a.subDomRole]) : undefined;
|
|
this.sexualOrientation = a.orientation ? this.readable(Orientation[a.orientation]) : undefined;
|
|
}
|
|
|
|
readable(s: string): string {
|
|
return s.replace(/([A-Z])/g, ' $1').trim().toLowerCase()
|
|
.replace(/(always|usually) (submissive|dominant)/, '$2')
|
|
.replace(/bi (fe)?male preference/, 'bisexual');
|
|
}
|
|
|
|
byScore(_tagId: any): string {
|
|
return '';
|
|
|
|
// too much
|
|
// if (!this.match) {
|
|
// return '';
|
|
// }
|
|
//
|
|
// const score = this.match.merged[tagId];
|
|
//
|
|
// if (!score) {
|
|
// return '';
|
|
// }
|
|
//
|
|
// return score.getRecommendedClass();
|
|
}
|
|
|
|
|
|
getOnlineStatus(): string {
|
|
if (!this.onlineCharacter) {
|
|
return 'Offline';
|
|
}
|
|
|
|
const s = this.onlineCharacter.status as string;
|
|
|
|
return `${s.substr(0, 1).toUpperCase()}${s.substr(1)}`;
|
|
}
|
|
|
|
|
|
async getCharacterData(characterName: string): Promise<ComplexCharacter> {
|
|
const cache = await core.cache.profileCache.get(characterName);
|
|
|
|
if (cache) {
|
|
return cache.character;
|
|
}
|
|
|
|
return methods.characterData(characterName, this.id, false);
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<style lang="scss">
|
|
.character-preview {
|
|
padding: 10px;
|
|
padding-right: 15px;
|
|
background-color: var(--input-bg);
|
|
max-height: 100%;
|
|
overflow: hidden;
|
|
opacity: 0.95;
|
|
border-radius: 0 5px 5px 5px;
|
|
border: 1px solid var(--secondary);
|
|
|
|
.unicorn {
|
|
margin-left: 8px;
|
|
}
|
|
|
|
.summary {
|
|
font-size: 125%;
|
|
|
|
.uc {
|
|
display: inline-block;
|
|
|
|
&::first-letter {
|
|
text-transform: capitalize;
|
|
}
|
|
}
|
|
|
|
.match {
|
|
background-color: var(--scoreMatchBg);
|
|
border: solid 1px var(--scoreMatchFg);
|
|
}
|
|
|
|
.weak-match {
|
|
background-color: var(--scoreWeakMatchBg);
|
|
border: 1px solid var(--scoreWeakMatchFg);
|
|
}
|
|
|
|
.weak-mismatch {
|
|
background-color: var(--scoreWeakMismatchBg);
|
|
border: 1px solid var(--scoreWeakMismatchFg);
|
|
}
|
|
|
|
.mismatch {
|
|
background-color: var(--scoreMismatchBg);
|
|
border: 1px solid var(--scoreMismatchFg);
|
|
}
|
|
}
|
|
|
|
.matched-tags {
|
|
margin-top: 1rem;
|
|
}
|
|
|
|
h1 {
|
|
line-height: 100%;
|
|
margin-bottom: 0;
|
|
font-size: 2em;
|
|
}
|
|
|
|
h3 {
|
|
font-size: 1.1rem;
|
|
color: var(--dark);
|
|
}
|
|
|
|
h4 {
|
|
font-size: 1.25rem;
|
|
margin-bottom: 0;
|
|
|
|
.message-time {
|
|
font-size: 80%;
|
|
font-weight: normal;
|
|
color: var(--messageTimeFgColor);
|
|
margin-left: 2px;
|
|
}
|
|
}
|
|
|
|
.status-message,
|
|
.latest-ad-message {
|
|
display: block;
|
|
background-color: rgba(0,0,0,0.2);
|
|
padding: 10px;
|
|
border-radius: 5px;
|
|
margin-top: 1.3rem;
|
|
}
|
|
|
|
.character-avatar {
|
|
width: 100%;
|
|
height: auto;
|
|
}
|
|
}
|
|
</style>
|