git ssb

16+

Dominic / patchbay



Commit d6637ae89d381d174f5c5aac3dafebebe079c33a

working public feed

mix irving committed on 2/23/2017, 8:51:33 AM
Parent: 75fbecbdcbff096403ae2a6adeda63498269fd05

Files changed

index.jschanged
main/html/scroller.jsadded
package.jsonchanged
router/html/page/public.jschanged
about/async/suggest.jsadded
channel/async/suggest.jsadded
junk/next-stepper.jsadded
message/html/compose.jsadded
index.jsView
@@ -9,11 +9,9 @@
99 // from more specialized to more general
1010 const sockets = combine(
1111 // require(patchgit)
1212 bulk(__dirname, [
13- 'main/**/*.js',
14- 'router/**/*.js',
15- 'styles/**/*.js'
13+ '!(node_modules|junk)/**/*.js'
1614 ]),
1715 require('patchcore')
1816 )
1917
main/html/scroller.jsView
@@ -1,0 +1,26 @@
1+const { h } = require('mutant')
2+const nest = require('depnest')
3+
4+exports.gives = nest('main.html.scroller')
5+
6+exports.create = function (api) {
7+ return nest('main.html.scroller', build_scroller)
8+
9+ function build_scroller ({ prepend = [], append = [] } = {}) {
10+ const content = h('section.content')
11+
12+ const container = h('Scroller', { style: { overflow: 'auto' } }, [
13+ h('div.wrapper', [
14+ h('header', prepend),
15+ content,
16+ h('footer', append)
17+ ])
18+ ])
19+
20+ return {
21+ content,
22+ container
23+ }
24+ }
25+}
26+
package.jsonView
@@ -27,9 +27,17 @@
2727 "insert-css": "^2.0.0",
2828 "libnested": "^1.2.1",
2929 "micro-css": "^1.0.0",
3030 "mutant": "^3.16.0",
31+ "mutant-pull-reduce": "^1.0.1",
3132 "open-external": "^0.1.1",
33+ "pull-cat": "^1.1.11",
34+ "pull-next": "0.0.2",
35+ "pull-scroll": "^1.0.3",
36+ "pull-stream": "^3.5.0",
3237 "read-directory": "^2.0.0",
33- "setimmediate": "^1.0.5"
38+ "setimmediate": "^1.0.5",
39+ "ssb-mentions": "^0.1.1",
40+ "suggest-box": "^2.2.3",
41+ "xtend": "^4.0.1"
3442 }
3543 }
router/html/page/public.jsView
@@ -1,13 +1,37 @@
11 const { h } = require('mutant')
22 const nest = require('depnest')
3+const pull = require('pull-stream')
4+const Scroller = require('pull-scroll')
35
6+const next = require('../../../junk/next-stepper')
7+
48 exports.gives = nest('router.html.page')
59
10+exports.needs = nest({
11+ 'sbot.pull.log': 'first',
12+ 'message.html.compose': 'first',
13+ 'message.html.render': 'first',
14+ 'main.html.scroller': 'first'
15+})
16+
617 exports.create = function (api) {
718 return nest('router.html.page', (path) => {
819 if (path !== '/public') return
920
10- return h('div.public', 'public')
21+ const composer = api.message.html.compose({ meta: { type: 'post' }, placeholder: 'Write a public message'})
22+ var { container, content } = api.main.html.scroller({ prepend: composer })
23+
24+ pull(
25+ next(api.sbot.pull.log, {old: false, limit: 100}),
26+ Scroller(container, content, api.message.html.render, true, false)
27+ )
28+
29+ pull(
30+ next(api.sbot.pull.log, {reverse: true, limit: 100, live: false}),
31+ Scroller(container, content, api.message.html.render, false, false)
32+ )
33+
34+ return container
1135 })
1236 }
1337
about/async/suggest.jsView
@@ -1,0 +1,71 @@
1+var nest = require('depnest')
2+var { Struct, map, computed, watch } = require('mutant')
3+
4+exports.gives = nest('about.async.suggest')
5+
6+exports.needs = nest({
7+ 'about.obs': {
8+ name: 'first',
9+ imageUrl: 'first'
10+ },
11+ 'contact.obs.following': 'first',
12+ 'feed.obs.recent': 'first',
13+ 'keys.sync.id': 'first'
14+})
15+
16+exports.create = function (api) {
17+ var suggestions = null
18+ var recentSuggestions = null
19+
20+ return nest('about.async.suggest', function () {
21+ loadSuggestions()
22+ return function (word) {
23+ if (!word) {
24+ return recentSuggestions()
25+ } else {
26+ return suggestions().filter((item) => {
27+ return item.title.toLowerCase().startsWith(word.toLowerCase())
28+ })
29+ }
30+ }
31+ })
32+
33+ function loadSuggestions () {
34+ if (!suggestions) {
35+ var id = api.keys.sync.id()
36+ var following = api.contact.obs.following(id)
37+ var recentlyUpdated = api.feed.obs.recent()
38+ var contacts = computed([following, 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+ recentSuggestions = map(computed(recentlyUpdated, (items) => Array.from(items).slice(0, 10)), suggestion, {idle: true})
49+ suggestions = map(contacts, suggestion, {idle: true})
50+ watch(recentSuggestions)
51+ watch(suggestions)
52+ }
53+ }
54+
55+ function suggestion (id) {
56+ var name = api.about.obs.name(id)
57+ return Struct({
58+ title: name,
59+ id,
60+ subtitle: id.substring(0, 10),
61+ value: computed([name, id], mention),
62+ image: api.about.obs.imageUrl(id),
63+ showBoth: true
64+ })
65+ }
66+}
67+
68+function mention (name, id) {
69+ return `[@${name}](${id})`
70+}
71+
channel/async/suggest.jsView
@@ -1,0 +1,70 @@
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', function () {
19+ loadSuggestions()
20+ return function (word) {
21+ if (!word) {
22+ return suggestions().slice(0, 100)
23+ } else {
24+ return suggestions().filter((item) => {
25+ return item.title.toLowerCase().startsWith(word.toLowerCase())
26+ })
27+ }
28+ }
29+ })
30+
31+ function loadSuggestions () {
32+ if (!suggestions) {
33+ var id = api.keys.sync.id()
34+ subscribed = api.channel.obs.subscribed(id)
35+ var recentlyUpdated = api.channel.obs.recent()
36+ var contacts = computed([subscribed, recentlyUpdated], function (a, b) {
37+ var result = Array.from(a)
38+ b.forEach((item, i) => {
39+ if (!result.includes(item)) {
40+ result.push(item)
41+ }
42+ })
43+ return result
44+ })
45+
46+ suggestions = map(contacts, suggestion, {idle: true})
47+ watch(suggestions)
48+ }
49+ }
50+
51+ function suggestion (id) {
52+ return Struct({
53+ title: id,
54+ id: `#${id}`,
55+ subtitle: computed([id, subscribed], subscribedCaption),
56+ value: computed([id], mention)
57+ })
58+ }
59+}
60+
61+function subscribedCaption (id, subscribed) {
62+ if (subscribed.has(id)) {
63+ return 'subscribed'
64+ }
65+}
66+
67+function mention (id) {
68+ return `[#${id}](#${id})`
69+}
70+
junk/next-stepper.jsView
@@ -1,0 +1,59 @@
1+const pull = require('pull-stream')
2+const Next = require('pull-next')
3+
4+module.exports = nextStepper
5+
6+// TODO - this should be another module?
7+
8+function nextStepper (createStream, opts, property, range) {
9+ range = range || (opts.reverse ? 'lt' : 'gt')
10+ property = property || 'timestamp'
11+
12+ var last = null
13+ var count = -1
14+
15+ return Next(function () {
16+ if (last) {
17+ if (count === 0) return
18+ var value = opts[range] = get(last, property)
19+ if (value == null) return
20+ last = null
21+ }
22+ return pull(
23+ createStream(clone(opts)),
24+ pull.through(function (msg) {
25+ count++
26+ if (!msg.sync) {
27+ last = msg
28+ }
29+ }, function (err) {
30+ // retry on errors...
31+ if (err) {
32+ count = -1
33+ return count
34+ }
35+ // end stream if there were no results
36+ if (last == null) last = {}
37+ })
38+ )
39+ })
40+}
41+
42+function get (obj, path) {
43+ if (!obj) return undefined
44+ if (typeof path === 'string') return obj[path]
45+ if (Array.isArray(path)) {
46+ for (var i = 0; obj && i < path.length; i++) {
47+ obj = obj[path[i]]
48+ }
49+ return obj
50+ }
51+}
52+
53+function clone (obj) {
54+ var _obj = {}
55+ for (var k in obj) _obj[k] = obj[k]
56+ return _obj
57+}
58+
59+
message/html/compose.jsView
@@ -1,0 +1,147 @@
1+const { h, when, send, resolve, Value, computed } = require('mutant')
2+const nest = require('depnest')
3+const mentions = require('ssb-mentions')
4+const extend = require('xtend')
5+const addSuggest = require('suggest-box')
6+
7+exports.needs = nest({
8+ 'about.async.suggest': 'first',
9+ 'blob.html.input': 'first',
10+ 'channel.async.suggest': 'first',
11+ 'emoji.sync': {
12+ names: 'first',
13+ url: 'first'
14+ },
15+ 'message.async.publish': 'first'
16+})
17+
18+exports.gives = nest('message.html.compose')
19+
20+exports.create = function (api) {
21+ return nest('message.html.compose', compose)
22+
23+ function compose ({ shrink = true, meta, prepublish, placeholder = 'Write a message' }, cb) {
24+ var files = []
25+ var filesById = {}
26+ var focused = Value(false)
27+ var hasContent = Value(false)
28+ var getProfileSuggestions = api.about.async.suggest()
29+ var getChannelSuggestions = api.channel.async.suggest()
30+
31+ var blurTimeout = null
32+
33+ var expanded = computed([shrink, focused, hasContent], (shrink, focused, hasContent) => {
34+ if (!shrink || hasContent) {
35+ return true
36+ } else {
37+ return focused
38+ }
39+ })
40+
41+ var textArea = h('textarea', {
42+ 'ev-input': function () {
43+ hasContent.set(!!textArea.value)
44+ },
45+ 'ev-blur': () => {
46+ clearTimeout(blurTimeout)
47+ blurTimeout = setTimeout(() => focused.set(false), 200)
48+ },
49+ 'ev-focus': send(focused.set, true),
50+ placeholder
51+ })
52+
53+ var fileInput = api.blob.html.input(file => {
54+ files.push(file)
55+ filesById[file.link] = file
56+
57+ var embed = file.type.indexOf('image/') === 0 ? '!' : ''
58+
59+ textArea.value += embed + `[${file.name}](${file.link})`
60+ console.log('added:', file)
61+ })
62+
63+ fileInput.onclick = function () {
64+ hasContent.set(true)
65+ }
66+
67+ var publishBtn = h('button', { 'ev-click': publish }, 'Publish')
68+
69+ var actions = h('section.actions', [
70+ fileInput,
71+ publishBtn
72+ ])
73+
74+ var composer = h('Compose', {
75+ classList: when(expanded, '-expanded', '-contracted')
76+ }, [
77+ textArea,
78+ actions
79+ ])
80+
81+ addSuggest(textArea, (inputText, cb) => {
82+ if (inputText[0] === '@') {
83+ cb(null, getProfileSuggestions(inputText.slice(1)))
84+ } else if (inputText[0] === '#') {
85+ cb(null, getChannelSuggestions(inputText.slice(1)))
86+ } else if (inputText[0] === ':') {
87+ // suggest emojis
88+ var word = inputText.slice(1)
89+ if (word[word.length - 1] === ':') {
90+ word = word.slice(0, -1)
91+ }
92+ // TODO: when no emoji typed, list some default ones
93+ cb(null, api.emoji.sync.names().filter(function (name) {
94+ return name.slice(0, word.length) === word
95+ }).slice(0, 100).map(function (emoji) {
96+ return {
97+ image: api.emoji.sync.url(emoji),
98+ title: emoji,
99+ subtitle: emoji,
100+ value: ':' + emoji + ':'
101+ }
102+ }))
103+ }
104+ }, {cls: 'SuggestBox'})
105+
106+ return composer
107+
108+ // scoped
109+
110+ function publish () {
111+ publishBtn.disabled = true
112+
113+ meta = extend(resolve(meta), {
114+ text: textArea.value,
115+ mentions: mentions(textArea.value).map(mention => {
116+ // merge markdown-detected mention with file info
117+ var file = filesById[mention.link]
118+ if (file) {
119+ if (file.type) mention.type = file.type
120+ if (file.size) mention.size = file.size
121+ }
122+ return mention
123+ })
124+ })
125+
126+ try {
127+ if (typeof prepublish === 'function') {
128+ meta = prepublish(meta)
129+ }
130+ } catch (err) {
131+ publishBtn.disabled = false
132+ if (cb) cb(err)
133+ else throw err
134+ }
135+
136+ return api.message.async.publish(meta, done)
137+
138+ function done (err, msg) {
139+ publishBtn.disabled = false
140+ if (err) throw err
141+ else if (msg) textArea.value = ''
142+ if (cb) cb(err, msg)
143+ }
144+ }
145+ }
146+}
147+

Built with git-ssb-web