logo

pleroma-fe

My custom branche(s) on git.pleroma.social/pleroma/pleroma-fe git clone https://hacktivis.me/git/pleroma-fe.git

suggestor.js (4482B)


  1. /**
  2. * suggest - generates a suggestor function to be used by emoji-input
  3. * data: object providing source information for specific types of suggestions:
  4. * data.emoji - optional, an array of all emoji available i.e.
  5. * (getters.standardEmojiList + state.instance.customEmoji)
  6. * data.users - optional, an array of all known users
  7. * updateUsersList - optional, a function to search and append to users
  8. *
  9. * Depending on data present one or both (or none) can be present, so if field
  10. * doesn't support user linking you can just provide only emoji.
  11. */
  12. export default data => {
  13. const emojiCurry = suggestEmoji(data.emoji)
  14. const usersCurry = data.store && suggestUsers(data.store)
  15. return (input, nameKeywordLocalizer) => {
  16. const firstChar = input[0]
  17. if (firstChar === ':' && data.emoji) {
  18. return emojiCurry(input, nameKeywordLocalizer)
  19. }
  20. if (firstChar === '@' && usersCurry) {
  21. return usersCurry(input)
  22. }
  23. return []
  24. }
  25. }
  26. export const suggestEmoji = emojis => (input, nameKeywordLocalizer) => {
  27. const noPrefix = input.toLowerCase().substr(1)
  28. return emojis
  29. .map(emoji => ({ ...emoji, ...nameKeywordLocalizer(emoji) }))
  30. .filter((emoji) => (emoji.names.concat(emoji.keywords)).filter(kw => kw.toLowerCase().match(noPrefix)).length)
  31. .map(k => {
  32. let score = 0
  33. // An exact match always wins
  34. score += Math.max(...k.names.map(name => name.toLowerCase() === noPrefix ? 200 : 0), 0)
  35. // Prioritize custom emoji a lot
  36. score += k.imageUrl ? 100 : 0
  37. // Prioritize prefix matches somewhat
  38. score += Math.max(...k.names.map(kw => kw.toLowerCase().startsWith(noPrefix) ? 10 : 0), 0)
  39. // Sort by length
  40. score -= k.displayText.length
  41. k.score = score
  42. return k
  43. })
  44. .sort((a, b) => {
  45. // Break ties alphabetically
  46. const alphabetically = a.displayText > b.displayText ? 0.5 : -0.5
  47. return b.score - a.score + alphabetically
  48. })
  49. }
  50. export const suggestUsers = ({ dispatch, state }) => {
  51. // Keep some persistent values in closure, most importantly for the
  52. // custom debounce to work. Lodash debounce does not return a promise.
  53. let suggestions = []
  54. let previousQuery = ''
  55. let timeout = null
  56. let cancelUserSearch = null
  57. const userSearch = (query) => dispatch('searchUsers', { query })
  58. const debounceUserSearch = (query) => {
  59. cancelUserSearch && cancelUserSearch()
  60. return new Promise((resolve, reject) => {
  61. timeout = setTimeout(() => {
  62. userSearch(query).then(resolve).catch(reject)
  63. }, 300)
  64. cancelUserSearch = () => {
  65. clearTimeout(timeout)
  66. resolve([])
  67. }
  68. })
  69. }
  70. return async input => {
  71. const noPrefix = input.toLowerCase().substr(1)
  72. if (previousQuery === noPrefix) return suggestions
  73. suggestions = []
  74. previousQuery = noPrefix
  75. // Fetch more and wait, don't fetch if there's the 2nd @ because
  76. // the backend user search can't deal with it.
  77. // Reference semantics make it so that we get the updated data after
  78. // the await.
  79. if (!noPrefix.includes('@')) {
  80. await debounceUserSearch(noPrefix)
  81. }
  82. const newSuggestions = state.users.users.filter(
  83. user =>
  84. user.screen_name && user.name && (
  85. user.screen_name.toLowerCase().startsWith(noPrefix) ||
  86. user.name.toLowerCase().startsWith(noPrefix))
  87. ).slice(0, 20).sort((a, b) => {
  88. let aScore = 0
  89. let bScore = 0
  90. // Matches on screen name (i.e. user@instance) makes a priority
  91. aScore += a.screen_name.toLowerCase().startsWith(noPrefix) ? 2 : 0
  92. bScore += b.screen_name.toLowerCase().startsWith(noPrefix) ? 2 : 0
  93. // Matches on name takes second priority
  94. aScore += a.name.toLowerCase().startsWith(noPrefix) ? 1 : 0
  95. bScore += b.name.toLowerCase().startsWith(noPrefix) ? 1 : 0
  96. const diff = (bScore - aScore) * 10
  97. // Then sort alphabetically
  98. const nameAlphabetically = a.name > b.name ? 1 : -1
  99. const screenNameAlphabetically = a.screen_name > b.screen_name ? 1 : -1
  100. return diff + nameAlphabetically + screenNameAlphabetically
  101. /* eslint-disable camelcase */
  102. }).map((user) => ({
  103. user,
  104. displayText: user.screen_name_ui,
  105. detailText: user.name,
  106. imageUrl: user.profile_image_url_original,
  107. replacement: '@' + user.screen_name + ' '
  108. }))
  109. /* eslint-enable camelcase */
  110. suggestions = newSuggestions || []
  111. return suggestions
  112. }
  113. }