diff --git a/bbcode/Editor.vue b/bbcode/Editor.vue index eaafa46..41e6083 100644 --- a/bbcode/Editor.vue +++ b/bbcode/Editor.vue @@ -216,12 +216,14 @@ if(this.undoIndex === 0 && this.undoStack[0] !== this.text) this.undoStack.unshift(this.text); if(this.undoStack.length > this.undoIndex + 1) { this.text = this.undoStack[++this.undoIndex]; + this.$emit('input', this.text); this.lastInput = Date.now(); } } else if(key === Keys.KeyY) { e.preventDefault(); if(this.undoIndex > 0) { this.text = this.undoStack[--this.undoIndex]; + this.$emit('input', this.text); this.lastInput = Date.now(); } } diff --git a/chat/ConversationView.vue b/chat/ConversationView.vue index 9105aa7..15c589e 100644 --- a/chat/ConversationView.vue +++ b/chat/ConversationView.vue @@ -174,6 +174,7 @@ keypressHandler!: EventListener; scrolledDown = true; scrolledUp = false; + ignoreScroll = false; adCountdown = 0; adsMode = l('channel.mode.ads'); @@ -184,11 +185,7 @@ icon: 'fa-question', handler: () => (this.$refs['helpDialog']).show() }]; - window.addEventListener('resize', this.resizeHandler = () => { - if(this.scrolledDown) - this.messageView.scrollTop = this.messageView.scrollHeight - this.messageView.offsetHeight; - this.onMessagesScroll(); - }); + window.addEventListener('resize', this.resizeHandler = () => this.keepScroll()); window.addEventListener('keypress', this.keypressHandler = () => { if(document.getSelection().isCollapsed && !anyDialogsShown && (document.activeElement === document.body || document.activeElement.tagName === 'A')) @@ -244,14 +241,14 @@ return this.conversation.messages.filter((x) => filter.test(x.text)); } - sendButton(): void { - setImmediate(async() => this.conversation.send()); + async sendButton(): Promise { + return this.conversation.send(); } @Watch('conversation') conversationChanged(): void { if(!anyDialogsShown) (this.$refs['textBox']).focus(); - setTimeout(() => this.messageView.scrollTop = this.messageView.scrollHeight - this.messageView.offsetHeight); + this.$nextTick(() => setTimeout(() => this.messageView.scrollTop = this.messageView.scrollHeight)); this.scrolledDown = true; } @@ -265,10 +262,17 @@ keepScroll(): void { if(this.scrolledDown) - this.$nextTick(() => setTimeout(() => this.messageView.scrollTop = this.messageView.scrollHeight, 0)); + this.$nextTick(() => setTimeout(() => { + this.ignoreScroll = true; + this.messageView.scrollTop = this.messageView.scrollHeight; + }, 0)); } onMessagesScroll(): void { + if(this.ignoreScroll) { + this.ignoreScroll = false; + return; + } if(this.messageView.scrollTop < 20) { if(!this.scrolledUp) { const firstMessage = this.messageView.firstElementChild; @@ -334,7 +338,7 @@ else if(getKey(e) === Keys.Enter) { if(e.shiftKey === this.settings.enterSend) return; e.preventDefault(); - setImmediate(async() => this.conversation.send()); + await this.conversation.send(); } } } diff --git a/chat/conversations.ts b/chat/conversations.ts index 7cdbadd..912127f 100644 --- a/chat/conversations.ts +++ b/chat/conversations.ts @@ -59,6 +59,10 @@ abstract class Conversation implements Interfaces.Conversation { state.savePinned(); //tslint:disable-line:no-floating-promises } + clearText(): void { + setImmediate(() => this.enteredText = ''); + } + async send(): Promise { if(this.enteredText.length === 0) return; if(isCommand(this.enteredText)) { @@ -67,7 +71,7 @@ abstract class Conversation implements Interfaces.Conversation { else { parsed.call(this); this.lastSent = this.enteredText; - this.enteredText = ''; + this.clearText(); } } else { this.lastSent = this.enteredText; @@ -186,7 +190,7 @@ class PrivateConversation extends Conversation implements Interfaces.PrivateConv const message = createMessage(MessageType.Message, core.characters.ownCharacter, this.enteredText); this.safeAddMessage(message); if(core.state.settings.logMessages) await core.logs.logMessage(this, message); - this.enteredText = ''; + this.clearText(); } private setOwnTyping(status: Interfaces.TypingStatus): void { @@ -257,8 +261,8 @@ class ChannelConversation extends Conversation implements Interfaces.ChannelConv } addModeMessage(mode: Channel.Mode, message: Interfaces.Message): void { - if(this._mode === mode) this.safeAddMessage(message); - else safeAddMessage(this[mode], message, 500); + safeAddMessage(this[mode], message, 500); + if(this._mode === mode) safeAddMessage(this.messages, message, this.maxMessages); } async addMessage(message: Interfaces.Message): Promise { @@ -283,6 +287,8 @@ class ChannelConversation extends Conversation implements Interfaces.ChannelConv } else this.addModeMessage('ads', message); } this.addModeMessage('both', message); + if(message.type !== Interfaces.Message.Type.Event) + safeAddMessage(this.reportMessages, message, 500); } clear(): void { @@ -310,7 +316,7 @@ class ChannelConversation extends Conversation implements Interfaces.ChannelConv createMessage(isAd ? MessageType.Ad : MessageType.Message, core.characters.ownCharacter, this.enteredText, new Date())); if(isAd) this.nextAd = Date.now() + core.connection.vars.lfrp_flood * 1000; - else this.enteredText = ''; + else this.clearText(); } } diff --git a/chat/slash_commands.ts b/chat/slash_commands.ts index 8d72182..ef9dc2d 100644 --- a/chat/slash_commands.ts +++ b/chat/slash_commands.ts @@ -355,10 +355,36 @@ const commands: {readonly [key: string]: Command | undefined} = { params: [{type: ParamType.String, delimiter: ' '}, {type: ParamType.String}] }, elf: { - exec: (conv: Conversation) => conv.infoText = - 'Now no one can say there\'s "not enough Elf." It\'s a well-kept secret, but elves love headpets. You should try it sometime.', + exec: (conv: Conversation) => conv.infoText = elf[Math.floor(Math.random() * elf.length)], documented: false } }; +const elf = [ //Ceran is to be thanked for most of these. + `Now no one can say there's "not enough Elf." It's a well-kept secret, but elves love headpets. You should try it sometime.`, + `Your task for the day: provide a soft cushion in a sunny area for maximum elf comfort, earn +2 Bliss pts.`, + `Your task for the day: pet an elf at least (3) times, receive Good Karma.`, + `Your task for the day: make dinner for an elf, receive +3 Luck pts.`, + `The reason that elves' ears are so long is so that they can better hear your sins. Now that's an Elf Fact!`, + `A "straight" elf is basically an oxymoron.`, + `Don't forget to water your elf today!`, + `Please don't let your elf eat out of the trash can.`, + `Elves are not allowed to have soda after 12pm, but they will anyway.`, + `Pet an elf on the ears (4) times. Any more and the elf will bite. Now that's an Elf Fact!`, + `There are two kinds of people in the world. People who like elves. And people with questionable taste.`, + `Love yourself, pet an elf!!!`, + `Your task for the day: leave out snacks for your local elves, they'll help dispel vermin!`, + `An elf in your home will discourage the presence of predators! Or summon them. I forget which.`, + `If you crack open an elf, there's just a smaller elf within. It's just smaller and smaller elves all the way down.`, + `In case of an emergency, an elf's ass can be used as a flotation device. Now that's an Elf Fact!`, + `Running low on inventory space? An elf can be used to slot additional cylindrical artifacts! Now that's an Elf Fact!`, + `The average elf can consume half their body weight in cum. Now that's an Elf Fact!`, + `Your task for the day: subjugate yourself to your elven overlords in order to achieve righteous bliss.`, + `Your task for the day: Rub an elf's tummy. Be wary of them bear-trapping your arm as the forces of darkness consume their soul.`, + `Listen, that elfroot you found in my sock drawer is PURELY medicinal!`, + `What are elves? We just don't know.`, + `As long as you pet an elf's head, they will be content. What will happen if you stop? No one's ever come back to tell the tale.`, + `Elves are very helpful creatures. Just ask them to carry something for you, and they will. Especially eggs.` +]; + export default commands; \ No newline at end of file diff --git a/electron/Index.vue b/electron/Index.vue index ddad171..76dbd7f 100644 --- a/electron/Index.vue +++ b/electron/Index.vue @@ -267,7 +267,7 @@ height: 100%; } - *:not([draggable]), *::after, *::before { + a[href^="#"]:not([draggable]) { -webkit-user-drag: none; -webkit-app-region: no-drag; } diff --git a/electron/build/linux-libs/libXss.so.1 b/electron/build/linux-libs/libXss.so.1 new file mode 100644 index 0000000..752f223 Binary files /dev/null and b/electron/build/linux-libs/libXss.so.1 differ diff --git a/electron/build/linux-libs/libXtst.so.6 b/electron/build/linux-libs/libXtst.so.6 new file mode 100644 index 0000000..1f6fbf3 Binary files /dev/null and b/electron/build/linux-libs/libXtst.so.6 differ diff --git a/electron/build/linux-libs/libappindicator.so.1 b/electron/build/linux-libs/libappindicator.so.1 new file mode 100644 index 0000000..a8c5dca Binary files /dev/null and b/electron/build/linux-libs/libappindicator.so.1 differ diff --git a/electron/build/linux-libs/libgconf-2.so.4 b/electron/build/linux-libs/libgconf-2.so.4 new file mode 100644 index 0000000..948925c Binary files /dev/null and b/electron/build/linux-libs/libgconf-2.so.4 differ diff --git a/electron/build/linux-libs/libindicator.so.7 b/electron/build/linux-libs/libindicator.so.7 new file mode 100644 index 0000000..f36a7fb Binary files /dev/null and b/electron/build/linux-libs/libindicator.so.7 differ diff --git a/electron/build/linux-libs/libnotify.so.4 b/electron/build/linux-libs/libnotify.so.4 new file mode 100644 index 0000000..e77ed01 Binary files /dev/null and b/electron/build/linux-libs/libnotify.so.4 differ diff --git a/electron/chat.ts b/electron/chat.ts index 6e99769..71a6db3 100644 --- a/electron/chat.ts +++ b/electron/chat.ts @@ -175,11 +175,12 @@ let dictDir = path.join(electron.remote.app.getPath('userData'), 'spellchecker') if(process.platform === 'win32') exec(`for /d %I in ("${dictDir}") do @echo %~sI`, (_, stdout) => { dictDir = stdout.trim(); }); electron.webFrame.setSpellCheckProvider('', false, {spellCheck: (text) => !spellchecker.isMisspelled(text)}); -electron.ipcRenderer.on('settings', async(_: Event, s: GeneralSettings) => { +function onSettings(s: GeneralSettings): void { settings = s; spellchecker.setDictionary(s.spellcheckLang, dictDir); for(const word of s.customDictionary) spellchecker.add(word); -}); +} +electron.ipcRenderer.on('settings', (_: Event, s: GeneralSettings) => onSettings(s)); const params = <{[key: string]: string | undefined}>qs.parse(window.location.search.substr(1)); let settings = JSON.parse(params['settings']!); @@ -192,7 +193,7 @@ if(params['import'] !== undefined) } catch { alert(l('importer.error')); } - +onSettings(settings); //tslint:disable-next-line:no-unused-expression new Index({ el: '#app', diff --git a/electron/filesystem.ts b/electron/filesystem.ts index 556f553..42f0b58 100644 --- a/electron/filesystem.ts +++ b/electron/filesystem.ts @@ -163,7 +163,7 @@ function loadIndex(name: string): Index { const dir = getLogDir(name); const files = fs.readdirSync(dir); for(const file of files) - if(file.substr(-4) === '.idx') { + if(file.substr(-4) === '.idx') try { const content = fs.readFileSync(path.join(dir, file)); let offset = content.readUInt8(0, noAssert) + 1; @@ -178,10 +178,10 @@ function loadIndex(name: string): Index { item.offsets.push(content.readUIntLE(offset + 2, 5, noAssert)); } index[file.slice(0, -4).toLowerCase()] = item; - } catch { + } catch(e) { + console.error(e); alert(l('logs.corruption.desktop')); } - } return index; } @@ -215,7 +215,8 @@ export class Logs implements Logging { } if(count !== 0) messages = messages.slice(count); return messages; - } catch { + } catch(e) { + console.error(e); alert(l('logs.corruption.desktop')); return []; } finally { @@ -253,7 +254,6 @@ export class Logs implements Logging { const length = end - pos; const buffer = Buffer.allocUnsafe(length); await read(fd, buffer, 0, length, pos); - fs.closeSync(fd); let offset = 0; while(offset < length) { const deserialized = deserializeMessage(buffer, offset); @@ -261,7 +261,8 @@ export class Logs implements Logging { offset += deserialized.size; } return messages; - } catch { + } catch(e) { + console.error(e); alert(l('logs.corruption.desktop')); return []; } finally { diff --git a/electron/pack.js b/electron/pack.js index cf00e22..9c15838 100644 --- a/electron/pack.js +++ b/electron/pack.js @@ -29,12 +29,13 @@ function mkdir(dir) { const distDir = path.join(__dirname, 'dist'); const isBeta = pkg.version.indexOf('beta') !== -1; -const spellcheckerPath = 'node_modules/spellchecker/build/Release/spellchecker.node', - keytarPath = 'node_modules/keytar/build/Release/keytar.node'; -mkdir(path.dirname(path.join(__dirname, 'app', spellcheckerPath))); -mkdir(path.dirname(path.join(__dirname, 'app', keytarPath))); -fs.copyFileSync(spellcheckerPath, path.join(__dirname, 'app', spellcheckerPath)); -fs.copyFileSync(keytarPath, path.join(__dirname, 'app', keytarPath)); +const spellcheckerPath = 'spellchecker/build/Release/spellchecker.node', keytarPath = 'keytar/build/Release/keytar.node'; +const modules = path.join(__dirname, 'app', 'node_modules'); +mkdir(path.dirname(path.join(modules, spellcheckerPath))); +mkdir(path.dirname(path.join(modules, keytarPath))); +fs.copyFileSync(require.resolve(spellcheckerPath), path.join(modules, spellcheckerPath)); +fs.copyFileSync(require.resolve(keytarPath), path.join(modules, keytarPath)); + require('electron-packager')({ dir: path.join(__dirname, 'app'), @@ -101,16 +102,22 @@ require('electron-packager')({ console.log('Creating Linux AppImage'); fs.renameSync(path.join(appPaths[0], 'F-Chat'), path.join(appPaths[0], 'AppRun')); fs.copyFileSync(path.join(__dirname, 'build', 'icon.png'), path.join(appPaths[0], 'icon.png')); + const libDir = path.join(appPaths[0], 'usr', 'lib'), libSource = path.join(__dirname, 'build', 'linux-libs'); + mkdir(libDir); + for(const file of fs.readdirSync(libSource)) + fs.copyFileSync(path.join(libSource, file), path.join(libDir, file)); fs.symlinkSync(path.join(appPaths[0], 'icon.png'), path.join(appPaths[0], '.DirIcon')); fs.writeFileSync(path.join(appPaths[0], 'fchat.desktop'), '[Desktop Entry]\nName=F-Chat\nExec=AppRun\nIcon=icon\nType=Application\nCategories=GTK;GNOME;Utility;'); require('axios').get('https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-x86_64.AppImage', {responseType: 'stream'}).then((res) => { const downloaded = path.join(distDir, 'appimagetool.AppImage'); - res.data.pipe(fs.createWriteStream(downloaded)); - res.data.on('end', () => { - const args = [appPaths[0], 'fchat.AppImage', '-u', 'zsync|https://client.f-list.net/fchat.AppImage.zsync']; + const stream = fs.createWriteStream(downloaded); + res.data.pipe(stream); + stream.on('close', () => { + const args = [appPaths[0], 'fchat.AppImage', '-u', 'zsync|httpos://client.f-list.net/fchat.AppImage.zsync']; if(process.argv.length > 2) args.push('-s', '--sign-key', process.argv[2]); else console.warn('Warning: Creating unsigned AppImage'); if(process.argv.length > 3) args.push('--sign-args', `--passphrase=${process.argv[3]}`); + fs.chmodSync(downloaded, 0o755); child_process.spawn(downloaded, ['--appimage-extract'], {cwd: distDir}).on('close', () => { const child = child_process.spawn(path.join(distDir, 'squashfs-root', 'AppRun'), args, {cwd: distDir}); child.stdout.on('data', (data) => console.log(data.toString())); diff --git a/electron/package.json b/electron/package.json index bf7a2d7..2f58d73 100644 --- a/electron/package.json +++ b/electron/package.json @@ -1,6 +1,6 @@ { "name": "fchat", - "version": "3.0.7", + "version": "3.0.8", "author": "The F-List Team", "description": "F-List.net Chat Client", "main": "main.js", diff --git a/mobile/android/app/build.gradle b/mobile/android/app/build.gradle index 4c70fa5..8147a60 100644 --- a/mobile/android/app/build.gradle +++ b/mobile/android/app/build.gradle @@ -8,8 +8,8 @@ android { applicationId "net.f_list.fchat" minSdkVersion 19 targetSdkVersion 27 - versionCode 18 - versionName "3.0.7" + versionCode 19 + versionName "3.0.8" } buildTypes { release { diff --git a/mobile/android/app/src/main/kotlin/net/f_list/fchat/MainActivity.kt b/mobile/android/app/src/main/kotlin/net/f_list/fchat/MainActivity.kt index 75e1d58..ad1c796 100644 --- a/mobile/android/app/src/main/kotlin/net/f_list/fchat/MainActivity.kt +++ b/mobile/android/app/src/main/kotlin/net/f_list/fchat/MainActivity.kt @@ -16,10 +16,8 @@ import android.webkit.JsResult import android.webkit.WebChromeClient import android.webkit.WebView import android.webkit.WebViewClient -import org.json.JSONTokener import java.io.FileOutputStream import java.net.URLDecoder -import java.util.* class MainActivity : Activity() { @@ -55,7 +53,7 @@ class MainActivity : Activity() { } webView.webChromeClient = object : WebChromeClient() { override fun onJsAlert(view: WebView, url: String, message: String, result: JsResult): Boolean { - AlertDialog.Builder(this@MainActivity).setTitle(R.string.app_name).setMessage(message).setPositiveButton(R.string.ok, { _, _ -> result.confirm() }).show() + AlertDialog.Builder(this@MainActivity).setTitle(R.string.app_name).setMessage(message).setPositiveButton(R.string.ok, { _, _ -> }).setOnDismissListener({ result.confirm() }).show() return true } diff --git a/mobile/android/app/src/main/kotlin/net/f_list/fchat/Notifications.kt b/mobile/android/app/src/main/kotlin/net/f_list/fchat/Notifications.kt index 20f5549..e993c47 100644 --- a/mobile/android/app/src/main/kotlin/net/f_list/fchat/Notifications.kt +++ b/mobile/android/app/src/main/kotlin/net/f_list/fchat/Notifications.kt @@ -8,11 +8,13 @@ import android.content.Context import android.content.Intent import android.graphics.Bitmap import android.graphics.BitmapFactory +import android.media.AudioAttributes import android.media.AudioManager import android.media.MediaPlayer import android.os.AsyncTask import android.os.Build import android.os.Vibrator +import android.provider.Settings import android.webkit.JavascriptInterface import java.net.URL @@ -20,19 +22,12 @@ class Notifications(private val ctx: Context) { init { if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { val manager = ctx.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager; - manager.createNotificationChannel(NotificationChannel("messages", ctx.getString(R.string.channel_messages), NotificationManager.IMPORTANCE_LOW)) + manager.createNotificationChannel(NotificationChannel("messages", ctx.getString(R.string.channel_messages), NotificationManager.IMPORTANCE_DEFAULT)) } } @JavascriptInterface fun notify(notify: Boolean, title: String, text: String, icon: String, sound: String?, data: String?): Int { - if(!notify) { - val vibrator = (ctx.getSystemService(Context.VIBRATOR_SERVICE) as Vibrator) - if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) - vibrator.vibrate(400, Notification.AUDIO_ATTRIBUTES_DEFAULT) - else vibrator.vibrate(400) - return 0 - } if(sound != null) { val player = MediaPlayer() val asset = ctx.assets.openFd("www/sounds/$sound.mp3") @@ -42,6 +37,15 @@ class Notifications(private val ctx: Context) { player.start() player.setOnCompletionListener { it.release() } } + if(!notify) { + if((ctx.getSystemService(Context.AUDIO_SERVICE) as AudioManager).ringerMode != AudioManager.RINGER_MODE_SILENT) { + val vibrator = (ctx.getSystemService(Context.VIBRATOR_SERVICE) as Vibrator) + if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) + vibrator.vibrate(400, Notification.AUDIO_ATTRIBUTES_DEFAULT) + else vibrator.vibrate(400) + } + return 0 + } val intent = Intent(ctx, MainActivity::class.java) intent.action = "notification" intent.putExtra("data", data) diff --git a/mobile/package.json b/mobile/package.json index f32b7e1..c8a51ab 100644 --- a/mobile/package.json +++ b/mobile/package.json @@ -1,6 +1,6 @@ { "name": "net.f_list.fchat", - "version": "3.0.7", + "version": "3.0.8", "displayName": "F-Chat", "author": "The F-List Team", "description": "F-List.net Chat Client", diff --git a/readme.md b/readme.md index 88e6ecf..e598424 100644 --- a/readme.md +++ b/readme.md @@ -20,7 +20,7 @@ See https://electron.atom.io/docs/tutorial/application-distribution/ - Run `yarn 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 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 as an argument. `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. ## Building for Mobile - Change into the `mobile` directory. diff --git a/site/character_page/character_page.vue b/site/character_page/character_page.vue index 41ebf68..02295c8 100644 --- a/site/character_page/character_page.vue +++ b/site/character_page/character_page.vue @@ -2,10 +2,10 @@
Loading character information.
{{error}}
-
+
-
+
@@ -146,6 +146,8 @@ } private async _getCharacter(): Promise { + this.error = ''; + this.character = null; if(this.name === undefined || this.name.length === 0) return; try { @@ -154,12 +156,11 @@ this.character = await methods.characterData(this.name, this.characterid); standardParser.allowInlines = true; standardParser.inlines = this.character.character.inlines; - this.loading = false; } catch(e) { - if(Utils.isJSONError(e)) - this.error = e.response.data.error; + this.error = Utils.isJSONError(e) ? e.response.data.error : (e).message; Utils.ajaxError(e, 'Failed to load character information.'); } + this.loading = false; } } \ No newline at end of file diff --git a/webchat/package.json b/webchat/package.json index f32b7e1..c8a51ab 100644 --- a/webchat/package.json +++ b/webchat/package.json @@ -1,6 +1,6 @@ { "name": "net.f_list.fchat", - "version": "3.0.7", + "version": "3.0.8", "displayName": "F-Chat", "author": "The F-List Team", "description": "F-List.net Chat Client",