git ssb

0+

mixmix / patch-suggest



Commit 2ec6232dcda4dc6c53e030d73bd1d357ce0d36be

initial commit - extraction from patchbay

mix irving committed on 9/20/2017, 5:15:55 AM

Files changed

.gitignoreadded
about/async/suggest.jsadded
channel/async/suggest.jsadded
emoji/async/suggest.jsadded
index.jsadded
package.jsonadded
styles/mcss.jsadded
.gitignoreView
@@ -1,0 +1,1 @@
1 +node_modules
about/async/suggest.jsView
@@ -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.jsView
@@ -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.jsView
@@ -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.jsView
@@ -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.jsonView
@@ -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.jsView
@@ -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