Files: 6b51ef76387825aa524424f71d8d722fa7eba2cf / about / async / suggest.js
5352 bytesRaw
1 | const nest = require('depnest') |
2 | const { isFeed } = require('ssb-ref') |
3 | const { h, Struct, map, concat, dictToCollection, computed, lookup, watch, keys, resolve } = require('mutant') |
4 | |
5 | const KEY_SAMPLE_LENGTH = 10 // includes @ |
6 | |
7 | exports.gives = nest('about.async.suggest') |
8 | |
9 | exports.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 | |
18 | exports.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 | |
169 | function 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 | |
175 | function mention (name, id) { |
176 | return `[@${name}](${id})` |
177 | } |
178 | |
179 |
Built with git-ssb-web