diff --git a/bbcode/Editor.vue b/bbcode/Editor.vue
index 077a32b..037df8f 100644
--- a/bbcode/Editor.vue
+++ b/bbcode/Editor.vue
@@ -1,9 +1,37 @@
 <template>
     <div class="bbcode-editor" style="display:flex;flex-wrap:wrap;justify-content:flex-end">
         <slot></slot>
-        <a v-show="hasToolbar" tabindex="0" class="btn btn-light bbcode-btn btn-sm" role="button" @click="showToolbar = true" @blur="showToolbar = false" style="border-bottom-left-radius: 0; border-bottom-right-radius: 0">
+        <a tabindex="0" class="btn btn-light bbcode-btn btn-sm" role="button" @click="showToolbar = true" @blur="showToolbar = false"
+            style="border-bottom-left-radius:0;border-bottom-right-radius:0" v-if="hasToolbar">
             <i class="fa fa-code"></i>
         </a>
+        <div class="bbcode-toolbar btn-toolbar" role="toolbar" :style="showToolbar ? {display: 'flex'} : undefined" @mousedown.stop.prevent
+            v-if="hasToolbar" style="flex:1 51%">
+            <div class="btn-group" style="flex-wrap:wrap">
+                <div class="btn btn-light btn-sm" v-for="button in buttons" :title="button.title" @click.prevent.stop="apply(button)">
+                    <i :class="(button.class ? button.class : 'fa ') + button.icon"></i>
+                </div>
+                <div @click="previewBBCode" class="btn btn-light btn-sm" :class="preview ? 'active' : ''"
+                    :title="preview ? 'Close Preview' : 'Preview'">
+                    <i class="fa fa-eye"></i>
+                </div>
+            </div>
+            <button type="button" class="close" aria-label="Close" style="margin-left:10px" @click="showToolbar = false">&times;</button>
+        </div>
+        <div class="bbcode-editor-text-area" style="order:100;width:100%;">
+            <textarea ref="input" v-model="text" @input="onInput" v-show="!preview" :maxlength="maxlength" :placeholder="placeholder"
+                :class="finalClasses" @keyup="onKeyUp" :disabled="disabled" @paste="onPaste" @keypress="$emit('keypress', $event)"
+                :style="hasToolbar ? {'border-top-left-radius': 0} : undefined" @keydown="onKeyDown"></textarea>
+            <textarea ref="sizer"></textarea>
+            <div class="bbcode-preview" v-show="preview">
+                <div class="bbcode-preview-warnings">
+                    <div class="alert alert-danger" v-show="previewWarnings.length">
+                        <li v-for="warning in previewWarnings">{{warning}}</li>
+                    </div>
+                </div>
+                <div class="bbcode" ref="preview-element"></div>
+            </div>
+        </div>
     </div>
 </template>
 
@@ -65,7 +93,7 @@
 
         @Hook('created')
         created(): void {
-            console.log('EDITOR', 'created');
+            // console.log('EDITOR', 'created');
             this.parser = new CoreBBCodeParser();
             this.resizeListener = () => {
                 const styles = getComputedStyle(this.element);
@@ -76,7 +104,7 @@
 
         @Hook('mounted')
         mounted(): void {
-            console.log('EDITOR', 'mounted');
+            // console.log('EDITOR', 'mounted');
             this.element = <HTMLTextAreaElement>this.$refs['input'];
             const styles = getComputedStyle(this.element);
             this.maxHeight = parseInt(styles.maxHeight, 10) || 250;
diff --git a/bbcode/UrlTagView.vue b/bbcode/UrlTagView.vue
index b22e25a..fa4465e 100644
--- a/bbcode/UrlTagView.vue
+++ b/bbcode/UrlTagView.vue
@@ -23,7 +23,7 @@
 <script lang="ts">
     import {Component, Hook, Prop} from '@f-list/vue-ts';
     import Vue from 'vue';
-    import {EventBus} from '../chat/event-bus';
+    import {EventBus} from '../chat/preview/event-bus';
     // import core from './core';
 
     @Component
diff --git a/chat/CharacterSearch.vue b/chat/CharacterSearch.vue
index 85d797c..745266a 100644
--- a/chat/CharacterSearch.vue
+++ b/chat/CharacterSearch.vue
@@ -48,7 +48,7 @@
     import l from './localize';
     import UserView from './UserView.vue';
     import * as _ from 'lodash';
-    import {EventBus} from './event-bus';
+    import {EventBus} from './preview/event-bus';
 
     type Options = {
         kinks: Kink[],
@@ -301,4 +301,4 @@
 
     }
 
-</style>
\ No newline at end of file
+</style>
diff --git a/chat/ChatView.vue b/chat/ChatView.vue
index e118962..e61a37d 100644
--- a/chat/ChatView.vue
+++ b/chat/ChatView.vue
@@ -122,7 +122,7 @@
     import {getStatusIcon} from './UserView.vue';
     import UserList from './UserList.vue';
     import UserMenu from './UserMenu.vue';
-    import ImagePreview from './ImagePreview.vue';
+    import ImagePreview from './preview/ImagePreview.vue';
 
     const unreadClasses = {
         [Conversation.UnreadState.None]: '',
@@ -458,4 +458,4 @@
             }
         }
     }
-</style>
\ No newline at end of file
+</style>
diff --git a/chat/ConversationView.vue b/chat/ConversationView.vue
index 29eed3f..933d700 100644
--- a/chat/ConversationView.vue
+++ b/chat/ConversationView.vue
@@ -161,7 +161,7 @@
     import {BBCodeView} from '../bbcode/view';
     import {isShowing as anyDialogsShown} from '../components/Modal.vue';
     import {Keys} from '../keys';
-    import AdView from './AdView.vue';
+    import AdView from './ads/AdView.vue';
     import {Editor} from './bbcode';
     import CommandHelp from './CommandHelp.vue';
     import { characterImage, getByteLength, getKey } from './common';
diff --git a/chat/UserMenu.vue b/chat/UserMenu.vue
index d76d7b4..1367bd2 100644
--- a/chat/UserMenu.vue
+++ b/chat/UserMenu.vue
@@ -48,7 +48,7 @@
     import Vue from 'vue';
     import {BBCodeView} from '../bbcode/view';
     import Modal from '../components/Modal.vue';
-    import AdView from './AdView.vue';
+    import AdView from './ads/AdView.vue';
     import {characterImage, errorToString, getByteLength, profileLink} from './common';
     import core from './core';
     import {Channel, Character} from './interfaces';
diff --git a/chat/UserView.vue b/chat/UserView.vue
index 240e204..819f348 100644
--- a/chat/UserView.vue
+++ b/chat/UserView.vue
@@ -8,7 +8,7 @@ import Vue from 'vue';
 import {Channel, Character} from '../fchat';
 import { Score, Scoring } from '../learn/matcher';
 import core from './core';
-import { EventBus } from './event-bus';
+import { EventBus } from './preview/event-bus';
 
 
 export function getStatusIcon(status: Character.Status): string {
diff --git a/chat/AdView.vue b/chat/ads/AdView.vue
similarity index 83%
rename from chat/AdView.vue
rename to chat/ads/AdView.vue
index d8d592f..94d36ca 100644
--- a/chat/AdView.vue
+++ b/chat/ads/AdView.vue
@@ -19,13 +19,13 @@
 
 import * as _ from 'lodash';
 import { Component, Hook, Prop, Watch } from '@f-list/vue-ts';
-import CustomDialog from '../components/custom_dialog';
-import Modal from '../components/Modal.vue';
-import { Character } from '../fchat/interfaces';
-import { AdCachedPosting } from '../learn/ad-cache';
-import core from './core';
-import {formatTime} from './common';
-import UserView from './UserView.vue';
+import CustomDialog from '../../components/custom_dialog';
+import Modal from '../../components/Modal.vue';
+import { Character } from '../../fchat/interfaces';
+import { AdCachedPosting } from '../../learn/ad-cache';
+import core from '../core';
+import {formatTime} from '../common';
+import UserView from '../UserView.vue';
 
 @Component({
     components: {modal: Modal, user: UserView}
diff --git a/chat/ad-manager.ts b/chat/ads/ad-manager.ts
similarity index 98%
rename from chat/ad-manager.ts
rename to chat/ads/ad-manager.ts
index 089c6cf..58e6198 100644
--- a/chat/ad-manager.ts
+++ b/chat/ads/ad-manager.ts
@@ -1,5 +1,5 @@
-import core from './core';
-import { Conversation } from './interfaces';
+import core from '../core';
+import { Conversation } from '../interfaces';
 import Timer = NodeJS.Timer;
 
 import throat from 'throat';
diff --git a/chat/conversations.ts b/chat/conversations.ts
index 5fa5bd2..80c2f83 100644
--- a/chat/conversations.ts
+++ b/chat/conversations.ts
@@ -1,14 +1,14 @@
 import {queuedJoin} from '../fchat/channels';
 import {decodeHTML} from '../fchat/common';
 import { CharacterCacheRecord } from '../learn/profile-cache';
-import { AdManager } from './ad-manager';
+import { AdManager } from './ads/ad-manager';
 import { characterImage, ConversationSettings, EventMessage, Message, messageToString } from './common';
 import core from './core';
 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';
+import {EventBus} from './preview/event-bus';
 
 function createMessage(this: any, type: MessageType, sender: Character, text: string, time?: Date): Message {
     if(type === MessageType.Message && isAction(text)) {
diff --git a/chat/interfaces.ts b/chat/interfaces.ts
index c413166..046e5dd 100644
--- a/chat/interfaces.ts
+++ b/chat/interfaces.ts
@@ -2,7 +2,7 @@
 import {Connection} from '../fchat';
 
 import {Channel, Character} from '../fchat/interfaces';
-import { AdManager } from './ad-manager';
+import { AdManager } from './ads/ad-manager';
 export {Connection, Channel, Character} from '../fchat/interfaces';
 export const userStatuses: ReadonlyArray<Character.Status> = ['online', 'looking', 'away', 'busy', 'dnd'];
 export const channelModes: ReadonlyArray<Channel.Mode> = ['chat', 'ads', 'both'];
@@ -201,4 +201,4 @@ export interface Notifications {
 export interface State {
     settings: Settings
     hiddenUsers: string[]
-}
\ No newline at end of file
+}
diff --git a/chat/ImagePreview.vue b/chat/preview/ImagePreview.vue
similarity index 73%
rename from chat/ImagePreview.vue
rename to chat/preview/ImagePreview.vue
index 83c101c..a12ed98 100644
--- a/chat/ImagePreview.vue
+++ b/chat/preview/ImagePreview.vue
@@ -20,59 +20,16 @@
 </template>
 
 <script lang="ts">
-    /*
-    [url=https://giphy.com/gifs/arianagrande-ariana-grande-thank-u-next-you-uldtLAK6tSOKP5PWw3]Test[/url]
-
-    [url=https://media1.tenor.com/images/097ee180965dd336f470b77d064f198f/tenor.gif?itemid=13664909]Test[/url]
-
-    [url=https://tenor.com/view/thank-unext-ariana-grande-thank-you-next-wink-winking-gif-13664909]Test[/url]
-
-    [url=https://www.sex.com/pin/58497794/]Test[/url]
-
-    [url=https://images.sex.com/images/pinporn/2019/09/10/620/21790701.gif]Test[/url]
-
-    [url=http://gfycatporn.com/deepthroat.php]Test[/url]
-
-    [url=https://imgur.com/LmEyXEM]Test[/url]
-
-    [url=https://static1.e621.net/data/6d/bf/6dbf0c369793dbb5a53d9814c17861eb.webm]Test[/url]
-
-    [url=https://www.youtube.com/watch?v=_52zdiltkRM]Test[/url]
-
-    [url=https://e621.net/post/show/1672753/2018-anthro-antlers-balls-bed-big_penis-black_hair]Test[/url]
-
-    [url=https://rule34.xxx/index.php?page=post&s=view&id=3213191]Test[/url]
-
-    [url=https://chan.sankakucomplex.com/post/show/6163997]Test[/url]
-
-    [url=https://chan.sankakucomplex.com/post/show/5774884]Test[/url]
-
-    [url=https://www.sex.com/pin/38152484-she-likes-it-rough/]Test[/url]
-
-    [url=https://www.sex.com/pin/57537179-cock-slapping-hungry-tongue/]Test[/url]
-
-    [url=https://imgur.com/gallery/ILsb94I]Imgur gallery[/url]
-
-    [url=https://imgur.com/CIKv6sA]Imgur image[/url]
-
-    [url=https://imgur.com/a/nMafj]Imgur album[/url]
-
-    [url=http://i.imgur.com/txEREOg.gifv]Imgur video[/url]
-
-    [url=https://www.punishbang.com/videos/1898/tied-redhead-is-on-her-knees-and-can/]Test[/url]
-
-    [url=https://www.pornhub.com/view_video.php?viewkey=ph5b2c03dc1e23b]Test[/url]
-
-    */
-
     import * as _ from 'lodash';
     import {Component, Hook} from '@f-list/vue-ts';
     import Vue from 'vue';
-    import core from './core';
+    import core from '../core';
     import { EventBus, EventBusEvent } from './event-bus';
-    import {domain} from '../bbcode/core';
+    import {domain} from '../../bbcode/core';
     import {ImagePreviewMutator} from './image-preview-mutator';
-    import {ImageUrlMutator} from './image-url-mutator';
+
+    import { ExternalImagePreviewHelper, LocalImagePreviewHelper } from './helper';
+
     import {Point, WebviewTag, remote} from 'electron';
     import Timer = NodeJS.Timer;
     import IpcMessageEvent = Electron.IpcMessageEvent;
@@ -91,196 +48,6 @@
     }
 
 
-    abstract class ImagePreviewHelper {
-        protected visible = false;
-        protected url: string | null = 'about:blank';
-        protected parent: ImagePreview;
-        protected debug: boolean;
-
-        abstract show(url: string): void;
-        abstract hide(): void;
-        abstract match(domainName: string): boolean;
-        abstract renderStyle(): Record<string, any>;
-
-        constructor(parent: ImagePreview) {
-            if (!parent) {
-                throw new Error('Empty parent!');
-            }
-
-            this.parent = parent;
-            this.debug = parent.debug;
-        }
-
-        isVisible(): boolean {
-            return this.visible;
-        }
-
-        getUrl(): string | null {
-            return this.url;
-        }
-
-        setDebug(debug: boolean): void {
-            this.debug = debug;
-        }
-    }
-
-
-    class LocalImagePreviewHelper extends ImagePreviewHelper {
-        hide(): void {
-            this.visible = false;
-            this.url = null;
-        }
-
-
-        show(url: string): void {
-            this.visible = true;
-            this.url = url;
-        }
-
-
-        match(domainName: string): boolean {
-            return ((domainName === 'f-list.net') || (domainName === 'static.f-list.net'));
-        }
-
-
-        renderStyle(): Record<string, any> {
-            return this.isVisible()
-                ? { backgroundImage: `url(${this.getUrl()})`, display: 'block' }
-                : { display: 'none' };
-        }
-    }
-
-
-    class ExternalImagePreviewHelper extends ImagePreviewHelper {
-        protected lastExternalUrl: string | null = null;
-
-        protected allowCachedUrl = true;
-
-        protected urlMutator = new ImageUrlMutator(this.parent.debug);
-
-        protected ratio: number | null = null;
-
-        hide(): void {
-            const wasVisible = this.visible;
-
-            if (this.parent.debug)
-                console.log('ImagePreview: exec hide mutator');
-
-            if (wasVisible) {
-                const webview = this.parent.getWebview();
-
-                if (this.allowCachedUrl) {
-                    webview.executeJavaScript(this.parent.jsMutator.getHideMutator());
-                } else {
-                    webview.loadURL('about:blank');
-                }
-
-                this.visible = false;
-            }
-        }
-
-
-        setRatio(ratio: number): void {
-            this.ratio = ratio;
-        }
-
-
-        setDebug(debug: boolean): void {
-            this.debug = debug;
-
-            this.urlMutator.setDebug(debug);
-        }
-
-
-        show(url: string): void {
-            const webview = this.parent.getWebview();
-
-            if (!this.parent) {
-                throw new Error('Empty parent v2');
-            }
-
-            if (!webview) {
-                throw new Error('Empty webview!');
-            }
-
-            // const oldUrl = this.url;
-            const oldLastExternalUrl = this.lastExternalUrl;
-
-            this.url = url;
-            this.lastExternalUrl = url;
-            this.visible = true;
-
-            try {
-                if ((this.allowCachedUrl) && ((webview.getURL() === url) || (url === oldLastExternalUrl))) {
-                    if (this.debug)
-                        console.log('ImagePreview: exec re-show mutator');
-
-                    webview.executeJavaScript(this.parent.jsMutator.getReShowMutator());
-                } else {
-                    if (this.debug)
-                        console.log('ImagePreview: must load; skip re-show because urls don\'t match', this.url, webview.getURL());
-
-                    this.ratio = null;
-
-                    // Broken promise chain on purpose
-                    // tslint:disable-next-line:no-floating-promises
-                    this.urlMutator.resolve(url)
-                        .then((finalUrl: string) => webview.loadURL(finalUrl));
-                }
-
-            } catch (err) {
-                console.error('ImagePreview: Webview reuse error', err);
-            }
-        }
-
-
-        match(domainName: string): boolean {
-            return !((domainName === 'f-list.net') || (domainName === 'static.f-list.net'));
-        }
-
-
-        determineScalingRatio(): Record<string, any> {
-            // ratio = width / height
-            const ratio = this.ratio;
-
-            if (!ratio) {
-                return {};
-            }
-
-            const ww = window.innerWidth;
-            const wh = window.innerHeight;
-
-            const maxWidth = Math.round(ww * 0.5);
-            const maxHeight = Math.round(wh * 0.7);
-
-            if (ratio >= 1) {
-                const presumedWidth = maxWidth;
-                const presumedHeight = presumedWidth / ratio;
-
-                return {
-                    width: `${presumedWidth}px`,
-                    height: `${presumedHeight}px`
-                };
-            // tslint:disable-next-line:unnecessary-else
-            } else {
-                const presumedHeight = maxHeight;
-                const presumedWidth = presumedHeight * ratio;
-
-                return {
-                    width: `${presumedWidth}px`,
-                    height: `${presumedHeight}px`
-                };
-            }
-        }
-
-        renderStyle(): Record<string, any> {
-            return this.isVisible()
-                ? _.merge({ display: 'flex' }, this.determineScalingRatio())
-                : { display: 'none' };
-        }
-    }
-
-
     @Component
     export default class ImagePreview extends Vue {
         private readonly MinTimePreviewVisible = 100;
@@ -760,9 +527,9 @@
 
 
 <style lang="scss">
-    @import "~bootstrap/scss/functions";
-    @import "~bootstrap/scss/variables";
-    @import "~bootstrap/scss/mixins/breakpoints";
+    @import "../../node_modules/bootstrap/scss/functions";
+    @import "../../node_modules/bootstrap/scss/variables";
+    @import "../../node_modules/bootstrap/scss/mixins/breakpoints";
 
     .image-preview-wrapper {
         z-index: 10000;
diff --git a/chat/event-bus.ts b/chat/preview/event-bus.ts
similarity index 86%
rename from chat/event-bus.ts
rename to chat/preview/event-bus.ts
index 0eea540..0a14730 100644
--- a/chat/event-bus.ts
+++ b/chat/preview/event-bus.ts
@@ -1,7 +1,7 @@
 import Vue from 'vue';
-import { Character } from '../site/character_page/interfaces';
-import { Message } from './common';
-import { Conversation } from './interfaces';
+import { Character } from '../../site/character_page/interfaces';
+import { Message } from '../common';
+import { Conversation } from '../interfaces';
 import ChannelConversation = Conversation.ChannelConversation;
 
 /**
diff --git a/chat/preview/helper/external.ts b/chat/preview/helper/external.ts
new file mode 100644
index 0000000..e52478b
--- /dev/null
+++ b/chat/preview/helper/external.ts
@@ -0,0 +1,133 @@
+import { ImageUrlMutator } from '../image-url-mutator';
+import { ImagePreviewHelper } from './index';
+import * as _ from 'lodash';
+
+export class ExternalImagePreviewHelper extends ImagePreviewHelper {
+    protected lastExternalUrl: string | null = null;
+
+    protected allowCachedUrl = true;
+
+    protected urlMutator = new ImageUrlMutator(this.parent.debug);
+
+    protected ratio: number | null = null;
+
+    hide(): void {
+        const wasVisible = this.visible;
+
+        if (this.parent.debug)
+            console.log('ImagePreview: exec hide mutator');
+
+        if (wasVisible) {
+            const webview = this.parent.getWebview();
+
+            if (this.allowCachedUrl) {
+                webview.executeJavaScript(this.parent.jsMutator.getHideMutator());
+            } else {
+                webview.loadURL('about:blank');
+            }
+
+            this.visible = false;
+        }
+    }
+
+
+    setRatio(ratio: number): void {
+        this.ratio = ratio;
+    }
+
+
+    setDebug(debug: boolean): void {
+        this.debug = debug;
+
+        this.urlMutator.setDebug(debug);
+    }
+
+
+    show(url: string): void {
+        const webview = this.parent.getWebview();
+
+        if (!this.parent) {
+            throw new Error('Empty parent v2');
+        }
+
+        if (!webview) {
+            throw new Error('Empty webview!');
+        }
+
+        // const oldUrl = this.url;
+        const oldLastExternalUrl = this.lastExternalUrl;
+
+        this.url = url;
+        this.lastExternalUrl = url;
+        this.visible = true;
+
+        try {
+            if ((this.allowCachedUrl) && ((webview.getURL() === url) || (url === oldLastExternalUrl))) {
+                if (this.debug)
+                    console.log('ImagePreview: exec re-show mutator');
+
+                webview.executeJavaScript(this.parent.jsMutator.getReShowMutator());
+            } else {
+                if (this.debug)
+                    console.log('ImagePreview: must load; skip re-show because urls don\'t match', this.url, webview.getURL());
+
+                this.ratio = null;
+
+                // Broken promise chain on purpose
+                // tslint:disable-next-line:no-floating-promises
+                this.urlMutator.resolve(url)
+                    .then((finalUrl: string) => webview.loadURL(finalUrl));
+            }
+
+        } catch (err) {
+            console.error('ImagePreview: Webview reuse error', err);
+        }
+    }
+
+
+    match(domainName: string): boolean {
+        return !((domainName === 'f-list.net') || (domainName === 'static.f-list.net'));
+    }
+
+
+    determineScalingRatio(): Record<string, any> {
+        // ratio = width / height
+        const ratio = this.ratio;
+
+        if (!ratio) {
+            return {};
+        }
+
+        const ww = window.innerWidth;
+        const wh = window.innerHeight;
+
+        const maxWidth = Math.round(ww * 0.5);
+        const maxHeight = Math.round(wh * 0.7);
+
+        if (ratio >= 1) {
+            const presumedWidth = maxWidth;
+            const presumedHeight = presumedWidth / ratio;
+
+            return {
+                width: `${presumedWidth}px`,
+                height: `${presumedHeight}px`
+            };
+        // tslint:disable-next-line:unnecessary-else
+        } else {
+            const presumedHeight = maxHeight;
+            const presumedWidth = presumedHeight * ratio;
+
+            return {
+                width: `${presumedWidth}px`,
+                height: `${presumedHeight}px`
+            };
+        }
+    }
+
+    renderStyle(): Record<string, any> {
+        return this.isVisible()
+            ? _.merge({ display: 'flex' }, this.determineScalingRatio())
+            : { display: 'none' };
+    }
+}
+
diff --git a/chat/preview/helper/index.ts b/chat/preview/helper/index.ts
new file mode 100644
index 0000000..7d040e8
--- /dev/null
+++ b/chat/preview/helper/index.ts
@@ -0,0 +1,38 @@
+import ImagePreview from '../ImagePreview.vue';
+
+export * from './external';
+export * from './local';
+
+export abstract class ImagePreviewHelper {
+    protected visible = false;
+    protected url: string | null = 'about:blank';
+    protected parent: ImagePreview;
+    protected debug: boolean;
+
+    abstract show(url: string): void;
+    abstract hide(): void;
+    abstract match(domainName: string): boolean;
+    abstract renderStyle(): Record<string, any>;
+
+    constructor(parent: ImagePreview) {
+        if (!parent) {
+            throw new Error('Empty parent!');
+        }
+
+        this.parent = parent;
+        this.debug = parent.debug;
+    }
+
+    isVisible(): boolean {
+        return this.visible;
+    }
+
+    getUrl(): string | null {
+        return this.url;
+    }
+
+    setDebug(debug: boolean): void {
+        this.debug = debug;
+    }
+}
+
diff --git a/chat/preview/helper/local.ts b/chat/preview/helper/local.ts
new file mode 100644
index 0000000..ecd34b0
--- /dev/null
+++ b/chat/preview/helper/local.ts
@@ -0,0 +1,27 @@
+import { ImagePreviewHelper } from './index';
+
+export class LocalImagePreviewHelper extends ImagePreviewHelper {
+    hide(): void {
+        this.visible = false;
+        this.url = null;
+    }
+
+
+    show(url: string): void {
+        this.visible = true;
+        this.url = url;
+    }
+
+
+    match(domainName: string): boolean {
+        return ((domainName === 'f-list.net') || (domainName === 'static.f-list.net'));
+    }
+
+
+    renderStyle(): Record<string, any> {
+        return this.isVisible()
+            ? { backgroundImage: `url(${this.getUrl()})`, display: 'block' }
+            : { display: 'none' };
+    }
+}
+
diff --git a/chat/image-preview-mutator.ts b/chat/preview/image-preview-mutator.ts
similarity index 99%
rename from chat/image-preview-mutator.ts
rename to chat/preview/image-preview-mutator.ts
index bf4d4a5..fe8c218 100644
--- a/chat/image-preview-mutator.ts
+++ b/chat/preview/image-preview-mutator.ts
@@ -4,7 +4,7 @@ import * as _ from 'lodash';
 import * as urlHelper from 'url';
 
 
-import { domain as extractDomain } from '../bbcode/core';
+import { domain as extractDomain } from '../../bbcode/core';
 
 export interface PreviewMutator {
     match: string | RegExp;
diff --git a/chat/image-url-mutator.ts b/chat/preview/image-url-mutator.ts
similarity index 100%
rename from chat/image-url-mutator.ts
rename to chat/preview/image-url-mutator.ts
diff --git a/chat/preview/test-urls.txt b/chat/preview/test-urls.txt
new file mode 100644
index 0000000..a8c4250
--- /dev/null
+++ b/chat/preview/test-urls.txt
@@ -0,0 +1,44 @@
+
+    [url=https://giphy.com/gifs/arianagrande-ariana-grande-thank-u-next-you-uldtLAK6tSOKP5PWw3]Test[/url]
+
+    [url=https://media1.tenor.com/images/097ee180965dd336f470b77d064f198f/tenor.gif?itemid=13664909]Test[/url]
+
+    [url=https://tenor.com/view/thank-unext-ariana-grande-thank-you-next-wink-winking-gif-13664909]Test[/url]
+
+    [url=https://www.sex.com/pin/58497794/]Test[/url]
+
+    [url=https://images.sex.com/images/pinporn/2019/09/10/620/21790701.gif]Test[/url]
+
+    [url=http://gfycatporn.com/deepthroat.php]Test[/url]
+
+    [url=https://imgur.com/LmEyXEM]Test[/url]
+
+    [url=https://static1.e621.net/data/6d/bf/6dbf0c369793dbb5a53d9814c17861eb.webm]Test[/url]
+
+    [url=https://www.youtube.com/watch?v=_52zdiltkRM]Test[/url]
+
+    [url=https://e621.net/post/show/1672753/2018-anthro-antlers-balls-bed-big_penis-black_hair]Test[/url]
+
+    [url=https://rule34.xxx/index.php?page=post&s=view&id=3213191]Test[/url]
+
+    [url=https://chan.sankakucomplex.com/post/show/6163997]Test[/url]
+
+    [url=https://chan.sankakucomplex.com/post/show/5774884]Test[/url]
+
+    [url=https://www.sex.com/pin/38152484-she-likes-it-rough/]Test[/url]
+
+    [url=https://www.sex.com/pin/57537179-cock-slapping-hungry-tongue/]Test[/url]
+
+    [url=https://imgur.com/gallery/ILsb94I]Imgur gallery[/url]
+
+    [url=https://imgur.com/CIKv6sA]Imgur image[/url]
+
+    [url=https://imgur.com/a/nMafj]Imgur album[/url]
+
+    [url=http://i.imgur.com/txEREOg.gifv]Imgur video[/url]
+
+    [url=https://www.punishbang.com/videos/1898/tied-redhead-is-on-her-knees-and-can/]Test[/url]
+
+    [url=https://www.pornhub.com/view_video.php?viewkey=ph5b2c03dc1e23b]Test[/url]
+
+
diff --git a/chat/profile_api.ts b/chat/profile_api.ts
index 2c866e3..4872878 100644
--- a/chat/profile_api.ts
+++ b/chat/profile_api.ts
@@ -19,7 +19,7 @@ import {
 } from '../site/character_page/interfaces';
 import * as Utils from '../site/utils';
 import core from './core';
-import { EventBus } from './event-bus';
+import { EventBus } from './preview/event-bus';
 
 const parserSettings = {
     siteDomain: 'https://www.f-list.net/',
diff --git a/electron/Window.vue b/electron/Window.vue
index c9feb5c..0b1f2fc 100644
--- a/electron/Window.vue
+++ b/electron/Window.vue
@@ -85,7 +85,7 @@
         @Hook('mounted')
         async mounted(): Promise<void> {
             // top bar devtools
-            browserWindow.webContents.openDevTools();
+            // browserWindow.webContents.openDevTools( { mode: 'detach' } );
 
             await this.addTab();
 
@@ -168,7 +168,7 @@
 
         get styling(): string {
             try {
-                return `<style>${fs.readFileSync(path.join(__dirname, `themes/${this.settings.theme}.css`, 'utf8')).toString()}</style>`;
+                return `<style>${fs.readFileSync(path.join(__dirname, `themes/${this.settings.theme}.css`), 'utf8').toString()}</style>`;
             } catch(e) {
                 if((<Error & {code: string}>e).code === 'ENOENT' && this.settings.theme !== 'default') {
                     this.settings.theme = 'default';
diff --git a/electron/chat.ts b/electron/chat.ts
index 715008c..7ac027e 100644
--- a/electron/chat.ts
+++ b/electron/chat.ts
@@ -40,7 +40,7 @@ import * as electron from 'electron';
 import * as path from 'path';
 import * as qs from 'querystring';
 import {getKey} from '../chat/common';
-import { EventBus } from '../chat/event-bus';
+import { EventBus } from '../chat/preview/event-bus';
 import {init as initCore} from '../chat/core';
 import l from '../chat/localize';
 import {setupRaven} from '../chat/vue-raven';
diff --git a/electron/main.ts b/electron/main.ts
index 249a9c6..5ae2250 100644
--- a/electron/main.ts
+++ b/electron/main.ts
@@ -38,10 +38,10 @@ import * as electron from 'electron';
 import log from 'electron-log'; //tslint:disable-line:match-default-export-name
 import * as fs from 'fs';
 import * as path from 'path';
-import * as url from 'url';
+// import * as url from 'url';
 import l from '../chat/localize';
 import {defaultHost, GeneralSettings} from './common';
-import {ensureDictionary, getAvailableDictionaries} from './dgit ictionaries';
+import {ensureDictionary, getAvailableDictionaries} from './dictionaries';
 import * as windowState from './window_state';
 import BrowserWindow = Electron.BrowserWindow;
 import MenuItem = Electron.MenuItem;
@@ -128,8 +128,8 @@ function createWindow(): Electron.BrowserWindow | undefined {
     };
 
     if(process.platform === 'darwin') {
-        // windowProperties.titleBarStyle = 'hiddenInset';
-        windowProperties.frame = true;
+        windowProperties.titleBarStyle = 'hiddenInset';
+        // windowProperties.frame = true;
     } else {
        windowProperties.frame = false;
     }
diff --git a/learn/cache-manager.ts b/learn/cache-manager.ts
index 9fa92af..d2e139c 100644
--- a/learn/cache-manager.ts
+++ b/learn/cache-manager.ts
@@ -1,6 +1,6 @@
 import * as _ from 'lodash';
 import core from '../chat/core';
-import { ChannelAdEvent, ChannelMessageEvent, CharacterDataEvent, EventBus } from '../chat/event-bus';
+import { ChannelAdEvent, ChannelMessageEvent, CharacterDataEvent, EventBus } from '../chat/preview/event-bus';
 import { Conversation } from '../chat/interfaces';
 import { methods } from '../site/character_page/data_store';
 import { Character as ComplexCharacter } from '../site/character_page/interfaces';