1import * as React from 'react'
2import * as Kb from '../../../../../common-adapters'
3import * as Styles from '../../../../../styles'
4import {resolveRootAsURL} from '../../../../../desktop/app/resolve-root.desktop'
5import {urlsToImgSet} from '../../../../../common-adapters/icon.desktop'
6import {Props} from '.'
7import SharedTimer, {SharedTimerID} from '../../../../../util/shared-timers'
8
9const copyChildren = (children: React.ReactNode): React.ReactNode =>
10  // @ts-ignore
11  React.Children.map(children, child => (child ? React.cloneElement(child) : child))
12
13export const animationDuration = 2000
14
15const retainedHeights = new Set<string>()
16
17type State = {
18  animating: boolean
19  children?: React.ReactNode
20  height: number
21}
22
23class ExplodingHeightRetainer extends React.PureComponent<Props, State> {
24  _boxRef = React.createRef<HTMLDivElement>()
25  state = {
26    animating: false,
27    children: this.props.retainHeight ? null : copyChildren(this.props.children), // no children if we already exploded
28    height: 17,
29  }
30  timerID?: SharedTimerID
31
32  static getDerivedStateFromProps(nextProps: Props, _: State) {
33    return nextProps.retainHeight ? null : {children: copyChildren(nextProps.children)}
34  }
35
36  componentDidMount() {
37    // remeasure if we are already exploded
38    if (this.props.retainHeight && retainedHeights.has(this.props.messageKey) && this.props.measure) {
39      retainedHeights.delete(this.props.messageKey)
40      this.props.measure()
41    }
42  }
43
44  componentDidUpdate(prevProps: Props) {
45    if (this.props.retainHeight) {
46      if (!prevProps.retainHeight) {
47        // destroy local copy of children when animation finishes
48        this.setState({animating: true}, () => {
49          this.timerID && SharedTimer.removeObserver(this.props.messageKey, this.timerID)
50          this.timerID = SharedTimer.addObserver(() => this.setState({animating: false, children: null}), {
51            key: this.props.messageKey,
52            ms: animationDuration,
53          })
54        })
55      }
56      return
57    }
58
59    this.setHeight()
60  }
61
62  setHeight() {
63    const node = this._boxRef.current
64    if (node instanceof HTMLElement) {
65      const height = node.clientHeight
66      if (height && height !== this.state.height) {
67        retainedHeights.add(this.props.messageKey)
68        this.setState({height})
69      }
70    }
71  }
72
73  componentWillUnmount() {
74    this.timerID && SharedTimer.removeObserver(this.props.messageKey, this.timerID)
75  }
76
77  render() {
78    return (
79      <Kb.Box
80        style={Styles.collapseStyles([
81          styles.container,
82          this.props.style,
83          // paddingRight is to compensate for the message menu
84          // to make sure we don't rewrap text when showing the animation
85          this.props.retainHeight && {
86            height: this.state.height,
87            paddingRight: 28,
88            position: 'relative',
89          },
90        ])}
91        forwardedRef={this._boxRef}
92      >
93        {this.state.children}
94        <Ashes
95          doneExploding={!this.state.animating}
96          exploded={this.props.retainHeight}
97          explodedBy={this.props.explodedBy}
98          height={this.state.height}
99        />
100      </Kb.Box>
101    )
102  }
103}
104
105const Ashes = (props: {doneExploding: boolean; exploded: boolean; explodedBy?: string; height: number}) => {
106  let explodedTag: React.ReactNode = null
107  if (props.doneExploding) {
108    explodedTag = props.explodedBy ? (
109      <Kb.Text type="BodyTiny" style={styles.exploded}>
110        EXPLODED BY{' '}
111        <Kb.ConnectedUsernames
112          type="BodySmallBold"
113          onUsernameClicked="profile"
114          usernames={props.explodedBy}
115          inline={true}
116          colorFollowing={true}
117          colorYou={true}
118          underline={true}
119        />
120      </Kb.Text>
121    ) : (
122      <Kb.Text type="BodyTiny" style={styles.exploded}>
123        EXPLODED
124      </Kb.Text>
125    )
126  }
127  return (
128    <AshBox className={Styles.classNames({'full-width': props.exploded})}>
129      {props.exploded && explodedTag}
130      <FlameFront height={props.height} stop={props.doneExploding} />
131    </AshBox>
132  )
133}
134
135const FlameFront = (props: {height: number; stop: boolean}) => {
136  if (props.stop) {
137    return null
138  }
139  const numBoxes = Math.max(Math.ceil(props.height / 17) - 1, 1)
140  const children: Array<React.ReactNode> = []
141  for (let i = 0; i < numBoxes; i++) {
142    children.push(
143      <Kb.Box style={styles.flame}>
144        <Kb.Animation
145          animationType={Styles.isDarkMode() ? 'darkExploding' : 'exploding'}
146          width={64}
147          height={64}
148        />
149      </Kb.Box>
150    )
151  }
152  return (
153    <Kb.Box className="flame-container" style={styles.flameContainer}>
154      {children}
155    </Kb.Box>
156  )
157}
158
159const explodedIllustrationUrl = (): string =>
160  Styles.isDarkMode()
161    ? urlsToImgSet({'68': resolveRootAsURL('../images/icons/dark-pattern-ashes-desktop-400-68.png')}, 68)
162    : urlsToImgSet({'68': resolveRootAsURL('../images/icons/pattern-ashes-desktop-400-68.png')}, 68)
163
164const styles = Styles.styleSheetCreate(
165  () =>
166    ({
167      ashBox: Styles.platformStyles({
168        isElectron: {
169          backgroundColor: Styles.globalColors.white, // exploded messages don't have hover effects and we need to cover the message
170          backgroundImage: explodedIllustrationUrl(),
171          backgroundRepeat: 'repeat',
172          backgroundSize: '400px 68px',
173          bottom: 0,
174          left: 0,
175          overflow: 'hidden',
176          position: 'absolute',
177          top: 0,
178          transition: `width 0s`,
179          width: 0,
180        },
181      }),
182      container: {...Styles.globalStyles.flexBoxColumn, flex: 1},
183      exploded: Styles.platformStyles({
184        isElectron: {
185          backgroundColor: Styles.globalColors.white,
186          bottom: 0,
187          color: Styles.globalColors.black_20_on_white,
188          padding: 2,
189          paddingLeft: Styles.globalMargins.tiny,
190          paddingTop: 0,
191          position: 'absolute',
192          right: 0,
193          whiteSpace: 'nowrap',
194        },
195      }),
196      flame: {
197        height: 17,
198      },
199      flameContainer: {
200        position: 'absolute',
201        right: -32,
202        top: -22,
203        width: 64,
204      },
205    } as const)
206)
207
208const AshBox = Styles.styled.div(
209  {
210    '&.full-width': {
211      overflow: 'visible',
212      transition: `width ${animationDuration}ms linear`,
213      width: '100%',
214    },
215  },
216  () => styles.ashBox
217)
218
219export default ExplodingHeightRetainer
220