From af1960ed024f7c2f12d6cbeaa451f53662d6a7e2 Mon Sep 17 00:00:00 2001
From: "Mr. Stallion" <mrstallion@nobody.nowhere.fauxemail.ext>
Date: Sun, 9 Jun 2019 18:33:52 -0500
Subject: [PATCH] Friendlier UX for image previews; profile kinks now inlined;
 character info sidebar cleaner; full size images on character images page

---
 chat/ImagePreview.vue                  | 173 ++++++++++++++++---------
 chat/UrlTagView.vue                    |  21 ++-
 chat/image-preview-mutator.ts          |  42 +++---
 electron/Window.vue                    |   3 +
 electron/main.ts                       |   1 +
 readme.md                              |  25 ++++
 site/character_page/character_page.vue |  84 +++++++++++-
 site/character_page/images.vue         |  19 ++-
 site/character_page/infotag.vue        |   2 +-
 site/character_page/kink.vue           |   5 +-
 site/character_page/kinks.vue          |   7 +-
 site/character_page/sidebar.vue        |  10 +-
 12 files changed, 273 insertions(+), 119 deletions(-)

diff --git a/chat/ImagePreview.vue b/chat/ImagePreview.vue
index 4d72e4d..aff0580 100644
--- a/chat/ImagePreview.vue
+++ b/chat/ImagePreview.vue
@@ -1,7 +1,7 @@
 <template>
     <!-- hiding elements instead of using 'v-if' is used here as an optimization -->
     <div class="image-preview-wrapper" :style="{display: visible ? 'block' : 'none'}">
-        <webview webpreferences="allowRunningInsecureContent" id="image-preview-ext" ref="imagePreviewExt" class="image-preview-external" :src="externalUrl" :style="{display: externalUrlVisible ? 'flex' : 'none'}"></webview>
+        <webview webpreferences="allowRunningInsecureContent, autoplayPolicy=no-user-gesture-required" id="image-preview-ext" ref="imagePreviewExt" class="image-preview-external" :src="externalUrl" :style="{display: externalUrlVisible ? 'flex' : 'none'}"></webview>
         <div
             class="image-preview-local"
             :style="{backgroundImage: `url(${internalUrl})`, display: internalUrlVisible ? 'block' : 'none'}"
@@ -16,31 +16,40 @@
     import {EventBus} from './event-bus';
     import {domain} from '../bbcode/core';
     import {ImagePreviewMutator} from './image-preview-mutator';
+    import {Point, screen} from 'electron';
 
     @Component
     export default class ImagePreview extends Vue {
+        private readonly MinTimePreviewVisible = 500;
+
         public visible: boolean = false;
 
         public externalUrlVisible: boolean = false;
         public internalUrlVisible: boolean = false;
 
-        public externalUrl: string|null = null;
-        public internalUrl: string|null = null;
+        public externalUrl: string | null = null;
+        public internalUrl: string | null = null;
 
-        public url: string|null = null;
-        public domain: string|undefined;
+        public url: string | null = null;
+        public domain: string | undefined;
 
         private jsMutator = new ImagePreviewMutator();
         private interval: any = null;
 
         private exitInterval: any = null;
-        private exitUrl: string|null = null;
+        private exitUrl: string | null = null;
+
+        private initialCursorPosition: Point | null = null;
+        private shouldDismiss = false;
+        private visibleSince = 0;
+
 
         @Hook('mounted')
-        onMounted() {
+        onMounted(): void {
             EventBus.$on(
                 'imagepreview-dismiss',
                 (eventData: any) => {
+                    // console.log('Event dismiss', eventData.url);
                     this.dismiss(eventData.url);
                 }
             );
@@ -48,6 +57,7 @@
             EventBus.$on(
                 'imagepreview-show',
                 (eventData: any) => {
+                    // console.log('Event show', eventData.url);
                     this.show(eventData.url);
                 }
             );
@@ -57,6 +67,8 @@
             webview.addEventListener(
                 'dom-ready',
                 () => {
+                    // webview.openDevTools();
+
                     const url = webview.getURL();
 
                     const js = this.jsMutator.getMutatorJsForSite(url);
@@ -64,67 +76,92 @@
                     if (js) {
                         webview.executeJavaScript(js);
                     }
-
-                    // webview.openDevTools();
-
-                    /* webview.executeJavaScript(
-                        "(() => {"
-                            + "$('#topbar').hide();"
-                            + "$('.post-header').hide();"
-                            + "$('#inside').css({padding: 0, margin: 0, width: '100%'});"
-                            + "$('#right-content').hide();"
-                            + "$('.post-container').css({width: '100%'});"
-                            + "$('.post-image img').css({width: '100%', 'min-height': 'unset'});"
-                            + "$('#recommendations').hide();"
-                            + "$('.left').css({float: 'none'});"
-                        + "})()"
-                    );*/
                 }
             );
+
+            webview.getWebContents().on(
+                'did-finish-load',
+                ()=> {
+                    webview.getWebContents().session.on(
+                        'will-download',
+                        (e: any) => {
+                            e.preventDefault();
+                        }
+                    );
+                }
+            );
+
+            setInterval(
+                () => {
+                    if (((this.visible) && (!this.exitInterval) && (!this.shouldDismiss)) || (this.interval))
+                        this.initialCursorPosition = screen.getCursorScreenPoint();
+
+                    if ((this.visible) && (this.shouldDismiss) && (this.hasMouseMovedSince()) && (!this.exitInterval) && (!this.interval))
+                        this.hide();
+                },
+                10
+            );
         }
 
 
-        dismiss(url: string) {
-            if (this.url !== url) {
-                // simply ignore
-                return;
-            }
+        private hide(): void {
+            this.cancelExitTimer();
 
-            let due = this.visible ? 1000 : 0;
+            this.url = null;
+            this.visible = false;
+
+            this.internalUrlVisible = false;
+            this.externalUrlVisible = false;
+
+            this.externalUrl = 'about:blank';
+            this.internalUrl = 'about:blank';
+
+            this.exitUrl = null;
+            this.exitInterval = null;
+
+            this.shouldDismiss = false;
+        }
+
+
+        dismiss(url: string): void {
+            if (this.url !== url)
+                return; // simply ignore
+
+            // console.log('DISMISS');
+
+            const due = this.visible ? this.MinTimePreviewVisible - Math.min(this.MinTimePreviewVisible, (Date.now() - this.visibleSince)) : 0;
 
             this.cancelTimer();
 
-            if (this.exitInterval) {
+            if (this.exitInterval)
                 return;
-            }
 
             this.exitUrl = this.url;
+            this.shouldDismiss = true;
 
+            if (!this.hasMouseMovedSince())
+                return;
+
+            // This timeout makes the preview window disappear with a slight delay, which helps UX
+            // when dealing with situations such as quickly scrolling text that moves the cursor away
+            // from the link
             this.exitInterval = setTimeout(
-                () => {
-                    this.url = null;
-                    this.visible = false;
-
-                    this.internalUrlVisible = false;
-                    this.externalUrlVisible = false;
-
-                    this.externalUrl = 'about:blank';
-                    this.internalUrl = 'about:blank';
-
-                    this.exitUrl = null;
-                    this.exitInterval = null;
-                },
+                () => this.hide(),
                 due
             );
         }
 
 
-        show(url: string) {
-            // url = 'https://imgur.com/a/2uzWx';
-            // url = 'http://lodash.com';
-            // url = 'https://rule34.xxx/index.php?page=post&s=view&id=3254983';
+        show(url: string): void {
+            // console.log('SHOW');
 
-            let due = ((url === this.exitUrl) && (this.exitInterval)) ? 0 : 100;
+            if ((this.visible) && (!this.hasMouseMovedSince()))
+                return;
+
+            if ((this.url === url) && ((this.visible) || (this.interval)))
+                return;
+
+            const due = ((url === this.exitUrl) && (this.exitInterval)) ? 0 : 100;
 
             this.url = url;
             this.domain = domain(url);
@@ -132,6 +169,8 @@
             this.cancelExitTimer();
             this.cancelTimer();
 
+            // This timer makes sure that just by accidentally brushing across a link won't show (blink) the preview
+            // -- you actually have to pause on it
             this.interval = setTimeout(
                 () => {
                     const isInternal = this.isInternalUrl();
@@ -145,46 +184,56 @@
                         this.externalUrl = this.url;
 
                     this.visible = true;
+                    this.visibleSince = Date.now();
+
+                    this.initialCursorPosition = screen.getCursorScreenPoint();
                 },
                 due
             );
         }
 
+        hasMouseMovedSince(): boolean {
+            if (!this.initialCursorPosition)
+                return true;
 
-        cancelTimer() {
-            if (this.interval) {
-                clearTimeout(this.interval);
+            try {
+                const p = screen.getCursorScreenPoint();
+
+                return ((p.x !== this.initialCursorPosition.x) || (p.y !== this.initialCursorPosition.y));
+            } catch (err) {
+                console.error(err);
+                return true;
             }
+        }
+
+        cancelTimer(): void {
+            if (this.interval)
+                clearTimeout(this.interval);
 
             this.interval = null;
         }
 
-
-        cancelExitTimer() {
-            if (this.exitInterval) {
+        cancelExitTimer(): void {
+            if (this.exitInterval)
                 clearTimeout(this.exitInterval);
-            }
 
             this.exitInterval = null;
         }
 
-
-        isVisible() {
+        isVisible(): boolean {
             return this.visible;
         }
 
-
-        getUrl() {
+        getUrl(): string | null {
             return this.url;
         }
 
-
-        isExternalUrl() {
+        isExternalUrl(): boolean {
             return !((this.domain === 'f-list.net') || (this.domain === 'static.f-list.net'));
         }
 
 
-        isInternalUrl() {
+        isInternalUrl(): boolean {
             return !this.isExternalUrl();
         }
     }
diff --git a/chat/UrlTagView.vue b/chat/UrlTagView.vue
index dd5a0e1..8f2e693 100644
--- a/chat/UrlTagView.vue
+++ b/chat/UrlTagView.vue
@@ -1,15 +1,15 @@
 <template>
-    <span
-        @mouseover="show()"
-        @mouseleave="dismiss()"
-    >
+    <span>
         <i class="fa fa-link"></i>
         <a
             :href="url"
             rel="nofollow noreferrer noopener"
             target="_blank"
             class="user-link"
-            :title="url"
+            @mouseover="show()"
+            @mouseenter="show()"
+            @mouseleave="dismiss()"
+            @mouseout="dismiss()"
         >{{text}}</a>
         <span
             class="link-domain bbcode-pseudo"
@@ -34,24 +34,21 @@
         @Prop({required: true})
         readonly domain!: string;
 
-        @Prop()
-        hover!: boolean = false;
-
         @Hook("beforeDestroy")
-        beforeDestroy() {
+        beforeDestroy(): void {
             this.dismiss();
         }
 
         @Hook("deactivated")
-        deactivate() {
+        deactivate(): void {
             this.dismiss();
         }
 
-        dismiss() {
+        dismiss(): void {
             EventBus.$emit('imagepreview-dismiss', {url: this.url});
         }
 
-        show() {
+        show(): void {
             EventBus.$emit('imagepreview-show', {url: this.url});
         }
     }
diff --git a/chat/image-preview-mutator.ts b/chat/image-preview-mutator.ts
index 1726b67..9095c21 100644
--- a/chat/image-preview-mutator.ts
+++ b/chat/image-preview-mutator.ts
@@ -34,17 +34,14 @@ export class ImagePreviewMutator {
     }
 
     protected init() {
-        this.add('e621.net', this.getBaseJsMutatorScript('#image'));
-        this.add('e-hentai.org', this.getBaseJsMutatorScript('#img'));
-        this.add('gelbooru.com', this.getBaseJsMutatorScript('#image'));
-        this.add('chan.sankakucomplex.com', this.getBaseJsMutatorScript('#image'));
+        this.add('e621.net', this.getBaseJsMutatorScript('#image, video'));
+        this.add('e-hentai.org', this.getBaseJsMutatorScript('#img, video'));
+        this.add('gelbooru.com', this.getBaseJsMutatorScript('#image, video'));
+        this.add('chan.sankakucomplex.com', this.getBaseJsMutatorScript('#image, video'));
+        this.add('gfycat.com', this.getBaseJsMutatorScript('video'));
 
-        this.add(
-            'gfycat.com',
-            `${this.getBaseJsMutatorScript('video')}
-            document.querySelector('video').play();
-            `
-        );
+        // this fixes videos only -- images are fine as is
+        this.add('i.imgur.com', this.getBaseJsMutatorScript('video'));
 
         this.add(
             'imgur.com',
@@ -56,25 +53,12 @@ export class ImagePreviewMutator {
                 if(imageCount > 1)
                     $('#flistWrapper').append('<div id="imageCount" style="position: absolute; bottom: 0; right: 0; background: green; border: 2px solid lightgreen; width: 5rem; height: 5rem; font-size: 2rem; font-weight: bold; color: white; border-radius: 5rem; margin: 0.75rem;"><div style="position: absolute; top: 50%; left: 50%; transform: translateY(-50%) translateX(-50%);">+' + (imageCount - 1) + '</div></div>');
             `
-
-            // "$('#topbar').hide();"
-            // + "$('.post-header').hide();"
-            // + "$('#inside').css({padding: 0, margin: 0, width: '100%'});"
-            // + "$('#right-content').hide();"
-            // + "$('.post-container').css({width: '100%'});"
-            // + "$('.post-image img').css({width: 'auto', 'min-height': 'unset', 'max-height': '100vh'});"
-            // + "$('#recommendations').hide();"
-            // + "$('.left').css({float: 'none'});"
-            // + "$('body').css({overflow: 'hidden'});"
-            // + "const imageCount = $('.post-image-container').length;"
-            // + "if(imageCount > 1) {"
-            //     + "$('body').append('<div id=\"imageCount\" style=\"position: absolute; bottom: 0; right: 0; background: green; border: 2px solid lightgreen; width: 5rem; height: 5rem; font-size: 2rem; font-weight: bold; color: white; border-radius: 5rem; margin: 0.75rem;\"><div style=\"position: absolute; top: 50%; left: 50%; transform: translateY(-50%) translateX(-50%);\">+' + (imageCount - 1) + '</div></div>');"
-            // + "}"
         );
 
+
         this.add(
             'rule34.xxx',
-            `${this.getBaseJsMutatorScript('#image')}
+            `${this.getBaseJsMutatorScript('#image, video')}
                 const content = document.querySelector('#content');
                 content.remove();
             `
@@ -95,6 +79,14 @@ export class ImagePreviewMutator {
             body.append(el);
             body.style = 'padding: 0; margin: 0; overflow: hidden; width: 100%; height: 100%';
             img.style = 'object-position: top left; object-fit: contain; width: 100%; height: 100%;'
+            
+            if (img.play) { img.muted = true; img.play(); }
+            
+            let removeList = [];
+            body.childNodes.forEach((el) => { if(el.id !== 'flistWrapper') { removeList.push(el); } });
+            removeList.forEach((el) => el.remove());
+            removeList = [];
+            
         `;
     }
 
diff --git a/electron/Window.vue b/electron/Window.vue
index dd43d47..f76ada3 100644
--- a/electron/Window.vue
+++ b/electron/Window.vue
@@ -84,7 +84,10 @@
 
         @Hook('mounted')
         mounted(): void {
+            // browserWindow.webContents.openDevTools();
+
             this.addTab();
+
             electron.ipcRenderer.on('settings', (_: Event, settings: GeneralSettings) => this.settings = settings);
             electron.ipcRenderer.on('allow-new-tabs', (_: Event, allow: boolean) => this.canOpenTab = allow);
             electron.ipcRenderer.on('open-tab', () => this.addTab());
diff --git a/electron/main.ts b/electron/main.ts
index 049ce70..abc0fdf 100644
--- a/electron/main.ts
+++ b/electron/main.ts
@@ -142,6 +142,7 @@ function createWindow(): Electron.BrowserWindow | undefined {
         window.show();
         if(lastState.maximized) window.maximize();
     });
+
     return window;
 }
 
diff --git a/readme.md b/readme.md
index 8e98543..c995d05 100644
--- a/readme.md
+++ b/readme.md
@@ -1,3 +1,28 @@
+# F-Chat Rising
+
+This repository contains a modified version of the mainline F-Chat 3.0 client.
+
+
+## Key Differences
+
+*   Ad auto-posting
+    *    Manage channel's ad settings in "Tab Settings"
+    *    Automatically repost ads every 11-18 minutes (randomized)
+    *    Auto-posting can rotate through multiple ads
+*   Link previews
+    *    Hover cursor over any `[url]` to see a preview of it
+*   Profile
+    *    Kinks are auto-compared when profile is loaded
+    *    Custom kink explanations are shown inline
+    *    Custom kinks are highlighted  
+    *    Gender, fur/human status, age, and sexual preference are highlighted if compatible or incompatible
+    *    Guestbook, friend, and group counts are visible on tabs
+    *    Character pictures can be expanded inline
+    *    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
+
+
+
 # F-List Exported
 This repository contains the open source parts of F-list and F-Chat 3.0.
 All necessary files to build F-Chat 3.0 as an Electron, mobile or web application are included.
diff --git a/site/character_page/character_page.vue b/site/character_page/character_page.vue
index 4142a5f..e21c617 100644
--- a/site/character_page/character_page.vue
+++ b/site/character_page/character_page.vue
@@ -166,4 +166,86 @@
             this.loading = false;
         }
     }
-</script>
\ No newline at end of file
+</script>
+
+
+<style lang="scss">
+
+    .custom-kink {
+        &:first-child {
+            margin-top: 0;
+        }
+
+        &:last-child {
+            margin-bottom: 0;
+        }
+
+        font-weight: bold;
+        margin-top: 14px;
+        margin-bottom: 14px;
+        margin-left: -6px;
+        margin-right: -6px;
+        color: #f2cd00;
+        border: 1px rgba(255, 255, 255, 0.1) solid;
+        border-radius: 2px;
+        /* border-collapse: collapse; */
+        padding: 5px;
+    }
+
+
+    .kink-custom-desc {
+        display: block;
+        font-weight: normal;
+        font-size: 0.9rem;
+        color: rgba(255, 255, 255, 0.7);
+        line-height: 125%;
+    }
+
+
+    .infotag-label {
+        display: block;
+        /* margin-bottom: 1rem; */
+        font-weight: normal !important;
+        line-height: 120%;
+        font-size: 85%;
+        color: rgba(255, 255, 255, 0.7);
+    }
+
+
+    .infotag-value {
+        display: block;
+        margin-bottom: 1rem;
+        font-weight: bold;
+        line-height: 120%;
+    }
+
+    .quick-info-value {
+        display: block;
+        font-weight: bold;
+    }
+
+    .quick-info-label {
+        display: block;
+        /* margin-bottom: 1rem; */
+        font-weight: normal !important;
+        line-height: 120%;
+        font-size: 85%;
+        color: rgba(255, 255, 255, 0.7);
+    }
+
+    .quick-info {
+        margin-bottom: 1rem;
+        font-size: 0.9rem;
+        color: rgba(255, 255, 255, 0.7);
+    }
+
+    img.character-image {
+        max-width: 33% !important;
+        width: 33% !important;
+        height: auto !important;
+        object-fit: contain;
+        object-position: top center;
+        vertical-align: top !important;
+    }
+
+</style>
\ No newline at end of file
diff --git a/site/character_page/images.vue b/site/character_page/images.vue
index 873727d..42950dc 100644
--- a/site/character_page/images.vue
+++ b/site/character_page/images.vue
@@ -1,12 +1,19 @@
 <template>
-    <div class="character-images row">
+<!--    <div class="character-images row">-->
+    <div class="character-images">
         <div v-show="loading" class="alert alert-info">Loading images.</div>
         <template v-if="!loading">
-            <div class="character-image col-6 col-sm-4 col-md-2" v-for="image in images" :key="image.id">
-                <a :href="imageUrl(image)" target="_blank" @click="handleImageClick($event, image)">
-                    <img :src="thumbUrl(image)" :title="image.description">
-                </a>
-            </div>
+            <img :src="imageUrl(image)" :title="image.description" class="character-image" v-for="image in images" :key="image.id">
+
+<!--            <div class="character-image col-6 col-sm-12 col-md-12" v-for="image in images" :key="image.id">-->
+<!--                <img :src="imageUrl(image)" :title="image.description">-->
+<!--            </div>-->
+<!--                -->
+
+<!--                <a :href="imageUrl(image)" target="_blank" @click="handleImageClick($event, image)">-->
+<!--                    <img :src="thumbUrl(image)" :title="image.description">-->
+<!--                </a>-->
+<!--            </div>-->
         </template>
         <div v-if="!loading && !images.length" class="alert alert-info">No images.</div>
         <div class="image-preview" v-show="previewImage" @click="previewImage = ''">
diff --git a/site/character_page/infotag.vue b/site/character_page/infotag.vue
index 473c12a..113ae9e 100644
--- a/site/character_page/infotag.vue
+++ b/site/character_page/infotag.vue
@@ -1,6 +1,6 @@
 <template>
     <div class="infotag">
-        <span class="infotag-label">{{label}}: </span>
+        <span class="infotag-label">{{label}}</span>
         <span v-if="!contactLink" class="infotag-value">{{value}}</span>
         <span v-if="contactLink" class="infotag-value"><a :href="contactLink">{{value}}</a></span>
     </div>
diff --git a/site/character_page/kink.vue b/site/character_page/kink.vue
index 1a7b4a1..9896f07 100644
--- a/site/character_page/kink.vue
+++ b/site/character_page/kink.vue
@@ -2,15 +2,16 @@
     <div class="character-kink" :class="kinkClasses" :id="kinkId" @click="toggleSubkinks" :data-custom="customId"
         @mouseover.stop="showTooltip = true" @mouseout.stop="showTooltip = false">
         <i v-show="kink.hasSubkinks" class="fa" :class="{'fa-minus': !listClosed, 'fa-plus': listClosed}"></i>
-        <i v-show="!kink.hasSubkinks && kink.isCustom" class="far fa-dot-circle custom-kink-icon"></i>
+        <i v-show="!kink.hasSubkinks && kink.isCustom" class="far custom-kink-icon"></i>
         <span class="kink-name">{{ kink.name }}</span>
+        <span class="kink-custom-desc" v-if="(kink.isCustom)">{{kink.description}}</span>
         <template v-if="kink.hasSubkinks">
             <div class="subkink-list" :class="{closed: this.listClosed}">
                 <kink v-for="subkink in kink.subkinks" :kink="subkink" :key="subkink.id" :comparisons="comparisons"
                     :highlights="highlights"></kink>
             </div>
         </template>
-        <div class="popover popover-top" v-if="showTooltip" style="display:block;bottom:100%;top:initial;margin-bottom:5px">
+        <div class="popover popover-top" v-if="((showTooltip) && (!kink.isCustom))" style="display:block;bottom:100%;top:initial;margin-bottom:5px">
             <div class="arrow" style="left:10%"></div>
             <h5 class="popover-header">{{kink.name}}</h5>
             <div class="popover-body"><p>{{kink.description}}</p></div>
diff --git a/site/character_page/kinks.vue b/site/character_page/kinks.vue
index 403034c..cc97baf 100644
--- a/site/character_page/kinks.vue
+++ b/site/character_page/kinks.vue
@@ -129,19 +129,16 @@
             this.highlighting = toAssign;
         }
 
-
         @Hook('mounted')
         async mounted(): Promise<void> {
             await this.compareKinks();
         }
 
-
         @Watch('character')
-        characterChanged(): void {
-            this.compareKinks();
+        async characterChanged(): Promise<void> {
+            await this.compareKinks();
         }
 
-
         get kinkGroups(): {[key: string]: KinkGroup | undefined} {
             return this.shared.kinks.kink_groups;
         }
diff --git a/site/character_page/sidebar.vue b/site/character_page/sidebar.vue
index 4b4c582..91bca7d 100644
--- a/site/character_page/sidebar.vue
+++ b/site/character_page/sidebar.vue
@@ -45,23 +45,23 @@
             <div class="quick-info-block">
                 <infotag-item v-for="infotag in quickInfoItems" :infotag="infotag" :key="infotag.id"></infotag-item>
                 <div class="quick-info">
-                    <span class="quick-info-label">Created: </span>
+                    <span class="quick-info-label">Created</span>
                     <span class="quick-info-value"><date :time="character.character.created_at"></date></span>
                 </div>
                 <div class="quick-info">
-                    <span class="quick-info-label">Last updated: </span>
+                    <span class="quick-info-label">Last Updated </span>
                     <span class="quick-info-value"><date :time="character.character.updated_at"></date></span>
                 </div>
                 <div class="quick-info" v-if="character.character.last_online_at">
-                    <span class="quick-info-label">Last online:</span>
+                    <span class="quick-info-label">Last Online</span>
                     <span class="quick-info-value"><date :time="character.character.last_online_at"></date></span>
                 </div>
                 <div class="quick-info">
-                    <span class="quick-info-label">Views: </span>
+                    <span class="quick-info-label">Views</span>
                     <span class="quick-info-value">{{character.character.views}}</span>
                 </div>
                 <div class="quick-info" v-if="character.character.timezone != null">
-                    <span class="quick-info-label">Timezone:</span>
+                    <span class="quick-info-label">Timezone</span>
                     <span class="quick-info-value">
                     UTC{{character.character.timezone > 0 ? '+' : ''}}{{character.character.timezone != 0 ? character.character.timezone : ''}}
                 </span>