aboutsummaryrefslogtreecommitdiff
path: root/src/components/emoji_input/suggestor.js
blob: 9bf53183217dc0392212c00e937e91b3ec1de370 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
/**
 * suggest - generates a suggestor function to be used by emoji-input
 * data: object providing source information for specific types of suggestions:
 * data.emoji - optional, an array of all emoji available i.e.
 *   (state.instance.emoji + state.instance.customEmoji)
 * data.users - optional, an array of all known users
 * updateUsersList - optional, a function to search and append to users
 *
 * Depending on data present one or both (or none) can be present, so if field
 * doesn't support user linking you can just provide only emoji.
 */

export default data => {
  const emojiCurry = suggestEmoji(data.emoji)
  const usersCurry = suggestUsers(data.dispatch)
  return input => {
    const firstChar = input[0]
    if (firstChar === ':' && data.emoji) {
      return emojiCurry(input)
    }
    if (firstChar === '@' && data.dispatch) {
      return usersCurry(input)
    }
    return []
  }
}

export const suggestEmoji = emojis => input => {
  const noPrefix = input.toLowerCase().substr(1)
  return emojis
    .filter(({ displayText }) => displayText.toLowerCase().match(noPrefix))
    .sort((a, b) => {
      let aScore = 0
      let bScore = 0

      // An exact match always wins
      aScore += a.displayText.toLowerCase() === noPrefix ? 200 : 0
      bScore += b.displayText.toLowerCase() === noPrefix ? 200 : 0

      // Prioritize custom emoji a lot
      aScore += a.imageUrl ? 100 : 0
      bScore += b.imageUrl ? 100 : 0

      // Prioritize prefix matches somewhat
      aScore += a.displayText.toLowerCase().startsWith(noPrefix) ? 10 : 0
      bScore += b.displayText.toLowerCase().startsWith(noPrefix) ? 10 : 0

      // Sort by length
      aScore -= a.displayText.length
      bScore -= b.displayText.length

      // Break ties alphabetically
      const alphabetically = a.displayText > b.displayText ? 0.5 : -0.5

      return bScore - aScore + alphabetically
    })
}

export const suggestUsers = (dispatch) => {
  let suggestions = []
  let previousQuery = ''
  let timeout = null

  const userSearch = (query) => dispatch('searchUsers', { query, saveUsers: false })
  const debounceUserSearch = (query) => {
    return new Promise((resolve, reject) => {
      clearTimeout(timeout)
      timeout = setTimeout(() => {
        userSearch(query).then(resolve).catch(reject)
      }, 300)
    })
  }

  return async input => {
    const noPrefix = input.toLowerCase().substr(1)
    if (previousQuery === noPrefix) return suggestions

    suggestions = []
    previousQuery = noPrefix
    const users = await debounceUserSearch(noPrefix)

    const newUsers = users.filter(
      user =>
        user.screen_name.toLowerCase().startsWith(noPrefix) ||
        user.name.toLowerCase().startsWith(noPrefix)

      /* taking only 20 results so that sorting is a bit cheaper, we display
       * only 5 anyway. could be inaccurate, but we ideally we should query
       * backend anyway
       */
    ).slice(0, 20).sort((a, b) => {
      let aScore = 0
      let bScore = 0

      // Matches on screen name (i.e. user@instance) makes a priority
      aScore += a.screen_name.toLowerCase().startsWith(noPrefix) ? 2 : 0
      bScore += b.screen_name.toLowerCase().startsWith(noPrefix) ? 2 : 0

      // Matches on name takes second priority
      aScore += a.name.toLowerCase().startsWith(noPrefix) ? 1 : 0
      bScore += b.name.toLowerCase().startsWith(noPrefix) ? 1 : 0

      const diff = (bScore - aScore) * 10

      // Then sort alphabetically
      const nameAlphabetically = a.name > b.name ? 1 : -1
      const screenNameAlphabetically = a.screen_name > b.screen_name ? 1 : -1

      return diff + nameAlphabetically + screenNameAlphabetically
      /* eslint-disable camelcase */
    }).map(({ screen_name, name, profile_image_url_original }) => ({
      displayText: screen_name,
      detailText: name,
      imageUrl: profile_image_url_original,
      replacement: '@' + screen_name + ' '
    }))

    suggestions = newUsers || []
    return suggestions
    /* eslint-enable camelcase */
  }
}