Smart filters and M1 build
This commit is contained in:
parent
65ab5ffa32
commit
bbc2ca2f83
|
@ -1,7 +1,8 @@
|
|||
# Changelog
|
||||
|
||||
## Canary
|
||||
* Added a way to hide/filter out characters, messages, and ads (Settings > Identity Politics)
|
||||
## 1.17.0
|
||||
* Added a way to hide/filter out characters, messages, and ads (Settings > Smart Filters)
|
||||
* Added MacOS M1 build
|
||||
|
||||
## 1.16.2
|
||||
* Fixed broken auto-ads
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
# Download
|
||||
[Windows](https://github.com/mrstallion/fchat-rising/releases/download/v1.16.2/F-Chat-Rising-1.16.2-win.exe) (75 MB)
|
||||
| [MacOS](https://github.com/mrstallion/fchat-rising/releases/download/v1.16.2/F-Chat-Rising-1.16.2-macos.dmg) (76 MB)
|
||||
| [Linux](https://github.com/mrstallion/fchat-rising/releases/download/v1.16.2/F-Chat-Rising-1.16.2-linux.AppImage) (76 MB)
|
||||
[Windows](https://github.com/mrstallion/fchat-rising/releases/download/v1.17.0/F-Chat-Rising-1.17.0-win.exe) (75 MB)
|
||||
| [MacOS](https://github.com/mrstallion/fchat-rising/releases/download/v1.17.0/F-Chat-Rising-1.17.0-macos.dmg) (76 MB)
|
||||
| [Linux](https://github.com/mrstallion/fchat-rising/releases/download/v1.17.0/F-Chat-Rising-1.17.0-linux.AppImage) (76 MB)
|
||||
|
||||
|
||||
# F-Chat Rising
|
||||
|
|
|
@ -270,7 +270,7 @@
|
|||
core.connection.onMessage('FKS', async (data) => {
|
||||
const results = data.characters.map((x) => ({ character: core.characters.get(x), profile: null }))
|
||||
.filter((x) => core.state.hiddenUsers.indexOf(x.character.name) === -1 && !x.character.isIgnored)
|
||||
.filter((x) => this.isSpeciesMatch(x) && this.isBodyTypeMatch(x) && this.isSmartFilterMatch(x))
|
||||
.filter((x) => this.isSpeciesMatch(x) && this.isBodyTypeMatch(x) && !this.isSmartFiltered(x))
|
||||
.sort(sort);
|
||||
|
||||
// pre-warm cache
|
||||
|
@ -348,7 +348,7 @@
|
|||
private resort(results = this.results) {
|
||||
this.results = (_.filter(
|
||||
results,
|
||||
(x) => this.isSpeciesMatch(x) && this.isBodyTypeMatch(x)
|
||||
(x) => this.isSpeciesMatch(x) && this.isBodyTypeMatch(x) && !this.isSmartFiltered(x)
|
||||
) as SearchResult[]).sort(sort);
|
||||
}
|
||||
|
||||
|
@ -399,12 +399,12 @@
|
|||
return this.data.bodytypes.indexOf(bodytype!.value) > -1
|
||||
}
|
||||
|
||||
isSmartFilterMatch(result: SearchResult) {
|
||||
isSmartFiltered(result: SearchResult) {
|
||||
if (!core.state.settings.risingFilter.hideSearchResults) {
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
return result.profile ? !result.profile?.match.isFiltered : true;
|
||||
return !!result.profile?.match.isFiltered;
|
||||
}
|
||||
|
||||
getSpeciesOptions(): SearchSpecies[] {
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<template>
|
||||
<modal :action="l('settings.action')" @submit="submit" @open="load()" id="settings" dialogClass="w-100">
|
||||
<tabs style="flex-shrink:0;margin-bottom:10px" v-model="selectedTab"
|
||||
:tabs="[l('settings.tabs.general'), l('settings.tabs.notifications'), 'F-Chat Rising 🦄', 'Identity Politics 🦄', l('settings.tabs.hideAds'), l('settings.tabs.import')]"></tabs>
|
||||
:tabs="[l('settings.tabs.general'), l('settings.tabs.notifications'), 'F-Chat Rising 🦄', 'Smart Filters 🦄', l('settings.tabs.hideAds'), l('settings.tabs.import')]"></tabs>
|
||||
<div v-show="selectedTab === '0'">
|
||||
<div class="form-group">
|
||||
<label class="control-label" for="disallowedTags">{{l('settings.disallowedTags')}}</label>
|
||||
|
@ -202,6 +202,14 @@
|
|||
</div>
|
||||
|
||||
<div v-show="selectedTab === '3'">
|
||||
<div class="warning">
|
||||
<h5>Danger Zone!</h5>
|
||||
<div>By activating filtering, you may no longer be able to see or receive all messages from F-Chat.
|
||||
Filters do not apply to friends or bookmarked characters.</div>
|
||||
|
||||
<div>Beta version. Some of these features and behaviors may be removed or significantly changed in the future.</div>
|
||||
</div>
|
||||
|
||||
<h5>Visibility</h5>
|
||||
|
||||
<div class="form-group filters">
|
||||
|
@ -235,9 +243,9 @@
|
|||
Hide <b>private messages</b> (PMs) from matching characters
|
||||
</label>
|
||||
|
||||
<label class="control-label" for="risingFilter.penalizeMatches">
|
||||
<input type="checkbox" id="risingFilter.penalizeMatches" v-model="risingFilter.penalizeMatches"/>
|
||||
Penalize <b>match scores</b> for matching characters
|
||||
<label class="control-label" for="risingFilter.showFilterIcon">
|
||||
<input type="checkbox" id="risingFilter.showFilterIcon" v-model="risingFilter.showFilterIcon"/>
|
||||
Show <b>filter icon</b> on matching characters
|
||||
</label>
|
||||
</div>
|
||||
|
||||
|
@ -246,17 +254,27 @@
|
|||
<input type="checkbox" id="risingFilter.autoReply" v-model="risingFilter.autoReply"/>
|
||||
Send an automatic 'no thank you' response to matching characters if they message you
|
||||
</label>
|
||||
|
||||
<label class="control-label" for="risingFilter.penalizeMatches">
|
||||
<input type="checkbox" id="risingFilter.penalizeMatches" v-model="risingFilter.penalizeMatches"/>
|
||||
Penalize <b>match scores</b> for matching characters
|
||||
</label>
|
||||
|
||||
<label class="control-label" for="risingFilter.rewardNonMatches">
|
||||
<input type="checkbox" id="risingFilter.rewardNonMatches" v-model="risingFilter.rewardNonMatches"/>
|
||||
Increase <b>match scores</b> for non-matching characters
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<h5>Character Age Match</h5>
|
||||
<div class="form-group">Leave empty for no limit.</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="control-label" for="risingFilter.minAge">Characters younger than</label>
|
||||
<input id="risingFilter.minAge" type="number" class="form-control" v-model="risingFilter.minAge"/>
|
||||
<label class="control-label" for="risingFilter.minAge">Characters younger than (years)</label>
|
||||
<input id="risingFilter.minAge" type="number" class="form-control" v-model="risingFilter.minAge" placeholder="Enter age" />
|
||||
|
||||
<label class="control-label" for="risingFilter.maxAge">Characters older than</label>
|
||||
<input id="risingFilter.maxAge" type="number" class="form-control" v-model="risingFilter.maxAge"/>
|
||||
<label class="control-label" for="risingFilter.maxAge">Characters older than (years)</label>
|
||||
<input id="risingFilter.maxAge" type="number" class="form-control" v-model="risingFilter.maxAge" placeholder="Enter age" />
|
||||
</div>
|
||||
|
||||
<h5>Type Match</h5>
|
||||
|
@ -271,7 +289,7 @@
|
|||
<div class="form-group">Filters are not applied to these character names. Separate names with a linefeed.</div>
|
||||
|
||||
<div class="form-group">
|
||||
<textarea class="form-control" :value="getExceptionList()" @change="(v) => setExceptionList(v)"></textarea>
|
||||
<textarea class="form-control" :value="getExceptionList()" @change="(v) => setExceptionList(v)" placeholder="Enter names"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -510,7 +528,7 @@
|
|||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
<style lang="scss">
|
||||
#settings .form-group {
|
||||
margin-left: 0;
|
||||
margin-right: 0;
|
||||
|
@ -522,4 +540,23 @@
|
|||
margin-left: 5px;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
#settings .warning {
|
||||
border: 1px solid var(--warning);
|
||||
padding: 10px;
|
||||
margin-bottom: 20px;
|
||||
border-radius: 3px;
|
||||
|
||||
div {
|
||||
margin-top: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
#settings .form-group.filters.age label {
|
||||
padding-top: 10px;
|
||||
}
|
||||
|
||||
#settings .form-group.filters.age input {
|
||||
margin-left: 5px;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<!-- Linebreaks inside this template will break BBCode views -->
|
||||
<template><span :class="userClass" v-bind:bbcodeTag.prop="'user'" v-bind:character.prop="character" v-bind:channel.prop="channel" @mouseover.prevent="show()" @mouseenter.prevent="show()" @mouseleave.prevent="dismiss()" @click.middle.prevent.stop="toggleStickyness()" @click.right.passive="dismiss(true)" @click.left.passive="dismiss(true)"><span v-if="!!statusClass" :class="statusClass"></span><span v-if="!!rankIcon" :class="rankIcon"></span>{{character.name}}<span v-if="!!matchClass" :class="matchClass">{{getMatchScoreTitle(matchScore)}}</span></span></template>
|
||||
<template><span :class="userClass" v-bind:bbcodeTag.prop="'user'" v-bind:character.prop="character" v-bind:channel.prop="channel" @mouseover.prevent="show()" @mouseenter.prevent="show()" @mouseleave.prevent="dismiss()" @click.middle.prevent.stop="toggleStickyness()" @click.right.passive="dismiss(true)" @click.left.passive="dismiss(true)"><span v-if="!!statusClass" :class="statusClass"></span><span v-if="!!rankIcon" :class="rankIcon"></span><span v-if="!!smartFilterIcon" :class="smartFilterIcon"></span>{{character.name}}<span v-if="!!matchClass" :class="matchClass">{{getMatchScoreTitle(matchScore)}}</span></span></template>
|
||||
|
||||
|
||||
<script lang="ts">
|
||||
|
@ -36,6 +36,7 @@ export function getStatusIcon(status: Character.Status): string {
|
|||
|
||||
export interface StatusClasses {
|
||||
rankIcon: string | null;
|
||||
smartFilterIcon: string | null;
|
||||
statusClass: string | null;
|
||||
matchClass: string | null;
|
||||
matchScore: number | string | null;
|
||||
|
@ -54,6 +55,7 @@ export function getStatusClasses(
|
|||
let statusClass = null;
|
||||
let matchClass = null;
|
||||
let matchScore = null;
|
||||
let smartFilterIcon: string | null = null;
|
||||
|
||||
if(character.isChatOp) {
|
||||
rankIcon = 'far fa-gem';
|
||||
|
@ -68,23 +70,30 @@ export function getStatusClasses(
|
|||
if ((showStatus) || (character.status === 'crown'))
|
||||
statusClass = `fa-fw ${getStatusIcon(character.status)}`;
|
||||
|
||||
if ((core.state.settings.risingAdScore) && (showMatch)) {
|
||||
const cache = core.cache.profileCache.getSync(character.name);
|
||||
const cache = ((showMatch) && ((core.state.settings.risingAdScore) || (core.state.settings.risingFilter.showFilterIcon)))
|
||||
? core.cache.profileCache.getSync(character.name)
|
||||
: undefined;
|
||||
|
||||
if (cache) {
|
||||
if ((cache.match.searchScore >= kinkMatchWeights.unicornThreshold) && (cache.match.matchScore === Scoring.MATCH)) {
|
||||
matchClass = 'match-found unicorn';
|
||||
matchScore = 'unicorn';
|
||||
} else {
|
||||
matchClass = `match-found ${Score.getClasses(cache.match.matchScore)}`;
|
||||
matchScore = cache.match.matchScore;
|
||||
}
|
||||
// undefined == not interested
|
||||
// null == no cache hit
|
||||
if (cache === null) {
|
||||
void core.cache.addProfile(character.name);
|
||||
}
|
||||
|
||||
if ((core.state.settings.risingAdScore) && (showMatch) && (cache)) {
|
||||
if ((cache.match.searchScore >= kinkMatchWeights.unicornThreshold) && (cache.match.matchScore === Scoring.MATCH)) {
|
||||
matchClass = 'match-found unicorn';
|
||||
matchScore = 'unicorn';
|
||||
} else {
|
||||
/* tslint:disable-next-line no-floating-promises */
|
||||
core.cache.addProfile(character.name);
|
||||
matchClass = `match-found ${Score.getClasses(cache.match.matchScore)}`;
|
||||
matchScore = cache.match.matchScore;
|
||||
}
|
||||
}
|
||||
|
||||
if (core.state.settings.risingFilter.showFilterIcon && cache?.match.isFiltered) {
|
||||
smartFilterIcon = 'user-filter fas fa-filter';
|
||||
}
|
||||
|
||||
const gender = character.gender !== undefined ? character.gender.toLowerCase() : 'none';
|
||||
|
||||
const isBookmark = (showBookmark) && (core.connection.isOpen) && (core.state.settings.colorBookmarks) &&
|
||||
|
@ -98,6 +107,7 @@ export function getStatusClasses(
|
|||
matchClass,
|
||||
matchScore,
|
||||
userClass,
|
||||
smartFilterIcon,
|
||||
isBookmark
|
||||
};
|
||||
}
|
||||
|
@ -130,6 +140,7 @@ export default class UserView extends Vue {
|
|||
userClass = '';
|
||||
|
||||
rankIcon: string | null = null;
|
||||
smartFilterIcon: string | null = null;
|
||||
statusClass: string | null = null;
|
||||
matchClass: string | null = null;
|
||||
matchScore: number | string | null = null;
|
||||
|
@ -198,6 +209,7 @@ export default class UserView extends Vue {
|
|||
const res = getStatusClasses(this.character, this.channel, !!this.showStatus, !!this.bookmark, !!this.match);
|
||||
|
||||
this.rankIcon = res.rankIcon;
|
||||
this.smartFilterIcon = res.smartFilterIcon;
|
||||
this.statusClass = res.statusClass;
|
||||
this.matchClass = res.matchClass;
|
||||
this.matchScore = res.matchScore;
|
||||
|
|
|
@ -56,23 +56,28 @@ export class Settings implements ISettings {
|
|||
risingColorblindMode = false;
|
||||
|
||||
risingFilter = {
|
||||
hideAds: false,
|
||||
hideSearchResults: false,
|
||||
hideAds: true,
|
||||
hideSearchResults: true,
|
||||
hideChannelMembers: false,
|
||||
hidePublicChannelMessages: false,
|
||||
hidePrivateChannelMessages: false,
|
||||
hidePrivateMessages: false,
|
||||
penalizeMatches: false,
|
||||
showFilterIcon: true,
|
||||
penalizeMatches: true,
|
||||
rewardNonMatches: false,
|
||||
autoReply: true,
|
||||
minAge: null,
|
||||
maxAge: null,
|
||||
smartFilters: {
|
||||
ageplay: false,
|
||||
anthro: false,
|
||||
female: false,
|
||||
feral: false,
|
||||
human: false,
|
||||
hyper: false,
|
||||
incest: false,
|
||||
intersex: false,
|
||||
male: false,
|
||||
microMacro: false,
|
||||
obesity: false,
|
||||
pokemon: false,
|
||||
|
|
|
@ -13,7 +13,6 @@ import throat from 'throat';
|
|||
import Bluebird from 'bluebird';
|
||||
import log from 'electron-log';
|
||||
import isChannel = Interfaces.isChannel;
|
||||
import isPrivate = Interfaces.isPrivate; //tslint:disable-line:match-default-export-name
|
||||
|
||||
function createMessage(this: any, type: MessageType, sender: Character, text: string, time?: Date): Message {
|
||||
if(type === MessageType.Message && isAction(text)) {
|
||||
|
@ -536,10 +535,16 @@ class State implements Interfaces.State {
|
|||
this.channelConversations.some((x) => x.unread === Interfaces.UnreadState.Mention);
|
||||
}
|
||||
|
||||
getPrivate(character: Character): PrivateConversation {
|
||||
getPrivate(character: Character): PrivateConversation;
|
||||
getPrivate(character: Character, noCreate: boolean = false): PrivateConversation | undefined {
|
||||
const key = character.name.toLowerCase();
|
||||
let conv = state.privateMap[key];
|
||||
if(conv !== undefined) return conv;
|
||||
|
||||
if (noCreate) {
|
||||
return;
|
||||
}
|
||||
|
||||
conv = new PrivateConversation(character);
|
||||
this.privateConversations.push(conv);
|
||||
this.privateMap[key] = conv;
|
||||
|
@ -613,6 +618,55 @@ function isOfInterest(this: any, character: Character): boolean {
|
|||
return character.isFriend || character.isBookmarked || state.privateMap[character.name.toLowerCase()] !== undefined;
|
||||
}
|
||||
|
||||
async function testSmartFilterForPrivateMessage(fromChar: Character.Character): Promise<boolean> {
|
||||
const cachedProfile = core.cache.profileCache.getSync(fromChar.name) || await core.cache.profileCache.get(fromChar.name);
|
||||
|
||||
if (
|
||||
cachedProfile &&
|
||||
cachedProfile.match.isFiltered &&
|
||||
core.state.settings.risingFilter.autoReply &&
|
||||
!cachedProfile.match.autoResponded
|
||||
) {
|
||||
cachedProfile.match.autoResponded = true;
|
||||
|
||||
log.debug('filter.autoresponse', { name: fromChar.name });
|
||||
|
||||
void Conversation.conversationThroat(
|
||||
async() => {
|
||||
await Conversation.testPostDelay();
|
||||
|
||||
// tslint:disable-next-line:prefer-template
|
||||
const m = '[Automated message] Sorry, the player of this character has indicated that they are not interested in characters matching your profile. They will not see your message.\n\n' +
|
||||
'Need a filter for yourself? Try out [url=https://mrstallion.github.io/fchat-rising/]F-Chat Rising[/url]';
|
||||
|
||||
core.connection.send('PRI', {recipient: fromChar.name, message: m});
|
||||
core.cache.markLastPostTime();
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
if (cachedProfile && cachedProfile.match.isFiltered && core.state.settings.risingFilter.hidePrivateMessages) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
async function testSmartFilterForChannel(fromChar: Character.Character, conversation: ChannelConversation): Promise<boolean> {
|
||||
if (
|
||||
(isChannel(conversation) && conversation.channel.owner === '' && core.state.settings.risingFilter.hidePublicChannelMessages) ||
|
||||
(isChannel(conversation) && conversation.channel.owner !== '' && core.state.settings.risingFilter.hidePrivateChannelMessages)
|
||||
) {
|
||||
const cachedProfile = core.cache.profileCache.getSync(fromChar.name) || await core.cache.profileCache.get(fromChar.name);
|
||||
|
||||
if (cachedProfile && cachedProfile.match.isFiltered && !fromChar.isChatOp) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
export default function(this: any): Interfaces.State {
|
||||
state = new State();
|
||||
window.addEventListener('focus', () => {
|
||||
|
@ -679,12 +733,17 @@ export default function(this: any): Interfaces.State {
|
|||
await conv.addMessage(new EventMessage(text));
|
||||
}
|
||||
});
|
||||
|
||||
connection.onMessage('PRI', async(data, time) => {
|
||||
const char = core.characters.get(data.character);
|
||||
if(char.isIgnored) return connection.send('IGN', {action: 'notify', character: data.character});
|
||||
const message = createMessage(MessageType.Message, char, decodeHTML(data.message), time);
|
||||
|
||||
if (await testSmartFilterForPrivateMessage(char) === true) {
|
||||
return;
|
||||
}
|
||||
|
||||
EventBus.$emit('private-message', { message });
|
||||
|
||||
const conv = state.getPrivate(char);
|
||||
await conv.addMessage(message);
|
||||
});
|
||||
|
@ -695,40 +754,14 @@ export default function(this: any): Interfaces.State {
|
|||
if(char.isIgnored) return;
|
||||
|
||||
const message = createMessage(MessageType.Message, char, decodeHTML(data.message), time);
|
||||
EventBus.$emit('channel-message', { message, channel: conversation });
|
||||
await conversation.addMessage(message);
|
||||
// message.type === MessageType.Message
|
||||
if (
|
||||
(isPrivate(conversation) && core.state.settings.risingFilter.hidePrivateMessages) ||
|
||||
(isChannel(conversation) && conversation.channel.owner === '' && core.state.settings.risingFilter.hidePublicChannelMessages) ||
|
||||
(isChannel(conversation) && conversation.channel.owner !== '' && core.state.settings.risingFilter.hidePrivateChannelMessages)
|
||||
) {
|
||||
const cachedProfile = core.cache.profileCache.getSync(char.name) || await core.cache.profileCache.get(char.name);
|
||||
|
||||
if (cachedProfile && isPrivate(conversation) && core.state.settings.risingFilter.autoReply && !cachedProfile.match.autoResponded) {
|
||||
cachedProfile.match.autoResponded = true;
|
||||
|
||||
log.debug('filter.autoresponse', { name: char.name });
|
||||
|
||||
void Conversation.conversationThroat(
|
||||
async() => {
|
||||
await Conversation.testPostDelay();
|
||||
|
||||
// tslint:disable-next-line:prefer-template
|
||||
const m = '[Automated message] Sorry, the player of this character has indicated that they are not interested in characters matching your profile. They will not see your message.\n\n' +
|
||||
'Need a filter for yourself? Try out [url=https://mrstallion.github.io/fchat-rising/]F-Chat Rising[/url]';
|
||||
|
||||
core.connection.send('PRI', {recipient: char.name, message: m});
|
||||
core.cache.markLastPostTime();
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
if (cachedProfile && cachedProfile.match.isFiltered) {
|
||||
return;
|
||||
}
|
||||
if (await testSmartFilterForChannel(char, conversation) === true) {
|
||||
return;
|
||||
}
|
||||
|
||||
await conversation.addMessage(message);
|
||||
EventBus.$emit('channel-message', { message, channel: conversation });
|
||||
|
||||
const words = conversation.settings.highlightWords.slice();
|
||||
if(conversation.settings.defaultHighlights) words.push(...core.state.settings.highlightWords);
|
||||
if(conversation.settings.highlight === Interfaces.Setting.Default && core.state.settings.highlight ||
|
||||
|
|
|
@ -26,6 +26,14 @@
|
|||
|
||||
<match-tags v-if="match" :match="match"></match-tags>
|
||||
|
||||
<div class="filter-matches" v-if="smartFilterIsFiltered">
|
||||
<h4>Smart Filter Matches</h4>
|
||||
|
||||
<span class="tags">
|
||||
<span v-for="filterName in smartFilterDetails" class="smart-filter-tag" :class="filterName">{{ (smartFilterLabels[filterName] || {}).name }}</span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- <div v-if="customs">-->
|
||||
<!-- <span v-for="c in customs" :class="Score.getClasses(c.score)">{{c.name}}</span>-->
|
||||
<!-- </div>-->
|
||||
|
@ -72,6 +80,8 @@ import {
|
|||
import { BBCodeView } from '../../bbcode/view';
|
||||
import { EventBus } from './event-bus';
|
||||
import { Character, CustomKink } from '../../interfaces';
|
||||
import { matchesSmartFilters, testSmartFilters } from '../../learn/filter/smart-filter';
|
||||
import { smartFilterTypes } from '../../learn/filter/types';
|
||||
|
||||
interface CustomKinkWithScore extends CustomKink {
|
||||
score: number;
|
||||
|
@ -97,6 +107,15 @@ export default class CharacterPreview extends Vue {
|
|||
latestAd?: AdCachedPosting;
|
||||
statusMessage?: string;
|
||||
|
||||
smartFilterIsFiltered?: boolean;
|
||||
smartFilterDetails?: string[];
|
||||
|
||||
smartFilterLabels: Record<string, { name: string }> = {
|
||||
...smartFilterTypes,
|
||||
ageMin: { name: 'Min age' },
|
||||
ageMax: { name: 'Max age' }
|
||||
};
|
||||
|
||||
age?: string;
|
||||
sexualOrientation?: string;
|
||||
species?: string;
|
||||
|
@ -170,6 +189,9 @@ export default class CharacterPreview extends Vue {
|
|||
this.customs = undefined;
|
||||
this.ownCharacter = core.characters.ownProfile;
|
||||
|
||||
this.smartFilterIsFiltered = false;
|
||||
this.smartFilterDetails = [];
|
||||
|
||||
this.updateOnlineStatus();
|
||||
this.updateAdStatus();
|
||||
|
||||
|
@ -177,11 +199,36 @@ export default class CharacterPreview extends Vue {
|
|||
this.character = await this.getCharacterData(characterName);
|
||||
this.match = Matcher.identifyBestMatchReport(this.ownCharacter!.character, this.character!.character);
|
||||
|
||||
this.updateSmartFilterReport();
|
||||
this.updateCustoms();
|
||||
this.updateDetails();
|
||||
}, 0);
|
||||
}
|
||||
|
||||
updateSmartFilterReport() {
|
||||
if (!this.character) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.smartFilterIsFiltered = matchesSmartFilters(this.character.character, core.state.settings.risingFilter);
|
||||
this.smartFilterDetails = [];
|
||||
|
||||
if (!this.smartFilterIsFiltered) {
|
||||
return;
|
||||
}
|
||||
|
||||
const results = testSmartFilters(this.character.character, core.state.settings.risingFilter);
|
||||
|
||||
if (!results) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.smartFilterDetails = [
|
||||
..._.map(_.filter(_.toPairs(results.ageCheck), (v) => v[1]), (v) => v[0]),
|
||||
..._.map(_.filter(_.toPairs(results.filters), (v) => v[1].isFiltered), (v: any) => v[0])
|
||||
];
|
||||
}
|
||||
|
||||
updateOnlineStatus(): void {
|
||||
this.onlineCharacter = core.characters.get(this.characterName!);
|
||||
|
||||
|
@ -384,7 +431,8 @@ export default class CharacterPreview extends Vue {
|
|||
}
|
||||
|
||||
.status-message,
|
||||
.latest-ad-message {
|
||||
.latest-ad-message,
|
||||
.filter-matches {
|
||||
display: block;
|
||||
background-color: rgba(0,0,0,0.2);
|
||||
padding: 10px;
|
||||
|
@ -392,6 +440,23 @@ export default class CharacterPreview extends Vue {
|
|||
margin-top: 1.3rem;
|
||||
}
|
||||
|
||||
.filter-matches {
|
||||
.tags {
|
||||
margin-top: 10px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.smart-filter-tag {
|
||||
display: inline-block;
|
||||
color: var(--messageTimeFgColor);
|
||||
margin-right: 4px;
|
||||
background-color: var(--messageTimeBgColor);
|
||||
border-radius: 2px;
|
||||
padding-left: 3px;
|
||||
padding-right: 3px;
|
||||
}
|
||||
}
|
||||
|
||||
.character-avatar {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
|
|
|
@ -50,7 +50,7 @@ theme: jekyll-theme-slate
|
|||
changelog: https://github.com/mrstallion/fchat-rising/blob/master/CHANGELOG.md
|
||||
|
||||
download:
|
||||
version: 1.16.2
|
||||
version: 1.17.0
|
||||
|
||||
url: https://github.com/mrstallion/fchat-rising/releases/download/v%VERSION%/F-Chat-Rising-%VERSION%-%PLATFORM_TAIL%
|
||||
|
||||
|
@ -58,14 +58,19 @@ download:
|
|||
- type: win
|
||||
name: Windows
|
||||
tail: win.exe
|
||||
size: 85 MB
|
||||
size: 84 MB
|
||||
|
||||
- type: mac
|
||||
name: MacOS
|
||||
tail: macos.dmg
|
||||
size: 82 MB
|
||||
name: MacOS (Intel)
|
||||
tail: macos-intel.dmg
|
||||
size: 80 MB
|
||||
instructions: ./macos-install
|
||||
|
||||
- type: mac
|
||||
name: MacOS (M1)
|
||||
tail: macos-m1.dmg
|
||||
size: 83 MB
|
||||
|
||||
- type: linux
|
||||
name: Linux
|
||||
tail: linux.AppImage
|
||||
|
|
|
@ -53,7 +53,8 @@ require('electron-packager')({
|
|||
icon: path.join(__dirname, 'build', 'icon'),
|
||||
ignore: ['\.map$'],
|
||||
osxSign: process.argv.length > 2 ? {identity: process.argv[2]} : false,
|
||||
prune: false
|
||||
prune: false,
|
||||
arch: process.platform === 'darwin' ? ['x64', 'arm64'] : undefined
|
||||
}).then((appPaths) => {
|
||||
if (process.env.SKIP_INSTALLER) {
|
||||
return;
|
||||
|
@ -84,33 +85,38 @@ require('electron-packager')({
|
|||
}).catch((e) => console.error(`Error while creating installer: ${e.message}`));
|
||||
} else if(process.platform === 'darwin') {
|
||||
console.log('Creating Mac DMG');
|
||||
const target = path.join(distDir, `F-Chat Rising.dmg`);
|
||||
if(fs.existsSync(target)) fs.unlinkSync(target);
|
||||
const appPath = path.join(appPaths[0], 'F-Chat.app');
|
||||
if(process.argv.length <= 2) console.warn('Warning: Creating unsigned DMG');
|
||||
require('appdmg')({
|
||||
basepath: appPaths[0],
|
||||
target,
|
||||
specification: {
|
||||
title: 'F-Chat Rising',
|
||||
icon: path.join(__dirname, 'build', 'icon.png'),
|
||||
background: path.join(__dirname, 'build', 'dmg.png'),
|
||||
contents: [{x: 555, y: 345, type: 'link', path: '/Applications'}, {x: 555, y: 105, type: 'file', path: appPath}],
|
||||
'code-sign': process.argv.length > 2 ? {
|
||||
'signing-identity': process.argv[2]
|
||||
} : undefined
|
||||
}
|
||||
}).on('error', console.error);
|
||||
const zipName = `F-Chat_Rising_${pkg.version}.zip`;
|
||||
const zipPath = path.join(distDir, zipName);
|
||||
if(fs.existsSync(zipPath)) fs.unlinkSync(zipPath);
|
||||
const child = child_process.spawn('zip', ['-r', '-y', '-9', zipPath, 'F-Chat.app'], {cwd: appPaths[0]});
|
||||
child.stdout.on('data', () => {});
|
||||
child.stderr.on('data', (data) => console.error(data.toString()));
|
||||
fs.writeFileSync(path.join(distDir, 'updates.json'), JSON.stringify({
|
||||
releases: [{version: pkg.version, updateTo: {url: 'https://client.f-list.net/darwin/' + zipName}}],
|
||||
currentRelease: pkg.version
|
||||
}));
|
||||
|
||||
_.each([{ name: 'Intel', path: appPaths[0] }, { name: 'M1', path: appPaths[1] }], (arch) => {
|
||||
console.log(arch.name, arch.path);
|
||||
|
||||
const target = path.join(distDir, `F-Chat Rising ${arch.name}.dmg`);
|
||||
if(fs.existsSync(target)) fs.unlinkSync(target);
|
||||
const appPath = path.join(arch.path, 'F-Chat.app');
|
||||
if(process.argv.length <= 2) console.warn('Warning: Creating unsigned DMG');
|
||||
require('appdmg')({
|
||||
basepath: arch.path,
|
||||
target,
|
||||
specification: {
|
||||
title: 'F-Chat Rising',
|
||||
icon: path.join(__dirname, 'build', 'icon.png'),
|
||||
background: path.join(__dirname, 'build', 'dmg.png'),
|
||||
contents: [{x: 555, y: 345, type: 'link', path: '/Applications'}, {x: 555, y: 105, type: 'file', path: appPath}],
|
||||
'code-sign': process.argv.length > 2 ? {
|
||||
'signing-identity': process.argv[2]
|
||||
} : undefined
|
||||
}
|
||||
}).on('error', console.error);
|
||||
const zipName = `F-Chat_Rising_${arch.name}_${pkg.version}.zip`;
|
||||
const zipPath = path.join(distDir, zipName);
|
||||
if(fs.existsSync(zipPath)) fs.unlinkSync(zipPath);
|
||||
const child = child_process.spawn('zip', ['-r', '-y', '-9', zipPath, 'F-Chat.app'], {cwd: arch.path});
|
||||
child.stdout.on('data', () => {});
|
||||
child.stderr.on('data', (data) => console.error(data.toString()));
|
||||
fs.writeFileSync(path.join(distDir, 'updates.json'), JSON.stringify({
|
||||
releases: [{version: pkg.version, updateTo: {url: 'https://client.f-list.net/darwin/' + zipName}}],
|
||||
currentRelease: pkg.version
|
||||
}));
|
||||
});
|
||||
} else {
|
||||
console.log('Creating Linux AppImage');
|
||||
fs.renameSync(path.join(appPaths[0], 'F-Chat'), path.join(appPaths[0], 'AppRun'));
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "fchat",
|
||||
"version": "1.16.2",
|
||||
"version": "1.17.0",
|
||||
"author": "The F-List Team and Mister Stallion (Esq.)",
|
||||
"description": "F-List.net Chat Client",
|
||||
"main": "main.js",
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import _ from 'lodash';
|
||||
import { Matcher } from '../matcher';
|
||||
import { BodyType, Build, Kink, Species, TagId } from '../matcher-types';
|
||||
import { BodyType, Build, Gender, Kink, Species, TagId } from '../matcher-types';
|
||||
import { SmartFilterSelection, SmartFilterSettings } from './types';
|
||||
import { Character } from '../../interfaces';
|
||||
import log from 'electron-log';
|
||||
|
@ -8,32 +8,44 @@ import core from '../../chat/core';
|
|||
|
||||
export interface SmartFilterOpts {
|
||||
name: string;
|
||||
kinks?: Kink[],
|
||||
bodyTypes?: BodyType[],
|
||||
builds?: Build[],
|
||||
species?: Species[]
|
||||
kinks?: Kink[];
|
||||
bodyTypes?: BodyType[];
|
||||
builds?: Build[];
|
||||
species?: Species[];
|
||||
genders?: Gender[];
|
||||
isAnthro?: boolean;
|
||||
isHuman?: boolean;
|
||||
}
|
||||
|
||||
export interface SmartFilterTestResult {
|
||||
isFiltered: boolean;
|
||||
builds: boolean;
|
||||
bodyTypes: boolean;
|
||||
species: boolean;
|
||||
genders: boolean;
|
||||
isAnthro: boolean;
|
||||
isHuman: boolean;
|
||||
kinks: boolean;
|
||||
}
|
||||
|
||||
export class SmartFilter {
|
||||
constructor(private opts: SmartFilterOpts) {}
|
||||
|
||||
test(c: Character): boolean {
|
||||
test(c: Character): SmartFilterTestResult {
|
||||
const builds = this.testBuilds(c);
|
||||
const bodyTypes = this.testBodyTypes(c);
|
||||
const species = this.testSpecies(c);
|
||||
const isAnthro = this.testIsAnthro(c);
|
||||
const isHuman = this.testIsHuman(c);
|
||||
const kinks = this.testKinks(c);
|
||||
const genders = this.testGenders(c);
|
||||
|
||||
const result = builds || bodyTypes || species || isAnthro || isHuman || kinks;
|
||||
const isFiltered = builds || bodyTypes || species || isAnthro || isHuman || kinks || genders;
|
||||
const result = { isFiltered, builds, bodyTypes, species, isAnthro, isHuman, kinks, genders };
|
||||
|
||||
log.debug('smart-filter.test',
|
||||
{ name: c.name, filterName: this.opts.name, result, builds, bodyTypes, species, isAnthro, isHuman, kinks });
|
||||
log.silly('smart-filter.test', { name: c.name, filterName: this.opts.name, result });
|
||||
|
||||
return this.testBuilds(c) || this.testBodyTypes(c) || this.testSpecies(c) || this.testIsAnthro(c) || this.testIsHuman(c) ||
|
||||
this.testKinks(c);
|
||||
return result;
|
||||
}
|
||||
|
||||
testKinks(c: Character): boolean {
|
||||
|
@ -65,6 +77,16 @@ export class SmartFilter {
|
|||
return !!build && !!_.find(this.opts.builds || [], build);
|
||||
}
|
||||
|
||||
testGenders(c: Character): boolean {
|
||||
if (!this.opts.genders) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const gender = Matcher.getTagValueList(TagId.Gender, c);
|
||||
|
||||
return !!gender && !!_.find(this.opts.genders || [], gender);
|
||||
}
|
||||
|
||||
testBodyTypes(c: Character): boolean {
|
||||
if (!this.opts.bodyTypes) {
|
||||
return false;
|
||||
|
@ -94,11 +116,11 @@ export class SmartFilter {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
export type SmartFilterCollection = {
|
||||
[key in keyof SmartFilterSelection]: SmartFilter;
|
||||
};
|
||||
|
||||
|
||||
export const smartFilters: SmartFilterCollection = {
|
||||
ageplay: new SmartFilter({
|
||||
name: 'ageplay',
|
||||
|
@ -110,6 +132,11 @@ export const smartFilters: SmartFilterCollection = {
|
|||
isAnthro: true
|
||||
}),
|
||||
|
||||
female: new SmartFilter({
|
||||
name: 'female',
|
||||
genders: [Gender.Female]
|
||||
}),
|
||||
|
||||
feral: new SmartFilter({
|
||||
name: 'feral',
|
||||
bodyTypes: [BodyType.Feral]
|
||||
|
@ -140,6 +167,16 @@ export const smartFilters: SmartFilterCollection = {
|
|||
kinks: [Kink.Incest, Kink.IncestParental, Kink.IncestSiblings, Kink.ParentChildPlay, Kink.ForcedIncest]
|
||||
}),
|
||||
|
||||
intersex: new SmartFilter({
|
||||
name: 'intersex',
|
||||
genders: [Gender.Transgender, Gender.Herm, Gender.MaleHerm, Gender.Cuntboy, Gender.Shemale]
|
||||
}),
|
||||
|
||||
male: new SmartFilter({
|
||||
name: 'male',
|
||||
genders: [Gender.Male]
|
||||
}),
|
||||
|
||||
microMacro: new SmartFilter({
|
||||
name: 'microMacro',
|
||||
kinks: [Kink.MacroAsses, Kink.MacroBalls, Kink.MacroBreasts, Kink.MacroCocks, Kink.Macrophilia, Kink.MegaMacro, Kink.Microphilia,
|
||||
|
@ -205,31 +242,59 @@ export const smartFilters: SmartFilterCollection = {
|
|||
})
|
||||
};
|
||||
|
||||
|
||||
export function matchesSmartFilters(c: Character, opts: SmartFilterSettings): boolean {
|
||||
export function testSmartFilters(c: Character, opts: SmartFilterSettings): {
|
||||
ageCheck: { ageMin: boolean; ageMax: boolean };
|
||||
filters: { [key in keyof SmartFilterCollection]: SmartFilterTestResult }
|
||||
} | null {
|
||||
if (c.name === core.characters.ownCharacter.name) {
|
||||
return false;
|
||||
return null;
|
||||
}
|
||||
|
||||
if (core.characters.get(c.name)?.isChatOp) {
|
||||
return false;
|
||||
const coreCharacter = core.characters.get(c.name);
|
||||
|
||||
if (coreCharacter?.isChatOp || coreCharacter?.isBookmarked || coreCharacter?.isFriend) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (opts.exceptionNames.includes(c.name)) {
|
||||
log.debug('smart-filter.exception', { name: c.name });
|
||||
return false;
|
||||
return null;
|
||||
}
|
||||
|
||||
const ageCheck = { ageMin: false, ageMax: false };
|
||||
|
||||
if (opts.minAge !== null || opts.maxAge !== null) {
|
||||
const age = Matcher.age(c);
|
||||
const age = Matcher.age(c) || Matcher.apparentAge(c)?.min || null;
|
||||
|
||||
if (age !== null) {
|
||||
if ((opts.minAge !== null && age < opts.minAge) || (opts.maxAge !== null && age > opts.maxAge)) {
|
||||
log.debug('smart-filter.age', { name: c.name, age, minAge: opts.minAge, maxAge: opts.maxAge });
|
||||
return true;
|
||||
if (opts.minAge !== null && age < opts.minAge) {
|
||||
log.debug('smart-filter.age.min', { name: c.name, age, minAge: opts.minAge });
|
||||
ageCheck.ageMin = true;
|
||||
}
|
||||
|
||||
if (opts.maxAge !== null && age > opts.maxAge) {
|
||||
log.debug('smart-filter.age.max', { name: c.name, age, maxAge: opts.maxAge });
|
||||
ageCheck.ageMax = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return !_.every(opts.smartFilters, (fs, filterName) => !fs || !(smartFilters as any)[filterName].test(c));
|
||||
return {
|
||||
ageCheck,
|
||||
filters: _.mapValues(smartFilters, (f, k) => (opts.smartFilters as any)[k] && f.test(c))
|
||||
};
|
||||
}
|
||||
|
||||
export function matchesSmartFilters(c: Character, opts: SmartFilterSettings): boolean {
|
||||
const match = testSmartFilters(c, opts);
|
||||
|
||||
if (!match) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (match.ageCheck.ageMax || match.ageCheck.ageMin) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return !_.every(match.filters, (filterResult) => !filterResult.isFiltered);
|
||||
}
|
||||
|
|
|
@ -1,15 +1,14 @@
|
|||
// <!-- [Automated message] Sorry, the player of this character has indicated that they are not interested in characters matching your profile.-->
|
||||
// <!-- Need a filter for yourself? Try out [F-Chat Rising](https://mrstallion.github.io/fchat-rising/)!-->
|
||||
|
||||
|
||||
export const smartFilterTypes = {
|
||||
ageplay: { name: 'Ageplay' },
|
||||
anthro: { name: 'Anthros' },
|
||||
female: { name: 'Females' },
|
||||
feral: { name: 'Ferals' },
|
||||
gore: { name: 'Gore/torture/death' },
|
||||
human: { name: 'Humans' },
|
||||
hyper: { name: 'Hyper' },
|
||||
incest: { name: 'Incest' },
|
||||
intersex: { name: 'Intersex' },
|
||||
male: { name: 'Males' },
|
||||
microMacro: { name: 'Micro/macro' },
|
||||
obesity: { name: 'Obesity' },
|
||||
pokemon: { name: 'Pokemons/Digimons' },
|
||||
|
@ -36,7 +35,9 @@ export interface SmartFilterSettings {
|
|||
hidePrivateChannelMessages: boolean;
|
||||
hidePrivateMessages: boolean;
|
||||
penalizeMatches: boolean;
|
||||
rewardNonMatches: boolean;
|
||||
autoReply: boolean;
|
||||
showFilterIcon: boolean;
|
||||
|
||||
minAge: number | null;
|
||||
maxAge: number | null;
|
||||
|
|
|
@ -1237,11 +1237,59 @@ export class Matcher {
|
|||
|
||||
static age(c: Character): number | null {
|
||||
const rawAge = Matcher.getTagValue(TagId.Age, c);
|
||||
const age = ((rawAge) && (rawAge.string)) ? parseInt(rawAge.string, 10) : null;
|
||||
|
||||
if (!rawAge || !rawAge.string) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const ageStr = rawAge.string.toLowerCase().trim();
|
||||
|
||||
if ((ageStr.indexOf('shota') >= 0) || (ageStr.indexOf('loli') >= 0) || (ageStr.indexOf('lolli') >= 0)) {
|
||||
return 10;
|
||||
}
|
||||
|
||||
const age = parseInt(rawAge.string, 10);
|
||||
|
||||
return age && !Number.isNaN(age) && Number.isFinite(age) ? age : null;
|
||||
}
|
||||
|
||||
static apparentAge(c: Character): { min: number, max: number } | null {
|
||||
const rawAge = Matcher.getTagValue(TagId.ApparentAge, c);
|
||||
|
||||
if ((!rawAge) || (!rawAge.string)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const ageStr = rawAge.string.trim().toLowerCase();
|
||||
|
||||
if (ageStr === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
// '18'
|
||||
if (/^[0-9]+$/.exec(ageStr)) {
|
||||
const val = parseInt(rawAge.string, 10);
|
||||
|
||||
return { min: val, max: val };
|
||||
}
|
||||
|
||||
// '18-22'
|
||||
const rangeMatch = ageStr.match(/^([0-9]+)-([0-9]+)$/);
|
||||
|
||||
if (rangeMatch) {
|
||||
const v1 = parseInt(rangeMatch[1], 10);
|
||||
const v2 = parseInt(rangeMatch[2], 10);
|
||||
|
||||
return { min: Math.min(v1, v2), max: Math.max(v1, v2) };
|
||||
}
|
||||
|
||||
if ((ageStr.indexOf('shota') >= 0) || (ageStr.indexOf('loli') >= 0) || (ageStr.indexOf('lolli') >= 0)) {
|
||||
return { min: 10, max: 10 };
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
static calculateSearchScoreForMatch(
|
||||
score: Scoring,
|
||||
match: MatchReport,
|
||||
|
|
|
@ -161,10 +161,11 @@ export class ProfileCache extends AsyncCache<CharacterCacheRecord> {
|
|||
// const totalScoreDimensions = match ? Matcher.countScoresTotal(match) : 0;
|
||||
// const dimensionsAtScoreLevel = match ? (Matcher.countScoresAtLevel(match, score) || 0) : 0;
|
||||
// const dimensionsAboveScoreLevel = match ? (Matcher.countScoresAboveLevel(match, Math.max(score, Scoring.WEAK_MATCH))) : 0;
|
||||
const isFiltered = matchesSmartFilters(c.character, core.state.settings.risingFilter);
|
||||
const risingFilter = core.state.settings.risingFilter;
|
||||
const isFiltered = matchesSmartFilters(c.character, risingFilter);
|
||||
|
||||
const searchScore = match
|
||||
? Matcher.calculateSearchScoreForMatch(score, match, isFiltered && core.state.settings.risingFilter.penalizeMatches ? -2 : 0)
|
||||
? Matcher.calculateSearchScoreForMatch(score, match, (isFiltered && risingFilter.penalizeMatches) ? -2 : (!isFiltered && risingFilter.rewardNonMatches) ? 1 : 0)
|
||||
: 0;
|
||||
|
||||
const matchDetails = { matchScore: score, searchScore, isFiltered };
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "f-list-rising",
|
||||
"version": "1.16.2",
|
||||
"version": "1.17.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",
|
||||
|
|
Loading…
Reference in New Issue