1import React, {Component} from 'react'
2import logger from '../../logger'
3import * as Kb from '../../common-adapters'
4import * as Styles from '../../styles'
5import {ignoreDisconnectOverlay} from '../../local-debug.desktop'
6import {RPCError} from '../../util/errors'
7import {Props} from './index'
8
9type Size = 'Closed' | 'Small' | 'Big'
10
11type State = {
12  size: Size
13  cachedSummary?: string
14  cachedDetails?: string
15}
16
17class GlobalError extends Component<Props, State> {
18  state: State
19  private timerID?: ReturnType<typeof setInterval>
20  private mounted: boolean = false
21
22  constructor(props: Props) {
23    super(props)
24
25    this.state = {
26      cachedDetails: this.detailsForError(props.error),
27      cachedSummary: this.summaryForError(props.error),
28      size: 'Closed',
29    }
30  }
31
32  componentWillUnmount() {
33    this.mounted = false
34    this.clearCountdown()
35  }
36
37  componentDidMount() {
38    this.mounted = true
39    this.resetError(!!this.props.error)
40  }
41
42  private onExpandClick = () => {
43    this.setState({size: 'Big'})
44    this.clearCountdown()
45  }
46
47  private clearCountdown() {
48    if (this.timerID) {
49      clearTimeout(this.timerID)
50    }
51    this.timerID = undefined
52  }
53
54  private resetError(newError: boolean) {
55    this.clearCountdown()
56    this.setState({size: newError ? 'Small' : 'Closed'})
57
58    if (newError) {
59      this.timerID = setTimeout(() => {
60        this.props.onDismiss()
61      }, 10000)
62    }
63  }
64
65  private summaryForError(err?: Error | RPCError) {
66    return err?.message
67  }
68
69  private detailsForError(err?: Error | RPCError) {
70    return err?.stack
71  }
72
73  componentDidUpdate(prevProps: Props) {
74    if (prevProps.error !== this.props.error) {
75      setTimeout(
76        () => {
77          if (this.mounted) {
78            this.setState({
79              cachedDetails: this.detailsForError(this.props.error),
80              cachedSummary: this.summaryForError(this.props.error),
81            })
82          }
83        },
84        this.props.error ? 0 : 7000
85      ) // if it's set, do it immediately, if it's cleared set it in a bit
86      this.resetError(!!this.props.error)
87    }
88  }
89
90  static maxHeightForSize(size: Size) {
91    return {
92      Big: 900,
93      Closed: 0,
94      Small: 35,
95    }[size]
96  }
97
98  private renderDaemonError() {
99    if (ignoreDisconnectOverlay) {
100      logger.warn('Ignoring disconnect overlay')
101      return null
102    }
103
104    const message =
105      (this.props.daemonError && this.props.daemonError.message) ||
106      'Keybase is currently unreachable. Trying to reconnect you…'
107    return (
108      <Kb.Box style={styles.containerOverlay}>
109        <Kb.Box style={styles.overlayRow}>
110          <Kb.Text center={true} type="BodySmallSemibold" style={styles.message}>
111            {message}
112          </Kb.Text>
113        </Kb.Box>
114        <Kb.Box style={styles.overlayFill}>
115          <Kb.Animation animationType="disconnected" height={175} width={600} />
116        </Kb.Box>
117      </Kb.Box>
118    )
119  }
120
121  private renderError() {
122    const {onDismiss} = this.props
123    const summary = this.state.cachedSummary
124    const details = this.state.cachedDetails
125
126    let stylesContainer: Styles.StylesCrossPlatform
127    switch (this.state.size) {
128      case 'Big':
129        stylesContainer = styles.containerBig
130        break
131      case 'Closed':
132        stylesContainer = styles.containerClosed
133        break
134      case 'Small':
135        stylesContainer = styles.containerSmall
136        break
137    }
138
139    return (
140      <Kb.Box style={stylesContainer} onClick={this.onExpandClick}>
141        <Kb.Box style={styles.innerContainer}>
142          <Kb.Text center={true} type="BodyBig" style={styles.summary}>
143            {summary}
144          </Kb.Text>
145          <Kb.Button
146            label="Please tell us"
147            onClick={this.props.onFeedback}
148            small={true}
149            type="Dim"
150            style={styles.feedbackButton}
151          />
152          {summary && (
153            <Kb.Icon
154              color={Styles.globalColors.white_75}
155              hoverColor={Styles.globalColors.white}
156              onClick={onDismiss}
157              style={styles.closeIcon}
158              type="iconfont-close"
159            />
160          )}
161        </Kb.Box>
162        <Kb.Text center={true} type="BodyBig" selectable={true} style={styles.details}>
163          {details}
164        </Kb.Text>
165      </Kb.Box>
166    )
167  }
168
169  render() {
170    if (this.props.daemonError) {
171      return this.renderDaemonError()
172    }
173    return this.renderError()
174  }
175}
176
177const containerBase = {
178  ...Styles.globalStyles.flexBoxColumn,
179  left: 0,
180  overflow: 'hidden',
181  position: 'absolute',
182  right: 0,
183  top: 0,
184  zIndex: 1000,
185  ...Styles.transition('max-height'),
186}
187
188const styles = Styles.styleSheetCreate(
189  () =>
190    ({
191      closeIcon: Styles.platformStyles({
192        isElectron: {
193          position: 'absolute',
194          right: Styles.globalMargins.xsmall,
195          top: 10,
196        },
197      }),
198      containerBig: {...containerBase, maxHeight: GlobalError.maxHeightForSize('Big')},
199      containerClosed: {...containerBase, maxHeight: GlobalError.maxHeightForSize('Closed')},
200      containerOverlay: {
201        ...Styles.globalStyles.flexBoxColumn,
202        bottom: 0,
203        left: 0,
204        position: 'absolute',
205        right: 0,
206        top: 0,
207        zIndex: 1000,
208      },
209      containerSmall: {...containerBase, maxHeight: GlobalError.maxHeightForSize('Small')},
210      details: {
211        backgroundColor: Styles.globalColors.black,
212        color: Styles.globalColors.white_75,
213        padding: 8,
214        paddingLeft: Styles.globalMargins.xlarge,
215        paddingRight: Styles.globalMargins.xlarge,
216      },
217      feedbackButton: {
218        marginRight: Styles.globalMargins.large,
219      },
220      innerContainer: {
221        ...Styles.globalStyles.flexBoxRow,
222        alignItems: 'center',
223        backgroundColor: Styles.globalColors.black,
224        flex: 1,
225        justifyContent: 'center',
226        minHeight: GlobalError.maxHeightForSize('Small'),
227        padding: Styles.globalMargins.xtiny,
228        position: 'relative',
229      },
230      message: {
231        color: Styles.globalColors.white,
232      },
233      overlayFill: {
234        ...Styles.globalStyles.flexBoxColumn,
235        alignItems: 'center',
236        backgroundColor: Styles.globalColors.white,
237        flex: 1,
238        justifyContent: 'center',
239      },
240      overlayRow: {
241        ...Styles.globalStyles.flexBoxRow,
242        alignItems: 'center',
243        backgroundColor: Styles.globalColors.blue,
244        justifyContent: 'center',
245        padding: 8,
246      },
247      summary: {
248        color: Styles.globalColors.white,
249        flex: 1,
250      },
251      summaryRow: {
252        ...Styles.globalStyles.flexBoxRow,
253        alignItems: 'center',
254        flex: 1,
255        justifyContent: 'center',
256        padding: Styles.globalMargins.xtiny,
257        position: 'relative',
258      },
259      summaryRowError: {
260        backgroundColor: Styles.globalColors.black,
261        minHeight: GlobalError.maxHeightForSize('Small'),
262      },
263    } as const)
264)
265
266export default GlobalError
267