From 75f099ce89dbefe0c3df93de862cbb78945843d0 Mon Sep 17 00:00:00 2001 From: "Mr. Stallion" Date: Fri, 7 Jun 2019 14:31:42 -0500 Subject: [PATCH] Refactored with better posting rules --- chat/ConversationView.vue | 159 +++++++++++++++++--------------------- chat/WebSocket.ts | 2 +- chat/ad-manager.ts | 120 ++++++++++++++++++++++++++++ chat/common.ts | 10 --- chat/conversations.ts | 30 ++----- chat/interfaces.ts | 15 +--- chat/localize.ts | 8 ++ tsconfig.json | 15 ++++ 8 files changed, 224 insertions(+), 135 deletions(-) create mode 100644 chat/ad-manager.ts create mode 100644 tsconfig.json 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">
-

Auto-Posting Ads

+

{{l('admgr.activeHeader')}}

{{adAutoPostUpdate}}
- + + {{l('admgr.renew')}}
@@ -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