Commit 2ec6232dcda4dc6c53e030d73bd1d357ce0d36be
initial commit - extraction from patchbay
mix irving committed on 9/20/2017, 5:15:55 AMFiles changed
.gitignore | added |
about/async/suggest.js | added |
channel/async/suggest.js | added |
emoji/async/suggest.js | added |
index.js | added |
package.json | added |
styles/mcss.js | added |
about/async/suggest.js | ||
---|---|---|
@@ -1,0 +1,134 @@ | ||
1 … | +const nest = require('depnest') | |
2 … | +const { h, Struct, map, concat, dictToCollection, computed, lookup, watch, keys, resolve } = require('mutant') | |
3 … | + | |
4 … | +const KEY_SAMPLE_LENGTH = 10 // includes @ | |
5 … | + | |
6 … | +exports.gives = nest('about.async.suggest') | |
7 … | + | |
8 … | +exports.needs = nest({ | |
9 … | + 'about.obs.groupedValues': 'first', | |
10 … | + 'about.obs.name': 'first', | |
11 … | + 'about.obs.imageUrl': 'first', | |
12 … | + 'contact.obs.following': 'first', | |
13 … | + 'feed.obs.recent': 'first', | |
14 … | + 'keys.sync.id': 'first' | |
15 … | +}) | |
16 … | + | |
17 … | +exports.create = function (api) { | |
18 … | + var suggestions = null | |
19 … | + var recentSuggestions = null | |
20 … | + | |
21 … | + return nest('about.async.suggest', suggestedProfile) | |
22 … | + | |
23 … | + function suggestedProfile () { | |
24 … | + loadSuggestions() | |
25 … | + return function (word) { | |
26 … | + if (!word) return recentSuggestions() | |
27 … | + | |
28 … | + wordLower = word.toLowerCase() | |
29 … | + return suggestions() | |
30 … | + .filter(item => ~item.title.toLowerCase().indexOf(wordLower)) | |
31 … | + .sort((a, b) => { | |
32 … | + // where name is matching exactly so far | |
33 … | + if (a.title.indexOf(word) === 0) return -1 | |
34 … | + if (b.title.indexOf(word) === 0) return +1 | |
35 … | + | |
36 … | + // where name is matching exactly so far (case insensitive) | |
37 … | + if (a.title.toLowerCase().indexOf(wordLower) === 0) return -1 | |
38 … | + if (b.title.toLowerCase().indexOf(wordLower) === 0) return +1 | |
39 … | + }) | |
40 … | + .reduce((sofar, match) => { | |
41 … | + // prune down to the first instance of each id | |
42 … | + // this presumes if you were typing e.g. "dino" you don't need "ahdinosaur" as well | |
43 … | + if (sofar.find(el => el.id === match.id)) return sofar | |
44 … | + | |
45 … | + return [...sofar, match] | |
46 … | + }, []) | |
47 … | + .sort((a, b) => { | |
48 … | + // bubble up names where typed word matches our name for them | |
49 … | + if (a._isPrefered) return -1 | |
50 … | + if (b._isPrefered) return +1 | |
51 … | + }) | |
52 … | + } | |
53 … | + } | |
54 … | + | |
55 … | + function loadSuggestions () { | |
56 … | + if (suggestions) return | |
57 … | + | |
58 … | + var id = api.keys.sync.id() | |
59 … | + var following = api.contact.obs.following(id) | |
60 … | + var recentlyUpdated = api.feed.obs.recent() | |
61 … | + var contacts = computed([following, recentlyUpdated], (a, b) => { | |
62 … | + var result = new Set(a) | |
63 … | + b.forEach(item => result.add(item)) | |
64 … | + | |
65 … | + return Array.from(result) | |
66 … | + }) | |
67 … | + | |
68 … | + recentSuggestions = map( | |
69 … | + computed(recentlyUpdated, (items) => Array.from(items).slice(0, 10)), | |
70 … | + suggestion, | |
71 … | + {idle: true} | |
72 … | + ) | |
73 … | + | |
74 … | + const suggestionsRecord = lookup(contacts, contact => { | |
75 … | + return [contact, keys(api.about.obs.groupedValues(contact, 'name'))] | |
76 … | + }) | |
77 … | + | |
78 … | + suggestions = concat( | |
79 … | + map(dictToCollection(suggestionsRecord), pluralSuggestions, {idle: true}) | |
80 … | + ) | |
81 … | + | |
82 … | + watch(recentSuggestions) | |
83 … | + watch(suggestions) | |
84 … | + } | |
85 … | + | |
86 … | + function pluralSuggestions (item) { | |
87 … | + const id = resolve(item.key) | |
88 … | + | |
89 … | + return computed([api.about.obs.name(id)], myNameForThem => { | |
90 … | + return map(item.value, name => { | |
91 … | + const names = item.value() | |
92 … | + | |
93 … | + const aliases = names | |
94 … | + .filter(n => n != name) | |
95 … | + .map(name => h('div.alias', | |
96 … | + { className: name === myNameForThem ? '-bold' : '' }, | |
97 … | + name | |
98 … | + )) | |
99 … | + | |
100 … | + return Struct({ | |
101 … | + id, | |
102 … | + title: name, | |
103 … | + subtitle: [ | |
104 … | + h('div.aliases', aliases), | |
105 … | + h('div.key', id.substring(0, KEY_SAMPLE_LENGTH)) | |
106 … | + ], | |
107 … | + value: mention(name, id), | |
108 … | + image: api.about.obs.imageUrl(id), | |
109 … | + showBoth: true, | |
110 … | + _isPrefered: name === myNameForThem | |
111 … | + }) | |
112 … | + }) | |
113 … | + }) | |
114 … | + | |
115 … | + } | |
116 … | + | |
117 … | + // feeds recentSuggestions | |
118 … | + function suggestion (id) { | |
119 … | + var name = api.about.obs.name(id) | |
120 … | + return Struct({ | |
121 … | + id, | |
122 … | + title: name, | |
123 … | + subtitle: h('div.key', id.substring(0, KEY_SAMPLE_LENGTH)), | |
124 … | + value: computed([name, id], mention), | |
125 … | + image: api.about.obs.imageUrl(id), | |
126 … | + showBoth: true | |
127 … | + }) | |
128 … | + } | |
129 … | +} | |
130 … | + | |
131 … | +function mention (name, id) { | |
132 … | + return `[@${name}](${id})` | |
133 … | +} | |
134 … | + |
channel/async/suggest.js | ||
---|---|---|
@@ -1,0 +1,72 @@ | ||
1 … | +const nest = require('depnest') | |
2 … | +const { computed, watch, map, Struct } = require('mutant') | |
3 … | + | |
4 … | +exports.gives = nest('channel.async.suggest') | |
5 … | + | |
6 … | +exports.needs = nest({ | |
7 … | + 'channel.obs': { | |
8 … | + recent: 'first', | |
9 … | + subscribed: 'first' | |
10 … | + }, | |
11 … | + 'keys.sync.id': 'first' | |
12 … | +}) | |
13 … | + | |
14 … | +exports.create = function (api) { | |
15 … | + var suggestions = null | |
16 … | + var subscribed = null | |
17 … | + | |
18 … | + return nest('channel.async.suggest', suggestedChannels) | |
19 … | + | |
20 … | + function suggestedChannels () { | |
21 … | + loadSuggestions() | |
22 … | + return function (word) { | |
23 … | + if (!word) { | |
24 … | + return suggestions().slice(0, 100) | |
25 … | + } else { | |
26 … | + word = word.toLowerCase() | |
27 … | + return suggestions() | |
28 … | + .filter(item => ~item.title.toLowerCase().indexOf(word)) | |
29 … | + } | |
30 … | + } | |
31 … | + } | |
32 … | + | |
33 … | + function loadSuggestions () { | |
34 … | + if (!suggestions) { | |
35 … | + var id = api.keys.sync.id() | |
36 … | + subscribed = api.channel.obs.subscribed(id) | |
37 … | + var recentlyUpdated = api.channel.obs.recent() | |
38 … | + var contacts = computed([subscribed, recentlyUpdated], function (a, b) { | |
39 … | + var result = Array.from(a) | |
40 … | + b.forEach((item, i) => { | |
41 … | + if (!result.includes(item)) { | |
42 … | + result.push(item) | |
43 … | + } | |
44 … | + }) | |
45 … | + return result | |
46 … | + }) | |
47 … | + | |
48 … | + suggestions = map(contacts, suggestion, {idle: true}) | |
49 … | + watch(suggestions) | |
50 … | + } | |
51 … | + } | |
52 … | + | |
53 … | + function suggestion (id) { | |
54 … | + return Struct({ | |
55 … | + title: id, | |
56 … | + id: `#${id}`, | |
57 … | + subtitle: computed([id, subscribed], subscribedCaption), | |
58 … | + value: computed([id], mention) | |
59 … | + }) | |
60 … | + } | |
61 … | +} | |
62 … | + | |
63 … | +function subscribedCaption (id, subscribed) { | |
64 … | + if (subscribed.has(id)) { | |
65 … | + return 'subscribed' | |
66 … | + } | |
67 … | +} | |
68 … | + | |
69 … | +function mention (id) { | |
70 … | + return `[#${id}](#${id})` | |
71 … | +} | |
72 … | + |
emoji/async/suggest.js | ||
---|---|---|
@@ -1,0 +1,37 @@ | ||
1 … | +const nest = require('depnest') | |
2 … | + | |
3 … | +exports.gives = nest('emoji.async.suggest') | |
4 … | + | |
5 … | +exports.needs = nest({ | |
6 … | + 'emoji.sync.names': 'first', | |
7 … | + 'emoji.sync.url': 'first' | |
8 … | +}) | |
9 … | + | |
10 … | +exports.create = function (api) { | |
11 … | + var suggestions = null | |
12 … | + var subscribed = null | |
13 … | + | |
14 … | + return nest('emoji.async.suggest', suggestedEmoji) | |
15 … | + | |
16 … | + function suggestedEmoji (word) { | |
17 … | + return function (word) { | |
18 … | + if (word[word.length - 1] === ':') { | |
19 … | + word = word.slice(0, -1) | |
20 … | + } | |
21 … | + word = word.toLowerCase() | |
22 … | + // TODO: when no emoji typed, list some default ones | |
23 … | + | |
24 … | + return api.emoji.sync.names() | |
25 … | + .filter(name => ~name.indexOf(word)) | |
26 … | + .slice(0, 100).map(emoji => { | |
27 … | + return { | |
28 … | + image: api.emoji.sync.url(emoji), | |
29 … | + title: emoji, | |
30 … | + subtitle: emoji, | |
31 … | + value: ':' + emoji + ':' | |
32 … | + } | |
33 … | + }) | |
34 … | + } | |
35 … | + } | |
36 … | +} | |
37 … | + |
index.js | ||
---|---|---|
@@ -1,0 +1,10 @@ | ||
1 … | +const nest = require('depnest') | |
2 … | + | |
3 … | +module.exports = { | |
4 … | + patchSuggest: nest({ | |
5 … | + 'about.async.suggest': require('./about/async/suggest'), | |
6 … | + 'channel.async.suggest': require('./channel/async/suggest'), | |
7 … | + 'emoji.async.suggest': require('./emoji/async/suggest'), | |
8 … | + 'styles.mcss': require('./styles/mcss') | |
9 … | + }) | |
10 … | +} |
package.json | ||
---|---|---|
@@ -1,0 +1,28 @@ | ||
1 … | +{ | |
2 … | + "name": "patch-suggest", | |
3 … | + "version": "0.0.1", | |
4 … | + "description": "The commons suggesters for text fields - for patch-* family apps", | |
5 … | + "main": "index.js", | |
6 … | + "scripts": { | |
7 … | + "test": "echo \"Error: no test specified\" && exit 1" | |
8 … | + }, | |
9 … | + "repository": { | |
10 … | + "type": "git", | |
11 … | + "url": "git+https://github.com/mixmix/patch-suggest.git" | |
12 … | + }, | |
13 … | + "keywords": [ | |
14 … | + "patchcore", | |
15 … | + "depject", | |
16 … | + "suggest" | |
17 … | + ], | |
18 … | + "author": "mixmix", | |
19 … | + "license": "AGPL-3.0", | |
20 … | + "bugs": { | |
21 … | + "url": "https://github.com/mixmix/patch-suggest/issues" | |
22 … | + }, | |
23 … | + "homepage": "https://github.com/mixmix/patch-suggest", | |
24 … | + "dependencies": { | |
25 … | + "depnest": "^1.3.0", | |
26 … | + "mutant": "^3.21.2" | |
27 … | + } | |
28 … | +} |
styles/mcss.js | ||
---|---|---|
@@ -1,0 +1,111 @@ | ||
1 … | +const nest = require('depnest') | |
2 … | + | |
3 … | +exports.gives = nest('styles.mcss') | |
4 … | + | |
5 … | +const suggestBox = ` | |
6 … | +body { | |
7 … | + div.suggest-box { | |
8 … | + width: max-content | |
9 … | + background-color: #fff | |
10 … | + | |
11 … | + min-width: 20rem | |
12 … | + max-width: 35rem | |
13 … | + padding: .2rem .5rem | |
14 … | + margin-top: .35rem | |
15 … | + border: 1px gainsboro solid | |
16 … | + | |
17 … | + ul { | |
18 … | + list-style-type: none | |
19 … | + padding: 0 | |
20 … | + | |
21 … | + li { | |
22 … | + display: flex | |
23 … | + align-items: center | |
24 … | + | |
25 … | + padding-right: .2rem | |
26 … | + margin-bottom: .2rem | |
27 … | + | |
28 … | + img { | |
29 … | + height: 36px | |
30 … | + width: 36px | |
31 … | + min-width: 36px | |
32 … | + padding: .2rem | |
33 … | + } | |
34 … | + | |
35 … | + strong { | |
36 … | + font-weight: 300 | |
37 … | + min-width: 7rem | |
38 … | + margin-left: .5rem | |
39 … | + margin-right: .5rem | |
40 … | + | |
41 … | + span.subtle { | |
42 … | + color: #aaa | |
43 … | + } | |
44 … | + } | |
45 … | + | |
46 … | + small { | |
47 … | + flex-grow: 1 | |
48 … | + | |
49 … | + margin-left: .5rem | |
50 … | + padding-right: .2rem | |
51 … | + font-size: 1rem | |
52 … | + | |
53 … | + display: flex | |
54 … | + justify-content: flex-end | |
55 … | + | |
56 … | + div.aliases { | |
57 … | + flex-grow: 1 | |
58 … | + | |
59 … | + font-size: .8rem | |
60 … | + color: #666 | |
61 … | + margin-right: .5rem | |
62 … | + | |
63 … | + display: flex | |
64 … | + flex-wrap: wrap | |
65 … | + | |
66 … | + div.alias { | |
67 … | + margin-right: .4rem | |
68 … | + -bold { | |
69 … | + font-weight: 600 | |
70 … | + } | |
71 … | + } | |
72 … | + } | |
73 … | + | |
74 … | + div.key { | |
75 … | + align-self: flex-end | |
76 … | + | |
77 … | + margin: auto 0 | |
78 … | + | |
79 … | + font-family: monospace | |
80 … | + font-size: .8rem | |
81 … | + min-width: 5rem | |
82 … | + } | |
83 … | + } | |
84 … | + } | |
85 … | + | |
86 … | + li.selected { | |
87 … | + color: #fff | |
88 … | + background: #0caaf9 | |
89 … | + | |
90 … | + img {} | |
91 … | + strong {} | |
92 … | + small { | |
93 … | + div.aliases { | |
94 … | + color: #eee | |
95 … | + } | |
96 … | + } | |
97 … | + } | |
98 … | + } | |
99 … | + } | |
100 … | +} | |
101 … | +` | |
102 … | + | |
103 … | +exports.create = (api) => { | |
104 … | + return nest('styles.mcss', mcss) | |
105 … | + | |
106 … | + function mcss (sofar = {}) { | |
107 … | + sofar['patchSuggest.suggestBox'] = suggestBox | |
108 … | + | |
109 … | + return sofar | |
110 … | + } | |
111 … | +} |
Built with git-ssb-web