diff --git a/chat/ConversationView.vue b/chat/ConversationView.vue
index 65ee938..1f9172e 100644
--- a/chat/ConversationView.vue
+++ b/chat/ConversationView.vue
@@ -70,14 +70,15 @@
                 @click="hideSearch"><i class="fas fa-times"></i></a>
         </div>
         <div class="auto-ads" v-show="isAutopostingAds()">
-            <h4>Auto-Posting Ads</h4>
+            <h4>{{l('admgr.activeHeader')}}</h4>
             <div class="update">{{adAutoPostUpdate}}</div>
 
-
             <div v-show="adAutoPostNextAd" class="next">
-                <h5>Coming Next</h5>
+                <h5>{{l('admgr.comingNext')}}</h5>
                 <div>{{(adAutoPostNextAd ? adAutoPostNextAd.substr(0, 50) : '')}}...</div>
             </div>
+
+            <a class="btn btn-sm btn-outline-primary renew-autoposts" @click="renewAutoPosting()">{{l('admgr.renew')}}</a>
         </div>
         <div class="border-top messages" :class="isChannel(conversation) ? 'messages-' + conversation.mode : undefined" ref="messages"
             @scroll="onMessagesScroll" style="flex:1;overflow:auto;margin-top:2px">
@@ -117,15 +118,15 @@
                 <ul class="nav nav-pills send-ads-switcher" v-if="isChannel(conversation)"
                     style="position:relative;z-index:10;margin-right:5px">
                     <li class="nav-item">
-                        <a href="#" :class="{active: !conversation.isSendingAds, disabled: conversation.channel.mode != 'both'}"
+                        <a href="#" :class="{active: !conversation.isSendingAds, disabled: (conversation.channel.mode != 'both') || (conversation.adManager.isActive())}"
                             class="nav-link" @click.prevent="setSendingAds(false)">{{l('channel.mode.chat')}}</a>
                     </li>
                     <li class="nav-item">
-                        <a href="#" :class="{active: conversation.isSendingAds, disabled: conversation.channel.mode != 'both'}"
+                        <a href="#" :class="{active: conversation.isSendingAds, disabled: (conversation.channel.mode != 'both') || (conversation.adManager.isActive())}"
                             class="nav-link" @click.prevent="setSendingAds(true)">{{adsMode}}</a>
                     </li>
                     <li class="nav-item">
-                        <a href="#" :class="{active: conversation.adState.active}" class="nav-link" @click="autoPostAds()">Auto-Post Ads</a>
+                        <a href="#" :class="{active: conversation.adManager.isActive()}" class="nav-link toggle-autopost" @click="toggleAutoPostAds()">{{l('admgr.toggleAutoPost')}}</a>
                     </li>
                 </ul>
                 <div class="btn btn-sm btn-primary" v-show="!settings.enterSend" @click="sendButton">{{l('chat.send')}}</div>
@@ -146,7 +147,7 @@
     import {Keys} from '../keys';
     import {BBCodeView, Editor} from './bbcode';
     import CommandHelp from './CommandHelp.vue';
-    import { AdState, characterImage, getByteLength, getKey } from "./common";
+    import { characterImage, getByteLength, getKey } from "./common";
     import ConversationSettings from './ConversationSettings.vue';
     import core from './core';
     import {Channel, channelModes, Character, Conversation, Settings} from './interfaces';
@@ -191,8 +192,8 @@
         adCountdown = 0;
         adsMode = l('channel.mode.ads');
         autoPostingUpdater = 0;
-        adAutoPostUpdate: string|null = null;
-        adAutoPostNextAd: string|null = null;
+        adAutoPostUpdate: string | null = null;
+        adAutoPostNextAd: string | null = null;
         isChannel = Conversation.isChannel;
         isPrivate = Conversation.isPrivate;
 
@@ -236,7 +237,8 @@
                 setAdCountdown();
             });
 
-            this.$watch('conversation.adState.active', () => (this.refreshAutoPostingTimer()));
+            this.$watch(() => this.conversation.adManager.isActive(), () => (this.refreshAutoPostingTimer()));
+            this.refreshAutoPostingTimer();
         }
 
         @Hook('destroyed')
@@ -273,6 +275,7 @@
             if(!anyDialogsShown) (<Editor>this.$refs['textBox']).focus();
             this.$nextTick(() => setTimeout(() => this.messageView.scrollTop = this.messageView.scrollHeight));
             this.scrolledDown = true;
+            this.refreshAutoPostingTimer();
         }
 
         @Watch('conversation.messages')
@@ -399,78 +402,29 @@
 
 
         isAutopostingAds(): boolean {
-            return this.conversation.adState.active;
+            return this.conversation.adManager.isActive();
         }
 
 
-        clearAutoPostAds(): void {
-            if (this.conversation.adState.interval) {
-                clearTimeout(this.conversation.adState.interval);
-            }
-
-            this.conversation.adState = new AdState();
+        stopAutoPostAds(): void {
+            this.conversation.adManager.stop();
         }
 
 
-        autoPostAds(): void {
+        renewAutoPosting(): void {
+            this.conversation.adManager.renew();
+
+            this.refreshAutoPostingTimer();
+        }
+
+
+        toggleAutoPostAds(): void {
             if(this.isAutopostingAds()) {
-                this.clearAutoPostAds();
-                this.refreshAutoPostingTimer();
-                return;
+                this.stopAutoPostAds();
+            } else {
+                this.conversation.adManager.start();
             }
 
-            const conversation = this.conversation;
-
-            /**** Do not use 'this' keyword below this line, it will operate differently than you expect ****/
-
-            const chanConv = (<Conversation.ChannelConversation>conversation);
-
-            const adState = conversation.adState;
-            const initialWait = Math.max(0, chanConv.nextAd - Date.now()) * 1.1;
-
-            adState.adIndex = 0;
-
-            const sendNextPost = async () => {
-                const ads = conversation.settings.adSettings.ads;
-                const index = (adState.adIndex || 0);
-
-                if ((ads.length === 0) || ((adState.expireDue) && (adState.expireDue.getTime() < Date.now()))) {
-                    conversation.adState = new AdState();
-                    return;
-                }
-
-                const msg = ads[index % ads.length];
-
-                await chanConv.sendAd(msg);
-
-                const nextInMs = Math.max(0, (chanConv.nextAd - Date.now())) * 1.1;
-
-                adState.adIndex = index + 1;
-                adState.nextPostDue = new Date(Date.now() + nextInMs);
-
-                adState.interval = setTimeout(
-                    async () => {
-                        await sendNextPost();
-                    },
-                    nextInMs
-                );
-            };
-
-
-            adState.active = true;
-            adState.nextPostDue = new Date(Date.now() + initialWait);
-            adState.expireDue = new Date(Date.now() + 2 * 60 * 60 * 1000);
-
-
-            adState.interval = setTimeout(
-                async () => {
-                    adState.firstPost = new Date();
-
-                    await sendNextPost();
-                },
-                initialWait
-            );
-
             this.refreshAutoPostingTimer();
         }
 
@@ -487,25 +441,28 @@
             }
 
             const updateAutoPostingState = () => {
-                const adState = this.conversation.adState;
-                const ads = this.conversation.settings.adSettings.ads;
+                const adManager = this.conversation.adManager;
 
-                if(ads.length > 0) {
-                    this.adAutoPostNextAd = ads[(adState.adIndex || 0) % ads.length];
+                this.adAutoPostNextAd = adManager.getNextAd() || null;
 
-                    const diff = ((adState.nextPostDue || new Date()).getTime() - Date.now()) / 1000;
-                    const expDiff = ((adState.expireDue || new Date()).getTime() - Date.now()) / 1000;
+                if(this.adAutoPostNextAd) {
+                    const diff = ((adManager.getNextPostDue() || new Date()).getTime() - Date.now()) / 1000;
+                    const expDiff = ((adManager.getExpireDue() || new Date()).getTime() - Date.now()) / 1000;
 
-                    if((adState.nextPostDue) && (!adState.firstPost)) {
-                        this.adAutoPostUpdate = `Posting beings in ${Math.floor(diff / 60)}m ${Math.floor(diff % 60)}s`;
-                    } else {
-                        this.adAutoPostUpdate = `Next ad in ${Math.floor(diff / 60)}m ${Math.floor(diff % 60)}s`;
-                    }
+                    const diffMins = Math.floor(diff / 60);
+                    const diffSecs = Math.floor(diff % 60);
+                    const expDiffMins = Math.floor(expDiff / 60);
+                    const expDiffSecs = Math.floor(expDiff % 60);
 
-                    this.adAutoPostUpdate += `, auto-posting expires in ${Math.floor(expDiff / 60)}m ${Math.floor(expDiff % 60)}s`;
+                    this.adAutoPostUpdate = l(
+                        ((adManager.getNextPostDue()) && (!adManager.getFirstPost())) ? 'admgr.postingBegins' : 'admgr.nextPostDue',
+                        diffMins,
+                        diffSecs
+                    ) + l('admgr.expiresIn', expDiffMins, expDiffSecs);
                 } else {
                     this.adAutoPostNextAd = null;
-                    this.adAutoPostUpdate = 'No ads have been set up -- auto-posting will be cancelled.';
+
+                    this.adAutoPostUpdate = l('admgr.noAds');
                 }
             };
 
@@ -514,7 +471,6 @@
             updateAutoPostingState();
         }
 
-
         hasSFC(message: Conversation.Message): message is Conversation.SFCMessage {
             return (<Partial<Conversation.SFCMessage>>message).sfc !== undefined;
         }
@@ -559,6 +515,12 @@
             padding: 3px 10px;
         }
 
+
+        .toggle-autopost {
+            margin-left: 1px;
+        }
+
+
         .auto-ads {
             background-color: rgba(255, 128, 32, 0.8);
             padding-left: 10px;
@@ -566,6 +528,29 @@
             padding-top: 5px;
             padding-bottom: 5px;
             margin: 0;
+            position: relative;
+
+            .renew-autoposts {
+                display: block;
+                float: right;
+                /* margin-top: auto; */
+                /* margin-bottom: auto; */
+                position: absolute;
+                /* bottom: 1px; */
+                right: 10px;
+                top: 50%;
+                transform: translateY(-50%);
+                border-color: rgba(255, 255, 255, 0.5);
+                color: rgba(255, 255, 255, 0.9);
+
+                &:hover {
+                    background-color: rgba(255, 255, 255, 0.3);
+                }
+
+                &:active {
+                    background-color: rgba(255, 255, 255, 0.6);
+                }
+            }
 
             h4 {
                 font-size: 1.1rem;
diff --git a/chat/WebSocket.ts b/chat/WebSocket.ts
index 087ca4b..7194779 100644
--- a/chat/WebSocket.ts
+++ b/chat/WebSocket.ts
@@ -1,7 +1,7 @@
 import {WebSocketConnection} from '../fchat';
 
 export default class Socket implements WebSocketConnection {
-    static host = 'wss://chat.f-list.net:9799';
+    static host = 'wss://chat.f-list.net/chat2';
     private socket: WebSocket;
     private lastHandler: Promise<void> = Promise.resolve();
 
diff --git a/chat/ad-manager.ts b/chat/ad-manager.ts
new file mode 100644
index 0000000..d75b7c8
--- /dev/null
+++ b/chat/ad-manager.ts
@@ -0,0 +1,120 @@
+import { Conversation } from './interfaces';
+
+export class AdManager {
+    static readonly POSTING_PERIOD = 3 * 60 * 60 * 1000;
+    static readonly START_VARIANCE = 3 * 60 * 1000;
+    static readonly POST_VARIANCE = 10 * 60 * 1000;
+    static readonly POST_DELAY = 2 * 60 * 1000;
+
+    private conversation: Conversation;
+
+    private adIndex = 0;
+    private active = false;
+    private nextPostDue?: Date;
+    private expireDue?: Date;
+    private firstPost?: Date;
+    private interval?: any;
+
+    constructor(conversation: Conversation) {
+        this.conversation = conversation;
+    }
+
+    isActive(): boolean {
+        return this.active;
+    }
+
+    private async sendNextPost(): Promise<void> {
+        const msg = this.getNextAd();
+
+        if ((!msg) || ((this.expireDue) && (this.expireDue.getTime() < Date.now()))) {
+            this.stop();
+            return;
+        }
+
+        const chanConv = (<Conversation.ChannelConversation>this.conversation);
+
+        await chanConv.sendAd(msg);
+
+        // post next ad every 12 - 22 minutes
+        const nextInMs = Math.max(0, (chanConv.nextAd - Date.now())) +
+            AdManager.POST_DELAY +
+            Math.random() * AdManager.POST_VARIANCE;
+
+        this.adIndex = this.adIndex + 1;
+        this.nextPostDue = new Date(Date.now() + nextInMs);
+
+        this.interval = setTimeout(
+            async() => {
+                await this.sendNextPost();
+            },
+            nextInMs
+        );
+    }
+
+    getAds(): string[] {
+        return this.conversation.settings.adSettings.ads;
+    }
+
+    getNextAd(): string | undefined {
+        const ads = this.getAds();
+
+        if (ads.length === 0)
+            return;
+
+        return ads[this.adIndex % ads.length];
+    }
+
+    getNextPostDue(): Date | undefined {
+        return this.nextPostDue;
+    }
+
+    getExpireDue(): Date | undefined {
+        return this.expireDue;
+    }
+
+    getFirstPost(): Date | undefined {
+        return this.firstPost;
+    }
+
+    start(): void {
+        const chanConv = (<Conversation.ChannelConversation>this.conversation);
+        const initialWait = Math.max(Math.random() * AdManager.START_VARIANCE, (chanConv.nextAd - Date.now()) * 1.1);
+
+        this.adIndex = 0;
+        this.active = true;
+        this.nextPostDue = new Date(Date.now() + initialWait);
+        this.expireDue = new Date(Date.now() + AdManager.POSTING_PERIOD);
+
+        this.interval = setTimeout(
+            async() => {
+                this.firstPost = new Date();
+
+                await this.sendNextPost();
+            },
+            initialWait
+        );
+    }
+
+    stop(): void {
+        if (this.interval)
+            clearTimeout(this.interval);
+
+        delete this.interval;
+        delete this.nextPostDue;
+        delete this.expireDue;
+        delete this.firstPost;
+
+        this.active = false;
+        this.adIndex = 0;
+
+        // const message = new EventMessage(`Advertisements on channel [channel]${this.conversation.name}[/channel] have expired.`);
+        // addEventMessage(message);
+    }
+
+    renew(): void {
+        if (!this.active)
+            return;
+
+        this.expireDue = new Date(Date.now() + 3 * 60 * 60 * 1000);
+    }
+}
diff --git a/chat/common.ts b/chat/common.ts
index 385acb3..5666910 100644
--- a/chat/common.ts
+++ b/chat/common.ts
@@ -50,16 +50,6 @@ export class AdSettings implements Conversation.AdSettings {
 }
 
 
-export class AdState implements Conversation.AdState {
-    active = false;
-    firstPost?: Date = undefined;
-    nextPostDue?: Date = undefined;
-    interval?: any = undefined;
-    adIndex?: number = undefined;
-    expireDue?: Date = undefined;
-}
-
-
 export class ConversationSettings implements Conversation.Settings {
     notify = Conversation.Setting.Default;
     highlight = Conversation.Setting.Default;
diff --git a/chat/conversations.ts b/chat/conversations.ts
index 74a23bf..0da8fe0 100644
--- a/chat/conversations.ts
+++ b/chat/conversations.ts
@@ -1,6 +1,7 @@
 import {queuedJoin} from '../fchat/channels';
 import {decodeHTML} from '../fchat/common';
-import { AdState, characterImage, ConversationSettings, EventMessage, Message, messageToString } from './common';
+import { AdManager } from './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';
@@ -30,14 +31,15 @@ abstract class Conversation implements Interfaces.Conversation {
     infoText = '';
     abstract readonly maxMessageLength: number | undefined;
     _settings: Interfaces.Settings | undefined;
-    _adState: Interfaces.AdState | undefined;
     protected abstract context: CommandContext;
     protected maxMessages = 50;
     protected allMessages: Interfaces.Message[] = [];
     readonly reportMessages: Interfaces.Message[] = [];
     private lastSent = '';
+    adManager: AdManager;
 
     constructor(readonly key: string, public _isPinned: boolean) {
+        this.adManager = new AdManager(this);
     }
 
     get settings(): Interfaces.Settings {
@@ -50,17 +52,6 @@ abstract class Conversation implements Interfaces.Conversation {
         state.setSettings(this.key, value); //tslint:disable-line:no-floating-promises
     }
 
-    get adState(): Interfaces.AdState {
-        //tslint:disable-next-line:strict-boolean-expressions
-        return this._adState || (this._adState = state.adStates[this.key] || new AdState());
-    }
-
-    set adState(value: Interfaces.AdState) {
-        this._adState = value;
-        state.setAdState(this.key, value); //tslint:disable-line:no-floating-promises
-    }
-
-
     get isPinned(): boolean {
         return this._isPinned;
     }
@@ -200,12 +191,11 @@ class PrivateConversation extends Conversation implements Interfaces.PrivateConv
             return;
         }
 
-        if(this.adState.active) {
+        if(this.adManager.isActive()) {
             this.errorText = 'Cannot send ads manually while ad auto-posting is active';
             return;
         }
 
-
         core.connection.send('PRI', {recipient: this.name, message: this.enteredText});
         const message = createMessage(MessageType.Message, core.characters.ownCharacter, this.enteredText);
         this.safeAddMessage(message);
@@ -331,7 +321,7 @@ class ChannelConversation extends Conversation implements Interfaces.ChannelConv
     protected async doSend(): Promise<void> {
         const isAd = this.isSendingAds;
 
-        if(this.adState.active) {
+        if(this.adManager.isActive()) {
             this.errorText = 'Cannot post ads manually while ad auto-posting is active';
             return;
         }
@@ -349,7 +339,6 @@ class ChannelConversation extends Conversation implements Interfaces.ChannelConv
         else this.clearText();
     }
 
-
     async sendAd(text: string): Promise<void> {
         if (text.length < 1)
             return;
@@ -399,7 +388,6 @@ class State implements Interfaces.State {
     recentChannels: Interfaces.RecentChannelConversation[] = [];
     pinned!: {channels: string[], private: string[]};
     settings!: {[key: string]: Interfaces.Settings};
-    adStates: {[key: string]: Interfaces.AdState} = {};
     modes!: {[key: string]: Channel.Mode | undefined};
     windowFocused = document.hasFocus();
 
@@ -444,12 +432,6 @@ class State implements Interfaces.State {
         await core.settingsStore.set('conversationSettings', this.settings);
     }
 
-
-    setAdState(key: string, value: Interfaces.AdState): void {
-        this.adStates[key] = value;
-    }
-
-
     show(conversation: Conversation): void {
         this.selectedConversation.onHide();
         conversation.unread = Interfaces.UnreadState.None;
diff --git a/chat/interfaces.ts b/chat/interfaces.ts
index fb161d8..e3bc650 100644
--- a/chat/interfaces.ts
+++ b/chat/interfaces.ts
@@ -2,6 +2,7 @@
 import {Connection} from '../fchat';
 
 import {Channel, Character} from '../fchat/interfaces';
+import { AdManager } from './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'];
@@ -98,22 +99,10 @@ export namespace Conversation {
         readonly adSettings: AdSettings;
     }
 
-
     export interface AdSettings {
         readonly ads: string[];
     }
 
-
-    export interface AdState {
-        active: boolean;
-        firstPost?: Date;
-        nextPostDue?: Date;
-        expireDue?: Date;
-        interval?: any;
-        adIndex?: number;
-    }
-
-
     export const enum UnreadState { None, Unread, Mention }
 
     export interface Conversation {
@@ -127,7 +116,7 @@ export namespace Conversation {
         readonly key: string
         readonly unread: UnreadState
         settings: Settings
-        adState: AdState
+        readonly adManager: AdManager;
         send(): Promise<void>
         clear(): void
         loadLastSent(): void
diff --git a/chat/localize.ts b/chat/localize.ts
index e8725ad..1f114bc 100644
--- a/chat/localize.ts
+++ b/chat/localize.ts
@@ -17,6 +17,14 @@ const strings: {[key: string]: string | undefined} = {
     'action.updateAvailable': 'UPDATE AVAILABLE',
     'action.update': 'Restart now!',
     'action.cancel': 'Cancel',
+    'admgr.postingBegins': 'Posting beings in {0}m {1}s',
+    'admgr.nextPostDue': 'Next ad in {0}m {1}s',
+    'admgr.expiresIn': ', auto-posting expires in {0}m {1}s',
+    'admgr.noAds': 'No ads have been set up -- auto-posting will be cancelled.',
+    'admgr.activeHeader': 'Auto-Posting Ads',
+    'admgr.comingNext': 'Coming Next',
+    'admgr.renew': 'Renew',
+    'admgr.toggleAutoPost': 'Auto-Post Ads',
     'consoleWarning.head': 'THIS IS THE DANGER ZONE.',
     'consoleWarning.body': `ANYTHING YOU WRITE OR PASTE IN HERE COULD BE USED TO STEAL YOUR PASSWORDS OR TAKE OVER YOUR ENTIRE COMPUTER. This is where happiness goes to die. If you aren't a developer or a special kind of daredevil, please get out of here!`,
     'help': 'Help',
diff --git a/tsconfig.json b/tsconfig.json
new file mode 100644
index 0000000..1e0abea
--- /dev/null
+++ b/tsconfig.json
@@ -0,0 +1,15 @@
+{
+  "compilerOptions": {
+    "target": "es2017",
+    "module": "commonjs",
+    "sourceMap": true,
+    "allowJs": true,
+    "noEmitHelpers": true,
+    "importHelpers": true,
+    "forceConsistentCasingInFileNames": true,
+    "strict": true,
+    "noUnusedLocals": true,
+    "noUnusedParameters": true
+  },
+  "include": ["./electron/main.ts"]
+}
\ No newline at end of file