From b1a63ab6fb96db3edd8ad2098bc4ea79195041bb Mon Sep 17 00:00:00 2001 From: MayaWolf Date: Wed, 6 Dec 2017 04:34:51 +0100 Subject: [PATCH] 0.2.9 - Hide ads, image viewer, bugfixes --- bbcode/core.ts | 5 ++-- bbcode/editor.ts | 2 +- chat/CharacterSearch.vue | 3 +- chat/ConversationView.vue | 3 +- chat/UserMenu.vue | 13 +++++++++ chat/conversations.ts | 2 +- chat/core.ts | 9 ++++++ chat/interfaces.ts | 2 ++ chat/localize.ts | 2 ++ components/Modal.vue | 8 ++++-- electron/Index.vue | 9 ++++-- electron/application.json | 2 +- electron/main.ts | 6 ++-- electron/spellchecker.ts | 38 +++++++++++++++----------- fchat/channels.ts | 27 ++++++++++-------- less/bbcode.less | 2 +- less/character_page.less | 22 +++++++++++++-- less/themes/variables/dark.less | 3 +- less/themes/variables/default.less | 5 ++-- site/character_page/character_page.vue | 7 +++-- site/character_page/images.vue | 16 ++++++++++- 21 files changed, 136 insertions(+), 50 deletions(-) diff --git a/bbcode/core.ts b/bbcode/core.ts index ba6d9dc..2266de6 100644 --- a/bbcode/core.ts +++ b/bbcode/core.ts @@ -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); } } \ No newline at end of file diff --git a/bbcode/editor.ts b/bbcode/editor.ts index e020a09..635714e 100644 --- a/bbcode/editor.ts +++ b/bbcode/editor.ts @@ -44,7 +44,7 @@ export let defaultButtons: ReadonlyArray = [ 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', diff --git a/chat/CharacterSearch.vue b/chat/CharacterSearch.vue index 4d3e6fa..62397db 100644 --- a/chat/CharacterSearch.vue +++ b/chat/CharacterSearch.vue @@ -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)); (this.$children[0]).fixDropdowns(); } diff --git a/chat/ConversationView.vue b/chat/ConversationView.vue index a51fdb2..1e70916 100644 --- a/chat/ConversationView.vue +++ b/chat/ConversationView.vue @@ -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; diff --git a/chat/UserMenu.vue b/chat/UserMenu.vue index a02ae81..3eb6e30 100644 --- a/chat/UserMenu.vue +++ b/chat/UserMenu.vue @@ -27,6 +27,9 @@
  • {{l('user.' + (character.isIgnored ? 'unignore' : 'ignore'))}}
  • +
  • + {{l('user.' + (isHidden ? 'unhide' : 'hide'))}} +
  • {{l('user.report')}}
  • @@ -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; } diff --git a/chat/conversations.ts b/chat/conversations.ts index 86393bc..84dd094 100644 --- a/chat/conversations.ts +++ b/chat/conversations.ts @@ -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)); diff --git a/chat/core.ts b/chat/core.ts index 9cd2ff1..8758e5b 100644 --- a/chat/core.ts +++ b/chat/core.ts @@ -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 = 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[key] = loadedSettings[key]; state._settings = settings; + const hiddenUsers = await core.settingsStore.get('hiddenUsers'); + state.hiddenUsers = hiddenUsers !== undefined ? hiddenUsers : []; } }; diff --git a/chat/interfaces.ts b/chat/interfaces.ts index 6e7752f..88121ee 100644 --- a/chat/interfaces.ts +++ b/chat/interfaces.ts @@ -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[] } \ No newline at end of file diff --git a/chat/localize.ts b/chat/localize.ts index 64ba7eb..534d092 100644 --- a/chat/localize.ts +++ b/chat/localize.ts @@ -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', diff --git a/components/Modal.vue b/components/Modal.vue index c1b6202..9b2cebe 100644 --- a/components/Modal.vue +++ b/components/Modal.vue @@ -1,11 +1,13 @@ @@ -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 ``; diff --git a/electron/application.json b/electron/application.json index 5ccd150..19b1b6c 100644 --- a/electron/application.json +++ b/electron/application.json @@ -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", diff --git a/electron/main.ts b/electron/main.ts index 2234373..5ef5444 100644 --- a/electron/main.ts +++ b/electron/main.ts @@ -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()); \ No newline at end of file diff --git a/electron/spellchecker.ts b/electron/spellchecker.ts index e56807f..e73f048 100644 --- a/electron/spellchecker.ts +++ b/electron/spellchecker.ts @@ -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> { - 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(`${downloadUrl}index.json`)).data; + await writeFile(indexPath, JSON.stringify(availableDictionaries)); + } else availableDictionaries = JSON.parse(fs.readFileSync(indexPath, 'utf8')); + } + return Object.keys(availableDictionaries).sort(); } export async function setDictionary(lang: string | undefined): Promise { - 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((await Axios.get(`${downloadUrl}${lang}/index.dic`, requestConfig)).data)); - await writeFile(path.join(dir, `${dictName}.aff`), - new Buffer((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(`${downloadUrl}${dict.file}.dic`, requestConfig)).data)); + await writeFile(path.join(dir, `${lang}.aff`), + new Buffer((await Axios.get(`${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 { diff --git a/fchat/channels.ts b/fchat/channels.ts index 61898af..64e2186 100644 --- a/fchat/channels.ts +++ b/fchat/channels.ts @@ -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)); diff --git a/less/bbcode.less b/less/bbcode.less index de45c90..006e165 100644 --- a/less/bbcode.less +++ b/less/bbcode.less @@ -42,7 +42,7 @@ color: @white-color; } -.blackColor { +.blackText { color: @black-color; } diff --git a/less/character_page.less b/less/character_page.less index 26e9b4a..9472ae3 100644 --- a/less/character_page.less +++ b/less/character_page.less @@ -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%; + } +} \ No newline at end of file diff --git a/less/themes/variables/dark.less b/less/themes/variables/dark.less index 30aab38..3da3865 100644 --- a/less/themes/variables/dark.less +++ b/less/themes/variables/dark.less @@ -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%); diff --git a/less/themes/variables/default.less b/less/themes/variables/default.less index e90bba3..989a5d2 100644 --- a/less/themes/variables/default.less +++ b/less/themes/variables/default.less @@ -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%); diff --git a/site/character_page/character_page.vue b/site/character_page/character_page.vue index fd26374..b4ed63f 100644 --- a/site/character_page/character_page.vue +++ b/site/character_page/character_page.vue @@ -24,7 +24,8 @@
  • Info
  • -
  • Groups
  • +
  • Groups +
  • Images ({{ character.character.image_count }})
  • - +
    @@ -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; diff --git a/site/character_page/images.vue b/site/character_page/images.vue index b17a94b..ff04743 100644 --- a/site/character_page/images.vue +++ b/site/character_page/images.vue @@ -3,12 +3,16 @@
    Loading images.
    No images.
    +
    + + +
    @@ -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(); + } + } } \ No newline at end of file