diff --git a/CHANGELOG.md b/CHANGELOG.md
index 06e58e1..e11314f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,7 +1,8 @@
 # Changelog
 
 ## Canary
-*   More conifugrable settings for F-Chat Rising
+*   More configurable settings for F-Chat Rising
+*   Hover mouse over a character name to see a character preview
 
 
 ## 1.3.0
diff --git a/bbcode/UrlTagView.vue b/bbcode/UrlTagView.vue
index fa4465e..29f5d46 100644
--- a/bbcode/UrlTagView.vue
+++ b/bbcode/UrlTagView.vue
@@ -55,7 +55,6 @@
             EventBus.$emit('imagepreview-show', {url: this.url});
         }
 
-
         toggleStickyness(): void {
             EventBus.$emit('imagepreview-toggle-stickyness', {url: this.url});
         }
diff --git a/chat/UserMenu.vue b/chat/UserMenu.vue
index d2821c2..be06699 100644
--- a/chat/UserMenu.vue
+++ b/chat/UserMenu.vue
@@ -10,9 +10,7 @@
             <bbcode id="userMenuStatus" :text="character.statusText" v-show="character.statusText" class="list-group-item"
                 style="max-height:200px;overflow:auto;clear:both"></bbcode>
 
-            <div v-if="match" class="list-group-item menu-character-score">
-              <span v-for="(score, key) in match" :class="score.getRecommendedClass()"><i :class="score.getRecommendedIcon()"></i> {{getTagDesc(key)}}</span>
-            </div>
+            <match-tags v-if="match" :match="match" class="list-group-item"></match-tags>
 
             <a tabindex="-1" :href="profileLink" target="_blank" v-if="showProfileFirst" class="list-group-item list-group-item-action">
                 <span class="fa fa-fw fa-user"></span>{{l('user.profile')}}</a>
@@ -59,12 +57,12 @@ import core from './core';
 import { Channel, Character } from './interfaces';
 import l from './localize';
 import ReportDialog from './ReportDialog.vue';
-import { Matcher, MatchResultScores } from '../learn/matcher';
-import { TagId } from '../learn/matcher-types';
+import { Matcher, MatchReport } from '../learn/matcher';
 import _ from 'lodash';
+import MatchTags from './preview/MatchTags.vue';
 
 @Component({
-        components: {bbcode: BBCodeView(core.bbCodeParser), modal: Modal, 'ad-view': CharacterAdView}
+        components: {'match-tags': MatchTags, bbcode: BBCodeView(core.bbCodeParser), modal: Modal, 'ad-view': CharacterAdView}
     })
     export default class UserMenu extends Vue {
         @Prop({required: true})
@@ -80,7 +78,7 @@ import _ from 'lodash';
         memo = '';
         memoId = 0;
         memoLoading = false;
-        match: MatchResultScores | null = null;
+        match: MatchReport | null = null;
 
         openConversation(jump: boolean): void {
             const conversation = core.conversations.getPrivate(this.character!);
@@ -226,9 +224,6 @@ import _ from 'lodash';
             this.showContextMenu = false;
         }
 
-        getTagDesc(key: any): any {
-          return TagId[key].toString().replace(/([A-Z])/g, ' $1').trim();
-        }
 
         private async openMenu(touch: MouseEvent | Touch, character: Character, channel: Channel | undefined): Promise<void> {
             this.channel = channel;
@@ -246,7 +241,7 @@ import _ from 'lodash';
                 const match = Matcher.identifyBestMatchReport(myProfile.character, theirProfile.character.character);
 
                 if (_.keys(match.merged).length > 0) {
-                  this.match = match.merged;
+                  this.match = match;
                 }
               }
             }
@@ -272,44 +267,6 @@ import _ from 'lodash';
         border-top-width: 0;
         z-index: -1;
     }
-
-    #userMenu {
-      .menu-character-score {
-        span {
-          padding-left: 3px;
-          padding-right: 3px;
-          margin-bottom: 3px;
-          margin-right: 3px;
-          display: inline-block;
-          border: 1px solid;
-          border-radius: 3px;
-
-          i {
-            color: white;
-          }
-
-          &.match {
-            background-color: var(--scoreMatchBg);
-            border: solid 1px var(--scoreMatchFg);
-          }
-
-          &.weak-match {
-            background-color: var(--scoreWeakMatchBg);
-            border: 1px solid var(--scoreWeakMatchFg);
-          }
-
-          &.weak-mismatch {
-            background-color: var(--scoreWeakMismatchBg);
-            border: 1px solid var(--scoreWeakMismatchFg);
-          }
-
-          &.mismatch {
-            background-color: var(--scoreMismatchBg);
-            border: 1px solid var(--scoreMismatchFg);
-          }
-        }
-      }
-    }
 </style>
 
 
diff --git a/chat/UserView.vue b/chat/UserView.vue
index 14aa25e..511b8fb 100644
--- a/chat/UserView.vue
+++ b/chat/UserView.vue
@@ -1,5 +1,5 @@
 <!-- Linebreaks inside this template will break BBCode views -->
-<template><span :class="userClass" v-bind:bbcodeTag.prop="'user'" v-bind:character.prop="character" v-bind:channel.prop="channel"><span v-if="!!statusClass" :class="statusClass"></span><span v-if="!!rankIcon" :class="rankIcon"></span>{{character.name}}<span v-if="!!matchClass" :class="matchClass">{{getMatchScoreTitle(matchScore)}}</span></span></template>
+<template><span :class="userClass" v-bind:bbcodeTag.prop="'user'" v-bind:character.prop="character" v-bind:channel.prop="channel" @mouseover.prevent="show()" @mouseenter.prevent="show()" @mouseleave.prevent="dismiss()" @mouseout.prevent="dismiss()" @click.middle.prevent="toggleStickyness()" @click.right.passive="dismiss(true)" @click.left.passive="dismiss(true)"><span v-if="!!statusClass" :class="statusClass"></span><span v-if="!!rankIcon" :class="rankIcon"></span>{{character.name}}<span v-if="!!matchClass" :class="matchClass">{{getMatchScoreTitle(matchScore)}}</span></span></template>
 
 
 <script lang="ts">
@@ -33,6 +33,70 @@ export function getStatusIcon(status: Character.Status): string {
 }
 
 
+export interface StatusClasses {
+  rankIcon: string | null;
+  statusClass: string | null;
+  matchClass: string | null;
+  matchScore: number | null;
+  userClass: string;
+  isBookmark: boolean;
+}
+
+export function getStatusClasses(
+  character: Character,
+  channel: Channel | undefined,
+  showStatus: boolean,
+  showBookmark: boolean,
+  showMatch: boolean
+): StatusClasses {
+    let rankIcon: string | null = null;
+    let statusClass = null;
+    let matchClass = null;
+    let matchScore = null;
+
+    if(character.isChatOp) {
+        rankIcon = 'far fa-gem';
+    } else if(channel !== undefined) {
+        rankIcon = (channel.owner === character.name)
+            ? 'fa fa-key'
+            : channel.opList.indexOf(character.name) !== -1
+                ? (channel.id.substr(0, 4) === 'adh-' ? 'fa fa-shield-alt' : 'fa fa-star')
+                : null;
+    }
+
+    if ((showStatus) || (character.status === 'crown'))
+        statusClass = `fa-fw ${getStatusIcon(character.status)}`;
+
+    if ((core.state.settings.risingAdScore) && (showMatch)) {
+        const cache = core.cache.profileCache.getSync(character.name);
+
+        if (cache) {
+            matchClass = `match-found ${Score.getClasses(cache.matchScore)}`;
+            matchScore = cache.matchScore;
+        } else {
+            /* tslint:disable-next-line no-floating-promises */
+            core.cache.addProfile(character.name);
+        }
+    }
+
+    const gender = character.gender !== undefined ? character.gender.toLowerCase() : 'none';
+
+    const isBookmark = (showBookmark) && (core.connection.isOpen) && (core.state.settings.colorBookmarks) &&
+        ((character.isFriend) || (character.isBookmarked));
+
+    const userClass = `user-view gender-${gender}${isBookmark ? ' user-bookmark' : ''}`;
+
+    return {
+      rankIcon,
+      statusClass,
+      matchClass,
+      matchScore,
+      userClass,
+      isBookmark
+    };
+}
+
+
 @Component({
     components: {
 
@@ -54,6 +118,9 @@ export default class UserView extends Vue {
     @Prop()
     readonly match?: boolean = false;
 
+    @Prop({default: true})
+    readonly preview: boolean = true;
+
     userClass = '';
 
     rankIcon: string | null = null;
@@ -100,8 +167,14 @@ export default class UserView extends Vue {
     onBeforeDestroy(): void {
         if (this.scoreWatcher)
             EventBus.$off('character-score', this.scoreWatcher);
+
+        this.dismiss();
     }
 
+    @Hook('deactivated')
+    deactivate(): void {
+        this.dismiss();
+    }
 
     @Hook('beforeUpdate')
     onBeforeUpdate(): void {
@@ -114,55 +187,13 @@ export default class UserView extends Vue {
     }
 
     update(): void {
-        this.rankIcon = null;
-        this.statusClass = null;
-        this.matchClass = null;
-        this.matchScore = null;
+      const res = getStatusClasses(this.character, this.channel, !!this.showStatus, !!this.bookmark, !!this.match);
 
-        // if (this.match) console.log('Update', this.character.name);
-
-        if(this.character.isChatOp) {
-            this.rankIcon = 'far fa-gem';
-        } else if(this.channel !== undefined) {
-            this.rankIcon = (this.channel.owner === this.character.name)
-                ? 'fa fa-key'
-                : this.channel.opList.indexOf(this.character.name) !== -1
-                    ? (this.channel.id.substr(0, 4) === 'adh-' ? 'fa fa-shield-alt' : 'fa fa-star')
-                    : null;
-        }
-
-        if ((this.showStatus) || (this.character.status === 'crown'))
-            this.statusClass = `fa-fw ${getStatusIcon(this.character.status)}`;
-
-        // if (this.match) console.log('Update prematch', this.character.name);
-
-        if ((core.state.settings.risingAdScore) && (this.match)) {
-            const cache = core.cache.profileCache.getSync(this.character.name);
-
-            if (cache) {
-                this.matchClass = `match-found ${Score.getClasses(cache.matchScore)}`;
-                this.matchScore = cache.matchScore;
-
-                // console.log('Found match data', this.character.name, cache.matchScore);
-            } else {
-                // console.log('Need match data', this.character.name);
-
-                /* tslint:disable-next-line no-floating-promises */
-                core.cache.addProfile(this.character.name);
-            }
-        }
-
-        // if (this.match) console.log('Update post match', this.character.name);
-
-        const gender = this.character.gender !== undefined ? this.character.gender.toLowerCase() : 'none';
-
-        const isBookmark = (this.bookmark) && (core.connection.isOpen) && (core.state.settings.colorBookmarks) &&
-            ((this.character.isFriend) || (this.character.isBookmarked));
-
-
-        this.userClass = `user-view gender-${gender}${isBookmark ? ' user-bookmark' : ''}`;
-
-        // if (this.match) console.log('Update done');
+      this.rankIcon = res.rankIcon;
+      this.statusClass = res.statusClass;
+      this.matchClass = res.matchClass;
+      this.matchScore = res.matchScore;
+      this.userClass = res.userClass;
     }
 
 
@@ -183,47 +214,37 @@ export default class UserView extends Vue {
 
         return '';
     }
-}
 
-//tslint:disable-next-line:variable-name
-/* const UserView = Vue.extend({
-    functional: true,
-    render(this: void | Vue, createElement: CreateElement, context?: RenderContext): VNode {
-        const props = <{character: Character, channel?: Channel, showStatus?: true, bookmark?: false, match?: false}>(
-            context !== undefined ? context.props : (<Vue>this).$options.propsData);
 
-        const character = props.character;
+    getCharacterUrl(): string {
+      return `flist-character://${this.character.name}`;
+    }
 
-        let matchClasses: string | undefined;
 
-        if (props.match) {
-            const cache = core.cache.profileCache.getSync(character.name);
-
-            if (cache) {
-                matchClasses = Score.getClasses(cache.matchScore);
-            }
+    dismiss(force: boolean = false): void {
+        if (!this.preview) {
+          return;
         }
 
-        let rankIcon;
-        if(character.isChatOp) rankIcon = 'far fa-gem';
-        else if(props.channel !== undefined)
-            rankIcon = props.channel.owner === character.name ? 'fa fa-key' : props.channel.opList.indexOf(character.name) !== -1 ?
-                (props.channel.id.substr(0, 4) === 'adh-' ? 'fa fa-shield-alt' : 'fa fa-star') : '';
-        else rankIcon = '';
-        const children: (VNode | string)[] = [character.name];
-        if(rankIcon !== '') children.unshift(createElement('span', {staticClass: rankIcon}));
-        if(props.showStatus !== undefined || character.status === 'crown')
-            children.unshift(createElement('span', {staticClass: `fa-fw ${getStatusIcon(character.status)}`}));
-        const gender = character.gender !== undefined ? character.gender.toLowerCase() : 'none';
-        const isBookmark = props.bookmark !== false && core.connection.isOpen && core.state.settings.colorBookmarks &&
-            (character.isFriend || character.isBookmarked);
-        return createElement('span', {
-            attrs: {class: `user-view gender-${gender}${isBookmark ? ' user-bookmark' : ''} ${matchClasses}`},
-            domProps: {character, channel: props.channel, bbcodeTag: 'user'}
-        }, children);
+        EventBus.$emit('imagepreview-dismiss', {url: this.getCharacterUrl(), force});
     }
-});
 
-export default UserView;
-*/
+
+    show(): void {
+        if (!this.preview) {
+          return;
+        }
+
+        EventBus.$emit('imagepreview-show', {url: this.getCharacterUrl()});
+    }
+
+
+    toggleStickyness(): void {
+        if (!this.preview) {
+          return;
+        }
+
+        EventBus.$emit('imagepreview-toggle-stickyness', {url: this.getCharacterUrl()});
+    }
+}
 </script>
diff --git a/chat/preview/CharacterPreview.vue b/chat/preview/CharacterPreview.vue
new file mode 100644
index 0000000..6c693a9
--- /dev/null
+++ b/chat/preview/CharacterPreview.vue
@@ -0,0 +1,290 @@
+<template>
+  <div class="character-preview">
+    <div v-if="match && character" class="row">
+      <div class="col-2">
+        <img :src="avatarUrl(character.character.name)" class="character-avatar">
+      </div>
+
+      <div class="col-8">
+        <h1><span class="character-name" :class="(statusClasses || {}).userClass">{{ character.character.name }}</span></h1>
+        <h3>{{ getOnlineStatus() }}</h3>
+
+        <div class="summary">
+          <span class="uc">
+            <span v-if="age" :class="byScore(TagId.Age)">{{age}}-years-old </span>
+            <span v-if="sexualOrientation" :class="byScore(TagId.Orientation)">{{sexualOrientation}} </span>
+            <span v-if="gender" :class="byScore(TagId.Gender)">{{gender}} </span>
+            <span v-if="species" :class="byScore(TagId.Species)">{{species}} </span>
+          </span>
+
+          <span v-if="furryPref" :class="byScore(TagId.FurryPreference)"><br /><span class="uc">{{furryPref}}</span></span>
+          <span v-if="subDomRole" :class="byScore(TagId.SubDomRole)"><br /><span class="uc">{{subDomRole}}</span></span>
+        </div>
+
+        <match-tags v-if="match" :match="match"></match-tags>
+
+        <div v-if="latestAd">
+          <h4>Latest Ad <span class="message-time">{{formatTime(latestAd.datePosted)}}</span></h4>
+          <bbcode :text="latestAd.message"></bbcode>
+        </div>
+      </div>
+    </div>
+    <div v-else>
+      Loading...
+    </div>
+  </div>
+</template>
+
+<script lang="ts">
+import { Component, Prop } from '@f-list/vue-ts';
+import Vue from 'vue';
+import core from '../core';
+import { methods } from '../../site/character_page/data_store';
+import {Character as ComplexCharacter} from '../../site/character_page/interfaces';
+import { Matcher, MatchReport } from '../../learn/matcher';
+import { Character as CharacterStatus } from '../../fchat';
+import { getStatusClasses, StatusClasses } from '../UserView.vue';
+import * as _ from 'lodash';
+import { AdCachedPosting } from '../../learn/ad-cache';
+import {formatTime} from '../common';
+import * as Utils from '../../site/utils';
+import MatchTags from './MatchTags.vue';
+import {
+  furryPreferenceMapping,
+  Gender,
+  Orientation,
+  Species,
+  SubDomRole,
+  TagId
+} from '../../learn/matcher-types';
+import { BBCodeView } from '../../bbcode/view';
+
+
+@Component({
+    components: {
+      'match-tags': MatchTags,
+      bbcode: BBCodeView(core.bbCodeParser)
+    }
+})
+export default class CharacterPreview extends Vue {
+  @Prop
+  readonly id?: number;
+
+  characterName?: string;
+  character?: ComplexCharacter;
+  match?: MatchReport;
+  ownCharacter?: ComplexCharacter;
+  onlineCharacter?: CharacterStatus;
+  statusClasses?: StatusClasses;
+  latestAd?: AdCachedPosting;
+
+  age?: string;
+  sexualOrientation?: string;
+  species?: string;
+  gender?: string;
+  furryPref?: string;
+  subDomRole?: string;
+
+  formatTime = formatTime;
+  readonly avatarUrl = Utils.avatarURL;
+  TagId = TagId;
+
+  async load(characterName: string): Promise<void> {
+    if (
+      (this.characterName === characterName)
+      && (this.match)
+      && (this.character)
+      && (this.ownCharacter)
+      && (this.ownCharacter.character.name === core.characters.ownProfile.character.name)
+    ) {
+      this.updateOnlineStatus();
+      this.updateAdStatus();
+      return;
+    }
+
+    this.characterName = characterName;
+
+    this.match = undefined;
+    this.character = undefined;
+    this.ownCharacter = core.characters.ownProfile;
+
+    this.updateOnlineStatus();
+    this.updateAdStatus();
+
+    this.character = await this.getCharacterData(characterName);
+    this.match = Matcher.identifyBestMatchReport(this.ownCharacter.character, this.character.character);
+
+    this.updateDetails();
+  }
+
+
+  updateOnlineStatus(): void {
+    this.onlineCharacter = core.characters.get(this.characterName!);
+
+    if (!this.onlineCharacter) {
+      this.statusClasses = undefined;
+      return;
+    }
+
+    this.statusClasses = getStatusClasses(this.onlineCharacter, undefined, true, true, false);
+  }
+
+
+  updateAdStatus(): void {
+    const cache = core.cache.adCache.get(this.characterName!);
+
+    if ((!cache) || (cache.posts.length === 0)) {
+      this.latestAd = undefined;
+      return;
+    }
+
+    this.latestAd = cache.posts[cache.posts.length - 1];
+  }
+
+
+  updateDetails(): void {
+    if (!this.match) {
+      this.age = undefined;
+      this.species = undefined;
+      this.gender = undefined;
+      this.furryPref = undefined;
+      this.subDomRole = undefined;
+      this.sexualOrientation = undefined;
+      return;
+    }
+
+    const a = this.match.them.yourAnalysis;
+    const c = this.match.them.you;
+
+    const rawSpecies = Matcher.getTagValue(TagId.Species, c);
+    const rawAge = Matcher.getTagValue(TagId.Age, c);
+
+    if ((a.species) && (!Species[a.species])) {
+      console.log('SPECIES', a.species, rawSpecies);
+    }
+
+    this.age = a.age ? this.readable(`${a.age}`) : (rawAge && rawAge.string) || undefined;
+    this.species = a.species ? this.readable(Species[a.species]) : (rawSpecies && rawSpecies.string) || undefined;
+    this.gender = a.gender ? this.readable(Gender[a.gender]) : undefined;
+    this.furryPref = a.furryPreference ? this.readable(furryPreferenceMapping[a.furryPreference]) : undefined;
+    this.subDomRole = a.subDomRole ? this.readable(SubDomRole[a.subDomRole]) : undefined;
+    this.sexualOrientation = a.orientation ? this.readable(Orientation[a.orientation]) : undefined;
+  }
+
+  readable(s: string): string {
+    return s.replace(/([A-Z])/g, ' $1').trim().toLowerCase()
+      .replace(/(always|usually) (submissive|dominant)/, '$2')
+      .replace(/bi (fe)?male preference/, 'bisexual');
+  }
+
+  byScore(_tagId: any): string {
+    return '';
+
+    // too much
+    // if (!this.match) {
+    //   return '';
+    // }
+    //
+    // const score = this.match.merged[tagId];
+    //
+    // if (!score) {
+    //   return '';
+    // }
+    //
+    // return score.getRecommendedClass();
+  }
+
+
+  getOnlineStatus(): string {
+    if (!this.onlineCharacter) {
+      return 'Offline';
+    }
+
+    const s = this.onlineCharacter.status as string;
+
+    return `${s.substr(0, 1).toUpperCase()}${s.substr(1)}`;
+  }
+
+
+  async getCharacterData(characterName: string): Promise<ComplexCharacter> {
+      const cache = await core.cache.profileCache.get(characterName);
+
+      if (cache) {
+        return cache.character;
+      }
+
+      return methods.characterData(characterName, this.id, false);
+  }
+}
+</script>
+
+<style lang="scss">
+  .character-preview {
+    padding: 10px;
+    background-color: var(--input-bg);
+
+    .summary {
+      font-size: 125%;
+
+      .uc {
+        display: inline-block;
+
+        &::first-letter {
+          text-transform: capitalize;
+        }
+      }
+
+      .match {
+        background-color: var(--scoreMatchBg);
+        border: solid 1px var(--scoreMatchFg);
+      }
+
+      .weak-match {
+        background-color: var(--scoreWeakMatchBg);
+        border: 1px solid var(--scoreWeakMatchFg);
+      }
+
+      .weak-mismatch {
+        background-color: var(--scoreWeakMismatchBg);
+        border: 1px solid var(--scoreWeakMismatchFg);
+      }
+
+      .mismatch {
+        background-color: var(--scoreMismatchBg);
+        border: 1px solid var(--scoreMismatchFg);
+      }
+    }
+
+    .matched-tags {
+      margin-top: 1rem;
+    }
+
+    h1 {
+      line-height: 100%;
+      margin-bottom: 0;
+      font-size: 2em;
+    }
+
+    h3 {
+      font-size: 1.1rem;
+      color: var(--dark);
+    }
+
+    h4 {
+      font-size: 1rem;
+      margin-top: 1rem;
+
+      .message-time {
+        font-size: 80%;
+        font-weight: normal;
+        color: var(--messageTimeFgColor);
+        margin-left: 2px;
+      }
+    }
+
+    .character-avatar {
+      width: 100%;
+      height: auto;
+    }
+  }
+</style>
diff --git a/chat/preview/ImagePreview.vue b/chat/preview/ImagePreview.vue
index 125ac5c..fde29a1 100644
--- a/chat/preview/ImagePreview.vue
+++ b/chat/preview/ImagePreview.vue
@@ -21,15 +21,20 @@
             id="image-preview-ext"
             ref="imagePreviewExt"
             class="image-preview-external"
-            :style="externalPreviewStyle">
+            :style="previewStyles.ExternalImagePreviewHelper">
         </webview>
 
         <div
             class="image-preview-local"
-            :style="localPreviewStyle"
+            :style="previewStyles.LocalImagePreviewHelper"
         >
         </div>
 
+        <character-preview
+            :style="previewStyles.CharacterPreviewHelper"
+            ref="characterPreview"
+        ></character-preview>
+
         <i id="preview-spinner" class="fas fa-circle-notch fa-spin" v-show="shouldShowSpinner"></i>
         <i id="preview-error" class="fas fa-times" v-show="shouldShowError"></i>
     </div>
@@ -44,12 +49,17 @@
     import {domain} from '../../bbcode/core';
     import {ImageDomMutator} from './image-dom-mutator';
 
-    import { ExternalImagePreviewHelper, LocalImagePreviewHelper } from './helper';
+    import {
+      ExternalImagePreviewHelper,
+      LocalImagePreviewHelper,
+      PreviewManager,
+      CharacterPreviewHelper, RenderStyle
+    } from './helper';
 
     import {Point, WebviewTag, remote} from 'electron';
     import Timer = NodeJS.Timer;
     import IpcMessageEvent = Electron.IpcMessageEvent;
-
+    import CharacterPreview from './CharacterPreview.vue';
 
     const screen = remote.screen;
 
@@ -63,15 +73,30 @@
         httpStatusText: string;
     }
 
-
-    @Component
+    @Component({
+        components: {
+          'character-preview': CharacterPreview
+        }
+    })
     export default class ImagePreview extends Vue {
         private readonly MinTimePreviewVisible = 100;
 
         visible = false;
 
-        externalPreviewHelper = new ExternalImagePreviewHelper(this);
-        localPreviewHelper = new LocalImagePreviewHelper(this);
+        previewManager = new PreviewManager(
+          this,
+          [
+            new ExternalImagePreviewHelper(this),
+            new LocalImagePreviewHelper(this),
+            new CharacterPreviewHelper(this)
+            // new ChannelPreviewHelper(this)
+          ]
+        );
+
+        // externalPreviewHelper = new ExternalImagePreviewHelper(this);
+        // localPreviewHelper = new LocalImagePreviewHelper(this);
+        // externalPreviewStyle: Record<string, any> = {};
+        // localPreviewStyle: Record<string, any> = {};
 
         url: string | null = null;
         domain: string | undefined;
@@ -82,15 +107,11 @@
 
         jsMutator = new ImageDomMutator(this.debug);
 
-        externalPreviewStyle: Record<string, any> = {};
-        localPreviewStyle: Record<string, any> = {};
-
         state = 'hidden';
 
         shouldShowSpinner = false;
         shouldShowError = true;
 
-
         private interval: Timer | null = null;
 
         private exitInterval: Timer | null = null;
@@ -100,6 +121,9 @@
         private shouldDismiss = false;
         private visibleSince = 0;
 
+        previewStyles: Record<string, RenderStyle> = {};
+
+
         @Hook('mounted')
         onMounted(): void {
             console.warn('Mounted ImagePreview');
@@ -299,43 +323,33 @@
 
 
         reRenderStyles(): void {
-            // tslint:disable-next-line:no-unsafe-any
-            this.externalPreviewStyle = this.externalPreviewHelper.renderStyle();
-            // tslint:disable-next-line:no-unsafe-any
-            this.localPreviewStyle = this.localPreviewHelper.renderStyle();
-
-            this.debugLog(
-                'ImagePreview: reRenderStyles', 'external',
-                JSON.parse(JSON.stringify(this.externalPreviewStyle)),
-                'local', JSON.parse(JSON.stringify(this.localPreviewStyle))
-            );
+            this.previewStyles = this.previewManager.renderStyles();
         }
 
 
         updatePreviewSize(width: number, height: number): void {
-            if (!this.externalPreviewHelper.isVisible()) {
-                return;
+            const helper = this.previewManager.getVisiblePreview();
+
+            if ((!helper) || (!helper.reactsToSizeUpdates())) {
+              return;
             }
 
             if ((width) && (height)) {
                 this.debugLog('ImagePreview: updatePreviewSize', width, height, width / height);
 
-                this.externalPreviewHelper.setRatio(width / height);
+                helper.setRatio(width / height);
                 this.reRenderStyles();
             }
         }
 
 
         hide(): void {
-            this.debugLog('ImagePreview: hide', this.externalPreviewHelper.isVisible(), this.localPreviewHelper.isVisible());
-
             this.cancelExitTimer();
 
             this.url = null;
             this.visible = false;
 
-            this.localPreviewHelper.hide();
-            this.externalPreviewHelper.hide();
+            this.previewManager.hide();
 
             this.exitUrl = null;
             this.exitInterval = null;
@@ -378,7 +392,7 @@
             if ((!this.hasMouseMovedSince()) && (!force))
                 return;
 
-            this.debugLog('ImagePreview: dismiss.exec', this.externalPreviewHelper.isVisible(), this.localPreviewHelper.isVisible(), url);
+            this.debugLog('ImagePreview: dismiss.exec', this.previewManager.getVisibilityStatus(), url);
 
             // This timeout makes the preview window disappear with a slight delay, which helps UX
             // when dealing with situations such as quickly scrolling text that moves the cursor away
@@ -393,7 +407,7 @@
         show(initialUrl: string): void {
             const url = this.jsMutator.mutateUrl(initialUrl);
 
-            this.debugLog('ImagePreview: show', this.externalPreviewHelper.isVisible(), this.localPreviewHelper.isVisible(),
+            this.debugLog('ImagePreview: show', this.previewManager.getVisibilityStatus(),
                 this.visible, this.hasMouseMovedSince(), !!this.interval, this.sticky, url);
 
             // console.log('SHOW');
@@ -430,15 +444,7 @@
                 () => {
                     this.debugLog('ImagePreview: show.timeout', this.url);
 
-                    const isLocal = this.localPreviewHelper.match(this.domain as string);
-
-                    isLocal
-                        ? this.localPreviewHelper.show(this.url as string)
-                        : this.localPreviewHelper.hide();
-
-                    this.externalPreviewHelper.match(this.domain as string)
-                        ? this.externalPreviewHelper.show(this.url as string)
-                        : this.externalPreviewHelper.hide();
+                    const helper = this.previewManager.show(this.url || undefined, this.domain);
 
                     this.interval = null;
                     this.visible = true;
@@ -449,7 +455,11 @@
 
                     this.reRenderStyles();
 
-                    this.setState(isLocal ? 'loaded' : 'loading');
+                    if (helper) {
+                      this.setState(helper.shouldTrackLoading() ? 'loading' : 'loaded');
+                    } else {
+                      this.setState('loaded');
+                    }
                 },
                 due
             ) as Timer;
@@ -504,8 +514,7 @@
             this.debug = !this.debug;
 
             this.jsMutator.setDebug(this.debug);
-            this.localPreviewHelper.setDebug(this.debug);
-            this.externalPreviewHelper.setDebug(this.debug);
+            this.previewManager.setDebug(this.debug);
 
             if (this.debug) {
                 const webview = this.getWebview();
@@ -550,26 +559,44 @@
                 this.hide();
         }
 
+
         toggleJsMode(): void {
             this.runJs = !this.runJs;
         }
 
-        reloadUrl(): void {
-            if (this.externalPreviewHelper.isVisible()) {
-                const webview = this.getWebview();
 
-                webview.reload();
+        reloadUrl(): void {
+            const helper = this.previewManager.getVisiblePreview();
+
+            if ((!helper) || (!helper.usesWebView())) {
+              return;
             }
+
+            // helper.reload();
+            this.getWebview().reload();
         }
 
+
         getWebview(): WebviewTag {
             return this.$refs.imagePreviewExt as WebviewTag;
         }
 
 
+        getCharacterPreview(): CharacterPreview {
+          return this.$refs.characterPreview as CharacterPreview;
+        }
+
+
         reset(): void {
-            this.externalPreviewHelper = new ExternalImagePreviewHelper(this);
-            this.localPreviewHelper = new LocalImagePreviewHelper(this);
+            this.previewManager = new PreviewManager(
+              this,
+              [
+                new ExternalImagePreviewHelper(this),
+                new LocalImagePreviewHelper(this),
+                new CharacterPreviewHelper(this)
+                // new ChannelPreviewHelper(this)
+              ]
+            );
 
             this.url = null;
             this.domain = undefined;
@@ -612,8 +639,15 @@
                 : false;
         }
 
+
         testError(): boolean {
-            return ((this.state === 'error') && (this.externalPreviewHelper.isVisible()));
+            const helper = this.previewManager.getVisiblePreview();
+
+            if ((!helper) || (!helper.usesWebView())) {
+              return false;
+            }
+
+            return (this.state === 'error');
         }
     }
 </script>
@@ -693,6 +727,7 @@
             border: 1px solid rgba(255, 255, 255, 0.3);
             padding: 0.5rem;
             box-shadow: 2px 2px 3px rgba(0, 0, 0, 0.2);
+            z-index: 1000;
 
             a i.fa {
                 font-size: 1.25rem;
diff --git a/chat/preview/MatchTags.vue b/chat/preview/MatchTags.vue
new file mode 100644
index 0000000..db329e6
--- /dev/null
+++ b/chat/preview/MatchTags.vue
@@ -0,0 +1,80 @@
+<template>
+  <div class="matched-tags">
+    <span v-for="(score, key) in merged" :class="score.getRecommendedClass()"><i :class="score.getRecommendedIcon()"></i> {{getTagDesc(key)}}</span>
+  </div>
+</template>
+
+<script lang="ts">
+import { Component, Hook, Prop } from '@f-list/vue-ts';
+import Vue from 'vue';
+import { MatchReport, MatchResultScores } from '../../learn/matcher';
+import { TagId } from '../../learn/matcher-types';
+
+@Component({
+    components: {
+
+    }
+})
+export default class MatchTags extends Vue {
+  @Prop({required: true})
+  readonly match!: MatchReport;
+
+  merged!: MatchResultScores;
+
+
+  @Hook('mounted')
+  onMounted(): void {
+    this.merged = this.match.merged;
+  }
+
+
+  // @Watch('match', { deep: true })
+  // onMatchUpdate(match: MatchReport): void {
+  //   // console.log('ON UPDATED ETA', match);
+  //   this.merged = match.merged;
+  // }
+
+
+  getTagDesc(key: any): any {
+    return TagId[key].toString().replace(/([A-Z])/g, ' $1').trim();
+  }
+}
+</script>
+
+<style lang="scss">
+.matched-tags {
+  span {
+    padding-left: 3px;
+    padding-right: 3px;
+    margin-bottom: 3px;
+    margin-right: 3px;
+    display: inline-block;
+    border: 1px solid;
+    border-radius: 3px;
+
+    i {
+      color: white;
+    }
+
+    &.match {
+      background-color: var(--scoreMatchBg);
+      border: solid 1px var(--scoreMatchFg);
+    }
+
+    &.weak-match {
+      background-color: var(--scoreWeakMatchBg);
+      border: 1px solid var(--scoreWeakMatchFg);
+    }
+
+    &.weak-mismatch {
+      background-color: var(--scoreWeakMismatchBg);
+      border: 1px solid var(--scoreWeakMismatchFg);
+    }
+
+    &.mismatch {
+      background-color: var(--scoreMismatchBg);
+      border: 1px solid var(--scoreMismatchFg);
+    }
+  }
+}
+</style>
diff --git a/chat/preview/helper/character.ts b/chat/preview/helper/character.ts
new file mode 100644
index 0000000..4ba3cba
--- /dev/null
+++ b/chat/preview/helper/character.ts
@@ -0,0 +1,68 @@
+import { ImagePreviewHelper } from './helper';
+
+export class CharacterPreviewHelper extends ImagePreviewHelper {
+    static readonly FLIST_CHARACTER_PROTOCOL_TESTER = /^flist-character:\/\/(.+)/;
+
+    hide(): void {
+        this.visible = false;
+        this.url = undefined;
+    }
+
+
+    show(url: string | undefined): void {
+        this.visible = true;
+        this.url = url;
+
+        if (!url) {
+            return;
+        }
+
+        const match = url.match(CharacterPreviewHelper.FLIST_CHARACTER_PROTOCOL_TESTER);
+
+        if (!match) {
+            return;
+        }
+
+        const characterName = match[1];
+
+        // tslint:disable-next-line no-floating-promises
+        this.parent.getCharacterPreview().load(characterName);
+    }
+
+
+    setRatio(_ratio: number): void {
+        // do nothing
+    }
+
+
+    reactsToSizeUpdates(): boolean {
+        return false;
+    }
+
+
+    shouldTrackLoading(): boolean {
+        return false;
+    }
+
+
+    usesWebView(): boolean {
+        return false;
+    }
+
+
+    match(_domainName: string | undefined, url: string | undefined): boolean {
+        if (!url) {
+            return false;
+        }
+
+        return CharacterPreviewHelper.FLIST_CHARACTER_PROTOCOL_TESTER.test(url);
+    }
+
+
+    renderStyle(): Record<string, any> {
+        return this.isVisible()
+            ? { display: 'block' }
+            : { display: 'none' };
+    }
+}
+
diff --git a/chat/preview/helper/external.ts b/chat/preview/helper/external.ts
index 3acad17..2f92663 100644
--- a/chat/preview/helper/external.ts
+++ b/chat/preview/helper/external.ts
@@ -3,7 +3,7 @@ import { ImagePreviewHelper } from './helper';
 import * as _ from 'lodash';
 
 export class ExternalImagePreviewHelper extends ImagePreviewHelper {
-    protected lastExternalUrl: string | null = null;
+    protected lastExternalUrl: string | undefined = undefined;
 
     protected allowCachedUrl = true;
 
@@ -47,6 +47,21 @@ export class ExternalImagePreviewHelper extends ImagePreviewHelper {
     }
 
 
+    reactsToSizeUpdates(): boolean {
+        return true;
+    }
+
+
+    shouldTrackLoading(): boolean {
+        return true;
+    }
+
+
+    usesWebView(): boolean {
+        return true;
+    }
+
+
     setDebug(debug: boolean): void {
         this.debug = debug;
 
@@ -54,7 +69,7 @@ export class ExternalImagePreviewHelper extends ImagePreviewHelper {
     }
 
 
-    show(url: string): void {
+    show(url: string | undefined): void {
         const webview = this.parent.getWebview();
 
         if (!this.parent) {
@@ -65,6 +80,10 @@ export class ExternalImagePreviewHelper extends ImagePreviewHelper {
             throw new Error('Empty webview!');
         }
 
+        if (!url) {
+            throw new Error('Empty URL!');
+        }
+
         // const oldUrl = this.url;
         // const oldLastExternalUrl = this.lastExternalUrl;
 
@@ -113,8 +132,13 @@ export class ExternalImagePreviewHelper extends ImagePreviewHelper {
     }
 
 
-    match(domainName: string): boolean {
-        return !((domainName === 'f-list.net') || (domainName === 'static.f-list.net'));
+    match(domainName: string | undefined, url: string | undefined): boolean {
+        if ((!domainName) || (!url)) {
+            return false;
+        }
+
+        return (ImagePreviewHelper.HTTP_TESTER.test(url))
+            && (!((domainName === 'f-list.net') || (domainName === 'static.f-list.net')));
     }
 
 
@@ -152,6 +176,7 @@ export class ExternalImagePreviewHelper extends ImagePreviewHelper {
         }
     }
 
+
     renderStyle(): Record<string, any> {
         return this.isVisible()
             ? _.merge({ display: 'flex' }, this.determineScalingRatio())
diff --git a/chat/preview/helper/helper.ts b/chat/preview/helper/helper.ts
index 74de924..66f9a24 100644
--- a/chat/preview/helper/helper.ts
+++ b/chat/preview/helper/helper.ts
@@ -1,16 +1,23 @@
 import ImagePreview from '../ImagePreview.vue';
 
 export abstract class ImagePreviewHelper {
+    static readonly HTTP_TESTER = /^https?:\/\//;
+
     protected visible = false;
-    protected url: string | null = 'about:blank';
+    protected url: string | undefined = 'about:blank';
     protected parent: ImagePreview;
     protected debug: boolean;
 
-    abstract show(url: string): void;
+    abstract show(url: string | undefined): void;
     abstract hide(): void;
-    abstract match(domainName: string): boolean;
+    abstract match(domainName: string | undefined, url: string | undefined): boolean;
     abstract renderStyle(): Record<string, any>;
 
+    abstract reactsToSizeUpdates(): boolean;
+    abstract setRatio(ratio: number): void;
+    abstract shouldTrackLoading(): boolean;
+    abstract usesWebView(): boolean;
+
     constructor(parent: ImagePreview) {
         if (!parent) {
             throw new Error('Empty parent!');
@@ -24,7 +31,7 @@ export abstract class ImagePreviewHelper {
         return this.visible;
     }
 
-    getUrl(): string | null {
+    getUrl(): string | undefined {
         return this.url;
     }
 
diff --git a/chat/preview/helper/index.ts b/chat/preview/helper/index.ts
index e70dad5..588b76f 100644
--- a/chat/preview/helper/index.ts
+++ b/chat/preview/helper/index.ts
@@ -1,4 +1,6 @@
-export * from './helper';
+export * from './character';
 export * from './external';
+export * from './helper';
 export * from './local';
+export * from './manager';
 
diff --git a/chat/preview/helper/local.ts b/chat/preview/helper/local.ts
index c0e4d0f..e3b2622 100644
--- a/chat/preview/helper/local.ts
+++ b/chat/preview/helper/local.ts
@@ -1,20 +1,46 @@
 import { ImagePreviewHelper } from './helper';
 
+
 export class LocalImagePreviewHelper extends ImagePreviewHelper {
     hide(): void {
         this.visible = false;
-        this.url = null;
+        this.url = undefined;
     }
 
 
-    show(url: string): void {
+    show(url: string | undefined): void {
         this.visible = true;
         this.url = url;
     }
 
 
-    match(domainName: string): boolean {
-        return ((domainName === 'f-list.net') || (domainName === 'static.f-list.net'));
+    setRatio(_ratio: number): void {
+        // do nothing
+    }
+
+
+    reactsToSizeUpdates(): boolean {
+        return false;
+    }
+
+
+    shouldTrackLoading(): boolean {
+        return false;
+    }
+
+
+    usesWebView(): boolean {
+        return false;
+    }
+
+
+    match(domainName: string | undefined, url: string | undefined): boolean {
+        if ((!domainName) || (!url)) {
+            return false;
+        }
+
+        return (ImagePreviewHelper.HTTP_TESTER.test(url))
+            && ((domainName === 'f-list.net') || (domainName === 'static.f-list.net'));
     }
 
 
diff --git a/chat/preview/helper/manager.ts b/chat/preview/helper/manager.ts
new file mode 100644
index 0000000..b16fd47
--- /dev/null
+++ b/chat/preview/helper/manager.ts
@@ -0,0 +1,107 @@
+import _ from 'lodash';
+import { ImagePreviewHelper } from './helper';
+import ImagePreview from '../ImagePreview.vue';
+
+export type RenderStyle = Record<string, any>;
+
+export interface PreviewManagerHelper {
+  helper: ImagePreviewHelper;
+  renderStyle: RenderStyle;
+}
+
+
+export class PreviewManager {
+  private parent: ImagePreview;
+
+  private helpers: PreviewManagerHelper[];
+
+  private debugMode = false;
+
+  constructor(parent: ImagePreview, helperInstances: ImagePreviewHelper[]) {
+    this.parent = parent;
+    this.helpers = _.map(helperInstances, (helper) => ({ helper, renderStyle: {}}));
+  }
+
+  match(domain: string | undefined, url: string | undefined): PreviewManagerHelper | undefined {
+    return _.find(this.helpers, (h) => h.helper.match(domain, url));
+  }
+
+  matchIndex(domain: string | undefined, url: string | undefined): number {
+    return _.findIndex(this.helpers, (h) => h.helper.match(domain, url));
+  }
+
+  renderStyles(): Record<string, RenderStyle> {
+    _.each(
+      this.helpers,
+      (h) => {
+        h.renderStyle = h.helper.renderStyle();
+
+        this.debugLog('ImagePreview: pm.renderStyles()', h.helper.constructor.name, JSON.parse(JSON.stringify(h.renderStyle)));
+      }
+    );
+
+    return _.fromPairs(
+      _.map(
+        this.helpers, (h) => ([h.helper.constructor.name, h.renderStyle])
+      )
+    );
+  }
+
+  getVisiblePreview(): ImagePreviewHelper | undefined {
+    const found = _.find(this.helpers, (h) => h.helper.isVisible());
+
+    return found ? found.helper : undefined;
+  }
+
+
+  show(url: string | undefined, domain: string | undefined): ImagePreviewHelper | undefined {
+    const matchedHelper = this.match(domain, url);
+
+    _.each(
+      _.filter(this.helpers, (h) => (h !== matchedHelper)),
+      (h) => h.helper.hide()
+    );
+
+    if (!matchedHelper) {
+      this.debugLog('ImagePreview: pm.show()', 'Unmatched helper', url, domain);
+      return undefined;
+    }
+
+    matchedHelper.helper.show(url);
+    return matchedHelper.helper;
+  }
+
+
+  hide(): void {
+    _.each(
+      this.helpers,
+      (h) => {
+        this.debugLog('ImagePreview: pm.hide()', h.helper.constructor.name, h.helper.isVisible());
+        h.helper.hide();
+      }
+    );
+  }
+
+
+  getVisibilityStatus(): Record<string, boolean> {
+    return _.fromPairs(
+      _.map(
+        this.helpers, (h) => [h.helper.constructor.name, h.helper.isVisible()]
+      )
+    );
+  }
+
+
+  setDebug(debugMode: boolean): void {
+    _.each(this.helpers, (h) => h.helper.setDebug(debugMode));
+
+    this.debugMode = debugMode;
+  }
+
+
+  debugLog(...messages: any[]): void {
+    if (this.debugMode) {
+      this.parent.debugLog(...messages);
+    }
+  }
+}
diff --git a/learn/matcher-types.ts b/learn/matcher-types.ts
index f0a6630..d184aa8 100644
--- a/learn/matcher-types.ts
+++ b/learn/matcher-types.ts
@@ -92,6 +92,15 @@ export enum FurryPreference {
     FurriesPreferredHumansOk = 149
 }
 
+export const furryPreferenceMapping = {
+    [FurryPreference.FurriesOnly]: 'furries only',
+    [FurryPreference.FursAndHumans]: 'loves furries and humans',
+    [FurryPreference.HumansOnly]: 'humans only',
+    [FurryPreference.HumansPreferredFurriesOk]: 'loves humans, likes furries',
+    [FurryPreference.FurriesPreferredHumansOk]: 'loves furries, likes humans'
+};
+
+
 export interface GenderKinkIdMap {
     [key: number]: Kink
 }
diff --git a/site/character_page/sidebar.vue b/site/character_page/sidebar.vue
index 4fed9c6..a6ef1f0 100644
--- a/site/character_page/sidebar.vue
+++ b/site/character_page/sidebar.vue
@@ -1,13 +1,12 @@
 <template>
     <div id="character-page-sidebar" class="card bg-light">
-        <div class="card-header">
-            <span class="character-name">{{ character.character.name }}</span>
+        <div class="card-body">
+            <img :src="avatarUrl(character.character.name)" class="character-avatar" style="margin-right:10px">
+
             <div v-if="character.character.title" class="character-title">{{ character.character.title }}</div>
             <character-action-menu :character="character" @rename="showRename()" @delete="showDelete()"
                 @block="showBlock()"></character-action-menu>
-        </div>
-        <div class="card-body">
-            <img :src="avatarUrl(character.character.name)" class="character-avatar" style="margin-right:10px">
+
             <div v-if="authenticated" class="d-flex justify-content-between flex-wrap character-links-block">
                 <template v-if="character.is_self">
                     <a :href="editUrl" class="edit-link"><i class="fa fa-fw fa-pencil-alt"></i>Edit</a>