├── mobile ├── ios │ ├── F-Chat │ │ ├── www │ │ ├── en.lproj │ │ │ └── Localizable.strings │ │ ├── Assets.xcassets │ │ │ ├── Contents.json │ │ │ ├── AppIcon.appiconset │ │ │ │ ├── icon-1024.jpg │ │ │ │ ├── icon-20.png │ │ │ │ ├── icon-76.png │ │ │ │ ├── icon-20@2x.png │ │ │ │ ├── icon-20@3x.png │ │ │ │ ├── icon-40@2x.png │ │ │ │ ├── icon-60@2x.png │ │ │ │ ├── icon-60@3x.png │ │ │ │ ├── icon-76@2x.png │ │ │ │ ├── icon-83.5@2x.png │ │ │ │ └── Contents.json │ │ │ └── LaunchStoryboard.imageset │ │ │ │ ├── Default@2x~universal~anyany.png │ │ │ │ ├── Default@2x~universal~comany.png │ │ │ │ ├── Default@2x~universal~comcom.png │ │ │ │ ├── Default@3x~universal~anyany.png │ │ │ │ ├── Default@3x~universal~anycom.png │ │ │ │ └── Default@3x~universal~comany.png │ │ ├── Background.swift │ │ ├── Info.plist │ │ ├── Base.lproj │ │ │ ├── Main.storyboard │ │ │ └── LaunchScreen.storyboard │ │ ├── AppDelegate.swift │ │ ├── native.js │ │ ├── Notification.swift │ │ └── File.swift │ ├── .gitignore │ └── F-Chat.xcodeproj │ │ └── project.xcworkspace │ │ └── contents.xcworkspacedata ├── android │ ├── settings.gradle │ ├── app │ │ ├── src │ │ │ └── main │ │ │ │ ├── assets │ │ │ │ └── www │ │ │ │ ├── res │ │ │ │ ├── mipmap-hdpi │ │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-mdpi │ │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-xhdpi │ │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-xxhdpi │ │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-xxxhdpi │ │ │ │ │ └── ic_launcher.png │ │ │ │ ├── drawable-hdpi │ │ │ │ │ └── ic_notification.png │ │ │ │ ├── drawable-mdpi │ │ │ │ │ └── ic_notification.png │ │ │ │ ├── drawable-xhdpi │ │ │ │ │ └── ic_notification.png │ │ │ │ ├── drawable-xxhdpi │ │ │ │ │ └── ic_notification.png │ │ │ │ ├── drawable-xxxhdpi │ │ │ │ │ └── ic_notification.png │ │ │ │ ├── values │ │ │ │ │ └── strings.xml │ │ │ │ └── layout │ │ │ │ │ └── activity_main.xml │ │ │ │ ├── kotlin │ │ │ │ └── net │ │ │ │ │ └── f_list │ │ │ │ │ └── fchat │ │ │ │ │ ├── Background.kt │ │ │ │ │ ├── File.kt │ │ │ │ │ ├── BackgroundService.kt │ │ │ │ │ └── Notifications.kt │ │ │ │ └── AndroidManifest.xml │ │ ├── .gitignore │ │ ├── build.gradle │ │ └── proguard-rules.pro │ ├── gradle │ │ └── wrapper │ │ │ ├── gradle-wrapper.jar │ │ │ └── gradle-wrapper.properties │ ├── .gitignore │ ├── .idea │ │ ├── misc.xml │ │ └── modules.xml │ ├── build.gradle │ ├── gradle.properties │ └── gradlew.bat ├── index.html ├── package.json ├── tsconfig.json ├── notifications.ts ├── webpack.config.js └── chat.ts ├── chat ├── assets │ ├── chat.mp3 │ ├── chat.ogg │ ├── chat.wav │ ├── login.mp3 │ ├── login.ogg │ ├── login.wav │ ├── logout.mp3 │ ├── logout.ogg │ ├── logout.wav │ ├── newnote.mp3 │ ├── newnote.ogg │ ├── newnote.wav │ ├── system.mp3 │ ├── system.ogg │ ├── system.wav │ ├── attention.mp3 │ ├── attention.ogg │ ├── attention.wav │ ├── modalert.mp3 │ ├── modalert.ogg │ ├── modalert.wav │ └── ic_notification.png ├── qs.d.ts ├── ChannelTagView.vue ├── Sidebar.vue ├── WebSocket.ts ├── RecentConversations.vue ├── zip.ts ├── user_view.ts ├── StatusSwitcher.vue ├── message_view.ts ├── vue-raven.ts ├── notifications.ts └── ReportDialog.vue ├── .gitignore ├── electron ├── build │ ├── dmg.png │ ├── icon.ico │ ├── icon.png │ ├── tray.png │ ├── badge.png │ ├── dmg@2x.png │ ├── icon.icns │ ├── tray@2x.png │ └── linux-libs │ │ ├── libXss.so.1 │ │ ├── libXtst.so.6 │ │ ├── libgconf-2.so.4 │ │ ├── libnotify.so.4 │ │ ├── libindicator.so.7 │ │ └── libappindicator.so.1 ├── tsconfig-main.json ├── window.ts ├── tsconfig-renderer.json ├── package.json ├── window.html ├── index.html ├── common.ts ├── notifications.ts ├── window_state.ts └── dictionaries.ts ├── scss ├── themes │ ├── variables │ │ ├── _light_derived.scss │ │ ├── _dark_derived.scss │ │ ├── _default_derived.scss │ │ ├── _light_variables.scss │ │ ├── _invert.scss │ │ ├── _default_variables.scss │ │ └── _dark_variables.scss │ ├── site │ │ ├── dark.scss │ │ ├── light.scss │ │ └── default.scss │ ├── _site.scss │ ├── chat │ │ ├── default.scss │ │ ├── light.scss │ │ └── dark.scss │ └── _chat.scss ├── _comments.scss ├── _tickets.scss ├── fa.scss ├── package.json ├── _functions.scss ├── importer.js ├── _threads.scss ├── _flist_overrides.scss ├── _eicons_editor.scss ├── _notes.scss ├── _core.scss ├── _bbcode_editor.scss ├── _character_editor.scss ├── _tag_input.scss ├── _bbcode.scss └── _flist_derived.scss ├── fchat ├── common.ts └── index.ts ├── webchat ├── package.json ├── sw.js ├── tsconfig.json ├── notifications.ts ├── webpack.config.js └── chat.ts ├── components ├── custom_dialog.ts ├── character_select.vue ├── character_link.vue ├── date_display.vue ├── tabs.ts ├── form_group.vue ├── collapse.vue ├── form_group_inputgroup.vue ├── Dropdown.vue ├── simple_pager.vue └── FilterableSelect.vue ├── site ├── character_page │ ├── data_store.ts │ ├── infotags.vue │ ├── contact_method.vue │ ├── delete_dialog.vue │ ├── copy_custom_menu.vue │ ├── groups.vue │ ├── friends.vue │ ├── infotag.vue │ ├── memo_dialog.vue │ ├── images.vue │ ├── kink.vue │ ├── copy_custom_dialog.vue │ ├── duplicate_dialog.vue │ └── context_menu.ts └── utils.ts ├── LICENSE ├── webpack.js ├── bbcode ├── view.ts └── editor.ts ├── package.json ├── tslint └── vuePropsRule.js ├── keys.ts └── interfaces.ts /mobile/ios/F-Chat/www: -------------------------------------------------------------------------------- 1 | ../../www -------------------------------------------------------------------------------- /mobile/ios/.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | xcuserdata/ 3 | -------------------------------------------------------------------------------- /mobile/android/settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app' 2 | -------------------------------------------------------------------------------- /mobile/android/app/src/main/assets/www: -------------------------------------------------------------------------------- 1 | ../../../../../www -------------------------------------------------------------------------------- /mobile/android/app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | /debug 3 | /release -------------------------------------------------------------------------------- /mobile/ios/F-Chat/en.lproj/Localizable.strings: -------------------------------------------------------------------------------- 1 | "Cancel" = "Cancel"; 2 | "OK" = "OK"; 3 | -------------------------------------------------------------------------------- /chat/assets/chat.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/f-list/exported/HEAD/chat/assets/chat.mp3 -------------------------------------------------------------------------------- /chat/assets/chat.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/f-list/exported/HEAD/chat/assets/chat.ogg -------------------------------------------------------------------------------- /chat/assets/chat.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/f-list/exported/HEAD/chat/assets/chat.wav -------------------------------------------------------------------------------- /chat/assets/login.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/f-list/exported/HEAD/chat/assets/login.mp3 -------------------------------------------------------------------------------- /chat/assets/login.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/f-list/exported/HEAD/chat/assets/login.ogg -------------------------------------------------------------------------------- /chat/assets/login.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/f-list/exported/HEAD/chat/assets/login.wav -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | /electron/app 3 | /electron/dist 4 | /mobile/www 5 | /webchat/dist -------------------------------------------------------------------------------- /chat/assets/logout.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/f-list/exported/HEAD/chat/assets/logout.mp3 -------------------------------------------------------------------------------- /chat/assets/logout.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/f-list/exported/HEAD/chat/assets/logout.ogg -------------------------------------------------------------------------------- /chat/assets/logout.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/f-list/exported/HEAD/chat/assets/logout.wav -------------------------------------------------------------------------------- /chat/assets/newnote.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/f-list/exported/HEAD/chat/assets/newnote.mp3 -------------------------------------------------------------------------------- /chat/assets/newnote.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/f-list/exported/HEAD/chat/assets/newnote.ogg -------------------------------------------------------------------------------- /chat/assets/newnote.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/f-list/exported/HEAD/chat/assets/newnote.wav -------------------------------------------------------------------------------- /chat/assets/system.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/f-list/exported/HEAD/chat/assets/system.mp3 -------------------------------------------------------------------------------- /chat/assets/system.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/f-list/exported/HEAD/chat/assets/system.ogg -------------------------------------------------------------------------------- /chat/assets/system.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/f-list/exported/HEAD/chat/assets/system.wav -------------------------------------------------------------------------------- /chat/qs.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'qs' { 2 | export function stringify(data: object): string; 3 | } -------------------------------------------------------------------------------- /electron/build/dmg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/f-list/exported/HEAD/electron/build/dmg.png -------------------------------------------------------------------------------- /electron/build/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/f-list/exported/HEAD/electron/build/icon.ico -------------------------------------------------------------------------------- /electron/build/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/f-list/exported/HEAD/electron/build/icon.png -------------------------------------------------------------------------------- /electron/build/tray.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/f-list/exported/HEAD/electron/build/tray.png -------------------------------------------------------------------------------- /chat/assets/attention.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/f-list/exported/HEAD/chat/assets/attention.mp3 -------------------------------------------------------------------------------- /chat/assets/attention.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/f-list/exported/HEAD/chat/assets/attention.ogg -------------------------------------------------------------------------------- /chat/assets/attention.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/f-list/exported/HEAD/chat/assets/attention.wav -------------------------------------------------------------------------------- /chat/assets/modalert.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/f-list/exported/HEAD/chat/assets/modalert.mp3 -------------------------------------------------------------------------------- /chat/assets/modalert.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/f-list/exported/HEAD/chat/assets/modalert.ogg -------------------------------------------------------------------------------- /chat/assets/modalert.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/f-list/exported/HEAD/chat/assets/modalert.wav -------------------------------------------------------------------------------- /electron/build/badge.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/f-list/exported/HEAD/electron/build/badge.png -------------------------------------------------------------------------------- /electron/build/dmg@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/f-list/exported/HEAD/electron/build/dmg@2x.png -------------------------------------------------------------------------------- /electron/build/icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/f-list/exported/HEAD/electron/build/icon.icns -------------------------------------------------------------------------------- /electron/build/tray@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/f-list/exported/HEAD/electron/build/tray@2x.png -------------------------------------------------------------------------------- /scss/themes/variables/_light_derived.scss: -------------------------------------------------------------------------------- 1 | .whiteText { 2 | @include text-outline($gray-600); 3 | } -------------------------------------------------------------------------------- /chat/assets/ic_notification.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/f-list/exported/HEAD/chat/assets/ic_notification.png -------------------------------------------------------------------------------- /electron/build/linux-libs/libXss.so.1: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/f-list/exported/HEAD/electron/build/linux-libs/libXss.so.1 -------------------------------------------------------------------------------- /electron/build/linux-libs/libXtst.so.6: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/f-list/exported/HEAD/electron/build/linux-libs/libXtst.so.6 -------------------------------------------------------------------------------- /scss/themes/variables/_dark_derived.scss: -------------------------------------------------------------------------------- 1 | $blue-color: #06f; 2 | 3 | .blackText { 4 | @include text-outline($gray-600); 5 | } -------------------------------------------------------------------------------- /electron/build/linux-libs/libgconf-2.so.4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/f-list/exported/HEAD/electron/build/linux-libs/libgconf-2.so.4 -------------------------------------------------------------------------------- /electron/build/linux-libs/libnotify.so.4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/f-list/exported/HEAD/electron/build/linux-libs/libnotify.so.4 -------------------------------------------------------------------------------- /mobile/ios/F-Chat/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /electron/build/linux-libs/libindicator.so.7: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/f-list/exported/HEAD/electron/build/linux-libs/libindicator.so.7 -------------------------------------------------------------------------------- /electron/build/linux-libs/libappindicator.so.1: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/f-list/exported/HEAD/electron/build/linux-libs/libappindicator.so.1 -------------------------------------------------------------------------------- /mobile/android/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/f-list/exported/HEAD/mobile/android/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /scss/_comments.scss: -------------------------------------------------------------------------------- 1 | $comment-grid-columns: 50 !default; 2 | .comment-offset-1 { 3 | margin-left: percentage((1 / $comment-grid-columns)); 4 | } 5 | -------------------------------------------------------------------------------- /mobile/android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/f-list/exported/HEAD/mobile/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /mobile/android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/f-list/exported/HEAD/mobile/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /mobile/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/f-list/exported/HEAD/mobile/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /mobile/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/f-list/exported/HEAD/mobile/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /mobile/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/f-list/exported/HEAD/mobile/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /mobile/android/app/src/main/res/drawable-hdpi/ic_notification.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/f-list/exported/HEAD/mobile/android/app/src/main/res/drawable-hdpi/ic_notification.png -------------------------------------------------------------------------------- /mobile/android/app/src/main/res/drawable-mdpi/ic_notification.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/f-list/exported/HEAD/mobile/android/app/src/main/res/drawable-mdpi/ic_notification.png -------------------------------------------------------------------------------- /mobile/android/app/src/main/res/drawable-xhdpi/ic_notification.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/f-list/exported/HEAD/mobile/android/app/src/main/res/drawable-xhdpi/ic_notification.png -------------------------------------------------------------------------------- /mobile/ios/F-Chat/Assets.xcassets/AppIcon.appiconset/icon-1024.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/f-list/exported/HEAD/mobile/ios/F-Chat/Assets.xcassets/AppIcon.appiconset/icon-1024.jpg -------------------------------------------------------------------------------- /mobile/ios/F-Chat/Assets.xcassets/AppIcon.appiconset/icon-20.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/f-list/exported/HEAD/mobile/ios/F-Chat/Assets.xcassets/AppIcon.appiconset/icon-20.png -------------------------------------------------------------------------------- /mobile/ios/F-Chat/Assets.xcassets/AppIcon.appiconset/icon-76.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/f-list/exported/HEAD/mobile/ios/F-Chat/Assets.xcassets/AppIcon.appiconset/icon-76.png -------------------------------------------------------------------------------- /mobile/android/app/src/main/res/drawable-xxhdpi/ic_notification.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/f-list/exported/HEAD/mobile/android/app/src/main/res/drawable-xxhdpi/ic_notification.png -------------------------------------------------------------------------------- /mobile/android/app/src/main/res/drawable-xxxhdpi/ic_notification.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/f-list/exported/HEAD/mobile/android/app/src/main/res/drawable-xxxhdpi/ic_notification.png -------------------------------------------------------------------------------- /mobile/ios/F-Chat/Assets.xcassets/AppIcon.appiconset/icon-20@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/f-list/exported/HEAD/mobile/ios/F-Chat/Assets.xcassets/AppIcon.appiconset/icon-20@2x.png -------------------------------------------------------------------------------- /mobile/ios/F-Chat/Assets.xcassets/AppIcon.appiconset/icon-20@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/f-list/exported/HEAD/mobile/ios/F-Chat/Assets.xcassets/AppIcon.appiconset/icon-20@3x.png -------------------------------------------------------------------------------- /mobile/ios/F-Chat/Assets.xcassets/AppIcon.appiconset/icon-40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/f-list/exported/HEAD/mobile/ios/F-Chat/Assets.xcassets/AppIcon.appiconset/icon-40@2x.png -------------------------------------------------------------------------------- /mobile/ios/F-Chat/Assets.xcassets/AppIcon.appiconset/icon-60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/f-list/exported/HEAD/mobile/ios/F-Chat/Assets.xcassets/AppIcon.appiconset/icon-60@2x.png -------------------------------------------------------------------------------- /mobile/ios/F-Chat/Assets.xcassets/AppIcon.appiconset/icon-60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/f-list/exported/HEAD/mobile/ios/F-Chat/Assets.xcassets/AppIcon.appiconset/icon-60@3x.png -------------------------------------------------------------------------------- /mobile/ios/F-Chat/Assets.xcassets/AppIcon.appiconset/icon-76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/f-list/exported/HEAD/mobile/ios/F-Chat/Assets.xcassets/AppIcon.appiconset/icon-76@2x.png -------------------------------------------------------------------------------- /mobile/ios/F-Chat/Assets.xcassets/AppIcon.appiconset/icon-83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/f-list/exported/HEAD/mobile/ios/F-Chat/Assets.xcassets/AppIcon.appiconset/icon-83.5@2x.png -------------------------------------------------------------------------------- /scss/themes/variables/_default_derived.scss: -------------------------------------------------------------------------------- 1 | .purpleText { 2 | @include text-outline(#306); 3 | } 4 | 5 | .blackText { 6 | @include text-outline($gray-600); 7 | } 8 | 9 | $blue-color: #06f; -------------------------------------------------------------------------------- /mobile/android/.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | *.apk 3 | .gradle 4 | /local.properties 5 | .idea/* 6 | !.idea/modules.xml 7 | !.idea/misc.xml 8 | .DS_Store 9 | /build 10 | /captures 11 | .externalNativeBuild 12 | -------------------------------------------------------------------------------- /mobile/ios/F-Chat/Assets.xcassets/LaunchStoryboard.imageset/Default@2x~universal~anyany.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/f-list/exported/HEAD/mobile/ios/F-Chat/Assets.xcassets/LaunchStoryboard.imageset/Default@2x~universal~anyany.png -------------------------------------------------------------------------------- /mobile/ios/F-Chat/Assets.xcassets/LaunchStoryboard.imageset/Default@2x~universal~comany.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/f-list/exported/HEAD/mobile/ios/F-Chat/Assets.xcassets/LaunchStoryboard.imageset/Default@2x~universal~comany.png -------------------------------------------------------------------------------- /mobile/ios/F-Chat/Assets.xcassets/LaunchStoryboard.imageset/Default@2x~universal~comcom.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/f-list/exported/HEAD/mobile/ios/F-Chat/Assets.xcassets/LaunchStoryboard.imageset/Default@2x~universal~comcom.png -------------------------------------------------------------------------------- /mobile/ios/F-Chat/Assets.xcassets/LaunchStoryboard.imageset/Default@3x~universal~anyany.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/f-list/exported/HEAD/mobile/ios/F-Chat/Assets.xcassets/LaunchStoryboard.imageset/Default@3x~universal~anyany.png -------------------------------------------------------------------------------- /mobile/ios/F-Chat/Assets.xcassets/LaunchStoryboard.imageset/Default@3x~universal~anycom.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/f-list/exported/HEAD/mobile/ios/F-Chat/Assets.xcassets/LaunchStoryboard.imageset/Default@3x~universal~anycom.png -------------------------------------------------------------------------------- /mobile/ios/F-Chat/Assets.xcassets/LaunchStoryboard.imageset/Default@3x~universal~comany.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/f-list/exported/HEAD/mobile/ios/F-Chat/Assets.xcassets/LaunchStoryboard.imageset/Default@3x~universal~comany.png -------------------------------------------------------------------------------- /scss/_tickets.scss: -------------------------------------------------------------------------------- 1 | .ticket-reply-well { 2 | //@include well(); 3 | margin-bottom: 0; 4 | } 5 | 6 | .ticket-reply-well.staff { 7 | background-color: theme-color-bg("info"); 8 | border-color: theme-color-border("info"); 9 | } -------------------------------------------------------------------------------- /mobile/ios/F-Chat.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /fchat/common.ts: -------------------------------------------------------------------------------- 1 | const ltRegex = /</gi, gtRegex = />/gi, ampRegex = /&/gi; 2 | 3 | export function decodeHTML(this: void | never, str: string): string { 4 | return str.replace(ltRegex, '<').replace(gtRegex, '>').replace(ampRegex, '&'); 5 | } -------------------------------------------------------------------------------- /mobile/android/.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /mobile/android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-6.1.1-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /fchat/index.ts: -------------------------------------------------------------------------------- 1 | export {default as Characters} from './characters'; 2 | export {default as Channels} from './channels'; 3 | export {default as ChatConnection} from './connection'; 4 | export {Connection, Character, Channel, WebSocketConnection} from './interfaces'; 5 | export {decodeHTML} from './common'; -------------------------------------------------------------------------------- /mobile/android/app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | F-Chat 3 | Running in Background 4 | Messages 5 | OK 6 | Cancel 7 | 8 | -------------------------------------------------------------------------------- /scss/themes/site/dark.scss: -------------------------------------------------------------------------------- 1 | @import "~bootstrap/scss/functions"; 2 | @import "../../functions"; 3 | @import "../variables/dark_variables"; 4 | @import "~bootstrap/scss/variables"; 5 | @import "../../flist_derived"; 6 | @import "../variables/dark_derived"; 7 | 8 | // Apply variables to theme. 9 | @import "../site"; 10 | -------------------------------------------------------------------------------- /scss/themes/site/light.scss: -------------------------------------------------------------------------------- 1 | @import "~bootstrap/scss/functions"; 2 | @import "../../functions"; 3 | @import "../variables/light_variables"; 4 | @import "~bootstrap/scss/variables"; 5 | @import "../../flist_derived"; 6 | @import "../variables/light_derived"; 7 | 8 | // Apply variables to theme. 9 | @import "../site"; 10 | -------------------------------------------------------------------------------- /scss/fa.scss: -------------------------------------------------------------------------------- 1 | $fa-font-path: "~@fortawesome/fontawesome-free/webfonts" !default; 2 | @import "~@fortawesome/fontawesome-free/scss/fontawesome.scss"; 3 | @import "~@fortawesome/fontawesome-free/scss/solid.scss"; 4 | @import "~@fortawesome/fontawesome-free/scss/regular.scss"; 5 | @import "~@fortawesome/fontawesome-free/scss/brands.scss"; -------------------------------------------------------------------------------- /scss/themes/site/default.scss: -------------------------------------------------------------------------------- 1 | @import "~bootstrap/scss/functions"; 2 | @import "../../functions"; 3 | @import "../variables/default_variables"; 4 | @import "~bootstrap/scss/variables"; 5 | @import "../../flist_derived"; 6 | @import "../variables/default_derived"; 7 | 8 | // Apply variables to theme. 9 | @import "../site"; 10 | -------------------------------------------------------------------------------- /mobile/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | FChat 3.0 7 | 8 | 9 |
10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /scss/themes/_site.scss: -------------------------------------------------------------------------------- 1 | @import "~bootstrap/scss/bootstrap"; 2 | @import "../core"; 3 | @import "../character_editor"; 4 | @import "../character_page"; 5 | @import "../eicons_editor"; 6 | @import "../bbcode_editor"; 7 | @import "../bbcode"; 8 | @import "../comments"; 9 | @import "../tickets"; 10 | @import "../notes"; 11 | @import "../threads"; 12 | @import "../flist_overrides"; 13 | @import "../tag_input"; 14 | -------------------------------------------------------------------------------- /electron/tsconfig-main.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2017", 4 | "module": "commonjs", 5 | "sourceMap": true, 6 | "allowJs": true, 7 | "noEmitHelpers": true, 8 | "importHelpers": true, 9 | "forceConsistentCasingInFileNames": true, 10 | "strict": true, 11 | "noUnusedLocals": true, 12 | "noUnusedParameters": true 13 | }, 14 | "include": ["main.ts"] 15 | } -------------------------------------------------------------------------------- /scss/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fchat", 3 | "version": "3.0.0", 4 | "author": "The F-List Team", 5 | "description": "F-Chat Themes", 6 | "license": "MIT", 7 | "dependencies": { 8 | "@fortawesome/fontawesome-free": "^5.6.3", 9 | "bootstrap": "^4.2.1", 10 | "node-sass": "^4.11.0" 11 | }, 12 | "scripts": { 13 | "build": "node-sass --importer=./importer --output css" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /mobile/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "net.f_list.fchat", 3 | "version": "3.0.16", 4 | "displayName": "F-Chat", 5 | "author": "The F-List Team", 6 | "description": "F-List.net Chat Client", 7 | "main": "main.js", 8 | "license": "MIT", 9 | "scripts": { 10 | "build": "node ../webpack development", 11 | "build:dist": "node ../webpack production", 12 | "watch": "node ../webpack watch" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /webchat/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "net.f_list.fchat", 3 | "version": "3.0.16", 4 | "displayName": "F-Chat", 5 | "author": "The F-List Team", 6 | "description": "F-List.net Chat Client", 7 | "main": "main.js", 8 | "license": "MIT", 9 | "scripts": { 10 | "build": "node ../webpack development", 11 | "build:dist": "node ../webpack production", 12 | "watch": "node ../webpack watch" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /electron/window.ts: -------------------------------------------------------------------------------- 1 | import * as qs from 'querystring'; 2 | import {GeneralSettings} from './common'; 3 | import Window from './Window.vue'; 4 | 5 | const params = <{[key: string]: string | undefined}>qs.parse(window.location.search.substr(1)); 6 | const settings = JSON.parse(params['settings']!); 7 | //tslint:disable-next-line:no-unused-expression 8 | new Window({ 9 | el: '#app', 10 | data: {settings} 11 | }); -------------------------------------------------------------------------------- /scss/_functions.scss: -------------------------------------------------------------------------------- 1 | @function theme-color-bg($color-name: "primary") { 2 | @return theme-color-level($color-name, -10); 3 | } 4 | 5 | @function theme-color-border($color-name: "primary") { 6 | @return theme-color-level($color-name, -9); 7 | } 8 | 9 | @mixin text-outline($color) { 10 | text-shadow: $color 1px 0, $color -1px 0, $color 0 1px, $color 0 -1px, $color 1px 1px, $color -1px 1px, $color 1px -1px, $color -1px -1px; 11 | } -------------------------------------------------------------------------------- /scss/themes/variables/_light_variables.scss: -------------------------------------------------------------------------------- 1 | $warning: #e09d3e; 2 | $gray-100: #e5e5e5 !default; 3 | $gray-200: #cccccc !default; 4 | $gray-300: #b2b2b2 !default; 5 | $gray-400: #999999 !default; 6 | $gray-500: #7f7f7f !default; 7 | $gray-600: #666666 !default; 8 | $gray-700: #4c4c4c !default; 9 | $gray-800: #333333 !default; 10 | $gray-900: #191919 !default; 11 | $secondary: $gray-400; 12 | 13 | $body-bg: $gray-100; 14 | $text-muted: $gray-500; -------------------------------------------------------------------------------- /mobile/android/.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /mobile/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "module": "commonjs", 5 | "sourceMap": true, 6 | "experimentalDecorators": true, 7 | "allowJs": true, 8 | "noEmitHelpers": true, 9 | "importHelpers": true, 10 | "forceConsistentCasingInFileNames": true, 11 | "strict": true, 12 | "noUnusedLocals": true, 13 | "noUnusedParameters": true 14 | }, 15 | "include": ["chat.ts", "../**/*.d.ts"] 16 | } -------------------------------------------------------------------------------- /webchat/sw.js: -------------------------------------------------------------------------------- 1 | let client; 2 | 3 | self.addEventListener('install', function(event) { 4 | self.skipWaiting(); 5 | }); 6 | 7 | self.addEventListener('activate', function(event){ 8 | event.waitUntil(clients.claim()); 9 | client = clients.matchAll().then(x => client = x[0]); 10 | }); 11 | 12 | self.addEventListener('notificationclick', function(event) { 13 | event.notification.close(); 14 | client.postMessage(event.notification.data); 15 | }); -------------------------------------------------------------------------------- /webchat/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "module": "commonjs", 5 | "sourceMap": true, 6 | "experimentalDecorators": true, 7 | "allowJs": true, 8 | "noEmitHelpers": true, 9 | "importHelpers": true, 10 | "forceConsistentCasingInFileNames": true, 11 | "strict": true, 12 | "noUnusedLocals": true, 13 | "noUnusedParameters": true 14 | }, 15 | "include": ["chat.ts", "../**/*.d.ts"] 16 | } -------------------------------------------------------------------------------- /components/custom_dialog.ts: -------------------------------------------------------------------------------- 1 | import {Component} from '@f-list/vue-ts'; 2 | import Vue from 'vue'; 3 | import Modal from './Modal.vue'; 4 | 5 | @Component({ 6 | components: {Modal} 7 | }) 8 | export default class CustomDialog extends Vue { 9 | protected get dialog(): Modal { 10 | return this.$children[0]; 11 | } 12 | 13 | show(): void { 14 | this.dialog.show(); 15 | } 16 | 17 | hide(): void { 18 | this.dialog.hide(); 19 | } 20 | } -------------------------------------------------------------------------------- /electron/tsconfig-renderer.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2017", 4 | "module": "commonjs", 5 | "sourceMap": true, 6 | "experimentalDecorators": true, 7 | "allowJs": true, 8 | "noEmitHelpers": true, 9 | "importHelpers": true, 10 | "strict": true, 11 | "forceConsistentCasingInFileNames": true, 12 | "noUnusedLocals": true, 13 | "noUnusedParameters": true 14 | }, 15 | "include": ["chat.ts", "window.ts", "../**/*.d.ts"] 16 | } -------------------------------------------------------------------------------- /electron/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fchat", 3 | "version": "3.0.17", 4 | "author": "The F-List Team", 5 | "description": "F-List.net Chat Client", 6 | "main": "main.js", 7 | "id": "fchat", 8 | "license": "MIT", 9 | "scripts": { 10 | "build": "node ../webpack development", 11 | "build:dist": "node ../webpack production", 12 | "watch": "node ../webpack watch", 13 | "start": "../node_modules/.bin/electron app", 14 | "pack": "node ./pack" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /electron/window.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | F-Chat 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /electron/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | F-Chat 7 | 8 | 9 | 10 |
11 |
12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /scss/importer.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const fs = require('fs'); 3 | 4 | module.exports = function importer(url) { 5 | if(url[0] === '~') { 6 | const file = url.substr(1); 7 | for(const base of require.resolve.paths('')) { 8 | const filePath = path.resolve(base, file); 9 | if(fs.existsSync(path.dirname(filePath))) { 10 | url = filePath; 11 | break; 12 | } 13 | } 14 | } 15 | return {file: url}; 16 | }; -------------------------------------------------------------------------------- /mobile/android/app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 11 | 12 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /scss/themes/variables/_invert.scss: -------------------------------------------------------------------------------- 1 | @function lighten($color, $amount) { 2 | @return hsla(hue($color), saturation($color), lightness($color) - $amount, alpha($color)); 3 | } 4 | 5 | @function darken($color, $amount) { 6 | @return hsla(hue($color), saturation($color), lightness($color) + $amount, alpha($color)); 7 | } 8 | 9 | @function theme-color-level($color-name: "primary", $level: 0) { 10 | $color: theme-color($color-name); 11 | $color-base: if($level < 0, #000, #fff); 12 | $level: abs($level); 13 | 14 | @return mix($color-base, $color, $level * $theme-color-interval); 15 | } 16 | 17 | // Alert color levels 18 | $alert-border-level: 4; 19 | $theme-is-dark: true; -------------------------------------------------------------------------------- /mobile/android/app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | apply plugin: 'kotlin-android' 3 | 4 | android { 5 | compileSdkVersion 28 6 | buildToolsVersion "29.0.3" 7 | defaultConfig { 8 | applicationId "net.f_list.fchat" 9 | minSdkVersion 21 10 | targetSdkVersion 27 11 | versionCode 28 12 | versionName "3.0.16" 13 | } 14 | buildTypes { 15 | release { 16 | minifyEnabled true 17 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 18 | } 19 | } 20 | } 21 | 22 | dependencies { 23 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" 24 | } 25 | repositories { 26 | mavenCentral() 27 | } 28 | -------------------------------------------------------------------------------- /scss/_threads.scss: -------------------------------------------------------------------------------- 1 | .threadPost { 2 | //@include well(); 3 | //@include row(); 4 | margin-bottom: 0; 5 | } 6 | 7 | .threadPost.comment { 8 | background-color: theme-color-bg("info"); 9 | } 10 | 11 | .threadPost.setting { 12 | background-color: theme-color-bg("success"); 13 | } 14 | 15 | .threadPost.deleted { 16 | background-color: theme-color-bg("warning"); 17 | } 18 | 19 | .setting-post { 20 | @extend .dropdown; 21 | } 22 | 23 | .setting-post-popout { 24 | @extend .dropdown-menu; 25 | display: block; 26 | top: auto; 27 | bottom: 100%; 28 | margin-bottom: 2px; 29 | max-height: 250px; 30 | overflow: scroll; 31 | overflow-x: hidden; 32 | } -------------------------------------------------------------------------------- /mobile/android/build.gradle: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | 3 | buildscript { 4 | ext.kotlin_version = '1.3.60' 5 | repositories { 6 | jcenter() 7 | google() 8 | } 9 | dependencies { 10 | classpath 'com.android.tools.build:gradle:3.5.0' 11 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 12 | 13 | // NOTE: Do not place your application dependencies here; they belong 14 | // in the individual module build.gradle files 15 | } 16 | } 17 | 18 | allprojects { 19 | repositories { 20 | jcenter() 21 | google() 22 | } 23 | } 24 | 25 | task clean(type: Delete) { 26 | delete rootProject.buildDir 27 | } 28 | -------------------------------------------------------------------------------- /site/character_page/data_store.ts: -------------------------------------------------------------------------------- 1 | import {Component} from 'vue'; 2 | import {SharedStore, StoreMethods} from './interfaces'; 3 | 4 | export let Store: SharedStore = { 5 | shared: undefined!, 6 | authenticated: false 7 | }; 8 | 9 | export const registeredComponents: {[key: string]: Component | undefined} = {}; 10 | 11 | export function registerComponent(name: string, component: Component): void { 12 | registeredComponents[name] = component; 13 | } 14 | 15 | export function registerMethod(name: K, func: StoreMethods[K]): void { 16 | methods[name] = func; 17 | } 18 | 19 | export const methods: StoreMethods = {}; //tslint:disable-line:no-object-literal-type-assertion -------------------------------------------------------------------------------- /mobile/android/gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | # IDE (e.g. Android Studio) users: 3 | # Gradle settings configured through the IDE *will override* 4 | # any settings specified in this file. 5 | # For more details on how to configure your build environment visit 6 | # http://www.gradle.org/docs/current/userguide/build_environment.html 7 | # Specifies the JVM arguments used for the daemon process. 8 | # The setting is particularly useful for tweaking memory settings. 9 | org.gradle.jvmargs=-Xmx1536m 10 | # When configured, Gradle will run in incubating parallel mode. 11 | # This option should only be used with decoupled projects. More details, visit 12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 13 | # org.gradle.parallel=true 14 | -------------------------------------------------------------------------------- /scss/themes/chat/default.scss: -------------------------------------------------------------------------------- 1 | @import "~bootstrap/scss/functions"; 2 | @import "../../functions"; 3 | @import "../variables/default_variables"; 4 | @import "~bootstrap/scss/variables"; 5 | @import "../../flist_derived"; 6 | @import "../variables/default_derived"; 7 | 8 | // Apply variables to theme. 9 | @import "../chat"; 10 | 11 | * { 12 | &::-webkit-scrollbar-track { 13 | box-shadow: inset 0 0 2px $card-border-color; 14 | border-radius: 10px; 15 | } 16 | 17 | &::-webkit-scrollbar { 18 | width: 12px; 19 | } 20 | 21 | &::-webkit-scrollbar-thumb { 22 | border-radius: 10px; 23 | box-shadow: inset 0 0 2px rgba(0, 0, 0, 0.8); 24 | background-color: $gray-300; 25 | &:hover { 26 | background-color: $gray-500; 27 | } 28 | 29 | &:active { 30 | background-color: $gray-700; 31 | } 32 | } 33 | } -------------------------------------------------------------------------------- /electron/common.ts: -------------------------------------------------------------------------------- 1 | import * as electron from 'electron'; 2 | import * as path from 'path'; 3 | 4 | export const defaultHost = 'wss://chat.f-list.net/chat2'; 5 | 6 | export class GeneralSettings { 7 | account = ''; 8 | closeToTray = true; 9 | profileViewer = true; 10 | host = defaultHost; 11 | logDirectory = path.join(electron.app.getPath('userData'), 'data'); 12 | spellcheckLang: string | undefined = 'en_GB'; 13 | theme = 'default'; 14 | version = electron.app.getVersion(); 15 | beta = false; 16 | customDictionary: string[] = []; 17 | hwAcceleration = true; 18 | } 19 | 20 | //tslint:disable 21 | const Module = require('module'); 22 | 23 | export function nativeRequire(module: string): T { 24 | return Module.prototype.require.call({paths: Module._nodeModulePaths(__dirname)}, module); 25 | } 26 | 27 | //tslint:enable -------------------------------------------------------------------------------- /scss/themes/chat/light.scss: -------------------------------------------------------------------------------- 1 | @import "~bootstrap/scss/functions"; 2 | @import "../../functions"; 3 | @import "../variables/light_variables"; 4 | @import "~bootstrap/scss/variables"; 5 | @import "../../flist_derived"; 6 | @import "../variables/light_derived"; 7 | 8 | // Apply variables to theme. 9 | @import "../chat"; 10 | 11 | * { 12 | &::-webkit-scrollbar-track { 13 | box-shadow: inset 0 0 2px $gray-500; 14 | border-radius: 10px; 15 | } 16 | 17 | &::-webkit-scrollbar { 18 | width: 12px; 19 | } 20 | 21 | &::-webkit-scrollbar-thumb { 22 | border-radius: 10px; 23 | box-shadow: inset 0 0 2px rgba(0, 0, 0, 0.8); 24 | background-color: $gray-800; 25 | &:hover { 26 | background-color: $gray-600; 27 | } 28 | 29 | &:active { 30 | background-color: $gray-500; 31 | } 32 | } 33 | } -------------------------------------------------------------------------------- /scss/themes/chat/dark.scss: -------------------------------------------------------------------------------- 1 | @import "~bootstrap/scss/functions"; 2 | @import "../../functions"; 3 | @import "../variables/dark_variables"; 4 | @import "~bootstrap/scss/variables"; 5 | @import "../../flist_derived"; 6 | @import "../variables/dark_derived"; 7 | 8 | // Apply variables to theme. 9 | @import "../chat"; 10 | 11 | * { 12 | &::-webkit-scrollbar-track { 13 | box-shadow: inset 0 0 2px $card-border-color; 14 | border-radius: 10px; 15 | } 16 | 17 | &::-webkit-scrollbar { 18 | width: 12px; 19 | } 20 | 21 | &::-webkit-scrollbar-thumb { 22 | border-radius: 10px; 23 | box-shadow: inset 0 0 2px rgba(0, 0, 0, 0.8); 24 | background-color: $gray-300; 25 | &:hover { 26 | background-color: $gray-500; 27 | } 28 | 29 | &:active { 30 | background-color: $gray-700; 31 | } 32 | } 33 | } -------------------------------------------------------------------------------- /components/character_select.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | -------------------------------------------------------------------------------- /mobile/android/app/src/main/kotlin/net/f_list/fchat/Background.kt: -------------------------------------------------------------------------------- 1 | package net.f_list.fchat 2 | 3 | import android.content.Context 4 | import android.content.Intent 5 | import android.os.PowerManager 6 | import android.webkit.JavascriptInterface 7 | 8 | class Background(private val ctx: Context) { 9 | private val serviceIntent: Intent by lazy { Intent(ctx, BackgroundService::class.java) } 10 | private val powerManager: PowerManager by lazy { ctx.getSystemService(Context.POWER_SERVICE) as PowerManager } 11 | private var wakeLock: PowerManager.WakeLock? = null 12 | 13 | @JavascriptInterface 14 | fun start() { 15 | wakeLock = powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "fchat") 16 | wakeLock!!.acquire() 17 | ctx.startService(serviceIntent) 18 | } 19 | 20 | @JavascriptInterface 21 | fun stop() { 22 | if(wakeLock != null && wakeLock!!.isHeld) wakeLock!!.release() 23 | ctx.stopService(serviceIntent) 24 | } 25 | } -------------------------------------------------------------------------------- /scss/_flist_overrides.scss: -------------------------------------------------------------------------------- 1 | hr { 2 | margin-top: 5px; 3 | margin-bottom: 5px; 4 | } 5 | 6 | .modal-dialog.modal-wide { 7 | max-width: 95%; 8 | } 9 | 10 | .card-title { 11 | font-weight: bold; 12 | } 13 | 14 | .nav-tabs-scroll { 15 | overflow-x: auto; 16 | 17 | .nav-tabs { 18 | flex-wrap: nowrap; 19 | 20 | .nav-item { 21 | flex-shrink: 0; 22 | } 23 | } 24 | } 25 | 26 | sub { 27 | position: static; 28 | vertical-align: sub; 29 | } 30 | 31 | sup { 32 | position: static; 33 | vertical-align: super; 34 | } 35 | 36 | $theme-is-dark: false !default; 37 | 38 | // HACK: Bootstrap offers no way to override these by default, and they are SUPER bright. 39 | // The level numbers have been changed to make them work for dark themes. 40 | @if $theme-is-dark { 41 | @each $color, $value in $theme-colors { 42 | @include table-row-variant($color, theme-color-level($color, 5)); 43 | } 44 | } 45 | 46 | @include table-row-variant(active, $table-active-bg); -------------------------------------------------------------------------------- /scss/_eicons_editor.scss: -------------------------------------------------------------------------------- 1 | // User icon editor. 2 | .characterImageIcon { 3 | width: 150px; 4 | height: 200px; 5 | border-radius: 25px; 6 | overflow: hidden; 7 | border: 2px #111 solid; 8 | display: inline-block; 9 | margin-left: 10px; 10 | } 11 | 12 | .characterImagePreviewIcon { 13 | width: 100px; 14 | height: 100px; 15 | float: left; 16 | background-size: contain; 17 | background-repeat: no-repeat; 18 | } 19 | 20 | .characterImageActionsIcon { 21 | width: 46px; 22 | float: right; 23 | padding-top: 10px; 24 | text-align: center; 25 | } 26 | 27 | .characterImageActionsIcon a { 28 | width: 30px; 29 | height: 30px; 30 | display: inline-block; 31 | padding-bottom: 15px; 32 | } 33 | 34 | .characterImageIcon a img { 35 | width: 100%; 36 | height: 100%; 37 | } 38 | 39 | .characterImageDescriptionIcon { 40 | width: 100%; 41 | clear: both; 42 | box-sizing: border-box; 43 | padding: 10px; 44 | position: relative; 45 | } 46 | -------------------------------------------------------------------------------- /mobile/android/app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in C:\Android\android-sdk/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | 12 | # If your project uses WebView with JS, uncomment the following 13 | # and specify the fully qualified class name to the JavaScript interface 14 | # class: 15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 16 | # public *; 17 | #} 18 | 19 | # Uncomment this to preserve the line number information for 20 | # debugging stack traces. 21 | #-keepattributes SourceFile,LineNumberTable 22 | 23 | # If you keep the line number information, uncomment this to 24 | # hide the original source file name. 25 | #-renamesourcefileattribute SourceFile 26 | -------------------------------------------------------------------------------- /scss/_notes.scss: -------------------------------------------------------------------------------- 1 | .note-folder { 2 | display: flex; 3 | width: 100%; 4 | .note-folder-total, .note-folder-unread { 5 | width: 3.5rem; 6 | text-align: center; 7 | } 8 | .note-folder-unread { 9 | font-weight: bold; 10 | } 11 | .note-folder-name { 12 | flex: 1; 13 | } 14 | } 15 | 16 | .conversation-from-me, .conversation-from-them { 17 | margin-bottom: 5px; 18 | max-width: percentage((($grid-columns - 4) / $grid-columns)); 19 | word-wrap: break-word; 20 | } 21 | 22 | .conversation-from-me { 23 | margin-right: percentage(( 4 / $grid-columns )); 24 | background-color: $note-conversation-you-bg; 25 | color: $note-conversation-you-text; 26 | border-color: $note-conversation-you-border; 27 | //text-align: left; 28 | } 29 | 30 | .conversation-from-them { 31 | margin-left: percentage(( 4 / $grid-columns )); 32 | background-color: $note-conversation-them-bg; 33 | color: $note-conversation-them-text; 34 | border-color: $note-conversation-them-border; 35 | //text-align: right; 36 | } 37 | -------------------------------------------------------------------------------- /electron/notifications.ts: -------------------------------------------------------------------------------- 1 | import {remote} from 'electron'; 2 | import core from '../chat/core'; 3 | import {Conversation} from '../chat/interfaces'; 4 | //tslint:disable-next-line:match-default-export-name 5 | import BaseNotifications from '../chat/notifications'; 6 | 7 | const browserWindow = remote.getCurrentWindow(); 8 | 9 | export default class Notifications extends BaseNotifications { 10 | async notify(conversation: Conversation, title: string, body: string, icon: string, sound: string): Promise { 11 | if(!this.shouldNotify(conversation)) return; 12 | this.playSound(sound); 13 | browserWindow.flashFrame(true); 14 | if(core.state.settings.notifications) { 15 | const notification = new Notification(title, this.getOptions(conversation, body, icon)); 16 | notification.onclick = () => { 17 | browserWindow.webContents.send('show-tab', remote.getCurrentWebContents().id); 18 | conversation.show(); 19 | browserWindow.focus(); 20 | notification.close(); 21 | }; 22 | } 23 | } 24 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 F-List 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /mobile/ios/F-Chat/Background.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import WebKit 3 | import AVFoundation 4 | 5 | class Background: NSObject, WKScriptMessageHandler { 6 | var player = try! AVAudioPlayer(contentsOf: Bundle.main.url(forResource: "www/sounds/login", withExtension: "wav")!) 7 | 8 | override init() { 9 | let session = AVAudioSession.sharedInstance(); 10 | try! session.setCategory(AVAudioSession.Category.playback, options: .mixWithOthers) 11 | player.volume = 0 12 | player.numberOfLoops = -1; 13 | player.play() 14 | } 15 | 16 | func userContentController(_ controller: WKUserContentController, didReceive message: WKScriptMessage) { 17 | let data = message.body as! [String: AnyObject] 18 | let key = data["_id"] as! String 19 | switch(data["_type"] as! String) { 20 | case "start": 21 | player.play() 22 | case "stop": 23 | player.stop() 24 | default: 25 | message.webView!.evaluateJavaScript("nativeError('\(key)',new Error('Unknown message type'))") 26 | return 27 | } 28 | message.webView!.evaluateJavaScript("nativeMessage('\(key)')") 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /webchat/notifications.ts: -------------------------------------------------------------------------------- 1 | import core from '../chat/core'; 2 | import {Conversation} from '../chat/interfaces'; 3 | //tslint:disable-next-line:match-default-export-name 4 | import BaseNotifications from '../chat/notifications'; 5 | 6 | export default class Notifications extends BaseNotifications { 7 | async notify(conversation: Conversation, title: string, body: string, icon: string, sound: string): Promise { 8 | if(!this.shouldNotify(conversation)) return; 9 | try { 10 | await super.notify(conversation, title, body, icon, sound); 11 | } catch { 12 | //tslint:disable-next-line:no-require-imports no-submodule-imports 13 | await navigator.serviceWorker.register(require('file-loader!./sw.js')); 14 | const reg = await navigator.serviceWorker.ready; 15 | await reg.showNotification(title, this.getOptions(conversation, body, icon)); 16 | navigator.serviceWorker.onmessage = (e) => { 17 | const conv = core.conversations.byKey((<{key: string}>e.data).key); 18 | if(conv !== undefined) conv.show(); 19 | window.focus(); 20 | }; 21 | } 22 | } 23 | } -------------------------------------------------------------------------------- /scss/_core.scss: -------------------------------------------------------------------------------- 1 | .flash-messages-fixed { 2 | top: 0px; 3 | left: auto; 4 | right: auto; 5 | width: 100%; 6 | height: auto; 7 | position: fixed; 8 | z-index: 9000; 9 | } 10 | 11 | .flash-message { 12 | @extend .alert; 13 | position: relative; 14 | border-bottom-color: rgba(0, 0, 0, 0.3); 15 | margin-bottom: 0; 16 | z-index: 150; 17 | } 18 | 19 | .flash-message-enter-active, .flash-message-leave-active { 20 | transition: all 0.2s; 21 | } 22 | 23 | .flash-message-enter, .flash-message-leave-to { 24 | opacity: 0; 25 | transform: translateX(100px); 26 | } 27 | 28 | .character-menu-item { 29 | width: 250px; 30 | .character-link { 31 | display: inline-block; 32 | padding-right: 0; 33 | } 34 | .character-edit-link { 35 | margin-left: auto; 36 | padding: 3px 20px 2px 0; 37 | display: inline-block; 38 | } 39 | } 40 | 41 | .sidebar-top-padded { 42 | margin-top: 20px; 43 | } 44 | 45 | @mixin force-word-wrapping { 46 | overflow-wrap: break-word; 47 | word-wrap: break-word; 48 | 49 | -ms-hyphens: auto; 50 | -moz-hyphens: auto; 51 | -webkit-hyphens: auto; 52 | hyphens: auto; 53 | } 54 | 55 | * { 56 | min-height: 0; 57 | min-width: 0; 58 | -webkit-overflow-scrolling: touch; 59 | } -------------------------------------------------------------------------------- /webpack.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack'); 2 | const path = require('path'); 3 | 4 | const mode = process.argv[2]; 5 | let config = require(path.resolve(process.argv[3] || 'webpack.config')); 6 | if(typeof config === 'function') config = config(mode); 7 | config = Array.isArray(config) ? config : [config]; 8 | for(const item of config) item.mode = mode === 'watch' ? 'development' : mode; 9 | 10 | let lastHash = null; 11 | function compilerCallback(err, stats) { 12 | if(mode !== 'watch' || err) compiler.purgeInputFileSystem(); 13 | if(err) { 14 | lastHash = null; 15 | console.error(err.stack || err); 16 | if(err.details) console.error(err.details); 17 | process.exit(1); 18 | } 19 | if(stats.hash !== lastHash) { 20 | lastHash = stats.hash; 21 | const statsString = stats.toString({colors: require("supports-color"), context: config[0].context, cached: false, cachedAssets: false, exclude: ['node_modules']}); 22 | if(statsString) process.stdout.write(statsString + '\n'); 23 | } 24 | if(mode !== 'watch' && stats.hasErrors()) process.exitCode = 2; 25 | } 26 | 27 | const compiler = webpack(config); 28 | if(mode === 'watch') compiler.watch({}, compilerCallback); 29 | else compiler.run(compilerCallback); -------------------------------------------------------------------------------- /mobile/android/app/src/main/kotlin/net/f_list/fchat/File.kt: -------------------------------------------------------------------------------- 1 | package net.f_list.fchat 2 | 3 | import android.content.Context 4 | import android.webkit.JavascriptInterface 5 | import org.json.JSONArray 6 | import java.io.File 7 | import java.io.FileOutputStream 8 | import java.io.RandomAccessFile 9 | import java.util.* 10 | 11 | class File(private val ctx: Context) { 12 | @JavascriptInterface 13 | fun read(name: String): String? { 14 | val file = File(ctx.filesDir, name) 15 | if(!file.exists()) return null 16 | return file.readText() 17 | } 18 | 19 | @JavascriptInterface 20 | fun getSize(name: String) = File(ctx.filesDir, name).length() 21 | 22 | @JavascriptInterface 23 | fun write(name: String, data: String) { 24 | FileOutputStream(File(ctx.filesDir, name)).use { it.write(data.toByteArray()) } 25 | } 26 | 27 | @JavascriptInterface 28 | fun listFilesN(name: String) = JSONArray(File(ctx.filesDir, name).listFiles().filter { it.isFile }.map { it.name }).toString() 29 | 30 | @JavascriptInterface 31 | fun listDirectoriesN(name: String) = JSONArray(File(ctx.filesDir, name).listFiles().filter { it.isDirectory }.map { it.name }).toString() 32 | 33 | @JavascriptInterface 34 | fun ensureDirectory(name: String) { 35 | File(ctx.filesDir, name).mkdirs() 36 | } 37 | } -------------------------------------------------------------------------------- /mobile/android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 17 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /mobile/notifications.ts: -------------------------------------------------------------------------------- 1 | import core from '../chat/core'; 2 | import {Conversation} from '../chat/interfaces'; 3 | import BaseNotifications from '../chat/notifications'; //tslint:disable-line:match-default-export-name 4 | 5 | declare global { 6 | const NativeNotification: { 7 | notify(notify: boolean, title: string, text: string, icon: string, sound: string | null, data: string): void 8 | requestPermission(): void 9 | }; 10 | } 11 | 12 | document.addEventListener('notification-clicked', (e: Event) => { 13 | const conv = core.conversations.byKey((e).detail.data); 14 | if(conv !== undefined) conv.show(); 15 | }); 16 | 17 | export default class Notifications extends BaseNotifications { 18 | async notify(conversation: Conversation, title: string, body: string, icon: string, sound: string): Promise { 19 | if(!this.shouldNotify(conversation)) return; 20 | NativeNotification.notify(core.state.settings.notifications && this.isInBackground, title, body, icon, 21 | core.state.settings.playSound ? sound : null, conversation.key); //tslint:disable-line:no-null-keyword 22 | } 23 | 24 | async requestPermission(): Promise { 25 | return NativeNotification.requestPermission(); 26 | } 27 | } -------------------------------------------------------------------------------- /scss/_bbcode_editor.scss: -------------------------------------------------------------------------------- 1 | .bbcode-editor-text-area { 2 | textarea { 3 | min-height: 150px; 4 | &:focus { 5 | box-shadow: 0 0 0 ($input-btn-focus-width / 2) $input-btn-focus-color; 6 | } 7 | } 8 | } 9 | 10 | .bbcode-toolbar { 11 | align-items: flex-start; 12 | flex-wrap: nowrap; 13 | @media (max-width: breakpoint-max(xs)) { 14 | background: $text-background-color; 15 | padding: 10px; 16 | position: absolute; 17 | top: 0; 18 | border-radius: 3px; 19 | z-index: 20; 20 | display: none; 21 | .btn { 22 | margin: 3px; 23 | border-radius: $btn-border-radius !important; 24 | } 25 | } 26 | @media (min-width: breakpoint-min(sm)) { 27 | .close { 28 | display: none; 29 | } 30 | .btn { 31 | border-bottom-left-radius: 0; 32 | border-bottom-right-radius: 0; 33 | border: $input-border-width solid $input-border-color; 34 | border-bottom: 0; 35 | } 36 | } 37 | } 38 | 39 | .bbcode-btn { 40 | @media (min-width: breakpoint-min(sm)) { 41 | display: none; 42 | } 43 | } 44 | 45 | .bbcode-editor-controls { 46 | display: flex; 47 | align-items: center; 48 | float: right; 49 | order: 1; 50 | justify-content: flex-end; 51 | @media (max-width: breakpoint-max(xs)) { 52 | flex: 1 49%; 53 | } 54 | } -------------------------------------------------------------------------------- /components/character_link.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | -------------------------------------------------------------------------------- /components/date_display.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | -------------------------------------------------------------------------------- /chat/ChannelTagView.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | -------------------------------------------------------------------------------- /bbcode/view.ts: -------------------------------------------------------------------------------- 1 | import {CreateElement, FunctionalComponentOptions, RenderContext, VNode} from 'vue'; 2 | import {DefaultProps, RecordPropsDefinition} from 'vue/types/options'; //tslint:disable-line:no-submodule-imports 3 | import {BBCodeElement} from './core'; 4 | import {BBCodeParser} from './parser'; 5 | 6 | export const BBCodeView = (parser: BBCodeParser): FunctionalComponentOptions> => ({ 7 | functional: true, 8 | render(createElement: CreateElement, context: RenderContext): VNode { 9 | /*tslint:disable:no-unsafe-any*///because we're not actually supposed to do any of this 10 | context.data.hook = { 11 | insert(node: VNode): void { 12 | node.elm!.appendChild(parser.parseEverything( 13 | context.props.text !== undefined ? context.props.text : context.props.unsafeText)); 14 | if(context.props.afterInsert !== undefined) context.props.afterInsert(node.elm); 15 | }, 16 | destroy(node: VNode): void { 17 | const element = ((node.elm).firstChild); 18 | if(element.cleanup !== undefined) element.cleanup(); 19 | } 20 | }; 21 | const vnode = createElement('span', context.data); 22 | vnode.key = context.props.text; 23 | return vnode; 24 | //tslint:enable 25 | } 26 | }); -------------------------------------------------------------------------------- /chat/Sidebar.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | -------------------------------------------------------------------------------- /chat/WebSocket.ts: -------------------------------------------------------------------------------- 1 | import {WebSocketConnection} from '../fchat'; 2 | 3 | export default class Socket implements WebSocketConnection { 4 | static host = 'wss://chat.f-list.net/chat2'; 5 | private socket: WebSocket; 6 | private lastHandler: Promise = Promise.resolve(); 7 | 8 | constructor() { 9 | this.socket = new WebSocket(Socket.host); 10 | } 11 | 12 | get readyState(): WebSocketConnection.ReadyState { 13 | return this.socket.readyState; 14 | } 15 | 16 | close(): void { 17 | this.socket.close(); 18 | } 19 | 20 | onMessage(handler: (message: string) => void): void { 21 | this.socket.addEventListener('message', (e) => { 22 | this.lastHandler = this.lastHandler.then(() => handler(e.data), (err) => { 23 | window.requestAnimationFrame(() => { throw err; }); 24 | handler(e.data); 25 | }); 26 | }); 27 | } 28 | 29 | onOpen(handler: () => void): void { 30 | this.socket.addEventListener('open', handler); 31 | } 32 | 33 | onClose(handler: () => void): void { 34 | this.socket.addEventListener('close', handler); 35 | } 36 | 37 | onError(handler: (error: Error) => void): void { 38 | this.socket.addEventListener('error', () => handler(new Error())); 39 | } 40 | 41 | send(message: string): void { 42 | this.socket.send(message); 43 | } 44 | } -------------------------------------------------------------------------------- /mobile/android/app/src/main/kotlin/net/f_list/fchat/BackgroundService.kt: -------------------------------------------------------------------------------- 1 | package net.f_list.fchat 2 | 3 | import android.app.Notification 4 | import android.app.NotificationChannel 5 | import android.app.NotificationManager 6 | import android.app.PendingIntent 7 | import android.app.Service 8 | import android.content.Context 9 | import android.content.Intent 10 | import android.os.Build 11 | import android.os.IBinder 12 | 13 | class BackgroundService : Service() { 14 | override fun onBind(p0: Intent?): IBinder? { 15 | return null 16 | } 17 | 18 | override fun onCreate() { 19 | super.onCreate() 20 | val notification = Notification.Builder(this).setContentTitle(getString(R.string.app_name)) 21 | .setContentIntent(PendingIntent.getActivity(this, 1, Intent(this, MainActivity::class.java), PendingIntent.FLAG_UPDATE_CURRENT)) 22 | .setSmallIcon(R.drawable.ic_notification).setAutoCancel(true).setPriority(Notification.PRIORITY_LOW) 23 | if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { 24 | val manager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager; 25 | manager.createNotificationChannel(NotificationChannel("background", getString(R.string.channel_background), NotificationManager.IMPORTANCE_LOW)); 26 | notification.setChannelId("background"); 27 | } 28 | startForeground(1, notification.build()) 29 | } 30 | 31 | override fun onDestroy() { 32 | super.onDestroy() 33 | stopForeground(true) 34 | } 35 | } -------------------------------------------------------------------------------- /site/character_page/infotags.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | -------------------------------------------------------------------------------- /site/character_page/contact_method.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | -------------------------------------------------------------------------------- /site/character_page/delete_dialog.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "flist-exported", 3 | "version": "1.0.0", 4 | "author": "The F-List Team", 5 | "description": "F-List Exported", 6 | "license": "MIT", 7 | "devDependencies": { 8 | "@f-list/fork-ts-checker-webpack-plugin": "^1.3.7", 9 | "@f-list/vue-ts": "^1.0.3", 10 | "@fortawesome/fontawesome-free": "^5.9.0", 11 | "@types/lodash": "^4.14.134", 12 | "@types/sortablejs": "^1.7.2", 13 | "axios": "^0.19.0", 14 | "bootstrap": "^4.3.1", 15 | "css-loader": "^3.0.0", 16 | "date-fns": "^1.30.1", 17 | "electron": "^5.0.4", 18 | "electron-log": "^3.0.1", 19 | "electron-packager": "^14.0.0", 20 | "electron-rebuild": "^1.8.4", 21 | "extract-loader": "^3.1.0", 22 | "file-loader": "^4.0.0", 23 | "lodash": "^4.17.11", 24 | "node-sass": "^4.11.0", 25 | "optimize-css-assets-webpack-plugin": "^5.0.1", 26 | "qs": "^6.6.0", 27 | "raven-js": "^3.27.2", 28 | "sass-loader": "^7.1.0", 29 | "sortablejs": "~1.9.0", 30 | "style-loader": "^0.23.1", 31 | "ts-loader": "^6.0.3", 32 | "tslib": "^1.10.0", 33 | "tslint": "^5.17.0", 34 | "typescript": "^3.5.2", 35 | "vue": "^2.6.8", 36 | "vue-loader": "^15.7.0", 37 | "vue-template-compiler": "^2.6.8", 38 | "webpack": "^4.35.0" 39 | }, 40 | "dependencies": { 41 | "keytar": "^4.10.0", 42 | "spellchecker": "^3.6.0" 43 | }, 44 | "optionalDependencies": { 45 | "appdmg": "^0.6.0", 46 | "electron-squirrel-startup": "^1.0.0", 47 | "electron-winstaller": "^3.0.4" 48 | }, 49 | "scripts": { 50 | "postinstall": "electron-rebuild -fo spellchecker,keytar" 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /site/character_page/copy_custom_menu.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | -------------------------------------------------------------------------------- /components/tabs.ts: -------------------------------------------------------------------------------- 1 | import Vue, {CreateElement, VNode} from 'vue'; 2 | 3 | //tslint:disable-next-line:variable-name 4 | const Tabs = Vue.extend({ 5 | props: ['value', 'tabs'], 6 | render(this: Vue & {readonly value?: string, _v?: string, selected?: string, tabs: {readonly [key: string]: string}}, 7 | createElement: CreateElement): VNode { 8 | let children: {[key: string]: string | VNode | undefined}; 9 | if(this.$slots['default'] !== undefined) { 10 | children = {}; 11 | this.$slots['default']!.forEach((child, i) => { 12 | if(child.context !== undefined) children[child.key !== undefined ? child.key : i] = child; 13 | }); 14 | } else children = this.tabs; 15 | const keys = Object.keys(children); 16 | if(this._v !== this.value) 17 | this.selected = this._v = this.value; 18 | if(this._v === undefined || children[this._v] === undefined) 19 | this.$emit('input', this._v = keys[0]); 20 | if(this.selected !== this._v && children[this.selected!] !== undefined) 21 | this.$emit('input', this._v = this.selected); 22 | return createElement('div', {staticClass: 'nav-tabs-scroll'}, 23 | [createElement('ul', {staticClass: 'nav nav-tabs'}, keys.map((key) => createElement('li', {staticClass: 'nav-item'}, 24 | [createElement('a', { 25 | attrs: {href: '#'}, 26 | staticClass: 'nav-link', class: {active: this._v === key}, on: {click: () => this.$emit('input', key)} 27 | }, [children[key]!])])))]); 28 | } 29 | }); 30 | 31 | export default Tabs; -------------------------------------------------------------------------------- /scss/_character_editor.scss: -------------------------------------------------------------------------------- 1 | .bbcodeTextArea { 2 | max-width: 100%; 3 | min-height: 200px; 4 | } 5 | 6 | .kinkChoice.selected { 7 | font-weight: bold; 8 | } 9 | 10 | .characterEditorSidebar { 11 | position: fixed; 12 | } 13 | 14 | .characterList.characterListSelected { 15 | border-width: 2px; 16 | border-color: $character-list-selected-border; 17 | } 18 | 19 | // Character image editor. 20 | .characterImage { 21 | width: 250px; 22 | height: 300px; 23 | border-radius: 25px; 24 | overflow: hidden; 25 | border: 2px #111 solid; 26 | display: inline-block; 27 | margin-left: 10px; 28 | } 29 | 30 | .characterImage.characterImageSelected { 31 | border-color: $character-image-selected-border; 32 | } 33 | 34 | .characterImagePreview { 35 | width: 200px; 36 | height: 200px; 37 | float: left; 38 | background-size: contain; 39 | background-repeat: no-repeat; 40 | } 41 | 42 | .characterImageActions { 43 | width: 46px; 44 | float: right; 45 | padding-top: 10px; 46 | text-align: center; 47 | } 48 | 49 | .characterImageActions a { 50 | width: 30px; 51 | height: 30px; 52 | display: inline-block; 53 | padding-bottom: 15px; 54 | } 55 | 56 | .characterImage a img { 57 | width: 100%; 58 | height: 100%; 59 | } 60 | 61 | .characterImageDescription { 62 | width: 100%; 63 | height: 100px; 64 | clear: both; 65 | box-sizing: border-box; 66 | padding: 10px; 67 | position: relative; 68 | overflow-y: scroll; 69 | } 70 | 71 | .kink-list-enter-active, .kink-list-leave-active { 72 | transition: all 0.2s; 73 | } 74 | 75 | .kink-list-enter, .kink-list-leave-to { 76 | opacity: 0; 77 | transform: translateX(100px); 78 | } 79 | -------------------------------------------------------------------------------- /scss/themes/_chat.scss: -------------------------------------------------------------------------------- 1 | @import "~bootstrap/scss/mixins"; 2 | @import "~bootstrap/scss/root"; 3 | @import "~bootstrap/scss/reboot"; 4 | @import "~bootstrap/scss/type"; 5 | @import "~bootstrap/scss/images"; 6 | @import "~bootstrap/scss/grid"; 7 | @import "~bootstrap/scss/forms"; 8 | @import "~bootstrap/scss/buttons"; 9 | @import "~bootstrap/scss/transitions"; 10 | @import "~bootstrap/scss/dropdown"; 11 | @import "~bootstrap/scss/button-group"; 12 | @import "~bootstrap/scss/input-group"; 13 | @import "~bootstrap/scss/custom-forms"; 14 | @import "~bootstrap/scss/nav"; 15 | @import "~bootstrap/scss/card"; 16 | @import "~bootstrap/scss/pagination"; 17 | @import "~bootstrap/scss/alert"; 18 | @import "~bootstrap/scss/progress"; 19 | @import "~bootstrap/scss/list-group"; 20 | @import "~bootstrap/scss/close"; 21 | @import "~bootstrap/scss/modal"; 22 | @import "~bootstrap/scss/popover"; 23 | @import "~bootstrap/scss/utilities/background"; 24 | @import "~bootstrap/scss/utilities/display"; 25 | @import "~bootstrap/scss/utilities/flex"; 26 | @import "~bootstrap/scss/utilities/sizing"; 27 | 28 | @import "../core"; 29 | @import "../chat"; 30 | @import "../character_page"; 31 | @import "../eicons_editor"; 32 | @import "../bbcode_editor"; 33 | @import "../bbcode"; 34 | @import "../flist_overrides"; 35 | 36 | * { 37 | &::-webkit-scrollbar-track { 38 | box-shadow: inset 0 0 8px $card-border-color; 39 | border-radius: 10px; 40 | } 41 | 42 | &::-webkit-scrollbar { 43 | width: 12px; 44 | } 45 | 46 | &::-webkit-scrollbar-thumb { 47 | border-radius: 10px; 48 | box-shadow: inset 0 0 4px rgba(0, 0, 0, 0.8); 49 | background-color: $gray-300; 50 | &:hover { 51 | background-color: $gray-500; 52 | } 53 | 54 | &:active { 55 | background-color: $gray-700; 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /components/form_group.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | -------------------------------------------------------------------------------- /mobile/ios/F-Chat/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | UIBackgroundModes 24 | 25 | audio 26 | 27 | UILaunchStoryboardName 28 | LaunchScreen 29 | UIMainStoryboardFile 30 | Main 31 | UIRequiredDeviceCapabilities 32 | 33 | armv7 34 | 35 | UISupportedInterfaceOrientations 36 | 37 | UIInterfaceOrientationPortrait 38 | UIInterfaceOrientationLandscapeLeft 39 | UIInterfaceOrientationLandscapeRight 40 | 41 | UISupportedInterfaceOrientations~ipad 42 | 43 | UIInterfaceOrientationPortrait 44 | UIInterfaceOrientationPortraitUpsideDown 45 | UIInterfaceOrientationLandscapeLeft 46 | UIInterfaceOrientationLandscapeRight 47 | 48 | UIViewControllerBasedStatusBarAppearance 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /components/collapse.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | -------------------------------------------------------------------------------- /scss/_tag_input.scss: -------------------------------------------------------------------------------- 1 | .tag-input-control { 2 | display: inline-block; 3 | margin-bottom: 0; 4 | vertical-align: middle; 5 | height: $input-height; // Make inputs at least the height of their button counterpart (base line-height + padding + border) 6 | width: 100%; 7 | padding: $input-padding-y $input-padding-x; 8 | font-size: $font-size-base; 9 | line-height: $input-line-height; 10 | color: $input-color; 11 | background-color: $input-bg; 12 | background-clip: padding-box; 13 | background-image: none; // Reset unusual Firefox-on-Android default style; see https://github.com/necolas/normalize.css/issues/214 14 | border: $input-border-width solid $input-border-color; 15 | border-radius: $input-border-radius; // Note: This has no effect on s in CSS. 16 | @include box-shadow($input-box-shadow); 17 | @include transition($input-transition); 18 | 19 | .tag-input { 20 | background-color: $input-bg; 21 | color: $input-color; 22 | border: none; 23 | width: auto; 24 | &:focus { 25 | box-shadow: none; 26 | outline: none; 27 | } 28 | } 29 | } 30 | 31 | .form-inline .tag-input-control { 32 | width: auto; 33 | } 34 | 35 | .tag-error { 36 | border: 1px solid theme-color-level(danger, $alert-border-level); 37 | background-color: theme-color-level(danger, $alert-bg-level); 38 | .tag-input { 39 | text-color: theme-color-level(danger, $alert-color-level); 40 | background-color: theme-color-level(danger, $alert-bg-level); 41 | } 42 | } 43 | 44 | .suggestion-important { 45 | font-weight: bold !important; 46 | } 47 | 48 | .suggestion-description { 49 | display: block; 50 | font-style: italic; 51 | font-size: $font-size-sm; 52 | } -------------------------------------------------------------------------------- /mobile/ios/F-Chat/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /site/character_page/groups.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | -------------------------------------------------------------------------------- /tslint/vuePropsRule.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | const ts = require("typescript"); 4 | const Lint = require("tslint"); 5 | class Rule extends Lint.Rules.AbstractRule { 6 | apply(sourceFile) { 7 | return this.applyWithFunction(sourceFile, walk, undefined); 8 | } 9 | } 10 | exports.Rule = Rule; 11 | function walk(ctx) { 12 | if (ctx.sourceFile.isDeclarationFile) 13 | return; 14 | return ts.forEachChild(ctx.sourceFile, cb); 15 | function cb(node) { 16 | if (node.kind !== ts.SyntaxKind.PropertyDeclaration) 17 | return ts.forEachChild(node, cb); 18 | if (!node.decorators) 19 | return; 20 | const property = node; 21 | for (const decorator of node.decorators) { 22 | const call = decorator.expression.kind == ts.SyntaxKind.CallExpression ? decorator.expression : undefined; 23 | const name = call && call.expression.getText() || decorator.expression.getText(); 24 | if (name === 'Prop') { 25 | if (!node.modifiers || !node.modifiers.some((x) => x.kind === ts.SyntaxKind.ReadonlyKeyword)) 26 | ctx.addFailureAtNode(property.name, 'Vue property should be readonly'); 27 | if (call && call.arguments.length > 0 && 28 | call.arguments[0].properties.map((x) => x.name.getText()).some((x) => x === 'default' || x === 'required')) { 29 | if (property.questionToken !== undefined) 30 | ctx.addFailureAtNode(property.name, 'Vue property is required and should not be optional.'); 31 | } 32 | else if (property.questionToken === undefined) 33 | ctx.addFailureAtNode(property.name, 'Vue property should be optional - it is not required and has no default value.'); 34 | } 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /components/form_group_inputgroup.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | -------------------------------------------------------------------------------- /site/character_page/friends.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | -------------------------------------------------------------------------------- /scss/themes/variables/_default_variables.scss: -------------------------------------------------------------------------------- 1 | // Base colors, used for pretty much everything. 2 | // On dark based themes these are inverted in order to cooperate with bootstrap which assumes that all themes are light based. 3 | $gray-base: #080810; 4 | $white: $gray-base; 5 | $gray-100: lighten($gray-base, 10%); 6 | $gray-200: lighten($gray-base, 20%); 7 | $gray-300: lighten($gray-base, 30%); 8 | $gray-400: lighten($gray-base, 40%); 9 | $gray-500: lighten($gray-base, 50%); 10 | $gray-600: lighten($gray-base, 60%); 11 | $gray-700: lighten($gray-base, 70%); 12 | $gray-800: lighten($gray-base, 80%); 13 | $gray-900: lighten($gray-base, 90%); 14 | $black: #fff; 15 | 16 | // Theme and brand colors 17 | $primary: #0447af; 18 | $secondary: $gray-400; 19 | $success: darken(#009900, 5%); 20 | $info: #0447af; 21 | $warning: #f29c00; 22 | $danger: #930300; 23 | $light: $gray-200; 24 | $text-muted: $gray-500; 25 | $text-dark: $gray-600; 26 | $component-active-color: $gray-900; 27 | 28 | // Core page element colors 29 | $body-bg: $gray-100; 30 | $link-color: $gray-800; 31 | $link-hover-color: lighten($link-color, 15%); 32 | $dropdown-bg: $gray-200; 33 | $dropdown-divider-bg: $gray-300; 34 | 35 | // Modal Dialogs 36 | $modal-backdrop-bg: $white; 37 | $modal-content-bg: $gray-100; 38 | $modal-header-border-color: $gray-300; 39 | 40 | // Fix YIQ(automatic text contrast) preferring black over white on dark themes. 41 | $yiq-text-light: $gray-800; 42 | 43 | // Reduce default padding slightly between columns in grids. 44 | $grid-gutter-width: 20px; 45 | 46 | // Form Elements 47 | $input-bg: $gray-200; 48 | $input-color: $black; 49 | $custom-select-bg: $gray-200; 50 | 51 | // List groups 52 | $list-group-bg: $gray-200; 53 | 54 | // Pagination 55 | $pagination-active-color: $link-color; 56 | 57 | // F-List specific color helpers to help make monochromatic themes easier to deal with. 58 | $text-background-color: $gray-200; 59 | $text-background-color-disabled: $gray-100; 60 | 61 | @import "invert"; -------------------------------------------------------------------------------- /scss/themes/variables/_dark_variables.scss: -------------------------------------------------------------------------------- 1 | // Base colors, used for pretty much everything. 2 | // On dark based themes these are inverted in order to cooperate with bootstrap which assumes that all themes are light based. 3 | $gray-base: #000; 4 | $white: $gray-base; 5 | $gray-100: lighten($gray-base, 10%); 6 | $gray-200: lighten($gray-base, 20%); 7 | $gray-300: lighten($gray-base, 30%); 8 | $gray-400: lighten($gray-base, 40%); 9 | $gray-500: lighten($gray-base, 50%); 10 | $gray-600: lighten($gray-base, 60%); 11 | $gray-700: lighten($gray-base, 70%); 12 | $gray-800: lighten($gray-base, 80%); 13 | $gray-900: lighten($gray-base, 90%); 14 | $black: #fff; 15 | 16 | // Theme and brand colors 17 | $primary: #003399; 18 | $secondary: $gray-300; 19 | $success: darken(#009900, 5%); 20 | $info: #003399; 21 | $warning: #c26c00; 22 | $danger: darken(#930300, 5%); 23 | $light: $gray-100; 24 | $text-muted: $gray-400; 25 | $text-dark: $gray-500; 26 | $component-active-color: $gray-800; 27 | 28 | // Core page element colors 29 | $body-bg: $gray-base; 30 | $body-color: $gray-800; 31 | $link-color: $gray-700; 32 | $link-hover-color: lighten($link-color, 15%); 33 | $dropdown-bg: $gray-200; 34 | 35 | // Modal Dialogs 36 | $modal-backdrop-bg: $white; 37 | $modal-content-bg: $gray-100; 38 | $modal-header-border-color: $gray-200; 39 | 40 | // Fix YIQ(automatic text contrast) preferring black over white on dark themes. 41 | $yiq-text-light: $gray-800; 42 | 43 | // Reduce default padding slightly between columns in grids. 44 | $grid-gutter-width: 20px; 45 | 46 | // Form Elements 47 | $input-bg: $gray-100; 48 | $input-color: $black; 49 | $input-border-color: $secondary; 50 | $custom-select-bg: $gray-200; 51 | 52 | // List groups 53 | $list-group-bg: $gray-200; 54 | 55 | // Pagination 56 | $pagination-active-color: $link-color; 57 | 58 | // F-List specific color helpers to help make monochromatic themes easier to deal with. 59 | $text-background-color: $gray-100; 60 | $text-background-color-disabled: $gray-200; 61 | 62 | @import "invert"; -------------------------------------------------------------------------------- /site/character_page/infotag.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | -------------------------------------------------------------------------------- /electron/window_state.ts: -------------------------------------------------------------------------------- 1 | import {app, screen} from 'electron'; 2 | import log from 'electron-log'; //tslint:disable-line:match-default-export-name 3 | import * as fs from 'fs'; 4 | import * as path from 'path'; 5 | 6 | const baseDir = path.join(app.getPath('userData'), 'data'); 7 | const windowStatePath = path.join(baseDir, 'window.json'); 8 | 9 | interface SavedWindowState { 10 | x?: number 11 | y?: number 12 | height: number 13 | width: number 14 | maximized: boolean 15 | } 16 | 17 | function mapToScreen(state: SavedWindowState): SavedWindowState { 18 | let x = state.x !== undefined ? state.x : 0; 19 | let y = state.y !== undefined ? state.y : 0; 20 | const primaryDisplay = screen.getPrimaryDisplay(); 21 | const targetDisplay = screen.getDisplayMatching({x, y, height: state.height, width: state.width}); 22 | if(primaryDisplay.scaleFactor !== 1 && targetDisplay.id !== primaryDisplay.id) { 23 | x /= primaryDisplay.scaleFactor; 24 | y /= primaryDisplay.scaleFactor; 25 | } 26 | state.x = x !== 0 ? x : undefined; 27 | state.y = y !== 0 ? y : undefined; 28 | return state; 29 | } 30 | 31 | export function setSavedWindowState(window: Electron.BrowserWindow): void { 32 | const bounds = window.getBounds(); 33 | const maximized = window.isMaximized(); 34 | const windowState: SavedWindowState = { 35 | height: bounds.height, 36 | maximized, 37 | width: bounds.width, 38 | x: bounds.x, 39 | y: bounds.y 40 | }; 41 | fs.writeFileSync(windowStatePath, JSON.stringify(windowState)); 42 | } 43 | 44 | export function getSavedWindowState(): SavedWindowState { 45 | const defaultState = { 46 | height: 768, 47 | maximized: false, 48 | width: 1024 49 | }; 50 | if(!fs.existsSync(windowStatePath)) 51 | return defaultState; 52 | try { 53 | let savedState = JSON.parse(fs.readFileSync(windowStatePath, 'utf-8')); 54 | savedState = mapToScreen(savedState); 55 | return savedState; 56 | } catch (e) { 57 | log.error(e); 58 | return defaultState; 59 | } 60 | } -------------------------------------------------------------------------------- /mobile/ios/F-Chat/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | @UIApplicationMain 4 | class AppDelegate: UIResponder, UIApplicationDelegate { 5 | var window: UIWindow? 6 | 7 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 8 | // Override point for customization after application launch. 9 | return true 10 | } 11 | 12 | func applicationWillResignActive(_ application: UIApplication) { 13 | // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. 14 | // Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game. 15 | } 16 | 17 | func applicationDidEnterBackground(_ application: UIApplication) { 18 | // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. 19 | // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. 20 | } 21 | 22 | func applicationWillEnterForeground(_ application: UIApplication) { 23 | // Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background. 24 | } 25 | 26 | func applicationDidBecomeActive(_ application: UIApplication) { 27 | // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. 28 | } 29 | 30 | func applicationWillTerminate(_ application: UIApplication) { 31 | // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /components/Dropdown.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | -------------------------------------------------------------------------------- /keys.ts: -------------------------------------------------------------------------------- 1 | export const enum Keys { 2 | Backspace = 8, 3 | Tab = 9, 4 | Enter = 13, 5 | Shift = 16, 6 | Ctrl = 17, 7 | Alt = 18, 8 | Pause = 19, 9 | CapsLock = 20, 10 | Escape = 27, 11 | Space = 32, 12 | PageUp = 33, 13 | PageDown = 34, 14 | End = 35, 15 | Home = 36, 16 | 17 | ArrowLeft = 37, 18 | ArrowUp = 38, 19 | ArrowRight = 39, 20 | ArrowDown = 40, 21 | 22 | PrintScreen = 44, 23 | Insert = 45, 24 | Delete = 46, 25 | 26 | Digit0 = 48, 27 | Digit1 = 49, 28 | Digit2 = 50, 29 | Digit3 = 51, 30 | Digit4 = 52, 31 | Digit5 = 53, 32 | Digit6 = 54, 33 | Digit7 = 55, 34 | Digit8 = 56, 35 | Digit9 = 57, 36 | 37 | KeyA = 65, 38 | KeyB = 66, 39 | KeyC = 67, 40 | KeyD = 68, 41 | KeyE = 69, 42 | KeyF = 70, 43 | KeyG = 71, 44 | KeyH = 72, 45 | KeyI = 73, 46 | KeyJ = 74, 47 | KeyK = 75, 48 | KeyL = 76, 49 | KeyM = 77, 50 | KeyN = 78, 51 | KeyO = 79, 52 | KeyP = 80, 53 | KeyQ = 81, 54 | KeyR = 82, 55 | KeyS = 83, 56 | KeyT = 84, 57 | KeyU = 85, 58 | KeyV = 86, 59 | KeyW = 87, 60 | KeyX = 88, 61 | KeyY = 89, 62 | KeyZ = 90, 63 | 64 | LeftWindowKey = 91, 65 | RightWindowKey = 92, 66 | SelectKey = 93, 67 | 68 | Numpad0 = 96, 69 | Numpad1 = 97, 70 | Numpad2 = 98, 71 | Numpad3 = 99, 72 | Numpad4 = 100, 73 | Numpad5 = 101, 74 | Numpad6 = 102, 75 | Numpad7 = 103, 76 | Numpad8 = 104, 77 | Numpad9 = 105, 78 | 79 | NumpadMultiply = 106, 80 | NumpadAdd = 107, 81 | NumpadSubtract = 109, 82 | NumpadDecimal = 110, 83 | NumpadDivide = 111, 84 | 85 | F1 = 112, 86 | F2 = 113, 87 | F3 = 114, 88 | F4 = 115, 89 | F5 = 116, 90 | F6 = 117, 91 | F7 = 118, 92 | F8 = 119, 93 | F9 = 120, 94 | F10 = 121, 95 | F11 = 122, 96 | F12 = 123, 97 | 98 | NumLock = 144, 99 | ScrollLock = 145, 100 | 101 | Semicolon = 186, 102 | Equal = 187, 103 | Comma = 188, 104 | Minus = 189, 105 | Period = 190, 106 | ForwardSlash = 191, 107 | Backquote = 192, 108 | 109 | BracketLeft = 219, 110 | BracketRight = 221, 111 | Quote = 222 112 | } -------------------------------------------------------------------------------- /scss/_bbcode.scss: -------------------------------------------------------------------------------- 1 | .redText { 2 | color: $red-color; 3 | } 4 | 5 | .blueText { 6 | color: $blue-color; 7 | } 8 | 9 | .greenText { 10 | color: $green-color; 11 | } 12 | 13 | .yellowText { 14 | color: $yellow-color; 15 | } 16 | 17 | .cyanText { 18 | color: $cyan-color; 19 | } 20 | 21 | .purpleText { 22 | color: $purple-color; 23 | } 24 | 25 | .brownText { 26 | color: $brown-color; 27 | } 28 | 29 | .pinkText { 30 | color: $pink-color; 31 | } 32 | 33 | .grayText { 34 | color: $gray-color; 35 | } 36 | 37 | .orangeText { 38 | color: $orange-color; 39 | } 40 | 41 | .whiteText { 42 | color: $white-color; 43 | } 44 | 45 | .blackText { 46 | color: $black-color; 47 | } 48 | 49 | /* Tweak these to be consistent with how bootstrap does sizing. */ 50 | span.bigText { 51 | font-size: $font-size-lg; 52 | } 53 | 54 | span.smallText { 55 | font-size: $font-size-sm; 56 | } 57 | 58 | span.leftText { 59 | display: block; 60 | text-align: left; 61 | } 62 | 63 | span.centerText { 64 | display: block; 65 | text-align: center; 66 | } 67 | 68 | span.rightText { 69 | display: block; 70 | text-align: right; 71 | } 72 | 73 | span.justifyText { 74 | display: block; 75 | text-align: justify; 76 | } 77 | 78 | div.indentText { 79 | padding-left: 5%; 80 | } 81 | 82 | .character-avatar { 83 | display: inline; 84 | height: 100px; 85 | width: 100px; 86 | &.icon { 87 | height: 50px; 88 | width: 50px; 89 | } 90 | } 91 | 92 | .bbcode { 93 | white-space: pre-wrap; 94 | .row > * { 95 | margin-bottom: 5px; 96 | } 97 | } 98 | 99 | .bbcode-collapse-header { 100 | font-weight: bold; 101 | cursor: pointer; 102 | width: 100%; 103 | min-height: $line-height-base; 104 | } 105 | 106 | .bbcode-collapse-body { 107 | margin-left: 0.5rem; 108 | transition: height 0.2s; 109 | overflow-y: hidden; 110 | 111 | &.closed { 112 | padding: 0; 113 | } 114 | } 115 | 116 | .bbcode { 117 | @include force-word-wrapping; 118 | max-width: 100%; 119 | a { 120 | text-decoration: underline; 121 | &:hover { 122 | text-decoration: none; 123 | } 124 | } 125 | } 126 | 127 | .link-domain { 128 | color: $text-muted; 129 | text-shadow: none; 130 | } 131 | 132 | .user-link { 133 | text-shadow: none; 134 | } 135 | -------------------------------------------------------------------------------- /webchat/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const ForkTsCheckerWebpackPlugin = require('@f-list/fork-ts-checker-webpack-plugin'); 3 | const VueLoaderPlugin = require('vue-loader/lib/plugin'); 4 | const vueTransformer = require('@f-list/vue-ts/transform').default; 5 | 6 | const config = { 7 | entry: __dirname + '/chat.ts', 8 | output: { 9 | path: __dirname + '/dist' 10 | }, 11 | context: __dirname, 12 | module: { 13 | rules: [ 14 | { 15 | test: /\.ts$/, 16 | loader: 'ts-loader', 17 | options: { 18 | appendTsSuffixTo: [/\.vue$/], 19 | configFile: __dirname + '/tsconfig.json', 20 | transpileOnly: true, 21 | getCustomTransformers: () => ({before: [vueTransformer]}) 22 | } 23 | }, 24 | { 25 | test: /\.vue$/, 26 | loader: 'vue-loader', 27 | options: { 28 | compilerOptions: { 29 | preserveWhitespace: false 30 | } 31 | } 32 | }, 33 | {test: /\.eot(\?v=\d+\.\d+\.\d+)?$/, loader: 'file-loader'}, 34 | {test: /\.(woff2?)$/, loader: 'file-loader'}, 35 | {test: /\.ttf(\?v=\d+\.\d+\.\d+)?$/, loader: 'file-loader'}, 36 | {test: /\.svg(\?v=\d+\.\d+\.\d+)?$/, loader: 'file-loader'}, 37 | {test: /\.(wav|mp3|ogg)$/, loader: 'file-loader?name=sounds/[name].[ext]'}, 38 | {test: /\.(png|html)$/, loader: 'file-loader?name=[name].[ext]'}, 39 | {test: /\.scss$/, use: ['vue-style-loader', 'css-loader', 'sass-loader']}, 40 | {test: /\.css$/, use: ['vue-style-loader', 'css-loader']}, 41 | ] 42 | }, 43 | plugins: [ 44 | new ForkTsCheckerWebpackPlugin({async: false, vue: true, tslint: path.join(__dirname, '../tslint.json')}), 45 | new VueLoaderPlugin() 46 | ], 47 | resolve: { 48 | 'extensions': ['.ts', '.js', '.vue', '.scss'] 49 | } 50 | }; 51 | 52 | module.exports = function(mode) { 53 | if(mode === 'production') { 54 | process.env.NODE_ENV = 'production'; 55 | config.devtool = 'source-map'; 56 | } else { 57 | config.devtool = 'none'; 58 | } 59 | return config; 60 | }; -------------------------------------------------------------------------------- /chat/RecentConversations.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 49 | 50 | -------------------------------------------------------------------------------- /scss/_flist_derived.scss: -------------------------------------------------------------------------------- 1 | // BBcode colors 2 | $red-color: #f00 !default; 3 | $green-color: #0f0 !default; 4 | $blue-color: #00f !default; 5 | $yellow-color: #ff0 !default; 6 | $cyan-color: #0ff !default; 7 | $purple-color: #93f !default; 8 | $white-color: #fff !default; 9 | $black-color: #000 !default; 10 | $brown-color: #8a6d3b !default; 11 | $pink-color: #f9c !default; 12 | $gray-color: #ccc !default; 13 | $orange-color: #f60 !default; 14 | $collapse-header-bg: $card-bg !default; 15 | $collapse-border: darken($card-border-color, 25%) !default; 16 | $text-dark: $text-muted !default; 17 | 18 | // Character page quick kink comparison 19 | $quick-compare-active-border: $black-color !default; 20 | $quick-compare-favorite-bg: theme-color-level("info", -6) !default; 21 | $quick-compare-yes-bg: theme-color-level("success", -6) !default; 22 | $quick-compare-maybe-bg: theme-color-level("warning", -6) !default; 23 | $quick-compare-no-bg: theme-color-level("danger", -6) !default; 24 | 25 | // character page badges 26 | $character-badge-bg: darken($card-bg, 10%) !default; 27 | $character-badge-border: darken($card-border-color, 10%) !default; 28 | $character-badge-subscriber-bg: theme-color-bg("info") !default; 29 | $character-badge-subscriber-border: theme-color-border("info") !default; 30 | 31 | // Character editor 32 | $character-list-selected-border: $success !default; 33 | $character-image-selected-border: $success !default; 34 | 35 | // Notes conversation view 36 | $note-conversation-you-bg: theme-color-bg("info") !default; 37 | $note-conversation-you-text: theme-color-level("info", 6) !default; 38 | $note-conversation-you-border: theme-color-border("info") !default; 39 | $note-conversation-them-bg: $card-bg !default; 40 | $note-conversation-them-text: $body-color !default; 41 | $note-conversation-them-border: $card-border-color !default; 42 | 43 | $nav-link-hover-color: $link-color !default; 44 | 45 | // General color extensions missing from bootstrap 46 | $text-background-color: $body-bg !default; 47 | $text-background-color-disabled: $gray-800 !default; 48 | 49 | .nav-link.disabled { 50 | cursor: default; 51 | } 52 | 53 | .custom-select:hover { 54 | text-decoration: none; 55 | cursor: default; 56 | } 57 | 58 | select { 59 | @extend .custom-select; 60 | -webkit-appearance: none; 61 | -moz-appearance: none; 62 | } 63 | 64 | a.btn { 65 | color: $link-color; 66 | 67 | &:hover { 68 | color: $link-hover-color; 69 | } 70 | } -------------------------------------------------------------------------------- /site/character_page/memo_dialog.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | -------------------------------------------------------------------------------- /electron/dictionaries.ts: -------------------------------------------------------------------------------- 1 | import Axios from 'axios'; 2 | import * as electron from 'electron'; 3 | import log from 'electron-log'; //tslint:disable-line:match-default-export-name 4 | import * as fs from 'fs'; 5 | import * as path from 'path'; 6 | import {promisify} from 'util'; 7 | 8 | const dictDir = path.join(electron.app.getPath('userData'), 'spellchecker'); 9 | fs.mkdirSync(dictDir, {recursive: true}); 10 | 11 | const downloadedPath = path.join(dictDir, 'downloaded.json'); 12 | const downloadUrl = 'https://client.f-list.net/dicts/'; 13 | type File = {name: string, hash: string}; 14 | type DictionaryIndex = {[key: string]: {dic: File, aff: File} | undefined}; 15 | let availableDictionaries: DictionaryIndex | undefined; 16 | let downloadedDictionaries: {[key: string]: File | undefined} = {}; 17 | const writeFile = promisify(fs.writeFile); 18 | 19 | export async function getAvailableDictionaries(): Promise> { 20 | if(availableDictionaries === undefined) 21 | try { 22 | availableDictionaries = (await Axios.get(`${downloadUrl}index.json`)).data; 23 | if(fs.existsSync(downloadedPath)) 24 | downloadedDictionaries = <{[key: string]: File}>JSON.parse(fs.readFileSync(downloadedPath, 'utf-8')); 25 | } catch(e) { 26 | availableDictionaries = {}; 27 | log.error(`Error loading dictionaries: ${e}`); 28 | } 29 | return Object.keys(availableDictionaries).sort(); 30 | } 31 | 32 | export async function ensureDictionary(lang: string): Promise { 33 | await getAvailableDictionaries(); 34 | const dict = availableDictionaries![lang]; 35 | if(dict === undefined) return; 36 | async function ensure(type: 'aff' | 'dic'): Promise { 37 | const file = dict![type]; 38 | const filePath = path.join(dictDir, `${lang}.${type}`); 39 | const downloaded = downloadedDictionaries[file.name]; 40 | if(downloaded === undefined || downloaded.hash !== file.hash || !fs.existsSync(filePath)) { 41 | const dictionary = (await Axios.get(`${downloadUrl}${file.name}`, {responseType: 'arraybuffer'})).data; 42 | await writeFile(filePath, Buffer.from(dictionary)); 43 | downloadedDictionaries[file.name] = file; 44 | await writeFile(downloadedPath, JSON.stringify(downloadedDictionaries)); 45 | } 46 | } 47 | await ensure('aff'); 48 | await ensure('dic'); 49 | } -------------------------------------------------------------------------------- /mobile/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const ForkTsCheckerWebpackPlugin = require('@f-list/fork-ts-checker-webpack-plugin'); 3 | const VueLoaderPlugin = require('vue-loader/lib/plugin'); 4 | const vueTransformer = require('@f-list/vue-ts/transform').default; 5 | 6 | const config = { 7 | entry: { 8 | chat: [__dirname + '/chat.ts', __dirname + '/index.html'] 9 | }, 10 | output: { 11 | path: __dirname + '/www', 12 | filename: '[name].js' 13 | }, 14 | context: __dirname, 15 | module: { 16 | rules: [ 17 | { 18 | test: /\.ts$/, 19 | loader: 'ts-loader', 20 | options: { 21 | appendTsSuffixTo: [/\.vue$/], 22 | configFile: __dirname + '/tsconfig.json', 23 | transpileOnly: true, 24 | getCustomTransformers: () => ({before: [vueTransformer]}) 25 | } 26 | }, 27 | { 28 | test: /\.vue$/, 29 | loader: 'vue-loader', 30 | options: { 31 | compilerOptions: { 32 | preserveWhitespace: false 33 | } 34 | } 35 | }, 36 | {test: /\.eot(\?v=\d+\.\d+\.\d+)?$/, loader: 'file-loader'}, 37 | {test: /\.(woff2?)$/, loader: 'file-loader'}, 38 | {test: /\.ttf(\?v=\d+\.\d+\.\d+)?$/, loader: 'file-loader'}, 39 | {test: /\.svg(\?v=\d+\.\d+\.\d+)?$/, loader: 'file-loader'}, 40 | {test: /\.(wav|mp3|ogg)$/, loader: 'file-loader?name=sounds/[name].[ext]'}, 41 | {test: /\.(png|html)$/, loader: 'file-loader?name=[name].[ext]'}, 42 | {test: /(? 29 | * @version 3.0 30 | * @see {@link https://github.com/f-list/exported|GitHub repo} 31 | */ 32 | import Axios from 'axios'; 33 | import {init as initCore} from '../chat/core'; 34 | import {setupRaven} from '../chat/vue-raven'; 35 | import Socket from '../chat/WebSocket'; 36 | import Connection from '../fchat/connection'; 37 | import {appVersion, Logs, SettingsStore} from './filesystem'; 38 | import Index from './Index.vue'; 39 | import Notifications from './notifications'; 40 | 41 | const version = (<{version: string}>require('./package.json')).version; //tslint:disable-line:no-require-imports 42 | (window)['setupPlatform'] = (platform: string) => { //tslint:disable-line:no-any 43 | Axios.defaults.params = { __fchat: `mobile-${platform}/${version}` }; 44 | }; 45 | 46 | if(process.env.NODE_ENV === 'production') 47 | setupRaven('https://a9239b17b0a14f72ba85e8729b9d1612@sentry.f-list.net/2', `mobile-${version}`); 48 | 49 | const connection = new Connection('F-Chat 3.0 (Mobile)', appVersion, Socket); 50 | initCore(connection, Logs, SettingsStore, Notifications); 51 | 52 | new Index({ //tslint:disable-line:no-unused-expression 53 | el: '#app' 54 | }); -------------------------------------------------------------------------------- /site/character_page/images.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | -------------------------------------------------------------------------------- /chat/zip.ts: -------------------------------------------------------------------------------- 1 | import {getByteLength} from './common'; 2 | 3 | let crcTable!: number[]; 4 | 5 | export default class Zip { 6 | private blob: BlobPart[] = []; 7 | private files: {header: BlobPart[], offset: number, name: string}[] = []; 8 | private offset = 0; 9 | 10 | constructor() { 11 | if(crcTable !== undefined!) return; 12 | crcTable = []; 13 | for(let c, n = 0; n < 256; n++) { 14 | c = n; 15 | for(let k = 0; k < 8; k++) 16 | c = ((c & 1) ? ((c >>> 1) ^ 0xEDB88320) : (c >>> 1)); //tslint:disable-line:strict-boolean-expressions 17 | crcTable[n] = c; 18 | } 19 | } 20 | 21 | addFile(name: string, content: string): void { 22 | let crc = -1; 23 | let length = 0; 24 | const nameLength = getByteLength(name); 25 | for(let i = 0, strlen = content.length; i < strlen; ++i) { 26 | let c = content.charCodeAt(i); 27 | if(c > 0xD800 && c < 0xD8FF) //surrogate pairs 28 | c = (c - 0xD800) * 0x400 + content.charCodeAt(++i) - 0xDC00 + 0x10000; 29 | let l = c < 0x80 ? 1 : c < 0x800 ? 2 : c < 0x10000 ? 3 : c < 0x200000 ? 4 : c < 0x4000000 ? 5 : 6; 30 | length += l; 31 | let byte = l === 1 ? c : ((0xFF00 >> l) % 256) | (c >>> (l - 1) * 6); 32 | --l; 33 | while(true) { 34 | crc = (crc >>> 8) ^ crcTable[(crc ^ byte) & 0xFF]; 35 | if(--l >= 0) byte = ((c >>> (l * 6)) & 0x3F) | 0x80; 36 | else break; 37 | } 38 | } 39 | crc = (crc ^ (-1)) >>> 0; 40 | const file = { 41 | header: [Uint16Array.of(0, 0, 0, 0, 0), Uint32Array.of(crc, length, length), Uint16Array.of(nameLength, 0)], 42 | offset: this.offset, name 43 | }; 44 | this.blob.push(Uint32Array.of(0x04034B50)); 45 | this.blob.push(...file.header); 46 | this.blob.push(name, content); 47 | this.offset += nameLength + length + 30; 48 | this.files.push(file); 49 | } 50 | 51 | build(): Blob { 52 | const start = this.offset; 53 | for(const file of this.files) { 54 | this.blob.push(Uint16Array.of(0x4B50, 0x0201, 0)); 55 | this.blob.push(...file.header); 56 | this.blob.push(Uint16Array.of(0, 0, 0, 0, 0), Uint32Array.of(file.offset), file.name); 57 | this.offset += getByteLength(file.name) + 46; 58 | } 59 | this.blob.push(Uint16Array.of(0x4B50, 0x0605, 0, 0, this.files.length, this.files.length), 60 | Uint32Array.of(this.offset - start, start), Uint16Array.of(0)); 61 | return new Blob(this.blob); 62 | } 63 | } -------------------------------------------------------------------------------- /mobile/android/gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 12 | set DEFAULT_JVM_OPTS= 13 | 14 | set DIRNAME=%~dp0 15 | if "%DIRNAME%" == "" set DIRNAME=. 16 | set APP_BASE_NAME=%~n0 17 | set APP_HOME=%DIRNAME% 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windowz variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | if "%@eval[2+2]" == "4" goto 4NT_args 53 | 54 | :win9xME_args 55 | @rem Slurp the command line arguments. 56 | set CMD_LINE_ARGS= 57 | set _SKIP=2 58 | 59 | :win9xME_args_slurp 60 | if "x%~1" == "x" goto execute 61 | 62 | set CMD_LINE_ARGS=%* 63 | goto execute 64 | 65 | :4NT_args 66 | @rem Get arguments from the 4NT Shell from JP Software 67 | set CMD_LINE_ARGS=%$ 68 | 69 | :execute 70 | @rem Setup the command line 71 | 72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if "%ERRORLEVEL%"=="0" goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 85 | exit /b 1 86 | 87 | :mainEnd 88 | if "%OS%"=="Windows_NT" endlocal 89 | 90 | :omega 91 | -------------------------------------------------------------------------------- /mobile/ios/F-Chat/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "20x20", 6 | "filename" : "icon-20@2x.png", 7 | "scale" : "2x" 8 | }, 9 | { 10 | "idiom" : "iphone", 11 | "size" : "20x20", 12 | "filename" : "icon-20@3x.png", 13 | "scale" : "3x" 14 | }, 15 | { 16 | "idiom" : "iphone", 17 | "size" : "29x29", 18 | "scale" : "2x" 19 | }, 20 | { 21 | "idiom" : "iphone", 22 | "size" : "29x29", 23 | "scale" : "3x" 24 | }, 25 | { 26 | "idiom" : "iphone", 27 | "size" : "40x40", 28 | "filename" : "icon-40@2x.png", 29 | "scale" : "2x" 30 | }, 31 | { 32 | "idiom" : "iphone", 33 | "size" : "40x40", 34 | "filename" : "icon-60@2x.png", 35 | "scale" : "3x" 36 | }, 37 | { 38 | "idiom" : "iphone", 39 | "size" : "60x60", 40 | "filename" : "icon-60@2x.png", 41 | "scale" : "2x" 42 | }, 43 | { 44 | "idiom" : "iphone", 45 | "size" : "60x60", 46 | "filename" : "icon-60@3x.png", 47 | "scale" : "3x" 48 | }, 49 | { 50 | "idiom" : "ipad", 51 | "size" : "20x20", 52 | "filename" : "icon-20.png", 53 | "scale" : "1x" 54 | }, 55 | { 56 | "idiom" : "ipad", 57 | "size" : "20x20", 58 | "filename" : "icon-20@2x.png", 59 | "scale" : "2x" 60 | }, 61 | { 62 | "idiom" : "ipad", 63 | "size" : "29x29", 64 | "scale" : "1x" 65 | }, 66 | { 67 | "idiom" : "ipad", 68 | "size" : "29x29", 69 | "scale" : "2x" 70 | }, 71 | { 72 | "idiom" : "ipad", 73 | "size" : "40x40", 74 | "filename" : "icon-20@2x.png", 75 | "scale" : "1x" 76 | }, 77 | { 78 | "idiom" : "ipad", 79 | "size" : "40x40", 80 | "filename" : "icon-40@2x.png", 81 | "scale" : "2x" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "size" : "76x76", 86 | "filename" : "icon-76.png", 87 | "scale" : "1x" 88 | }, 89 | { 90 | "idiom" : "ipad", 91 | "size" : "76x76", 92 | "filename" : "icon-76@2x.png", 93 | "scale" : "2x" 94 | }, 95 | { 96 | "idiom" : "ipad", 97 | "size" : "83.5x83.5", 98 | "filename" : "icon-83.5@2x.png", 99 | "scale" : "2x" 100 | }, 101 | { 102 | "idiom" : "ios-marketing", 103 | "size" : "1024x1024", 104 | "filename" : "icon-1024.jpg", 105 | "scale" : "1x" 106 | } 107 | ], 108 | "info" : { 109 | "version" : 1, 110 | "author" : "xcode" 111 | } 112 | } -------------------------------------------------------------------------------- /site/character_page/kink.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | -------------------------------------------------------------------------------- /chat/user_view.ts: -------------------------------------------------------------------------------- 1 | // TODO convert this to single-file once Vue supports it for functional components. 2 | //template: 3 | // {{character.name}} 5 | 6 | import Vue, {CreateElement, RenderContext, VNode} from 'vue'; 7 | import {Channel, Character} from '../fchat'; 8 | import core from './core'; 9 | 10 | export function getStatusIcon(status: Character.Status): string { 11 | switch(status) { 12 | case 'online': 13 | return 'far fa-user'; 14 | case 'looking': 15 | return 'fa fa-eye'; 16 | case 'dnd': 17 | return 'fa fa-minus-circle'; 18 | case 'offline': 19 | return 'fa fa-ban'; 20 | case 'away': 21 | return 'far fa-circle'; 22 | case 'busy': 23 | return 'fa fa-cog'; 24 | case 'idle': 25 | return 'far fa-clock'; 26 | case 'crown': 27 | return 'fa fa-birthday-cake'; 28 | } 29 | } 30 | 31 | //tslint:disable-next-line:variable-name 32 | const UserView = Vue.extend({ 33 | functional: true, 34 | render(this: void | Vue, createElement: CreateElement, context?: RenderContext): VNode { 35 | const props = <{character: Character, channel?: Channel, showStatus?: true, bookmark?: false}>( 36 | context !== undefined ? context.props : (this).$options.propsData); 37 | const character = props.character; 38 | let rankIcon; 39 | if(character.isChatOp) rankIcon = 'far fa-gem'; 40 | else if(props.channel !== undefined) 41 | rankIcon = props.channel.owner === character.name ? 'fa fa-key' : props.channel.opList.indexOf(character.name) !== -1 ? 42 | (props.channel.id.substr(0, 4) === 'adh-' ? 'fa fa-shield-alt' : 'fa fa-star') : ''; 43 | else rankIcon = ''; 44 | const children: (VNode | string)[] = [character.name]; 45 | if(rankIcon !== '') children.unshift(createElement('span', {staticClass: rankIcon})); 46 | if(props.showStatus !== undefined || character.status === 'crown') 47 | children.unshift(createElement('span', {staticClass: `fa-fw ${getStatusIcon(character.status)}`})); 48 | const gender = character.gender !== undefined ? character.gender.toLowerCase() : 'none'; 49 | const isBookmark = props.bookmark !== false && core.connection.isOpen && core.state.settings.colorBookmarks && 50 | (character.isFriend || character.isBookmarked); 51 | return createElement('span', { 52 | attrs: {class: `user-view gender-${gender}${isBookmark ? ' user-bookmark' : ''}`}, 53 | domProps: {character, channel: props.channel, bbcodeTag: 'user'} 54 | }, children); 55 | } 56 | }); 57 | 58 | export default UserView; -------------------------------------------------------------------------------- /mobile/ios/F-Chat/native.js: -------------------------------------------------------------------------------- 1 | var key = 0; 2 | var handlers = {}; 3 | 4 | function sendMessage(handler, type, data) { 5 | return new Promise(function(resolve, reject) { 6 | data._id = 'm' + key++; 7 | data._type = type; 8 | window.webkit.messageHandlers[handler].postMessage(data); 9 | handlers[data._id] = {resolve: resolve, reject: reject}; 10 | }); 11 | } 12 | 13 | window.nativeMessage = function(key, data) { 14 | handlers[key].resolve(data); 15 | delete handlers[key]; 16 | }; 17 | 18 | window.nativeError = function(key, error) { 19 | handlers[key].reject(error); 20 | delete handlers[key]; 21 | }; 22 | 23 | window.NativeFile = { 24 | read: function(name) { 25 | return sendMessage('File', 'read', {name: name}); 26 | }, 27 | write: function(name, data) { 28 | return sendMessage('File', 'write', {name: name, data: data}); 29 | }, 30 | listDirectories: function(name) { 31 | return sendMessage('File', 'listDirectories', {name: name}); 32 | }, 33 | listFiles: function(name) { 34 | return sendMessage('File', 'listFiles', {name: name}); 35 | }, 36 | getSize: function(name) { 37 | return sendMessage('File', 'getSize', {name: name}); 38 | }, 39 | ensureDirectory: function(name) { 40 | return sendMessage('File', 'ensureDirectory', {name: name}); 41 | } 42 | }; 43 | 44 | window.NativeNotification = { 45 | notify: function(notify, title, text, icon, sound, data) { 46 | return sendMessage('Notification', 'notify', {notify: notify, title: title, text: text, icon: icon, sound: sound, data: data}); 47 | }, 48 | requestPermission: function() { 49 | return sendMessage('Notification', 'requestPermission', {}); 50 | } 51 | }; 52 | 53 | window.NativeBackground = { 54 | start: function() { 55 | return sendMessage('Background', 'start', {}); 56 | }, 57 | stop: function(name) { 58 | return sendMessage('Background', 'stop', {}); 59 | } 60 | }; 61 | 62 | window.NativeLogs = { 63 | init: function(character) { 64 | return sendMessage('Logs', 'init', {character: character}); 65 | }, 66 | logMessage: function(key, c, time, type, sender, message) { 67 | return sendMessage('Logs', 'logMessage', {key: key, conversation: c, time: time, type: type, sender: sender, message: message}); 68 | }, 69 | getBacklog: function(key) { 70 | return sendMessage('Logs', 'getBacklog', {key: key}); 71 | }, 72 | getLogs: function(character, key, date) { 73 | return sendMessage('Logs', 'getLogs', {character: character, key: key, date: date}); 74 | }, 75 | loadIndex: function(character) { 76 | return sendMessage('Logs', 'loadIndex', {character: character}); 77 | }, 78 | getCharacters: function() { 79 | return sendMessage('Logs', 'getCharacters', {}); 80 | }, 81 | repair: function(character) { 82 | return sendMessage('Logs', 'repair', {character: character}); 83 | } 84 | }; -------------------------------------------------------------------------------- /chat/StatusSwitcher.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | -------------------------------------------------------------------------------- /mobile/ios/F-Chat/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /chat/message_view.ts: -------------------------------------------------------------------------------- 1 | import {Component, Prop} from '@f-list/vue-ts'; 2 | import {CreateElement, default as Vue, VNode, VNodeChildrenArrayContents} from 'vue'; 3 | import {BBCodeView} from '../bbcode/view'; 4 | import {Channel} from '../fchat'; 5 | import {formatTime} from './common'; 6 | import core from './core'; 7 | import {Conversation} from './interfaces'; 8 | import UserView from './user_view'; 9 | 10 | const userPostfix: {[key: number]: string | undefined} = { 11 | [Conversation.Message.Type.Message]: ': ', 12 | [Conversation.Message.Type.Ad]: ': ', 13 | [Conversation.Message.Type.Action]: '' 14 | }; 15 | @Component({ 16 | render(this: MessageView, createElement: CreateElement): VNode { 17 | const message = this.message; 18 | const children: VNodeChildrenArrayContents = 19 | [createElement('span', {staticClass: 'message-time'}, `[${formatTime(message.time)}] `)]; 20 | const separators = core.connection.isOpen ? core.state.settings.messageSeparators : false; 21 | /*tslint:disable-next-line:prefer-template*///unreasonable here 22 | let classes = `message message-${Conversation.Message.Type[message.type].toLowerCase()}` + (separators ? ' message-block' : '') + 23 | (message.type !== Conversation.Message.Type.Event && message.sender.name === core.connection.character ? ' message-own' : '') + 24 | ((this.classes !== undefined) ? ` ${this.classes}` : ''); 25 | if(message.type !== Conversation.Message.Type.Event) { 26 | children.push((message.type === Conversation.Message.Type.Action) ? '*' : '', 27 | createElement(UserView, {props: {character: message.sender, channel: this.channel}}), 28 | userPostfix[message.type] !== undefined ? userPostfix[message.type]! : ' '); 29 | if(message.isHighlight) classes += ' message-highlight'; 30 | } 31 | const isAd = message.type === Conversation.Message.Type.Ad && !this.logs; 32 | children.push(createElement(BBCodeView(core.bbCodeParser), 33 | {props: {unsafeText: message.text, afterInsert: isAd ? (elm: HTMLElement) => { 34 | setImmediate(() => { 35 | elm = elm.parentElement!; 36 | if(elm.scrollHeight > elm.offsetHeight) { 37 | const expand = document.createElement('div'); 38 | expand.className = 'expand fas fa-caret-down'; 39 | expand.addEventListener('click', function(): void { this.parentElement!.className += ' expanded'; }); 40 | elm.appendChild(expand); 41 | } 42 | }); 43 | } : undefined}})); 44 | const node = createElement('div', {attrs: {class: classes}}, children); 45 | node.key = message.id; 46 | return node; 47 | } 48 | }) 49 | export default class MessageView extends Vue { 50 | @Prop({required: true}) 51 | readonly message!: Conversation.Message; 52 | @Prop 53 | readonly classes?: string; 54 | @Prop 55 | readonly channel?: Channel; 56 | @Prop 57 | readonly logs?: true; 58 | } -------------------------------------------------------------------------------- /site/utils.ts: -------------------------------------------------------------------------------- 1 | import Axios, {AxiosError, AxiosResponse} from 'axios'; 2 | import {InlineDisplayMode, Settings, SimpleCharacter} from '../interfaces'; 3 | 4 | type FlashMessageType = 'info' | 'success' | 'warning' | 'danger'; 5 | type FlashMessageImpl = (type: FlashMessageType, message: string) => void; 6 | 7 | let flashImpl: FlashMessageImpl = (type: FlashMessageType, message: string) => { 8 | console.log(`${type}: ${message}`); 9 | }; 10 | 11 | export function setFlashMessageImplementation(impl: FlashMessageImpl): void { 12 | flashImpl = impl; 13 | } 14 | 15 | export function avatarURL(name: string): string { 16 | const uregex = /^[a-zA-Z0-9_\-\s]+$/; 17 | if(!uregex.test(name)) return '#'; 18 | return `${staticDomain}images/avatar/${name.toLowerCase()}.png`; 19 | } 20 | 21 | export function characterURL(name: string): string { 22 | const uregex = /^[a-zA-Z0-9_\-\s]+$/; 23 | if(!uregex.test(name)) return '#'; 24 | return `${siteDomain}c/${name}`; 25 | } 26 | 27 | //tslint:disable-next-line:no-any 28 | export function isJSONError(error: any): error is Error & {response: AxiosResponse<{[key: string]: object | string | number}>} { 29 | return (error).response !== undefined && typeof (error).response!.data === 'object'; 30 | } 31 | 32 | export function ajaxError(error: any, prefix: string, showFlashMessage: boolean = true): void { //tslint:disable-line:no-any 33 | let message: string | undefined; 34 | if(error instanceof Error) { 35 | if(Axios.isCancel(error)) return; 36 | 37 | if(isJSONError(error)) { 38 | const data = <{error?: string | string[]}>error.response.data; 39 | if(typeof (data.error) === 'string') 40 | message = data.error; 41 | else if(typeof (data.error) === 'object' && data.error.length > 0) 42 | message = data.error[0]; 43 | } 44 | if(message === undefined) 45 | message = (error).response !== undefined ? 46 | (error).response.statusText : error.name; 47 | } else message = error; 48 | console.error(error); 49 | if(showFlashMessage) flashError(`[ERROR] ${prefix}: ${message}`); 50 | } 51 | 52 | export function flashError(message: string): void { 53 | flashMessage('danger', message); 54 | } 55 | 56 | export function flashSuccess(message: string): void { 57 | flashMessage('success', message); 58 | } 59 | 60 | export function flashMessage(type: FlashMessageType, message: string): void { 61 | flashImpl(type, message); 62 | } 63 | 64 | export let siteDomain = ''; 65 | export let staticDomain = ''; 66 | 67 | export let settings: Settings = { 68 | animateEicons: true, 69 | inlineDisplayMode: InlineDisplayMode.DISPLAY_ALL, 70 | defaultCharacter: -1, 71 | fuzzyDates: true 72 | }; 73 | 74 | export let characters: SimpleCharacter[] = []; 75 | 76 | export function setDomains(site: string, stat: string): void { 77 | siteDomain = site; 78 | staticDomain = stat; 79 | } 80 | 81 | export function init(s: Settings, c: SimpleCharacter[]): void { 82 | settings = s; 83 | characters = c; 84 | } -------------------------------------------------------------------------------- /chat/vue-raven.ts: -------------------------------------------------------------------------------- 1 | import * as Raven from 'raven-js'; 2 | import Vue from 'vue'; 3 | 4 | /*tslint:disable:no-unsafe-any no-any*///hack 5 | function formatComponentName(vm: any): string { 6 | if(vm === undefined) return 'undefined'; 7 | if(vm.$root === vm) return ''; 8 | const name = vm._isVue 9 | ? vm.$options.name || vm.$options._componentTag 10 | : vm.name; 11 | return (name ? `component <${name}>` : 'anonymous component') + (vm._isVue && vm.$options.__file ? ` at ${vm.$options.__file}` : ''); 12 | } 13 | //tslint:enable 14 | 15 | /*tslint:disable:no-unbound-method strict-type-predicates*///hack 16 | function VueRaven(this: void, raven: Raven.RavenStatic): Raven.RavenStatic { 17 | if(typeof Vue.config !== 'object') return raven; 18 | const oldOnError = Vue.config.errorHandler; 19 | Vue.config.errorHandler = (error: Error, vm: Vue, info: string): void => { 20 | raven.captureException(error, { 21 | extra: { 22 | componentName: formatComponentName(vm), 23 | //propsData: vm.$options.propsData, 24 | info 25 | } 26 | }); 27 | 28 | if(typeof oldOnError === 'function') oldOnError.call(this, error, vm, info); 29 | else console.log(error); 30 | }; 31 | 32 | const oldOnWarn = Vue.config.warnHandler; 33 | Vue.config.warnHandler = (message: string, vm: Vue, trace: string): void => { 34 | raven.captureMessage(message + trace, { 35 | extra: { 36 | componentName: formatComponentName(vm) 37 | //propsData: vm.$options.propsData 38 | } 39 | }); 40 | console.warn(`${message}: ${trace}`); 41 | if(typeof oldOnWarn === 'function') 42 | oldOnWarn.call(this, message, vm, trace); 43 | }; 44 | 45 | return raven; 46 | } 47 | //tslint:enable 48 | 49 | export function setupRaven(dsn: string, version: string): void { 50 | return; //TODO sentry temporarily disabled 51 | Raven.config(dsn, { 52 | release: version, 53 | dataCallback: (data: {culprit?: string, exception?: {values: {stacktrace: {frames: {filename: string}[]}}[]}}) => { 54 | if(data.culprit !== undefined) { 55 | const end = data.culprit.lastIndexOf('?'); 56 | data.culprit = `~${data.culprit.substring(data.culprit.lastIndexOf('/'), end === -1 ? undefined : end)}`; 57 | } 58 | if(data.exception !== undefined) 59 | for(const ex of data.exception.values) 60 | for(const frame of ex.stacktrace.frames) { 61 | const index = frame.filename.lastIndexOf('/'); 62 | const endIndex = frame.filename.lastIndexOf('?'); 63 | frame.filename = 64 | `~${frame.filename.substring(index !== -1 ? index : 0, endIndex === -1 ? undefined : endIndex)}`; 65 | } 66 | } 67 | }).addPlugin(VueRaven, Vue).install(); 68 | (window).onunhandledrejection = (e: PromiseRejectionEvent) => { 69 | Raven.captureException(e.reason); 70 | }; 71 | } -------------------------------------------------------------------------------- /mobile/ios/F-Chat/Notification.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import UserNotifications 3 | import WebKit 4 | import AVFoundation 5 | 6 | class Notification: NSObject, WKScriptMessageHandler, UNUserNotificationCenterDelegate { 7 | let center = UNUserNotificationCenter.current() 8 | let baseDir = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask).first! 9 | var webView: WKWebView! 10 | 11 | func userContentController(_ controller: WKUserContentController, didReceive message: WKScriptMessage) { 12 | center.delegate = self 13 | self.webView = message.webView 14 | let data = message.body as! [String: AnyObject] 15 | let key = data["_id"] as! String 16 | let callback = { (result: String?) in 17 | let output = result == nil ? "undefined" : "'\(result!)'"; 18 | DispatchQueue.main.async { 19 | message.webView!.evaluateJavaScript("nativeMessage('\(key)',\(output))") 20 | } 21 | } 22 | switch(data["_type"] as! String) { 23 | case "notify": 24 | notify(data["notify"] as! Bool, data["title"] as! String, data["text"] as! String, data["icon"] as! String, data["sound"] as? String, data["data"] as! String, callback) 25 | case "requestPermission": 26 | requestPermission(callback) 27 | default: 28 | message.webView!.evaluateJavaScript("nativeError('\(key)',new Error('Unknown message type'))") 29 | return 30 | } 31 | } 32 | 33 | func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) { 34 | if(response.actionIdentifier == UNNotificationDefaultActionIdentifier) { 35 | webView.evaluateJavaScript("document.dispatchEvent(new CustomEvent('notification-clicked',{detail:{data:'\(response.notification.request.content.userInfo["data"]!)'}}))") 36 | } 37 | completionHandler() 38 | } 39 | 40 | func notify(_ notify: Bool, _ title: String, _ text: String, _ icon: String, _ sound: String?, _ data: String, _ cb: (String?) -> Void) { 41 | if(!notify) { 42 | if(sound != nil) { 43 | let player = try! AVAudioPlayer(contentsOf: Bundle.main.url(forResource: "www/sounds/" + sound!, withExtension: "wav")!) 44 | player.play() 45 | } 46 | cb(nil) 47 | return 48 | } 49 | let content = UNMutableNotificationContent() 50 | content.title = title 51 | if(sound != nil) { 52 | content.sound = UNNotificationSound(named: UNNotificationSoundName(Bundle.main.path(forResource: "www/sounds/" + sound!, ofType: "wav")!)) 53 | } 54 | content.body = text 55 | content.userInfo["data"] = data 56 | center.add(UNNotificationRequest(identifier: "1", content: content, trigger: UNTimeIntervalNotificationTrigger.init(timeInterval: 1, repeats: false))) 57 | cb("1"); 58 | } 59 | 60 | func requestPermission(_ cb: @escaping (String?) -> Void) { 61 | center.requestAuthorization(options: [.alert, .sound]) { (_, _) in 62 | cb(nil) 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /mobile/android/app/src/main/kotlin/net/f_list/fchat/Notifications.kt: -------------------------------------------------------------------------------- 1 | package net.f_list.fchat 2 | 3 | import android.app.Notification 4 | import android.app.NotificationChannel 5 | import android.app.NotificationManager 6 | import android.app.PendingIntent 7 | import android.content.Context 8 | import android.content.Intent 9 | import android.graphics.Bitmap 10 | import android.graphics.BitmapFactory 11 | import android.media.AudioAttributes 12 | import android.media.AudioManager 13 | import android.media.MediaPlayer 14 | import android.os.AsyncTask 15 | import android.os.Build 16 | import android.os.Vibrator 17 | import android.provider.Settings 18 | import android.webkit.JavascriptInterface 19 | import java.net.URL 20 | 21 | class Notifications(private val ctx: Context) { 22 | init { 23 | if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { 24 | val manager = ctx.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager; 25 | manager.createNotificationChannel(NotificationChannel("messages", ctx.getString(R.string.channel_messages), NotificationManager.IMPORTANCE_HIGH)) 26 | } 27 | } 28 | 29 | @JavascriptInterface 30 | fun notify(notify: Boolean, title: String, text: String, icon: String, sound: String?, data: String?): Int { 31 | if(sound != null) { 32 | val player = MediaPlayer() 33 | val asset = ctx.assets.openFd("www/sounds/$sound.mp3") 34 | player.setDataSource(asset.fileDescriptor, asset.startOffset, asset.length) 35 | player.setAudioStreamType(AudioManager.STREAM_NOTIFICATION) 36 | player.prepare() 37 | player.start() 38 | player.setOnCompletionListener { it.release() } 39 | } 40 | if(!notify) { 41 | if((ctx.getSystemService(Context.AUDIO_SERVICE) as AudioManager).ringerMode != AudioManager.RINGER_MODE_SILENT) { 42 | val vibrator = (ctx.getSystemService(Context.VIBRATOR_SERVICE) as Vibrator) 43 | if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) 44 | vibrator.vibrate(400, Notification.AUDIO_ATTRIBUTES_DEFAULT) 45 | else vibrator.vibrate(400) 46 | } 47 | return 0 48 | } 49 | val intent = Intent(ctx, MainActivity::class.java) 50 | intent.action = "notification" 51 | intent.putExtra("data", data) 52 | val notification = Notification.Builder(ctx).setContentTitle(title).setContentText(text).setSmallIcon(R.drawable.ic_notification).setAutoCancel(true) 53 | .setContentIntent(PendingIntent.getActivity(ctx, 1, intent, PendingIntent.FLAG_UPDATE_CURRENT)).setDefaults(Notification.DEFAULT_VIBRATE or Notification.DEFAULT_LIGHTS) 54 | if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) notification.setChannelId("messages") 55 | object : AsyncTask() { 56 | override fun doInBackground(vararg args: String): Bitmap? { 57 | return try { 58 | val connection = URL(args[0]).openConnection() 59 | BitmapFactory.decodeStream(connection.getInputStream()) 60 | } catch(e: Exception) { 61 | null 62 | } 63 | } 64 | 65 | override fun onPostExecute(result: Bitmap?) { 66 | notification.setLargeIcon(result) 67 | (ctx.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager).notify(2, notification.build()) 68 | } 69 | }.execute(icon) 70 | return 2 71 | } 72 | 73 | @JavascriptInterface 74 | fun requestPermission() { 75 | 76 | } 77 | } -------------------------------------------------------------------------------- /site/character_page/copy_custom_dialog.vue: -------------------------------------------------------------------------------- 1 | 24 | 25 | -------------------------------------------------------------------------------- /site/character_page/duplicate_dialog.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | -------------------------------------------------------------------------------- /site/character_page/context_menu.ts: -------------------------------------------------------------------------------- 1 | import {Component} from '@f-list/vue-ts'; 2 | import Vue from 'vue'; 3 | 4 | @Component 5 | export default abstract class ContextMenu extends Vue { 6 | abstract propName: string; 7 | showMenu = false; 8 | position = {left: 0, top: 0}; 9 | selectedItem: HTMLElement | undefined; 10 | touchTimer = 0; 11 | 12 | abstract itemSelected(element: HTMLElement): void; 13 | 14 | shouldShowMenu(_: HTMLElement): boolean { 15 | return true; 16 | } 17 | 18 | hideMenu(): void { 19 | this.showMenu = false; 20 | this.selectedItem = undefined; 21 | } 22 | 23 | bindOffclick(): void { 24 | document.body.addEventListener('click', () => this.hideMenu()); 25 | } 26 | 27 | private fixPosition(e: MouseEvent | Touch): void { 28 | const getMenuPosition = (input: number, direction: string): number => { 29 | const win = (window)[`inner${direction}`]; 30 | const menu = (this.$refs['menu'])[`offset${direction}`]; 31 | let position = input; 32 | 33 | if(input + menu > win) 34 | position = win - menu - 5; 35 | 36 | return position; 37 | }; 38 | 39 | const left = getMenuPosition(e.clientX, 'Width'); 40 | const top = getMenuPosition(e.clientY, 'Height'); 41 | this.position = {left, top}; 42 | } 43 | 44 | innerClick(): void { 45 | this.itemSelected(this.selectedItem!); 46 | this.hideMenu(); 47 | } 48 | 49 | outerClick(event: MouseEvent | TouchEvent): void { 50 | // Provide an opt-out 51 | if(event.ctrlKey) return; 52 | if(event.type === 'touchend') window.clearTimeout(this.touchTimer); 53 | const targetingEvent = event instanceof TouchEvent ? event.touches[0] : event; 54 | const findTarget = (): HTMLElement | undefined => { 55 | let element = targetingEvent.target; 56 | while(element !== document.body) { 57 | if(typeof element.dataset[this.propName] !== 'undefined' || element.parentElement === null) break; 58 | element = element.parentElement; 59 | } 60 | return typeof element.dataset[this.propName] === 'undefined' ? undefined : element; 61 | }; 62 | const target = findTarget(); 63 | if(target === undefined) { 64 | this.hideMenu(); 65 | return; 66 | } 67 | switch(event.type) { 68 | case 'click': 69 | case 'contextmenu': 70 | this.openMenu(targetingEvent, target); 71 | break; 72 | case 'touchstart': 73 | this.touchTimer = window.setTimeout(() => this.openMenu(targetingEvent, target), 500); 74 | } 75 | event.preventDefault(); 76 | } 77 | 78 | private openMenu(event: MouseEvent | Touch, element: HTMLElement): void { 79 | if(!this.shouldShowMenu(element)) 80 | return; 81 | this.showMenu = true; 82 | this.selectedItem = element; 83 | this.$nextTick(() => { 84 | this.fixPosition(event); 85 | }); 86 | } 87 | 88 | get positionStyle(): object { 89 | return {left: `${this.position.left}px`, top: `${this.position.top}px;`}; 90 | } 91 | 92 | } -------------------------------------------------------------------------------- /components/simple_pager.vue: -------------------------------------------------------------------------------- 1 | 25 | 26 | -------------------------------------------------------------------------------- /chat/notifications.ts: -------------------------------------------------------------------------------- 1 | import core from './core'; 2 | import {Conversation, Notifications as Interface} from './interfaces'; 3 | 4 | const codecs: {[key: string]: string} = {mpeg: 'mp3', wav: 'wav', ogg: 'ogg'}; 5 | 6 | export default class Notifications implements Interface { 7 | isInBackground = false; 8 | 9 | protected shouldNotify(conversation: Conversation): boolean { 10 | return core.characters.ownCharacter.status !== 'dnd' && (this.isInBackground || 11 | conversation !== core.conversations.selectedConversation || core.state.settings.alwaysNotify); 12 | } 13 | 14 | async notify(conversation: Conversation, title: string, body: string, icon: string, sound: string): Promise { 15 | if(!this.shouldNotify(conversation)) return; 16 | this.playSound(sound); 17 | if(core.state.settings.notifications && (<{Notification?: object}>window).Notification !== undefined 18 | && Notification.permission === 'granted') { 19 | const notification = new Notification(title, this.getOptions(conversation, body, icon)); 20 | notification.onclick = () => { 21 | conversation.show(); 22 | window.focus(); 23 | if('close' in notification) notification.close(); 24 | }; 25 | if('close' in notification) window.setTimeout(() => notification.close(), 5000); 26 | } 27 | } 28 | 29 | getOptions(conversation: Conversation, body: string, icon: string): 30 | NotificationOptions & {badge: string, silent: boolean, renotify: boolean} { 31 | const badge = require(`./assets/ic_notification.png`); //tslint:disable-line:no-require-imports 32 | return { 33 | body, icon: core.state.settings.showAvatars ? icon : undefined, badge, silent: true, data: {key: conversation.key}, 34 | tag: conversation.key, renotify: true 35 | }; 36 | } 37 | 38 | playSound(sound: string): void { 39 | if(!core.state.settings.playSound) return; 40 | const audio = document.getElementById(`soundplayer-${sound}`); 41 | audio.volume = 1; 42 | audio.muted = false; 43 | const promise = audio.play(); 44 | if(promise instanceof Promise) promise.catch((e) => console.error(e)); 45 | } 46 | 47 | async initSounds(sounds: ReadonlyArray): Promise { 48 | const promises = []; 49 | for(const sound of sounds) { 50 | const id = `soundplayer-${sound}`; 51 | if(document.getElementById(id) !== null) continue; 52 | const audio = document.createElement('audio'); 53 | audio.preload = 'auto'; 54 | audio.id = id; 55 | for(const name in codecs) { 56 | const src = document.createElement('source'); 57 | src.type = `audio/${name}`; 58 | //tslint:disable-next-line:no-require-imports 59 | src.src = require(`./assets/${sound}.${codecs[name]}`); 60 | audio.appendChild(src); 61 | } 62 | document.body.appendChild(audio); 63 | audio.volume = 0; 64 | audio.muted = true; 65 | const promise = audio.play(); 66 | if(promise instanceof Promise) 67 | promises.push(promise.catch((e) => console.error(e))); 68 | } 69 | return Promise.all(promises); //tslint:disable-line:no-any 70 | } 71 | 72 | async requestPermission(): Promise { 73 | if((<{Notification?: object}>window).Notification !== undefined) await Notification.requestPermission(); 74 | } 75 | } -------------------------------------------------------------------------------- /bbcode/editor.ts: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import {Keys} from '../keys'; 3 | 4 | export interface EditorButton { 5 | title: string; 6 | tag: string; 7 | icon: string; 8 | key?: Keys; 9 | class?: string; 10 | startText?: string; 11 | endText?: string; 12 | handler?(vm: Vue): void; 13 | } 14 | 15 | export interface EditorSelection { 16 | start: number; 17 | end: number; 18 | length: number; 19 | text: string; 20 | } 21 | /*tslint:disable:max-line-length*/ 22 | export let defaultButtons: ReadonlyArray = [ 23 | { 24 | title: 'Bold (Ctrl+B)\n\nMakes text appear with a bold weight.', 25 | tag: 'b', 26 | icon: 'fa-bold', 27 | key: Keys.KeyB 28 | }, 29 | { 30 | title: 'Italic (Ctrl+I)\n\nMakes text appear with an italic style.', 31 | tag: 'i', 32 | icon: 'fa-italic', 33 | key: Keys.KeyI 34 | }, 35 | { 36 | title: 'Underline (Ctrl+U)\n\nMakes text appear with an underline beneath it.', 37 | tag: 'u', 38 | icon: 'fa-underline', 39 | key: Keys.KeyU 40 | }, 41 | { 42 | title: 'Strikethrough (Ctrl+S)\n\nPlaces a horizontal line through the text. Usually used to signify a correction or redaction without omitting the text.', 43 | tag: 's', 44 | icon: 'fa-strikethrough', 45 | key: Keys.KeyS 46 | }, 47 | { 48 | title: 'Color (Ctrl+D)\n\nStyles text with a color. Valid colors are: red, orange, yellow, green, cyan, blue, purple, pink, black, brown, white and gray.', 49 | tag: 'color', 50 | startText: '[color=]', 51 | icon: 'fa-eye-dropper', 52 | key: Keys.KeyD 53 | }, 54 | { 55 | title: 'Superscript (Ctrl+↑)\n\nLifts text above the text baseline. Makes text slightly smaller. Cannot be nested.', 56 | tag: 'sup', 57 | icon: 'fa-superscript', 58 | key: Keys.ArrowUp 59 | }, 60 | { 61 | title: 'Subscript (Ctrl+↓)\n\nPushes text below the text baseline. Makes text slightly smaller. Cannot be nested.', 62 | tag: 'sub', 63 | icon: 'fa-subscript', 64 | key: Keys.ArrowDown 65 | }, 66 | { 67 | title: 'URL (Ctrl+L)\n\nCreates a clickable link to another page of your choosing.', 68 | tag: 'url', 69 | startText: '[url=]', 70 | icon: 'fa-link', 71 | key: Keys.KeyL 72 | }, 73 | { 74 | title: 'User (Ctrl+R)\n\nLinks to a character\'s profile.', 75 | tag: 'user', 76 | icon: 'fa-user', 77 | key: Keys.KeyR 78 | }, 79 | { 80 | title: 'Icon (Ctrl+O)\n\nShows a character\'s profile image, linking to their profile.', 81 | tag: 'icon', 82 | icon: 'fa-user-circle', 83 | key: Keys.KeyO 84 | }, 85 | { 86 | title: 'EIcon (Ctrl+E)\n\nShows a previously uploaded eicon. If the icon is a gif, it will be shown as animated unless a user has turned this off.', 87 | tag: 'eicon', 88 | class: 'far ', 89 | icon: 'fa-smile', 90 | key: Keys.KeyE 91 | }, 92 | { 93 | title: 'Spoiler (Ctrl+K)\n\nHidden until explicitly clicked by the viewer.', 94 | tag: 'spoiler', 95 | icon: 'fa-eye-slash', 96 | key: Keys.KeyK 97 | }, 98 | { 99 | title: 'Noparse (Ctrl+N)\n\nAll BBCode placed within this tag will be ignored and treated as text. Great for sharing structure without it being rendered.', 100 | tag: 'noparse', 101 | icon: 'fa-ban', 102 | key: Keys.KeyN 103 | } 104 | ]; -------------------------------------------------------------------------------- /interfaces.ts: -------------------------------------------------------------------------------- 1 | export interface SimpleCharacter { 2 | id: number 3 | name: string 4 | deleted: boolean 5 | } 6 | 7 | export interface InlineImage { 8 | id: number 9 | name: string 10 | hash: string 11 | extension: string 12 | nsfw: boolean 13 | } 14 | 15 | export type CharacterImage = CharacterImageOld | CharacterImageNew; 16 | 17 | export interface CharacterImageNew { 18 | id: number 19 | extension: string 20 | description: string 21 | hash: string 22 | sort_order: number | null 23 | } 24 | 25 | export interface CharacterImageOld { 26 | id: number 27 | extension: string 28 | hash: string 29 | height: number 30 | width: number 31 | description: string 32 | sort_order: number | null 33 | url: string 34 | } 35 | 36 | export type InfotagType = 'number' | 'text' | 'list'; 37 | 38 | export interface CharacterInfotag { 39 | list?: number 40 | string?: string 41 | number?: number 42 | } 43 | 44 | export interface Infotag { 45 | id: number 46 | name: string 47 | type: InfotagType 48 | search_field: string 49 | validator?: string 50 | allow_legacy: boolean 51 | infotag_group: number 52 | } 53 | 54 | export interface Character extends SimpleCharacter { 55 | id: number 56 | name: string 57 | title: string 58 | description: string 59 | kinks: {[key: number]: KinkChoice | number | undefined} 60 | inlines: {[key: string]: InlineImage} 61 | customs: {[key: string]: CustomKink | undefined} 62 | infotags: {[key: number]: CharacterInfotag | undefined} 63 | created_at: number 64 | updated_at: number 65 | views: number 66 | last_online_at?: number 67 | timezone?: number 68 | image_count?: number 69 | online_chat?: boolean 70 | } 71 | 72 | export type KinkChoice = 'favorite' | 'yes' | 'maybe' | 'no'; 73 | 74 | export interface CharacterSettings { 75 | readonly customs_first: boolean 76 | readonly show_friends: boolean 77 | readonly show_badges: boolean 78 | readonly guestbook: boolean 79 | readonly block_bookmarks: boolean 80 | readonly public: boolean 81 | readonly moderate_guestbook: boolean 82 | readonly hide_timezone: boolean 83 | readonly hide_contact_details: boolean 84 | } 85 | 86 | export interface Kink { 87 | id: number 88 | name: string 89 | description: string 90 | kink_group: number 91 | } 92 | 93 | export interface CustomKink { 94 | id: number 95 | name: string 96 | choice: KinkChoice 97 | description: string 98 | } 99 | 100 | export interface KinkGroup { 101 | id: number 102 | name: string 103 | description: string 104 | sort_order: number 105 | } 106 | 107 | export interface InfotagGroup { 108 | id: number 109 | name: string 110 | description: string 111 | sort_order: number 112 | } 113 | 114 | export interface ListItem { 115 | id: number 116 | name: string 117 | value: string 118 | sort_order: number 119 | } 120 | 121 | export const enum InlineDisplayMode {DISPLAY_ALL, DISPLAY_SFW, DISPLAY_NONE} 122 | 123 | export interface Settings { 124 | animateEicons: boolean 125 | inlineDisplayMode: InlineDisplayMode 126 | defaultCharacter: number 127 | fuzzyDates: boolean 128 | } 129 | 130 | export interface SharedDefinitions { 131 | readonly listItems: {readonly [key: string]: Readonly} 132 | readonly kinks: {readonly [key: string]: Readonly} 133 | readonly kinkGroups: {readonly [key: string]: Readonly} 134 | readonly infotags: {readonly [key: string]: Readonly} 135 | readonly infotagGroups: {readonly [key: string]: Readonly} 136 | } -------------------------------------------------------------------------------- /mobile/ios/F-Chat/File.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import WebKit 3 | 4 | class File: NSObject, WKScriptMessageHandler { 5 | let fm = FileManager.default; 6 | let baseDir = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask).first! 7 | static let escapeRegex = try! NSRegularExpression(pattern: "([`$\\\\])", options: [.caseInsensitive]) 8 | 9 | override init() { 10 | super.init(); 11 | try! fm.createDirectory(at: baseDir, withIntermediateDirectories: true, attributes: nil) 12 | } 13 | 14 | static func escape(_ str: String) -> String { 15 | return "`" + escapeRegex.stringByReplacingMatches(in: str, range: NSMakeRange(0, str.count), withTemplate: "\\\\$1") + "`" 16 | } 17 | 18 | func userContentController(_ controller: WKUserContentController, didReceive message: WKScriptMessage) { 19 | let data = message.body as! [String: AnyObject] 20 | let key = data["_id"] as! String 21 | do { 22 | var result: String? 23 | switch(data["_type"] as! String) { 24 | case "read": 25 | result = try read(data["name"] as! String) 26 | case "write": 27 | try write(data["name"] as! String, data["data"] as! String) 28 | case "listDirectories": 29 | result = try list(data["name"] as! String, directories: true) 30 | case "listFiles": 31 | result = try list(data["name"] as! String, directories: false) 32 | case "getSize": 33 | result = try getSize(data["name"] as! String) 34 | case "ensureDirectory": 35 | try ensureDirectory(data["name"] as! String) 36 | default: 37 | message.webView!.evaluateJavaScript("nativeError('\(key)',new Error('Unknown message type'))") 38 | return 39 | } 40 | let output = result == nil ? "undefined" : result!; 41 | message.webView!.evaluateJavaScript("nativeMessage('\(key)',\(output))") 42 | } catch(let error) { 43 | message.webView!.evaluateJavaScript("nativeError('\(key)',new Error('File-\(data["_type"]!): \(error.localizedDescription)'))") 44 | } 45 | } 46 | 47 | func read(_ name: String) throws -> String? { 48 | let url = baseDir.appendingPathComponent(name, isDirectory: false) 49 | if(!fm.fileExists(atPath: url.path)) { return nil; } 50 | return File.escape(try String(contentsOf: url, encoding: .utf8)) 51 | } 52 | 53 | func write(_ name: String, _ data: String) throws { 54 | try data.write(to: baseDir.appendingPathComponent(name, isDirectory: false), atomically: true, encoding: .utf8) 55 | } 56 | 57 | func list(_ name: String, directories: Bool) throws -> String { 58 | let url = baseDir.appendingPathComponent(name, isDirectory: true) 59 | let entries = try fm.contentsOfDirectory(at: url, includingPropertiesForKeys: nil, options: [.skipsHiddenFiles]).filter { 60 | try $0.resourceValues(forKeys: [.isDirectoryKey]).isDirectory == directories 61 | }.map { $0.lastPathComponent } 62 | return String(data: try JSONSerialization.data(withJSONObject: entries), encoding: .utf8)!; 63 | } 64 | 65 | func getSize(_ name: String) throws -> String { 66 | let path = baseDir.appendingPathComponent(name, isDirectory: false).path; 67 | if(!fm.fileExists(atPath: path)) { return "0"; } 68 | return String(try fm.attributesOfItem(atPath: path)[.size] as! UInt64) 69 | } 70 | 71 | func ensureDirectory(_ name: String) throws { 72 | try fm.createDirectory(at: baseDir.appendingPathComponent(name, isDirectory: true), withIntermediateDirectories: true, attributes: nil) 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /webchat/chat.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * MIT License 4 | * 5 | * Copyright (c) 2018 F-List 6 | * 7 | * Permission is hereby granted, free of charge, to any person obtaining a copy 8 | * of this software and associated documentation files (the "Software"), to deal 9 | * in the Software without restriction, including without limitation the rights 10 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | * copies of the Software, and to permit persons to whom the Software is 12 | * furnished to do so, subject to the following conditions: 13 | * 14 | * The above copyright notice and this permission notice shall be included in all 15 | * copies or substantial portions of the Software. 16 | * 17 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | * SOFTWARE. 24 | * 25 | * This license header applies to this file and all of the non-third-party assets it includes. 26 | * @file The entry point for the web version of F-Chat 3.0. 27 | * @copyright 2018 F-List 28 | * @author Maya Wolf 29 | * @version 3.0 30 | * @see {@link https://github.com/f-list/exported|GitHub repo} 31 | */ 32 | import Axios from 'axios'; 33 | import Chat from '../chat/Chat.vue'; 34 | import {init as initCore} from '../chat/core'; 35 | import l from '../chat/localize'; 36 | import {setupRaven} from '../chat/vue-raven'; 37 | import Socket from '../chat/WebSocket'; 38 | import Connection from '../fchat/connection'; 39 | import {SimpleCharacter} from '../interfaces'; 40 | import '../scss/fa.scss'; //tslint:disable-line:no-import-side-effect 41 | import {Logs, SettingsStore} from './logs'; 42 | import Notifications from './notifications'; 43 | 44 | if(typeof (<{Promise?: object}>window).Promise !== 'function') //tslint:disable-line:strict-type-predicates 45 | alert('Your browser is too old to be supported by F-Chat 3.0. Please update to a newer version.'); 46 | 47 | const version = (<{version: string}>require('./package.json')).version; //tslint:disable-line:no-require-imports 48 | Axios.defaults.params = { __fchat: `web/${version}` }; 49 | 50 | if(process.env.NODE_ENV === 'production') 51 | setupRaven('https://a9239b17b0a14f72ba85e8729b9d1612@sentry.f-list.net/2', `web-${version}`); 52 | 53 | declare const chatSettings: {account: string, theme: string, characters: ReadonlyArray, defaultCharacter: number | null}; 54 | 55 | const ticketProvider = async() => { 56 | const data = (await Axios.post<{ticket?: string, error: string}>( 57 | '/json/getApiTicket.php?no_friends=true&no_bookmarks=true&no_characters=true')).data; 58 | if(data.ticket !== undefined) return data.ticket; 59 | throw new Error(data.error); 60 | }; 61 | 62 | const connection = new Connection('F-Chat 3.0 (Web)', version, Socket); 63 | connection.setCredentials(chatSettings.account, ticketProvider); 64 | initCore(connection, Logs, SettingsStore, Notifications); 65 | 66 | window.addEventListener('beforeunload', (e) => { 67 | if(!connection.isOpen) return; 68 | e.returnValue = l('chat.confirmLeave'); 69 | return l('chat.confirmLeave'); 70 | }); 71 | 72 | require(`!style-loader?{"attrs":{"id":"themeStyle"}}!css-loader!sass-loader!../scss/themes/chat/${chatSettings.theme}.scss`); 73 | 74 | new Chat({ //tslint:disable-line:no-unused-expression 75 | el: '#app', 76 | propsData: {ownCharacters: chatSettings.characters, defaultCharacter: chatSettings.defaultCharacter, version} 77 | }); 78 | -------------------------------------------------------------------------------- /components/FilterableSelect.vue: -------------------------------------------------------------------------------- 1 | 24 | 25 | 83 | 84 | -------------------------------------------------------------------------------- /chat/ReportDialog.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | --------------------------------------------------------------------------------