git ssb

0+

mixmix / patch-suggest



Tree: 6b51ef76387825aa524424f71d8d722fa7eba2cf

Files: 6b51ef76387825aa524424f71d8d722fa7eba2cf / about / async / suggest.js

5352 bytesRaw
1const nest = require('depnest')
2const { isFeed } = require('ssb-ref')
3const { h, Struct, map, concat, dictToCollection, computed, lookup, watch, keys, resolve } = require('mutant')
4
5const KEY_SAMPLE_LENGTH = 10 // includes @
6
7exports.gives = nest('about.async.suggest')
8
9exports.needs = nest({
10 'about.obs.groupedValues': 'first',
11 'about.obs.name': 'first',
12 'about.obs.imageUrl': 'first',
13 'contact.obs.following': 'first',
14 'feed.obs.recent': 'first',
15 'keys.sync.id': 'first'
16})
17
18exports.create = function (api) {
19 var recentSuggestions = null
20 var suggestions = null
21
22 return nest('about.async.suggest', suggestedProfile)
23
24 function suggestedProfile () {
25 loadSuggestions()
26
27 return function (word, extraIds = []) {
28 var moreSuggestions = buildSuggestions(extraIds)
29
30 if (!word && extraIds.length) return resolve(moreSuggestions)
31 if (!word) return resolve(recentSuggestions)
32
33 var wordNormed = normalise(word)
34
35 return suggestions()
36 .concat(resolve(moreSuggestions))
37 .filter(item => ~normalise(item.title).indexOf(wordNormed))
38 .sort((a, b) => {
39 // where name is is an exact match
40 if (a.title === word) return -1
41 if (b.title === word) return +1
42
43 // TODO - move all this into the suggestion building and decorate the suggestion?
44 const normedATitle = normalise(a.title)
45 const normedBTitle = normalise(b.title)
46
47 // where normalised name is an exact match
48 if (normedATitle === wordNormed) return -1
49 if (normedBTitle === wordNormed) return +1
50
51 // where name is matching exactly so far
52 if (a.title.indexOf(word) === 0) return -1
53 if (b.title.indexOf(word) === 0) return +1
54
55 // where name is matching exactly so far (case insensitive)
56 if (normedATitle.indexOf(wordNormed) === 0) return -1
57 if (normedBTitle.indexOf(wordNormed) === 0) return +1
58 })
59 .reduce((sofar, match) => {
60 // prune down to the first instance of each id
61 // this presumes if you were typing e.g. "dino" you don't need "ahdinosaur" as well
62 if (sofar.find(el => el.id === match.id)) return sofar
63
64 return [...sofar, match]
65 }, [])
66 .sort((a, b) => {
67 // bubble up names where typed word matches our name for them
68 if (a._isPrefered) return -1
69 if (b._isPrefered) return +1
70 })
71 }
72 }
73
74
75 //// PRIVATE ////
76
77 function loadSuggestions () {
78 if (suggestions) return
79
80 var myId = api.keys.sync.id()
81 var following = api.contact.obs.following(myId)
82 var recentlyUpdated = api.feed.obs.recent()
83
84 recentSuggestions = map(
85 computed(recentlyUpdated, (items) => Array.from(items).slice(0, 10)),
86 buildSuggestion,
87 {idle: true}
88 )
89 watch(recentSuggestions)
90
91 var contacts = computed([following, recentlyUpdated], (a, b) => {
92 var result = new Set(a)
93 b.forEach(item => result.add(item))
94
95 return Array.from(result)
96 })
97 const suggestionsRecord = lookup(contacts, contact => {
98 return [contact, keys(api.about.obs.groupedValues(contact, 'name'))]
99 })
100 suggestions = concat(
101 map(
102 dictToCollection(suggestionsRecord),
103 pluralSuggestions,
104 {idle: true}
105 )
106 )
107 watch(suggestions)
108 }
109
110 function pluralSuggestions (item) {
111 const id = resolve(item.key)
112
113 return computed([api.about.obs.name(id)], myNameForThem => {
114 return map(item.value, name => {
115 const names = item.value()
116
117 const aliases = names
118 .filter(n => n != name)
119 .map(name => h('div.alias',
120 { className: name === myNameForThem ? '-bold' : '' },
121 name
122 ))
123
124 return Struct({
125 id,
126 title: name,
127 subtitle: [
128 h('div.aliases', aliases),
129 h('div.key', id.substring(0, KEY_SAMPLE_LENGTH))
130 ],
131 value: mention(name, id),
132 image: api.about.obs.imageUrl(id),
133 showBoth: true,
134 _isPrefered: normalise(name) === normalise(myNameForThem)
135 })
136 })
137 })
138 }
139
140 function buildSuggestions (idsObs) {
141 return concat([ // (mix) urg, I don't know why this is needed, but it makes it behave the same as asuggestions - when you resolve this it resolves the whole structure
142 computed([idsObs], ids => { // NOTE [] is needed here
143 return ids
144 .filter(isFeed)
145 .reduce((sofar, feedId) => {
146 if (sofar.includes(feedId)) return sofar
147 return [...sofar, feedId]
148 }, [])
149 .map(buildSuggestion)
150 })
151 ])
152 }
153
154 // used to cobble together additional suggestions
155 function buildSuggestion (id) {
156 var name = api.about.obs.name(id)
157 return Struct({
158 // return {
159 id,
160 title: name,
161 subtitle: h('div.key', id.substring(0, KEY_SAMPLE_LENGTH)),
162 value: computed([name, id], mention),
163 image: api.about.obs.imageUrl(id),
164 showBoth: true
165 })
166 }
167}
168
169function normalise (word) {
170 // TODO - this shouldn't need a reslve.
171 // It's generated by buildSuggestion title, but not pluralSuggestions title
172 return resolve(word).toLowerCase().replace(/(\s|-)/g, '')
173}
174
175function mention (name, id) {
176 return `[@${name}](${id})`
177}
178
179

Built with git-ssb-web