Guest User

Untitled

a guest
Jan 23rd, 2019
86
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 19.13 KB | None | 0 0
  1. // Packages
  2. import electron from 'electron'
  3. import { Component } from 'react'
  4. import { func, object, bool } from 'prop-types'
  5. import exists from 'path-exists'
  6. import isEqual from 'react-fast-compare'
  7. import setRef from 'react-refs'
  8. import {
  9. SortableContainer,
  10. SortableElement,
  11. arrayMove
  12. } from 'react-sortable-hoc'
  13. import makeUnique from 'make-unique'
  14. import ms from 'ms'
  15. import isDev from 'electron-is-dev'
  16. import Identicon from 'identicon.js'
  17. import crypto from 'crypto'
  18.  
  19. // Styles
  20. import {
  21. wrapStyle,
  22. listStyle,
  23. itemStyle,
  24. helperStyle
  25. } from '../../styles/components/feed/switcher'
  26.  
  27. // Components
  28. import Clear from '../../vectors/clear'
  29. import Avatar from './avatar'
  30.  
  31. class Switcher extends Component {
  32. state = {
  33. teams: [],
  34. scope: null,
  35. updateFailed: false,
  36. initialized: false,
  37. syncInterval: '5s',
  38. queue: []
  39. }
  40.  
  41. remote = electron.remote || false
  42. ipcRenderer = electron.ipcRenderer || false
  43. setReference = setRef.bind(this)
  44.  
  45. load = file => {
  46. if (electron.remote) {
  47. return electron.remote.require(file)
  48. }
  49.  
  50. return null
  51. }
  52.  
  53. configUtils = this.load('./utils/config')
  54.  
  55. // Don't update state when dragging teams
  56. moving = false
  57.  
  58. // Ensure that config doesn't get checked when the
  59. // file is updated from this component
  60. savingConfig = false
  61.  
  62. showWindow = () => {
  63. if (this.timer && this.state.syncInterval !== '5s') {
  64. clearInterval(this.timer)
  65.  
  66. // Refresh the teams and events when the window gets
  67. // shown, so that they're always up-to-date
  68. this.loadTeams()
  69.  
  70. // Restart the timer so we keep everything in sync every 5s
  71. this.listTimer()
  72. this.setState({ syncInterval: '5s' })
  73. }
  74.  
  75. document.addEventListener('keydown', this.keyDown.bind(this))
  76. }
  77.  
  78. hideWindow = () => {
  79. if (this.timer && this.state.syncInterval !== '5m') {
  80. clearInterval(this.timer)
  81.  
  82. // Restart the timer so we keep everything in sync every 5m
  83. this.listTimer()
  84. this.setState({ syncInterval: '5m' })
  85. }
  86.  
  87. document.removeEventListener('keydown', this.keyDown.bind(this))
  88. }
  89.  
  90. componentWillReceiveProps({ activeScope }) {
  91. if (activeScope) {
  92. this.changeScope(activeScope, true, true, true)
  93. return
  94. }
  95.  
  96. if (this.state.scope !== null) {
  97. return
  98. }
  99.  
  100. this.setState({
  101. scope: this.props.defaultScope
  102. })
  103. }
  104.  
  105. componentWillMount() {
  106. // Support SSR
  107. if (!this.remote || typeof window === 'undefined') {
  108. return
  109. }
  110.  
  111. const currentWindow = this.remote.getCurrentWindow()
  112.  
  113. if (!currentWindow) {
  114. return
  115. }
  116.  
  117. currentWindow.on('show', this.showWindow)
  118. currentWindow.on('hide', this.hideWindow)
  119.  
  120. window.addEventListener('beforeunload', () => {
  121. currentWindow.removeListener('show', this.showWindow)
  122. currentWindow.removeListener('hide', this.hideWindow)
  123. })
  124. }
  125.  
  126. listTimer = () => {
  127. const { getCurrentWindow } = this.remote
  128. const { isVisible } = getCurrentWindow()
  129.  
  130. const time = isVisible() ? '5s' : '5m'
  131.  
  132. this.timer = setTimeout(async () => {
  133. try {
  134. // It's important that this is being `await`ed
  135. await this.loadTeams()
  136. } catch (err) {
  137. if (isDev) {
  138. console.error(err)
  139. }
  140. }
  141.  
  142. // Once everything is done or has failed,
  143. // try it again after some time.
  144. this.listTimer()
  145. }, ms(time))
  146. }
  147.  
  148. async componentDidMount() {
  149. // Show a UI banner if the installation
  150. // of an update failed
  151. this.ipcRenderer.on('update-failed', () => {
  152. this.setState({ updateFailed: true })
  153. })
  154.  
  155. // Only start updating teams once they're loaded!
  156. // This needs to be async so that we can already
  157. // start the state timer below for the data that's already cached
  158. if (!this.props.online) {
  159. this.listTimer()
  160. return
  161. }
  162.  
  163. this.loadTeams(true)
  164. .then(this.listTimer)
  165. .catch(this.listTimer)
  166.  
  167. // Check the config for `currentTeam`
  168. await this.checkCurrentTeam()
  169.  
  170. // Update the scope if the config changes
  171. this.listenToConfig()
  172. }
  173.  
  174. async checkTeamOrder() {
  175. const order = await this.getTeamOrder()
  176. const updated = await this.applyTeamOrder(this.state.teams, order)
  177.  
  178. if (updated) {
  179. this.setState({ teams: updated })
  180. }
  181. }
  182.  
  183. listenToConfig() {
  184. if (!this.ipcRenderer) {
  185. return
  186. }
  187.  
  188. this.ipcRenderer.on('config-changed', async (event, config) => {
  189. if (this.state.teams.length === 0) {
  190. return
  191. }
  192.  
  193. if (this.savingConfig) {
  194. this.savingConfig = false
  195. return
  196. }
  197.  
  198. // Load the teams in case there is a brand new team
  199. await this.loadTeams()
  200.  
  201. // Check for the `currentTeam` property in the config
  202. await this.checkCurrentTeam(config)
  203.  
  204. // Do the same for the `desktop.teamOrder` property
  205. await this.checkTeamOrder()
  206. })
  207. }
  208.  
  209. resetScope = () => {
  210. this.changeScope({
  211. id: this.props.defaultScope
  212. })
  213. }
  214.  
  215. generateAvatar(str) {
  216. const hash = crypto.createHash('md5')
  217. hash.update(str)
  218. const imgData = new Identicon(hash.digest('hex')).toString()
  219. return 'data:image/png;base64,' + imgData
  220. }
  221.  
  222. async checkCurrentTeam(config) {
  223. if (!this.remote) {
  224. return
  225. }
  226.  
  227. if (!config) {
  228. const { getConfig } = this.remote.require('./utils/config')
  229.  
  230. try {
  231. config = await getConfig()
  232. } catch (err) {
  233. // The config is not valid, so no need to update
  234. // the current team.
  235. return
  236. }
  237. }
  238.  
  239. if (!config.currentTeam) {
  240. this.resetScope()
  241. return
  242. }
  243.  
  244. // Legacy config
  245. if (typeof config.currentTeam === 'object') {
  246. this.changeScope(config.currentTeam, true)
  247. return
  248. }
  249.  
  250. const { teams } = {
  251. teams: [
  252. {
  253. id: 'aaa',
  254. name: 'Trello',
  255. avatarUrl: this.generateAvatar('IZSNu3Ty')
  256. },
  257. {
  258. id: 'bbb',
  259. name: 'IZSNu3Ty',
  260. avatarUrl: this.generateAvatar('di61gxME')
  261. }
  262. ]
  263. }
  264.  
  265. const related = teams.find(team => team.id === config.currentTeam)
  266.  
  267. // The team was deleted
  268. if (!related) {
  269. this.resetScope()
  270. return
  271. }
  272.  
  273. this.changeScope(related, true)
  274. }
  275.  
  276. async saveConfig(newConfig) {
  277. const { saveConfig } = this.configUtils
  278.  
  279. // Ensure that we're not handling the
  280. // event triggered by changes made to the config
  281. // because the changes were triggered manually
  282. // inside this app
  283. this.savingConfig = true
  284.  
  285. // Then update the config file
  286. await saveConfig(newConfig, 'config')
  287. }
  288.  
  289. async getTeamOrder() {
  290. const { getConfig } = this.configUtils
  291. let config
  292.  
  293. try {
  294. config = await getConfig()
  295. } catch (err) {}
  296.  
  297. if (!config || !config.desktop || !config.desktop.teamOrder) {
  298. return false
  299. }
  300.  
  301. const order = config.desktop.teamOrder
  302.  
  303. if (!Array.isArray(order) || order.length === 0) {
  304. return false
  305. }
  306.  
  307. return order
  308. }
  309.  
  310. updateTouchBar() {
  311. if (!this.remote) {
  312. return
  313. }
  314.  
  315. const { getCurrentWindow, TouchBar } = this.remote
  316. const currentWindow = getCurrentWindow()
  317. const buttons = []
  318.  
  319. for (const team of this.state.teams) {
  320. const active = team.id === this.state.scope
  321. const backgroundColor = active ? '#3782D1' : null
  322.  
  323. const button = new TouchBar.TouchBarButton({
  324. label: team.name || 'You',
  325. backgroundColor,
  326. click: () => this.changeScope(team, true, true)
  327. })
  328.  
  329. buttons.push(button)
  330. }
  331.  
  332. currentWindow.setTouchBar(new TouchBar(buttons))
  333. }
  334.  
  335. async applyTeamOrder(list, order) {
  336. const newList = []
  337.  
  338. if (!order) {
  339. return list
  340. }
  341.  
  342. for (const position of order) {
  343. const index = order.indexOf(position)
  344.  
  345. newList[index] = list.find(item => {
  346. const name = item.slug || item.name
  347. return name === position
  348. })
  349. }
  350.  
  351. // Apply the new data at the end, but keep order
  352. return this.merge(newList, list)
  353. }
  354.  
  355. merge(first, second) {
  356. const merged = first.concat(second)
  357. return makeUnique(merged, (a, b) => a.id === b.id)
  358. }
  359.  
  360. async haveUpdated(data) {
  361. const newData = JSON.parse(JSON.stringify(data))
  362. let currentData = JSON.parse(JSON.stringify(this.state.teams))
  363.  
  364. if (currentData.length > 0) {
  365. // Remove teams that the user has left
  366. currentData = currentData.filter(team => {
  367. return Boolean(newData.find(item => item.id === team.id))
  368. })
  369. }
  370.  
  371. const ordered = this.merge(currentData, newData)
  372. const copy = JSON.parse(JSON.stringify(ordered))
  373. const order = await this.getTeamOrder()
  374.  
  375. if (!order) {
  376. return ordered
  377. }
  378.  
  379. for (const item of order) {
  380. const isPart = newData.find(team => {
  381. return team.name === item || team.slug === item
  382. })
  383.  
  384. // If the saved team order contains a team that
  385. // the user is not a part of, we can ignore it.
  386. if (!isPart) {
  387. return ordered
  388. }
  389. }
  390.  
  391. if (isEqual(ordered, currentData)) {
  392. return false
  393. }
  394.  
  395. // Then order the teams as saved in the config
  396. return this.applyTeamOrder(copy, order)
  397. }
  398.  
  399. async loadTeams(firstLoad) {
  400. if (!this.remote) {
  401. return
  402. }
  403.  
  404. const currentWindow = this.remote.getCurrentWindow()
  405.  
  406. // If the window isn't visible, don't pull the teams
  407. // Ensure to always load the first chunk
  408. if (!currentWindow.isVisible() && this.state.initialized) {
  409. if (this.props.setTeams) {
  410. // When passing `null`, the feed will only
  411. // update the events, not the teams
  412. await this.props.setTeams(null, firstLoad)
  413. }
  414.  
  415. return
  416. }
  417.  
  418. const data = {
  419. teams: [
  420. {
  421. id: 'aaa',
  422. name: 'Trello',
  423. avatarUrl: this.generateAvatar('IZSNu3Ty')
  424. },
  425. {
  426. id: 'bbb',
  427. name: 'IZSNu3Ty',
  428. avatarUrl: this.generateAvatar('di61gxME')
  429. }
  430. ]
  431. }
  432.  
  433. if (!data || !data.teams) {
  434. return
  435. }
  436.  
  437. const teams = data.teams
  438.  
  439. const updated = await this.haveUpdated(teams)
  440.  
  441. const scopeExists = updated.find(team => {
  442. return this.state.scope === team.id
  443. })
  444.  
  445. if (!scopeExists) {
  446. this.resetScope()
  447. }
  448.  
  449. if (updated) {
  450. this.setState({ teams: updated })
  451. }
  452.  
  453. if (this.props.setTeams) {
  454. // When passing `null`, the feed will only
  455. // update the events, not the teams
  456. await this.props.setTeams(updated || null, firstLoad)
  457. }
  458. }
  459.  
  460. keyDown(event) {
  461. const activeItem = document.activeElement
  462.  
  463. if (activeItem && activeItem.tagName === 'INPUT') {
  464. return
  465. }
  466.  
  467. const code = event.code
  468. const number = code.includes('Digit') ? code.split('Digit')[1] : false
  469.  
  470. if (number && number <= 9 && this.state.teams.length > 1) {
  471. if (this.state.teams[number - 1]) {
  472. event.preventDefault()
  473.  
  474. const relatedTeam = this.state.teams[number - 1]
  475. this.changeScope(relatedTeam)
  476. }
  477. }
  478. }
  479.  
  480. componentDidUpdate(prevProps, prevState) {
  481. const aaa = parseInt(Math.random() * 1000, 10)
  482. console.log('componentDidUpdate1', aaa)
  483. const { teams, scope } = this.state
  484.  
  485. const teamsChanged = !isEqual(teams, prevState.teams)
  486. const scopeChanged = !isEqual(scope, prevState.scope)
  487.  
  488. console.log('componentDidUpdate2', aaa)
  489. if (teamsChanged || scopeChanged) {
  490. this.updateTouchBar()
  491. }
  492.  
  493. console.log('componentDidUpdate3', aaa)
  494. while (this.state.queue.length > 0) {
  495. const queue = this.state.queue
  496.  
  497. queue.shift()()
  498.  
  499. this.setState({ queue })
  500. }
  501. console.log('componentDidUpdate4', aaa)
  502.  
  503. if (this.state.initialized) {
  504. return
  505. }
  506. console.log('componentDidUpdate5', aaa)
  507.  
  508. const teamsCount = teams.length
  509.  
  510. if (teamsCount === 0) {
  511. return
  512. }
  513. console.log('componentDidUpdate6', aaa)
  514.  
  515. const when = 100 + 100 * teamsCount + 600
  516. console.log('componentDidUpdate7', aaa)
  517.  
  518. setTimeout(() => {
  519. // Ensure that the animations for the teams
  520. // fading in works after recovering from offline mode
  521. if (!this.props.online) {
  522. return
  523. }
  524.  
  525. console.log('set initialized to true!!!!!!!!!!', aaa, this.state)
  526. this.setState({
  527. initialized: true
  528. })
  529. }, when)
  530. console.log('componentDidUpdate8', aaa)
  531. }
  532.  
  533. static getDerivedStateFromProps(props) {
  534. // Ensure that the animations for the teams
  535. // fading in works after recovering from offline mode.
  536. if (!props.online) {
  537. return {
  538. initialized: false
  539. }
  540. }
  541.  
  542. return null
  543. }
  544.  
  545. async updateConfig(team) {
  546. if (!this.remote) {
  547. return
  548. }
  549.  
  550. const info = {
  551. currentTeam: team
  552. }
  553.  
  554. // And then update the config file
  555. await this.saveConfig(info)
  556. }
  557.  
  558. changeScope(team, saveToConfig, byHand, noFeed) {
  559. // If the clicked item in the team switcher is
  560. // already the active one, don't do anything
  561. if (this.state.scope === team.id) {
  562. return
  563. }
  564.  
  565. if (!noFeed && this.props.setFeedScope) {
  566. // Load different messages into the feed
  567. this.props.setFeedScope(team.id)
  568. }
  569.  
  570. // Make the team/user icon look active by
  571. // syncing the scope with the feed
  572. this.setState({ scope: team.id })
  573.  
  574. // Save the new `currentTeam` to the config
  575. if (saveToConfig) {
  576. const queueFunction = (fn, context, params) => {
  577. return () => {
  578. fn.apply(context, params)
  579. }
  580. }
  581.  
  582. this.setState({
  583. queue: this.state.queue.concat([
  584. queueFunction(this.updateConfig, this, [team, byHand])
  585. ])
  586. })
  587. }
  588. }
  589.  
  590. shouldComponentUpdate(nextProps, nextState) {
  591. // There are two cases in which we do not want to re-render:
  592. //
  593. // - Someone is dragging something around in the UI (this.moving)
  594. // - The state and/or props didn't change (rest of the statement)
  595. //
  596. // It is extremely important to understand that `shouldComponentUpdate` will
  597. // be called even if the state AND props did not change. Because that is exactly
  598. // the purpose of this function: To decide whether something changed.
  599. if (
  600. this.moving ||
  601. (isEqual(this.state, nextState) && isEqual(this.props, nextProps))
  602. ) {
  603. return false
  604. }
  605.  
  606. return true
  607. }
  608.  
  609. openMenu = () => {
  610. // The menu toggler element has children
  611. // we have the ability to prevent the event from
  612. // bubbling up from those, but we need to
  613. // use `this.menu` to make sure the menu always gets
  614. // bounds to the parent
  615. const { bottom, left, height, width } = this.menu.getBoundingClientRect()
  616. const sender = electron.ipcRenderer || false
  617.  
  618. if (!sender) {
  619. return
  620. }
  621.  
  622. sender.send('open-menu', {
  623. x: left,
  624. y: bottom,
  625. height,
  626. width
  627. })
  628. }
  629.  
  630. saveTeamOrder(teams) {
  631. const teamOrder = []
  632.  
  633. for (const team of teams) {
  634. teamOrder.push(team.slug || team.name)
  635. }
  636.  
  637. this.saveConfig({
  638. desktop: { teamOrder }
  639. })
  640. }
  641.  
  642. onSortEnd = ({ oldIndex, newIndex }) => {
  643. document.body.classList.toggle('is-moving')
  644.  
  645. // Allow the state to update again
  646. this.moving = false
  647.  
  648. // Don't update if it was dropped at the same position
  649. if (oldIndex === newIndex) {
  650. return
  651. }
  652.  
  653. const teams = arrayMove(this.state.teams, oldIndex, newIndex)
  654. this.saveTeamOrder(teams)
  655.  
  656. // Ensure that we're not dealing with the same
  657. // objects or array ever again
  658. this.setState({
  659. teams: JSON.parse(JSON.stringify(teams))
  660. })
  661. }
  662.  
  663. onSortStart = () => {
  664. document.body.classList.toggle('is-moving')
  665.  
  666. // Prevent the state from being updated
  667. this.moving = true
  668. }
  669.  
  670. scrollToEnd = event => {
  671. event.preventDefault()
  672.  
  673. if (!this.list) {
  674. return
  675. }
  676.  
  677. const list = this.list
  678. list.scrollLeft = list.offsetWidth
  679. }
  680.  
  681. renderItem() {
  682. // eslint-disable-next-line new-cap
  683. return SortableElement(({ team }) => {
  684. const isActive = this.state.scope === team.id
  685. const isUser = team.id && !team.id.includes('team')
  686. const index = this.state.teams.indexOf(team)
  687. const shouldScale = !this.state.initialized
  688. console.log('shouldScale', shouldScale)
  689. const darkBg = this.props.darkBg
  690.  
  691. const classes = []
  692.  
  693. if (isActive) {
  694. classes.push('active')
  695. }
  696.  
  697. if (darkBg) {
  698. classes.push('dark')
  699. }
  700.  
  701. const clicked = event => {
  702. event.preventDefault()
  703. this.changeScope(team, true, true)
  704. }
  705.  
  706. return (
  707. <li onClick={clicked} className={classes.join(' ')} key={team.id}>
  708. <Avatar
  709. team={team}
  710. isUser={isUser}
  711. scale={shouldScale}
  712. delay={index}
  713. hash={team.avatar}
  714. avatarUrl={team.avatarUrl}
  715. />
  716.  
  717. <style jsx>{itemStyle}</style>
  718. </li>
  719. )
  720. })
  721. }
  722.  
  723. renderTeams() {
  724. const Item = this.renderItem()
  725.  
  726. return this.state.teams.map((team, index) => (
  727. <Item key={team.id} index={index} team={team} />
  728. ))
  729. }
  730.  
  731. renderList() {
  732. const teams = this.renderTeams()
  733.  
  734. // eslint-disable-next-line new-cap
  735. return SortableContainer(() => (
  736. <ul>
  737. {teams}
  738. <style jsx>{listStyle}</style>
  739. </ul>
  740. ))
  741. }
  742.  
  743. allowDrag = event => {
  744. if (process.platform === 'win32') {
  745. return !event.ctrlKey
  746. }
  747.  
  748. return !event.metaKey
  749. }
  750.  
  751. retryUpdate = () => {
  752. if (!this.remote) {
  753. return
  754. }
  755.  
  756. const { app } = this.remote
  757.  
  758. // Restart the application
  759. app.relaunch()
  760. app.exit(0)
  761. }
  762.  
  763. closeUpdateMessage = () => {
  764. this.setState({
  765. updateFailed: false
  766. })
  767. }
  768.  
  769. render() {
  770. console.log('switcher render', this.state, '\n\n')
  771. const List = this.renderList()
  772. const { updateFailed, teams } = this.state
  773. const delay = teams.length
  774. const { darkBg, online } = this.props
  775.  
  776. return (
  777. <div>
  778. {updateFailed && (
  779. <span className="update-failed">
  780. <p>
  781. The app failed to update! &mdash;{' '}
  782. <a onClick={this.retryUpdate}>Retry?</a>
  783. </p>
  784. <Clear onClick={this.closeUpdateMessage} color="#fff" />
  785. </span>
  786. )}
  787. <aside className={darkBg ? 'dark' : ''}>
  788. {online ? (
  789. <div className="list-container" ref={this.setReference} name="list">
  790. <div className="list-scroll">
  791. <List
  792. axis="x"
  793. lockAxis="x"
  794. shouldCancelStart={this.allowDrag}
  795. onSortEnd={this.onSortEnd}
  796. onSortStart={this.onSortStart}
  797. helperClass="switcher-helper"
  798. lockToContainerEdges={true}
  799. lockOffset="0%"
  800. />
  801. </div>
  802.  
  803. <span className="shadow" onClick={this.scrollToEnd} />
  804. </div>
  805. ) : (
  806. <p className="offline">{'You are offline'}</p>
  807. )}
  808.  
  809. <a
  810. className="toggle-menu"
  811. onClick={this.openMenu}
  812. onContextMenu={this.openMenu}
  813. ref={this.setReference}
  814. name="menu"
  815. >
  816. <i />
  817. <i />
  818. <i />
  819. </a>
  820. </aside>
  821.  
  822. <style jsx>{wrapStyle}</style>
  823.  
  824. <style jsx global>
  825. {helperStyle}
  826. </style>
  827. </div>
  828. )
  829. }
  830. }
  831.  
  832. Switcher.propTypes = {
  833. setFeedScope: func,
  834. setTeams: func,
  835. activeScope: object,
  836. darkBg: bool,
  837. online: bool
  838. }
  839.  
  840. export default Switcher
Add Comment
Please, Sign In to add comment