Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
277471b
chore: update root deps and Node LTS
CodingWithAnxiety Dec 31, 2025
4ac5926
build: modernize webpack configs and vue-ts transform
CodingWithAnxiety Dec 31, 2025
9949905
electron: update runtime for Electron 39
CodingWithAnxiety Dec 31, 2025
c3b60f0
build: add electron-builder scripts
CodingWithAnxiety Dec 31, 2025
d91a04c
feat: set up pnpm workspace and split deps
CodingWithAnxiety Jan 9, 2026
0006575
feat: align scss build with sass cli
CodingWithAnxiety Jan 9, 2026
8567ab2
fix: update webpack loader configs
CodingWithAnxiety Jan 9, 2026
f68ee77
fix: tighten vue watch typing
CodingWithAnxiety Jan 9, 2026
0174b91
docs: update build docs and remove legacy packer
CodingWithAnxiety Jan 9, 2026
bad81cb
chore: update pnpm lockfile
CodingWithAnxiety Jan 9, 2026
dca0ae5
chore: removes binary headers called by pack.js (now removed)
CodingWithAnxiety Jan 9, 2026
a75c325
fix: enable esModuleInterop for vue tsconfigs
CodingWithAnxiety Jan 9, 2026
6e03b19
chore(android): update Gradle, AGP, and SDK config
CodingWithAnxiety Jan 9, 2026
9ff141c
fix(android): handle API 34 permissions and storage
CodingWithAnxiety Jan 9, 2026
79c6702
fix(web): shim Vue default export for mobile bundle
CodingWithAnxiety Jan 9, 2026
5ae96c8
chore: update docs and Android Studio metadata
CodingWithAnxiety Jan 9, 2026
0b87331
fix: Update readme.md with correct heading stuff.
CodingWithAnxiety Jan 9, 2026
d088b3b
build: update webpack asset handling
CodingWithAnxiety Jan 10, 2026
b037169
chore: remove yarn lockfiles
CodingWithAnxiety Jan 10, 2026
4302898
fix: Fixes some weird webpack shenanigans. (I love webpack!)
CodingWithAnxiety Jan 10, 2026
467c761
fix(electron): use raven default import for login context
CodingWithAnxiety Jan 10, 2026
b4d2569
fix(chat): proper user menu dismissal and avatar sync
CodingWithAnxiety Jan 10, 2026
008f958
fix: Fix user list tabs by rendering panes with v-if
CodingWithAnxiety Jan 10, 2026
3963a16
raven mobile fix
CodingWithAnxiety Jan 11, 2026
0f15893
fix: Fix foreground service type for BackgroundService
CodingWithAnxiety Jan 11, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .nvmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
22
122 changes: 122 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
# Contributing to Chat 3 Client <!-- omit in toc -->
Interested in contributing to F-Chat 3.0? This is where to start.

## Table of Contents <!-- omit in toc -->
- [Beginnings](#beginnings)
- [Repository Dependancies](#repository-dependancies)
- [Mobile](#mobile)
- [Setting up your development environment](#setting-up-your-development-environment)
- [Building](#building)
- [Electron](#electron)
- [Webchat](#webchat)
- [Themes](#themes)
- [Project layout](#project-layout)
- [Branches](#branches)
- [Tags](#tags)
- [Style guidelines](#style-guidelines)

## Beginnings

Want to add a feature or fix a bug? This guide covers the basics for getting set up.

### Repository Dependancies
This repo is primarily **Vue 2**, **TypeScript**, and **JavaScript**. You will need:
- **Node.js 22** (see `.nvmrc`)
- **pnpm 10**
- Optional: **fnm** (or another Node version manager)

#### Mobile
- **Android**: Android Studio (includes SDK + platform tools), a recent JDK (17+ recommended), and Gradle (via the Android plugin).
- **iOS**: macOS with Xcode (includes Command Line Tools).
-
### Setting up your development environment

```sh
git clone <repo-url>
cd chat3client
pnpm install
```

### Building
#### Electron

For development:

```sh
pnpm -C electron build
pnpm -C electron start
```

For distribution (electron-builder):

```sh
pnpm -C electron build:dist
pnpm -C electron pack
```

Cross-platform packaging script (`electron/build/build.mjs`):

```sh
pnpm -C electron build:win
pnpm -C electron build:linux
pnpm -C electron build:mac
```

Direct usage:

```sh
node electron/build/build.mjs --os linux --format deb AppImage
node electron/build/build.mjs --os windows --arch x64 ia32
node electron/build/build.mjs --os linux --docker
```

```sh
pnpm -C mobile build
```

Android:

```sh
cd mobile/android
./gradlew assembleDebug
```

iOS:

Open `mobile/ios/F-Chat.xcodeproj` in Xcode and run.

#### Webchat

```sh
pnpm -C webchat build
```

#### Themes

```sh
pnpm -C scss install
pnpm -C scss build
```

Output lands in `scss/css`.

### Project layout

#### Branches

- **master**: stable releases
- **development**: integration branch for active work
- **feature/***: new features
- **fix/***: bug fixes
- **hotfix/***: urgent production fixes

#### Tags

We follow semantic versioning:
- **vX.Y.Z**: stable release
- **vX.Y.Z-DEV-X.Y**: pre-release
- **vX.Y.Z-rc-X.Y**: release candidate

## Style guidelines

Currently none lol (TODO, Linting. Shivers.)
6 changes: 3 additions & 3 deletions chat/UserList.vue
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<template>
<sidebar id="user-list" :label="l('users.title')" icon="fa-users" :right="true" :open="expanded">
<tabs style="flex-shrink:0" :tabs="channel ? [l('users.friends'), l('users.members')] : [l('users.friends')]" v-model="tab"></tabs>
<div class="users" style="padding-left:10px" v-show="tab === '0'">
<div class="users" style="padding-left:10px" v-if="tab === '0'">
<h4>{{l('users.friends')}}</h4>
<div v-for="character in friends" :key="character.name">
<user :character="character" :showStatus="true" :bookmark="false"></user>
Expand All @@ -11,7 +11,7 @@
<user :character="character" :showStatus="true" :bookmark="false"></user>
</div>
</div>
<div v-if="channel" style="padding-left:5px;flex:1;display:flex;flex-direction:column" v-show="tab === '1'">
<div v-if="channel && tab === '1'" style="padding-left:5px;flex:1;display:flex;flex-direction:column">
<div class="users" style="flex:1;padding-left:5px">
<h4>{{l('users.memberCount', channel.sortedMembers.length)}}</h4>
<div v-for="member in filteredMembers" :key="member.character.name">
Expand Down Expand Up @@ -106,4 +106,4 @@
display: flex;
}
}
</style>
</style>
6 changes: 3 additions & 3 deletions chat/UserMenu.vue
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<template>
<div>
<div id="userMenu" class="list-group" v-show="showContextMenu" :style="position" v-if="character"
<div id="userMenu" class="list-group" v-if="character && showContextMenu" :style="position"
style="position:fixed;padding:10px 10px 5px;display:block;width:220px;z-index:1100" ref="menu">
<div style="min-height: 65px;padding:5px;overflow:auto" class="list-group-item" @click.stop>
<img :src="characterImage" style="width:60px;height:60px;margin-right:5px;float:left" v-if="showAvatars"/>
Expand Down Expand Up @@ -194,6 +194,7 @@
this.position = {left: `${touch.clientX}px`, top: `${touch.clientY}px`};
this.$nextTick(() => {
const menu = <HTMLElement>this.$refs['menu'];
if(this.character !== character) return;
this.characterImage = characterImage(character.name);
if((parseInt(this.position.left, 10) + menu.offsetWidth) > window.innerWidth)
this.position.left = `${window.innerWidth - menu.offsetWidth - 1}px`;
Expand All @@ -211,6 +212,5 @@

#userMenu .list-group-item-action {
border-top-width: 0;
z-index: -1;
}
</style>
</style>
8 changes: 4 additions & 4 deletions chat/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ const data = {
Vue.set(vue, module, subState);
(<VueState[K]>data[module]) = subState;
},
watch<T>(getter: (this: VueState) => T, callback: WatchHandler<T>): void {
watch<T>(getter: (this: VueState) => T, callback: Exclude<WatchHandler<T>, string>): void {
vue.$watch(getter, callback);
},
async reloadSettings(): Promise<void> {
Expand All @@ -80,7 +80,7 @@ export function init(this: void, connection: Connection, logsClass: new() => Log
data.register('characters', Characters(connection));
data.register('channels', Channels(connection, core.characters));
data.register('conversations', Conversations());
data.watch(() => state.hiddenUsers, async(newValue) => {
data.watch(() => state.hiddenUsers, async(newValue: string[]) => {
if(data.settingsStore !== undefined) await data.settingsStore.set('hiddenUsers', newValue);
});
connection.onEvent('connecting', async() => {
Expand All @@ -99,9 +99,9 @@ export interface Core {
readonly channels: Channel.State
readonly bbCodeParser: BBCodeParser
readonly notifications: Notifications
watch<T>(getter: (this: VueState) => T, callback: WatchHandler<T>): void
watch<T>(getter: (this: VueState) => T, callback: Exclude<WatchHandler<T>, string>): void
}

const core = <Core><any>data; /*tslint:disable-line:no-any*///hack

export default core;
export default core;
4 changes: 2 additions & 2 deletions chat/vue-raven.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import * as Raven from 'raven-js';
import Raven from 'raven-js';
import Vue from 'vue';

/*tslint:disable:no-unsafe-any no-any*///hack
Expand Down Expand Up @@ -68,4 +68,4 @@ export function setupRaven(dsn: string, version: string): void {
(<Window & {onunhandledrejection(e: PromiseRejectionEvent): void}>window).onunhandledrejection = (e: PromiseRejectionEvent) => {
Raven.captureException(<Error>e.reason);
};
}
}
39 changes: 16 additions & 23 deletions electron/Index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -77,12 +77,12 @@
import {Component, Hook} from '@f-list/vue-ts';
import Axios from 'axios';
import * as electron from 'electron';
import * as remote from '@electron/remote';
import log from 'electron-log'; //tslint:disable-line:match-default-export-name
import * as fs from 'fs';
import * as path from 'path';
import * as qs from 'querystring';
import * as Raven from 'raven-js';
import {promisify} from 'util';
import Raven from 'raven-js';
import Vue from 'vue';
import Chat from '../chat/Chat.vue';
import {getKey, Settings} from '../chat/common';
Expand All @@ -94,24 +94,17 @@
import {SimpleCharacter} from '../interfaces';
import {Keys} from '../keys';
import CharacterPage from '../site/character_page/character_page.vue';
import {defaultHost, GeneralSettings, nativeRequire} from './common';
import {defaultHost, GeneralSettings} from './common';
import {fixLogs} from './filesystem';
import * as SlimcatImporter from './importer';
import {SecureStore} from './secure-store';

const webContents = electron.remote.getCurrentWebContents();
const parent = electron.remote.getCurrentWindow().webContents;
const webContents = remote.getCurrentWebContents();
// tslint:disable-next-line:no-require-imports no-submodule-imports
require('@electron/remote/main').enable(webContents);
const parent = remote.getCurrentWindow().webContents;

log.info('About to load keytar');
/*tslint:disable:no-any*///because this is hacky
const keyStore = nativeRequire<{
getPassword(account: string): Promise<string>
setPassword(account: string, password: string): Promise<void>
deletePassword(account: string): Promise<void>
[key: string]: (...args: any[]) => Promise<any>
}>('keytar/build/Release/keytar.node');
for(const key in keyStore) keyStore[key] = promisify(<(...args: any[]) => any>keyStore[key].bind(keyStore, 'fchat'));
//tslint:enable
log.info('Loaded keytar.');
const keyStore = new SecureStore('fchat-accounts', remote);

@Component({
components: {chat: Chat, modal: Modal, characterPage: CharacterPage, logs: Logs}
Expand All @@ -135,14 +128,14 @@
@Hook('created')
created(): void {
if(this.settings.account.length > 0) this.saveLogin = true;
keyStore.getPassword(this.settings.account)
.then((value: string) => this.password = value, (err: Error) => this.error = err.message);
keyStore.getPassword('f-list.net', this.settings.account)
.then((value: string | null) => this.password = value || '', (err: Error) => this.error = err.message);

Vue.set(core.state, 'generalSettings', this.settings);

electron.ipcRenderer.on('settings',
(_: Event, settings: GeneralSettings) => core.state.generalSettings = this.settings = settings);
electron.ipcRenderer.on('open-profile', (_: Event, name: string) => {
(_event: Electron.IpcRendererEvent, settings: GeneralSettings) => core.state.generalSettings = this.settings = settings);
electron.ipcRenderer.on('open-profile', (_event: Electron.IpcRendererEvent, name: string) => {
const profileViewer = <Modal>this.$refs['profileViewer'];
this.profileName = name;
profileViewer.show();
Expand All @@ -162,7 +155,7 @@
if(this.loggingIn) return;
this.loggingIn = true;
try {
if(!this.saveLogin) await keyStore.deletePassword(this.settings.account);
if(!this.saveLogin) await keyStore.deletePassword('f-list.net', this.settings.account);
const data = <{ticket?: string, error: string, characters: {[key: string]: number}, default_character: number}>
(await Axios.post('https://www.f-list.net/json/getApiTicket.php', qs.stringify({
account: this.settings.account, password: this.password, no_friends: true, no_bookmarks: true,
Expand All @@ -174,7 +167,7 @@
}
if(this.saveLogin) {
electron.ipcRenderer.send('save-login', this.settings.account, this.settings.host);
await keyStore.setPassword(this.settings.account, this.password);
await keyStore.setPassword('f-list.net', this.settings.account, this.password);
}
Socket.host = this.settings.host;

Expand Down Expand Up @@ -251,7 +244,7 @@
}

async openProfileInBrowser(): Promise<void> {
return electron.remote.shell.openExternal(`https://www.f-list.net/c/${this.profileName}`);
return remote.shell.openExternal(`https://www.f-list.net/c/${this.profileName}`);
}

get styling(): string {
Expand Down
Loading