diff --git a/.gitignore b/.gitignore
index 97c08d2..6ee9746 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,4 +2,6 @@ node_modules/
/electron/app
/electron/dist
/mobile/www
-/webchat/dist
\ No newline at end of file
+/webchat/dist
+
+.idea/workspace.xml
diff --git a/chat/ConversationSettings.vue b/chat/ConversationSettings.vue
index 12b2726..75b628f 100644
--- a/chat/ConversationSettings.vue
+++ b/chat/ConversationSettings.vue
@@ -35,6 +35,12 @@
+
+
+
+
+
+
@@ -58,6 +64,7 @@
highlightWords!: string;
joinMessages!: Conversation.Setting;
defaultHighlights!: boolean;
+ ads!: string[];
load(): void {
const settings = this.conversation.settings;
@@ -66,16 +73,29 @@
this.highlightWords = settings.highlightWords.join(',');
this.joinMessages = settings.joinMessages;
this.defaultHighlights = settings.defaultHighlights;
+ this.ads = settings.adSettings.ads.slice(0);
+
+ if (this.ads.length === 0) {
+ this.ads.push('');
+ }
}
submit(): void {
this.conversation.settings = {
notify: this.notify,
highlight: this.highlight,
- highlightWords: this.highlightWords.split(',').map((x) => x.trim()).filter((x) => x.length),
+ highlightWords: this.highlightWords.split(',').map((x) => x.trim()).filter((x) => (x.length > 0)),
joinMessages: this.joinMessages,
- defaultHighlights: this.defaultHighlights
+ defaultHighlights: this.defaultHighlights,
+ adSettings: {
+ ads: this.ads.filter((ad: string) => (ad.length > 0))
+ }
};
}
+
+
+ addAd(): void {
+ this.ads.push('');
+ }
}
\ No newline at end of file
diff --git a/chat/ConversationView.vue b/chat/ConversationView.vue
index ad26b29..65ee938 100644
--- a/chat/ConversationView.vue
+++ b/chat/ConversationView.vue
@@ -69,6 +69,16 @@
+
+
Auto-Posting Ads
+
{{adAutoPostUpdate}}
+
+
+
+
Coming Next
+
{{(adAutoPostNextAd ? adAutoPostNextAd.substr(0, 50) : '')}}...
+
+
@@ -133,7 +146,7 @@
import {Keys} from '../keys';
import {BBCodeView, Editor} from './bbcode';
import CommandHelp from './CommandHelp.vue';
- import {characterImage, getByteLength, getKey} from './common';
+ import { AdState, characterImage, getByteLength, getKey } from "./common";
import ConversationSettings from './ConversationSettings.vue';
import core from './core';
import {Channel, channelModes, Character, Conversation, Settings} from './interfaces';
@@ -177,6 +190,9 @@
ignoreScroll = false;
adCountdown = 0;
adsMode = l('channel.mode.ads');
+ autoPostingUpdater = 0;
+ adAutoPostUpdate: string|null = null;
+ adAutoPostNextAd: string|null = null;
isChannel = Conversation.isChannel;
isPrivate = Conversation.isPrivate;
@@ -219,6 +235,8 @@
this.adCountdown = window.setInterval(setAdCountdown, 1000);
setAdCountdown();
});
+
+ this.$watch('conversation.adState.active', () => (this.refreshAutoPostingTimer()));
}
@Hook('destroyed')
@@ -227,6 +245,8 @@
window.removeEventListener('keydown', this.keydownHandler);
window.removeEventListener('keypress', this.keypressHandler);
clearInterval(this.searchTimer);
+ clearInterval(this.autoPostingUpdater);
+ clearInterval(this.adCountdown);
}
hideSearch(): void {
@@ -377,6 +397,124 @@
(this.$refs['manageDialog']).show();
}
+
+ isAutopostingAds(): boolean {
+ return this.conversation.adState.active;
+ }
+
+
+ clearAutoPostAds(): void {
+ if (this.conversation.adState.interval) {
+ clearTimeout(this.conversation.adState.interval);
+ }
+
+ this.conversation.adState = new AdState();
+ }
+
+
+ autoPostAds(): void {
+ if(this.isAutopostingAds()) {
+ this.clearAutoPostAds();
+ this.refreshAutoPostingTimer();
+ return;
+ }
+
+ 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();
+ }
+
+
+ refreshAutoPostingTimer() {
+ if (this.autoPostingUpdater) {
+ window.clearInterval(this.autoPostingUpdater);
+ }
+
+ if(this.isAutopostingAds() === false) {
+ this.adAutoPostUpdate = null;
+ this.adAutoPostNextAd = null;
+ return;
+ }
+
+ const updateAutoPostingState = () => {
+ const adState = this.conversation.adState;
+ const ads = this.conversation.settings.adSettings.ads;
+
+ if(ads.length > 0) {
+ this.adAutoPostNextAd = ads[(adState.adIndex || 0) % ads.length];
+
+ const diff = ((adState.nextPostDue || new Date()).getTime() - Date.now()) / 1000;
+ const expDiff = ((adState.expireDue || 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`;
+ }
+
+ this.adAutoPostUpdate += `, auto-posting expires in ${Math.floor(expDiff / 60)}m ${Math.floor(expDiff % 60)}s`;
+ } else {
+ this.adAutoPostNextAd = null;
+ this.adAutoPostUpdate = 'No ads have been set up -- auto-posting will be cancelled.';
+ }
+ };
+
+ this.autoPostingUpdater = window.setInterval(updateAutoPostingState, 1000);
+
+ updateAutoPostingState();
+ }
+
+
hasSFC(message: Conversation.Message): message is Conversation.SFCMessage {
return (>message).sfc !== undefined;
}
@@ -421,6 +559,39 @@
padding: 3px 10px;
}
+ .auto-ads {
+ background-color: rgba(255, 128, 32, 0.8);
+ padding-left: 10px;
+ padding-right: 10px;
+ padding-top: 5px;
+ padding-bottom: 5px;
+ margin: 0;
+
+ h4 {
+ font-size: 1.1rem;
+ margin: 0;
+ line-height: 100%;
+ }
+
+ .update {
+ color: rgba(255, 255, 255, 0.6);
+ font-size: 13px;
+ }
+
+ .next {
+ margin-top: 0.5rem;
+ color: rgba(255, 255, 255, 0.4);
+ font-size: 11px;
+
+ h5 {
+ font-size: 0.8rem;
+ margin: 0;
+ line-height: 100%;
+ }
+ }
+
+ }
+
@media (max-width: breakpoint-max(sm)) {
.mode-switcher a {
padding: 5px 8px;
diff --git a/chat/common.ts b/chat/common.ts
index 851056c..385acb3 100644
--- a/chat/common.ts
+++ b/chat/common.ts
@@ -44,12 +44,29 @@ export class Settings implements ISettings {
bbCodeBar = true;
}
+
+export class AdSettings implements Conversation.AdSettings {
+ ads: string[] = [];
+}
+
+
+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;
highlightWords: string[] = [];
joinMessages = Conversation.Setting.Default;
defaultHighlights = true;
+ adSettings: Conversation.AdSettings = { ads: [] };
}
function pad(num: number): string | number {
diff --git a/chat/conversations.ts b/chat/conversations.ts
index 9043138..74a23bf 100644
--- a/chat/conversations.ts
+++ b/chat/conversations.ts
@@ -1,6 +1,6 @@
import {queuedJoin} from '../fchat/channels';
import {decodeHTML} from '../fchat/common';
-import {characterImage, ConversationSettings, EventMessage, Message, messageToString} from './common';
+import { AdState, characterImage, ConversationSettings, EventMessage, Message, messageToString } from './common';
import core from './core';
import {Channel, Character, Conversation as Interfaces} from './interfaces';
import l from './localize';
@@ -30,6 +30,7 @@ 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[] = [];
@@ -49,6 +50,17 @@ 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;
}
@@ -65,6 +77,7 @@ abstract class Conversation implements Interfaces.Conversation {
async send(): Promise {
if(this.enteredText.length === 0) return;
+
if(isCommand(this.enteredText)) {
const parsed = parseCommand(this.enteredText, this.context);
if(typeof parsed === 'string') this.errorText = parsed;
@@ -186,6 +199,13 @@ class PrivateConversation extends Conversation implements Interfaces.PrivateConv
this.errorText = l('chat.errorIgnored', this.character.name);
return;
}
+
+ if(this.adState.active) {
+ 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);
@@ -310,7 +330,17 @@ class ChannelConversation extends Conversation implements Interfaces.ChannelConv
protected async doSend(): Promise {
const isAd = this.isSendingAds;
- if(isAd && Date.now() < this.nextAd) return;
+
+ if(this.adState.active) {
+ this.errorText = 'Cannot post ads manually while ad auto-posting is active';
+ return;
+ }
+
+ if(isAd && Date.now() < this.nextAd) {
+ this.errorText = 'You must wait at least ten minutes between ad posts on this channel';
+ return;
+ }
+
core.connection.send(isAd ? 'LRP' : 'MSG', {channel: this.channel.id, message: this.enteredText});
await this.addMessage(
createMessage(isAd ? MessageType.Ad : MessageType.Message, core.characters.ownCharacter, this.enteredText, new Date()));
@@ -318,6 +348,18 @@ class ChannelConversation extends Conversation implements Interfaces.ChannelConv
this.nextAd = Date.now() + core.connection.vars.lfrp_flood * 1000;
else this.clearText();
}
+
+
+ async sendAd(text: string): Promise {
+ if (text.length < 1)
+ return;
+
+ await this.addMessage(
+ createMessage(MessageType.Ad, core.characters.ownCharacter, text, new Date())
+ );
+
+ this.nextAd = Date.now() + core.connection.vars.lfrp_flood * 1000;
+ }
}
class ConsoleConversation extends Conversation {
@@ -357,6 +399,7 @@ 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();
@@ -401,6 +444,12 @@ 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 1ab53be..fb161d8 100644
--- a/chat/interfaces.ts
+++ b/chat/interfaces.ts
@@ -62,6 +62,7 @@ export namespace Conversation {
mode: Channel.Mode
readonly nextAd: number
isSendingAds: boolean
+ sendAd(text: string): Promise
}
export function isPrivate(conversation: Conversation): conversation is PrivateConversation {
@@ -94,8 +95,25 @@ export namespace Conversation {
readonly highlightWords: ReadonlyArray;
readonly joinMessages: Setting;
readonly defaultHighlights: boolean;
+ 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 {
@@ -109,6 +127,7 @@ export namespace Conversation {
readonly key: string
readonly unread: UnreadState
settings: Settings
+ adState: AdState
send(): Promise
clear(): void
loadLastSent(): void