Profile analyzer

This commit is contained in:
Mr. Stallion 2023-03-11 21:43:58 -08:00
parent 6b2d49f630
commit 21ff1d25b9
25 changed files with 556 additions and 38 deletions

View File

@ -1,5 +1,15 @@
# Changelog
## 1.21.0
* Added clearer broadcast messages
* Removed extra arrow from gallery view (credit: [@FatCatClient](https://github.com/FatCatClient))
* Added profile analyser to help improve profile matching
* Dictionary lookup view now has a 'open in browser' button
* Character memos are now displayed more prominently
* Fixed redgifs.com V3 image previews
* Fixed rule34video.com image previews
* 'Select all channels' for Post Ads
## 1.20.0
* Kink scoring is skipped if characters have only a few shared kinks
* Kink scoring gives more weight to 'favorite' and 'no' categories

View File

@ -4,4 +4,5 @@ This project contains contributions from:
* [Mr Stallion, esq.](https://github.com/mrstallion)
* [ButterCheezii](https://github.com/ButterCheezii)
* [FatCatClient](https://github.com/FatCatClient)
* [F-List Team](https://github.com/f-list) (original F-Chat 3.0 client)

View File

@ -1,8 +1,8 @@
# Download
[Windows](https://github.com/mrstallion/fchat-rising/releases/download/v1.20.0/F-Chat-Rising-1.20.0-win.exe) (82 MB)
| [MacOS Intel](https://github.com/mrstallion/fchat-rising/releases/download/v1.20.0/F-Chat-Rising-1.20.0-macos-intel.dmg) (82 MB)
| [MacOS M1](https://github.com/mrstallion/fchat-rising/releases/download/v1.20.0/F-Chat-Rising-1.20.0-macos-m1.dmg) (84 MB)
| [Linux](https://github.com/mrstallion/fchat-rising/releases/download/v1.20.0/F-Chat-Rising-1.20.0-linux.AppImage) (82 MB)
[Windows](https://github.com/mrstallion/fchat-rising/releases/download/v1.21.0/F-Chat-Rising-1.21.0-win.exe) (82 MB)
| [MacOS Intel](https://github.com/mrstallion/fchat-rising/releases/download/v1.21.0/F-Chat-Rising-1.21.0-macos-intel.dmg) (82 MB)
| [MacOS M1](https://github.com/mrstallion/fchat-rising/releases/download/v1.21.0/F-Chat-Rising-1.21.0-macos-m1.dmg) (84 MB)
| [Linux](https://github.com/mrstallion/fchat-rising/releases/download/v1.21.0/F-Chat-Rising-1.21.0-linux.AppImage) (82 MB)
# F-Chat Rising

View File

@ -31,6 +31,10 @@
</span>
</div>
<div><a href="#" @click.prevent="showProfileAnalyzer()" class="btn"><span class="fas fa-user-md"></span>
Profile Analyzer</a>
</div>
<div class="list-group conversation-nav">
<a :class="getClasses(conversations.consoleTab)" href="#" @click.prevent="conversations.consoleTab.show()"
class="list-group-item list-group-item-action">
@ -114,6 +118,15 @@
<image-preview ref="imagePreview"></image-preview>
<add-pm-partner ref="addPmPartnerDialog"></add-pm-partner>
<note-status v-if="coreState.settings.risingShowUnreadOfflineCount"></note-status>
<modal :buttons="false" ref="profileAnalysis" dialogClass="profile-analysis" >
<profile-analysis></profile-analysis>
<template slot="title">
{{ownCharacter.name}}
<a class="btn" @click="showProfileAnalyzer"><i class="fa fa-sync" /></a>
</template>
</modal>
</div>
</template>/me
@ -148,6 +161,8 @@
// import { EventBus } from './preview/event-bus';
import AdCenterDialog from './ads/AdCenter.vue';
import AdLauncherDialog from './ads/AdLauncher.vue';
import Modal from '../components/Modal.vue';
import ProfileAnalysis from '../learn/recommend/ProfileAnalysis.vue';
const unreadClasses = {
[Conversation.UnreadState.None]: '',
@ -164,7 +179,9 @@
'add-pm-partner': PmPartnerAdder,
'note-status': NoteStatus,
adCenter: AdCenterDialog,
adLauncher: AdLauncherDialog
adLauncher: AdLauncherDialog,
modal: Modal,
'profile-analysis': ProfileAnalysis
}
})
export default class ChatView extends Vue {
@ -370,6 +387,11 @@
(<AdLauncherDialog>this.$refs['adLauncher']).show();
}
showProfileAnalyzer(): void {
(this.$refs.profileAnalysis as any).show();
void (this.$refs.profileAnalysis as any).$children[0].analyze();
}
showAddPmPartner(): void {
(<PmPartnerAdder>this.$refs['addPmPartnerDialog']).show();
}

View File

@ -21,10 +21,15 @@
<a href="#" @click.prevent="showChannels()" class="btn">
<span class="fa fa-tv"></span><span class="btn-text">Channels</span>
</a>
<a href="#" @click.prevent="showMemo()" class="btn">
<span class="fas fa-edit"></span><span class="btn-text">Memo</span>
</a>
</div>
<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 v-show="userMemo"><b>Memo:</b> {{ userMemo }}</div>
</div>
</div>
</div>
@ -178,6 +183,10 @@
<manage-channel ref="manageDialog" v-if="isChannel(conversation)" :channel="conversation.channel"></manage-channel>
<ad-view ref="adViewer" v-if="isPrivate(conversation) && conversation.character" :character="conversation.character"></ad-view>
<channel-list ref="channelList" v-if="isPrivate(conversation)" :character="conversation.character"></channel-list>
<modal :action="l('user.memo.action')" ref="userMemoEditor" @submit="updateMemo" dialogClass="w-100">
<div style="float:right;text-align:right;">{{getByteLength(editorMemo)}} / 1000</div>
<textarea class="form-control" v-model="editorMemo" maxlength="1000"></textarea>
</modal>
</div>
</template>
@ -186,12 +195,12 @@
import Vue from 'vue';
import {EditorButton, EditorSelection} from '../bbcode/editor';
import {BBCodeView} from '../bbcode/view';
import {isShowing as anyDialogsShown} from '../components/Modal.vue';
import Modal, {isShowing as anyDialogsShown} from '../components/Modal.vue';
import {Keys} from '../keys';
import CharacterAdView from './character/CharacterAdView.vue';
import {Editor} from './bbcode';
import CommandHelp from './CommandHelp.vue';
import { characterImage, getByteLength, getKey } from './common';
import { characterImage, errorToString, getByteLength, getKey } from './common';
import ConversationSettings from './ConversationSettings.vue';
import ConversationAdSettings from './ads/ConversationAdSettings.vue';
import core from './core';
@ -207,6 +216,9 @@
import * as _ from 'lodash';
import Dropdown from '../components/Dropdown.vue';
import { EventBus } from './preview/event-bus';
// import { CharacterMemo } from '../site/character_page/interfaces';
import { MemoManager } from './character/memo';
import { CharacterMemo } from '../site/character_page/interfaces';
@Component({
components: {
@ -221,7 +233,8 @@
'ad-view': CharacterAdView,
'channel-list': CharacterChannelList,
dropdown: Dropdown,
adSettings: ConversationAdSettings
adSettings: ConversationAdSettings,
modal: Modal
}
})
export default class ConversationView extends Vue {
@ -258,6 +271,10 @@
isPrivate = Conversation.isPrivate;
showNonMatchingAds = true;
userMemo: string = '';
editorMemo: string = '';
memoManager?: MemoManager;
ownName?: string;
@Hook('beforeMount')
@ -315,10 +332,15 @@
this.configUpdateHook = () => this.updateOwnName();
EventBus.$on('configuration-update', this.configUpdateHook);
this.memoUpdateHook = (e: any) => this.refreshMemo(e);
EventBus.$on('character-memo', this.memoUpdateHook);
}
protected configUpdateHook: any;
protected memoUpdateHook: any;
@Hook('destroyed')
destroyed(): void {
window.removeEventListener('resize', this.resizeHandler);
@ -329,6 +351,7 @@
clearInterval(this.adCountdown);
EventBus.$off('configuration-update', this.configUpdateHook);
EventBus.$off('character-memo', this.memoUpdateHook);
}
hideSearch(): void {
@ -355,13 +378,19 @@
}
@Watch('conversation')
conversationChanged(): void {
async conversationChanged(): Promise<void> {
this.updateOwnName();
if(!anyDialogsShown) (<Editor>this.$refs['textBox']).focus();
this.$nextTick(() => setTimeout(() => this.messageView.scrollTop = this.messageView.scrollHeight));
this.scrolledDown = true;
this.refreshAutoPostingTimer();
this.userMemo = '';
if (this.isPrivate(this.conversation)) {
const c = await core.cache.profileCache.get(this.conversation.name);
this.userMemo = c?.character?.memo?.memo || '';
}
}
@Watch('conversation.messages')
@ -621,6 +650,35 @@
return (<Partial<Conversation.SFCMessage>>message).sfc !== undefined;
}
updateMemo(): void {
this.memoManager?.set(this.editorMemo).catch((e: object) => alert(errorToString(e)))
this.userMemo = this.editorMemo;
}
refreshMemo(event: { character: string, memo: CharacterMemo }): void {
this.userMemo = event.memo.memo;
}
async showMemo(): Promise<void> {
if (this.isPrivate(this.conversation)) {
const c = this.conversation.character;
this.editorMemo = '';
(<Modal>this.$refs['userMemoEditor']).show();
try {
this.memoManager = new MemoManager(c.name);
await this.memoManager.load();
this.userMemo = this.memoManager.get().memo;
this.editorMemo = this.userMemo;
} catch(e) {
alert(errorToString(e));
}
}
}
get characterImage(): string {
return characterImage(this.conversation.name);
}

View File

@ -60,6 +60,7 @@ import ReportDialog from './ReportDialog.vue';
import { Matcher, MatchReport } from '../learn/matcher';
import _ from 'lodash';
import MatchTags from './preview/MatchTags.vue';
import { MemoManager } from './character/memo';
@Component({
components: {'match-tags': MatchTags, bbcode: BBCodeView(core.bbCodeParser), modal: Modal, 'ad-view': CharacterAdView}
@ -76,9 +77,10 @@ import MatchTags from './preview/MatchTags.vue';
touchedElement: HTMLElement | undefined;
channel: Channel | undefined;
memo = '';
memoId = 0;
// memoId = 0;
memoLoading = false;
match: MatchReport | null = null;
memoManager?: MemoManager;
openConversation(jump: boolean): void {
const conversation = core.conversations.getPrivate(this.character!);
@ -115,24 +117,24 @@ import MatchTags from './preview/MatchTags.vue';
async showMemo(): Promise<void> {
this.memoLoading = true;
this.memo = '';
this.memoManager = new MemoManager(this.character!.name);
(<Modal>this.$refs['memo']).show();
try {
const memo = await core.connection.queryApi<{note: string | null, id: number}>('character-memo-get2.php',
{target: this.character!.name});
this.memoId = memo.id;
this.memo = memo.note !== null ? memo.note : '';
this.memoLoading = false;
await this.memoManager.load();
this.memo = this.memoManager.get().memo;
this.memoLoading = false;
} catch(e) {
alert(errorToString(e));
}
}
updateMemo(): void {
core.connection.queryApi('character-memo-save.php', {target: this.memoId, note: this.memo})
.catch((e: object) => alert(errorToString(e)));
this.memoManager?.set(this.memo).catch((e: object) => alert(errorToString(e)))
}
showAdLogs(): void {
if (!this.hasAdLogs()) {
return;

View File

@ -1,5 +1,5 @@
<template>
<modal action="Post Ads" @submit="submit" ref="dialog" @open="load" dialogClass="w-100" class="adLauncher" :buttonText="'Start Posting Ads'">
<modal action="Post Ads" @submit="submit" ref="dialog" @reopen="load" @open="load" dialogClass="w-100" class="adLauncher" :buttonText="'Start Posting Ads'">
<div v-if="hasAds()">
<h4>Ad Tags</h4>
<div class="form-group">
@ -15,6 +15,13 @@
<div class="form-group">
<p>Serve ads on these channels:</p>
<p v-if="channels.length === 0">You have no channels open that support ad posting. Open some channels first.</p>
<label class="control-label">
<input type="checkbox" id="ard-all-channels" @change="selectAllChannels($event)" />
<i>Select/unselect all</i>
</label>
<label class="control-label" :for="`adr-channel-${index}`" v-for="(channel,index) in channels">
<input type="checkbox" v-model="channel.value" :id="`adr-channel-${index}`" />
{{ channel.title }}
@ -129,6 +136,15 @@ export default class AdLauncherDialog extends CustomDialog {
(<AdCenterDialog>this.$parent.$refs['adCenter']).show();
}
selectAllChannels(e: any): void {
const newValue = e.target.checked;
e.preventDefault();
e.stopPropagation();
_.each(this.channels, (c) => c.value = newValue);
}
submit(e: Event) {
const tags = this.getWantedTags();
const channelIds = this.getWantedChannels();

53
chat/character/memo.ts Normal file
View File

@ -0,0 +1,53 @@
import core from '../core';
import { CharacterMemo } from '../../site/character_page/interfaces';
import { EventBus } from '../preview/event-bus';
export class MemoManager {
memo?: CharacterMemo;
constructor(protected character: string) {
}
get(): CharacterMemo {
if (!this.memo) {
throw new Error('Missing character memo');
}
return this.memo;
}
async set(message: string | null): Promise<void> {
if (!this.memo) {
await this.load(true);
}
const response = await core.connection.queryApi('character-memo-save.php', {target: this.memo!.id, note: message});
this.memo!.memo = (response as any).note;
await this.updateStores();
}
protected async updateStores(): Promise<void> {
const character = await core.cache.profileCache.get(this.character);
if (character && character.character?.memo?.memo !== this.memo!.memo) {
character.character.memo = this.memo;
await core.cache.profileCache.register(character.character);
}
EventBus.$emit('character-memo', { character: this.character, memo: this.memo! });
}
async load(skipStoreUpdate: boolean = false): Promise<void> {
const memo = await core.connection.queryApi<{note: string | null, id: number}>('character-memo-get2.php', {target: this.character});
this.memo = { id: memo.id, memo: memo.note || '' };
if (!skipStoreUpdate) {
await this.updateStores();
}
}
}

View File

@ -36,6 +36,11 @@
<!-- <span v-for="c in customs" :class="Score.getClasses(c.score)">{{c.name}}</span>-->
<!-- </div>-->
<div class="memo" v-if="memo">
<h4>Memo</h4>
<div>{{ memo }}</div>
</div>
<div class="status-message" v-if="statusMessage">
<h4>Status <span v-if="latestAd && (statusMessage === latestAd.message)">&amp; Latest Ad</span></h4>
<bbcode :text="statusMessage"></bbcode>
@ -116,6 +121,7 @@ export default class CharacterPreview extends Vue {
statusClasses?: StatusClasses;
latestAd?: AdCachedPosting;
statusMessage?: string;
memo?: string;
smartFilterIsFiltered?: boolean;
smartFilterDetails?: string[];
@ -199,6 +205,7 @@ export default class CharacterPreview extends Vue {
this.match = undefined;
this.character = undefined;
this.customs = undefined;
this.memo = undefined;
this.ownCharacter = core.characters.ownProfile;
this.conversation = undefined;
@ -218,6 +225,7 @@ export default class CharacterPreview extends Vue {
this.updateSmartFilterReport();
this.updateCustoms();
this.updateDetails();
this.updateMemo();
}, 0);
}
@ -293,6 +301,9 @@ export default class CharacterPreview extends Vue {
this.latestAd = cache.posts[cache.posts.length - 1];
}
updateMemo(): void {
this.memo = this.character?.memo?.memo;
}
updateCustoms(): void {
this.customs = _.orderBy(
@ -469,7 +480,8 @@ export default class CharacterPreview extends Vue {
.status-message,
.latest-ad-message,
.conversation {
.conversation,
.memo {
display: block;
background-color: rgba(0,0,0,0.2);
padding: 10px;

View File

@ -58,6 +58,10 @@ const previewInitiationTime = Date.now();
return;
}
if (window.location.href.match(/^https?:\/\/(www\.)?rule34video\.com/)) {
return;
}
if (window.location.href.match(/^https?:\/\/[a-zA-Z0-9-]+\.tumblr\.com/)) {
// Because Tumblr sucks with their iframes
const og = document.querySelectorAll('meta[property="og:image"]:not([content=""])');

View File

@ -19,7 +19,8 @@ import { NoteCheckerCount } from '../../site/note-checker';
* 'channel-ad': {message: Message, channel: Conversation, profile: ComplexCharacter | undefined}
* 'channel-message': {message: Message, channel: Conversation}
* 'select-conversation': { conversation: Conversation }
* 'note-counts-update': {}
* 'note-counts-update': {},
* 'character-memo': { character: string, memo: CharacterMemo }
*/

View File

@ -174,6 +174,7 @@ export class ImageDomMutator {
this.add('xhamster.com', this.getBaseJsMutatorScript(['#photo_slider video', '#photo_slider img', 'video', 'img']));
this.add('shadbase.com', this.getBaseJsMutatorScript(['#comic video', '#comic img', 'video', 'img']));
this.add('instagram.com', this.getBaseJsMutatorScript(['article video', 'article img', 'video', 'img']));
this.add('rule34video.com', this.getBaseJsMutatorScript(['video'], true, [], false, true));
this.add(
'pornhub.com',
@ -237,7 +238,6 @@ export class ImageDomMutator {
'dom-ready'
);
this.add(
'hentai-foundry.com',
this.getBaseJsMutatorScript(['#picBox video', '#picBox img']),

View File

@ -38,6 +38,15 @@ export class ImageUrlMutator {
// async(): Promise<string> => 'https://i.imgur.com/ScNLbsp.png'
// );
this.add(
/^https?:\/\/rule34video.com\/videos\/([0-9a-zA-Z-_]+)/,
async(_url: string, match: RegExpMatchArray): Promise<string> => {
const videoId = match[1];
return `https://rule34video.com/embed/${videoId}`;
}
);
this.add(
/^https?:\/\/(www.)?pornhub.com\/view_video.php\?viewkey=([a-z0-9A-Z]+)/,
async(_url: string, match: RegExpMatchArray): Promise<string> => {
@ -60,7 +69,7 @@ export class ImageUrlMutator {
);
this.add(
/^https?:\/\/(www.)?redgifs.com\/watch\/([a-z0-9A-Z]+)/,
/^https?:\/\/(www.|v3.)?redgifs.com\/watch\/([a-z0-9A-Z]+)/,
async(_url: string, match: RegExpMatchArray): Promise<string> => {
const redgifId = match[2];

View File

@ -130,3 +130,10 @@
[url=https://www.redgifs.com/watch/blissfulhandywhoopingcrane]Redgifs.com[/url]
[url=https://media.giphy.com/media/P7hen0jkFBud3EZ6ez/giphy.gif]Giphy[/url]
[url=https://v3.redgifs.com/watch/bruisedinconsequentialseabird#rel=niche%3Afit-girls;order=trending]Redgifs V3[/url]
[url=https://rule34video.com/videos/3087711/bonding-ritual-4k-no-wm/]Rule34video[/url]

View File

@ -85,7 +85,10 @@
show(keepOpen: boolean = false): void {
this.keepOpen = keepOpen;
if(this.isShown) return;
if(this.isShown) {
this.$emit('reopen');
return;
}
this.isShown = true;
dialogStack.push(this);
this.$emit('open');

View File

@ -50,7 +50,7 @@ theme: jekyll-theme-slate
changelog: https://github.com/mrstallion/fchat-rising/blob/master/CHANGELOG.md
download:
version: 1.20.0
version: 1.21.0
url: https://github.com/mrstallion/fchat-rising/releases/download/v%VERSION%/F-Chat-Rising-%VERSION%-%PLATFORM_TAIL%

View File

@ -97,13 +97,15 @@
<word-definition :expression="wordDefinitionLookup" ref="wordDefinitionLookup"></word-definition>
<template slot="title">
{{wordDefinitionLookup}}
<a class="btn wordDefBtn dictionary" @click="openDefinitionWithDictionary"><i>D</i></a>
<a class="btn wordDefBtn thesaurus" @click="openDefinitionWithThesaurus"><i>T</i></a>
<a class="btn wordDefBtn urbandictionary" @click="openDefinitionWithUrbanDictionary"><i>UD</i></a>
<a class="btn wordDefBtn wikipedia" @click="openDefinitionWithWikipedia"><i>W</i></a>
<a class="btn" @click="openWordDefinitionInBrowser"><i class="fa fa-external-link-alt"/></a>
</template>
</modal>
<logs ref="logsDialog"></logs>
</div>
</template>
@ -133,6 +135,7 @@
// import { Sqlite3Store } from '../learn/store/sqlite3';
import CharacterPage from '../site/character_page/character_page.vue';
import WordDefinition from '../learn/dictionary/WordDefinition.vue';
import ProfileAnalysis from '../learn/recommend/ProfileAnalysis.vue';
import {defaultHost, GeneralSettings, nativeRequire} from './common';
import { fixLogs /*SettingsStore, Logs as FSLogs*/ } from './filesystem';
import * as SlimcatImporter from './importer';
@ -200,7 +203,8 @@
logs: Logs,
'word-definition': WordDefinition,
BBCodeTester: BBCodeTester,
bbcode: BBCodeView(core.bbCodeParser)
bbcode: BBCodeView(core.bbCodeParser),
'profile-analysis': ProfileAnalysis
}
})
export default class Index extends Vue {
@ -601,6 +605,14 @@
}
async openWordDefinitionInBrowser(): Promise<void> {
await remote.shell.openExternal((this.$refs.wordDefinitionLookup as any).getWebUrl());
// tslint:disable-next-line: no-any no-unsafe-any
(this.$refs.wordDefinitionViewer as any).hide();
}
unpinUrlPreview(e: Event): void {
const imagePreview = (this.$refs['chat'] as Chat)?.getChatView()?.getImagePreview();

View File

@ -1,6 +1,6 @@
{
"name": "fchat",
"version": "1.20.0",
"version": "1.21.0",
"author": "The F-List Team and Mister Stallion (Esq.)",
"description": "F-List.net Chat Client",
"main": "main.js",

View File

@ -524,6 +524,10 @@ export class Matcher {
return this.formatScoring(score, postLengthPreferenceMapping[theirLength]);
}
static getSpeciesName(species: Species): string {
return speciesNames[species] || `${Species[species].toLowerCase()}s`;
}
private resolveSpeciesScore(): Score {
const you = this.you;
const theirAnalysis = this.theirAnalysis;

View File

@ -0,0 +1,91 @@
<template>
<div class="profile-analysis-wrapper" ref="profileAnalysisWrapper">
<div v-if="!analyzing && !recommendations.length">
<h3>Looking good!</h3>
<p>The profile analyzer could not find any improvement recommendations for your profile.</p>
</div>
<div v-else-if="analyzing && !recommendations.length">
<p>Having problems with finding good matches?</p>
<p>&nbsp;</p>
<p>The profile analyzer will identify if your profile could benefit from adjustments.</p>
<p>&nbsp;</p>
<h3>Analyzing...</h3>
</div>
<div v-else>
<p>Having problems with finding good matches?</p>
<p>&nbsp;</p>
<p>The profile analyzer recommends the following adjustments to your profile:</p>
<ul>
<li v-for="r in recommendations" class="recommendation" :class="r.level">
<h3>{{r.title}}</h3>
<p>{{r.desc}}</p>
</li>
</ul>
</div>
</div>
</template>
<script lang="ts">
import { Component } from '@f-list/vue-ts';
import Vue from 'vue';
import core from '../../chat/core';
import { ProfileRecommendation, ProfileRecommendationAnalyzer } from './profile-recommendation';
import { CharacterAnalysis } from '../matcher';
import { methods } from '../../site/character_page/data_store';
@Component({})
export default class ProfileAnalysis extends Vue {
recommendations: ProfileRecommendation[] = [];
analyzing = false;
async analyze() {
this.analyzing = true;
this.recommendations = [];
const char = await methods.characterData(core.characters.ownProfile.character.name, core.characters.ownProfile.character.id, true);
const profile = new CharacterAnalysis(char.character);
const analyzer = new ProfileRecommendationAnalyzer(profile);
this.analyzing = false;
this.recommendations = analyzer.analyze();
}
}
</script>
<style lang="scss">
.profile-analysis-wrapper {
h3 {
font-size: 130%;
margin-bottom: 0;
}
p {
font-size: 95%;
margin: 0;
}
ul {
list-style-type: none;
margin: 0;
padding: 0;
}
li {
padding: 10px;
margin: 5px;
line-height: 120%;
border-radius: 3px;
&.critical {
background-color: var(--scoreMismatchBg);
}
&.note {
background-color: var(--scoreWeakMismatchBg);
}
}
}
</style>

View File

@ -0,0 +1,203 @@
import _ from 'lodash';
import { CharacterAnalysis, Matcher } from '../matcher';
import { FurryPreference, Kink, mammalSpecies, Species } from '../matcher-types';
export enum ProfileRecommendationLevel {
INFO = 'info',
NOTE = 'note',
CRITICAL = 'critical'
}
export interface ProfileRecommendationUrlParams {
// TBD
}
export interface ProfileRecommendation {
code: string;
level: ProfileRecommendationLevel;
title: string;
desc: string;
urlParams?: ProfileRecommendationUrlParams
}
export class ProfileRecommendationAnalyzer {
protected recommendations: ProfileRecommendation[] = [];
constructor(protected readonly profile: CharacterAnalysis) {
//
}
protected add(code: string, level: ProfileRecommendationLevel, title: string, desc: string, urlParams?: ProfileRecommendationUrlParams): void {
this.recommendations.push({ code, level, title, desc, urlParams });
}
analyze(): ProfileRecommendation[] {
this.recommendations = [];
this.checkMissingProperties();
this.checkSpeciesPreferences();
this.checkKinkCounts();
this.checkCustomKinks();
this.checkPortrait();
this.checkImages();
this.checkInlineImage();
this.checkDescriptionLength();
return this.recommendations;
}
protected checkPortrait(): void {
// this.profile.character.
// do nothing
}
protected checkImages(): void {
if (!this.profile.character.image_count) {
this.add(`ADD_IMAGE`, ProfileRecommendationLevel.CRITICAL, 'Add a profile image', 'Profiles with images are more attractive to other players.');
} else if (this.profile.character.image_count > 1 && this.profile.character.image_count < 3) {
this.add(`ADD_MORE_IMAGES`, ProfileRecommendationLevel.NOTE, 'Add more profile images', 'Profiles with images are more attractive try to have at least 3 images in your profile.');
}
}
protected checkInlineImage(): void {
if (_.keys(this.profile.character.inlines).length < 1) {
this.add(`ADD_INLINE_IMAGE`, ProfileRecommendationLevel.NOTE, 'Add an inline image', 'Profiles with inline images are more engaging to other players.');
}
}
protected checkDescriptionLength(): void {
const desc = this.profile.character.description.trim();
if (desc.length < 20) {
this.add(`ADD_DESCRIPTION`, ProfileRecommendationLevel.CRITICAL, 'Add description', 'Profiles with descriptions are more likely to draw attention from other players.');
} else if (desc.length < 400) {
this.add(`EXPAND_DESCRIPTION`, ProfileRecommendationLevel.NOTE, 'Extend your description', 'Long descriptions are more attractive to other players. Try expanding your description to at least 400 characters.');
}
}
protected checkCustomKinks(): void {
const counts = _.reduce(this.profile.character.customs, (accum, kink) => {
if (kink) {
accum.total += 1;
if (kink.description) {
accum.filled += 1;
}
}
return accum;
}, { filled: 0, total: 0 });
if (counts.total === 0) {
this.add(`ADD_CUSTOM_KINK`, ProfileRecommendationLevel.CRITICAL, 'Add custom kinks', `Custom kinks will help your profile stand out. Try adding at least 5 custom kinks.`);
} else if (counts.total < 5) {
this.add(`ADD_MORE_CUSTOM_KINKS`, ProfileRecommendationLevel.NOTE, 'Add more custom kinks', `Players pay a lot of attention to custom kinks. Try adding at least 5 custom kinks.`);
}
if (counts.filled < counts.total && counts.total > 0) {
this.add(`ADD_MORE_CUSTOM_KINK_DESCRIPTIONS`, ProfileRecommendationLevel.NOTE, 'Add descriptions to custom kinks', `Some or all of your custom kinks are missing descriptions. Add descriptions to your custom kinks to attract more players.`);
}
}
protected checkKinkCounts(): void {
const counts = _.reduce(this.profile.character.kinks, (accum, kinkLevel) => {
if (_.isString(kinkLevel) && kinkLevel) {
accum[kinkLevel as keyof typeof accum] += 1;
}
return accum;
}, { favorite: 0, yes: 0, maybe: 0, no: 0 });
const minCountPerType = 5;
const totalCount = counts.favorite + counts.yes + counts.maybe + counts.no;
if (totalCount < 10) {
this.add(`ADD_MORE_KINKS`, ProfileRecommendationLevel.CRITICAL, `Add more kinks`, `You should have at least 10 kinks for the matching algorithm to work well.`);
} else {
_.each(counts, (count, key) => {
if (count < minCountPerType) {
this.add(`ADD_MORE_KINKS_${key.toString().toUpperCase()}`, ProfileRecommendationLevel.CRITICAL, `Add more '${key}' kinks`, `You should have at least ${minCountPerType} '${key}' kinks for the matching algorithm to work well.`);
}
});
}
}
protected checkMissingProperties(): void {
const p = this.profile;
if (p.age === null) {
this.add('AGE', ProfileRecommendationLevel.CRITICAL, 'Enter age', 'Specifying the age of your character will improve your matches with other players.');
}
if (p.orientation === null) {
this.add('ORIENTATION', ProfileRecommendationLevel.CRITICAL, 'Enter sexual orientation', 'Specifying the sexual orientation of your character will improve your matches with other players.');
}
if (p.species === null) {
this.add('SPECIES', ProfileRecommendationLevel.CRITICAL, 'Enter species', 'Specifying the species of your character even if it\'s \'human\' will improve your matches with other players.');
}
if (p.furryPreference === null) {
this.add('FURRY_PREFERENCE', ProfileRecommendationLevel.CRITICAL, 'Enter furry preference', 'Specifying whether you like to play with anthro characters will improve your matches with other players.');
}
if (p.subDomRole === null) {
this.add('SUB_DOM_ROLE', ProfileRecommendationLevel.CRITICAL, 'Enter sub/dom role', 'Specifying your preferred sub/dom role will improve your matches with other players.');
}
if (p.position === null) {
this.add('POSITION', ProfileRecommendationLevel.CRITICAL, 'Enter position', 'Specifying your preferred position (e.g. "top", "bottom") will improve your matches with other players.');
}
if (p.postLengthPreference === null) {
this.add('POST_LENGTH', ProfileRecommendationLevel.CRITICAL, 'Enter post length preference', 'Specifying your post length preference will improve your matches with other players.');
}
if (p.bodyType === null) {
this.add('BODY_TYPE', ProfileRecommendationLevel.CRITICAL, 'Enter body type', 'Specifying your character\'s body type will improve your matches with other players.');
}
}
protected checkSpeciesPreferences(): void {
const p = this.profile;
const c = this.profile.character;
if (p.furryPreference === null) {
return;
}
if (p.furryPreference === FurryPreference.FurriesOnly) {
if (Matcher.getKinkPreference(c, Kink.Humans)! > 0) {
this.add('KINK_MISMATCH_FURRIES_ONLY_HUMAN', ProfileRecommendationLevel.NOTE, 'Inconsistent kink', 'Your "furries-only" profile has a positive "humans" kink. If you are open to playing with humans, consider updating your preference from "furries only" to "furs and humans".');
}
}
if (p.furryPreference === FurryPreference.HumansOnly) {
if (Matcher.getKinkPreference(c, Kink.AnimalsFerals)! >= 0 || Matcher.getKinkPreference(c, Kink.Zoophilia)! >= 0) {
// do nothing
} else {
const likedAnthros = this.getLikedAnimals();
_.each(likedAnthros, (species) => {
this.add('KINK_MISMATCH_HUMANS_ONLY_ANTHRO', ProfileRecommendationLevel.NOTE, 'Inconsistent kink', `Your "humans-only" profile has a positive "furry" kink (${Matcher.getSpeciesName(species)}). If you are open to playing with anthros, consider updating your preference from "humans only" to "furs and humans"`);
});
}
}
if (p.furryPreference !== FurryPreference.HumansOnly) {
const likedAnthros = this.getLikedAnimals();
if (likedAnthros && !_.difference(likedAnthros, [Kink.AnthroCharacters, Kink.Mammals, Kink.Humans] as any as Species[])) {
this.add('KINK_NO_SPECIES', ProfileRecommendationLevel.NOTE, 'Add preferred species', 'Specifying which anthro species you like (e.g. "equines", or "canines") in your kinks can improve your matches.');
}
}
}
protected getLikedAnimals(): Species[] {
const c = this.profile.character;
return _.filter(mammalSpecies, (species) => Matcher.getKinkPreference(c, species)! > 0);
}
}

View File

@ -1,6 +1,6 @@
{
"name": "f-list-rising",
"version": "1.20.0",
"version": "1.21.0",
"author": "The F-List Team and and Mister Stallion (Esq.)",
"description": "A heavily modded F-Chat 3.0 client for F-List",
"license": "MIT",

View File

@ -340,10 +340,14 @@
memo(memo: {id: number, memo: string}): void {
Vue.set(this.character!, 'memo', memo);
void core.cache.profileCache.register(this.character!);
}
bookmarked(state: boolean): void {
Vue.set(this.character!, 'bookmarked', state);
void core.cache.profileCache.register(this.character!);
}
protected async loadSelfCharacter(): Promise<void> {

View File

@ -96,15 +96,17 @@ export interface CharacterGroup {
owner: boolean
}
export interface CharacterMemo {
id: number;
memo: string;
}
export interface Character {
readonly is_self: boolean
character: CharacterInfo
readonly settings: CharacterSettings
readonly badges?: string[]
memo?: {
id: number
memo: string
}
memo?: CharacterMemo;
readonly character_list?: {
id: number
name: string
@ -158,4 +160,4 @@ export interface FriendsByCharacter {
pending: FriendRequest[]
incoming: FriendRequest[]
name: string
}
}

View File

@ -17,7 +17,8 @@
import Modal from '../../components/Modal.vue';
import {SimpleCharacter} from '../../interfaces';
import * as Utils from '../utils';
import {methods} from './data_store';
// import {methods} from './data_store';
import { MemoManager } from '../../chat/character/memo';
export interface Memo {
id: number
@ -61,8 +62,11 @@
async save(): Promise<void> {
try {
this.saving = true;
const memoReply = await methods.memoUpdate(this.character.id, this.message);
this.$emit('memo', this.message !== '' ? memoReply : undefined);
const memoManager = new MemoManager(this.character.name);
await memoManager.set(this.message);
this.$emit('memo', memoManager.get());
this.hide();
} catch(e) {
Utils.ajaxError(e, 'Unable to set memo.');
@ -70,4 +74,4 @@
this.saving = false;
}
}
</script>
</script>