diff --git a/chat/ConversationView.vue b/chat/ConversationView.vue
index 8a12ec7..0bdcc20 100644
--- a/chat/ConversationView.vue
+++ b/chat/ConversationView.vue
@@ -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>
\ No newline at end of file
diff --git a/chat/ImagePreview.vue b/chat/ImagePreview.vue
index 7220c42..57ffef9 100644
--- a/chat/ImagePreview.vue
+++ b/chat/ImagePreview.vue
@@ -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>
 
diff --git a/chat/common.ts b/chat/common.ts
index 402ce43..1674b03 100644
--- a/chat/common.ts
+++ b/chat/common.ts
@@ -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
diff --git a/chat/conversations.ts b/chat/conversations.ts
index a47686a..74fc1fe 100644
--- a/chat/conversations.ts
+++ b/chat/conversations.ts
@@ -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);
diff --git a/chat/core.ts b/chat/core.ts
index 13a581b..4b3827b 100644
--- a/chat/core.ts
+++ b/chat/core.ts
@@ -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
diff --git a/chat/event-bus.ts b/chat/event-bus.ts
index 29a1426..5142b74 100644
--- a/chat/event-bus.ts
+++ b/chat/event-bus.ts
@@ -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();
 
diff --git a/chat/image-preview-mutator.ts b/chat/image-preview-mutator.ts
index 840c201..167f866 100644
--- a/chat/image-preview-mutator.ts
+++ b/chat/image-preview-mutator.ts
@@ -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(); }
+            `
+        );
+    }
 }
diff --git a/chat/interfaces.ts b/chat/interfaces.ts
index e3bc650..d16c2f9 100644
--- a/chat/interfaces.ts
+++ b/chat/interfaces.ts
@@ -13,6 +13,8 @@ export namespace Conversation {
         readonly type: Message.Type
         readonly text: string
         readonly time: Date
+
+        score?: number;
     }
 
     export interface EventMessage extends BaseMessage {
diff --git a/chat/message_view.ts b/chat/message_view.ts
index add358d..c14fb0c 100644
--- a/chat/message_view.ts
+++ b/chat/message_view.ts
@@ -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}`;
+    }
+
 }
\ No newline at end of file
diff --git a/chat/profile_api.ts b/chat/profile_api.ts
index abd6abe..b663839 100644
--- a/chat/profile_api.ts
+++ b/chat/profile_api.ts
@@ -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;
 }
diff --git a/fchat/characters.ts b/fchat/characters.ts
index eafc0c8..8abb9df 100644
--- a/fchat/characters.ts
+++ b/fchat/characters.ts
@@ -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, '');
diff --git a/fchat/interfaces.ts b/fchat/interfaces.ts
index d6e3cf4..c7b6d43 100644
--- a/fchat/interfaces.ts
+++ b/fchat/interfaces.ts
@@ -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
     }
 
diff --git a/learn/ad-cache.ts b/learn/ad-cache.ts
new file mode 100644
index 0000000..fe9fc73
--- /dev/null
+++ b/learn/ad-cache.ts
@@ -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;
+    }
+}
diff --git a/learn/cache-manager.ts b/learn/cache-manager.ts
new file mode 100644
index 0000000..0c18262
--- /dev/null
+++ b/learn/cache-manager.ts
@@ -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);
+    }
+}
diff --git a/learn/cache.ts b/learn/cache.ts
new file mode 100644
index 0000000..c364474
--- /dev/null
+++ b/learn/cache.ts
@@ -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();
+    }
+}
diff --git a/learn/channel-conversation-cache.ts b/learn/channel-conversation-cache.ts
new file mode 100644
index 0000000..8f8a19c
--- /dev/null
+++ b/learn/channel-conversation-cache.ts
@@ -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);
+    }
+
+}
diff --git a/learn/character-profiler.ts b/learn/character-profiler.ts
new file mode 100644
index 0000000..9f9f63a
--- /dev/null
+++ b/learn/character-profiler.ts
@@ -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
+    }
+}
\ No newline at end of file
diff --git a/learn/personal-profiler.ts b/learn/personal-profiler.ts
new file mode 100644
index 0000000..e69de29
diff --git a/learn/profile-cache.ts b/learn/profile-cache.ts
new file mode 100644
index 0000000..1a0655b
--- /dev/null
+++ b/learn/profile-cache.ts
@@ -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;
+    }
+}
diff --git a/readme.md b/readme.md
index 111474a..d72809b 100644
--- a/readme.md
+++ b/readme.md
@@ -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
diff --git a/site/character_page/character_page.vue b/site/character_page/character_page.vue
index 456f958..ede50db 100644
--- a/site/character_page/character_page.vue
+++ b/site/character_page/character_page.vue
@@ -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>
diff --git a/site/character_page/interfaces.ts b/site/character_page/interfaces.ts
index a53f5ef..26536c8 100644
--- a/site/character_page/interfaces.ts
+++ b/site/character_page/interfaces.ts
@@ -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>
diff --git a/site/character_page/matcher.ts b/site/character_page/matcher.ts
index d37c552..518068b 100644
--- a/site/character_page/matcher.ts
+++ b/site/character_page/matcher.ts
@@ -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;
+    }
 }