'use strict' ; import { app , ipcMain , shell , session , Menu , Tray , BrowserWindow , BrowserWindowConstructorOptions , MenuItemConstructorOptions , IpcMainEvent , Notification , NotificationConstructorOptions , nativeTheme , IpcMainInvokeEvent } from 'electron' ; import Datastore from 'nedb' ; import { isEmpty } from 'lodash' ; import log from 'electron-log' ; import windowStateKeeper from 'electron-window-state' ; import simplayer from 'simplayer' ; import path from 'path' ; import ContextMenu from 'electron-context-menu' ; import { initSplashScreen , Config } from '@trodi/electron-splashscreen' ; import openAboutWindow from 'about-window' ; import { Entity , detector , NotificationType } from 'megalodon' ; import sanitizeHtml from 'sanitize-html' ; import AutoLaunch from 'auto-launch' ; import minimist from 'minimist' ; import Authentication from './auth' ; import Account from './account' ; import { StreamingURL , UserStreaming , DirectStreaming , LocalStreaming , PublicStreaming , ListStreaming , TagStreaming } from './websocket' ; import Preferences from './preferences' ; import Fonts from './fonts' ; import Hashtags from './hashtags' ; import i18next from '~/src/config/i18n' ; import { i18n as I18n } from 'i18next' ; import Language , { LanguageType } from '../constants/language' ; import { LocalAccount } from '~/src/types/localAccount' ; import { LocalTag } from '~/src/types/localTag' ; import { Notify } from '~/src/types/notify' ; import { StreamingError } from '~/src/errors/streamingError' ; import HashtagCache from './cache/hashtag' ; import AccountCache from './cache/account' ; import { InsertAccountCache } from '~/src/types/insertAccountCache' ; import { Proxy } from '~/src/types/proxy' ; import ProxyConfiguration from './proxy' ; import confirm from './timelines' ; import { EnabledTimelines } from '~/src/types/enabledTimelines' ; import { Menu as MenuPreferences } from '~/src/types/preference' ; import { LocalMarker } from '~/src/types/localMarker' ; import Marker from './marker' ; import newDB from './database' ; import Settings from './settings' ; import { BaseSettings , Setting } from '~/src/types/setting' ; /**
 * Context menu
 */ ContextMenu ( { showCopyImageAddress : true , showSaveImageAs : true } ) ; /**
 * Set log level
 */ log . transports . console . level = 'debug' ; log . transports . file . level = 'info' ; declare namespace global { let __static ; } /**
 * Set `__static` path to static files in production
 * https://simulatedgreg.gitbooks.io/electron-vue/content/en/using-static-assets.html
 */ if ( process . env . NODE_ENV !== 'development' ) { global . __static = path . join ( __dirname , '/static' ) . replace ( /\\/g , '\\\\' ) ; } let mainWindow ; let tray ; const winURL = process . env . NODE_ENV === 'development' ? `http://localhost:9080` : path . join ( 'file://' , __dirname , '/index.html' ) ; if ( process . platform !== 'darwin' ) { const gotTheLock = app . requestSingleInstanceLock ( ) ; if ( ! gotTheLock ) { app . quit ( ) ; } else { app . on ( 'second-instance' , ( ) => { if ( mainWindow ) { if ( mainWindow . isMinimized ( ) ) mainWindow . restore ( ) ; if ( ! mainWindow ! . isVisible ( ) ) { mainWindow ! . show ( ) ; mainWindow ! . setSkipTaskbar ( false ) ; } mainWindow . focus ( ) ; } } ) ; } } const appId = 'org.whalebird.desktop' ; const splashURL = process . env . NODE_ENV === 'development' ? path . resolve ( __dirname , '../../static/splash-screen.html' ) : path . join ( __dirname , '/static/splash-screen.html' ) ; const userData = app . getPath ( 'userData' ) ; const appPath = app . getPath ( 'exe' ) ; const accountDBPath = process . env . NODE_ENV === 'production' ? userData + '/db/account.db' : 'account.db' ; const accountDB = new Datastore ( { filename : accountDBPath , autoload : true } ) ; const accountRepo = new Account ( accountDB ) ; accountRepo . initialize ( ) . catch ( ( err ) => log . error ( err ) ) ; const hashtagsDBPath = process . env . NODE_ENV === 'production' ? userData + '/db/hashtags.db' : 'hashtags.db' ; const hashtagsDB = new Datastore ( { filename : hashtagsDBPath , autoload : true } ) ; const settingsDBPath = process . env . NODE_ENV === 'production' ? userData + './db/settings.json' : 'settings.json' ; const preferencesDBPath = process . env . NODE_ENV === 'production' ? userData + './db/preferences.json' : 'preferences.json' ; const lokiDatabasePath = process . env . NODE_ENV === 'production' ? userData + '/db/lokiDatabase.db' : 'lokiDatabase.db' ; let markerRepo = null ; /**
 * Cache path
 */ const hashtagCachePath = process . env . NODE_ENV === 'production' ? userData + '/cache/hashtag.db' : 'cache/hashtag.db' ; const hashtagCache = new HashtagCache ( hashtagCachePath ) ; const accountCachePath = process . env . NODE_ENV === 'production' ? userData + '/cache/account.db' : 'cache/account.db' ; const accountCache = new AccountCache ( accountCachePath ) ; const soundBasePath = process . env . NODE_ENV === 'development' ? path . join ( __dirname , '../../build/sounds/' ) : path . join ( process . resourcesPath ! , 'build/sounds/' ) ; const iconBasePath = process . env . NODE_ENV === 'development' ? path . resolve ( __dirname , '../../build/icons/' ) : path . resolve ( process . resourcesPath ! , 'build/icons/' ) ; let launcher = null ; const proxyConfiguration = new ProxyConfiguration ( preferencesDBPath ) ; if ( process . platform !== 'darwin' ) { launcher = new AutoLaunch ( { name : 'Whalebird' , path : appPath } ) ; } async function listAccounts ( ) { try { const accounts = await accountRepo . listAccounts ( ) ; return accounts ; } catch ( err ) { return [ ] ; } } async function changeAccount ( account , index ) { if ( mainWindow === null ) { await createWindow ( ) ; mainWindow ! . webContents . on ( 'did-finish-load' , ( ) => { mainWindow ! . webContents . send ( 'change-account' , Object . assign ( account , { index : index } ) ) ; } ) ; } else { mainWindow . show ( ) ; mainWindow . webContents . send ( 'change-account' , Object . assign ( account , { index : index } ) ) ; } } async function getLanguage ( ) { try { const preferences = new Preferences ( preferencesDBPath ) ; const conf = await preferences . load ( ) ; return conf . language . language ; } catch ( err ) { log . warn ( err ) ; return Language . en . key ; } } const getSpellChecker = async ( ) : Promise < boolean > => { try { const preferences = new Preferences ( preferencesDBPath ) ; const conf = await preferences . load ( ) ; return conf . language . spellchecker . enabled ; } catch ( err ) { return true ; } } ; const getMenuPreferences = async ( ) : Promise < MenuPreferences > => { const preferences = new Preferences ( preferencesDBPath ) ; const conf = await preferences . load ( ) ; return conf . menu ; } ; return const updateApplicationMenu = async ( accountsChange ) : Promise < boolean > => { const menuPreferences = await getMenuPreferences ( ) ; const menu = ApplicationMenu ( accountsChange , menuPreferences , i18next ) ; Menu . setApplicationMenu ( menu ) ; let autoHideMenuBar = false ; if ( menuPreferences . autoHideMenu ) { autoHideMenuBar = true ; } return autoHideMenuBar ; } ; /**
 * Set dock menu for mac
 */ const updateDockMenu = async ( accountsChange ) => { if ( process . platform !== 'darwin' ) { return ; } const dockMenu = Menu . buildFromTemplate ( accountsChange ) ; app . dock . setMenu ( dockMenu ) ; } ; async function createWindow ( ) { /**
       DB
    */ const lokiDB = await newDB ( lokiDatabasePath ) ; markerRepo = new Marker ( lokiDB ) ; /**
     * List accounts
     */ const accounts = await listAccounts ( ) ; const accountsChange = accounts . map ( ( a , index ) => { return { label : a . domain , accelerator : `CmdOrCtrl+${ index + 1 }` , click : ( ) => changeAccount ( a , index ) } ; } ) ; /**
     * Get language
     */ const language = await getLanguage ( ) ; i18next . changeLanguage ( language ) ; /**
     * Get spellcheck
     */ const spellcheck = await getSpellChecker ( ) ; /**
     * Load system theme color for dark mode
     */ nativeTheme . themeSource = 'system' ; /**
     * Set Application Menu
     */ const autoHideMenuBar = await updateApplicationMenu ( accountsChange ) ; /**
     * Set dock menu for mac
     */ await updateDockMenu ( accountsChange ) ; /**
     * Windows10 don't notify, so we have to set appId
     * https://github.com/electron/electron/issues/10864
     */ app . setAppUserModelId ( appId ) ; /**
     * Enable accessibility
     */ app . accessibilitySupportEnabled = true ; /**
     * Initial window options
     */ const mainWindowState = windowStateKeeper ( { defaultWidth : 1000 , defaultHeight : 563 } ) ; const titleBarStyle = process . platform === 'win32' ? 'default' : 'hidden' ; const mainOpts = { titleBarStyle : titleBarStyle , x : mainWindowState . x , y : mainWindowState . y , width : mainWindowState . width , height : mainWindowState . height , backgroundColor : '#fff' , useContentSize : true , icon : path . join ( iconBasePath , '256x256.png' ) , autoHideMenuBar : autoHideMenuBar , webPreferences : { nodeIntegration : true , contextIsolation : false , preload : path . resolve ( __dirname , './preload.js' ) , spellcheck : spellcheck } } ; const config = { windowOpts : mainOpts , templateUrl : splashURL , splashScreenOpts : { width : 425 , height : 325 } } ; mainWindow = initSplashScreen ( config ) ; mainWindowState . manage ( mainWindow ) ; /**
     * Get system proxy configuration.
     */ if ( session && session . defaultSession ) { const proxyInfo = await session . defaultSession . resolveProxy ( 'https://mastodon.social' ) ; proxyConfiguration . setSystemProxy ( proxyInfo ) ; log . info ( `System proxy configuration: ${ proxyInfo }` ) ; } /**
     * Set proxy for BrowserWindow
     */ const proxyConfig = await proxyConfiguration . forMastodon ( ) ; if ( proxyConfig ) { await mainWindow . webContents . session . setProxy ( { proxyRules : `${ proxyConfig . protocol }://${ proxyConfig . host }:${ proxyConfig . port }` } ) ; } mainWindow . loadURL ( winURL ) ; mainWindow . webContents . on ( 'will-navigate' , event => event . preventDefault ( ) ) ; if ( process . platform !== 'darwin' ) { tray = new Tray ( path . join ( iconBasePath , 'tray_icon.png' ) ) ; const trayMenu = TrayMenu ( accountsChange , i18next ) ; tray . setContextMenu ( trayMenu ) ; tray . setToolTip ( i18next . t ( 'main_menu.application.name' ) ) ; tray . on ( 'click' , ( ) => { if ( mainWindow ! . isVisible ( ) ) { mainWindow ! . hide ( ) ; mainWindow ! . setSkipTaskbar ( true ) ; } else { mainWindow ! . show ( ) ; mainWindow ! . setSkipTaskbar ( false ) ; } } ) ; mainWindow . on ( 'close' , event => { mainWindow ! . hide ( ) ; mainWindow ! . setSkipTaskbar ( true ) ; event . preventDefault ( ) ; } ) ; } else { mainWindow . on ( 'closed' , ( ) => { mainWindow = null ; } ) ; } } const args = minimist ( process . argv . slice ( process . env . NODE_ENV === 'development' ? 2 : 1 ) ) ; if ( args . help ) { console . log ( `
Whalebird is Mastodon, Pleroma and Misskey client for desktop.

Usage
 $ whalebird

Options
 --help    show help
` ) ; process . exit ( 0 ) ; } app . commandLine . appendSwitch ( 'disable-renderer-backgrounding' ) ; app . on ( 'ready' , createWindow ) ; app . on ( 'window-all-closed' , ( ) => { if ( process . platform !== 'darwin' ) { app . quit ( ) ; } else { const menu = Menu . getApplicationMenu ( ) ; if ( menu ) { if ( menu . items [ 0 ] . submenu ) { menu . items [ 0 ] . submenu . items [ 2 ] . enabled = false ; } if ( menu . items [ 1 ] . submenu ) { menu . items [ 1 ] . submenu . items [ 0 ] . enabled = false ; } if ( menu . items [ 4 ] . submenu ) { menu . items [ 4 ] . submenu . items [ 1 ] . enabled = true ; menu . items [ 4 ] . submenu . items [ 4 ] . enabled = false ; } } } } ) ; app . on ( 'activate' , ( ) => { if ( mainWindow === null ) { createWindow ( ) ; } } ) ; const auth = new Authentication ( accountRepo ) ; type AuthRequest = { instance : string ; sns : 'mastodon' | 'pleroma' | 'misskey' ; } ; ipcMain . handle ( 'get-auth-url' , async ( _ , request ) => { const proxy = await proxyConfiguration . forMastodon ( ) ; const url = await auth . getAuthorizationUrl ( request . sns , request . instance , proxy ) ; log . debug ( url ) ; shell . openExternal ( url ) ; return url ; } ) ; type TokenRequest = { code : string | null ; sns : 'mastodon' | 'pleroma' | 'misskey' ; } ; ipcMain . handle ( 'get-and-update-access-token' , async ( _ , request ) => { const proxy = await proxyConfiguration . forMastodon ( ) ; const token = await auth . getAndUpdateAccessToken ( request . sns , request . code , proxy ) ; const accounts = await listAccounts ( ) ; const accountsChange = accounts . map ( ( a , index ) => { return { label : a . domain , accelerator : `CmdOrCtrl+${ index + 1 }` , click : ( ) => changeAccount ( a , index ) } ; } ) ; await updateApplicationMenu ( accountsChange ) ; await updateDockMenu ( accountsChange ) ; if ( process . platform !== 'darwin' && tray !== null ) { tray . setContextMenu ( TrayMenu ( accountsChange , i18next ) ) ; } return new Promise ( ( resolve , reject ) => { accountDB . findOne ( { accessToken : token } , ( err , doc ) => { if ( err ) return reject ( err ) ; if ( isEmpty ( doc ) ) return reject ( err ) ; resolve ( doc . _id ) ; } ) ; } ) ; } ) ; ipcMain . handle ( 'list-accounts' , async ( _ ) => { const accounts = await accountRepo . listAccounts ( ) ; return accounts ; } ) ; ipcMain . handle ( 'get-local-account' , async ( _ , id ) => { const account = await accountRepo . getAccount ( id ) ; return account ; } ) ; ipcMain . handle ( 'update-account' , async ( _ , acct ) => { const proxy = await proxyConfiguration . forMastodon ( ) ; const ac = await accountRepo . refresh ( acct , proxy ) ; return ac ; } ) ; ipcMain . handle ( 'remove-account' , async ( _ , id ) => { const accountId = await accountRepo . removeAccount ( id ) ; const accounts = await listAccounts ( ) ; const accountsChange = accounts . map ( ( a , index ) => { return { label : a . domain , accelerator : `CmdOrCtrl+${ index + 1 }` , click : ( ) => changeAccount ( a , index ) } ; } ) ; await updateApplicationMenu ( accountsChange ) ; await updateDockMenu ( accountsChange ) ; if ( process . platform !== 'darwin' && tray !== null ) { tray . setContextMenu ( TrayMenu ( accountsChange , i18next ) ) ; } stopUserStreaming ( accountId ) ; } ) ; ipcMain . handle ( 'forward-account' , async ( _ , acct ) => { await accountRepo . forwardAccount ( acct ) ; } ) ; ipcMain . handle ( 'backward-account' , async ( _ , acct ) => { await accountRepo . backwardAccount ( acct ) ; } ) ; ipcMain . handle ( 'refresh-accounts' , async ( _ ) => { const proxy = await proxyConfiguration . forMastodon ( ) ; const accounts = await accountRepo . refreshAccounts ( proxy ) ; return accounts ; } ) ; ipcMain . handle ( 'remove-all-accounts' , async ( _ ) => { await accountRepo . removeAll ( ) ; const accounts = await listAccounts ( ) ; const accountsChange = accounts . map ( ( a , index ) => { return { label : a . domain , accelerator : `CmdOrCtrl+${ index + 1 }` , click : ( ) => changeAccount ( a , index ) } ; } ) ; await updateApplicationMenu ( accountsChange ) ; await updateDockMenu ( accountsChange ) ; if ( process . platform !== 'darwin' && tray !== null ) { tray . setContextMenu ( TrayMenu ( accountsChange , i18next ) ) ; } } ) ; ipcMain . handle ( 'change-auto-launch' , async ( _ , enable ) => { if ( launcher ) { const enabled = await launcher . isEnabled ( ) ; if ( ! enabled && enable && launcher ) { launcher . enable ( ) ; } else if ( enabled && ! enable && launcher ) { launcher . disable ( ) ; } return enable ; } else { return false ; } } ) ; ipcMain . on ( 'reset-badge' , ( ) => { if ( process . platform === 'darwin' ) { app . dock . setBadge ( '' ) ; } } ) ; ipcMain . handle ( 'confirm-timelines' , async ( _event , account ) : Promise < EnabledTimelines > => { const proxy = await proxyConfiguration . forMastodon ( ) ; const timelines = await confirm ( account , proxy ) ; return timelines ; } ) ; const userStreamings = { } ; ipcMain . on ( 'start-all-user-streamings' , ( event , accounts ) => { accounts . map ( async ( account ) => { const id = account . _id ! ; try { const acct = await accountRepo . getAccount ( id ) ; if ( userStreamings [ id ] ) { userStreamings [ id ] ! . stop ( ) ; userStreamings [ id ] = null ; } const proxy = await proxyConfiguration . forMastodon ( ) ; const sns = await detector ( acct . baseURL , proxy ) ; const url = await StreamingURL ( sns , acct , proxy ) ; userStreamings [ id ] = new UserStreaming ( sns , acct , url , proxy ) ; userStreamings [ id ] ! . start ( async ( update ) => { if ( ! event . sender . isDestroyed ( ) ) { event . sender . send ( `update-start-all-user-streamings-${ id }` , update ) ; } update . tags . map ( async ( tag ) => { await hashtagCache . insertHashtag ( tag . name ) . catch ( err => console . error ( err ) ) ; } ) ; await accountCache . insertAccount ( id , update . account . acct ) . catch ( err => console . error ( err ) ) ; } , ( notification ) => { const preferences = new Preferences ( preferencesDBPath ) ; preferences . load ( ) . then ( conf => { const options = createNotification ( notification , conf . notification . notify ) ; if ( options !== null ) { const notify = new Notification ( options ) ; notify . on ( 'click' , _ => { if ( ! event . sender . isDestroyed ( ) ) { event . sender . send ( 'open-notification-tab' , id ) ; } } ) ; notify . show ( ) ; } } ) ; if ( process . platform === 'darwin' ) { app . dock . setBadge ( '•' ) ; } if ( ! event . sender . isDestroyed ( ) ) { event . sender . send ( `notification-start-all-user-streamings-${ id }` , notification ) ; if ( notification . type === 'mention' ) { event . sender . send ( `mention-start-all-user-streamings-${ id }` , notification ) ; } } } , ( statusId ) => { if ( ! event . sender . isDestroyed ( ) ) { event . sender . send ( `delete-start-all-user-streamings-${ id }` , statusId ) ; } } , ( err ) => { log . error ( err ) ; if ( ! event . sender . isDestroyed ( ) ) { event . sender . send ( 'error-start-all-user-streamings' , err ) ; } } ) ; } catch ( err ) { log . error ( err ) ; const streamingError = new StreamingError ( err . message , account . domain ) ; if ( ! event . sender . isDestroyed ( ) ) { event . sender . send ( 'error-start-all-user-streamings' , streamingError ) ; } } } ) ; } ) ; ipcMain . on ( 'stop-all-user-streamings' , ( ) => { Object . keys ( userStreamings ) . forEach ( ( key ) => { if ( userStreamings [ key ] ) { userStreamings [ key ] ! . stop ( ) ; userStreamings [ key ] = null ; } } ) ; } ) ; param id const stopUserStreaming = ( id ) => { Object . keys ( userStreamings ) . forEach ( ( key ) => { if ( key === id && userStreamings [ id ] ) { userStreamings [ id ] ! . stop ( ) ; userStreamings [ id ] = null ; } } ) ; } ; type StreamingSetting = { account : LocalAccount ; } ; let directMessagesStreaming = null ; ipcMain . on ( 'start-directmessages-streaming' , async ( event , obj ) => { const { account } = obj ; try { const acct = await accountRepo . getAccount ( account . _id ! ) ; if ( directMessagesStreaming !== null ) { directMessagesStreaming . stop ( ) ; directMessagesStreaming = null ; } const proxy = await proxyConfiguration . forMastodon ( ) ; const sns = await detector ( acct . baseURL , proxy ) ; const url = await StreamingURL ( sns , acct , proxy ) ; directMessagesStreaming = new DirectStreaming ( sns , acct , url , proxy ) ; directMessagesStreaming . start ( ( update ) => { if ( ! event . sender . isDestroyed ( ) ) { event . sender . send ( 'update-start-directmessages-streaming' , update ) ; } } , ( id ) => { if ( ! event . sender . isDestroyed ( ) ) { event . sender . send ( 'delete-start-directmessages-streaming' , id ) ; } } , ( err ) => { log . error ( err ) ; if ( ! event . sender . isDestroyed ( ) ) { event . sender . send ( 'error-start-directmessages-streaming' , err ) ; } } ) ; } catch ( err ) { log . error ( err ) ; if ( ! event . sender . isDestroyed ( ) ) { event . sender . send ( 'error-start-directmessages-streaming' , err ) ; } } } ) ; ipcMain . on ( 'stop-directmessages-streaming' , ( ) => { if ( directMessagesStreaming !== null ) { directMessagesStreaming . stop ( ) ; directMessagesStreaming = null ; } } ) ; let localStreaming = null ; ipcMain . on ( 'start-local-streaming' , async ( event , obj ) => { const { account } = obj ; try { const acct = await accountRepo . getAccount ( account . _id ! ) ; if ( localStreaming !== null ) { localStreaming . stop ( ) ; localStreaming = null ; } const proxy = await proxyConfiguration . forMastodon ( ) ; const sns = await detector ( acct . baseURL , proxy ) ; const url = await StreamingURL ( sns , acct , proxy ) ; localStreaming = new LocalStreaming ( sns , acct , url , proxy ) ; localStreaming . start ( ( update ) => { if ( ! event . sender . isDestroyed ( ) ) { event . sender . send ( 'update-start-local-streaming' , update ) ; } } , ( id ) => { if ( ! event . sender . isDestroyed ( ) ) { event . sender . send ( 'delete-start-local-streaming' , id ) ; } } , ( err ) => { log . error ( err ) ; if ( ! event . sender . isDestroyed ( ) ) { event . sender . send ( 'error-start-local-streaming' , err ) ; } } ) ; } catch ( err ) { log . error ( err ) ; if ( ! event . sender . isDestroyed ( ) ) { event . sender . send ( 'error-start-local-streaming' , err ) ; } } } ) ; ipcMain . on ( 'stop-local-streaming' , ( ) => { if ( localStreaming !== null ) { localStreaming . stop ( ) ; localStreaming = null ; } } ) ; let publicStreaming = null ; ipcMain . on ( 'start-public-streaming' , async ( event , obj ) => { const { account } = obj ; try { const acct = await accountRepo . getAccount ( account . _id ! ) ; if ( publicStreaming !== null ) { publicStreaming . stop ( ) ; publicStreaming = null ; } const proxy = await proxyConfiguration . forMastodon ( ) ; const sns = await detector ( acct . baseURL , proxy ) ; const url = await StreamingURL ( sns , acct , proxy ) ; publicStreaming = new PublicStreaming ( sns , acct , url , proxy ) ; publicStreaming . start ( ( update ) => { if ( ! event . sender . isDestroyed ( ) ) { event . sender . send ( 'update-start-public-streaming' , update ) ; } } , ( id ) => { if ( ! event . sender . isDestroyed ( ) ) { event . sender . send ( 'delete-start-public-streaming' , id ) ; } } , ( err ) => { log . error ( err ) ; if ( ! event . sender . isDestroyed ( ) ) { event . sender . send ( 'error-start-public-streaming' , err ) ; } } ) ; } catch ( err ) { log . error ( err ) ; if ( ! event . sender . isDestroyed ( ) ) { event . sender . send ( 'error-start-public-streaming' , err ) ; } } } ) ; ipcMain . on ( 'stop-public-streaming' , ( ) => { if ( publicStreaming !== null ) { publicStreaming . stop ( ) ; publicStreaming = null ; } } ) ; let listStreaming = null ; type ListID = { listID : string ; } ; ipcMain . on ( 'start-list-streaming' , async ( event , obj ) => { const { listID , account } = obj ; try { const acct = await accountRepo . getAccount ( account . _id ! ) ; if ( listStreaming !== null ) { listStreaming . stop ( ) ; listStreaming = null ; } const proxy = await proxyConfiguration . forMastodon ( ) ; const sns = await detector ( acct . baseURL , proxy ) ; const url = await StreamingURL ( sns , acct , proxy ) ; listStreaming = new ListStreaming ( sns , acct , url , proxy ) ; listStreaming . start ( listID , ( update ) => { if ( ! event . sender . isDestroyed ( ) ) { event . sender . send ( 'update-start-list-streaming' , update ) ; } } , ( id ) => { if ( ! event . sender . isDestroyed ( ) ) { event . sender . send ( 'delete-start-list-streaming' , id ) ; } } , ( err ) => { log . error ( err ) ; if ( ! event . sender . isDestroyed ( ) ) { event . sender . send ( 'error-start-list-streaming' , err ) ; } } ) ; } catch ( err ) { log . error ( err ) ; if ( ! event . sender . isDestroyed ( ) ) { event . sender . send ( 'error-start-list-streaming' , err ) ; } } } ) ; ipcMain . on ( 'stop-list-streaming' , ( ) => { if ( listStreaming !== null ) { listStreaming . stop ( ) ; listStreaming = null ; } } ) ; let tagStreaming = null ; type Tag = { tag : string ; } ; ipcMain . on ( 'start-tag-streaming' , async ( event , obj ) => { const { tag , account } = obj ; try { const acct = await accountRepo . getAccount ( account . _id ! ) ; if ( tagStreaming !== null ) { tagStreaming . stop ( ) ; tagStreaming = null ; } const proxy = await proxyConfiguration . forMastodon ( ) ; const sns = await detector ( acct . baseURL , proxy ) ; const url = await StreamingURL ( sns , acct , proxy ) ; tagStreaming = new TagStreaming ( sns , acct , url , proxy ) ; tagStreaming . start ( tag , ( update ) => { if ( ! event . sender . isDestroyed ( ) ) { event . sender . send ( 'update-start-tag-streaming' , update ) ; } } , ( id ) => { if ( ! event . sender . isDestroyed ( ) ) { event . sender . send ( 'delete-start-tag-streaming' , id ) ; } } , ( err ) => { log . error ( err ) ; if ( ! event . sender . isDestroyed ( ) ) { event . sender . send ( 'error-start-tag-streaming' , err ) ; } } ) ; } catch ( err ) { log . error ( err ) ; if ( ! event . sender . isDestroyed ( ) ) { event . sender . send ( 'error-start-tag-streaming' , err ) ; } } } ) ; ipcMain . on ( 'stop-tag-streaming' , ( ) => { if ( tagStreaming !== null ) { tagStreaming . stop ( ) ; tagStreaming = null ; } } ) ; ipcMain . on ( 'fav-rt-action-sound' , ( ) => { const preferences = new Preferences ( preferencesDBPath ) ; preferences . load ( ) . then ( conf => { if ( conf . general . sound . fav_rb ) { const sound = path . join ( soundBasePath , 'operation_sound01.wav' ) ; simplayer ( sound , ( err ) => { if ( err ) log . error ( err ) ; } ) ; } } ) . catch ( err => log . error ( err ) ) ; } ) ; ipcMain . on ( 'toot-action-sound' , ( ) => { const preferences = new Preferences ( preferencesDBPath ) ; preferences . load ( ) . then ( conf => { if ( conf . general . sound . toot ) { const sound = path . join ( soundBasePath , 'operation_sound02.wav' ) ; simplayer ( sound , ( err ) => { if ( err ) log . error ( err ) ; } ) ; } } ) . catch ( err => log . error ( err ) ) ; } ) ; ipcMain . handle ( 'get-preferences' , async ( _ ) => { const preferences = new Preferences ( preferencesDBPath ) ; let enabled = false ; if ( launcher ) { enabled = await launcher . isEnabled ( ) ; } await preferences . update ( { general : { other : { launch : enabled } } } ) . catch ( err => console . error ( err ) ) ; const conf = await preferences . load ( ) ; return conf ; } ) ; ipcMain . handle ( 'update-preferences' , async ( _ , data ) => { const preferences = new Preferences ( preferencesDBPath ) ; const conf = await preferences . update ( data ) ; return conf ; } ) ; ipcMain . handle ( 'reset-preferences' , async ( _ ) => { const preferences = new Preferences ( preferencesDBPath ) ; const conf = await preferences . reset ( ) ; return conf ; } ) ; ipcMain . handle ( 'system-use-dark-theme' , async ( _ ) => { return nativeTheme . shouldUseDarkColors ; } ) ; ipcMain . on ( 'change-collapse' , ( _event , value ) => { const preferences = new Preferences ( preferencesDBPath ) ; preferences . update ( { state : { collapse : value } } ) . catch ( err => { log . error ( err ) ; } ) ; } ) ; ipcMain . handle ( 'get-collapse' , async ( _ ) => { const preferences = new Preferences ( preferencesDBPath ) ; const conf = await preferences . load ( ) ; return conf . state . collapse ; } ) ; ipcMain . handle ( 'change-global-header' , async ( _ , value ) => { const preferences = new Preferences ( preferencesDBPath ) ; const conf = await preferences . update ( { state : { hideGlobalHeader : value } } ) ; return conf ; } ) ; ipcMain . handle ( 'get-global-header' , async ( _ ) => { const preferences = new Preferences ( preferencesDBPath ) ; const conf = await preferences . load ( ) ; return conf . state . hideGlobalHeader ; } ) ; ipcMain . handle ( 'update-proxy-config' , async ( _event , proxy ) => { const preferences = new Preferences ( preferencesDBPath ) ; try { const conf = await preferences . update ( { proxy : proxy } ) ; const proxyConfig = await proxyConfiguration . forMastodon ( ) ; if ( proxyConfig ) { await mainWindow ?. webContents . session . setProxy ( { proxyRules : `${ proxyConfig . protocol }://${ proxyConfig . host }:${ proxyConfig . port }` } ) ; } else { await mainWindow ?. webContents . session . setProxy ( { } ) ; } return conf ; } catch ( err ) { log . error ( err ) ; } return null ; } ) ; ipcMain . handle ( 'change-language' , async ( _ , value ) => { const preferences = new Preferences ( preferencesDBPath ) ; const conf = await preferences . update ( { language : { language : value } } ) ; i18next . changeLanguage ( conf . language . language ) ; const accounts = await listAccounts ( ) ; const accountsChange = accounts . map ( ( a , index ) => { return { label : a . domain , accelerator : `CmdOrCtrl+${ index + 1 }` , click : ( ) => changeAccount ( a , index ) } ; } ) ; await updateApplicationMenu ( accountsChange ) ; await updateDockMenu ( accountsChange ) ; if ( process . platform !== 'darwin' && tray !== null ) { tray . setContextMenu ( TrayMenu ( accountsChange , i18next ) ) ; } return conf . language . language ; } ) ; ipcMain . handle ( 'toggle-spellchecker' , async ( _ , value ) => { mainWindow ?. webContents . session . setSpellCheckerEnabled ( value ) ; const preferences = new Preferences ( preferencesDBPath ) ; const conf = await preferences . update ( { language : { spellchecker : { enabled : value } } } ) ; return conf . language . spellchecker . enabled ; } ) ; ipcMain . handle ( 'update-spellchecker-languages' , async ( _ , languages ) => { const decoded = languages . map ( l => { const d = decodeLanguage ( l ) ; return d . rfc4646 ; } ) ; mainWindow ?. webContents . session . setSpellCheckerLanguages ( decoded ) ; const preferences = new Preferences ( preferencesDBPath ) ; const conf = await preferences . update ( { language : { spellchecker : { languages : languages } } } ) ; return conf . language . spellchecker . languages ; } ) ; ipcMain . handle ( 'get-home-marker' , async ( _ , ownerID ) => { if ( markerRepo === null ) { return null ; } const marker = await markerRepo . get ( ownerID , 'home' ) ; return marker ; } ) ; ipcMain . handle ( 'get-notifications-marker' , async ( _ , ownerID ) => { if ( markerRepo === null ) { return null ; } const marker = await markerRepo . get ( ownerID , 'notifications' ) ; return marker ; } ) ; ipcMain . on ( 'save-marker' , async ( _ , marker ) : Promise < LocalMarker | null > => { if ( marker . owner_id === null || marker . owner_id === undefined || marker . owner_id === '' ) { return null ; } if ( markerRepo === null ) { return null ; } const res = await markerRepo . save ( marker ) ; return res ; } ) ; ipcMain . handle ( 'save-hashtag' , async ( _ , tag ) => { const hashtags = new Hashtags ( hashtagsDB ) ; await hashtags . insertTag ( tag ) ; } ) ; ipcMain . handle ( 'list-hashtags' , async ( _ ) => { const hashtags = new Hashtags ( hashtagsDB ) ; const tags = await hashtags . listTags ( ) ; return tags ; } ) ; ipcMain . handle ( 'remove-hashtag' , async ( _ , tag ) => { const hashtags = new Hashtags ( hashtagsDB ) ; await hashtags . removeTag ( tag ) ; } ) ; ipcMain . handle ( 'list-fonts' , async ( _ ) => { const list = await Fonts ( ) ; return list ; } ) ; ipcMain . handle ( 'get-account-setting' , async ( _ , accountID ) : Promise < Setting > => { const settings = new Settings ( settingsDBPath ) ; const setting = await settings . get ( accountID ) ; return setting ; } ) ; ipcMain . handle ( 'update-account-setting' , async ( _ , setting ) : Promise < BaseSettings > => { const settings = new Settings ( settingsDBPath ) ; const res = await settings . update ( setting ) ; return res ; } ) ; ipcMain . handle ( 'get-cache-hashtags' , async ( _ ) => { const tags = await hashtagCache . listTags ( ) ; return tags ; } ) ; ipcMain . handle ( 'insert-cache-hashtags' , async ( _ , tags ) => { await Promise . all ( tags . map ( async ( name ) => { await hashtagCache . insertHashtag ( name ) . catch ( err => console . error ( err ) ) ; } ) ) ; } ) ; ipcMain . handle ( 'get-cache-accounts' , async ( _ , ownerID ) => { const accounts = await accountCache . listAccounts ( ownerID ) ; return accounts ; } ) ; ipcMain . handle ( 'insert-cache-accounts' , async ( _ , obj ) => { const { ownerID , accts } = obj ; Promise . all ( accts . map ( async ( acct ) => { await accountCache . insertAccount ( ownerID , acct ) . catch ( err => console . error ( err ) ) ; } ) ) ; } ) ; ipcMain . on ( 'relaunch' , ( ) => { app . relaunch ( ) ; app . exit ( ) ; } ) ; /**
 * Auto Updater
 *
 * Uncomment the following code below and install `electron-updater` to
 * support auto updating. Code Signing with a valid certificate is required.
 * https://simulatedgreg.gitbooks.io/electron-vue/content/en/using-electron-builder.html#auto-updating
 */ /**
 * Genrate application menu
 */ const ApplicationMenu = ( accountsChange , menu , i18n ) : Menu => { /**
     * For mac menu
     */ const macGeneralMenu = process . platform !== 'darwin' ? [ ] : [ { type : 'separator' } , { label : i18n . t ( 'main_menu.application.services' ) , role : 'services' } , { type : 'separator' } , { label : i18n . t ( 'main_menu.application.hide' ) , role : 'hide' } , { label : i18n . t ( 'main_menu.application.hide_others' ) , role : 'hideOthers' } , { label : i18n . t ( 'main_menu.application.show_all' ) , role : 'unhide' } ] ; const macWindowMenu = process . platform === 'darwin' ? [ ] : [ { label : i18n . t ( 'main_menu.window.always_show_menu_bar' ) , type : 'checkbox' , checked : ! menu . autoHideMenu , click : item => { changeMenuAutoHide ( ! item . checked ) ; } } , { type : 'separator' } ] ; const applicationQuitMenu = process . platform === 'darwin' ? [ { label : i18n . t ( 'main_menu.application.quit' ) , accelerator : 'CmdOrCtrl+Q' , role : 'quit' } ] : [ { label : i18n . t ( 'main_menu.application.quit' ) , accelerator : 'CmdOrCtrl+Q' , click : ( ) => { mainWindow ! . destroy ( ) ; } } ] ; const template = [ { label : i18n . t ( 'main_menu.application.name' ) , submenu : [ { label : i18n . t ( 'main_menu.application.about' ) , role : 'about' , click : ( ) => { openAboutWindow ( { icon_path : path . join ( iconBasePath , '256x256.png' ) , copyright : 'Copyright (c) 2021 AkiraFukushima' , package_json_dir : path . resolve ( __dirname , '../../' ) , open_devtools : process . env . NODE_ENV !== 'production' } ) ; } } , { type : 'separator' } , { label : i18n . t ( 'main_menu.application.preferences' ) , accelerator : 'CmdOrCtrl+,' , click : ( ) => { mainWindow ! . webContents . send ( 'open-preferences' ) ; } } , ... macGeneralMenu , { type : 'separator' } , ... applicationQuitMenu ] } , { label : i18n . t ( 'main_menu.toot.name' ) , submenu : [ { label : i18n . t ( 'main_menu.toot.new' ) , accelerator : 'CmdOrCtrl+N' , click : ( ) => { mainWindow ! . webContents . send ( 'CmdOrCtrl+N' ) ; } } ] } , { label : i18n . t ( 'main_menu.edit.name' ) , submenu : [ { label : i18n . t ( 'main_menu.edit.undo' ) , accelerator : 'CmdOrCtrl+Z' , role : 'undo' } , { label : i18n . t ( 'main_menu.edit.redo' ) , accelerator : 'Shift+CmdOrCtrl+Z' , role : 'redo' } , { type : 'separator' } , { label : i18n . t ( 'main_menu.edit.cut' ) , accelerator : 'CmdOrCtrl+X' , role : 'cut' } , { label : i18n . t ( 'main_menu.edit.copy' ) , accelerator : 'CmdOrCtrl+C' , role : 'copy' } , { label : i18n . t ( 'main_menu.edit.paste' ) , accelerator : 'CmdOrCtrl+V' , role : 'paste' } , { label : i18n . t ( 'main_menu.edit.select_all' ) , accelerator : 'CmdOrCtrl+A' , role : 'selectall' } ] as Array < MenuItemConstructorOptions > } , { label : i18n . t ( 'main_menu.view.name' ) , submenu : [ { label : i18n . t ( 'main_menu.view.toggle_full_screen' ) , role : 'togglefullscreen' } ] } , { label : i18n . t ( 'main_menu.window.name' ) , submenu : [ ... macWindowMenu , { label : i18n . t ( 'main_menu.window.close' ) , role : 'close' } , { label : i18n . t ( 'main_menu.window.open' ) , enabled : false , click : ( ) => { reopenWindow ( ) ; } } , { label : i18n . t ( 'main_menu.window.minimize' ) , role : 'minimize' } , { type : 'separator' } , { label : i18n . t ( 'main_menu.window.jump_to' ) , accelerator : 'CmdOrCtrl+K' , enabled : true , click : ( ) => { mainWindow ! . webContents . send ( 'CmdOrCtrl+K' ) ; } } , { type : 'separator' } , ... accountsChange ] } ] ; return Menu . buildFromTemplate ( template ) ; } ; const TrayMenu = ( accountsChange , i18n ) : Menu => { const template = [ ... accountsChange , { label : i18n . t ( 'main_menu.application.open' ) , click : async ( ) => { if ( mainWindow ) { mainWindow . show ( ) ; } else { await createWindow ( ) ; } } } , { label : i18n . t ( 'main_menu.application.quit' ) , click : ( ) => { mainWindow ! . destroy ( ) ; } } ] ; const menu = Menu . buildFromTemplate ( template ) ; return menu ; } ; const changeMenuAutoHide = async ( autoHide ) => { if ( mainWindow === null ) { return null ; } mainWindow . autoHideMenuBar = autoHide ; mainWindow . setMenuBarVisibility ( ! autoHide ) ; const preferences = new Preferences ( preferencesDBPath ) ; preferences . update ( { menu : { autoHideMenu : autoHide } } ) ; return null ; } ; async function reopenWindow ( ) { if ( mainWindow === null ) { await createWindow ( ) ; return null ; } else { return null ; } } const createNotification = ( notification , notifyConfig ) : NotificationConstructorOptions | null => { switch ( notification . type ) { case NotificationType . Favourite : if ( notifyConfig . favourite ) { return { title : i18next . t ( 'notification.favourite.title' ) , body : i18next . t ( 'notification.favourite.body' , { username : username ( notification . account ) } ) , silent : false } as NotificationConstructorOptions ; } break ; case NotificationType . Follow : if ( notifyConfig . follow ) { return { title : i18next . t ( 'notification.follow.title' ) , body : i18next . t ( 'notification.follow.body' , { username : username ( notification . account ) } ) , silent : false } as NotificationConstructorOptions ; } break ; case NotificationType . FollowRequest : if ( notifyConfig . follow_request ) { return { title : i18next . t ( 'notification.follow_request.title' ) , body : i18next . t ( 'notification.follow_request.body' , { username : username ( notification . account ) } ) , silent : false } as NotificationConstructorOptions ; } break ; case NotificationType . Mention : if ( notifyConfig . reply ) { return { title : `${ username ( notification . status ! . account ) }` , body : sanitizeHtml ( notification . status ! . content , { allowedTags : [ ] , allowedAttributes : [ ] } ) , silent : false } as NotificationConstructorOptions ; } break ; case NotificationType . Reblog : if ( notifyConfig . reblog ) { if ( notification . status && notification . status . quote ) { return { title : i18next . t ( 'notification.quote.title' ) , body : i18next . t ( 'notification.quote.body' , { username : username ( notification . account ) } ) , silent : false } as NotificationConstructorOptions ; } else { return { title : i18next . t ( 'notification.reblog.title' ) , body : i18next . t ( 'notification.reblog.body' , { username : username ( notification . account ) } ) , silent : false } as NotificationConstructorOptions ; } } break ; case NotificationType . EmojiReaction : if ( notifyConfig . reaction ) { return { title : i18next . t ( 'notification.reaction.title' ) , body : i18next . t ( 'notification.reaction.body' , { username : username ( notification . account ) } ) , silent : false } as NotificationConstructorOptions ; } break ; case NotificationType . Status : if ( notifyConfig . status ) { return { title : i18next . t ( 'notification.status.title' ) , body : i18next . t ( 'notification.status.body' , { username : username ( notification . account ) } ) , silent : false } as NotificationConstructorOptions ; } break ; case NotificationType . PollVote : if ( notifyConfig . poll_vote ) { return { title : i18next . t ( 'notification.poll_vote.title' ) , body : i18next . t ( 'notification.poll_vote.body' , { username : username ( notification . account ) } ) , silent : false } as NotificationConstructorOptions ; } break ; case NotificationType . PollExpired : if ( notifyConfig . poll_expired ) { return { title : i18next . t ( 'notification.poll_expired.title' ) , body : i18next . t ( 'notification.poll_expired.body' , { username : username ( notification . account ) } ) , silent : false } as NotificationConstructorOptions ; } break ; default : break ; } return null ; } ; const username = ( account ) : string => { if ( account . display_name !== '' ) { return account . display_name ; } else { return account . username ; } } ; const decodeLanguage = ( lang ) : LanguageType => { const l = Object . keys ( Language ) . find ( k => Language [ k ] . key === lang ) ; if ( l === undefined ) { return Language . en ; } else { return Language [ l ] ; } } ;