1/* eslint-env browser */
2import * as React from 'react'
3import * as Kb from '../../../../common-adapters'
4import * as Styles from '../../../../styles'
5import * as Types from '../../../../constants/types/chat2'
6import SetExplodingMessagePopup from '../../messages/set-explode-popup/container'
7import {formatDurationShort} from '../../../../util/timestamp'
8import {KeyEventHandler} from '../../../../util/key-event-handler.desktop'
9import WalletsIcon from './wallets-icon/container'
10import {PlatformInputPropsInternal} from './platform-input'
11import Typing from './typing/container'
12import AddSuggestors from '../suggestors'
13import {indefiniteArticle} from '../../../../util/string'
14import {EmojiPickerDesktop} from '../../messages/react-button/emoji-picker/container'
15
16type State = {
17  emojiPickerOpen: boolean
18}
19
20class _PlatformInput extends React.Component<PlatformInputPropsInternal, State> {
21  _input: Kb.PlainInput | null = null
22  _lastText?: string
23  _fileInput: HTMLInputElement | null = null
24  state = {
25    emojiPickerOpen: false,
26  }
27
28  _inputSetRef = (ref: null | Kb.PlainInput) => {
29    this._input = ref
30    this.props.inputSetRef(ref)
31    // @ts-ignore this is probably wrong: https://github.com/DefinitelyTyped/DefinitelyTyped/issues/31065
32    this.props.inputRef.current = ref
33  }
34
35  _inputFocus = () => {
36    this._input && this._input.focus()
37  }
38
39  _emojiPickerToggle = () => {
40    this.setState(({emojiPickerOpen}) => ({emojiPickerOpen: !emojiPickerOpen}))
41  }
42
43  _filePickerFiles = () => (this._fileInput && this._fileInput.files) || []
44
45  _filePickerOpen = () => {
46    this._fileInput && this._fileInput.click()
47  }
48
49  _filePickerSetRef = (r: HTMLInputElement | null) => {
50    this._fileInput = r
51  }
52
53  _filePickerSetValue = (value: string) => {
54    if (this._fileInput) this._fileInput.value = value
55  }
56
57  _getText = () => {
58    return this._lastText || ''
59  }
60
61  // Key-handling code shared by both the input key handler
62  // (_onKeyDown) and the global key handler
63  // (_globalKeyDownPressHandler).
64  _commonOnKeyDown = (e: React.KeyboardEvent | KeyboardEvent) => {
65    const text = this._getText()
66    if (e.key === 'ArrowUp' && !this.props.isEditing && !text) {
67      e.preventDefault()
68      this.props.onEditLastMessage()
69      return true
70    } else if (e.key === 'Escape' && this.props.isEditing) {
71      this.props.onCancelEditing()
72      return true
73    } else if (e.key === 'Escape' && this.props.showReplyPreview) {
74      this.props.onCancelReply()
75      return true
76    } else if (e.key === 'u' && (e.ctrlKey || e.metaKey)) {
77      this._filePickerOpen()
78      return true
79    } else if (e.key === 'PageDown') {
80      this.props.onRequestScrollDown()
81      return true
82    } else if (e.key === 'PageUp') {
83      this.props.onRequestScrollUp()
84      return true
85    }
86
87    return false
88  }
89
90  _onKeyDown = (e: React.KeyboardEvent) => {
91    this._commonOnKeyDown(e)
92    this.props.onKeyDown && this.props.onKeyDown(e)
93  }
94
95  _onChangeText = (text: string) => {
96    this._lastText = text
97    this.props.onChangeText(text)
98  }
99
100  _globalKeyDownPressHandler = (ev: KeyboardEvent) => {
101    const target = ev.target
102    if (target instanceof HTMLInputElement || target instanceof HTMLTextAreaElement) {
103      return
104    }
105
106    if (this._commonOnKeyDown(ev)) {
107      return
108    }
109
110    const isPasteKey = ev.key === 'v' && (ev.ctrlKey || ev.metaKey)
111    const isValidSpecialKey = [
112      'Backspace',
113      'Delete',
114      'ArrowLeft',
115      'ArrowRight',
116      'ArrowUp',
117      'ArrowDown',
118      'Enter',
119      'Escape',
120    ].includes(ev.key)
121    if (ev.type === 'keypress' || isPasteKey || isValidSpecialKey) {
122      this._inputFocus()
123    }
124  }
125
126  _insertEmoji = (emojiColons: string) => {
127    if (this._input) {
128      this._input.transformText(({text, selection}) => {
129        const newText = text.slice(0, selection.start || 0) + emojiColons + text.slice(selection.end || 0)
130        const pos = (selection.start || 0) + emojiColons.length
131        return {
132          selection: {end: pos, start: pos},
133          text: newText,
134        }
135      }, true)
136      this._inputFocus()
137    }
138  }
139
140  _pickFile = () => {
141    const fileList = this._filePickerFiles()
142    const paths: Array<string> = fileList.length
143      ? Array.prototype.map
144          .call(fileList, (f: File) => {
145            // We rely on path being here, even though it's
146            // not part of the File spec.
147            return f.path as string
148          })
149          .reduce<Array<string>>((arr, p: any) => {
150            p && arr.push(p)
151            return arr
152          }, [])
153      : []
154    if (paths) {
155      this.props.onAttach(paths)
156    }
157    this._filePickerSetValue('')
158  }
159
160  _toggleShowingMenu = () => {
161    if (this.props.isEditing || this.props.cannotWrite) return
162    this.props.toggleShowingMenu()
163  }
164
165  private getHintText = () => {
166    if (this.props.cannotWrite) {
167      return `You must be at least ${indefiniteArticle(this.props.minWriterRole)} ${
168        this.props.minWriterRole
169      } to post.`
170    } else if (this.props.isEditing) {
171      return 'Edit your message'
172    } else if (this.props.isExploding) {
173      return 'Write an exploding message'
174    }
175    return this.props.inputHintText || 'Write a message'
176  }
177
178  render() {
179    return (
180      <KeyEventHandler
181        onKeyDown={this._globalKeyDownPressHandler}
182        onKeyPress={this._globalKeyDownPressHandler}
183      >
184        <Kb.Box style={styles.container}>
185          <Kb.Box
186            style={Styles.collapseStyles([
187              styles.inputWrapper,
188              {
189                backgroundColor: this.props.isEditing
190                  ? Styles.globalColors.yellowLight
191                  : Styles.globalColors.white,
192                borderColor: this.props.explodingModeSeconds
193                  ? Styles.globalColors.black
194                  : Styles.globalColors.black_20,
195              },
196            ])}
197          >
198            {!this.props.isEditing && !this.props.cannotWrite && (
199              <HoverBox
200                className={Styles.classNames({expanded: this.props.showingMenu})}
201                onClick={this._toggleShowingMenu}
202                ref={this.props.setAttachmentRef}
203                style={Styles.collapseStyles([
204                  styles.explodingIconContainer,
205                  !this.props.cannotWrite && styles.explodingIconContainerClickable,
206                  !!this.props.explodingModeSeconds && {
207                    backgroundColor: Styles.globalColors.black,
208                  },
209                ])}
210              >
211                {this.props.explodingModeSeconds ? (
212                  <Kb.Text type="BodyTinyBold" negative={true}>
213                    {formatDurationShort(this.props.explodingModeSeconds * 1000)}
214                  </Kb.Text>
215                ) : (
216                  <Kb.WithTooltip tooltip="Timer">
217                    <Kb.Icon
218                      className="timer"
219                      colorOverride={this.props.cannotWrite ? Styles.globalColors.black_20 : null}
220                      onClick={this.props.cannotWrite ? undefined : this._toggleShowingMenu}
221                      padding="xtiny"
222                      type="iconfont-timer"
223                    />
224                  </Kb.WithTooltip>
225                )}
226              </HoverBox>
227            )}
228            {this.props.isEditing && (
229              <Kb.Button
230                label="Cancel"
231                onClick={this.props.onCancelEditing}
232                small={true}
233                style={styles.cancelEditingBtn}
234                type="Dim"
235              />
236            )}
237            <input
238              type="file"
239              style={styles.hidden}
240              ref={this._filePickerSetRef}
241              onChange={this._pickFile}
242              multiple={true}
243            />
244            <Kb.Box2 direction="horizontal" fullWidth={true} style={styles.inputBox}>
245              <Kb.PlainInput
246                allowKeyboardEvents={true}
247                disabled={this.props.cannotWrite ?? false}
248                autoFocus={false}
249                ref={this._inputSetRef}
250                placeholder={this.getHintText()}
251                style={Styles.collapseStyles([styles.input, this.props.isEditing && styles.inputEditing])}
252                onChangeText={this._onChangeText}
253                multiline={true}
254                rowsMin={1}
255                rowsMax={10}
256                onKeyDown={this._onKeyDown}
257              />
258            </Kb.Box2>
259            {this.props.showingMenu && (
260              <SetExplodingMessagePopup
261                attachTo={this.props.getAttachmentRef}
262                conversationIDKey={this.props.conversationIDKey}
263                onAfterSelect={this._inputFocus}
264                onHidden={this.props.toggleShowingMenu}
265                visible={this.props.showingMenu}
266              />
267            )}
268            {this.state.emojiPickerOpen && (
269              <EmojiPicker
270                conversationIDKey={this.props.conversationIDKey}
271                emojiPickerToggle={this._emojiPickerToggle}
272                onClick={this._insertEmoji}
273              />
274            )}
275            {!this.props.cannotWrite && this.props.showWalletsIcon && (
276              <Kb.WithTooltip tooltip="Lumens">
277                <WalletsIcon
278                  size={16}
279                  style={styles.walletsIcon}
280                  conversationIDKey={this.props.conversationIDKey}
281                />
282              </Kb.WithTooltip>
283            )}
284            {!this.props.cannotWrite && (
285              <>
286                <Kb.WithTooltip tooltip="GIF">
287                  <Kb.Box style={styles.icon}>
288                    <Kb.Icon onClick={this.props.onGiphyToggle} type="iconfont-gif" />
289                  </Kb.Box>
290                </Kb.WithTooltip>
291                <Kb.WithTooltip tooltip="Emoji">
292                  <Kb.Box style={styles.icon}>
293                    <Kb.Icon
294                      color={this.state.emojiPickerOpen ? Styles.globalColors.black : null}
295                      onClick={this._emojiPickerToggle}
296                      type="iconfont-emoji"
297                    />
298                  </Kb.Box>
299                </Kb.WithTooltip>
300                <Kb.WithTooltip tooltip="Attachment">
301                  <Kb.Box style={styles.icon}>
302                    <Kb.Icon onClick={this._filePickerOpen} type="iconfont-attachment" />
303                  </Kb.Box>
304                </Kb.WithTooltip>
305              </>
306            )}
307          </Kb.Box>
308          <Kb.Box style={styles.footerContainer}>
309            <Typing conversationIDKey={this.props.conversationIDKey} />
310            <Kb.Text
311              lineClamp={1}
312              type="BodyTiny"
313              style={styles.footer}
314              onClick={this._inputFocus}
315              selectable={true}
316            >
317              {`*bold*, _italics_, \`code\`, >quote, @user, @team, #channel`}
318            </Kb.Text>
319          </Kb.Box>
320        </Kb.Box>
321      </KeyEventHandler>
322    )
323  }
324}
325const PlatformInput = AddSuggestors(_PlatformInput)
326
327const EmojiPicker = ({
328  conversationIDKey,
329  emojiPickerToggle,
330  onClick,
331}: {
332  conversationIDKey: Types.ConversationIDKey
333  emojiPickerToggle: () => void
334  onClick: (c: any) => void
335}) => (
336  <Kb.Box>
337    <Kb.Box style={styles.emojiPickerContainerWrapper} onClick={emojiPickerToggle} />
338    <Kb.Box style={styles.emojiPickerRelative}>
339      <Kb.Box style={styles.emojiPickerContainer}>
340        <EmojiPickerDesktop
341          conversationIDKey={conversationIDKey}
342          onPickAction={onClick}
343          onDidPick={emojiPickerToggle}
344        />
345      </Kb.Box>
346    </Kb.Box>
347  </Kb.Box>
348)
349
350const styles = Styles.styleSheetCreate(
351  () =>
352    ({
353      cancelEditingBtn: {
354        margin: Styles.globalMargins.xtiny,
355      },
356      container: {
357        ...Styles.globalStyles.flexBoxColumn,
358        backgroundColor: Styles.globalColors.white,
359        width: '100%',
360      },
361      emojiPickerContainer: Styles.platformStyles({
362        common: {
363          borderRadius: 4,
364          bottom: 32,
365          position: 'absolute',
366          right: -64,
367        },
368        isElectron: {
369          ...Styles.desktopStyles.boxShadow,
370        },
371      }),
372      emojiPickerContainerWrapper: {
373        ...Styles.globalStyles.fillAbsolute,
374      },
375      emojiPickerRelative: {
376        position: 'relative',
377      },
378      explodingIconContainer: Styles.platformStyles({
379        common: {
380          ...Styles.globalStyles.flexBoxColumn,
381          alignItems: 'center',
382          alignSelf: 'stretch',
383          borderBottomLeftRadius: 3,
384          borderTopLeftRadius: 3,
385          justifyContent: 'center',
386          textAlign: 'center',
387          width: 32,
388        },
389        isElectron: {
390          borderRight: `1px solid ${Styles.globalColors.black_20}`,
391        },
392      }),
393      explodingIconContainerClickable: Styles.platformStyles({
394        isElectron: {...Styles.desktopStyles.clickable},
395      }),
396      footer: {
397        alignSelf: 'flex-end',
398        color: Styles.globalColors.black_20,
399        marginBottom: Styles.globalMargins.xtiny,
400        marginRight: Styles.globalMargins.medium + 2,
401        marginTop: 2,
402        textAlign: 'right',
403      },
404      footerContainer: {
405        ...Styles.globalStyles.flexBoxRow,
406        alignItems: 'flex-start',
407        justifyContent: 'space-between',
408      },
409      hidden: {
410        display: 'none',
411      },
412      icon: {
413        alignSelf: 'flex-end',
414        marginBottom: 2,
415        marginRight: Styles.globalMargins.xtiny,
416        padding: Styles.globalMargins.xtiny,
417      },
418      input: Styles.platformStyles({
419        isElectron: {
420          backgroundColor: Styles.globalColors.transparent,
421          height: 22,
422          // Line height change is so that emojis (unicode characters inside
423          // textarea) are not clipped at the top. This change is accompanied by
424          // a change in padding to offset the increased line height
425          lineHeight: 22,
426          minHeight: 22,
427        },
428      }),
429      inputBox: {
430        flex: 1,
431        paddingBottom: Styles.globalMargins.xtiny,
432        paddingLeft: 6,
433        paddingRight: 6,
434        paddingTop: Styles.globalMargins.tiny - 2,
435        textAlign: 'left',
436      },
437      inputEditing: {
438        color: Styles.globalColors.blackOrBlack,
439      },
440      inputWrapper: {
441        ...Styles.globalStyles.flexBoxRow,
442        alignItems: 'flex-end',
443        borderRadius: 4,
444        borderStyle: 'solid',
445        borderWidth: 1,
446        marginLeft: Styles.globalMargins.small,
447        marginRight: Styles.globalMargins.small,
448        paddingRight: Styles.globalMargins.xtiny,
449      },
450      walletsIcon: {
451        alignSelf: 'flex-end',
452        marginBottom: 2,
453        marginRight: Styles.globalMargins.xtiny,
454      },
455    } as const)
456)
457
458const HoverBox = Styles.styled(Kb.Box)(() => ({
459  ':hover .timer, &.expanded .timer': {
460    color: Styles.globalColors.black,
461  },
462}))
463
464export default Kb.OverlayParentHOC(PlatformInput)
465