Highlight ad matches
This commit is contained in:
parent
6671fd9374
commit
b0129c75cb
|
@ -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>
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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(); }
|
||||
`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -13,6 +13,8 @@ export namespace Conversation {
|
|||
readonly type: Message.Type
|
||||
readonly text: string
|
||||
readonly time: Date
|
||||
|
||||
score?: number;
|
||||
}
|
||||
|
||||
export interface EventMessage extends BaseMessage {
|
||||
|
|
|
@ -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}`;
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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, '');
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue