ad editor / post ads
This commit is contained in:
parent
4a56ef254a
commit
5b0e8e2e68
17
CHANGELOG.md
17
CHANGELOG.md
|
@ -1,5 +1,22 @@
|
|||
# Changelog
|
||||
|
||||
## Todo
|
||||
* Settings allow selecting which match comparisons you care about
|
||||
* Import/Export ads
|
||||
|
||||
## 1.20.0
|
||||
* Kink scoring is skipped if characters have only a few shared kinks
|
||||
* Kink scoring gives more weight to 'favorite' and 'no' categories
|
||||
* Fixed auto-responder not responding to previously unknown characters
|
||||
* Fixed re-order tabs
|
||||
* Added Ad Editor:
|
||||
* Central ad editor for all ads
|
||||
* Button near 'Console' in the Sidebar
|
||||
* Added Post Ads:
|
||||
* Select ads based on your mood, preference, etc.
|
||||
* Launch ads on multiple channels
|
||||
* Button near 'Console' in the Sidebar
|
||||
|
||||
## 1.19.3
|
||||
* Added option to have character portrait displayed next to text input
|
||||
* Fixed asexual orientation ID
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
# Download
|
||||
[Windows](https://github.com/mrstallion/fchat-rising/releases/download/v1.19.3/F-Chat-Rising-1.19.3-win.exe) (82 MB)
|
||||
| [MacOS Intel](https://github.com/mrstallion/fchat-rising/releases/download/v1.19.3/F-Chat-Rising-1.19.3-macos-intel.dmg) (82 MB)
|
||||
| [MacOS M1](https://github.com/mrstallion/fchat-rising/releases/download/v1.19.3/F-Chat-Rising-1.19.3-macos-m1.dmg) (84 MB)
|
||||
| [Linux](https://github.com/mrstallion/fchat-rising/releases/download/v1.19.3/F-Chat-Rising-1.19.3-linux.AppImage) (82 MB)
|
||||
[Windows](https://github.com/mrstallion/fchat-rising/releases/download/v1.20.0/F-Chat-Rising-1.20.0-win.exe) (82 MB)
|
||||
| [MacOS Intel](https://github.com/mrstallion/fchat-rising/releases/download/v1.20.0/F-Chat-Rising-1.20.0-macos-intel.dmg) (82 MB)
|
||||
| [MacOS M1](https://github.com/mrstallion/fchat-rising/releases/download/v1.20.0/F-Chat-Rising-1.20.0-macos-m1.dmg) (84 MB)
|
||||
| [Linux](https://github.com/mrstallion/fchat-rising/releases/download/v1.20.0/F-Chat-Rising-1.20.0-linux.AppImage) (82 MB)
|
||||
|
||||
|
||||
# F-Chat Rising
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
style="border-bottom-left-radius:0;border-bottom-right-radius:0" v-if="hasToolbar">
|
||||
<i class="fa fa-code"></i>
|
||||
</a>
|
||||
<div class="bbcode-toolbar btn-toolbar" role="toolbar" :style="showToolbar ? {display: 'flex'} : undefined" @mousedown.stop.prevent
|
||||
<div class="bbcode-toolbar btn-toolbar" role="toolbar" :disabled="disabled" :style="showToolbar ? {display: 'flex'} : undefined" @mousedown.stop.prevent
|
||||
v-if="hasToolbar" style="flex:1 51%">
|
||||
<div class="btn-group" style="flex-wrap:wrap">
|
||||
<div v-if="!!characterName" class="character-btn">
|
||||
|
|
|
@ -19,6 +19,18 @@
|
|||
{{l('settings.open')}}</a></div>
|
||||
<div><a href="#" @click.prevent="showRecent()" class="btn"><span class="fas fa-history"></span>
|
||||
{{l('chat.recentConversations')}}</a></div>
|
||||
|
||||
<div><a href="#" @click.prevent="showAdCenter()" class="btn"><span class="fas fa-ad"></span>
|
||||
Ad Editor</a></div>
|
||||
|
||||
<div><a href="#" @click.prevent="showAdLauncher()" class="btn"><span class="fas fa-play"></span>
|
||||
Post Ads</a>
|
||||
|
||||
<span v-show="adsAreRunning()" class="adControls">
|
||||
<span aria-label="Stop All Ads" class="fas fa-stop" @click.prevent="stopAllAds()"></span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="list-group conversation-nav">
|
||||
<a :class="getClasses(conversations.consoleTab)" href="#" @click.prevent="conversations.consoleTab.show()"
|
||||
class="list-group-item list-group-item-action">
|
||||
|
@ -51,6 +63,7 @@
|
|||
</div>
|
||||
<a href="#" @click.prevent="showChannels()" class="btn"><span class="fas fa-list"></span>
|
||||
{{l('chat.channels')}}</a>
|
||||
|
||||
<div class="list-group conversation-nav" ref="channelConversations">
|
||||
<a v-for="conversation in conversations.channelConversations" href="#" @click.prevent="conversation.show()"
|
||||
:class="getClasses(conversation)" class="list-group-item list-group-item-action item-channel" :key="conversation.key"
|
||||
|
@ -92,6 +105,8 @@
|
|||
<channels ref="channelsDialog"></channels>
|
||||
<status-switcher ref="statusDialog"></status-switcher>
|
||||
<character-search ref="searchDialog"></character-search>
|
||||
<adLauncher ref="adLauncher"></adLauncher>
|
||||
<adCenter ref="adCenter"></adCenter>
|
||||
<settings ref="settingsDialog"></settings>
|
||||
<report-dialog ref="reportDialog"></report-dialog>
|
||||
<user-menu ref="userMenu" :reportDialog="$refs['reportDialog']"></user-menu>
|
||||
|
@ -131,6 +146,8 @@
|
|||
import NoteStatus from '../site/NoteStatus.vue';
|
||||
import { Dialog } from '../helpers/dialog';
|
||||
// import { EventBus } from './preview/event-bus';
|
||||
import AdCenterDialog from './ads/AdCenter.vue';
|
||||
import AdLauncherDialog from './ads/AdLauncher.vue';
|
||||
|
||||
const unreadClasses = {
|
||||
[Conversation.UnreadState.None]: '',
|
||||
|
@ -145,7 +162,9 @@
|
|||
'user-menu': UserMenu, 'recent-conversations': RecentConversations,
|
||||
'image-preview': ImagePreview,
|
||||
'add-pm-partner': PmPartnerAdder,
|
||||
'note-status': NoteStatus
|
||||
'note-status': NoteStatus,
|
||||
adCenter: AdCenterDialog,
|
||||
adLauncher: AdLauncherDialog
|
||||
}
|
||||
})
|
||||
export default class ChatView extends Vue {
|
||||
|
@ -216,6 +235,8 @@
|
|||
}, (value) => {
|
||||
this.setFontSize(value);
|
||||
});
|
||||
|
||||
void core.adCenter.load();
|
||||
}
|
||||
|
||||
@Hook('destroyed')
|
||||
|
@ -341,6 +362,14 @@
|
|||
(<StatusSwitcher>this.$refs['statusDialog']).show();
|
||||
}
|
||||
|
||||
showAdCenter(): void {
|
||||
(<AdCenterDialog>this.$refs['adCenter']).show();
|
||||
}
|
||||
|
||||
showAdLauncher(): void {
|
||||
(<AdLauncherDialog>this.$refs['adLauncher']).show();
|
||||
}
|
||||
|
||||
showAddPmPartner(): void {
|
||||
(<PmPartnerAdder>this.$refs['addPmPartnerDialog']).show();
|
||||
}
|
||||
|
@ -372,6 +401,14 @@
|
|||
getImagePreview(): ImagePreview | undefined {
|
||||
return this.$refs['imagePreview'] as ImagePreview;
|
||||
}
|
||||
|
||||
adsAreRunning(): boolean {
|
||||
return core.adCenter.adsAreRunning();
|
||||
}
|
||||
|
||||
stopAllAds(): void {
|
||||
core.adCenter.stopAllAds();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
|
@ -543,5 +580,20 @@
|
|||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.adControls {
|
||||
float: right;
|
||||
margin-right: 0.25rem;
|
||||
margin-top: 3px;
|
||||
|
||||
span {
|
||||
color: var(--danger);
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
color: var(--red);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -0,0 +1,162 @@
|
|||
<template>
|
||||
<modal :action="'Ad Editor'" @submit="submit" ref="dialog" @open="load" dialogClass="w-100"
|
||||
:buttonText="'Save'">
|
||||
|
||||
<div class="form-group ad-list" v-for="(ad, index) in ads">
|
||||
<label :for="'adm-content-' + index" class="control-label">Ad #{{(index + 1)}}
|
||||
<a v-if="(index > 0)" @click="moveAdUp(index)" title="Move Up"><i class="fa fa-arrow-up"></i></a>
|
||||
<a v-if="(index < ads.length - 1)" @click="moveAdDown(index)" title="Move Down"><i class="fa fa-arrow-down"></i></a>
|
||||
<a @click="removeAd(index)" title="Remove Ad"><i class="fas fa-times-circle"></i></a>
|
||||
</label>
|
||||
|
||||
<editor :id="'adm-content-' + index" v-model="ad.content" :hasToolbar="true" class="form-control" :maxlength="core.connection.vars.lfrp_max" :disabled="ad.disabled">
|
||||
</editor>
|
||||
|
||||
<tagEditor :id="'adm-tags-' + index" v-model="ad.tags" placeholder="Enter one or more tags, e.g. 'romantic'" :add-tag-on-keys="[13, 188, 9, 32]" class="form-control" :disabled="ad.disabled" :add-tag-on-blur="true"></tagEditor>
|
||||
|
||||
<label class="control-label disable" :for="'adm-disabled-' + index">
|
||||
<input type="checkbox" :id="'adm-disabled-' + index" v-model='ad.disabled' />
|
||||
Disable
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<button class="btn btn-outline-secondary" @click="addAd()">Add Another</button>
|
||||
|
||||
</modal>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {Component} from '@f-list/vue-ts';
|
||||
import CustomDialog from '../../components/custom_dialog';
|
||||
import Modal from '../../components/Modal.vue';
|
||||
import {Conversation} from '../interfaces';
|
||||
import l from '../localize';
|
||||
import {Editor} from '../bbcode';
|
||||
import core from '../core';
|
||||
import { Dialog } from '../../helpers/dialog';
|
||||
import InputTag from 'vue-input-tag';
|
||||
import { Ad } from './ad-center';
|
||||
import _ from 'lodash';
|
||||
|
||||
@Component({
|
||||
components: {modal: Modal, editor: Editor, tagEditor: InputTag},
|
||||
})
|
||||
export default class AdCenterDialog extends CustomDialog {
|
||||
l = l;
|
||||
setting = Conversation.Setting;
|
||||
ads!: Ad[];
|
||||
core = core;
|
||||
|
||||
load(): void {
|
||||
this.ads = _.cloneDeep(core.adCenter.get());
|
||||
|
||||
if (this.ads.length === 0) {
|
||||
this.addAd();
|
||||
}
|
||||
}
|
||||
|
||||
async submit(): Promise<void> {
|
||||
await core.adCenter.set(this.ads);
|
||||
}
|
||||
|
||||
addAd(): void {
|
||||
this.ads.push({
|
||||
content: '',
|
||||
disabled: false,
|
||||
tags: []
|
||||
});
|
||||
}
|
||||
|
||||
removeAd(index: number): void {
|
||||
// if (confirm('Are you sure you wish to remove this ad?')) {
|
||||
if (Dialog.confirmDialog('Are you sure you wish to remove this ad?')) {
|
||||
this.ads.splice(index, 1);
|
||||
}
|
||||
}
|
||||
|
||||
moveAdUp(index: number): void {
|
||||
const ad = this.ads.splice(index, 1);
|
||||
|
||||
this.ads.splice(index - 1, 0, ad[0]);
|
||||
}
|
||||
|
||||
|
||||
moveAdDown(index: number): void {
|
||||
const ad = this.ads.splice(index, 1);
|
||||
|
||||
this.ads.splice(index + 1, 0, ad[0]);
|
||||
}
|
||||
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.w-100 {
|
||||
min-width: 70%;
|
||||
}
|
||||
|
||||
.form-group.ad-list {
|
||||
label {
|
||||
font-size: 140%;
|
||||
|
||||
a {
|
||||
padding-right: 0.3rem;
|
||||
opacity:0.3;
|
||||
font-size: 70%;
|
||||
|
||||
&:hover {
|
||||
opacity:0.6
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.bbcode-preview {
|
||||
margin-top: 0;
|
||||
border: 1px solid;
|
||||
padding: 5px;
|
||||
border-radius: 0 5px 5px 5px;
|
||||
background: var(--input-bg);
|
||||
border-color: var(--secondary);
|
||||
}
|
||||
|
||||
.bbcode-editor {
|
||||
border: none;
|
||||
background: none;
|
||||
height: auto;
|
||||
|
||||
textarea {
|
||||
width: 100%;
|
||||
color: var(--input-color);
|
||||
background-color: var(--input-bg);
|
||||
border-radius: 0 5px 5px 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.vue-input-tag-wrapper[disabled=disabled],
|
||||
textarea[disabled=disabled],
|
||||
div.bbcode-toolbar[disabled=disabled] {
|
||||
opacity: 0.5;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.vue-input-tag-wrapper {
|
||||
margin: 0.375rem 0.75rem;
|
||||
width: auto;
|
||||
height: auto;
|
||||
padding-bottom: 0;
|
||||
|
||||
input {
|
||||
width: 260px;
|
||||
color: var(--gray-dark);
|
||||
}
|
||||
}
|
||||
|
||||
label.disable {
|
||||
color: var(--gray-dark);
|
||||
margin: 0.375rem 0.75rem;
|
||||
font-size: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
|
|
@ -0,0 +1,197 @@
|
|||
<template>
|
||||
<modal action="Post Ads" @submit="submit" ref="dialog" @open="load" dialogClass="w-100" class="adLauncher" :buttonText="'Start Posting Ads'">
|
||||
<div v-if="hasAds()">
|
||||
<h4>Ad Tags</h4>
|
||||
<div class="form-group">
|
||||
<p>Serve ads that match any one of these tags:</p>
|
||||
|
||||
<label class="control-label" :for="`adr-tag-${index}`" v-for="(tag,index) in tags">
|
||||
<input type="checkbox" v-model="tag.value" :id="`adr-tag-${index}`" />
|
||||
{{ tag.title }}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<h4>Target Channels</h4>
|
||||
<div class="form-group">
|
||||
<p>Serve ads on these channels:</p>
|
||||
|
||||
<label class="control-label" :for="`adr-channel-${index}`" v-for="(channel,index) in channels">
|
||||
<input type="checkbox" v-model="channel.value" :id="`adr-channel-${index}`" />
|
||||
{{ channel.title }}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<h4>Post Order</h4>
|
||||
<div class="form-group">
|
||||
<label class="control-label" for="adOrderRandom">
|
||||
<input type="radio" v-model="adOrder" value="random" id="adOrderRandom" />
|
||||
Random order
|
||||
</label>
|
||||
<label class="control-label" for="adOrderAdCenter">
|
||||
<input type="radio" v-model="adOrder" value="ad-center" id="adOrderAdCenter" />
|
||||
Follow order in Ad Center
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<h4>Campaign</h4>
|
||||
<div class="form-group">
|
||||
<label class="control-label" for="timeoutMinutes">
|
||||
Timeout
|
||||
</label>
|
||||
|
||||
<select class="form-control" v-model="timeoutMinutes" id="timeoutMinutes">
|
||||
<option v-for="timeout in timeoutOptions" :value="timeout.value">{{timeout.title}}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<p class="matches">
|
||||
<b>{{matchCount}}</b> ads will be used.
|
||||
</p>
|
||||
</div>
|
||||
<div v-else>
|
||||
<h4>No Ads to Post!</h4>
|
||||
|
||||
<p>Use the <button class="btn btn-outline-secondary" @click="openAdEditor()">Ad Editor</button> to create some ads first, then return here to post them.</p>
|
||||
</div>
|
||||
</modal>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {Component, Watch} from '@f-list/vue-ts';
|
||||
import CustomDialog from '../../components/custom_dialog';
|
||||
import Modal from '../../components/Modal.vue';
|
||||
import core from '../core';
|
||||
import _ from 'lodash';
|
||||
import AdCenterDialog from './AdCenter.vue';
|
||||
|
||||
@Component({
|
||||
components: {modal: Modal}
|
||||
})
|
||||
export default class AdLauncherDialog extends CustomDialog {
|
||||
adOrder: 'random' | 'ad-center' = 'random';
|
||||
|
||||
matchCount = 0;
|
||||
|
||||
timeoutMinutes = 180;
|
||||
|
||||
tags: { value: boolean, title: string }[] = [];
|
||||
|
||||
channels: { value: boolean, title: string, id: string }[] = [];
|
||||
|
||||
timeoutOptions = [
|
||||
{ value: 30, title: '30 minutes' },
|
||||
{ value: 60, title: '1 hour' },
|
||||
{ value: 120, title: '2 hours' },
|
||||
{ value: 180, title: '3 hours' }
|
||||
]
|
||||
|
||||
load() {
|
||||
this.channels = _.map(_.filter(core.channels.joinedChannels, (c) => (c.mode === 'ads' || c.mode === 'both')),
|
||||
(c) => ({ value: false, title: c.name, id: c.id }));
|
||||
|
||||
this.tags = _.map(core.adCenter.getActiveTags(), (t) => ({ value: false, title: t }));
|
||||
|
||||
this.checkCanSubmit();
|
||||
}
|
||||
|
||||
hasAds(): boolean {
|
||||
return core.adCenter.getActiveAds().length > 0;
|
||||
}
|
||||
|
||||
@Watch('tags', { deep: true })
|
||||
updateTags(): void {
|
||||
this.matchCount = core.adCenter.getMatchingAds(this.getWantedTags()).length;
|
||||
this.checkCanSubmit();
|
||||
}
|
||||
|
||||
@Watch('channels', { deep: true })
|
||||
updateChannels(): void {
|
||||
this.checkCanSubmit();
|
||||
}
|
||||
|
||||
checkCanSubmit() {
|
||||
const channelCount = _.filter(this.channels, (channel) => channel.value).length;
|
||||
const tagCount = _.filter(this.tags, (tag) => tag.value).length;
|
||||
|
||||
this.dialog.forceDisabled(tagCount === 0 || channelCount === 0);
|
||||
}
|
||||
|
||||
getWantedTags(): string[] {
|
||||
return _.map(_.filter(this.tags, (t) => t.value), (t) => t.title);
|
||||
}
|
||||
|
||||
getWantedChannels(): string[] {
|
||||
return _.map(_.filter(this.channels, (t) => t.value), (t) => t.id);
|
||||
}
|
||||
|
||||
openAdEditor(): void {
|
||||
this.hide();
|
||||
(<AdCenterDialog>this.$parent.$refs['adCenter']).show();
|
||||
}
|
||||
|
||||
submit(e: Event) {
|
||||
const tags = this.getWantedTags();
|
||||
const channelIds = this.getWantedChannels();
|
||||
|
||||
if (tags.length === 0) {
|
||||
e.preventDefault();
|
||||
alert('Select at least one tag to post');
|
||||
return;
|
||||
}
|
||||
|
||||
if (channelIds.length === 0) {
|
||||
e.preventDefault();
|
||||
alert('Select at least one channel to post in');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_.every(channelIds, (channelId) => {
|
||||
if (core.adCenter.isSafeToOverride(channelId)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const chan = core.channels.getChannel(channelId);
|
||||
|
||||
if (!chan) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return confirm(`Warning: This action will overwrite ads on channel ${chan.name}. Ads that are not stored in the Ad Center will be lost. Are you sure you wish to continue?`);
|
||||
})) {
|
||||
e.preventDefault();
|
||||
return;
|
||||
}
|
||||
|
||||
core.adCenter.schedule(
|
||||
tags,
|
||||
channelIds,
|
||||
this.adOrder,
|
||||
this.timeoutMinutes
|
||||
);
|
||||
|
||||
this.hide();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.adLauncher {
|
||||
label {
|
||||
display: block;
|
||||
margin-left: 0.75rem;
|
||||
color: var(--gray-dark);
|
||||
}
|
||||
|
||||
select {
|
||||
margin-left: 0.75rem;
|
||||
width: auto;
|
||||
padding-right: 1.5rem;
|
||||
}
|
||||
|
||||
.matches {
|
||||
margin: 0;
|
||||
margin-top: 2rem;
|
||||
color: var(--gray);
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -2,6 +2,24 @@
|
|||
<modal :action="`Ads for ${conversation.name}`" @submit="submit" ref="dialog" @open="load()" dialogClass="w-100"
|
||||
:buttonText="l('conversationSettings.save')">
|
||||
|
||||
<div class="phased-out-warning">
|
||||
<h4>Prepare to Move</h4>
|
||||
|
||||
<p>
|
||||
Channel-specific ads are being phased out.
|
||||
|
||||
Use <button class="btn btn-outline-secondary" @click="openAdEditor()">Ad Editor</button>
|
||||
and
|
||||
<button class="btn btn-outline-secondary" @click="openPostAds()">Post Ads</button>
|
||||
instead,
|
||||
always available on the left sidebar.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<button class="btn btn-outline-secondary" @click="copyAds()">Copy Channel Ads to Ad Editor</button>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="control-label" for="randomOrder">
|
||||
<input type="checkbox" v-model="randomOrder" id="randomOrder" />
|
||||
|
@ -33,6 +51,8 @@
|
|||
import {Editor} from '../bbcode';
|
||||
import core from '../core';
|
||||
import { Dialog } from '../../helpers/dialog';
|
||||
import AdCenterDialog from './AdCenter.vue';
|
||||
import _ from 'lodash';
|
||||
|
||||
@Component({
|
||||
components: {modal: Modal, editor: Editor}
|
||||
|
@ -94,6 +114,28 @@
|
|||
this.ads.splice(index + 1, 0, ad[0]);
|
||||
}
|
||||
|
||||
openAdEditor() {
|
||||
this.hide();
|
||||
(<AdCenterDialog>this.$parent.$parent.$refs['adCenter']).show();
|
||||
}
|
||||
|
||||
openPostAds() {
|
||||
this.hide();
|
||||
(<AdCenterDialog>this.$parent.$parent.$refs['adLauncher']).show();
|
||||
}
|
||||
|
||||
async copyAds(): Promise<void> {
|
||||
await Promise.all(_.map(
|
||||
this.ads,
|
||||
async (ad) => {
|
||||
if (core.adCenter.isMissingFromAdCenter(ad)) {
|
||||
await core.adCenter.add(ad);
|
||||
}
|
||||
}
|
||||
));
|
||||
|
||||
this.openAdEditor();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
|
@ -139,5 +181,11 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
.phased-out-warning {
|
||||
border: 1px solid orange;
|
||||
padding: 15px;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
</style>
|
||||
|
||||
|
|
|
@ -0,0 +1,118 @@
|
|||
import _ from 'lodash';
|
||||
import core from '../core';
|
||||
import { Conversation } from '../interfaces';
|
||||
|
||||
export interface Ad {
|
||||
disabled: boolean;
|
||||
tags: string[];
|
||||
content: string;
|
||||
}
|
||||
|
||||
export class AdCenter {
|
||||
private ads: Ad[] = [];
|
||||
|
||||
async load(): Promise<void> {
|
||||
this.ads = (await core.settingsStore.get('ads')) || [];
|
||||
}
|
||||
|
||||
get(): Ad[] {
|
||||
return this.ads;
|
||||
}
|
||||
|
||||
async set(ads: Ad[]): Promise<void> {
|
||||
const cleanedAds = _.map(
|
||||
_.filter(ads, (ad) => (ad.content.trim().length > 0)),
|
||||
(ad): Ad => {
|
||||
const filteredTags = _.map(_.filter(ad.tags, (tag) => tag.trim().length > 0), (tag) => tag.trim());
|
||||
|
||||
return {
|
||||
...ad,
|
||||
content: ad.content.trim(),
|
||||
tags: filteredTags.length > 0 ? filteredTags : ['default']
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
this.ads = cleanedAds;
|
||||
|
||||
await core.settingsStore.set('ads', cleanedAds);
|
||||
}
|
||||
|
||||
async add(content: string, tags: string[] = ['default']): Promise<void> {
|
||||
this.ads.push({ content, tags, disabled: false });
|
||||
|
||||
await this.set(this.ads);
|
||||
}
|
||||
|
||||
getTags(ads: Ad[] = this.ads): string[] {
|
||||
return _.uniq(_.flatten(_.map(ads, (ad) => ad.tags)));
|
||||
}
|
||||
|
||||
getActiveTags(): string[] {
|
||||
return this.getTags(this.getActiveAds());
|
||||
}
|
||||
|
||||
getActiveAds(): Ad[] {
|
||||
return _.filter(this.ads, (ad) => !ad.disabled);
|
||||
}
|
||||
|
||||
getMatchingAds(tags: string[]): Ad[] {
|
||||
return _.filter(this.ads, (ad) => !ad.disabled && _.intersection(ad.tags, tags).length > 0);
|
||||
}
|
||||
|
||||
schedule(tags: string[], channelIds: string[], order: 'random' | 'ad-center', timeoutMinutes: number): void {
|
||||
const ads = this.getMatchingAds(tags);
|
||||
|
||||
_.each(channelIds, (channelId) => this.scheduleForChannel(channelId, ads, order, timeoutMinutes));
|
||||
}
|
||||
|
||||
adsAreRunning(): boolean {
|
||||
return !_.every(core.conversations.channelConversations, (conv) => !conv.isSendingAutomatedAds());
|
||||
}
|
||||
|
||||
stopAllAds(): void {
|
||||
_.each(core.conversations.channelConversations, (conv) => conv.adManager.stop());
|
||||
}
|
||||
|
||||
protected getConversation(channelId: string): Conversation.ChannelConversation | undefined {
|
||||
return core.conversations.channelConversations.find((c) => c.channel.id === channelId);
|
||||
}
|
||||
|
||||
isMissingFromAdCenter(adContentToTest: string): boolean {
|
||||
const cleaned = adContentToTest.trim().toLowerCase();
|
||||
|
||||
return _.every(this.ads, (ad) => ad.content.trim().toLowerCase() !== cleaned);
|
||||
}
|
||||
|
||||
isSafeToOverride(channelId: string): boolean {
|
||||
const conv = this.getConversation(channelId);
|
||||
|
||||
if (!conv) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return _.every(conv.settings.adSettings.ads, (adContent) => !this.isMissingFromAdCenter(adContent));
|
||||
}
|
||||
|
||||
// tslint:disable-next-line:prefer-function-over-method
|
||||
protected scheduleForChannel(channelId: string, ads: Ad[], order: 'random' | 'ad-center', timeoutMinutes: number): void {
|
||||
const conv = this.getConversation(channelId);
|
||||
|
||||
if (!conv) {
|
||||
return;
|
||||
}
|
||||
|
||||
conv.settings = {
|
||||
...conv.settings,
|
||||
|
||||
adSettings: {
|
||||
...conv.settings.adSettings,
|
||||
ads: _.map(_.filter(ads, (ad) => !ad.disabled && ad.content.trim().length > 0), (ad) => ad.content.trim()),
|
||||
randomOrder: order === 'random'
|
||||
}
|
||||
};
|
||||
|
||||
conv.adManager.stop();
|
||||
conv.adManager.start(timeoutMinutes * 60 * 1000);
|
||||
}
|
||||
}
|
|
@ -163,7 +163,7 @@ export class AdManager {
|
|||
return this.firstPost;
|
||||
}
|
||||
|
||||
start(): void {
|
||||
start(timeoutMs = AdManager.POSTING_PERIOD): void {
|
||||
const chanConv = (<Conversation.ChannelConversation>this.conversation);
|
||||
|
||||
const initialWait = Math.max(
|
||||
|
@ -180,7 +180,7 @@ export class AdManager {
|
|||
(this.conversation.settings.adSettings.lastAdTimestamp || 0) + (core.connection.vars.lfrp_flood * 1000)
|
||||
));
|
||||
|
||||
this.expireDue = new Date(Date.now() + AdManager.POSTING_PERIOD);
|
||||
this.expireDue = new Date(Date.now() + timeoutMs);
|
||||
this.adMap = this.generateAdMap();
|
||||
|
||||
// tslint:disable-next-line: no-unnecessary-type-assertion
|
||||
|
|
|
@ -6,6 +6,7 @@ import {Settings as SettingsImpl} from './common';
|
|||
import Conversations from './conversations';
|
||||
import {Channel, Character, Connection, Conversation, Logs, Notifications, Settings, State as StateInterface} from './interfaces';
|
||||
import { AdCoordinatorGuest } from './ads/ad-coordinator-guest';
|
||||
import { AdCenter } from './ads/ad-center';
|
||||
import { GeneralSettings } from '../electron/common';
|
||||
import { SiteSession } from '../site/site-session';
|
||||
import _ from 'lodash';
|
||||
|
@ -64,6 +65,7 @@ const data = {
|
|||
notifications: <Notifications | undefined>undefined,
|
||||
cache: <CacheManager | undefined>undefined,
|
||||
adCoordinator: <AdCoordinatorGuest | undefined>undefined,
|
||||
adCenter: <AdCenter | undefined>undefined,
|
||||
siteSession: <SiteSession | undefined>undefined,
|
||||
|
||||
register<K extends 'characters' | 'conversations' | 'channels'>(module: K, subState: VueState[K]): void {
|
||||
|
@ -96,6 +98,7 @@ export function init(
|
|||
data.notifications = new notificationsClass();
|
||||
data.cache = new CacheManager();
|
||||
data.adCoordinator = new AdCoordinatorGuest();
|
||||
data.adCenter = new AdCenter();
|
||||
data.siteSession = new SiteSession();
|
||||
|
||||
(data.state as any).generalSettings = settings;
|
||||
|
@ -126,6 +129,7 @@ export interface Core {
|
|||
readonly notifications: Notifications
|
||||
readonly cache: CacheManager
|
||||
readonly adCoordinator: AdCoordinatorGuest;
|
||||
readonly adCenter: AdCenter;
|
||||
readonly siteSession: SiteSession;
|
||||
|
||||
watch<T>(getter: (this: VueState) => T, callback: WatchHandler<T>): void
|
||||
|
|
|
@ -7,6 +7,7 @@ import { SmartFilterSettings } from '../learn/filter/types';
|
|||
export {Connection, Channel, Character} from '../fchat/interfaces';
|
||||
export const userStatuses: ReadonlyArray<Character.Status> = ['online', 'looking', 'away', 'busy', 'dnd'];
|
||||
export const channelModes: ReadonlyArray<Channel.Mode> = ['chat', 'ads', 'both'];
|
||||
import { Ad } from './ads/ad-center';
|
||||
|
||||
export namespace Conversation {
|
||||
interface BaseMessage {
|
||||
|
@ -185,6 +186,7 @@ export namespace Settings {
|
|||
searchHistory: (ExtendedSearchData | SearchData)[]
|
||||
hideNonMatchingAds: boolean
|
||||
hideProfileComparisonSummary: boolean
|
||||
ads: Ad[]
|
||||
};
|
||||
|
||||
export interface Store {
|
||||
|
|
|
@ -14,7 +14,7 @@
|
|||
</div>
|
||||
<div class="modal-footer" v-if="buttons">
|
||||
<button type="button" class="btn btn-secondary" @click="hideWithCheck" v-if="showCancel">Cancel</button>
|
||||
<button type="button" class="btn" :class="buttonClass" @click="submit" :disabled="disabled">
|
||||
<button type="button" class="btn" :class="buttonClass" @click="submit" :disabled="shouldBeDisabled()">
|
||||
{{submitText}}
|
||||
</button>
|
||||
</div>
|
||||
|
@ -62,12 +62,22 @@
|
|||
@Prop
|
||||
readonly buttonText?: string;
|
||||
isShown = false;
|
||||
|
||||
keepOpen = false;
|
||||
forcedDisabled = false;
|
||||
|
||||
get submitText(): string {
|
||||
return this.buttonText !== undefined ? this.buttonText : this.action;
|
||||
}
|
||||
|
||||
forceDisabled(disabled: boolean): void {
|
||||
this.forcedDisabled = disabled;
|
||||
}
|
||||
|
||||
shouldBeDisabled(): boolean {
|
||||
return this.disabled || this.forcedDisabled;
|
||||
}
|
||||
|
||||
submit(e: Event): void {
|
||||
this.$emit('submit', e);
|
||||
if(!e.defaultPrevented) this.hideWithCheck();
|
||||
|
|
|
@ -10,8 +10,8 @@ export default class CustomDialog extends Vue {
|
|||
return <Modal>this.$children[0];
|
||||
}
|
||||
|
||||
show(): void {
|
||||
this.dialog.show();
|
||||
show(keepOpen?: boolean): void {
|
||||
this.dialog.show(keepOpen);
|
||||
}
|
||||
|
||||
hide(): void {
|
||||
|
|
|
@ -50,7 +50,7 @@ theme: jekyll-theme-slate
|
|||
changelog: https://github.com/mrstallion/fchat-rising/blob/master/CHANGELOG.md
|
||||
|
||||
download:
|
||||
version: 1.19.3
|
||||
version: 1.20.0
|
||||
|
||||
url: https://github.com/mrstallion/fchat-rising/releases/download/v%VERSION%/F-Chat-Rising-%VERSION%-%PLATFORM_TAIL%
|
||||
|
||||
|
|
|
@ -305,6 +305,7 @@ export class SettingsStore implements Settings.Store {
|
|||
const file = path.join(getSettingsDir(character), key);
|
||||
return <Settings.Keys[K]>JSON.parse(fs.readFileSync(file, 'utf8'));
|
||||
} catch(e) {
|
||||
console.error('READ KEY FAILURE', e, key, character);
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "fchat",
|
||||
"version": "1.19.3",
|
||||
"version": "1.20.0",
|
||||
"author": "The F-List Team and Mister Stallion (Esq.)",
|
||||
"description": "F-List.net Chat Client",
|
||||
"main": "main.js",
|
||||
|
|
|
@ -6,3 +6,7 @@ declare module "!!raw-loader!*" {
|
|||
declare module "any-ascii" {
|
||||
export default function anyAscii(s: string): string;
|
||||
}
|
||||
|
||||
declare module 'vue-input-tag' {
|
||||
|
||||
}
|
||||
|
|
|
@ -730,8 +730,8 @@ export const kinkMatchScoreMap = {
|
|||
favorite: {
|
||||
favorite: 1,
|
||||
yes: 0.35,
|
||||
maybe: -0.35,
|
||||
no: -1
|
||||
maybe: -0.5,
|
||||
no: -1.5
|
||||
},
|
||||
|
||||
yes: {
|
||||
|
@ -749,8 +749,8 @@ export const kinkMatchScoreMap = {
|
|||
},
|
||||
|
||||
no: {
|
||||
favorite: -1,
|
||||
yes: -0.35,
|
||||
favorite: -1.5,
|
||||
yes: -0.5,
|
||||
maybe: 0,
|
||||
no: 0
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "f-list-rising",
|
||||
"version": "1.19.3",
|
||||
"version": "1.20.0",
|
||||
"author": "The F-List Team and and Mister Stallion (Esq.)",
|
||||
"description": "A heavily modded F-Chat 3.0 client for F-List",
|
||||
"license": "MIT",
|
||||
|
@ -48,6 +48,7 @@
|
|||
"tslint": "^6.1.3",
|
||||
"typescript": "^3.9.7",
|
||||
"vue": "2.6.12",
|
||||
"vue-input-tag": "^2.0.7",
|
||||
"vue-loader": "^15.9.8",
|
||||
"vue-template-compiler": "2.6.12",
|
||||
"webpack": "5.8.0"
|
||||
|
|
|
@ -7504,6 +7504,13 @@ vue-hot-reload-api@^2.3.0:
|
|||
resolved "https://registry.yarnpkg.com/vue-hot-reload-api/-/vue-hot-reload-api-2.3.4.tgz#532955cc1eb208a3d990b3a9f9a70574657e08f2"
|
||||
integrity sha512-BXq3jwIagosjgNVae6tkHzzIk6a8MHFtzAdwhnV5VlvPTFxDCvIttgSiHWjdGoTJvXtmRu5HacExfdarRcFhog==
|
||||
|
||||
vue-input-tag@^2.0.7:
|
||||
version "2.0.7"
|
||||
resolved "https://registry.yarnpkg.com/vue-input-tag/-/vue-input-tag-2.0.7.tgz#daea4db750df34a8c87605f88455dc5f64e1052b"
|
||||
integrity sha512-MiTVXg/OQ0SEPmQRPBw77akCrANAn/MUvo8U5fXLEwwtzGAASpGSru2uBvCMakDB5zF0ZV3ynkX/V8tOfSU6qA==
|
||||
dependencies:
|
||||
vue "^2.5.17"
|
||||
|
||||
vue-loader@15.9.8, vue-loader@^15.9.8:
|
||||
version "15.9.8"
|
||||
resolved "https://registry.yarnpkg.com/vue-loader/-/vue-loader-15.9.8.tgz#4b0f602afaf66a996be1e534fb9609dc4ab10e61"
|
||||
|
@ -7536,7 +7543,7 @@ vue-template-es2015-compiler@^1.9.0:
|
|||
resolved "https://registry.yarnpkg.com/vue-template-es2015-compiler/-/vue-template-es2015-compiler-1.9.1.tgz#1ee3bc9a16ecbf5118be334bb15f9c46f82f5825"
|
||||
integrity sha512-4gDntzrifFnCEvyoO8PqyJDmguXgVPxKiIxrBKjIowvL9l+N66196+72XVYR8BBf1Uv1Fgt3bGevJ+sEmxfZzw==
|
||||
|
||||
vue@2.6.12, vue@^2.5.20:
|
||||
vue@2.6.12, vue@^2.5.17, vue@^2.5.20:
|
||||
version "2.6.12"
|
||||
resolved "https://registry.yarnpkg.com/vue/-/vue-2.6.12.tgz#f5ebd4fa6bd2869403e29a896aed4904456c9123"
|
||||
integrity sha512-uhmLFETqPPNyuLLbsKz6ioJ4q7AZHzD8ZVFNATNyICSZouqP2Sz0rotWQC8UNBF6VGSCs5abnKJoStA6JbCbfg==
|
||||
|
|
Loading…
Reference in New Issue