Guest User

Untitled

a guest
Jan 23rd, 2019
82
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 20.04 KB | None | 0 0
  1. import crypto from 'crypto'
  2. import electron from 'electron'
  3. import { Component } from 'react'
  4. import { func, bool, string, object } from 'prop-types'
  5. import ms from 'ms'
  6. import makeUnique from 'make-unique'
  7. import isDev from 'electron-is-dev'
  8. import setRef from 'react-refs'
  9. import Identicon from 'identicon.js'
  10. import debug from '../../utils/debug'
  11. import {
  12. wrapStyle,
  13. listStyle,
  14. itemStyle,
  15. helperStyle
  16. } from '../../styles/components/feed/switcher'
  17. import Clear from '../../vectors/clear'
  18. import Avatar from './avatar'
  19.  
  20. class Switcher extends Component {
  21. state = {
  22. teams: [],
  23. scope: null,
  24. updateFailed: false,
  25. initialized: false,
  26. syncInterval: '5s',
  27. queue: []
  28. }
  29.  
  30. remote = electron.remote || false
  31. ipcRenderer = electron.ipcRenderer || false
  32. setReference = setRef.bind(this)
  33.  
  34. load = file => {
  35. if (electron.remote) {
  36. return electron.remote.require(file)
  37. }
  38.  
  39. return null
  40. }
  41.  
  42. configUtils = this.load('./utils/config')
  43.  
  44. // Ensure that config doesn't get checked when the
  45. // file is updated from this component
  46. savingConfig = false
  47.  
  48. // ;global.timer 何時會 set:
  49. // componentdidmount 時侯就會執行 listtimer 而佢入面就會 set 佐 global.timer
  50. showWindow = () => {
  51. debug('switcher', 'showWindow', 'global.timer:', global.timer, 'this.state.syncInterval:', this.state.syncInterval)
  52. // 當曾經 componentdidmount 和曾經 hide 過 window 後,再 show window 時執行
  53. if (global.timer && this.state.syncInterval !== '5s') {
  54. // 清除 timer
  55. debug('switcher', 'showWindow', 'clear timer')
  56.  
  57. // Refresh the teams and events when the window gets
  58. // shown, so that they're always up-to-date
  59. // 載入多次 teams
  60. debug('switcher', 'showWindow', 'loadTeams')
  61. this.loadTeams()
  62.  
  63. // Restart the timer so we keep everything in sync every 5s
  64. // 開返個 timer
  65. debug('switcher', 'showWindow', 'start the timer with 5s')
  66. this.listTimer()
  67. // 因為已經 show windows, 所以 timer 每五秒更新一次
  68. this.setState({ syncInterval: '5s' })
  69. }
  70. }
  71.  
  72. // 若果此時 window 是 show 的狀態
  73. // hide window 時會 取消 timer, 重新啟用 timer 為每 5m 執行一次
  74. hideWindow = () => {
  75. debug('switcher', 'hideWindow', 'global.timer:', global.timer, 'this.state.syncInterval:', this.state.syncInterval)
  76. if (global.timer && this.state.syncInterval !== '5m') {
  77. debug('switcher', 'hideWindow', 'clear timer')
  78.  
  79. // Restart the timer so we keep everything in sync every 5m
  80. debug('switcher', 'hideWindow', 'start the timer with 5m')
  81. this.listTimer()
  82. this.setState({ syncInterval: '5m' })
  83. }
  84. }
  85.  
  86. // 當 switcher 收到 new props 時, 執行 changeScope
  87. // eslint-disable-next-line react/no-deprecated
  88. componentWillReceiveProps({ activeScope }) {
  89. debug('switcher', 'componentWillReceiveProps')
  90.  
  91. if (activeScope) {
  92. debug('switcher', 'componentWillReceiveProps', 'changeScope with activeScope')
  93. this.changeScope(activeScope, true, true, true)
  94. return
  95. }
  96.  
  97. if (this.state.scope !== null) {
  98. return
  99. }
  100.  
  101. debug('switcher', 'componentWillReceiveProps', 'this.state.scope:', this.state.scope)
  102. debug('switcher', 'componentWillReceiveProps', 'set state.scope to default scope')
  103. this.setState({
  104. scope: this.props.defaultScope
  105. })
  106. }
  107.  
  108. // 主要作用係 set global.timer
  109. // 而 global.timer 的作用就係執行 this.loadteam
  110. listTimer = () => {
  111. debug('switcher:listTimer', 'listTimer')
  112. const { getCurrentWindow } = this.remote
  113. const { isVisible } = getCurrentWindow()
  114.  
  115. const time = isVisible() ? '5s' : '5m'
  116. debug('switcher:listTimer', 'refresh time:', time)
  117.  
  118. debug('switcher:listTimer', 'clear timer', global.timer)
  119. clearTimeout(global.timer)
  120. debug('switcher:listTimer', 'start the timeout')
  121. global.timer = setTimeout(async () => {
  122. try {
  123. // It's important that this is being `await`ed
  124. debug('switcher:listTimer', 'loadTeams')
  125. await this.loadTeams()
  126. } catch (error) {
  127. if (isDev) {
  128. console.error(error)
  129. }
  130. }
  131.  
  132. // Once everything is done or has failed,
  133. // try it again after some time.
  134. debug('switcher:listTimer', 'call listTimer again!!!')
  135. this.listTimer()
  136. }, ms(time))
  137. debug('switcher:listTimer', 'global.timer:', global.timer)
  138. }
  139.  
  140. // eslint-disable-next-line react/no-deprecated
  141. componentWillMount () {
  142. }
  143.  
  144. // 增加 showWindow 同 hideWindow 的 listener
  145. // eslint-disable-next-line react/no-deprecated
  146. async componentDidMount() {
  147. debug('switcher', 'componentDidMount')
  148.  
  149. // Support SSR
  150. if (!this.remote || typeof window === 'undefined') {
  151. return
  152. }
  153.  
  154. const currentWindow = this.remote.getCurrentWindow()
  155.  
  156. if (!currentWindow) {
  157. return
  158. }
  159.  
  160. debug('switcher:componentWillMount', 'add show/hide listener')
  161. currentWindow.removeListener('show', this.showWindow)
  162. currentWindow.removeListener('hide', this.hideWindow)
  163. currentWindow.on('show', this.showWindow)
  164. currentWindow.on('hide', this.hideWindow)
  165.  
  166. window.addEventListener('beforeunload', () => {
  167. debug('switcher', 'beforeunload', 'remove show/hide listener')
  168. currentWindow.removeListener('show', this.showWindow)
  169. currentWindow.removeListener('hide', this.hideWindow)
  170. })
  171.  
  172. // 當收到 update-failed 就會 set state, 由更新委員會傳過黎
  173.  
  174. // Show a UI banner if the installation
  175. // of an update failed
  176. debug('switcher', 'componentDidMount', 'setup update-failed event')
  177. this.ipcRenderer.on('update-failed', () => {
  178. this.setState({ updateFailed: true })
  179. })
  180.  
  181. // Only start updating teams once they're loaded!
  182. // This needs to be async so that we can already
  183. // start the state timer below for the data that's already cached
  184. if (!this.props.online) {
  185. debug('switcher', 'componentDidMount', 'not online, start the list timer')
  186. this.listTimer()
  187. return
  188. }
  189.  
  190. debug('switcher', 'componentDidMount', 'loadTeams(firstLoad) (async)')
  191. this.loadTeams(true)
  192. .then(() => {
  193. debug('switcher', 'componentDidMount', 'call listTimer, after loadTeams(firstLoad) (async)')
  194. this.listTimer()
  195. })
  196. .catch(() => {
  197. debug('switcher', 'componentDidMount', 'call listTimer, after loadTeams(firstLoad) (async)')
  198. this.listTimer()
  199. })
  200.  
  201. // Check the config for `currentTeam`
  202. debug('switcher', 'componentDidMount', 'checkCurrentTeam')
  203. await this.checkCurrentTeam()
  204.  
  205. // Update the scope if the config changes
  206. debug('switcher', 'componentDidMount', 'listenToConfig')
  207. this.listenToConfig()
  208. }
  209.  
  210. componentWillUnmount () {
  211. debug('switcher:componentWillUnmount', 'componentWillUnmount')
  212.  
  213. // Support SSR
  214. if (!this.remote || typeof window === 'undefined') {
  215. return
  216. }
  217.  
  218. const currentWindow = this.remote.getCurrentWindow()
  219.  
  220. if (!currentWindow) {
  221. return
  222. }
  223.  
  224. currentWindow.removeListener('show', this.showWindow)
  225. currentWindow.removeListener('hide', this.hideWindow)
  226. }
  227.  
  228. listenToConfig() {
  229. debug('switcher', 'listenToConfig')
  230. if (!this.ipcRenderer) {
  231. return
  232. }
  233.  
  234. debug('switcher', 'listenToConfig', 'setup config-changed event')
  235. this.ipcRenderer.on('config-changed', async (event, config) => {
  236. if (this.state.teams.length === 0) {
  237. return
  238. }
  239.  
  240. if (this.savingConfig) {
  241. this.savingConfig = false
  242. return
  243. }
  244.  
  245. // Load the teams in case there is a brand new team
  246. debug('switcher', 'config-changed', 'loadTeams')
  247. await this.loadTeams()
  248.  
  249. // Check for the `currentTeam` property in the config
  250. debug('switcher', 'config-changed', 'checkCurrentTeam')
  251. await this.checkCurrentTeam(config)
  252. })
  253. }
  254.  
  255. resetScope() {
  256. debug('switcher', 'resetScope')
  257. this.changeScope({ id: this.props.defaultScope })
  258. }
  259.  
  260. async checkCurrentTeam(config) {
  261. debug('switcher', 'checkCurrentTeam')
  262. if (!this.remote) {
  263. return
  264. }
  265.  
  266. if (!config) {
  267. debug('switcher', 'checkCurrentTeam', 'getConfig')
  268. const { getConfig } = this.remote.require('./utils/config')
  269.  
  270. try {
  271. config = await getConfig()
  272. } catch (error) {
  273. // The config is not valid, so no need to update
  274. // the current team.
  275. console.log(error)
  276. return
  277. }
  278. debug('switcher', 'checkCurrentTeam', 'getConfig end', config)
  279. }
  280.  
  281. if (!config.currentTeam) {
  282. debug('switcher', 'checkCurrentTeam', 'resetScope')
  283. this.resetScope()
  284. return
  285. }
  286.  
  287. // Legacy config
  288. if (typeof config.currentTeam === 'object') {
  289. debug('switcher', 'checkCurrentTeam', 'changeScope', config.currentTeam)
  290. this.changeScope(config.currentTeam, true)
  291. return
  292. }
  293.  
  294. debug('switcher', 'checkCurrentTeam', 'getTeams')
  295. const { teams } = this.getTeams()
  296. debug('switcher', 'checkCurrentTeam', 'getTeams end')
  297. const related = teams.find(team => team.id === config.currentTeam)
  298. debug('switcher', 'checkCurrentTeam', 'get related team object', { teamId: config.currentTeam })
  299.  
  300. // The team was deleted
  301. if (!related) {
  302. debug('switcher', 'checkCurrentTeam', 'call resetScope because no found related team object')
  303. this.resetScope()
  304. return
  305. }
  306.  
  307. debug('switcher', 'checkCurrentTeam', 'call changeScope')
  308. this.changeScope(related, true)
  309. }
  310.  
  311. async saveConfig(newConfig) {
  312. debug('switcher', 'saveConfig')
  313. const { saveConfig } = this.configUtils
  314.  
  315. // Ensure that we're not handling the
  316. // event triggered by changes made to the config
  317. // because the changes were triggered manually
  318. // inside this app
  319. this.savingConfig = true
  320.  
  321. // Then update the config file
  322. debug('switcher', 'saveConfig', 'call saveConfig')
  323. await saveConfig(newConfig, 'config')
  324. }
  325.  
  326. generateAvatar = (str) => {
  327. debug('switcher:generateAvatar', 'str:', str)
  328. const hash = crypto.createHash('md5')
  329. hash.update(str)
  330. const imgData = new Identicon(hash.digest('hex')).toString()
  331. debug('switcher:generateAvatar', 'end', imgData.substr(0, 10))
  332. return 'data:image/png;base64,' + imgData
  333. }
  334.  
  335. getTeams = () => {
  336. debug('switcher', 'getTeams')
  337. const result = {
  338. teams: [
  339. {
  340. id: 'aaa',
  341. name: 'AAA',
  342. avatarUrl: this.generateAvatar('aaa111')
  343. },
  344. {
  345. id: 'bbb',
  346. name: 'BBB',
  347. avatarUrl: this.generateAvatar('bbb222')
  348. }
  349. ]
  350. }
  351.  
  352. debug('switcher', 'getTeams end')
  353. return result
  354. }
  355.  
  356. merge(first, second) {
  357. debug('switcher', 'merge')
  358. const merged = first.concat(second)
  359. return makeUnique(merged, (a, b) => a.id === b.id)
  360. }
  361.  
  362. async haveUpdated(data) {
  363. debug('switcher', 'haveUpdated')
  364. const newData = JSON.parse(JSON.stringify(data))
  365. let currentData = JSON.parse(JSON.stringify(this.state.teams))
  366.  
  367. if (currentData.length > 0) {
  368. // Remove teams that the user has left
  369. currentData = currentData.filter(team => {
  370. return Boolean(newData.find(item => item.id === team.id))
  371. })
  372. }
  373.  
  374. const ordered = this.merge(currentData, newData)
  375. debug('switcher', 'haveUpdated', 'after merge')
  376. return ordered
  377. }
  378.  
  379. // 邊到用到, showWindow, listTimer, componentDidMount
  380. // 載入 teams 並放到 state
  381. // 有時用 setState
  382. // 有時用 setTeams
  383. async loadTeams(firstLoad) {
  384. debug('switcher:loadTeams', 'firstLoad:', firstLoad)
  385. if (!this.remote) {
  386. return
  387. }
  388.  
  389. const currentWindow = this.remote.getCurrentWindow()
  390.  
  391. // 若 window 處於隱藏狀態, 並且已經經過 componentdidmount initialized
  392. // 那就 setTeams null
  393. // 為什麼呢? 為了清除所有 teams 當 window 處於隱藏狀態
  394. // If the window isn't visible, don't pull the teams
  395. // Ensure to always load the first chunk
  396. if (!currentWindow.isVisible() && this.state.initialized) {
  397. if (this.props.setTeams) {
  398. // When passing `null`, the feed will only
  399. // update the events, not the teams
  400. debug('switcher:loadTeams', 'props.setTeams null means tell feed.js update events only')
  401. await this.props.setTeams(null, firstLoad)
  402. }
  403.  
  404. return
  405. }
  406.  
  407. // Call api 返回一個 object
  408. // 裡面包含 teams
  409. debug('switcher:loadTeams', 'getTeams')
  410. const data = this.getTeams()
  411.  
  412. if (!data || !data.teams) {
  413. return
  414. }
  415.  
  416. const { teams } = data
  417.  
  418. const updated = await this.haveUpdated(teams)
  419.  
  420. const scopeExists = updated.find(team => {
  421. return this.state.scope === team.id
  422. })
  423.  
  424. if (!scopeExists) {
  425. debug('switcher:loadTeams', 'scope (team id) not found in updated teams, so reset scope')
  426. this.resetScope()
  427. }
  428.  
  429. if (updated) {
  430. debug('switcher:loadTeams', 'set temas state to updated teams')
  431. this.setState({ teams: updated })
  432. }
  433.  
  434. if (this.props.setTeams) {
  435. // When passing `null`, the feed will only
  436. // update the events, not the teams
  437. debug('switcher:loadTeams', 'call props.setTeams to tell feed.js')
  438. await this.props.setTeams(updated || null, firstLoad)
  439. }
  440. }
  441.  
  442. componentDidUpdate() {
  443. const aaa = parseInt(Math.random() * 1000, 10)
  444. console.log('componentDidUpdate1', aaa, this.state)
  445. debug('switcher', 'componentDidUpdate start')
  446.  
  447. const { teams } = this.state
  448.  
  449. console.log('componentDidUpdate2', aaa)
  450. debug('switcher', 'componentDidUpdate', 'queue length:', this.state.queue.length)
  451. while (this.state.queue.length > 0) {
  452. const { queue } = this.state
  453.  
  454. queue.shift()()
  455.  
  456. this.setState({ queue })
  457. }
  458. console.log('componentDidUpdate3', aaa)
  459. debug('switcher', 'componentDidUpdate', 'queue length:', this.state.queue.length)
  460.  
  461. debug('switcher', 'componentDidUpdate', 'this.state.initialized:', this.state.initialized)
  462. if (this.state.initialized) {
  463. return
  464. }
  465. console.log('componentDidUpdate4', aaa)
  466.  
  467. const teamsCount = teams.length
  468. debug('switcher', 'componentDidUpdate', 'teamsCount:', teamsCount)
  469.  
  470. if (teamsCount === 0) {
  471. return
  472. }
  473. console.log('componentDidUpdate5', aaa)
  474.  
  475. const when = 100 + 100 * teamsCount + 600
  476.  
  477. console.log('componentDidUpdate6', aaa)
  478. debug('switcher', 'componentDidUpdate', 'ready to set initialized state to true')
  479. setTimeout(() => {
  480. // Ensure that the animations for the teams
  481. // fading in works after recovering from offline mode
  482. if (!this.props.online) {
  483. return
  484. }
  485.  
  486. console.log('set initialized to true', aaa, this.state)
  487. debug('switcher', 'componentDidUpdate', 'set initialized state to true')
  488. this.setState({
  489. initialized: true
  490. })
  491. }, when)
  492.  
  493. console.log('componentDidUpdate7', aaa)
  494. debug('switcher', 'componentDidUpdate end')
  495. }
  496.  
  497. async updateConfig(team) {
  498. debug('switcher', 'updateConfig')
  499. if (!this.remote) {
  500. return
  501. }
  502.  
  503. const info = {
  504. currentTeam: team
  505. }
  506.  
  507. // And then update the config file
  508. debug('switcher', 'updateConfig', 'this.saveConfig')
  509. await this.saveConfig(info)
  510. }
  511.  
  512. // 當 switcher 收到 new props 時
  513. // activeScope 有改動時
  514. // 就會 changeScope
  515. // saveToConfig: 會將 team 儲存到 config file,但需要經過 queue 排序才會執行
  516. // byHand: queue 會用到 (但唔知用黎做乜)
  517. // noFeed: true 不會執行 this.props.setFeedScope
  518. changeScope(team, saveToConfig, byHand, noFeed) {
  519. debug('switcher:changeScope', { saveToConfig, byHand, noFeed })
  520. // If the clicked item in the team switcher is
  521. // already the active one, don't do anything
  522. debug('switcher:changeScope', 'change to the same scope?', this.state.scope === team.id)
  523. if (this.state.scope === team.id) {
  524. return
  525. }
  526.  
  527. if (!noFeed && this.props.setFeedScope) {
  528. // Load different messages into the feed
  529. debug('switcher:changeScope', 'call props.setFeedScope', 'team.id:', team.id)
  530. this.props.setFeedScope(team.id)
  531. }
  532.  
  533. // Make the team/user icon look active by
  534. // syncing the scope with the feed
  535. debug('switcher:changeScope', 'set state scope to', 'team.id:', team.id)
  536. this.setState({ scope: team.id })
  537.  
  538. // Save the new `currentTeam` to the config
  539. if (saveToConfig) {
  540. debug('switcher:changeScope', 'add queue function updateConfig with params')
  541. const queueFunction = (fn, context, params) => {
  542. return () => {
  543. fn.apply(context, params)
  544. }
  545. }
  546.  
  547. this.setState({
  548. queue: this.state.queue.concat([
  549. queueFunction(this.updateConfig, this, [team, byHand])
  550. ])
  551. })
  552. }
  553. }
  554.  
  555. openMenu = () => {
  556. // The menu toggler element has children
  557. // we have the ability to prevent the event from
  558. // bubbling up from those, but we need to
  559. // use `this.menu` to make sure the menu always gets
  560. // bounds to the parent
  561. const { bottom, left, height, width } = this.menu.getBoundingClientRect()
  562. const sender = electron.ipcRenderer || false
  563.  
  564. if (!sender) {
  565. return
  566. }
  567.  
  568. sender.send('open-menu', {
  569. x: left,
  570. y: bottom,
  571. height,
  572. width
  573. })
  574. }
  575.  
  576. renderItem() {
  577. // eslint-disable-next-line new-cap
  578. return ({ team }) => {
  579. const isActive = this.state.scope === team.id
  580. const index = this.state.teams.indexOf(team)
  581. const shouldScale = !this.state.initialized
  582. const { darkBg } = this.props
  583.  
  584. const classes = []
  585.  
  586. if (isActive) {
  587. classes.push('active')
  588. }
  589.  
  590. if (darkBg) {
  591. classes.push('dark')
  592. }
  593.  
  594. const clicked = event => {
  595. event.preventDefault()
  596. this.changeScope(team, true, true)
  597. }
  598.  
  599. return (
  600. <li onClick={clicked} className={classes.join(' ')} key={team.id}>
  601. <Avatar
  602. team={team}
  603. scale={shouldScale}
  604. delay={index}
  605. avatarUrl={team.avatarUrl}
  606. />
  607.  
  608. <style jsx>{itemStyle}</style>
  609. </li>
  610. )
  611. }
  612. }
  613.  
  614. renderTeams() {
  615. const Item = this.renderItem()
  616.  
  617. return this.state.teams.map((team, index) => (
  618. <Item key={team.id} index={index} team={team} />
  619. ))
  620. }
  621.  
  622. renderList() {
  623. const teams = this.renderTeams()
  624.  
  625. // eslint-disable-next-line new-cap
  626. return () => (
  627. <ul>
  628. {teams}
  629. <style jsx>{listStyle}</style>
  630. </ul>
  631. )
  632. }
  633.  
  634. retryUpdate = () => {
  635. if (!this.remote) {
  636. return
  637. }
  638.  
  639. const { app } = this.remote
  640.  
  641. // Restart the application
  642. app.relaunch()
  643. app.exit(0)
  644. }
  645.  
  646. closeUpdateMessage = () => {
  647. this.setState({
  648. updateFailed: false
  649. })
  650. }
  651.  
  652. render() {
  653. console.log('switcher render', this.state, '\n\n')
  654. debug('switcher', 'render')
  655.  
  656. const List = this.renderList()
  657. const { updateFailed } = this.state
  658. const { darkBg, online } = this.props
  659.  
  660. return (
  661. <div>
  662. {updateFailed && (
  663. <span className="update-failed">
  664. <p>
  665. The app failed to update! &mdash;{' '}
  666. <a onClick={this.retryUpdate}>Retry?</a>
  667. </p>
  668. <Clear onClick={this.closeUpdateMessage} color="#fff" />
  669. </span>
  670. )}
  671. <aside className={darkBg ? 'dark' : ''}>
  672. {online ? (
  673. <div className="list-container" ref={this.setReference} name="list">
  674. <div className="list-scroll">
  675. <List
  676. axis="x"
  677. lockAxis="x"
  678. helperClass="switcher-helper"
  679. lockToContainerEdges={true}
  680. lockOffset="0%"
  681. />
  682. </div>
  683. </div>
  684. ) : (
  685. <p className="offline">{'You are offline'}</p>
  686. )}
  687.  
  688. <a
  689. className="toggle-menu"
  690. onClick={this.openMenu}
  691. onContextMenu={this.openMenu}
  692. ref={this.setReference}
  693. name="menu"
  694. >
  695. <i />
  696. <i />
  697. <i />
  698. </a>
  699. </aside>
  700.  
  701. <style jsx>{wrapStyle}</style>
  702.  
  703. <style jsx global>
  704. {helperStyle}
  705. </style>
  706. </div>
  707. )
  708. }
  709. }
  710.  
  711. Switcher.propTypes = {
  712. setFeedScope: func,
  713. setTeams: func,
  714. activeScope: object,
  715. darkBg: bool,
  716. online: bool,
  717. defaultScope: string
  718. }
  719.  
  720. export default Switcher
Add Comment
Please, Sign In to add comment