Highlight ad matches

This commit is contained in:
Mr. Stallion 2019-07-06 20:37:15 -05:00
parent 6671fd9374
commit b0129c75cb
23 changed files with 806 additions and 25 deletions

View File

@ -592,4 +592,51 @@
flex-basis: 100%;
}
}
.message.message-score {
padding-left: 5px;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
&.match {
border-left: 12px solid #027b02;
background-color: rgba(1, 76, 1, 0.45);
}
&.weak-match {
border-left: 12px solid #015a01;
background-color: rgba(0, 58, 0, 0.35);
}
&.weak-mismatch {
background-color: rgba(208, 188, 0, 0.0);
border-left: 12px solid rgb(138, 123, 0);
.bbcode {
filter: grayscale(0.7);
}
.bbcode,
.user-view,
.message-time {
opacity: 0.4;
}
}
&.mismatch {
border-left: 12px solid #841a1a;
.bbcode {
filter: grayscale(0.8);
}
.bbcode,
.user-view,
.message-time {
opacity: 0.3;
}
}
}
</style>

View File

@ -95,7 +95,7 @@
}
);
const webview = this.$refs.imagePreviewExt as WebviewTag;
const webview = this.getWebview();
webview.addEventListener(
'dom-ready',
@ -179,7 +179,7 @@
this.visible = false;
if (this.externalUrlVisible) {
const webview = this.$refs.imagePreviewExt as WebviewTag;
const webview = this.getWebview();
webview.executeJavaScript(this.jsMutator.getHideMutator());
}
@ -263,10 +263,21 @@
this.internalUrlVisible = isInternal;
this.externalUrlVisible = !isInternal;
if (isInternal)
if (isInternal) {
this.internalUrl = this.url;
else
} else {
const webview = this.getWebview();
try {
if (webview.getURL() === this.url) {
webview.executeJavaScript(this.jsMutator.getReShowMutator());
}
} catch (err) {
console.log('Webview reuse error', err);
}
this.externalUrl = this.url;
}
this.visible = true;
this.visibleSince = Date.now();
@ -328,7 +339,7 @@
this.jsMutator.setDebug(this.debug);
if (this.debug) {
const webview = this.$refs.imagePreviewExt as WebviewTag;
const webview = this.getWebview();
webview.openDevTools();
}
@ -347,11 +358,16 @@
reloadUrl(): void {
if (this.externalUrlVisible) {
const webview = this.$refs.imagePreviewExt as WebviewTag;
const webview = this.getWebview();
webview.reload();
}
}
getWebview(): WebviewTag {
return this.$refs.imagePreviewExt as WebviewTag;
}
}
</script>

View File

@ -94,6 +94,8 @@ export class Message implements Conversation.ChatMessage {
readonly id = ++messageId;
isHighlight = false;
score?: number;
constructor(readonly type: Conversation.Message.Type, readonly sender: Character, readonly text: string,
readonly time: Date = new Date()) {
if(Conversation.Message.Type[type] === undefined) throw new Error('Unknown type'); //tslint:disable-line

View File

@ -7,6 +7,7 @@ import {Channel, Character, Conversation as Interfaces} from './interfaces';
import l from './localize';
import {CommandContext, isAction, isCommand, isWarn, parse as parseCommand} from './slash_commands';
import MessageType = Interfaces.Message.Type;
import {EventBus} from '../chat/event-bus';
function createMessage(this: void, type: MessageType, sender: Character, text: string, time?: Date): Message {
if(type === MessageType.Message && isAction(text)) {
@ -549,6 +550,7 @@ export default function(this: void): Interfaces.State {
const char = core.characters.get(data.character);
if(char.isIgnored) return connection.send('IGN', {action: 'notify', character: data.character});
const message = createMessage(MessageType.Message, char, decodeHTML(data.message), time);
EventBus.$emit('private-message', { message });
const conv = state.getPrivate(char);
await conv.addMessage(message);
});
@ -558,6 +560,7 @@ export default function(this: void): Interfaces.State {
if(conversation === undefined) return core.channels.leave(data.channel);
if(char.isIgnored && !isOp(conversation)) return;
const message = createMessage(MessageType.Message, char, decodeHTML(data.message), time);
EventBus.$emit('channel-message', { message, channel: conversation });
await conversation.addMessage(message);
const words = conversation.settings.highlightWords.slice();
@ -586,7 +589,18 @@ export default function(this: void): Interfaces.State {
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;
await conv.addMessage(new Message(MessageType.Ad, char, decodeHTML(data.message), time));
const msg = new Message(MessageType.Ad, char, decodeHTML(data.message), time);
if (core.characters.ownProfile) {
const p = core.cache.profileCache.get(char.name);
if (p) {
msg.score = p.matchScore;
}
}
EventBus.$emit('channel-ad', { message: msg, channel: conv });
await conv.addMessage(msg);
});
connection.onMessage('RLL', async(data, time) => {
const sender = core.characters.get(data.character);

View File

@ -1,4 +1,5 @@
import Vue, {WatchHandler} from 'vue';
import { CacheManager } from '../learn/cache-manager';
import BBCodeParser from './bbcode';
import {Settings as SettingsImpl} from './common';
import {Channel, Character, Connection, Conversation, Logs, Notifications, Settings, State as StateInterface} from './interfaces';
@ -60,6 +61,7 @@ const data = {
channels: <Channel.State | undefined>undefined,
characters: <Character.State | undefined>undefined,
notifications: <Notifications | undefined>undefined,
cache: <CacheManager | undefined>undefined,
register(this: void | never, module: 'characters' | 'conversations' | 'channels',
subState: Channel.State | Character.State | Conversation.State): void {
Vue.set(vue, module, subState);
@ -85,6 +87,10 @@ export function init(this: void, connection: Connection, logsClass: new() => Log
data.logs = new logsClass();
data.settingsStore = new settingsClass();
data.notifications = new notificationsClass();
data.cache = new CacheManager();
data.cache.start();
connection.onEvent('connecting', async() => {
await data.reloadSettings();
data.bbCodeParser = createBBCodeParser();
@ -101,6 +107,8 @@ export interface Core {
readonly channels: Channel.State
readonly bbCodeParser: BBCodeParser
readonly notifications: Notifications
readonly cache: CacheManager
register(module: 'conversations', state: Conversation.State): void
register(module: 'channels', state: Channel.State): void
register(module: 'characters', state: Character.State): void

View File

@ -1,9 +1,37 @@
import Vue from 'vue';
import { Character } from '../site/character_page/interfaces';
import { Message } from './common';
import { Conversation } from './interfaces';
import ChannelConversation = Conversation.ChannelConversation;
/**
* 'imagepreview-dismiss': {url: string}
* 'imagepreview-show': {url: string}
* 'imagepreview-toggle-stickyness': {url: string}
* 'character-data': {character: Character}
* 'private-message': {message: Message}
* 'channel-ad': {message: Message, channel: Conversation}
* 'channel-message': {message: Message, channel: Conversation}
*/
export interface EventBusEvent {
// tslint:disable: no-any
[key: string]: any;
}
export interface ChannelMessageEvent extends EventBusEvent {
message: Message;
channel: ChannelConversation;
}
// tslint:disable-next-line no-empty-interface
export interface ChannelAdEvent extends ChannelMessageEvent {}
export interface CharacterDataEvent {
character: Character;
}
export const EventBus = new Vue();

View File

@ -168,7 +168,7 @@ export class ImagePreviewMutator {
let removeList = [];
const safeIds = ['flistWrapper', 'flistError', 'flistHider'];
body.childNodes.forEach((el) => ((safeIds.indexOf(el.id) < 0) ? removeList.push(el) : true)
body.childNodes.forEach((el) => ((safeIds.indexOf(el.id) < 0) ? removeList.push(el) : true));
${skipElementRemove ? '' : 'removeList.forEach((el) => el.remove());'}
removeList = [];
@ -255,4 +255,14 @@ export class ImagePreviewMutator {
"></div>
`);
}
getReShowMutator(): string {
return this.wrapJs(
`
const el = document.querySelector('#flistHider');
if (el) { el.remove(); }
`
);
}
}

View File

@ -13,6 +13,8 @@ export namespace Conversation {
readonly type: Message.Type
readonly text: string
readonly time: Date
score?: number;
}
export interface EventMessage extends BaseMessage {

View File

@ -1,6 +1,7 @@
import {Component, Prop} from '@f-list/vue-ts';
import { Component, Prop, Watch } from '@f-list/vue-ts';
import {CreateElement, default as Vue, VNode, VNodeChildrenArrayContents} from 'vue';
import {Channel} from '../fchat';
import { Score, Scoring } from '../site/character_page/matcher';
import {BBCodeView} from './bbcode';
import {formatTime} from './common';
import core from './core';
@ -21,7 +22,8 @@ const userPostfix: {[key: number]: string | undefined} = {
/*tslint:disable-next-line:prefer-template*///unreasonable here
let classes = `message message-${Conversation.Message.Type[message.type].toLowerCase()}` + (separators ? ' message-block' : '') +
(message.type !== Conversation.Message.Type.Event && message.sender.name === core.connection.character ? ' message-own' : '') +
((this.classes !== undefined) ? ` ${this.classes}` : '');
((this.classes !== undefined) ? ` ${this.classes}` : '') +
` ${this.scoreClasses}`;
if(message.type !== Conversation.Message.Type.Event) {
children.push((message.type === Conversation.Message.Type.Action) ? '*' : '',
createElement(UserView, {props: {character: message.sender, channel: this.channel}}),
@ -54,4 +56,41 @@ export default class MessageView extends Vue {
readonly channel?: Channel;
@Prop
readonly logs?: true;
scoreClasses = this.getMessageScoreClasses(this.message);
@Watch('message.score')
scoreUpdate(): void {
console.log('Message score update', this.message.score, this.message.text);
this.scoreClasses = this.getMessageScoreClasses(this.message);
this.$forceUpdate();
}
getMessageScoreClasses(message: Conversation.Message): string {
if ((!('score' in message)) || (message.score === undefined) || (message.score === 0)) {
return '';
}
console.log('Score was', message.score);
return `message-score ${Score.getClasses(message.score as Scoring)}`;
// const baseClass = message.score > 0 ? 'message-score-positive' : 'message-score-negative';
//
// const score = Math.abs(message.score);
//
// let scoreStrength = 'message-score-normal';
//
// if (score > 3) {
// scoreStrength = 'message-score-high';
// } else if (score > 1.5) {
// scoreStrength = 'message-score-medium';
// }
//
// return `message-score ${baseClass} ${scoreStrength}`;
}
}

View File

@ -29,7 +29,10 @@ const parserSettings = {
inlineDisplayMode: InlineDisplayMode.DISPLAY_ALL
};
async function characterData(name: string | undefined): Promise<Character> {
// tslint:disable-next-line: ban-ts-ignore
// @ts-ignore
async function characterData(name: string | undefined, id: number = -1, skipEvent: boolean = false): Promise<Character> {
const data = await core.connection.queryApi<CharacterInfo & {
badges: string[]
customs_first: boolean
@ -95,7 +98,8 @@ async function characterData(name: string | undefined): Promise<Character> {
self_staff: false
};
EventBus.$emit('character-data', charData);
if (!skipEvent)
EventBus.$emit('character-data', { character: charData });
return charData;
}

View File

@ -1,5 +1,8 @@
import core from '../chat/core';
import { methods } from '../site/character_page/data_store';
import {decodeHTML} from './common';
import {Character as Interfaces, Connection} from './interfaces';
import { Character as CharacterProfile } from '../site/character_page/interfaces';
class Character implements Interfaces.Character {
gender: Interfaces.Gender = 'None';
@ -16,7 +19,10 @@ class Character implements Interfaces.Character {
class State implements Interfaces.State {
characters: {[key: string]: Character | undefined} = {};
ownCharacter: Character = <any>undefined; /*tslint:disable-line:no-any*///hack
ownProfile: CharacterProfile = <any>undefined; /*tslint:disable-line:no-any*///hack
friends: Character[] = [];
bookmarks: Character[] = [];
ignoreList: string[] = [];
@ -49,6 +55,12 @@ class State implements Interfaces.State {
character.status = status;
character.statusText = decodeHTML(text);
}
async resolveOwnProfile(): Promise<void> {
await methods.fieldsGet();
this.ownProfile = await methods.characterData(this.ownCharacter.name, -1, false);
}
}
let state: State;
@ -108,9 +120,18 @@ export default function(this: void, connection: Connection): Interfaces.State {
connection.onMessage('FLN', (data) => {
state.setStatus(state.get(data.character), 'offline', '');
});
connection.onMessage('NLN', (data) => {
connection.onMessage('NLN', async(data) => {
const character = state.get(data.identity);
if(data.identity === connection.character) state.ownCharacter = character;
if(data.identity === connection.character) {
state.ownCharacter = character;
await state.resolveOwnProfile();
// tslint:disable-next-line no-unnecessary-type-assertion
core.cache.setProfile(state.ownProfile as CharacterProfile);
}
character.name = data.identity;
character.gender = data.gender;
state.setStatus(character, data.status, '');

View File

@ -1,3 +1,5 @@
import { Character as CharacterProfile } from '../site/character_page/interfaces';
//tslint:disable:no-shadowed-variable
export namespace Connection {
export type ClientCommands = {
@ -165,6 +167,8 @@ export namespace Character {
readonly friendList: ReadonlyArray<string>
readonly bookmarkList: ReadonlyArray<string>
readonly ownProfile: CharacterProfile;
get(name: string): Character
}

62
learn/ad-cache.ts Normal file
View File

@ -0,0 +1,62 @@
import { Cache } from './cache';
export interface AdCachedPosting {
channelName: string;
datePosted: Date;
message: string;
}
export interface AdPosting extends AdCachedPosting {
name: string;
}
export class AdCacheRecord {
protected name: string;
protected posts: AdCachedPosting[] = [];
constructor(name: string, posting?: AdPosting) {
this.name = name;
if (posting)
this.add(posting);
}
add(ad: AdPosting): void {
this.posts.push(
{
channelName: ad.channelName,
datePosted: ad.datePosted,
message: ad.message
}
);
}
count(): number {
return this.posts.length;
}
getDateLastPosted(): Date | null {
if (this.posts.length === 0)
return null;
return this.posts[this.posts.length - 1].datePosted;
}
}
export class AdCache<RecordType extends AdCacheRecord = AdCacheRecord> extends Cache<RecordType> {
register(ad: AdPosting): void {
const k = Cache.nameKey(ad.name);
if (k in this.cache) {
const adh = this.cache[k];
adh.add(ad);
return;
}
this.cache[k] = new AdCacheRecord(name, ad) as RecordType;
}
}

218
learn/cache-manager.ts Normal file
View File

@ -0,0 +1,218 @@
import * as _ from 'lodash';
import core from '../chat/core';
import { ChannelAdEvent, ChannelMessageEvent, CharacterDataEvent, EventBus } from '../chat/event-bus';
import { Conversation } from '../chat/interfaces';
import { methods } from '../site/character_page/data_store';
import { Character } from '../site/character_page/interfaces';
import { Gender } from '../site/character_page/matcher';
import { AdCache } from './ad-cache';
import { ChannelConversationCache } from './channel-conversation-cache';
import { CharacterProfiler } from './character-profiler';
import { ProfileCache } from './profile-cache';
import Timer = NodeJS.Timer;
import ChannelConversation = Conversation.ChannelConversation;
import Message = Conversation.Message;
export interface ProfileCacheQueueEntry {
name: string;
key: string;
added: Date;
gender?: Gender;
score: number;
}
export class CacheManager {
static readonly PROFILE_QUERY_DELAY = 1000; //1 * 1000;
adCache: AdCache = new AdCache();
profileCache: ProfileCache = new ProfileCache();
channelConversationCache: ChannelConversationCache = new ChannelConversationCache();
protected queue: ProfileCacheQueueEntry[] = [];
protected profileTimer: Timer | null = null;
protected characterProfiler: CharacterProfiler | undefined;
queueForFetching(name: string): void {
const key = ProfileCache.nameKey(name);
if (this.profileCache.has(key))
return;
if (!!_.find(this.queue, (q: ProfileCacheQueueEntry) => (q.key === key)))
return;
const entry: ProfileCacheQueueEntry = {
name,
key,
added: new Date(),
score: 0
};
this.queue.push(entry);
}
async fetchProfile(name: string): Promise<void> {
try {
await methods.fieldsGet();
const c = await methods.characterData(name, -1, true);
const r = this.profileCache.register(c);
this.updateAdScoringForProfile(c, r.matchScore);
} catch (err) {
console.error('Failed to fetch profile for cache', name, err);
}
}
updateAdScoringForProfile(c: Character, score: number): void {
_.each(
core.conversations.channelConversations,
(ch: ChannelConversation) => {
_.each(
ch.messages, (m: Conversation.Message) => {
if ((m.type === Message.Type.Ad) && (m.sender) && (m.sender.name === c.character.name)) {
console.log('Update score', score, ch.name, m.sender.name, m.text, m.id);
m.score = score;
}
}
);
}
);
}
addProfile(character: string | Character): void {
if (typeof character === 'string') {
// console.log('Learn discover', character);
this.queueForFetching(character);
return;
}
this.profileCache.register(character);
}
/*
* Preference in order (plan):
* + has messaged me
* + bookmarked / friend
*
* + genders I like
* + looking
* + online
*
* - busy
* - DND
* - away
*/
consumeNextInQueue(): ProfileCacheQueueEntry | null {
if (this.queue.length === 0) {
return null;
}
// re-score
_.each(this.queue, (e: ProfileCacheQueueEntry) => this.calculateScore(e));
this.queue = _.sortBy(this.queue, 'score');
return this.queue.pop() as ProfileCacheQueueEntry;
}
calculateScore(e: ProfileCacheQueueEntry): number {
return this.characterProfiler ? this.characterProfiler.calculateInterestScoreForQueueEntry(e) : 0;
}
start(): void {
this.stop();
EventBus.$on(
'character-data',
(data: CharacterDataEvent) => {
this.addProfile(data.character);
}
);
EventBus.$on(
'channel-message',
(data: ChannelMessageEvent) => {
const message = data.message;
const channel = data.channel;
this.channelConversationCache.register(
{
name: message.sender.name,
channelName : channel.name,
datePosted: message.time,
message: message.text
}
);
this.addProfile(message.sender.name);
}
);
EventBus.$on(
'channel-ad',
(data: ChannelAdEvent) => {
const message = data.message;
const channel = data.channel;
this.adCache.register(
{
name: message.sender.name,
channelName : channel.name,
datePosted: message.time,
message: message.text
}
);
this.addProfile(message.sender.name);
}
);
// EventBus.$on(
// 'private-message',
// (data: any) => {}
// );
const scheduleNextFetch = () => {
this.profileTimer = setTimeout(
async() => {
const next = this.consumeNextInQueue();
if (next) {
// console.log('Learn fetch', next.name, next.score);
await this.fetchProfile(next.name);
}
scheduleNextFetch();
},
CacheManager.PROFILE_QUERY_DELAY
);
};
scheduleNextFetch();
}
stop(): void {
if (this.profileTimer) {
clearTimeout(this.profileTimer);
this.profileTimer = null;
}
// should do some $off here
}
setProfile(c: Character): void {
this.characterProfiler = new CharacterProfiler(c, this.adCache);
}
}

29
learn/cache.ts Normal file
View File

@ -0,0 +1,29 @@
interface CacheCollection<RecordType> {
[key: string]: RecordType
}
export abstract class Cache<RecordType> {
protected cache: CacheCollection<RecordType> = {};
get(name: string): RecordType | null {
const key = Cache.nameKey(name);
if (key in this.cache) {
return this.cache[key];
}
return null;
}
// tslint:disable-next-line: no-any
abstract register(record: any): void;
has(name: string): boolean {
return (name in this.cache);
}
static nameKey(name: string): string {
return name.toLowerCase();
}
}

View File

@ -0,0 +1,32 @@
import { Cache } from './cache';
import { AdCachedPosting, AdCacheRecord, AdCache } from './ad-cache';
export interface ChannelCachedPosting extends AdCachedPosting {
channelName: string;
datePosted: Date;
message: string;
}
export interface ChannelPosting extends ChannelCachedPosting {
name: string;
}
export class ChannelCacheRecord extends AdCacheRecord {}
export class ChannelConversationCache extends AdCache<ChannelCacheRecord> {
register(ad: ChannelPosting): void {
const k = Cache.nameKey(ad.name);
if (k in this.cache) {
const adh = this.cache[k];
adh.add(ad);
return;
}
this.cache[k] = new ChannelCacheRecord(name, ad);
}
}

View File

@ -0,0 +1,96 @@
import core from '../chat/core';
import { Character as CharacterFChatInf } from '../fchat';
import { Character } from '../site/character_page/interfaces';
import { Matcher } from '../site/character_page/matcher';
import { AdCache } from './ad-cache';
import { ProfileCacheQueueEntry } from './cache-manager';
export class CharacterProfiler {
static readonly ADVERTISEMENT_RECENT_RANGE = 22 * 60 * 1000;
static readonly ADVERTISEMENT_POTENTIAL_RAGE = 50 * 60 * 1000;
protected adCache: AdCache;
protected me: Character;
constructor(me: Character, adCache: AdCache) {
this.me = me;
this.adCache = adCache;
}
calculateInterestScoreForQueueEntry(entry: ProfileCacheQueueEntry): number {
const c = core.characters.get(entry.name);
if (!c)
return 0;
const genderScore = this.getInterestScoreForGender(this.me, c);
const statusScore = this.getInterestScoreForStatus(c);
const adScore = (genderScore > 0) ? this.getLastAdvertisementStatus(c) : 0;
const friendlyScore = this.getInterestScoreForFriendlies(c);
// tslint:disable-next-line: number-literal-format binary-expression-operand-order
return ((1.0 * genderScore) + (1.0 * statusScore) + (1.0 * adScore) + (1.0 * friendlyScore));
}
getInterestScoreForFriendlies(c: CharacterFChatInf.Character): number {
if(c.isFriend)
return 1;
if(c.isBookmarked)
return 0.5;
if(c.isIgnored)
return -1;
return 0;
}
getInterestScoreForGender(me: Character, c: CharacterFChatInf.Character): number {
const g = Matcher.strToGender(c.gender);
if (g === null) {
return 0;
}
const score = Matcher.scoreOrientationByGender(me.character, g);
return score.score;
}
getInterestScoreForStatus(c: CharacterFChatInf.Character): number {
if ((c.status === 'offline') || (c.status === 'away') || (c.status === 'busy') || (c.status === 'dnd'))
return -0.5;
if (c.status === 'looking')
return 0.5;
return 0;
}
getLastAdvertisementStatus(c: CharacterFChatInf.Character): number {
const ads = this.adCache.get(c.name);
if (!ads)
return 0;
const lastPost = ads.getDateLastPosted();
if (lastPost === null)
return 0;
const delta = Date.now() - lastPost.getTime();
if (delta < CharacterProfiler.ADVERTISEMENT_RECENT_RANGE)
return 1;
if (delta < CharacterProfiler.ADVERTISEMENT_POTENTIAL_RAGE)
return 0.5;
return -0.5; // has been advertising, but not recently, so likely busy
}
}

View File

71
learn/profile-cache.ts Normal file
View File

@ -0,0 +1,71 @@
import * as _ from 'lodash';
import core from '../chat/core';
import { Character } from '../site/character_page/interfaces';
import { Matcher, Score, Scoring } from '../site/character_page/matcher';
import { Cache } from './cache';
export interface CharacterCacheRecord {
character: Character;
lastFetched: Date;
added: Date;
matchScore: number;
}
export class ProfileCache extends Cache<CharacterCacheRecord> {
register(c: Character): CharacterCacheRecord {
const k = Cache.nameKey(c.character.name);
const score = ProfileCache.score(c);
if (k in this.cache) {
const rExisting = this.cache[k];
rExisting.character = c;
rExisting.lastFetched = new Date();
rExisting.matchScore = score;
return rExisting;
}
const rNew = {
character: c,
lastFetched: new Date(),
added: new Date(),
matchScore: score
};
this.cache[k] = rNew;
return rNew;
}
static score(c: Character): number {
const you = core.characters.ownProfile;
const m = Matcher.generateReport(you.character, c.character);
// let mul = Math.sign(Math.min(m.you.total, m.them.total));
// if (mul === 0)
// mul = 0.5;
// const score = Math.min(m.them.total, m.you.total); // mul * (Math.abs(m.you.total) + Math.abs(m.them.total));
const finalScore = _.reduce(
_.concat(_.values(m.them.scores), _.values(m.you.scores)),
(accum: Scoring | null, score: Score) => {
if (accum === null) {
return (score.score !== Scoring.NEUTRAL) ? score.score : null;
}
return (score.score === Scoring.NEUTRAL) ? accum : Math.min(accum, score.score);
},
null
);
// console.log('Profile score', c.character.name, score, m.you.total, m.them.total,
// m.you.total + m.them.total, m.you.total * m.them.total);
return (finalScore === null) ? Scoring.NEUTRAL : finalScore;
}
}

View File

@ -30,6 +30,8 @@ This repository contains a modified version of the mainline F-Chat 3.0 client.
* Improvements to log browsing
* Highlight ads from characters most interesting to you
* Fix broken BBCode, such as `[big]` in character profiles
* Ad cache, so you can find your chat partners ads easily (channel names too)
* Which channels my chart partner is on?
# F-List Exported

View File

@ -90,6 +90,8 @@
import MatchReportView from './match-report.vue';
const CHARACTER_CACHE_EXPIRE = 4 * 60 * 60 * 1000;
interface ShowableVueTab extends Vue {
show?(): void
}
@ -252,26 +254,49 @@
protected async loadSelfCharacter(): Promise<void> {
// console.log('SELF');
const ownChar = core.characters.ownCharacter;
// const ownChar = core.characters.ownCharacter;
this.selfCharacter = await methods.characterData(ownChar.name, -1);
// this.selfCharacter = await methods.characterData(ownChar.name, -1);
this.selfCharacter = core.characters.ownProfile;
// console.log('SELF LOADED');
this.updateMatches();
}
private async fetchCharacter(): Promise<Character> {
if (!this.name) {
throw new Error('A man must have a name');
}
// tslint:disable-next-line: await-promise
const cachedCharacter = await core.cache.profileCache.get(this.name);
if (cachedCharacter) {
if (Date.now() - cachedCharacter.lastFetched.getTime() <= CHARACTER_CACHE_EXPIRE) {
return cachedCharacter.character;
}
}
return methods.characterData(this.name, this.characterid, false);
}
private async _getCharacter(): Promise<void> {
this.character = undefined;
this.friendCount = null;
this.groupCount = null;
this.guestbookPostCount = null;
this.character = await methods.characterData(this.name, this.characterid);
if (!this.name) {
return;
}
this.character = await this.fetchCharacter();
standardParser.allowInlines = true;
standardParser.inlines = this.character.character.inlines;
console.log('LoadChar', this.name, this.character);
// console.log('LoadChar', this.name, this.character);
this.updateMatches();
@ -294,7 +319,7 @@
this.characterMatch = Matcher.generateReport(this.selfCharacter.character, this.character.character);
console.log('Match', this.selfCharacter.character.name, this.character.character.name, this.characterMatch);
// console.log('Match', this.selfCharacter.character.name, this.character.character.name, this.characterMatch);
}
}
</script>

View File

@ -22,7 +22,7 @@ export interface StoreMethods {
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>
characterData(name: string | undefined, id: number | undefined, skipEvent: boolean | undefined): Promise<Character>
characterDelete(id: number): Promise<void>
characterDuplicate(id: number, name: string): Promise<DuplicateResult>
characterFriends(id: number): Promise<FriendsByCharacter>

View File

@ -178,6 +178,22 @@ const speciesMapping: SpeciesMap = {
[Species.Minotaur]: ['minotaur']
};
interface FchatGenderMap {
[key: string]: Gender;
}
const fchatGenderMap: FchatGenderMap = {
None: Gender.None,
Male: Gender.Male,
Female: Gender.Female,
Shemale: Gender.Shemale,
Herm: Gender.Herm,
'Male-Herm': Gender.MaleHerm,
'Cunt-boy': Gender.Cuntboy,
Transgender: Gender.Transgender
};
interface KinkPreferenceMap {
[key: string]: KinkPreference;
}
@ -214,6 +230,7 @@ export interface MatchResult {
them: Character,
scores: MatchResultScores;
info: MatchResultCharacterInfo;
total: number
}
export enum Scoring {
@ -249,7 +266,11 @@ export class Score {
}
getRecommendedClass(): string {
return scoreClasses[this.score];
return Score.getClasses(this.score);
}
static getClasses(score: Scoring): string {
return scoreClasses[score];
}
}
@ -279,10 +300,12 @@ export class Matcher {
};
}
match(): MatchResult {
return {
const data = {
you: this.you,
them: this.them,
total: 0,
scores: {
[TagId.Orientation]: this.resolveOrientationScore(),
@ -297,7 +320,15 @@ export class Matcher {
gender: Matcher.getTagValueList(TagId.Gender, this.you),
orientation: Matcher.getTagValueList(TagId.Orientation, this.you)
}
};
};
data.total = _.reduce(
data.scores,
(accum: number, s: Score) => (accum + s.score),
0
);
return data;
}
private resolveOrientationScore(): Score {
@ -313,9 +344,17 @@ export class Matcher {
// Question: If someone identifies themselves as 'straight cuntboy', how should they be matched? like a straight female?
return Matcher.scoreOrientationByGender(you, theirGender);
}
static scoreOrientationByGender(you: Character, theirGender: Gender): Score {
const yourGender = Matcher.getTagValueList(TagId.Gender, you);
const yourOrientation = Matcher.getTagValueList(TagId.Orientation, you);
// CIS
// tslint:disable-next-line curly
if (Matcher.isCisGender(yourGender)) {
if ((yourGender !== null) && (Matcher.isCisGender(yourGender))) {
if (yourGender === theirGender) {
// same sex CIS
if (yourOrientation === Orientation.Straight)
@ -359,7 +398,6 @@ export class Matcher {
}
}
// Can't do anything with Gender.None
return new Score(Scoring.NEUTRAL);
}
@ -662,4 +700,17 @@ export class Matcher {
// tslint:disable-next-line: strict-type-predicates
return (foundSpeciesId === null) ? null : parseInt(foundSpeciesId, 10);
}
static strToGender(fchatGenderStr: string | undefined): Gender | null {
if (fchatGenderStr === undefined) {
return null;
}
if (fchatGenderStr in fchatGenderMap) {
return fchatGenderMap[fchatGenderStr];
}
return null;
}
}