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">
@@ -117,15 +118,15 @@
{{l('chat.send')}}
@@ -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) (
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);
-
- 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 (>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 = 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 {
+ const msg = this.getNextAd();
+
+ if ((!msg) || ((this.expireDue) && (this.expireDue.getTime() < Date.now()))) {
+ this.stop();
+ return;
+ }
+
+ const chanConv = (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 = (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 {
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 {
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 = ['online', 'looking', 'away', 'busy', 'dnd'];
export const channelModes: ReadonlyArray = ['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
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