Whalebird-desktop-client-ma.../src/main/index.ts

1249 lines
33 KiB
TypeScript
Raw Normal View History

2018-03-07 14:28:48 +01:00
'use strict'
2019-04-20 08:44:22 +02:00
import {
app,
ipcMain,
shell,
Menu,
Tray,
BrowserWindow,
BrowserWindowConstructorOptions,
MenuItemConstructorOptions,
Event,
Notification,
NotificationConstructorOptions
2019-04-20 08:44:22 +02:00
} from 'electron'
2018-03-08 15:08:33 +01:00
import Datastore from 'nedb'
2019-04-16 13:38:02 +02:00
import { isEmpty } from 'lodash'
import log from 'electron-log'
2018-03-24 15:23:25 +01:00
import windowStateKeeper from 'electron-window-state'
import simplayer from 'simplayer'
import path from 'path'
2018-06-01 07:19:56 +02:00
import ContextMenu from 'electron-context-menu'
import { initSplashScreen, Config } from '@trodi/electron-splashscreen'
2018-08-10 17:40:06 +02:00
import openAboutWindow from 'about-window'
import { Status, Notification as RemoteNotification, Account as RemoteAccount } from 'megalodon'
import sanitizeHtml from 'sanitize-html'
2018-03-08 15:08:33 +01:00
2018-03-08 09:41:39 +01:00
import Authentication from './auth'
import Account from './account'
import StreamingManager from './streamingManager'
import Preferences from './preferences'
2018-09-25 18:02:36 +02:00
import Fonts from './fonts'
2018-06-01 07:19:56 +02:00
import Hashtags from './hashtags'
import UnreadNotification from './unreadNotification'
import i18n from '~/src/config/i18n'
import Language from '../constants/language'
import { LocalAccount } from '~/src/types/localAccount'
import { LocalTag } from '~/src/types/localTag'
import { UnreadNotification as UnreadNotificationConfig } from '~/src/types/unreadNotification'
import { Notify } from '~/src/types/notify'
import { StreamingError } from '~/src/errors/streamingError'
2018-05-30 13:54:21 +02:00
/**
* Context menu
*/
ContextMenu({
showCopyImageAddress: true,
showSaveImageAs: true
})
2018-03-07 14:28:48 +01:00
/**
* Set log level
*/
log.transports.console.level = 'debug'
log.transports.file.level = 'info'
declare namespace global {
let __static: string
}
2018-03-07 14:28:48 +01:00
/**
* 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, '\\\\')
2018-03-07 14:28:48 +01:00
}
let mainWindow: BrowserWindow | null
let tray: Tray | null
2019-04-20 08:44:22 +02:00
const winURL = process.env.NODE_ENV === 'development' ? `http://localhost:9080` : `file://${__dirname}/index.html`
2018-03-07 14:28:48 +01:00
2019-04-20 08:44:22 +02:00
const splashURL =
process.env.NODE_ENV === 'development'
? path.resolve(__dirname, '../../static/splash-screen.html')
: `${__dirname}/static/splash-screen.html`
2018-03-22 08:55:58 +01:00
// https://github.com/louischatriot/nedb/issues/459
2018-03-22 08:49:39 +01:00
const userData = app.getPath('userData')
2019-04-20 08:44:22 +02:00
const accountDBPath = process.env.NODE_ENV === 'production' ? userData + '/db/account.db' : 'account.db'
let accountDB = new Datastore({
filename: accountDBPath,
2018-03-08 15:08:33 +01:00
autoload: true
})
const accountManager = new Account(accountDB)
2019-04-20 08:44:22 +02:00
accountManager.initialize().catch((err: Error) => log.error(err))
2019-04-20 08:44:22 +02:00
const hashtagsDBPath = process.env.NODE_ENV === 'production' ? userData + '/db/hashtags.db' : 'hashtags.db'
2018-06-01 07:19:56 +02:00
let hashtagsDB = new Datastore({
filename: hashtagsDBPath,
autoload: true
})
2019-04-20 08:44:22 +02:00
const unreadNotificationDBPath = process.env.NODE_ENV === 'production' ? userData + '/db/unread_notification.db' : 'unread_notification.db'
const unreadNotification = new UnreadNotification(unreadNotificationDBPath)
2019-04-20 08:44:22 +02:00
unreadNotification.initialize().catch((err: Error) => log.error(err))
2019-04-20 08:44:22 +02:00
const preferencesDBPath = process.env.NODE_ENV === 'production' ? userData + './db/preferences.json' : 'preferences.json'
2018-03-08 15:08:33 +01:00
2019-04-20 08:44:22 +02:00
const soundBasePath =
process.env.NODE_ENV === 'development' ? path.join(__dirname, '../../build/sounds/') : path.join(process.resourcesPath!, 'build/sounds/')
2019-04-20 08:44:22 +02:00
async function listAccounts(): Promise<Array<LocalAccount>> {
2018-03-21 04:22:45 +01:00
try {
const accounts = await accountManager.listAccounts()
2018-03-21 04:22:45 +01:00
return accounts
} catch (err) {
return []
}
}
2019-04-20 08:44:22 +02:00
async function changeAccount(account: LocalAccount, index: number) {
// In MacOS, user can hide the window.
// In this time, mainWindow in not exist, so we have to create window.
if (mainWindow === null) {
await createWindow()
// We have to wait the web contents is loaded.
mainWindow!.webContents.on('did-finish-load', () => {
mainWindow!.webContents.send('change-account', Object.assign(account, { index: index }))
})
} else {
mainWindow.webContents.send('change-account', Object.assign(account, { index: index }))
}
}
2019-04-20 08:44:22 +02:00
async function getLanguage() {
try {
const preferences = new Preferences(preferencesDBPath)
const conf = await preferences.load()
return conf.language.language
} catch (err) {
return Language.en.key
}
}
2018-09-08 08:12:19 +02:00
/**
* Minimize to tray when click close button
*/
2019-04-20 08:44:22 +02:00
async function setMinimizeToTray() {
mainWindow!.on('close', event => {
mainWindow!.hide()
mainWindow!.setSkipTaskbar(true)
2018-09-08 08:12:19 +02:00
event.preventDefault()
})
tray = new Tray(path.join(__dirname, '../../build/icons/256x256.png'))
const contextMenu = Menu.buildFromTemplate([
2019-04-20 08:44:22 +02:00
{
label: i18n.t('main_menu.application.quit'),
click: () => {
mainWindow!.destroy()
}
}
2018-09-08 08:12:19 +02:00
])
tray.setToolTip(i18n.t('main_menu.application.name'))
tray.setContextMenu(contextMenu)
tray.on('click', () => {
if (mainWindow!.isVisible()) {
mainWindow!.hide()
mainWindow!.setSkipTaskbar(true)
2018-09-08 08:12:19 +02:00
} else {
mainWindow!.show()
mainWindow!.setSkipTaskbar(false)
2018-09-08 08:12:19 +02:00
}
})
}
2019-04-20 08:44:22 +02:00
async function createWindow() {
/**
2018-03-21 04:22:45 +01:00
* List accounts
*/
const accounts = await listAccounts()
2019-04-17 12:52:01 +02:00
const accountsChange: Array<MenuItemConstructorOptions> = accounts.map((a, index) => {
return {
label: a.domain,
accelerator: `CmdOrCtrl+${index + 1}`,
click: () => changeAccount(a, index)
}
})
/**
* Get language
*/
const language = await getLanguage()
i18n.changeLanguage(language)
/**
* Set application menu
*/
2018-08-10 17:40:06 +02:00
ApplicationMenu(accountsChange, i18n)
/**
* Set dock menu for mac
*/
2018-07-21 07:29:52 +02:00
if (process.platform === 'darwin') {
const dockMenu = Menu.buildFromTemplate(accountsChange)
app.dock.setMenu(dockMenu)
}
2019-03-10 13:07:19 +01:00
/**
* Enable accessibility
*/
app.setAccessibilitySupportEnabled(true)
/**
* Initial window options
*/
let mainWindowState = windowStateKeeper({
defaultWidth: 1000,
defaultHeight: 563
})
const mainOpts: BrowserWindowConstructorOptions = {
titleBarStyle: 'hidden',
x: mainWindowState.x,
y: mainWindowState.y,
width: mainWindowState.width,
height: mainWindowState.height,
useContentSize: true,
icon: path.resolve(__dirname, '../../build/icons/256x256.png'),
webPreferences: {
// It is required to use ipcRenderer in renderer process.
// But it is not secure, so if you want to disable this option, please use preload script.
nodeIntegration: true
}
}
const config: Config = {
windowOpts: mainOpts,
templateUrl: splashURL,
splashScreenOpts: {
width: 425,
height: 325
}
}
mainWindow = initSplashScreen(config)
2018-03-07 14:28:48 +01:00
mainWindowState.manage(mainWindow)
2018-07-28 13:44:16 +02:00
mainWindow.loadURL(winURL)
2018-03-07 14:28:48 +01:00
2019-04-20 08:44:22 +02:00
mainWindow.webContents.on('will-navigate', event => event.preventDefault())
mainWindow.on('closed', () => {
mainWindow = null
})
2018-09-08 08:12:19 +02:00
// Minimize to tray for win32
if (process.platform === 'win32') {
setMinimizeToTray()
}
2018-03-07 14:28:48 +01:00
}
// Do not lower the rendering priority of Chromium when background
app.commandLine.appendSwitch('disable-renderer-backgrounding')
2018-03-07 14:28:48 +01:00
app.on('ready', createWindow)
app.on('window-all-closed', () => {
// this action is called when user click the close button.
// In macOS, close button does not shutdown application. It is hide application window.
if (process.platform !== 'darwin') {
app.quit()
} else {
// In MacOS, we should change disable some menu items.
const menu = Menu.getApplicationMenu()
if (menu !== null) {
// Preferences
2019-04-20 08:44:22 +02:00
;((menu.items[0] as MenuItemConstructorOptions).submenu as Menu).items[2].enabled = false as boolean
// New Toot
2019-04-20 08:44:22 +02:00
;((menu.items[1] as MenuItemConstructorOptions).submenu as Menu).items[0].enabled = false as boolean
// Open Window
2019-04-20 08:44:22 +02:00
;((menu.items[4] as MenuItemConstructorOptions).submenu as Menu).items[1].enabled = true as boolean
// Jump to
2019-04-20 08:44:22 +02:00
;((menu.items[4] as MenuItemConstructorOptions).submenu as Menu).items[4].enabled = false as boolean
}
}
2018-03-07 14:28:48 +01:00
})
app.on('activate', () => {
if (mainWindow === null) {
createWindow()
}
})
let auth = new Authentication(accountManager)
2018-03-08 09:41:39 +01:00
ipcMain.on('get-auth-url', (event: Event, domain: string) => {
2019-04-20 08:44:22 +02:00
auth
.getAuthorizationUrl(domain)
.then(url => {
log.debug(url)
event.sender.send('response-get-auth-url', url)
2018-03-10 15:28:55 +01:00
// Open authorize url in default browser.
2018-03-08 09:41:39 +01:00
shell.openExternal(url)
})
2019-04-20 08:44:22 +02:00
.catch(err => {
log.error(err)
event.sender.send('error-get-auth-url', err)
})
2018-03-08 09:41:39 +01:00
})
ipcMain.on('get-access-token', (event: Event, code: string) => {
2019-04-20 08:44:22 +02:00
auth
.getAccessToken(code)
.then(token => {
accountDB.findOne(
{
accessToken: token
},
(err, doc: any) => {
if (err) return event.sender.send('error-get-access-token', err)
if (isEmpty(doc)) return event.sender.send('error-get-access-token', 'error document is empty')
event.sender.send('response-get-access-token', doc._id)
}
)
})
2019-04-20 08:44:22 +02:00
.catch(err => {
log.error(err)
event.sender.send('error-get-access-token', err)
})
2018-03-08 09:41:39 +01:00
})
// environments
2019-04-17 12:52:01 +02:00
ipcMain.on('get-social-token', (event: Event) => {
const token = process.env.SOCIAL_TOKEN
2019-04-16 13:38:02 +02:00
if (isEmpty(token)) {
return event.sender.send('error-get-social-token', new EmptyTokenError())
}
event.sender.send('response-get-social-token', token)
})
// nedb
2019-04-17 12:52:01 +02:00
ipcMain.on('list-accounts', (event: Event) => {
2019-04-20 08:44:22 +02:00
accountManager
.listAccounts()
.catch(err => {
log.error(err)
event.sender.send('error-list-accounts', err)
2018-03-08 15:08:33 +01:00
})
2019-04-20 08:44:22 +02:00
.then(accounts => {
event.sender.send('response-list-accounts', accounts)
2018-03-08 15:08:33 +01:00
})
})
ipcMain.on('get-local-account', (event: Event, id: string) => {
2019-04-20 08:44:22 +02:00
accountManager
.getAccount(id)
.catch(err => {
log.error(err)
2018-03-09 09:36:57 +01:00
event.sender.send('error-get-local-account', err)
})
2019-04-20 08:44:22 +02:00
.then(account => {
event.sender.send('response-get-local-account', account)
2018-03-09 09:36:57 +01:00
})
})
ipcMain.on('update-account', (event: Event, acct: LocalAccount) => {
2019-04-20 08:44:22 +02:00
accountManager
.refresh(acct)
.then(ac => {
event.sender.send('response-update-account', ac)
})
2019-04-20 08:44:22 +02:00
.catch(err => {
event.sender.send('error-update-account', err)
})
})
ipcMain.on('remove-account', (event: Event, id: string) => {
2019-04-20 08:44:22 +02:00
accountManager
.removeAccount(id)
.then(id => {
stopUserStreaming(id)
event.sender.send('response-remove-account', id)
})
2019-04-20 08:44:22 +02:00
.catch(err => {
event.sender.send('error-remove-account', err)
})
})
ipcMain.on('forward-account', (event: Event, acct: LocalAccount) => {
2019-04-20 08:44:22 +02:00
accountManager
.forwardAccount(acct)
2018-04-02 02:07:09 +02:00
.then(() => {
event.sender.send('response-forward-account')
})
2019-04-20 08:44:22 +02:00
.catch(err => {
log.error(err)
2018-04-02 02:07:09 +02:00
event.sender.send('error-forward-account', err)
})
})
ipcMain.on('backward-account', (event: Event, acct: LocalAccount) => {
2019-04-20 08:44:22 +02:00
accountManager
.backwardAccount(acct)
.then(() => {
event.sender.send('response-backward-account')
})
2019-04-20 08:44:22 +02:00
.catch(err => {
event.sender.send('error-backward-account', err)
})
})
2019-04-17 12:52:01 +02:00
ipcMain.on('refresh-accounts', (event: Event) => {
2019-04-20 08:44:22 +02:00
accountManager
.refreshAccounts()
.then(accounts => {
event.sender.send('response-refresh-accounts', accounts)
})
2019-04-20 08:44:22 +02:00
.catch(err => {
event.sender.send('error-refresh-accounts', err)
})
})
2019-04-17 12:52:01 +02:00
ipcMain.on('remove-all-accounts', (event: Event) => {
2019-04-20 08:44:22 +02:00
accountManager
.removeAll()
.then(() => {
event.sender.send('response-remove-all-accounts')
})
2019-04-20 08:44:22 +02:00
.catch(err => {
log.error(err)
event.sender.send('error-remove-all-accounts', err)
})
})
// badge
ipcMain.on('reset-badge', () => {
if (process.platform === 'darwin') {
app.dock.setBadge('')
}
})
// user streaming
let userStreamings: { [key: string]: StreamingManager | null } = {}
ipcMain.on('start-all-user-streamings', (event: Event, accounts: Array<LocalAccount>) => {
accounts.map(account => {
const id: string = account._id!
accountManager
.getAccount(id)
.then(acct => {
// Stop old user streaming
if (userStreamings[id]) {
userStreamings[id]!.stop()
userStreamings[id] = null
}
userStreamings[id] = new StreamingManager(acct, true)
userStreamings[id]!.startUser(
(update: Status) => {
if (!event.sender.isDestroyed()) {
event.sender.send(`update-start-all-user-streamings-${id}`, update)
}
},
(notification: RemoteNotification) => {
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('•')
}
// In macOS and Windows, sometimes window is closed (not quit).
// But streamings are always running.
// When window is closed, we can not send event to webContents; because it is already destroyed.
// So we have to guard it.
if (!event.sender.isDestroyed()) {
// To update notification timeline
event.sender.send(`notification-start-all-user-streamings-${id}`, notification)
// Does not exist a endpoint for only mention. And mention is a part of notification.
// So we have to get mention from notification.
if (notification.type === 'mention') {
event.sender.send(`mention-start-all-user-streamings-${id}`, notification)
}
}
},
(statusId: string) => {
if (!event.sender.isDestroyed()) {
event.sender.send(`delete-start-all-user-streamings-${id}`, statusId)
}
},
(err: Error) => {
log.error(err)
// In macOS, sometimes window is closed (not quit).
// When window is closed, we can not send event to webContents; because it is destroyed.
// So we have to guard it.
if (!event.sender.isDestroyed()) {
event.sender.send('error-start-all-user-streamings', err)
}
}
)
})
.catch((err: Error) => {
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).map((key: string) => {
if (userStreamings[key]) {
userStreamings[key]!.stop()
userStreamings[key] = null
}
})
})
/**
* Stop an user streaming in all user streamings.
* @param id specified user id in nedb.
*/
const stopUserStreaming = (id: string) => {
Object.keys(userStreamings).map((key: string) => {
if (key === id && userStreamings[id]) {
userStreamings[id]!.stop()
userStreamings[id] = null
}
})
}
type StreamingSetting = {
2019-04-20 08:44:22 +02:00
account: LocalAccount
useWebsocket: boolean
}
let directMessagesStreaming: StreamingManager | null = null
ipcMain.on('start-directmessages-streaming', (event: Event, obj: StreamingSetting) => {
const { account, useWebsocket } = obj
2019-04-20 08:44:22 +02:00
accountManager
.getAccount(account._id!)
.then(acct => {
// Stop old directmessages streaming
if (directMessagesStreaming !== null) {
directMessagesStreaming.stop()
directMessagesStreaming = null
}
directMessagesStreaming = new StreamingManager(acct, useWebsocket)
directMessagesStreaming.start(
'direct',
'',
(update: Status) => {
if (!event.sender.isDestroyed()) {
event.sender.send('update-start-directmessages-streaming', update)
}
},
(id: string) => {
if (!event.sender.isDestroyed()) {
event.sender.send('delete-start-directmessages-streaming', id)
}
},
(err: Error) => {
log.error(err)
if (!event.sender.isDestroyed()) {
event.sender.send('error-start-directmessages-streaming', err)
}
}
)
})
2019-04-20 08:44:22 +02:00
.catch(err => {
log.error(err)
if (!event.sender.isDestroyed()) {
event.sender.send('error-start-directmessages-streaming', err)
}
})
})
2019-04-17 12:52:01 +02:00
ipcMain.on('stop-directmessages-streaming', () => {
if (directMessagesStreaming !== null) {
directMessagesStreaming.stop()
directMessagesStreaming = null
}
})
let localStreaming: StreamingManager | null = null
ipcMain.on('start-local-streaming', (event: Event, obj: StreamingSetting) => {
const { account, useWebsocket } = obj
2019-04-20 08:44:22 +02:00
accountManager
.getAccount(account._id!)
.then(acct => {
// Stop old local streaming
if (localStreaming !== null) {
localStreaming.stop()
localStreaming = null
}
localStreaming = new StreamingManager(acct, useWebsocket)
localStreaming.start(
'public/local',
'',
(update: Status) => {
if (!event.sender.isDestroyed()) {
event.sender.send('update-start-local-streaming', update)
}
},
(id: string) => {
if (!event.sender.isDestroyed()) {
event.sender.send('delete-start-local-streaming', id)
}
},
(err: Error) => {
log.error(err)
if (!event.sender.isDestroyed()) {
event.sender.send('error-start-local-streaming', err)
}
}
)
})
2019-04-20 08:44:22 +02:00
.catch(err => {
log.error(err)
if (!event.sender.isDestroyed()) {
event.sender.send('error-start-local-streaming', err)
}
})
})
2019-04-17 12:52:01 +02:00
ipcMain.on('stop-local-streaming', () => {
if (localStreaming !== null) {
localStreaming.stop()
localStreaming = null
}
})
let publicStreaming: StreamingManager | null = null
ipcMain.on('start-public-streaming', (event: Event, obj: StreamingSetting) => {
const { account, useWebsocket } = obj
2019-04-20 08:44:22 +02:00
accountManager
.getAccount(account._id!)
.then(acct => {
// Stop old public streaming
if (publicStreaming !== null) {
publicStreaming.stop()
publicStreaming = null
}
publicStreaming = new StreamingManager(acct, useWebsocket)
publicStreaming.start(
'public',
'',
(update: Status) => {
if (!event.sender.isDestroyed()) {
event.sender.send('update-start-public-streaming', update)
}
},
(id: string) => {
if (!event.sender.isDestroyed()) {
event.sender.send('delete-start-public-streaming', id)
}
},
(err: Error) => {
log.error(err)
if (!event.sender.isDestroyed()) {
event.sender.send('error-start-public-streaming', err)
}
}
)
})
2019-04-20 08:44:22 +02:00
.catch(err => {
log.error(err)
if (!event.sender.isDestroyed()) {
event.sender.send('error-start-public-streaming', err)
}
})
})
2019-04-17 12:52:01 +02:00
ipcMain.on('stop-public-streaming', () => {
if (publicStreaming !== null) {
publicStreaming.stop()
publicStreaming = null
}
})
let listStreaming: StreamingManager | null = null
type ListID = {
2019-05-27 15:56:54 +02:00
listID: string
}
ipcMain.on('start-list-streaming', (event: Event, obj: ListID & StreamingSetting) => {
2018-12-18 15:26:59 +01:00
const { listID, account, useWebsocket } = obj
2019-04-20 08:44:22 +02:00
accountManager
.getAccount(account._id!)
.then(acct => {
// Stop old list streaming
if (listStreaming !== null) {
listStreaming.stop()
listStreaming = null
}
2018-12-18 15:26:59 +01:00
listStreaming = new StreamingManager(acct, useWebsocket)
listStreaming.start(
'list',
2018-12-18 15:26:59 +01:00
`list=${listID}`,
(update: Status) => {
if (!event.sender.isDestroyed()) {
event.sender.send('update-start-list-streaming', update)
}
},
(id: string) => {
if (!event.sender.isDestroyed()) {
event.sender.send('delete-start-list-streaming', id)
}
},
(err: Error) => {
log.error(err)
if (!event.sender.isDestroyed()) {
event.sender.send('error-start-list-streaming', err)
}
}
)
})
2019-04-20 08:44:22 +02:00
.catch(err => {
log.error(err)
if (!event.sender.isDestroyed()) {
event.sender.send('error-start-list-streaming', err)
}
})
})
2019-04-17 12:52:01 +02:00
ipcMain.on('stop-list-streaming', () => {
2018-06-01 06:09:42 +02:00
if (listStreaming !== null) {
listStreaming.stop()
listStreaming = null
}
})
let tagStreaming: StreamingManager | null = null
type Tag = {
tag: string
}
ipcMain.on('start-tag-streaming', (event: Event, obj: Tag & StreamingSetting) => {
const { tag, account, useWebsocket } = obj
2019-04-20 08:44:22 +02:00
accountManager
.getAccount(account._id!)
.then(acct => {
// Stop old tag streaming
if (tagStreaming !== null) {
tagStreaming.stop()
tagStreaming = null
}
tagStreaming = new StreamingManager(acct, useWebsocket)
tagStreaming.start(
'hashtag',
`tag=${tag}`,
(update: Status) => {
if (!event.sender.isDestroyed()) {
event.sender.send('update-start-tag-streaming', update)
}
},
(id: string) => {
if (!event.sender.isDestroyed()) {
event.sender.send('delete-start-tag-streaming', id)
}
},
(err: Error) => {
log.error(err)
if (!event.sender.isDestroyed()) {
event.sender.send('error-start-tag-streaming', err)
}
}
)
})
2019-04-20 08:44:22 +02:00
.catch(err => {
log.error(err)
if (!event.sender.isDestroyed()) {
event.sender.send('error-start-tag-streaming', err)
}
})
})
2019-04-17 12:52:01 +02:00
ipcMain.on('stop-tag-streaming', () => {
if (tagStreaming !== null) {
tagStreaming.stop()
tagStreaming = null
}
})
// sounds
2019-04-17 12:52:01 +02:00
ipcMain.on('fav-rt-action-sound', () => {
const preferences = new Preferences(preferencesDBPath)
2019-04-20 08:44:22 +02:00
preferences
.load()
.then(conf => {
if (conf.general.sound.fav_rb) {
const sound = path.join(soundBasePath, 'operation_sound01.wav')
simplayer(sound, (err: Error) => {
if (err) log.error(err)
})
}
})
.catch(err => log.error(err))
})
2019-04-17 12:52:01 +02:00
ipcMain.on('toot-action-sound', () => {
const preferences = new Preferences(preferencesDBPath)
2019-04-20 08:44:22 +02:00
preferences
.load()
.then(conf => {
if (conf.general.sound.toot) {
const sound = path.join(soundBasePath, 'operation_sound02.wav')
simplayer(sound, (err: Error) => {
if (err) log.error(err)
})
}
})
.catch(err => log.error(err))
})
// preferences
2019-04-17 12:52:01 +02:00
ipcMain.on('get-preferences', (event: Event) => {
const preferences = new Preferences(preferencesDBPath)
2019-04-20 08:44:22 +02:00
preferences
.load()
.then(conf => {
event.sender.send('response-get-preferences', conf)
})
2019-04-20 08:44:22 +02:00
.catch(err => {
event.sender.send('error-get-preferences', err)
})
})
ipcMain.on('update-preferences', (event: Event, data: any) => {
const preferences = new Preferences(preferencesDBPath)
2019-04-20 08:44:22 +02:00
preferences
.update(data)
.then(conf => {
event.sender.send('response-update-preferences', conf)
})
2019-04-20 08:44:22 +02:00
.catch(err => {
event.sender.send('error-update-preferences', err)
})
})
2019-04-17 12:52:01 +02:00
ipcMain.on('change-collapse', (_event: Event, value: boolean) => {
const preferences = new Preferences(preferencesDBPath)
2019-04-20 08:44:22 +02:00
preferences
.update({
state: {
collapse: value
}
})
2019-04-20 08:44:22 +02:00
.catch(err => {
log.error(err)
})
})
2019-04-17 12:52:01 +02:00
ipcMain.on('get-collapse', (event: Event) => {
const preferences = new Preferences(preferencesDBPath)
2019-04-20 08:44:22 +02:00
preferences.load().then(conf => {
event.sender.send('response-get-collapse', conf.state.collapse)
})
})
ipcMain.on('change-global-header', (event: Event, value: boolean) => {
const preferences = new Preferences(preferencesDBPath)
2019-04-20 08:44:22 +02:00
preferences
.update({
state: {
hideGlobalHeader: value
}
})
2019-04-20 08:44:22 +02:00
.then(conf => {
event.sender.send('response-change-global-header', conf)
})
.catch(err => {
log.error(err)
})
})
2019-04-17 12:52:01 +02:00
ipcMain.on('get-global-header', (event: Event) => {
const preferences = new Preferences(preferencesDBPath)
2019-04-20 08:44:22 +02:00
preferences.load().then(conf => {
event.sender.send('response-get-global-header', conf.state.hideGlobalHeader)
})
})
ipcMain.on('change-language', (event: Event, value: string) => {
const preferences = new Preferences(preferencesDBPath)
2019-04-20 08:44:22 +02:00
preferences
.update({
language: {
language: value
}
})
2019-04-20 08:44:22 +02:00
.then(conf => {
i18n.changeLanguage(conf.language.language)
event.sender.send('response-change-language', conf.language.language)
})
})
2018-06-01 07:19:56 +02:00
// hashtag
ipcMain.on('save-hashtag', (event: Event, tag: string) => {
2018-06-01 07:19:56 +02:00
const hashtags = new Hashtags(hashtagsDB)
2019-04-20 08:44:22 +02:00
hashtags
.insertTag(tag)
.then(() => {
event.sender.send('response-save-hashtag')
})
2019-04-20 08:44:22 +02:00
.catch(err => {
2018-06-01 07:19:56 +02:00
log.error(err)
})
})
2019-04-17 12:52:01 +02:00
ipcMain.on('list-hashtags', (event: Event) => {
const hashtags = new Hashtags(hashtagsDB)
2019-04-20 08:44:22 +02:00
hashtags
.listTags()
.then(tags => {
event.sender.send('response-list-hashtags', tags)
})
2019-04-20 08:44:22 +02:00
.catch(err => {
event.sender.send('error-list-hashtags', err)
})
})
ipcMain.on('remove-hashtag', (event: Event, tag: LocalTag) => {
2018-06-02 08:30:20 +02:00
const hashtags = new Hashtags(hashtagsDB)
2019-04-20 08:44:22 +02:00
hashtags
.removeTag(tag)
2018-06-02 08:30:20 +02:00
.then(() => {
event.sender.send('response-remove-hashtag')
})
2019-04-20 08:44:22 +02:00
.catch(err => {
2018-06-02 08:30:20 +02:00
event.sender.send('error-remove-hashtag', err)
})
})
2018-09-25 18:02:36 +02:00
// Fonts
2019-04-17 12:52:01 +02:00
ipcMain.on('list-fonts', (event: Event) => {
2018-09-25 18:02:36 +02:00
Fonts()
.then(list => {
event.sender.send('response-list-fonts', list)
})
.catch(err => {
event.sender.send('error-list-fonts', err)
})
})
// Unread notifications
ipcMain.on('get-unread-notification', (event: Event, accountID: string) => {
2019-04-20 08:44:22 +02:00
unreadNotification
.findOne({
accountID: accountID
})
.then(doc => {
event.sender.send('response-get-unread-notification', doc)
})
.catch(err => {
console.warn(err)
event.sender.send('error-get-unread-notification', err)
})
})
ipcMain.on('update-unread-notification', (event: Event, config: UnreadNotificationConfig) => {
const { accountID } = config
2019-04-20 08:44:22 +02:00
unreadNotification
.insertOrUpdate(accountID!, config)
.then(_ => {
event.sender.send('response-update-unread-notification', true)
})
.catch(err => {
console.error(err)
event.sender.send('error-update-unread-notification', err)
})
})
// Application control
2019-04-17 12:52:01 +02:00
ipcMain.on('relaunch', () => {
app.relaunch()
app.exit()
})
2018-03-07 14:28:48 +01:00
/**
* 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
*/
/*
import { autoUpdater } from 'electron-updater'
autoUpdater.on('update-downloaded', () => {
autoUpdater.quitAndInstall()
})
app.on('ready', () => {
if (process.env.NODE_ENV === 'production') autoUpdater.checkForUpdates()
})
*/
class EmptyTokenError {}
2018-08-10 17:40:06 +02:00
/**
* Set application menu
*/
2019-04-17 12:52:01 +02:00
const ApplicationMenu = (accountsChange: Array<MenuItemConstructorOptions>, i18n: i18n.i18n) => {
2018-08-10 17:40:06 +02:00
/**
* For mac menu
*/
2019-04-20 08:44:22 +02:00
const macGeneralMenu: Array<MenuItemConstructorOptions> =
process.platform !== 'darwin'
? []
: [
{
type: 'separator'
},
{
label: i18n.t('main_menu.application.services'),
role: 'services',
submenu: []
},
{
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'
}
]
2018-08-10 17:40:06 +02:00
2019-04-17 12:52:01 +02:00
const template: Array<MenuItemConstructorOptions> = [
2018-08-10 17:40:06 +02:00
{
label: i18n.t('main_menu.application.name'),
submenu: [
{
label: i18n.t('main_menu.application.about'),
role: 'about',
click: () => {
openAboutWindow({
icon_path: path.resolve(__dirname, '../../build/icons/256x256.png'),
copyright: 'Copyright (c) 2018 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')
2018-08-10 17:40:06 +02:00
}
},
...macGeneralMenu,
{
type: 'separator'
},
{
label: i18n.t('main_menu.application.quit'),
accelerator: 'CmdOrCtrl+Q',
role: 'quit'
}
]
},
{
label: i18n.t('main_menu.toot.name'),
submenu: [
{
label: i18n.t('main_menu.toot.new'),
accelerator: 'CmdOrCtrl+N',
click: () => {
mainWindow!.webContents.send('CmdOrCtrl+N')
2018-08-10 17:40:06 +02:00
}
}
]
},
{
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'
}
]
},
{
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: [
{
label: i18n.t('main_menu.window.close'),
role: 'close'
},
{
label: i18n.t('main_menu.window.open'),
enabled: false,
click: () => {
reopenWindow()
}
},
2018-08-10 17:40:06 +02:00
{
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')
2018-08-10 17:40:06 +02:00
}
},
{
type: 'separator'
},
...accountsChange
]
}
]
2019-04-17 12:52:01 +02:00
const menu: Menu = Menu.buildFromTemplate(template)
2018-08-10 17:40:06 +02:00
Menu.setApplicationMenu(menu)
}
2019-04-20 08:44:22 +02:00
async function reopenWindow() {
if (mainWindow === null) {
await createWindow()
return null
} else {
return null
}
}
const createNotification = (notification: RemoteNotification, notifyConfig: Notify): NotificationConstructorOptions | null => {
switch (notification.type) {
case 'favourite':
if (notifyConfig.favourite) {
return {
title: i18n.t('notification.favourite.title'),
body: i18n.t('notification.favourite.body', { username: username(notification.account) }),
silent: false
} as NotificationConstructorOptions
}
break
case 'follow':
if (notifyConfig.follow) {
return {
title: i18n.t('notification.follow.title'),
body: i18n.t('notification.follow.body', { username: username(notification.account) }),
silent: false
} as NotificationConstructorOptions
}
break
case 'mention':
if (notifyConfig.reply) {
return {
title: `${username(notification.status!.account)}`,
body: sanitizeHtml(notification.status!.content, {
allowedTags: [],
allowedAttributes: []
}),
silent: false
} as NotificationConstructorOptions
}
break
case 'reblog':
if (notifyConfig.reblog) {
return {
title: i18n.t('notification.reblog.title'),
body: i18n.t('notification.reblog.body', { username: username(notification.account) }),
silent: false
} as NotificationConstructorOptions
}
break
default:
break
}
return null
}
const username = (account: RemoteAccount): string => {
if (account.display_name !== '') {
return account.display_name
} else {
return account.username
}
}