Commit 87dd7388171016a3b49fb155c4b07fd06b4c5436
get comment notifications mostly working
mix irving committed on 5/7/2018, 4:55:57 AMParent: fea2881ae175c88bac926fcff65d32598742e9a1
Files changed
app/html/comments.js | changed |
app/html/scroller.js | changed |
app/html/scroller.mcss | changed |
app/page/notifications.js | changed |
app/page/notifications.mcss | added |
message/html/comment.js | changed |
message/html/comment.mcss | changed |
ssb-server-blog-stats.js | changed |
app/html/comments.js | ||
---|---|---|
@@ -22,15 +22,12 @@ | ||
22 | 22 | return msgs |
23 | 23 | .filter(msg => forkOf(msg) === undefined) // exclude nested replies / forks |
24 | 24 | .filter(msg => msg.value.content.root) // exclude root message / blog |
25 | 25 | .map(comment => { |
26 | - const nestedReplies = msgs.filter(msg => forkOf(msg) === comment.key) | |
27 | - | |
28 | 26 | return Struct({ |
29 | - comment: comment, | |
30 | - replies: nestedReplies | |
27 | + comment, | |
28 | + replies: msgs.filter(msg => forkOf(msg) === comment.key) | |
31 | 29 | }) |
32 | - // return Object.assign({}, comment, { replies: nestedReplies }) | |
33 | 30 | }) |
34 | 31 | }) |
35 | 32 | |
36 | 33 | const root = computed(messages, ary => ary[0].key) |
@@ -58,9 +55,9 @@ | ||
58 | 55 | { |
59 | 56 | comparer: (a, b) => { |
60 | 57 | if (a === undefined || b === undefined) return false |
61 | 58 | |
62 | - return a.comment() === b.comment() && a.replies().length === b.replies().length | |
59 | + return a.comment().key === b.comment().key && a.replies().length === b.replies().length | |
63 | 60 | } |
64 | 61 | } |
65 | 62 | ), |
66 | 63 | compose({ meta, feedIdsInThread, shrink: false, canAttach: true, placeholder: strings.writeComment }) |
app/html/scroller.js | ||
---|---|---|
@@ -13,20 +13,20 @@ | ||
13 | 13 | return nest('app.html.scroller', createScroller) |
14 | 14 | |
15 | 15 | function createScroller (opts = {}) { |
16 | 16 | const { |
17 | - stream, | |
17 | + stream, // TODO - rename this to createStream (rename across app) | |
18 | 18 | filter = () => pull.filter((msg) => true), |
19 | 19 | indexProperty = ['value', 'timestamp'] |
20 | 20 | } = opts |
21 | 21 | |
22 | 22 | const streamToTop = pull( |
23 | - next(stream, {old: false, limit: 100, property: indexProperty }), | |
23 | + next(stream, { live: true, reverse: false, old: false, limit: 100, property: indexProperty }), | |
24 | 24 | filter() // is a pull-stream through |
25 | 25 | ) |
26 | 26 | |
27 | 27 | const streamToBottom = pull( |
28 | - next(stream, {reverse: true, limit: 100, live: false, property: indexProperty }), | |
28 | + next(stream, { live: false, reverse: true, limit: 100, property: indexProperty }), | |
29 | 29 | filter() |
30 | 30 | ) |
31 | 31 | |
32 | 32 | return Scroller(Object.assign({}, opts, { streamToTop, streamToBottom })) |
app/html/scroller.mcss | ||
---|---|---|
@@ -1,8 +1,8 @@ | ||
1 | 1 | Scroller { |
2 | 2 | overflow: auto |
3 | 3 | width: 100% |
4 | - height: 100% | |
4 | + /* height: 100% */ | |
5 | 5 | min-height: 0px |
6 | 6 | |
7 | 7 | section.top { |
8 | 8 | /* position: sticky */ |
app/page/notifications.js | ||
---|---|---|
@@ -1,146 +1,48 @@ | ||
1 | 1 | const nest = require('depnest') |
2 | -const { h, onceTrue, throttle, Value, Array: MutantArray, map } = require('mutant') | |
3 | -const pull = require('pull-stream') | |
2 | +const { h, onceTrue } = require('mutant') | |
3 | +const defer = require('pull-defer') | |
4 | 4 | |
5 | 5 | exports.gives = nest('app.page.notifications') |
6 | 6 | |
7 | 7 | exports.needs = nest({ |
8 | - // 'app.html.blogCard': 'first', | |
9 | - // 'app.html.topNav': 'first', | |
10 | - // 'app.html.scroller': 'first', | |
8 | + 'app.html.scroller': 'first', | |
11 | 9 | 'app.html.sideNav': 'first', |
12 | 10 | 'message.html.comment': 'first', |
13 | - // 'blog.sync.isBlog': 'first', | |
14 | - // 'feed.pull.public': 'first', | |
15 | - // 'feed.pull.type': 'first', | |
16 | - // 'history.sync.push': 'first', | |
17 | - // 'keys.sync.id': 'first', | |
18 | - // 'message.sync.isBlocked': 'first', | |
19 | 11 | 'sbot.obs.connection': 'first', |
20 | - 'translations.sync.strings': 'first', | |
21 | - // 'unread.sync.isUnread': 'first' | |
12 | + 'translations.sync.strings': 'first' | |
22 | 13 | }) |
23 | 14 | |
24 | 15 | exports.create = (api) => { |
25 | - // var blogsCache = MutantArray() | |
26 | - | |
27 | 16 | return nest('app.page.notifications', function (location) { |
28 | - // location here can expected to be: { page: 'notifications'} | |
17 | + // location here can expected to be: { page: 'notifications', section: * } | |
29 | 18 | |
30 | - var strings = api.translations.sync.strings() | |
31 | - | |
32 | - var commentsStore = MutantArray([]) | |
33 | - | |
34 | - onceTrue(api.sbot.obs.connection, server => { | |
35 | - pull( | |
36 | - server.blogStats.readAllComments(), | |
37 | - pull.drain(m => { | |
38 | - commentsStore.push(m) | |
39 | - }) | |
40 | - ) | |
19 | + var scroller = api.app.html.scroller({ | |
20 | + classList: ['content'], | |
21 | + stream: createBlogCommentStream, | |
22 | + render: Comment | |
41 | 23 | }) |
42 | 24 | |
43 | - // server.blogStats.getBlogs({ keys: true, values: false }, (err, data) => { | |
44 | - // if (err) throw err | |
25 | + function createBlogCommentStream (opts) { | |
26 | + const source = defer.source() | |
27 | + var resolved = false | |
45 | 28 | |
46 | - // const blogIds = data.map(d => d[1]) | |
29 | + onceTrue(api.sbot.obs.connection, server => { | |
30 | + if (resolved) return | |
47 | 31 | |
48 | - // var source = server.blogStats.read({ | |
49 | - // // live: true, | |
50 | - // gte: [ 'C', undefined, undefined ], | |
51 | - // lte: [ 'C~', null, null ], | |
52 | - // reverse: true, | |
53 | - // values: true, | |
54 | - // keys: true, | |
55 | - // seqs: false | |
56 | - // }) | |
57 | - // console.log(blogIds) | |
32 | + source.resolve(server.blogStats.readAllComments(opts)) | |
33 | + resolved = true | |
34 | + }) | |
58 | 35 | |
59 | - // pull( | |
60 | - // source, | |
61 | - // pull.filter(result => { | |
62 | - // return blogIds.includes(result.key[1]) | |
63 | - // }), | |
64 | - // pull.map(result => result.value), | |
65 | - // pull.drain(m => { | |
66 | - // commentsStore.push(m) | |
67 | - // }) | |
68 | - // ) | |
69 | - // }) | |
70 | - // }) | |
36 | + return source | |
37 | + } | |
71 | 38 | |
72 | - // var blogs = api.app.html.scroller({ | |
73 | - // classList: ['content'], | |
74 | - // prepend: api.app.html.topNav(location), | |
75 | - // // stream: api.feed.pull.public, | |
76 | - // stream: api.feed.pull.type('blog'), | |
77 | - // filter: () => pull( | |
78 | - // // pull.filter(api.blog.sync.isBlog), | |
79 | - // pull.filter(msg => !msg.value.content.root), // show only root messages | |
80 | - // pull.filter(msg => !api.message.sync.isBlocked(msg)) | |
81 | - // ), | |
82 | - // // FUTURE : if we need better perf, we can add a persistent cache. At the moment this page is fast enough though. | |
83 | - // // See implementation of app.html.sideNav for example | |
84 | - // store: blogsCache, | |
85 | - // updateTop: update, | |
86 | - // updateBottom: update, | |
87 | - // render | |
88 | - // }) | |
89 | - | |
90 | 39 | return h('Page -notifications', [ |
91 | 40 | api.app.html.sideNav(location), |
92 | - h('div.content', map( | |
93 | - throttle(commentsStore, 300), | |
94 | - msg => Comment(msg), | |
95 | - { | |
96 | - // comparer: (a, b) => a === b | |
97 | - } | |
98 | - )) | |
41 | + scroller | |
99 | 42 | ]) |
100 | 43 | |
101 | 44 | function Comment (msg) { |
102 | - return api.message.html.comment(msg) | |
45 | + return api.message.html.comment({ comment: msg, showRootLink: true }) | |
103 | 46 | } |
104 | 47 | }) |
105 | - | |
106 | -/* function update (soFar, newBlog) { */ | |
107 | -// soFar.transaction(() => { | |
108 | -// const { timestamp } = newBlog.value | |
109 | - | |
110 | -// var object = newBlog // Value(newBlog) | |
111 | - | |
112 | -// const index = indexOf(soFar, (blog) => newBlog.key === resolve(blog).key) | |
113 | -// // if blog already in cache, not needed again | |
114 | -// if (index >= 0) return | |
115 | - | |
116 | -// // Orders by: time received | |
117 | -// const justOlderPosition = indexOf(soFar, (msg) => newBlog.timestamp > resolve(msg).timestamp) | |
118 | - | |
119 | -// // Orders by: time published BUT the messagesByType stream streams _by time received_ | |
120 | -// // TODO - we need an index of all blogs otherwise the scroller doesn't work... | |
121 | -// // const justOlderPosition = indexOf(soFar, (msg) => timestamp > resolve(msg).value.timestamp) | |
122 | - | |
123 | -// if (justOlderPosition > -1) { | |
124 | -// soFar.insert(object, justOlderPosition) | |
125 | -// } else { | |
126 | -// soFar.push(object) | |
127 | -// } | |
128 | -// }) | |
129 | -// } | |
130 | - | |
131 | -// function render (blog) { | |
132 | -// const { recps, channel } = blog.value.content | |
133 | -// var onClick | |
134 | -// if (channel && !recps) { onClick = (ev) => api.history.sync.push(Object.assign({}, blog, { page: 'blogShow' })) } | |
135 | -// return api.app.html.blogCard(blog, { onClick }) | |
136 | -// } | |
137 | -// } | |
138 | - | |
139 | -// function indexOf (array, fn) { | |
140 | -// for (var i = 0; i < array.getLength(); i++) { | |
141 | -// if (fn(array.get(i))) { | |
142 | -// return i | |
143 | -// } | |
144 | -// } | |
145 | -// return -1 | |
146 | 48 | } |
app/page/notifications.mcss | ||
---|---|---|
@@ -1,0 +1,11 @@ | ||
1 | +Page -notifications { | |
2 | + div.Scroller.content { | |
3 | + | |
4 | + section.content { | |
5 | + div.Comment { | |
6 | + flex-grow: 1 | |
7 | + | |
8 | + } | |
9 | + } | |
10 | + } | |
11 | +} |
message/html/comment.js | ||
---|---|---|
@@ -1,49 +1,62 @@ | ||
1 | 1 | const nest = require('depnest') |
2 | -const { h, Value, map, when, resolve } = require('mutant') | |
2 | +const { h, Value, map, when, resolve, computed, onceTrue } = require('mutant') | |
3 | 3 | const get = require('lodash/get') |
4 | +const { heads } = require('ssb-sort') | |
4 | 5 | |
5 | 6 | exports.gives = nest('message.html.comment') |
6 | 7 | |
7 | 8 | exports.needs = nest({ |
8 | 9 | 'about.html.avatar': 'first', |
9 | 10 | 'about.obs.name': 'first', |
10 | - // 'backlinks.obs.for': 'first', | |
11 | + 'backlinks.obs.for': 'first', | |
12 | + 'blog.html.title': 'first', | |
11 | 13 | 'message.html.compose': 'first', |
12 | 14 | 'message.html.markdown': 'first', |
13 | 15 | 'message.html.timeago': 'first', |
14 | 16 | 'message.html.likes': 'first', |
15 | 17 | 'unread.sync.markRead': 'first', |
16 | 18 | 'unread.sync.isUnread': 'first', |
19 | + 'sbot.obs.connection': 'first', | |
17 | 20 | 'translations.sync.strings': 'first' |
18 | 21 | }) |
19 | 22 | |
20 | 23 | exports.create = (api) => { |
21 | 24 | return nest('message.html.comment', Comment) |
22 | 25 | |
23 | - function Comment ({ comment: msgObs, replies, branch }) { | |
26 | + function Comment ({ comment, replies, branch, showRootLink = false }) { | |
24 | 27 | const strings = api.translations.sync.strings() |
25 | - const msg = resolve(msgObs) | |
26 | - | |
27 | - const raw = get(msg, 'value.content.text') | |
28 | - var className = api.unread.sync.isUnread(msg) ? ' -unread' : ' -read' | |
29 | - api.unread.sync.markRead(msg) | |
30 | - | |
28 | + const msg = resolve(comment) | |
31 | 29 | var root = get(msg, 'value.content.root') |
32 | 30 | if (!root) return |
33 | 31 | |
34 | 32 | const { author, content } = msg.value |
35 | 33 | |
36 | - // // TODO - move this upstream into patchcore:feed.obs.thread ?? | |
37 | - // // OR change strategy to use forks | |
38 | - // const backlinks = api.backlinks.obs.for(msg.key) | |
39 | - // const nestedReplies = computed(backlinks, backlinks => { | |
40 | - // return backlinks.filter(backlinker => { | |
41 | - // const { type, root } = backlinker.value.content | |
42 | - // return type === 'post' && root === msg.key | |
43 | - // }) | |
44 | - // }) | |
34 | + if (!replies) { | |
35 | + replies = computed(api.backlinks.obs.for(msg.key), backlinks => { | |
36 | + return backlinks.filter(backlinker => { | |
37 | + const { type, root } = backlinker.value.content | |
38 | + return type === 'post' && root === msg.key | |
39 | + }) | |
40 | + }) | |
41 | + } | |
42 | + if (!branch) { | |
43 | + branch = computed(api.backlinks.obs.for(root), backlinks => { | |
44 | + return heads(backlinks) | |
45 | + }) | |
46 | + } | |
45 | 47 | |
48 | + var className = api.unread.sync.isUnread(msg) ? ' -unread' : ' -read' | |
49 | + api.unread.sync.markRead(msg) | |
50 | + | |
51 | + var title = Value() | |
52 | + if (showRootLink) { | |
53 | + processMessage(root, msg => { | |
54 | + var t = api.blog.html.title(msg) | |
55 | + title.set(t.innerText ? t.innerText : t) | |
56 | + }) | |
57 | + } | |
58 | + | |
46 | 59 | var nestedReplyCompose = Value(false) |
47 | 60 | const toggleCompose = () => nestedReplyCompose.set(!nestedReplyCompose()) |
48 | 61 | const nestedReplyComposer = api.message.html.compose({ |
49 | 62 | meta: { |
@@ -63,17 +76,24 @@ | ||
63 | 76 | h('div.left', api.about.html.avatar(author, 'tiny')), |
64 | 77 | h('div.right', [ |
65 | 78 | h('section.context', [ |
66 | 79 | h('div.name', api.about.obs.name(author)), |
67 | - api.message.html.timeago(msg) | |
80 | + api.message.html.timeago(msg), | |
81 | + when(showRootLink, h('a.rootLink', {href: root}, ['<< ', title, ' >>'])) | |
82 | + // TODO don't link to root, link to position of message within blog! | |
68 | 83 | ]), |
69 | - h('section.content', api.message.html.markdown(raw)), | |
84 | + h('section.content', api.message.html.markdown(get(msg, 'value.content.text'))), | |
70 | 85 | when(replies, |
71 | 86 | h('section.replies', |
72 | 87 | map( |
73 | 88 | replies, |
74 | 89 | NestedComment, |
75 | - { comparer: (a, b) => a === b } | |
90 | + { | |
91 | + comparer: (a, b) => { | |
92 | + if (a === undefined || b === undefined) return false | |
93 | + return a.key === b.key | |
94 | + } | |
95 | + } | |
76 | 96 | ) |
77 | 97 | ) |
78 | 98 | ), |
79 | 99 | h('section.actions', [ |
@@ -86,10 +106,10 @@ | ||
86 | 106 | ]) |
87 | 107 | ]) |
88 | 108 | } |
89 | 109 | |
90 | - function NestedComment (msgObs) { | |
91 | - const msg = resolve(msgObs) | |
110 | + function NestedComment (comment) { | |
111 | + const msg = resolve(comment) | |
92 | 112 | const raw = get(msg, 'value.content.text') |
93 | 113 | if (!raw) return |
94 | 114 | |
95 | 115 | const { author } = msg.value |
@@ -104,10 +124,12 @@ | ||
104 | 124 | h('section.content', api.message.html.markdown(raw)) |
105 | 125 | ]) |
106 | 126 | ]) |
107 | 127 | } |
108 | -} | |
109 | 128 | |
110 | -function forkOf (msg) { | |
111 | - return get(msg, 'value.content.fork') | |
129 | + function processMessage (key, fn) { | |
130 | + onceTrue(api.sbot.obs.connection, server => server.get(key, (err, value) => { | |
131 | + if (err) return console.error(err) | |
132 | + fn({ key, value }) | |
133 | + })) | |
134 | + } | |
112 | 135 | } |
113 | - |
message/html/comment.mcss | ||
---|---|---|
@@ -21,9 +21,14 @@ | ||
21 | 21 | div.name { |
22 | 22 | font-size: 1.2rem |
23 | 23 | margin-right: 1rem |
24 | 24 | } |
25 | - div.Timeago {} | |
25 | + div.Timeago { | |
26 | + margin-right: 1.5rem | |
27 | + } | |
28 | + a.rootLink { | |
29 | + font-size: .8rem | |
30 | + } | |
26 | 31 | } |
27 | 32 | |
28 | 33 | section.content { |
29 | 34 | font-size: .95rem |
ssb-server-blog-stats.js | ||
---|---|---|
@@ -125,32 +125,52 @@ | ||
125 | 125 | |
126 | 126 | return view.read(query) |
127 | 127 | } |
128 | 128 | |
129 | - function readAllComments () { | |
129 | + function readAllComments (opts = {}) { | |
130 | 130 | var source = defer.source() |
131 | 131 | |
132 | 132 | getBlogs({ keys: true, values: false }, (err, data) => { |
133 | 133 | if (err) throw err |
134 | 134 | |
135 | 135 | const blogIds = data.map(d => d[1]) |
136 | 136 | |
137 | + opts.type = 'post' | |
138 | + var limit = opts.limit | |
139 | + delete opts.limit | |
140 | + // have to remove limit from the query otherwise Next stalls out if it doesn't get a new result | |
141 | + | |
137 | 142 | const _source = pull( |
138 | - view.read({ | |
139 | - // live: true, | |
140 | - gt: [ 'C', null, null ], | |
141 | - lt: [ 'C', undefined, undefined ], | |
142 | - reverse: true, | |
143 | - values: true, | |
144 | - keys: true, | |
145 | - seqs: false | |
143 | + server.messagesByType(opts), | |
144 | + pull.filter(msg => { | |
145 | + if (msg.value.author === server.id) return false // exclude my posts | |
146 | + if (msg.value.content.root === undefined) return false // want only 'comments' (reply posts) | |
147 | + | |
148 | + return blogIds.includes(msg.value.content.root) // is about one of my blogs | |
146 | 149 | }), |
147 | - pull.filter(result => { | |
148 | - return blogIds.includes(result.key[1]) | |
149 | - }), | |
150 | - pull.map(result => result.value) | |
150 | + limit ? pull.take(limit) : true | |
151 | 151 | ) |
152 | 152 | |
153 | + // I don't know what order results some out of flumeview-level read | |
154 | + // which makes this perhaps unideal for Next / mutant-scroll | |
155 | + // const query = { | |
156 | + // gt: [ 'C', null, opts.gt || null ], | |
157 | + // lt: [ 'C', undefined, opts.lt || undefined ], | |
158 | + // reverse: opts.reverse === undefined ? true : opts.reverse, | |
159 | + // live: opts.reverse === undefined ? true : opts.reverse, | |
160 | + // values: true, | |
161 | + // keys: true, | |
162 | + // seqs: false | |
163 | + // } | |
164 | + // const _source = pull( | |
165 | + // view.read(query), | |
166 | + // pull.filter(result => { | |
167 | + // return blogIds.includes(result.key[1]) | |
168 | + // }), | |
169 | + // pull.map(result => result.value), | |
170 | + // pull.take(opts.limit) | |
171 | + // ) | |
172 | + | |
153 | 173 | source.resolve(_source) |
154 | 174 | }) |
155 | 175 | |
156 | 176 | return pull( |
Built with git-ssb-web