This commit is contained in:
MayaWolf 2018-09-28 02:08:10 +02:00
parent 39f9365299
commit 8810b29552
31 changed files with 1733 additions and 1075 deletions

View File

@ -99,7 +99,7 @@
return; return;
} }
const selection = document.getSelection(); const selection = document.getSelection();
if(selection.isCollapsed) return; if(selection === null || selection.isCollapsed) return;
const range = selection.getRangeAt(0); const range = selection.getRangeAt(0);
let start = range.startContainer, end = range.endContainer; let start = range.startContainer, end = range.endContainer;
let startValue: string; let startValue: string;

View File

@ -187,8 +187,9 @@
}]; }];
window.addEventListener('resize', this.resizeHandler = () => this.keepScroll()); window.addEventListener('resize', this.resizeHandler = () => this.keepScroll());
window.addEventListener('keypress', this.keypressHandler = () => { window.addEventListener('keypress', this.keypressHandler = () => {
if(document.getSelection().isCollapsed && !anyDialogsShown && const selection = document.getSelection();
(document.activeElement === document.body || document.activeElement.tagName === 'A')) if((selection === null || selection.isCollapsed) && !anyDialogsShown &&
(document.activeElement === document.body || document.activeElement === null || document.activeElement.tagName === 'A'))
(<Editor>this.$refs['textBox']).focus(); (<Editor>this.$refs['textBox']).focus();
}); });
window.addEventListener('keydown', this.keydownHandler = ((e: KeyboardEvent) => { window.addEventListener('keydown', this.keydownHandler = ((e: KeyboardEvent) => {
@ -211,11 +212,9 @@
this.adsMode = l('channel.mode.ads'); this.adsMode = l('channel.mode.ads');
} else this.adsMode = l('channel.mode.ads.countdown', Math.floor(diff / 60), Math.floor(diff % 60)); } else this.adsMode = l('channel.mode.ads.countdown', Math.floor(diff / 60), Math.floor(diff % 60));
}; };
if(Date.now() < value) { if(Date.now() < value && this.adCountdown === 0)
if(this.adCountdown === 0) this.adCountdown = window.setInterval(setAdCountdown, 1000);
this.adCountdown = window.setInterval(setAdCountdown, 1000); setAdCountdown();
setAdCountdown();
}
}); });
} }
@ -261,11 +260,13 @@
} }
keepScroll(): void { keepScroll(): void {
if(this.scrolledDown) if(this.scrolledDown) {
this.ignoreScroll = true;
this.$nextTick(() => setTimeout(() => { this.$nextTick(() => setTimeout(() => {
this.ignoreScroll = true; this.ignoreScroll = true;
this.messageView.scrollTop = this.messageView.scrollHeight; this.messageView.scrollTop = this.messageView.scrollHeight;
}, 0)); }, 0));
}
} }
onMessagesScroll(): void { onMessagesScroll(): void {

View File

@ -205,6 +205,7 @@
if(getKey(e) === Keys.KeyA && (e.ctrlKey || e.metaKey) && !e.altKey && !e.shiftKey) { if(getKey(e) === Keys.KeyA && (e.ctrlKey || e.metaKey) && !e.altKey && !e.shiftKey) {
e.preventDefault(); e.preventDefault();
const selection = document.getSelection(); const selection = document.getSelection();
if(selection === null) return;
selection.removeAllRanges(); selection.removeAllRanges();
if(this.messages.length > 0) { if(this.messages.length > 0) {
const range = document.createRange(); const range = document.createRange();

View File

@ -216,7 +216,7 @@
async submit(): Promise<void> { async submit(): Promise<void> {
const idleTimer = parseInt(this.idleTimer, 10); const idleTimer = parseInt(this.idleTimer, 10);
const fontSize = parseInt(this.fontSize, 10); const fontSize = parseFloat(this.fontSize);
core.state.settings = { core.state.settings = {
playSound: this.playSound, playSound: this.playSound,
clickOpensMessage: this.clickOpensMessage, clickOpensMessage: this.clickOpensMessage,

View File

@ -17,6 +17,7 @@ export const BBCodeView: Component = {
insert(node: VNode): void { insert(node: VNode): void {
node.elm!.appendChild(core.bbCodeParser.parseEverything( node.elm!.appendChild(core.bbCodeParser.parseEverything(
context.props.text !== undefined ? context.props.text : context.props.unsafeText)); context.props.text !== undefined ? context.props.text : context.props.unsafeText));
if(context.props.afterInsert !== undefined) context.props.afterInsert(node.elm);
}, },
destroy(node: VNode): void { destroy(node: VNode): void {
const element = (<BBCodeElement>(<Element>node.elm).firstChild); const element = (<BBCodeElement>(<Element>node.elm).firstChild);

View File

@ -613,30 +613,36 @@ export default function(this: void): Interfaces.State {
if(conv !== undefined) conv.typingStatus = data.status; if(conv !== undefined) conv.typingStatus = data.status;
}); });
connection.onMessage('CBU', async(data, time) => { connection.onMessage('CBU', async(data, time) => {
const text = l('events.ban', data.channel, data.character, data.operator);
const conv = state.channelMap[data.channel.toLowerCase()]; const conv = state.channelMap[data.channel.toLowerCase()];
if(conv === undefined) return core.channels.leave(data.channel); if(conv === undefined) return core.channels.leave(data.channel);
const text = l('events.ban', conv.name, data.character, data.operator);
conv.infoText = text; conv.infoText = text;
return addEventMessage(new EventMessage(text, time)); return addEventMessage(new EventMessage(text, time));
}); });
connection.onMessage('CKU', async(data, time) => { connection.onMessage('CKU', async(data, time) => {
const text = l('events.kick', data.channel, data.character, data.operator);
const conv = state.channelMap[data.channel.toLowerCase()]; const conv = state.channelMap[data.channel.toLowerCase()];
if(conv === undefined) return core.channels.leave(data.channel); if(conv === undefined) return core.channels.leave(data.channel);
const text = l('events.kick', conv.name, data.character, data.operator);
conv.infoText = text; conv.infoText = text;
return addEventMessage(new EventMessage(text, time)); return addEventMessage(new EventMessage(text, time));
}); });
connection.onMessage('CTU', async(data, time) => { connection.onMessage('CTU', async(data, time) => {
const text = l('events.timeout', data.channel, data.character, data.operator, data.length.toString());
const conv = state.channelMap[data.channel.toLowerCase()]; const conv = state.channelMap[data.channel.toLowerCase()];
if(conv === undefined) return core.channels.leave(data.channel); if(conv === undefined) return core.channels.leave(data.channel);
const text = l('events.timeout', conv.name, data.character, data.operator, data.length.toString());
conv.infoText = text; conv.infoText = text;
return addEventMessage(new EventMessage(text, time)); return addEventMessage(new EventMessage(text, time));
}); });
connection.onMessage('BRO', async(data, time) => { connection.onMessage('BRO', async(data, time) => {
const text = data.character === undefined ? decodeHTML(data.message) : if(data.character !== undefined) {
l('events.broadcast', `[user]${data.character}[/user]`, decodeHTML(data.message.substr(data.character.length + 23))); const content = decodeHTML(data.message.substr(data.character.length + 24));
return addEventMessage(new EventMessage(text, time)); const message = new EventMessage(l('events.broadcast', `[user]${data.character}[/user]`, content), time);
await state.consoleTab.addMessage(message);
await core.notifications.notify(state.consoleTab, l('events.broadcast.notification', data.character), content,
characterImage(data.character), 'attention');
for(const conv of (<Conversation[]>state.channelConversations).concat(state.privateConversations))
await conv.addMessage(message);
} else return addEventMessage(new EventMessage(decodeHTML(data.message), time));
}); });
connection.onMessage('CIU', async(data, time) => { connection.onMessage('CIU', async(data, time) => {
const text = l('events.invite', `[user]${data.sender}[/user]`, `[session=${data.title}]${data.name}[/session]`); const text = l('events.invite', `[user]${data.sender}[/user]`, `[session=${data.title}]${data.name}[/session]`);

View File

@ -176,6 +176,7 @@ Current log location: {1}`,
'settings.defaultHighlights': 'Use global highlight words', 'settings.defaultHighlights': 'Use global highlight words',
'settings.colorBookmarks': 'Show friends/bookmarks in a different colour', 'settings.colorBookmarks': 'Show friends/bookmarks in a different colour',
'settings.beta': 'Opt-in to test unstable prerelease updates', 'settings.beta': 'Opt-in to test unstable prerelease updates',
'settings.hwAcceleration': 'Enable hardware acceleration (requires restart)',
'settings.bbCodeBar': 'Show BBCode formatting bar', 'settings.bbCodeBar': 'Show BBCode formatting bar',
'fixLogs.action': 'Fix corrupted logs', 'fixLogs.action': 'Fix corrupted logs',
'fixLogs.text': `There are a few reason log files can become corrupted - log files from old versions with bugs that have since been fixed or incomplete file operations caused by computer crashes are the most common. 'fixLogs.text': `There are a few reason log files can become corrupted - log files from old versions with bugs that have since been fixed or incomplete file operations caused by computer crashes are the most common.
@ -221,7 +222,8 @@ Once this process has started, do not interrupt it or your logs will get corrupt
'characterSearch.error.noResults': 'There were no search results.', 'characterSearch.error.noResults': 'There were no search results.',
'characterSearch.error.throttle': 'You must wait five seconds between searches.', 'characterSearch.error.throttle': 'You must wait five seconds between searches.',
'characterSearch.error.tooManyResults': 'There are too many search results, please narrow your search.', 'characterSearch.error.tooManyResults': 'There are too many search results, please narrow your search.',
'events.broadcast': '{0} has broadcast {1}', 'events.broadcast': '{0} has broadcast: {1}',
'events.broadcast.notification': 'Broadcast from {0}',
'events.invite': '{0} has invited you to join {1}', 'events.invite': '{0} has invited you to join {1}',
'events.error': 'Error: {0}', 'events.error': 'Error: {0}',
'events.rtbCommentReply': '{0} replied to your comment on the {1}: {2}', 'events.rtbCommentReply': '{0} replied to your comment on the {1}: {2}',

View File

@ -37,7 +37,18 @@ const MessageView: Component = {
userPostfix[message.type] !== undefined ? userPostfix[message.type]! : ' '); userPostfix[message.type] !== undefined ? userPostfix[message.type]! : ' ');
if(message.isHighlight) classes += ' message-highlight'; if(message.isHighlight) classes += ' message-highlight';
} }
children.push(createElement(BBCodeView, {props: {unsafeText: message.text}})); children.push(createElement(BBCodeView,
{props: {unsafeText: message.text, afterInsert: message.type === Conversation.Message.Type.Ad ? (elm: HTMLElement) => {
setImmediate(() => {
elm = elm.parentElement!;
if(elm.scrollHeight > elm.offsetHeight) {
const expand = document.createElement('div');
expand.className = 'expand fas fa-caret-down';
expand.addEventListener('click', function(): void { this.parentElement!.className += ' expanded'; });
elm.appendChild(expand);
}
});
} : undefined}}));
const node = createElement('div', {attrs: {class: classes}}, children); const node = createElement('div', {attrs: {class: classes}}, children);
node.key = context.data.key; node.key = context.data.key;
return node; return node;

View File

@ -14,7 +14,8 @@ export default class Notifications implements Interface {
async notify(conversation: Conversation, title: string, body: string, icon: string, sound: string): Promise<void> { async notify(conversation: Conversation, title: string, body: string, icon: string, sound: string): Promise<void> {
if(!this.shouldNotify(conversation)) return; if(!this.shouldNotify(conversation)) return;
this.playSound(sound); this.playSound(sound);
if(core.state.settings.notifications && (<any>Notification).permission === 'granted') { //tslint:disable-line:no-any if(core.state.settings.notifications && (<{Notification?: object}>window).Notification !== undefined
&& Notification.permission === 'granted') {
const notification = new Notification(title, this.getOptions(conversation, body, icon)); const notification = new Notification(title, this.getOptions(conversation, body, icon));
notification.onclick = () => { notification.onclick = () => {
conversation.show(); conversation.show();
@ -69,6 +70,6 @@ export default class Notifications implements Interface {
} }
async requestPermission(): Promise<void> { async requestPermission(): Promise<void> {
await Notification.requestPermission(); if((<{Notification?: object}>window).Notification !== undefined) await Notification.requestPermission();
} }
} }

View File

@ -1,18 +1,18 @@
<template> <template>
<div style="display:flex;flex-direction:column;height:100%;padding:1px" :class="'platform-' + platform" @auxclick.prevent> <div style="display:flex;flex-direction:column;height:100%" :class="'platform-' + platform" @auxclick.prevent>
<div v-html="styling"></div> <div v-html="styling"></div>
<div style="display:flex;align-items:stretch;" class="border-bottom" id="window-tabs"> <div style="display:flex;align-items:stretch;border-bottom-width:1px" class="border-bottom" id="window-tabs">
<h4>F-Chat</h4> <h4 style="padding:2px 0">F-Chat</h4>
<div class="btn" :class="'btn-' + (hasUpdate ? 'warning' : 'light')" @click="openMenu" id="settings"> <div class="btn" :class="'btn-' + (hasUpdate ? 'warning' : 'light')" @click="openMenu" id="settings">
<i class="fa fa-cog"></i> <i class="fa fa-cog"></i>
</div> </div>
<ul class="nav nav-tabs" style="border-bottom:0;margin-bottom:-2px" ref="tabs"> <ul class="nav nav-tabs" style="border-bottom:0;margin-bottom:-1px;margin-top:1px" ref="tabs">
<li v-for="tab in tabs" :key="tab.view.id" class="nav-item" @click.middle="remove(tab)"> <li v-for="tab in tabs" :key="tab.view.id" class="nav-item" @click.middle="remove(tab)">
<a href="#" @click.prevent="show(tab)" class="nav-link" <a href="#" @click.prevent="show(tab)" class="nav-link tab"
:class="{active: tab === activeTab, hasNew: tab.hasNew && tab !== activeTab}"> :class="{active: tab === activeTab, hasNew: tab.hasNew && tab !== activeTab}">
<img v-if="tab.user" :src="'https://static.f-list.net/images/avatar/' + tab.user.toLowerCase() + '.png'"/> <img v-if="tab.user" :src="'https://static.f-list.net/images/avatar/' + tab.user.toLowerCase() + '.png'"/>
<span class="d-sm-inline d-none">{{tab.user || l('window.newTab')}}</span> <span class="d-sm-inline d-none">{{tab.user || l('window.newTab')}}</span>
<a href="#" class="btn" :aria-label="l('action.close')" style="margin-left:10px;padding:0;color:inherit" <a href="#" :aria-label="l('action.close')" style="margin-left:10px;padding:0;color:inherit;text-decoration:none"
@click.stop="remove(tab)"><i class="fa fa-times"></i> @click.stop="remove(tab)"><i class="fa fa-times"></i>
</a> </a>
</a> </a>
@ -21,7 +21,7 @@
<a href="#" @click.prevent="addTab" class="nav-link"><i class="fa fa-plus"></i></a> <a href="#" @click.prevent="addTab" class="nav-link"><i class="fa fa-plus"></i></a>
</li> </li>
</ul> </ul>
<div style="flex:1;display:flex;justify-content:flex-end;-webkit-app-region:drag;margin-top:3px" class="btn-group" <div style="flex:1;display:flex;justify-content:flex-end;-webkit-app-region:drag" class="btn-group"
id="windowButtons"> id="windowButtons">
<i class="far fa-window-minimize btn btn-light" @click.stop="minimize"></i> <i class="far fa-window-minimize btn btn-light" @click.stop="minimize"></i>
<i class="far btn btn-light" :class="'fa-window-' + (isMaximized ? 'restore' : 'maximize')" @click="maximize"></i> <i class="far btn btn-light" :class="'fa-window-' + (isMaximized ? 'restore' : 'maximize')" @click="maximize"></i>
@ -225,7 +225,7 @@
} }
remove(tab: Tab, shouldConfirm: boolean = true): void { remove(tab: Tab, shouldConfirm: boolean = true): void {
if(shouldConfirm && tab.user !== undefined && !confirm(l('chat.confirmLeave'))) return; if(this.lockTab || shouldConfirm && tab.user !== undefined && !confirm(l('chat.confirmLeave'))) return;
this.tabs.splice(this.tabs.indexOf(tab), 1); this.tabs.splice(this.tabs.indexOf(tab), 1);
electron.ipcRenderer.send('has-new', this.tabs.reduce((cur, t) => cur || t.hasNew, false)); electron.ipcRenderer.send('has-new', this.tabs.reduce((cur, t) => cur || t.hasNew, false));
delete this.tabMap[tab.view.webContents.id]; delete this.tabMap[tab.view.webContents.id];
@ -259,11 +259,12 @@
#window-tabs { #window-tabs {
user-select: none; user-select: none;
.btn { .btn {
border: 0;
border-radius: 0; border-radius: 0;
padding: 2px 15px; padding: 0 18px;
display: flex; display: flex;
margin: 0px -1px -1px 0;
align-items: center; align-items: center;
line-height: 1;
-webkit-app-region: no-drag; -webkit-app-region: no-drag;
} }
@ -287,10 +288,6 @@
height: 28px; height: 28px;
margin: -5px 3px -5px -5px; margin: -5px 3px -5px -5px;
} }
&.active {
margin-bottom: -2px;
}
} }
h4 { h4 {
@ -307,8 +304,8 @@
} }
#windowButtons .btn { #windowButtons .btn {
margin: -4px -1px -1px 0;
border-top: 0; border-top: 0;
font-size: 14px;
} }
.platform-darwin { .platform-darwin {
@ -322,8 +319,8 @@
} }
.btn, li a { .btn, li a {
padding-top: 5px; padding-top: 6px;
padding-bottom: 5px; padding-bottom: 6px;
} }
} }
} }

View File

@ -15,6 +15,7 @@ export class GeneralSettings {
version = electron.app.getVersion(); version = electron.app.getVersion();
beta = false; beta = false;
customDictionary: string[] = []; customDictionary: string[] = [];
hwAcceleration = true;
} }
export function mkdir(dir: string): void { export function mkdir(dir: string): void {

View File

@ -2,6 +2,7 @@
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta http-equiv="Content-Security-Policy" content="default-src 'self' 'unsafe-inline'; img-src file: data: https://static.f-list.net; connect-src *">
<title>F-Chat</title> <title>F-Chat</title>
<link href="fa.css" rel="stylesheet"> <link href="fa.css" rel="stylesheet">
</head> </head>

View File

@ -59,6 +59,19 @@ mkdir(settingsDir);
const settingsFile = path.join(settingsDir, 'settings'); const settingsFile = path.join(settingsDir, 'settings');
const settings = new GeneralSettings(); const settings = new GeneralSettings();
if(!fs.existsSync(settingsFile)) shouldImportSettings = true;
else
try {
Object.assign(settings, <GeneralSettings>JSON.parse(fs.readFileSync(settingsFile, 'utf8')));
} catch(e) {
log.error(`Error loading settings: ${e}`);
}
if(!settings.hwAcceleration) {
log.info('Disabling hardware acceleration.');
app.disableHardwareAcceleration();
}
async function setDictionary(lang: string | undefined): Promise<void> { async function setDictionary(lang: string | undefined): Promise<void> {
if(lang !== undefined) await ensureDictionary(lang); if(lang !== undefined) await ensureDictionary(lang);
settings.spellcheckLang = lang; settings.spellcheckLang = lang;
@ -142,14 +155,6 @@ function onReady(): void {
log.transports.file.file = path.join(baseDir, 'log.txt'); log.transports.file.file = path.join(baseDir, 'log.txt');
log.info('Starting application.'); log.info('Starting application.');
if(!fs.existsSync(settingsFile)) shouldImportSettings = true;
else
try {
Object.assign(settings, <GeneralSettings>JSON.parse(fs.readFileSync(settingsFile, 'utf8')));
} catch(e) {
log.error(`Error loading settings: ${e}`);
}
app.setAppUserModelId('com.squirrel.fchat.F-Chat'); app.setAppUserModelId('com.squirrel.fchat.F-Chat');
app.on('open-file', createWindow); app.on('open-file', createWindow);
@ -271,6 +276,12 @@ function onReady(): void {
label: x, label: x,
type: <'radio'>'radio' type: <'radio'>'radio'
})) }))
}, {
label: l('settings.hwAcceleration'), type: 'checkbox', checked: settings.hwAcceleration,
click: (item: Electron.MenuItem) => {
settings.hwAcceleration = item.checked;
setGeneralSettings(settings);
}
}, { }, {
label: l('settings.beta'), type: 'checkbox', checked: settings.beta, label: l('settings.beta'), type: 'checkbox', checked: settings.beta,
click: async(item: Electron.MenuItem) => { click: async(item: Electron.MenuItem) => {
@ -383,6 +394,7 @@ function onReady(): void {
} }
const isSquirrelStart = require('electron-squirrel-startup'); //tslint:disable-line:no-require-imports const isSquirrelStart = require('electron-squirrel-startup'); //tslint:disable-line:no-require-imports
if(isSquirrelStart || process.env.NODE_ENV === 'production' && app.makeSingleInstance(createWindow)) app.quit(); if(isSquirrelStart || process.env.NODE_ENV === 'production' && !app.requestSingleInstanceLock()) app.quit();
else app.on('ready', onReady); else app.on('ready', onReady);
app.on('second-instance', createWindow);
app.on('window-all-closed', () => app.quit()); app.on('window-all-closed', () => app.quit());

View File

@ -1,3 +1,4 @@
process.env.DEBUG = 'electron-windows-installer:main';
const path = require('path'); const path = require('path');
const pkg = require(path.join(__dirname, 'package.json')); const pkg = require(path.join(__dirname, 'package.json'));
const fs = require('fs'); const fs = require('fs');
@ -60,7 +61,7 @@ require('electron-packager')({
require('electron-winstaller').createWindowsInstaller({ require('electron-winstaller').createWindowsInstaller({
appDirectory: appPaths[0], appDirectory: appPaths[0],
outputDirectory: distDir, outputDirectory: distDir,
iconUrl: icon, iconUrl: 'file:///%localappdata%\\fchat\\app.ico',
setupIcon: icon, setupIcon: icon,
noMsi: true, noMsi: true,
exe: 'F-Chat.exe', exe: 'F-Chat.exe',

View File

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

View File

@ -1,5 +1,4 @@
const path = require('path'); const path = require('path');
const ExtractTextPlugin = require('extract-text-webpack-plugin');
const fs = require('fs'); const fs = require('fs');
const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin'); const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin');
const OptimizeCssAssetsPlugin = require('optimize-css-assets-webpack-plugin'); const OptimizeCssAssetsPlugin = require('optimize-css-assets-webpack-plugin');
@ -109,20 +108,15 @@ const mainConfig = {
module.exports = function(mode) { module.exports = function(mode) {
const themesDir = path.join(__dirname, '../scss/themes/chat'); const themesDir = path.join(__dirname, '../scss/themes/chat');
const themes = fs.readdirSync(themesDir); const themes = fs.readdirSync(themesDir);
const cssOptions = {use: ['css-loader', 'sass-loader']};
for(const theme of themes) { for(const theme of themes) {
if(!theme.endsWith('.scss')) continue; if(!theme.endsWith('.scss')) continue;
const absPath = path.join(themesDir, theme); const absPath = path.join(themesDir, theme);
rendererConfig.entry.chat.push(absPath); rendererConfig.entry.chat.push(absPath);
const plugin = new ExtractTextPlugin('themes/' + theme.slice(0, -5) + '.css'); rendererConfig.module.rules.unshift({test: absPath, loader: ['file-loader?name=themes/[name].css', 'extract-loader', 'css-loader', 'sass-loader']});
rendererConfig.plugins.push(plugin);
rendererConfig.module.rules.unshift({test: absPath, use: plugin.extract(cssOptions)});
} }
const faPath = path.join(themesDir, '../../fa.scss'); const faPath = path.join(themesDir, '../../fa.scss');
rendererConfig.entry.chat.push(faPath); rendererConfig.entry.chat.push(faPath);
const faPlugin = new ExtractTextPlugin('./fa.css'); rendererConfig.module.rules.unshift({test: faPath, loader: ['file-loader?name=fa.css', 'extract-loader', 'css-loader', 'sass-loader']});
rendererConfig.plugins.push(faPlugin);
rendererConfig.module.rules.unshift({test: faPath, use: faPlugin.extract(cssOptions)});
if(mode === 'production') { if(mode === 'production') {
process.env.NODE_ENV = 'production'; process.env.NODE_ENV = 'production';
mainConfig.devtool = rendererConfig.devtool = 'source-map'; mainConfig.devtool = rendererConfig.devtool = 'source-map';

View File

@ -2,6 +2,7 @@
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta http-equiv="Content-Security-Policy" content="default-src 'self' 'unsafe-inline'; img-src https://static.f-list.net">
<title>F-Chat</title> <title>F-Chat</title>
<link href="fa.css" rel="stylesheet"> <link href="fa.css" rel="stylesheet">
</head> </head>

View File

@ -1,7 +1,7 @@
<template> <template>
<div id="page" style="position: relative; padding: 10px;" v-if="settings"> <div id="page" style="position: relative; padding: 10px;" v-if="settings">
<div v-html="styling"></div> <div v-html="styling"></div>
<div v-if="!characters" style="display:flex; align-items:center; justify-content:center; height: 100%;"> <div v-if="!characters" style="display:flex; align-items:center; justify-content:center; min-height: 100%;">
<div class="card bg-light" style="width: 400px;"> <div class="card bg-light" style="width: 400px;">
<h3 class="card-header" style="margin-top:0">{{l('title')}}</h3> <h3 class="card-header" style="margin-top:0">{{l('title')}}</h3>
<div class="card-body"> <div class="card-body">

View File

@ -8,8 +8,8 @@ android {
applicationId "net.f_list.fchat" applicationId "net.f_list.fchat"
minSdkVersion 19 minSdkVersion 19
targetSdkVersion 27 targetSdkVersion 27
versionCode 19 versionCode 20
versionName "3.0.8" versionName "3.0.9"
} }
buildTypes { buildTypes {
release { release {

View File

@ -11,19 +11,27 @@ import android.net.Uri
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.os.Environment import android.os.Environment
import android.os.Handler
import android.view.KeyEvent
import android.view.ViewGroup import android.view.ViewGroup
import android.webkit.JsResult import android.webkit.JsResult
import android.webkit.WebChromeClient import android.webkit.WebChromeClient
import android.webkit.WebView import android.webkit.WebView
import android.webkit.WebViewClient import android.webkit.WebViewClient
import android.widget.EditText
import java.io.FileInputStream
import java.io.FileOutputStream import java.io.FileOutputStream
import java.net.URLDecoder import java.net.URLDecoder
import java.util.zip.ZipEntry
import java.util.zip.ZipOutputStream
class MainActivity : Activity() { class MainActivity : Activity() {
private lateinit var webView: WebView private lateinit var webView: WebView
private val profileRegex = Regex("^https?://(www\\.)?f-list.net/c/([^/#]+)/?#?") private val profileRegex = Regex("^https?://(www\\.)?f-list.net/c/([^/#]+)/?#?")
private val backgroundPlugin = Background(this) private val backgroundPlugin = Background(this)
private var debugPressed = 0
private val debugHandler = Handler()
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@ -91,6 +99,50 @@ class MainActivity : Activity() {
} }
private fun addFolder(folder: java.io.File, out: ZipOutputStream, path: String) {
for(file in folder.listFiles()) {
if(file.isDirectory) addFolder(file, out, "$path${file.name}/")
else {
out.putNextEntry(ZipEntry("$path${file.name}"))
FileInputStream(file).use { it.copyTo(out) }
}
}
}
val debug = Runnable {
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
val permission = checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE)
if(permission != PackageManager.PERMISSION_GRANTED) {
return@Runnable requestPermissions(arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE), 1)
}
}
val view = EditText(this)
view.hint = "Enter character name"
AlertDialog.Builder(this).setView(view).setPositiveButton("OK", { _, _ ->
val file = java.io.File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS), "test.zip")
val dest = FileOutputStream(file)
val out = ZipOutputStream(dest)
addFolder(java.io.File(filesDir, view.text.toString()), out, "")
out.close()
val downloadManager = getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
downloadManager.addCompletedDownload(file.name, file.name, false, "text/plain", file.absolutePath, file.length(), true)
}).setNegativeButton("Cancel", { dialog, _ -> dialog.dismiss() }).setTitle("DEBUG").show()
}
override fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean {
if(keyCode == KeyEvent.KEYCODE_VOLUME_DOWN) debugPressed = debugPressed or 1
else if(keyCode == KeyEvent.KEYCODE_VOLUME_UP) debugPressed = debugPressed or 2
if(debugPressed == 3) debugHandler.postDelayed(debug, 5000)
return super.onKeyDown(keyCode, event)
}
override fun onKeyUp(keyCode: Int, event: KeyEvent?): Boolean {
if(keyCode == KeyEvent.KEYCODE_VOLUME_DOWN) debugPressed = debugPressed xor 1
else if(keyCode == KeyEvent.KEYCODE_VOLUME_UP) debugPressed = debugPressed xor 2
debugHandler.removeCallbacks(debug)
return super.onKeyUp(keyCode, event)
}
override fun onBackPressed() { override fun onBackPressed() {
webView.evaluateJavascript("var e=new Event('backbutton',{cancelable:true});document.dispatchEvent(e);e.defaultPrevented", { webView.evaluateJavascript("var e=new Event('backbutton',{cancelable:true});document.dispatchEvent(e);e.defaultPrevented", {
if(it != "true") super.onBackPressed() if(it != "true") super.onBackPressed()

View File

@ -53,9 +53,13 @@ class Notifications(private val ctx: Context) {
.setContentIntent(PendingIntent.getActivity(ctx, 1, intent, PendingIntent.FLAG_UPDATE_CURRENT)).setDefaults(Notification.DEFAULT_VIBRATE or Notification.DEFAULT_LIGHTS) .setContentIntent(PendingIntent.getActivity(ctx, 1, intent, PendingIntent.FLAG_UPDATE_CURRENT)).setDefaults(Notification.DEFAULT_VIBRATE or Notification.DEFAULT_LIGHTS)
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) notification.setChannelId("messages") if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) notification.setChannelId("messages")
object : AsyncTask<String, Void, Bitmap>() { object : AsyncTask<String, Void, Bitmap>() {
override fun doInBackground(vararg args: String): Bitmap { override fun doInBackground(vararg args: String): Bitmap? {
val connection = URL(args[0]).openConnection() return try {
return BitmapFactory.decodeStream(connection.getInputStream()) val connection = URL(args[0]).openConnection()
BitmapFactory.decodeStream(connection.getInputStream())
} catch(e: Exception) {
null
}
} }
override fun onPostExecute(result: Bitmap?) { override fun onPostExecute(result: Bitmap?) {

View File

@ -21,12 +21,15 @@ class ViewController: UIViewController, WKNavigationDelegate, WKUIDelegate {
config.userContentController = controller config.userContentController = controller
config.mediaTypesRequiringUserActionForPlayback = [.video] config.mediaTypesRequiringUserActionForPlayback = [.video]
config.setValue(true, forKey: "_alwaysRunsAtForegroundPriority") config.setValue(true, forKey: "_alwaysRunsAtForegroundPriority")
webView = WKWebView(frame: .zero, configuration: config) webView = WKWebView(frame: UIApplication.shared.windows[0].frame, configuration: config)
webView.uiDelegate = self webView.uiDelegate = self
webView.navigationDelegate = self webView.navigationDelegate = self
view = webView view = webView
NotificationCenter.default.addObserver(self, selector: #selector(ViewController.keyboardWillShow), name: NSNotification.Name.UIKeyboardWillShow, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(ViewController.keyboardWillShow), name: NSNotification.Name.UIKeyboardWillShow, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(ViewController.keyboardDidShow), name: NSNotification.Name.UIKeyboardDidShow, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(ViewController.keyboardWillHide), name: NSNotification.Name.UIKeyboardWillHide, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(ViewController.keyboardWillHide), name: NSNotification.Name.UIKeyboardWillHide, object: nil)
webView.scrollView.contentInsetAdjustmentBehavior = .never
webView.scrollView.bounces = false
UIApplication.shared.statusBarStyle = .lightContent UIApplication.shared.statusBarStyle = .lightContent
(UIApplication.shared.value(forKey: "statusBar") as! UIView).backgroundColor = UIColor(white: 0, alpha: 0.5) (UIApplication.shared.value(forKey: "statusBar") as! UIView).backgroundColor = UIColor(white: 0, alpha: 0.5)
} }
@ -36,30 +39,25 @@ class ViewController: UIViewController, WKNavigationDelegate, WKUIDelegate {
let htmlPath = Bundle.main.path(forResource: "www/index", ofType: "html") let htmlPath = Bundle.main.path(forResource: "www/index", ofType: "html")
let url = URL(fileURLWithPath: htmlPath!, isDirectory: false) let url = URL(fileURLWithPath: htmlPath!, isDirectory: false)
webView.loadFileURL(url, allowingReadAccessTo: url) webView.loadFileURL(url, allowingReadAccessTo: url)
webView.scrollView.isScrollEnabled = false
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
} }
@objc func keyboardWillShow(notification: NSNotification) { @objc func keyboardWillShow(notification: NSNotification) {
let info = notification.userInfo! let info = notification.userInfo!
let frame = webView.frame
let newHeight = view.window!.frame.height - (info[UIKeyboardFrameEndUserInfoKey] as! NSValue).cgRectValue.height let newHeight = view.window!.frame.height - (info[UIKeyboardFrameEndUserInfoKey] as! NSValue).cgRectValue.height
UIView.animate(withDuration: (info[UIKeyboardAnimationDurationUserInfoKey] as! NSNumber).doubleValue, animations: { UIView.animate(withDuration: (info[UIKeyboardAnimationDurationUserInfoKey] as! NSNumber).doubleValue, animations: {
self.webView.scrollView.bounds = CGRect(x: 0, y: 0, width: frame.width, height: newHeight) self.webView.frame = CGRect(x: 0, y: 0, width: self.webView.frame.width, height: newHeight)
}, completion: { (_: Bool) in self.webView.evaluateJavaScript("window.dispatchEvent(new Event('resize'))", completionHandler: nil) }) })
}
@objc func keyboardDidShow(notification: NSNotification) {
webView.scrollView.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: webView.scrollView.contentInset.bottom - webView.scrollView.adjustedContentInset.bottom, right: 0)
} }
@objc func keyboardWillHide(notification: NSNotification) { @objc func keyboardWillHide(notification: NSNotification) {
let info = notification.userInfo! let info = notification.userInfo!
let frame = webView.scrollView.bounds
let newHeight = frame.height + (info[UIKeyboardFrameEndUserInfoKey] as! NSValue).cgRectValue.height
UIView.animate(withDuration: (info[UIKeyboardAnimationDurationUserInfoKey] as! NSNumber).doubleValue, animations: { UIView.animate(withDuration: (info[UIKeyboardAnimationDurationUserInfoKey] as! NSNumber).doubleValue, animations: {
self.webView.scrollView.bounds = CGRect(x: 0, y: 0, width: frame.width, height: newHeight) self.webView.frame = UIApplication.shared.windows[0].frame
}, completion: { (_: Bool) in self.webView.evaluateJavaScript("window.dispatchEvent(new Event('resize'))", completionHandler: nil) }) })
} }
func webView(_ webView: WKWebView, runJavaScriptAlertPanelWithMessage message: String, initiatedByFrame frame: WKFrameInfo, func webView(_ webView: WKWebView, runJavaScriptAlertPanelWithMessage message: String, initiatedByFrame frame: WKFrameInfo,
@ -101,4 +99,3 @@ class ViewController: UIViewController, WKNavigationDelegate, WKUIDelegate {
UIApplication.shared.open(navigationAction.request.url!) UIApplication.shared.open(navigationAction.request.url!)
} }
} }

View File

@ -1,6 +1,6 @@
{ {
"name": "net.f_list.fchat", "name": "net.f_list.fchat",
"version": "3.0.8", "version": "3.0.9",
"displayName": "F-Chat", "displayName": "F-Chat",
"author": "The F-List Team", "author": "The F-List Team",
"description": "F-List.net Chat Client", "description": "F-List.net Chat Client",

View File

@ -7,46 +7,46 @@
"devDependencies": { "devDependencies": {
"@fortawesome/fontawesome-free-webfonts": "^1.0.6", "@fortawesome/fontawesome-free-webfonts": "^1.0.6",
"@types/lodash": "^4.14.116", "@types/lodash": "^4.14.116",
"@types/node": "^10.5.6", "@types/node": "^10.11.2",
"@types/sortablejs": "^1.3.31", "@types/sortablejs": "^1.3.31",
"axios": "^0.18.0", "axios": "^0.18.0",
"bootstrap": "^4.1.3", "bootstrap": "^4.1.3",
"css-loader": "^1.0.0", "css-loader": "^1.0.0",
"date-fns": "^1.28.5", "date-fns": "^1.28.5",
"electron": "2.0.2", "electron": "^3.0.2",
"electron-log": "^2.2.16", "electron-log": "^2.2.17",
"electron-packager": "^12.1.0", "electron-packager": "^12.1.2",
"electron-rebuild": "^1.8.2", "electron-rebuild": "^1.8.2",
"extract-text-webpack-plugin": "4.0.0-beta.0", "extract-loader": "^3.0.0",
"file-loader": "^1.1.10", "file-loader": "^2.0.0",
"fork-ts-checker-webpack-plugin": "^0.4.4", "fork-ts-checker-webpack-plugin": "^0.4.9",
"lodash": "^4.16.4", "lodash": "^4.17.11",
"node-sass": "^4.8.3", "node-sass": "^4.9.3",
"optimize-css-assets-webpack-plugin": "^5.0.0", "optimize-css-assets-webpack-plugin": "^5.0.1",
"qs": "^6.5.1", "qs": "^6.5.1",
"raven-js": "^3.26.4", "raven-js": "^3.27.0",
"sass-loader": "^7.1.0", "sass-loader": "^7.1.0",
"sortablejs": "^1.6.0", "sortablejs": "^1.6.0",
"style-loader": "^0.21.0", "style-loader": "^0.23.0",
"ts-loader": "^4.2.0", "ts-loader": "^5.2.1",
"tslib": "^1.7.1", "tslib": "^1.7.1",
"tslint": "^5.7.0", "tslint": "^5.7.0",
"typescript": "^3.0.1", "typescript": "^3.1.1",
"vue": "^2.5.17", "vue": "^2.5.17",
"vue-class-component": "^6.0.0", "vue-class-component": "^6.0.0",
"vue-loader": "^15.2.6", "vue-loader": "^15.4.2",
"vue-property-decorator": "^7.0.0", "vue-property-decorator": "^7.1.1",
"vue-template-compiler": "^2.5.17", "vue-template-compiler": "^2.5.17",
"webpack": "^4.16.4" "webpack": "^4.20.2"
}, },
"dependencies": { "dependencies": {
"keytar": "^4.2.1", "keytar": "^4.2.1",
"spellchecker": "^3.4.3" "spellchecker": "^3.5.0"
}, },
"optionalDependencies": { "optionalDependencies": {
"appdmg": "^0.5.2", "appdmg": "^0.5.2",
"electron-squirrel-startup": "^1.0.0", "electron-squirrel-startup": "^1.0.0",
"electron-winstaller": "^2.6.4" "electron-winstaller": "^2.7.0"
}, },
"scripts": { "scripts": {
"postinstall": "electron-rebuild -o spellchecker,keytar" "postinstall": "electron-rebuild -o spellchecker,keytar"

View File

@ -17,7 +17,7 @@ All necessary files to build F-Chat 3.0 as an Electron, mobile or web applicatio
### Packaging ### Packaging
See https://electron.atom.io/docs/tutorial/application-distribution/ See https://electron.atom.io/docs/tutorial/application-distribution/
- Run `yarn build:dist` to create a minified production build. - Run `yarn build:dist` to create a minified production build.
- Run `yarn pack`. The generated installer is placed into the `dist` directory. - Run `yarn run pack`. The generated installer is placed into the `dist` directory.
- On Windows you can add the path to and password for a code signing certificate as arguments. - On Windows you can add the path to and password for a code signing certificate as arguments.
- On Mac you can add your code signing identity as an argument. `zip` is required to be installed. - On Mac you can add your code signing identity as an argument. `zip` is required to be installed.
- On Linux you can add a GPG key for signing and its password as arguments. `mksquashfs` and `zsyncmake` are required to be installed. - On Linux you can add a GPG key for signing and its password as arguments. `mksquashfs` and `zsyncmake` are required to be installed.

View File

@ -201,6 +201,35 @@
color: $text-dark; color: $text-dark;
} }
.message-ad {
&:not(.expanded) {
max-height: 100px;
overflow: hidden;
position: relative;
> .expand {
display: flex;
align-items: flex-end;
justify-content: center;
padding-bottom: 5px;
position: absolute;
top: 70px;
width: 100%;
left: 0;
height: 30px;
cursor: pointer;
background: linear-gradient(rgba($white, 0), $white);
&:hover {
background: linear-gradient(rgba($white, 0) 50%, $white);
}
}
}
> .expand {
display: none;
}
}
.message-highlight { .message-highlight {
background-color: theme-color-level("success", -8); background-color: theme-color-level("success", -8);
} }
@ -254,12 +283,17 @@ $genders: (
max-width: 98%; max-width: 98%;
} }
#window-tabs .hasNew { #window-tabs {
background-color: theme-color-level("warning", -2); .hasNew {
border-color: theme-color-level("warning", -4); background-color: theme-color-level("warning", -2);
color: color-yiq(theme-color("warning")); border-color: theme-color-level("warning", -4);
&:hover { color: color-yiq(theme-color("warning"));
background-color: theme-color-level("warning", -4); &:hover {
background-color: theme-color-level("warning", -4);
}
}
.tab:not(.active):not(:hover) {
opacity: 0.5;
} }
} }

View File

@ -10,5 +10,5 @@ $gray-800: #333333 !default;
$gray-900: #191919 !default; $gray-900: #191919 !default;
$secondary: $gray-400; $secondary: $gray-400;
$body-bg: #eeeeee; $body-bg: $gray-100;
$text-muted: $gray-500; $text-muted: $gray-500;

View File

@ -40,8 +40,7 @@ import '../scss/fa.scss'; //tslint:disable-line:no-import-side-effect
import {Logs, SettingsStore} from './logs'; import {Logs, SettingsStore} from './logs';
import Notifications from './notifications'; import Notifications from './notifications';
//@ts-ignore if(typeof (<{Promise?: object}>window).Promise !== 'function') //tslint:disable-line:strict-type-predicates
if(typeof window.Promise !== 'function' || typeof window.Notification !== 'function') //tslint:disable-line:strict-type-predicates
alert('Your browser is too old to be supported by F-Chat 3.0. Please update to a newer version.'); alert('Your browser is too old to be supported by F-Chat 3.0. Please update to a newer version.');
const version = (<{version: string}>require('./package.json')).version; //tslint:disable-line:no-require-imports const version = (<{version: string}>require('./package.json')).version; //tslint:disable-line:no-require-imports

View File

@ -45,7 +45,7 @@ type Index = {[key: string]: StoredConversation | undefined};
async function openDatabase(character: string): Promise<IDBDatabase> { async function openDatabase(character: string): Promise<IDBDatabase> {
const request = window.indexedDB.open(`logs-${character}`); const request = window.indexedDB.open(`logs-${character}`);
request.onupgradeneeded = () => { request.onupgradeneeded = () => {
const db = <IDBDatabase>request.result; const db = request.result;
const logsStore = db.createObjectStore('logs', {keyPath: 'id', autoIncrement: true}); const logsStore = db.createObjectStore('logs', {keyPath: 'id', autoIncrement: true});
logsStore.createIndex('conversation', 'conversation'); logsStore.createIndex('conversation', 'conversation');
logsStore.createIndex('conversation-day', hasComposite ? ['conversation', 'day'] : 'day'); logsStore.createIndex('conversation-day', hasComposite ? ['conversation', 'day'] : 'day');

View File

@ -1,6 +1,6 @@
{ {
"name": "net.f_list.fchat", "name": "net.f_list.fchat",
"version": "3.0.8", "version": "3.0.9",
"displayName": "F-Chat", "displayName": "F-Chat",
"author": "The F-List Team", "author": "The F-List Team",
"description": "F-List.net Chat Client", "description": "F-List.net Chat Client",

2474
yarn.lock

File diff suppressed because it is too large Load Diff