0.2.9 - Hide ads, image viewer, bugfixes

This commit is contained in:
MayaWolf 2017-12-06 04:34:51 +01:00
parent ebf7cb43c5
commit b1a63ab6fb
21 changed files with 136 additions and 50 deletions

View File

@ -1,7 +1,7 @@
import {BBCodeCustomTag, BBCodeParser, BBCodeSimpleTag} from './parser';
const urlFormat = '((?:(?:https?|ftps?|irc):)?\\/\\/[^\\s\\/$.?#"\']+\\.[^\\s"]*)';
export const findUrlRegex = new RegExp(`((?!\\[url(?:\\]|=))(?:.{4}[^\\s])\\s+|^.{0,4}\\s|^)${urlFormat}`, 'g');
export const findUrlRegex = new RegExp(`(\\[url[=\\]]\\s*)?${urlFormat}`, 'gi');
export const urlRegex = new RegExp(`^${urlFormat}$`);
function domain(url: string): string | undefined {
@ -83,7 +83,8 @@ export class CoreBBCodeParser extends BBCodeParser {
}
parseEverything(input: string): HTMLElement {
if(this.makeLinksClickable && input.length > 0) input = input.replace(findUrlRegex, '$1[url]$2[/url]');
if(this.makeLinksClickable && input.length > 0)
input = input.replace(findUrlRegex, (match, tag) => tag === undefined ? `[url]${match}[/url]` : match);
return super.parseEverything(input);
}
}

View File

@ -44,7 +44,7 @@ export let defaultButtons: ReadonlyArray<EditorButton> = [
key: 's'
},
{
title: 'Color (Ctrl+Q)\n\nStyles text with a color. Valid colors are: red, orange, yellow, green, cyan, blue, purple, pink, black, white, gray, primary, secondary, accent, and contrast.',
title: 'Color (Ctrl+D)\n\nStyles text with a color. Valid colors are: red, orange, yellow, green, cyan, blue, purple, pink, black, white, gray, primary, secondary, accent, and contrast.',
tag: 'color',
startText: '[color=]',
icon: 'fa-eyedropper',

View File

@ -112,7 +112,8 @@
this.error = l('characterSearch.error.tooManyResults');
}
});
core.connection.onMessage('FKS', (data) => this.results = data.characters.map((x: string) => core.characters.get(x)).sort(sort));
core.connection.onMessage('FKS', (data) => this.results = data.characters.filter((x) =>
core.state.hiddenUsers.indexOf(x) === -1).map((x) => core.characters.get(x)).sort(sort));
(<Modal>this.$children[0]).fixDropdowns();
}

View File

@ -242,7 +242,8 @@
}
} else {
if(this.tabOptions !== undefined) this.tabOptions = undefined;
if(getKey(e) === 'ArrowUp' && this.conversation.enteredText.length === 0)
if(getKey(e) === 'ArrowUp' && this.conversation.enteredText.length === 0
&& !e.shiftKey && !e.altKey && !e.ctrlKey && !e.metaKey)
this.conversation.loadLastSent();
else if(getKey(e) === 'Enter') {
if(e.shiftKey) return;

View File

@ -27,6 +27,9 @@
<li><a tabindex="-1" href="#" @click.prevent="setIgnored">
<span class="fa fa-fw fa-minus-circle"></span>{{l('user.' + (character.isIgnored ? 'unignore' : 'ignore'))}}
</a></li>
<li><a tabindex="-1" href="#" @click.prevent="setHidden">
<span class="fa fa-fw fa-eye-slash"></span>{{l('user.' + (isHidden ? 'unhide' : 'hide'))}}
</a></li>
<li><a tabindex="-1" href="#" @click.prevent="report">
<span class="fa fa-fw fa-exclamation-triangle"></span>{{l('user.report')}}</a></li>
<li v-show="isChannelMod"><a tabindex="-1" href="#" @click.prevent="channelKick">
@ -89,6 +92,12 @@
.catch((e: object) => alert(errorToString(e)));
}
setHidden(): void {
const index = core.state.hiddenUsers.indexOf(this.character!.name);
if(index !== -1) core.state.hiddenUsers.splice(index, 1);
else core.state.hiddenUsers.push(this.character!.name);
}
report(): void {
this.reportDialog.report(this.character!);
}
@ -128,6 +137,10 @@
return member !== undefined && member.rank > Channel.Rank.Member;
}
get isHidden(): boolean {
return core.state.hiddenUsers.indexOf(this.character!.name) !== -1;
}
get isChatOp(): boolean {
return core.characters.ownCharacter.isChatOp;
}

View File

@ -505,7 +505,7 @@ export default function(this: void): Interfaces.State {
});
connection.onMessage('LRP', (data, time) => {
const char = core.characters.get(data.character);
if(char.isIgnored) return;
if(char.isIgnored || core.state.hiddenUsers.indexOf(char.name) !== -1) return;
const conv = state.channelMap[data.channel.toLowerCase()];
if(conv === undefined) return core.channels.leave(data.channel);
conv.addMessage(new Message(MessageType.Ad, char, decodeHTML(data.message), time));

View File

@ -12,6 +12,7 @@ function createBBCodeParser(): BBCodeParser {
class State implements StateInterface {
_settings: Settings | undefined = undefined;
hiddenUsers: string[] = [];
get settings(): Settings {
if(this._settings === undefined) throw new Error('Settings load failed.');
@ -41,6 +42,12 @@ const vue = <Vue & VueState>new Vue({
characters: undefined,
conversations: undefined,
state
},
watch: {
'state.hiddenUsers': (newValue: string[]) => {
//tslint:disable-next-line:no-floating-promises
if(data.settingsStore !== undefined) data.settingsStore.set('hiddenUsers', newValue);
}
}
});
@ -68,6 +75,8 @@ const data = {
if(loadedSettings !== undefined)
for(const key in loadedSettings) settings[<keyof Settings>key] = loadedSettings[<keyof Settings>key];
state._settings = settings;
const hiddenUsers = await core.settingsStore.get('hiddenUsers');
state.hiddenUsers = hiddenUsers !== undefined ? hiddenUsers : [];
}
};

View File

@ -142,6 +142,7 @@ export namespace Settings {
pinned: {channels: string[], private: string[]},
conversationSettings: {[key: string]: Conversation.Settings}
recent: Conversation.RecentConversation[]
hiddenUsers: string[]
};
export interface Store {
@ -180,4 +181,5 @@ export interface Notifications {
export interface State {
settings: Settings
hiddenUsers: string[]
}

View File

@ -74,6 +74,8 @@ const strings: {[key: string]: string | undefined} = {
'user.unbookmark': 'Unbookmark',
'user.ignore': 'Ignore',
'user.unignore': 'Unignore',
'user.hide': 'Hide ads',
'user.unhide': 'Unhide ads',
'user.memo': 'View memo',
'user.memo.action': 'Update memo',
'user.report': 'Report user',

View File

@ -1,11 +1,13 @@
<template>
<div tabindex="-1" class="modal flex-modal" :style="isShown ? 'display:flex' : ''"
style="align-items: flex-start; padding: 30px; justify-content: center;">
style="align-items: flex-start; padding: 30px; justify-content: center;">
<div class="modal-dialog" :class="dialogClass" style="display: flex; flex-direction: column; max-height: 100%; margin: 0;">
<div class="modal-content" style="display:flex; flex-direction: column;">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="Close">&times;</button>
<h4 class="modal-title">{{action}}</h4>
<h4 class="modal-title">
<slot name="title">{{action}}</slot>
</h4>
</div>
<div class="modal-body" style="overflow: auto; display: flex; flex-direction: column">
<slot></slot>
@ -28,7 +30,7 @@
@Component
export default class Modal extends Vue {
@Prop({required: true})
@Prop({default: ''})
readonly action: string;
@Prop()
readonly dialogClass?: {string: boolean};

View File

@ -40,8 +40,9 @@
<div class="progress-bar" :style="{width: importProgress * 100 + '%'}"></div>
</div>
</modal>
<modal action="Profile" :buttons="false" ref="profileViewer" dialogClass="profile-viewer">
<character-page :authenticated="false" :hideGroups="true" :name="profileName"></character-page>
<modal :buttons="false" ref="profileViewer" dialogClass="profile-viewer">
<character-page :authenticated="false" :hideGroups="true" :name="profileName" :image-preview="true"></character-page>
<template slot="title">{{profileName}} <a class="btn fa fa-external-link" @click="openProfileInBrowser"></a></template>
</modal>
</div>
</template>
@ -355,6 +356,10 @@
preview.style.display = 'none';
}
openProfileInBrowser(): void {
electron.remote.shell.openExternal(`https://www.f-list.net/c/${this.profileName}`);
}
get styling(): string {
try {
return `<style>${fs.readFileSync(path.join(__dirname, `themes/${this.currentSettings.theme}.css`))}</style>`;

View File

@ -1,6 +1,6 @@
{
"name": "fchat",
"version": "0.2.7",
"version": "0.2.9",
"author": "The F-List Team",
"description": "F-List.net Chat Client",
"main": "main.js",

View File

@ -115,8 +115,10 @@ function createWindow(): void {
if(process.env.NODE_ENV === 'production') runUpdater();
}
app.on('ready', createWindow);
app.makeSingleInstance(() => {
const running = app.makeSingleInstance(() => {
if(windows.length < 3) createWindow();
return true;
});
if(running) app.quit();
else app.on('ready', createWindow);
app.on('window-all-closed', () => app.quit());

View File

@ -1,12 +1,13 @@
import Axios from 'axios';
import * as electron from 'electron';
import * as fs from 'fs';
import * as path from 'path';
import {promisify} from 'util';
import {mkdir, nativeRequire} from './common';
process.env.SPELLCHECKER_PREFER_HUNSPELL = '1';
const downloadUrl = 'https://github.com/wooorm/dictionaries/raw/master/dictionaries/';
const dir = `${__dirname}/spellchecker`;
const downloadUrl = 'https://client.f-list.net/dictionaries/';
const dir = path.join(electron.remote.app.getPath('userData'), 'spellchecker');
mkdir(dir);
//tslint:disable-next-line
const sc = nativeRequire<{
@ -18,30 +19,35 @@ const sc = nativeRequire<{
}
}
}>('spellchecker/build/Release/spellchecker.node');
let availableDictionaries: string[] | undefined;
type DictionaryIndex = {[key: string]: {file: string, time: number} | undefined};
let availableDictionaries: DictionaryIndex | undefined;
const writeFile = promisify(fs.writeFile);
const requestConfig = {responseType: 'arraybuffer'};
const spellchecker = new sc.Spellchecker();
export async function getAvailableDictionaries(): Promise<ReadonlyArray<string>> {
if(availableDictionaries !== undefined) return availableDictionaries;
const dicts = (<{name: string}[]>(await Axios.get('https://api.github.com/repos/wooorm/dictionaries/contents/dictionaries')).data)
.map((x: {name: string}) => x.name);
availableDictionaries = dicts;
return dicts;
if(availableDictionaries === undefined) {
const indexPath = path.join(dir, 'index.json');
if(!fs.existsSync(indexPath) || fs.statSync(indexPath).mtimeMs + 86400000 * 7 < Date.now()) {
availableDictionaries = (await Axios.get<DictionaryIndex>(`${downloadUrl}index.json`)).data;
await writeFile(indexPath, JSON.stringify(availableDictionaries));
} else availableDictionaries = <DictionaryIndex>JSON.parse(fs.readFileSync(indexPath, 'utf8'));
}
return Object.keys(availableDictionaries).sort();
}
export async function setDictionary(lang: string | undefined): Promise<void> {
const dictName = lang !== undefined ? lang.replace('-', '_') : undefined;
if(dictName !== undefined) {
const dicPath = path.join(dir, `${dictName}.dic`);
if(!fs.existsSync(dicPath)) {
await writeFile(dicPath, new Buffer(<string>(await Axios.get(`${downloadUrl}${lang}/index.dic`, requestConfig)).data));
await writeFile(path.join(dir, `${dictName}.aff`),
new Buffer(<string>(await Axios.get(`${downloadUrl}${lang}/index.aff`, requestConfig)).data));
const dict = availableDictionaries![lang!];
if(dict !== undefined) {
const dicPath = path.join(dir, `${lang}.dic`);
if(!fs.existsSync(dicPath) || fs.statSync(dicPath).mtimeMs / 1000 < dict.time) {
await writeFile(dicPath, new Buffer((await Axios.get<string>(`${downloadUrl}${dict.file}.dic`, requestConfig)).data));
await writeFile(path.join(dir, `${lang}.aff`),
new Buffer((await Axios.get<string>(`${downloadUrl}${dict.file}.aff`, requestConfig)).data));
fs.utimesSync(dicPath, dict.time, dict.time);
}
}
spellchecker.setDictionary(dictName, dir);
spellchecker.setDictionary(lang, dir);
}
export function getCorrections(word: string): ReadonlyArray<string> {

View File

@ -1,6 +1,11 @@
import {decodeHTML} from './common';
import {Channel as Interfaces, Character, Connection} from './interfaces';
interface SortableMember extends Interfaces.Member {
rank: Interfaces.Rank,
key: string
}
export function queuedJoin(this: void, channels: string[]): void {
const timer: NodeJS.Timer = setInterval(() => {
const channel = channels.shift();
@ -9,8 +14,7 @@ export function queuedJoin(this: void, channels: string[]): void {
}, 100);
}
function sortMember(this: void | never, array: Interfaces.Member[], member: Interfaces.Member): void {
const name = member.character.name;
function sortMember(this: void | never, array: SortableMember[], member: SortableMember): void {
let i = 0;
for(; i < array.length; ++i) {
const other = array[i];
@ -22,7 +26,7 @@ function sortMember(this: void | never, array: Interfaces.Member[], member: Inte
if(member.character.isFriend && !other.character.isFriend) break;
if(other.character.isBookmarked && !member.character.isBookmarked) continue;
if(member.character.isBookmarked && !other.character.isBookmarked) break;
if(name < other.character.name) break;
if(member.key < other.key) break;
}
array.splice(i, 0, member);
}
@ -32,13 +36,13 @@ class Channel implements Interfaces.Channel {
opList: string[];
owner = '';
mode: Interfaces.Mode = 'both';
members: {[key: string]: {character: Character, rank: Interfaces.Rank} | undefined} = {};
sortedMembers: Interfaces.Member[] = [];
members: {[key: string]: SortableMember | undefined} = {};
sortedMembers: SortableMember[] = [];
constructor(readonly id: string, readonly name: string) {
}
addMember(member: Interfaces.Member): void {
addMember(member: SortableMember): void {
this.members[member.character.name] = member;
sortMember(this.sortedMembers, member);
for(const handler of state.handlers) handler('join', this, member);
@ -53,16 +57,17 @@ class Channel implements Interfaces.Channel {
}
}
reSortMember(member: Interfaces.Member): void {
reSortMember(member: SortableMember): void {
this.sortedMembers.splice(this.sortedMembers.indexOf(member), 1);
sortMember(this.sortedMembers, member);
}
createMember(character: Character): {character: Character, rank: Interfaces.Rank} {
createMember(character: Character): SortableMember {
return {
character,
rank: this.owner === character.name ? Interfaces.Rank.Owner :
this.opList.indexOf(character.name) !== -1 ? Interfaces.Rank.Op : Interfaces.Rank.Member
this.opList.indexOf(character.name) !== -1 ? Interfaces.Rank.Op : Interfaces.Rank.Member,
key: character.name.toLowerCase()
};
}
}
@ -173,8 +178,8 @@ export default function(this: void, connection: Connection, characters: Characte
const channel = state.getChannel(data.channel);
if(channel === undefined) return state.leave(data.channel);
channel.mode = data.mode;
const members: {[key: string]: Interfaces.Member} = {};
const sorted: Interfaces.Member[] = [];
const members: {[key: string]: SortableMember} = {};
const sorted: SortableMember[] = [];
for(const user of data.users) {
const name = user.identity;
const member = channel.createMember(characters.get(name));

View File

@ -42,7 +42,7 @@
color: @white-color;
}
.blackColor {
.blackText {
color: @black-color;
}

View File

@ -27,7 +27,7 @@
cursor: pointer;
}
}
.badges-block,.contact-block,.quick-info-block,.character-list-block {
.badges-block, .contact-block, .quick-info-block, .character-list-block {
margin-top: 15px;
}
}
@ -37,7 +37,7 @@
background-color: @character-badge-bg;
border: 1px solid @character-badge-border;
border-radius: @border-radius-base;
.box-shadow(inset 0 1px 1px rgba(0,0,0,.05));
.box-shadow(inset 0 1px 1px rgba(0, 0, 0, .05));
&.character-badge-subscription-lifetime {
background-color: @character-badge-subscriber-bg;
@ -196,3 +196,21 @@
margin: 5px;
}
}
.image-preview {
position: fixed;
top: 0;
bottom: 0;
right: 0;
left: 0;
display: flex;
align-items: center;
justify-content: center;
img {
padding: 5px;
background: white;
z-index: 1100;
max-height: 100%;
max-width: 100%;
}
}

View File

@ -6,7 +6,7 @@
@gray-darker: lighten(@gray-base, 4%);
@gray-dark: lighten(@gray-base, 20%);
@gray: lighten(@gray-base, 55%);
@gray-light: lighten(@gray-base, 85%);
@gray-light: lighten(@gray-base, 80%);
@gray-lighter: lighten(@gray-base, 95%);
@body-bg: @gray-darker;
@ -19,6 +19,7 @@
@brand-success: #080;
@brand-info: #13b;
@brand-primary: @brand-info;
@blue-color: #36f;
@state-info-bg: darken(@brand-info, 15%);
@state-info-text: lighten(@brand-info, 30%);

View File

@ -6,8 +6,8 @@
@gray-darker: lighten(@gray-base, 15%);
@gray-dark: lighten(@gray-base, 25%);
@gray: lighten(@gray-base, 55%);
@gray-light: lighten(@gray-base, 76.7%);
@gray-lighter: lighten(@gray-base, 93.5%);
@gray-light: lighten(@gray-base, 73%);
@gray-lighter: lighten(@gray-base, 95%);
// @body-bg: #262626;
@body-bg: darken(@text-background-color-disabled, 3%);
@ -20,6 +20,7 @@
@brand-success: #009900;
@brand-info: #0447af;
@brand-primary: @brand-info;
@blue-color: #36f;
@state-info-bg: darken(@brand-info, 15%);
@state-info-text: lighten(@brand-info, 30%);

View File

@ -24,7 +24,8 @@
<li role="presentation" class="active"><a href="#overview" aria-controls="overview" role="tab" data-toggle="tab">Overview</a>
</li>
<li role="presentation"><a href="#infotags" aria-controls="infotags" role="tab" data-toggle="tab">Info</a></li>
<li role="presentation" v-if="!hideGroups"><a href="#groups" aria-controls="groups" role="tab" data-toggle="tab">Groups</a></li>
<li role="presentation" v-if="!hideGroups"><a href="#groups" aria-controls="groups" role="tab" data-toggle="tab">Groups</a>
</li>
<li role="presentation"><a href="#images" aria-controls="images" role="tab"
data-toggle="tab">Images ({{ character.character.image_count }})</a></li>
<li v-if="character.settings.guestbook" role="presentation"><a href="#guestbook" aria-controls="guestbook"
@ -45,7 +46,7 @@
<character-groups :character="character" ref="groups"></character-groups>
</div>
<div role="tabpanel" class="tab-pane" id="images" aria-labeledby="images-tab">
<character-images :character="character" ref="images"></character-images>
<character-images :character="character" ref="images" :use-preview="imagePreview"></character-images>
</div>
<div v-if="character.settings.guestbook" role="tabpanel" class="tab-pane" id="guestbook"
aria-labeledby="guestbook-tab">
@ -106,6 +107,8 @@
private readonly authenticated: boolean;
@Prop()
readonly hideGroups?: true;
@Prop()
readonly imagePreview?: true;
private shared: SharedStore = Store;
private character: Character | null = null;
loading = true;

View File

@ -3,12 +3,16 @@
<div v-show="loading" class="alert alert-info">Loading images.</div>
<template v-if="!loading">
<div class="character-image" v-for="image in images" :key="image.id">
<a :href="imageUrl(image)" target="_blank">
<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 = ''">
<img :src="previewImage" />
<div class="modal-backdrop in"></div>
</div>
</div>
</template>
@ -24,7 +28,10 @@
export default class ImagesView extends Vue {
@Prop({required: true})
private readonly character: Character;
@Prop()
private readonly usePreview?: boolean;
private shown = false;
previewImage = '';
images: CharacterImage[] = [];
loading = true;
error = '';
@ -47,5 +54,12 @@
}
this.loading = false;
}
handleImageClick(e: MouseEvent, image: CharacterImage): void {
if(this.usePreview) {
this.previewImage = methods.imageUrl(image);
e.preventDefault();
}
}
}
</script>