From 727b3d57122d8c6896887ae1e555b4c31f7174c2 Mon Sep 17 00:00:00 2001
From: "Mr. Stallion" <mrstallion@nobody.nowhere.fauxdomain.ext>
Date: Mon, 21 Oct 2019 17:55:05 -0500
Subject: [PATCH] Reconnect management for ad manager; Hide/show non-matching
 ads; throttle profile API queries

---
 chat/CharacterSearch.vue  |  5 ++++-
 chat/Chat.vue             |  5 +++++
 chat/ConversationView.vue | 37 +++++++++++++++++++++++++++++++++++--
 chat/ImagePreview.vue     | 25 +++++++++++++++++++++++++
 chat/ad-manager.ts        | 33 ++++++++++++++++++++++++++++++++-
 chat/conversations.ts     |  2 ++
 chat/profile_api.ts       | 11 +++++++++++
 learn/cache-manager.ts    | 17 ++++++++++++++++-
 package.json              |  1 +
 readme.md                 |  7 +++----
 yarn.lock                 |  5 +++++
 11 files changed, 139 insertions(+), 9 deletions(-)

diff --git a/chat/CharacterSearch.vue b/chat/CharacterSearch.vue
index 615fbd7..64998b7 100644
--- a/chat/CharacterSearch.vue
+++ b/chat/CharacterSearch.vue
@@ -19,7 +19,7 @@
         </div>
         <div v-else-if="results" class="results">
             <h4>{{l('characterSearch.results')}}</h4>
-            <div v-for="character in results" :key="character.name" :class="'status-' + character.status">
+            <div v-for="character in results" :key="character.name" class="search-result" :class="'status-' + character.status">
                 <template v-if="character.status === 'looking'" v-once>
                     <img :src="characterImage(character.name)" v-if="showAvatars"/>
                     <user :character="character" :showStatus="true" :match="true"></user>
@@ -243,6 +243,9 @@
             .user-view {
                 display: block;
             }
+            & > .search-result {
+                clear: both;
+            }
             & > .status-looking {
                 margin-bottom: 5px;
                 min-height: 50px;
diff --git a/chat/Chat.vue b/chat/Chat.vue
index ebd65de..bd783f2 100644
--- a/chat/Chat.vue
+++ b/chat/Chat.vue
@@ -128,6 +128,11 @@
                 if(this.connected) core.notifications.playSound('logout');
                 this.connected = false;
                 this.connecting = false;
+
+                if (!isReconnect) {
+                    core.conversations.channelConversations.forEach((chanConv) => chanConv.adManager.stop());
+                }
+
                 document.title = l('title');
             });
             core.connection.onEvent('connecting', async() => {
diff --git a/chat/ConversationView.vue b/chat/ConversationView.vue
index c166021..a5a1134 100644
--- a/chat/ConversationView.vue
+++ b/chat/ConversationView.vue
@@ -55,6 +55,10 @@
                         <a :class="isChannel(conversation) ? {active: conversation.mode == mode, disabled: conversation.channel.mode != 'both'} : undefined"
                             class="nav-link" href="#" @click.prevent="setMode(mode)">{{l('channel.mode.' + mode)}}</a>
                     </li>
+                    <li>
+                        <a @click.prevent="toggleNonMatchingAds()" :class="{active: showNonMatchingAds, disabled: !showNonMatchingAds}" v-show="(conversation.mode == 'both' || conversation.mode == 'ads')"
+                            class="nav-link" href="#">Non-Matching</a>
+                    </li>
                 </ul>
             </div>
             <div style="z-index:5;position:absolute;left:0;right:0;max-height:60%;overflow:auto"
@@ -88,8 +92,8 @@
 
             <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">
+        <div class="border-top messages" :class="getMessageWrapperClasses()" ref="messages"
+             @scroll="onMessagesScroll" style="flex:1;overflow:auto;margin-top:2px">
             <template v-for="message in messages">
                 <message-view :message="message" :channel="isChannel(conversation) ? conversation.channel : undefined" :key="message.id"
                     :classes="message == conversation.lastRead ? 'last-read' : ''">
@@ -209,6 +213,7 @@
         adAutoPostNextAd: string | null = null;
         isChannel = Conversation.isChannel;
         isPrivate = Conversation.isPrivate;
+        showNonMatchingAds = true;
 
         @Hook('mounted')
         mounted(): void {
@@ -389,6 +394,27 @@
             if(conv.channel.mode === 'both') conv.mode = mode;
         }
 
+
+        toggleNonMatchingAds(): void {
+            this.showNonMatchingAds = !this.showNonMatchingAds;
+        }
+
+
+        /* tslint:disable */
+        getMessageWrapperClasses(): any {
+            if (!this.isChannel(this.conversation)) {
+                return {};
+            }
+
+            const conv = <Conversation.ChannelConversation>this.conversation;
+            const classes:any = {};
+
+            classes['messages-' + conv.mode] = true;
+            classes['hide-non-matching'] = !this.showNonMatchingAds;
+
+            return classes;
+        }
+
         acceptReport(sfc: {callid: number}): void {
             core.connection.send('SFC', {action: 'confirm', callid: sfc.callid});
         }
@@ -682,6 +708,13 @@
         }
     }
 
+
+    .messages.hide-non-matching .message.message-score {
+        &.mismatch {
+            display: none;
+        }
+    }
+
     .message {
         &.message-event {
             font-size: 85%;
diff --git a/chat/ImagePreview.vue b/chat/ImagePreview.vue
index 3895467..f588107 100644
--- a/chat/ImagePreview.vue
+++ b/chat/ImagePreview.vue
@@ -19,6 +19,31 @@
 </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]
+    */
+
+
     import * as _ from 'lodash';
     import {Component, Hook} from '@f-list/vue-ts';
     import Vue from 'vue';
diff --git a/chat/ad-manager.ts b/chat/ad-manager.ts
index d08a0a7..089c6cf 100644
--- a/chat/ad-manager.ts
+++ b/chat/ad-manager.ts
@@ -1,12 +1,20 @@
+import core from './core';
 import { Conversation } from './interfaces';
 import Timer = NodeJS.Timer;
 
+import throat from 'throat';
+
+const adManagerThroat = throat(1);
+
+
 export class AdManager {
     static readonly POSTING_PERIOD = 3 * 60 * 60 * 1000;
     static readonly START_VARIANCE = 3 * 60 * 1000;
     static readonly POST_VARIANCE = 8 * 60 * 1000;
     static readonly POST_DELAY = 1.5 * 60 * 1000;
 
+    static readonly POST_MANUAL_THRESHOLD = 5 * 1000; // don't post anything within 5 seconds of other posts
+
     private conversation: Conversation;
 
     private adIndex = 0;
@@ -24,6 +32,29 @@ export class AdManager {
         return this.active;
     }
 
+
+    // tslint:disable-next-line
+    private async delay(ms: number): Promise<void> {
+        return new Promise((resolve) => setTimeout(resolve, ms));
+    }
+
+
+    // This makes sure there is a 5s delay between channel posts
+    private async sendAdToChannel(msg: string, conv: Conversation.ChannelConversation): Promise<void> {
+        await adManagerThroat(
+            async() => {
+                const delta = Date.now() - core.cache.getLastPost().getTime();
+
+                if ((delta > 0) && (delta < AdManager.POST_MANUAL_THRESHOLD)) {
+                    await this.delay(delta);
+                }
+
+                await conv.sendAd(msg);
+            }
+        );
+    }
+
+
     private async sendNextPost(): Promise<void> {
         const msg = this.getNextAd();
 
@@ -34,7 +65,7 @@ export class AdManager {
 
         const chanConv = (<Conversation.ChannelConversation>this.conversation);
 
-        await chanConv.sendAd(msg);
+        await this.sendAdToChannel(msg, chanConv);
 
         // post next ad every 12 - 22 minutes
         const nextInMs = Math.max(0, (chanConv.nextAd - Date.now())) +
diff --git a/chat/conversations.ts b/chat/conversations.ts
index 8f01842..0dea002 100644
--- a/chat/conversations.ts
+++ b/chat/conversations.ts
@@ -339,6 +339,8 @@ class ChannelConversation extends Conversation implements Interfaces.ChannelConv
         if(isAd)
             this.nextAd = Date.now() + core.connection.vars.lfrp_flood * 1000;
         else this.clearText();
+
+        core.cache.timeLastPost();
     }
 
     async sendAd(text: string): Promise<void> {
diff --git a/chat/profile_api.ts b/chat/profile_api.ts
index cf66739..90fe8b7 100644
--- a/chat/profile_api.ts
+++ b/chat/profile_api.ts
@@ -30,11 +30,22 @@ const parserSettings = {
 };
 
 
+import throat from 'throat';
+
+// Throttle queries so that only two profile requests can run at any given time
+const characterDataThroat = throat(2);
+
+
 // tslint:disable-next-line: ban-ts-ignore
 // @ts-ignore
 async function characterData(name: string | undefined, id: number = -1, skipEvent: boolean = false): Promise<Character> {
     // console.log('CharacterDataquery', name);
+    return characterDataThroat(async() => executeCharacterData(name, id, skipEvent));
+}
 
+// tslint:disable-next-line: ban-ts-ignore
+// @ts-ignore
+async function executeCharacterData(name: string | undefined, id: number = -1, skipEvent: boolean = false): Promise<Character> {
     const data = await core.connection.queryApi<CharacterInfo & {
         badges: string[]
         customs_first: boolean
diff --git a/learn/cache-manager.ts b/learn/cache-manager.ts
index d820120..27650ab 100644
--- a/learn/cache-manager.ts
+++ b/learn/cache-manager.ts
@@ -37,6 +37,16 @@ export class CacheManager {
 
     protected profileStore?: IndexedStore;
 
+    protected lastPost: Date = new Date();
+
+
+    timeLastPost(): void {
+        this.lastPost = new Date();
+    }
+
+    getLastPost(): Date {
+        return this.lastPost;
+    }
 
     async queueForFetching(name: string, skipCacheCheck: boolean = false): Promise<void> {
         if (!skipCacheCheck) {
@@ -65,7 +75,8 @@ export class CacheManager {
         // console.log('AddProfileForFetching', name, this.queue.length);
     }
 
-    async fetchProfile(name: string): Promise<void> {
+
+    async fetchProfile(name: string): Promise<ComplexCharacter | null> {
         try {
             await methods.fieldsGet();
 
@@ -74,8 +85,12 @@ export class CacheManager {
             const r = await this.profileCache.register(c);
 
             this.updateAdScoringForProfile(c, r.matchScore);
+
+            return c;
         } catch (err) {
             console.error('Failed to fetch profile for cache', name, err);
+
+            return null;
         }
     }
 
diff --git a/package.json b/package.json
index 179579c..1eb10ad 100644
--- a/package.json
+++ b/package.json
@@ -31,6 +31,7 @@
     "sass-loader": "^7.1.0",
     "sortablejs": "^1.8.0-rc1",
     "style-loader": "^0.23.1",
+    "throat": "^5.0.0",
     "ts-loader": "^5.3.1",
     "tslib": "^1.9.3",
     "tslint": "^5.12.0",
diff --git a/readme.md b/readme.md
index e72ee4b..ca4114f 100644
--- a/readme.md
+++ b/readme.md
@@ -14,6 +14,7 @@ This repository contains a heavily customized version of the mainline F-Chat 3.0
 
 *   Ads view
     *    Highlight ads from characters most interesting to you
+    *    Hide obviously unmatching ads
     *    View characters' recent ads
 *   Ad auto-posting
     *    Manage channel ad settings via "Tab Settings"
@@ -34,6 +35,7 @@ This repository contains a heavily customized version of the mainline F-Chat 3.0
     *    Cleaner presentation for the side bar details (age, etc.), sorted in most relevant order
     *    Less informative side bar details (views, contact) are separated and shown in a less prominent way
     *    Cleaner guestbook view
+    *    Profiles, images, guestbook posts, and groups are cached for faster view
 *   Character Search
     *    Search results are sorted based on match scores
     *    Display match score in search results
@@ -59,13 +61,10 @@ This repository contains a heavily customized version of the mainline F-Chat 3.0
 *   Split chat view
 *   Improvements to log browsing
 *   Fix broken BBCode, such as `[big]` in character profiles
-*   Which channels my chart partner is on?
 *   Reposition ad settings and toggle
-*   Cache image list, guestbook pages
 *   Save character status messages
-*   Bug: Invalid Ticket
-*   Bug: Posting on the same second
 *   Bug: Images tab count is off
+*   Logout cancels auto-ads
 
 
 
diff --git a/yarn.lock b/yarn.lock
index f814c75..bad4dc5 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -6178,6 +6178,11 @@ terser@^3.8.1:
     source-map "~0.6.1"
     source-map-support "~0.5.6"
 
+throat@^5.0.0:
+  version "5.0.0"
+  resolved "https://registry.yarnpkg.com/throat/-/throat-5.0.0.tgz#c5199235803aad18754a667d659b5e72ce16764b"
+  integrity sha512-fcwX4mndzpLQKBS1DVYhGAcYaYt7vsHNIvQV+WXMvnow5cgjPphq5CaayLaGsjRdSCKZFNGt7/GYAuXaNOiYCA==
+
 throttleit@0.0.2:
   version "0.0.2"
   resolved "https://registry.yarnpkg.com/throttleit/-/throttleit-0.0.2.tgz#cfedf88e60c00dd9697b61fdd2a8343a9b680eaf"