UNPKG

xuanxuan

Version:

企业即时通讯平台

963 lines (859 loc) 29.5 kB
import 'ion-sound'; import Path from 'path'; import React from 'react'; import ReactDOM from 'react-dom'; import Events from 'Events'; import Config from 'Config'; import Helper from 'Utils/helper'; import R, {EVENT} from 'Resource'; import User, {USER_STATUS}from 'Models/user'; import ReadyNotifier from 'Models/ready-notifier'; import API from 'Models/api'; import Socket from 'Models/socket'; import DAO from 'Models/dao'; import {ChatApp} from 'Models/apps'; import AboutView from 'Views/misc/about'; import ContactView from 'Views/contacts/contact'; import ConfirmCloseWindow from 'Views/windows/confirm-close-window'; import Modal from 'Components/modal'; import ImageView from 'Components/image-view'; import Lang from 'Lang'; import Theme from 'Theme'; import UserSettingView from 'Views/user-settings'; /** * Application * * Only for renderer process */ class AppBase extends ReadyNotifier { /** * Application constructor */ constructor() { super(); this.event = Events; this.config = Config; this.lang = Lang; this.config.ready(() => { this.resetUser(this.config.user); this._checkReady(); }); this.config.load(this.userDataPath); this.$ = { chat: new ChatApp(this) }; Object.keys(this.$).forEach(appName => { return this[appName] = this.$[appName]; }); this._initEvents(); if(window.ion) { window.ion.sound({ sounds: [ {name: 'message'} ], multiplay: true, volume: 1, path: this.config.soundPath, preload: true, }); if(DEBUG) { console.groupCollapsed('%cSOUND inited', 'display: inline-block; font-size: 10px; color: #689F38; background: #CCFF90; border: 1px solid #CCFF90; padding: 1px 5px; border-radius: 2px;'); console.log('ion', window.ion); console.groupEnd(); } } } makeLocalFileUrl(url) { return url; } get browserWindow() { throw new Error('Application.browserWindow getter should be implement in sub class.'); } get desktopPath() { if(this._desktopPath !== undefined) { return this._desktopPath; } throw new Error('Application.desktopPath getter should be implement in sub class.'); } get userDataPath() { throw new Error('Application.userDataPath getter should be implement in sub class.'); } get appRoot() { if(this._appRoot !== undefined) { return this._appRoot; } throw new Error('Application.appRoot getter should be implement in sub class.'); } _checkReady() { if(this.config && this.config.isReady) { this.ready(); } } openExternal(path, options) { throw new Error('Application.openExternal(path, options) should be implement in sub class.'); } requestAttention(attentions) { throw new Error('Application.requestAttention(attentions) should be implement in sub class.'); } setShowInTaskbar(flag) { throw new Error('Application.setShowInTaskbar(flag) should be implement in sub class.'); } /** * Initial function to init events * @return {void} */ _initEvents() { this._isWindowFocus = true; this._isWindowMinimized = false; this._isWindowHide = false; this.on(R.event.ui_link, link => { if(link.action === 'URL') { this.openExternal(link.target) } else if(link.action === 'Member' && link.target) { let member = this.dao.getMember(link.target); if(member) { Modal.show({ content: () => { return <ContactView onSendBtnClick={() => { Modal.hide(); }} member={member}/>; }, width: 500, actions: false }); } } }); this.on(R.event.database_rebuild, dbVersion => { Modal.show({ modal: true, closeButton: false, content: this.lang.main.databaseUpdateTip, width: 360, actions: [{type: 'cancel', label: this.lang.common.later}, {type: 'submit', label: this.lang.main.reload}], onSubmit: () => { this.reloadApp(); } }); }); this.on(R.event.socket_close, e => { this.user.changeStatus(this.user.isOnline ? USER_STATUS.disconnect : USER_STATUS.unverified, Lang.errors.SOCKET_CLOSE, 'socket_error'); }); this.on(R.event.socket_error, e => { this.user.changeStatus(this.user.isOnline ? USER_STATUS.disconnect : USER_STATUS.unverified, Lang.errors.SOCKET_ERROR, 'socket_error'); }); this.on(R.event.socket_timeout, e => { this.user.changeStatus(this.user.isOnline ? USER_STATUS.disconnect : USER_STATUS.unverified, Lang.errors.SOCKET_TIMEOUT, 'socket_error'); }); this.on(R.event.net_online, () => { if(this.user.isDisconnect) { this.emit(R.event.ui_messager, { id: 'autoLoginingMessager', clickAway: false, autoHide: false, content: Lang.login.autoLogining }); this.login(); } }); this.on(R.event.net_offline, () => { if(this.user.isOnline) { this.user.changeStatus(USER_STATUS.disconnect, Lang.errors.NET_OFFLINE, 'net_offline'); this.emit(R.event.ui_messager, { id: 'netOfflineMessager', clickAway: false, autoHide: false, content: Lang.errors.NET_OFFLINE, color: Theme.color.negative }); } }); this.on(R.event.user_kickoff, e => { this.user.changeStatus(USER_STATUS.unverified, Lang.errors.KICKOFF, 'kickoff'); }); this.browserWindow.on('focus', () => { this._isWindowFocus = true; this.emit(R.event.ui_focus_main_window); this.requestAttention(false); }); this.browserWindow.on('blur', () => { this._isWindowFocus = false; if(!this.user || this.user.isUnverified) { return; } if(this.user.getConfig('ui.app.hideWindowOnBlur')) { this.browserWindow.minimize(); } }); this.browserWindow.on('restore', () => { this._isWindowMinimized = false; if(!this.user || this.user.isUnverified) { return; } this.setShowInTaskbar(true); this.emit(R.event.ui_show_main_window); }); this.browserWindow.on('minimize', () => { this._isWindowMinimized = true; if(!this.user || this.user.isUnverified) { return; } if(this.user.getConfig('ui.app.removeFromTaskbarOnHide')) { this.setShowInTaskbar(false); } this.emit(R.event.ui_show_main_window); }); this.on(R.event.app_main_window_close, () => { if(!this.user || this.user.isUnverified) { this.quit(); return; } let userCloseOption = this.user.getConfig('ui.app.onClose', 'ask'); const handleCloseOption = option => { if(!option) option = userCloseOption; if(option === 'minimize') { this.browserWindow.minimize(); } else { this.browserWindow.hide(); if(DEBUG) console.error('WINDOW CLOSE...'); this.quit(); } }; if(userCloseOption !== 'close' && userCloseOption !== 'minimize') { userCloseOption = ''; Modal.show({ modal: true, header: this.lang.main.askOnCloseWindow.title, content: () => { return <ConfirmCloseWindow onOptionChange={select => { userCloseOption = select; }}/>; }, width: 400, actions: [{type: 'cancel'}, {type: 'submit'}], onSubmit: () => { if(userCloseOption) { if(userCloseOption.remember) { this.user.setConfig('ui.app.onClose', userCloseOption.option); } handleCloseOption(userCloseOption.option); } } }); this.showAndFocusWindow(); this.requestAttention(1); } else { handleCloseOption(userCloseOption); } }); this.on(R.event.app_quit, () => { this.quit(); }); this.on(R.event.user_config_change, (user) => { if(user.identify === this.user.identify) { this.delaySaveUser(); } }); } /** * Bind event * @param {String} event * @param {Function} listener * @return {Symbol} */ on(event, listener) { return this.event.on(event, listener); } /** * Bind once event */ once(event, listener) { return this.event.once(event, listener); } /** * Unbind event by name * @param {...[Symbol]} names * @return {Void} */ off(...names) { this.event.off(...names); } /** * Emit event */ emit(names, ...args) { this.event.emit(names, ...args); } /** * Get current user */ get user() { return this._user; } /** * Set current user */ set user(user) { this.resetUser(user, true); } /** * Set current user with options */ resetUser(user, saveConfig, notifyRemote) { const oldIdentify = this._user ? this._user.identify : null; if(!(user instanceof User)) { user = this.config.getUser(user); } if(this.saveUserTimerTask && this._user) { this.config.save(this._user); } user.listenStatus = true; this._user = user; if(oldIdentify !== user.identify) { this.badgeLabel = false; this.trayTooltip = false; this.emit(R.event.user_swap, user); } this.emit(EVENT.user_change, user); if(saveConfig) this.config.save(user); return user; } /** * Save user */ saveUser(user) { if(user) { this.resetUser(user, true); } else { this.config.save(this.user); } if(this.saveUserTimerTask) { clearTimeout(this.saveUserTimerTask); this.saveUserTimerTask = null; } } /** * Delay save user config * @return {void} */ delaySaveUser() { clearTimeout(this.saveUserTimerTask); this.saveUserTimerTask = null; if(this.user) { this.saveUserTimerTask = setTimeout(() => { this.saveUser(); }, 5000); } } /** * Do user login action */ login(user) { if(!user) user = this.user; if(!(user instanceof User)) { user = this.config.getUser(user); } if(user.isNewApi) { this.isUserLogining = true; this.emit(EVENT.user_login_begin, user); this.off(this._handleUserLoginFinishEvent); this._handleUserLoginFinishEvent = this.once(EVENT.user_login_message, (serverUser, error) => { this._handleUserLoginFinishEvent = false; this._handleUserLoginFinish(user, serverUser, error); }); API.requestServerInfo(user).then(user => { if(this.socket) { this.socket.destroy(); } this.socket = new Socket(this, user); this.emit(EVENT.app_socket_change, this.socket); }).catch((err) => { err.oringeMessage = err.message; err.message = Lang.errors[err && err.code ? err.code : 'WRONG_CONNECT'] || err.message; if(DEBUG) console.error(err); this.emit(EVENT.user_login_message, null, err); }); } else { return this.oldLogin(user); } } /** * Login with user for old version * @param {object} user * @return {void} */ oldLogin(user) { this.isUserLogining = true; this.emit(EVENT.user_login_begin, user); this.off(this._handleUserLoginFinishEvent); this._handleUserLoginFinishEvent = this.once(EVENT.user_login_message, (serverUser, error) => { this._handleUserLoginFinishEvent = false; this._handleUserLoginFinish(user, serverUser, error); }); API.getZentaoConfig(user.serverUrlRoot).then(zentaoConfig => { user.zentaoConfig = zentaoConfig; if(this.socket) { this.socket.destroy(); } this.socket = new Socket(this, user); this.emit(EVENT.app_socket_change, this.socket); }).catch(err => { err.oringeMessage = err.message; err.message = Lang.errors[err && err.code ? err.code : 'WRONG_CONNECT'] || err.message; if(DEBUG) console.error(err); this.emit(EVENT.user_login_message, null, err); }); } /** * Make user data path * @return {boolean} */ _makeUserDataPath(user) { user = user || this.user; let userDataPath = Path.join(this.userDataPath, 'users/' + user.identify); user.dataPath = userDataPath; return Helper.tryMkdirp(userDataPath).then(() => { return Promise.all([ Helper.tryMkdirp(Path.join(userDataPath, 'temp/')), Helper.tryMkdirp(Path.join(userDataPath, 'images/')), Helper.tryMkdirp(Path.join(userDataPath, 'files/')) ]); }); } /** * Handle user login with api data * @param {User} user * @param {Object} serverUser * @param {Error} error * @return {Void} */ _handleUserLoginFinish(user, serverUser, error) { if(serverUser) { // update user let serverStatus = serverUser.status; delete serverUser.status; const now = new Date(); if(user.signed && (!user.lastLoginTime || (new Date(user.lastLoginTime)).toLocaleDateString() !== now.toLocaleDateString())) { setTimeout(() => { this.emit(R.event.ui_messager, { id: 'userSignedMessager', clickAway: true, autoHide: 2000, content: Lang.login.todaySigned, color: Theme.color.positive }); }, 2000); } user.lastLoginTime = now.getTime(); user.assign(serverUser); user.fixAvatar(); // init dao if(!this.dao || this.dao.dbName !== user.identify) { this.dao = new DAO(user, this); } else { this.dao.user = user; } // update socket this.socket.user = user; this.socket.dao = this.dao; // init user data path this._makeUserDataPath(user).then(() => { if(DEBUG) { console.log('%cUSER DATA PATH ' + user.dataPath, 'display: inline-block; font-size: 10px; color: #009688; background: #A7FFEB; border: 1px solid #A7FFEB; padding: 1px 5px; border-radius: 2px;'); } // set user status this.user = user; this.config.save(user); this.isUserLogining = false; this.user.changeStatus(serverStatus || 'online'); setTimeout(() => { this.emit(R.event.user_login_finish, {user: user, result: true}); }, 2000); }).catch(err => { this.user = user; let error = new Error('Cant not init user data path.'); error.code = 'USER_DATA_PATH_DENY'; this.isUserLogining = false; this.emit(R.event.user_login_finish, {user: user, result: false, error}); if(DEBUG) console.error(error); }); } else { if(this.socket) { this.socket.destroy(); } this.user = user; this.isUserLogining = false; this.emit(R.event.user_login_finish, {user: user, result: false, error}); } } /** * Logout * @return {Void} */ logout() { if(this.user) { if(this.user.isOnline) { this.config.save(this.user, true); if(this.socket) { this.socket.uploadUserSettings(this.user); this.socket.logout(this.user); } } this.user.changeStatus(USER_STATUS.unverified); } this.dao = null; } /** * Chnage user status * @param {String} status * @return {Void} */ changeUserStatus(status) { if(status !== 'offline') { this.socket.changeUserStatus(status); } else { this.logout(); } } /** * Play soudn * @param {string} sound name * @return {void} */ playSound(sound) { // determine play sound by user config window.ion.sound.play(sound); } /** * Preview file */ previewFile(path, displayName) { if(Help.isOSX) { this.browserWindow.previewFile(path, displayName); } else { // TODO: preview file on windows } } /** * Set current badage label * @param {string | false} label * @return {void} */ set badgeLabel(label = '') { if(DEBUG) { console.error('Application.setShowInTaskbar(flag) should be implement in sub class.'); } } showWindow() { this.browserWindow.show(); this._isWindowHide = false; } hideWindow() { this.browserWindow.hide(); this._isWindowHide = true; } focusWindow() { this.browserWindow.focus(); } /** * Show and focus main window * @return {void} */ showAndFocusWindow() { this.showWindow(); this.focusWindow(); } get isWindowsFocus() { return this._isWindowFocus; } /** * Check whether the main window is open and focus * @return {boolean} */ get isWindowOpenAndFocus() { return this.isWindowsFocus && this.isWindowOpen; } /** * Check whether the main window is open */ get isWindowOpen() { return !this._isWindowMinimized && !this._isWindowHide; } /** * Set tooltip text on tray icon * @param {string | false} tooltip * @return {void} */ set trayTooltip(tooltip) { throw new Error('Application.trayTooltip setter should be implement in sub class.'); } /** * Flash tray icon * @param {boolean} flash * @return {void} */ flashTrayIcon(flash = true) { throw new Error('Application.flashTrayIcon(flash) should be implement in sub class.'); } /** * Create context menu * @param {Array[Object]} items * @return {Menu} */ createContextMenu(menu) { throw new Error('Application.createContextMenu(menu) should be implement in sub class.'); } /** * Popup context menu */ popupContextMenu(menu, x, y) { throw new Error('Application.popupContextMenu(menu, x, y) should be implement in sub class.'); } /** * Show save dialog * @param object options */ showSaveDialog(options, callback) { throw new Error('Application.showSaveDialog(options, callback) should be implement in sub class.'); } /** * Show open dialog */ showOpenDialog(options, callback) { throw new Error('Application.showOpenDialog(options, callback) should be implement in sub class.'); } /** * Open dialog window * @param {Object} options * @return {Promise} */ openDialog(options) { Modal.show(options); } /** * Open member profile window * @param {Object} options * @param {Member} member * @return {Promise} */ openProfile(options, member) { let title = null; member = member || (options ? options.member : null); if(!member) { member = this.user; title = this.lang.user.profile; } if(!member) return Promise.reject('Member is null.'); options = Object.assign({ content: () => { return <ContactView onSendBtnClick={() => { Modal.hide(); }} member={member}/>; }, width: 500, actions: false }, options); Modal.show({ content: () => { return <ContactView onSendBtnClick={() => { Modal.hide(); }} member={member}/>; }, width: 500, actions: false }); } openSettingDialog(options) { let userSettingView = null; Modal.show({ header: this.lang.common.settings, content: () => { return <UserSettingView config={this.user.config} ref={e => userSettingView = e}/>; }, width: 500, actions: [ {type: 'submit'}, {type: 'cancel'}, { type: 'secondary', label: this.lang.common.restoreDefault, click: () => { userSettingView.resetConfig(); }, style: {float: Helper.isWindowsOS ? 'none' : 'left'} }, ], actionsAlign: Helper.isWindowsOS ? 'left' : 'right', onSubmit: () => { if(userSettingView.configChanged) { this.user.resetConfig(userSettingView.getConfig()); } }, modal: true }); } /** * Open about window * @return {Promise} */ openAbout() { Modal.show({ header: this.lang.common.about, content: () => { return <AboutView/>; }, width: 400, actions: null }); } /** * Get all members * @return {Array[Member]} */ get members() { return this.dao.getMembers(true); } /** * Change @user to html link tag * @param {string} text * @param {string} format * @return {string} */ linkMembersInText(text, format = '<a class="link-app {className}" href="#Member/{id}">@{displayName}</a>') { if(text.indexOf('@') > -1) { this.dao.getMembers().forEach(m => { text = text.replace(new RegExp('@(' + m.account + '|' + m.realname + ')', 'g'), format.format({displayName: m.displayName, id: m.id, account: m.account, className: m.account === this.user.account ? 'at-me' : ''})); }); } return text; } /** * change http://example.com to html link tag * @param {string} text * @param {string} format * @return {string} */ linkHyperlinkInText(text, format = '<a href="{0}">{1}</a>') { let urlPattern = '\\b((?:[a-z][\\w\\-]+:(?:\\/{1,3}|[a-z0-9%])|www\\d{0,3}[.]|[a-z0-9.\\-]+[.][a-z]{2,4}\\/)(?:[^\\s()<>]|\\((?:[^\\s()<>]|(?:\\([^\\s()<>]+\\)))*\\))+(?:\\((?:[^\\s()<>]|(?:\\([^\\s()<>]+\\)))*\\)|[^\\s`!()\\[\\]{};:\'".,<>?«»“”‘’]))'; return text.replace(new RegExp(urlPattern, 'ig'), url => { let colonIdx = url.indexOf(':'); if(url.includes('://') || (colonIdx < 7 && colonIdx > 0)) { return format.format(url, url); } return format.format('http://' + url, url); }); } openImagePreview(imagePath, callback) { Modal.show({ content: () => { return <ImageView sourceImage={imagePath} />; }, closeButtonStyle: {color: '#fff', fill: '#fff', background: 'rgba(0,0,0,0.2)'}, fullscreen: true, clickThrough: true, transparent: true, actions: false, onHide: callback }); } /** * Capture screenshot image and save to file * * @param string filePath optional */ captureScreen(options, filePath, hideCurrentWindow, onlyBase64) { throw new Error('Application.captureScreen(options, filePath, hideCurrentWindow, onlyBase64) should be implement in sub class.'); } /** * Open capture screen window */ openCaptureScreen(screenSources = 0, hideCurrentWindow = false) { throw new Error('Application.openCaptureScreen(screenSources = 0, hideCurrentWindow = false) should be implement in sub class.'); } /** * Upload file to server with zentao API * @param {File} file * @param {object} params * @return {Promise} */ uploadFile(file, params) { return API.uploadFile(file, this.user, params); } /** * Download file from server with zentao API * @param {File} file * @param {function} onProgress * @return {Promise} */ downloadFile(file, onProgress) { if(!file.path) file.path = this.user.tempPath + file.name; if(!file.url) file.url = this.createFileDownloadLink(file, this.user); return API.downloadFile(file, this.user, onProgress); } /** * Create file download link with zentao API * @param {string} fileId * @return {string} */ createFileDownloadLink(file) { return API.createFileDownloadLink(file, this.user); } /** * Register global hotkey * @param {object} option * @param {string} name * @return {void} */ registerGlobalShortcut(name, accelerator, callback) { throw new Error('Application.registerGlobalShortcut(name, accelerator, callback) should be implement in sub class.'); } /** * Check a shortcu whether is registered */ isGlobalShortcutRegistered(accelerator) { throw new Error('Application.isGlobalShortcutRegistered(accelerator) should be implement in sub class.'); } /** * Unregister global hotkey * @param {gui.Shortcut | string | object} hotkey * @return {void} */ unregisterGlobalShortcut(name) { throw new Error('Application.unregisterGlobalShortcut(name) should be implement in sub class.'); } /** * Quit application */ quit() { this.browserWindow.hide(); this.logout(); if(this.saveUserTimerTask) { this.saveUser(); } } getDesktopCaptureSources(options, callback) { throw new Error('Application.getDesktopCaptureSources(options, callback) should be implement in sub class.'); } getPrimaryDisplay() { throw new Error('Application.getPrimaryDisplay(options, callback) should be implement in sub class.'); } getAllDisplays() { throw new Error('Application.getAllDisplays() should be implement in sub class.'); } createImageFromPath(path) { throw new Error('Application.createImageFromPath(path) should be implement in sub class.'); } getImageFromClipboard() { throw new Error('Application.getImageFromClipboard() should be implement in sub class.'); } copyImageToClipboard(image) { throw new Error('Application.copyImageToClipboard(image) should be implement in sub class.'); } openFileItem(file) { throw new Error('Application.openFileItem(file) should be implement in sub class.'); } showItemInFolder(file) { throw new Error('Application.showItemInFolder(file) should be implement in sub class.'); } } export default AppBase;