Files: 5a28f5df1543b0e5813e5fe7dbc74f2ec907ff5a / feed / obs / thread.js
4407 bytesRaw
1 | var nest = require('depnest') |
2 | var sort = require('ssb-sort') |
3 | var ref = require('ssb-ref') |
4 | var isBlog = require('scuttle-blog/isBlog') |
5 | var Blog = require('scuttle-blog') |
6 | |
7 | var { Array: MutantArray, Value, map, computed, concat, ProxyCollection } = require('mutant') |
8 | |
9 | exports.needs = nest({ |
10 | 'backlinks.obs.for': 'first', |
11 | 'sbot.async.get': 'first', |
12 | 'message.sync.unbox': 'first', |
13 | 'message.sync.root': 'first', |
14 | 'message.sync.isBlocked': 'first', |
15 | 'sbot.obs.connection': 'first' |
16 | }) |
17 | |
18 | exports.gives = nest('feed.obs.thread') |
19 | |
20 | exports.create = function (api) { |
21 | return nest('feed.obs.thread', thread) |
22 | |
23 | function thread (rootId, { branch } = {}) { |
24 | if (!ref.isLink(rootId)) throw new Error('an id must be specified') |
25 | var sync = Value(false) |
26 | var { isBlocked, root } = api.message.sync |
27 | var replies = ProxyCollection() |
28 | |
29 | var prepend = MutantArray() |
30 | api.sbot.async.get(rootId, (err, value) => { |
31 | var rootMessage = null |
32 | if (!err) { |
33 | rootMessage = unboxIfNeeded({key: rootId, value}) |
34 | if (isBlocked(rootMessage)) rootMessage.isBlocked = true |
35 | |
36 | if (isBlog(rootMessage)) { |
37 | // resolve the blog body before returning |
38 | Blog(api.sbot.obs.connection).async.get(rootMessage, (err, result) => { |
39 | if (!err) { |
40 | rootMessage.body = result.body |
41 | prepend.push(Value(rootMessage)) |
42 | sync.set(true) |
43 | } |
44 | }) |
45 | } else { |
46 | sync.set(true) |
47 | prepend.push(Value(rootMessage)) |
48 | } |
49 | } else { |
50 | sync.set(true) |
51 | } |
52 | |
53 | // calcaulate after message has been resolved so that we can check if thread author blocks the reply |
54 | // wrap computed in a map to turn into individual observables |
55 | replies.set(map(computed(backlinks, (msgs) => { |
56 | return sort(msgs.filter(msg => { |
57 | const { type, branch } = msg.value.content |
58 | return type !== 'vote' && !isBlocked(msg, rootMessage) && (root(msg) === rootId || matchAny(branch, rootId)) |
59 | })) |
60 | }), x => Value(x), { |
61 | // avoid refresh of entire list when items added |
62 | comparer: (a, b) => a === b |
63 | })) |
64 | }) |
65 | |
66 | var backlinks = api.backlinks.obs.for(rootId) |
67 | |
68 | // append the root message to the sorted replies list |
69 | // ------------------------- |
70 | // concat preserves the individual observable messages so that clients don't need to |
71 | // rerender the entire list when an item is added (map will only be called for new items) |
72 | // (we can't use a computed here as it would squash the individual observables into a single one) |
73 | var messages = concat([prepend, replies]) |
74 | |
75 | var result = { |
76 | messages, |
77 | lastId: computed(messages, (messages) => { |
78 | var branches = sort.heads(messages) |
79 | if (branches.length <= 1) { |
80 | branches = branches[0] |
81 | } |
82 | return branches |
83 | }), |
84 | rootId: computed(messages, (messages) => { |
85 | if (branch && messages.length) { |
86 | return messages[0].value.content.root |
87 | } else { |
88 | return rootId |
89 | } |
90 | }), |
91 | branchId: computed(messages, (messages) => { |
92 | if (branch) return rootId |
93 | }), |
94 | previousKey: function (msg) { |
95 | return PreviousKey(result.messages, msg) |
96 | }, |
97 | isPrivate: computed(messages, msgs => { |
98 | if (!msgs[0]) return false |
99 | |
100 | return msgs[0].value.private || false |
101 | }), |
102 | channel: computed(messages, msgs => { |
103 | if (!msgs[0]) return undefined |
104 | |
105 | return msgs[0].value.content.channel |
106 | }), |
107 | recps: computed(messages, msgs => { |
108 | if (!msgs[0]) return undefined |
109 | |
110 | return msgs[0].value.content.recps |
111 | }) |
112 | } |
113 | |
114 | result.sync = computed([backlinks.sync, sync], (a, b) => a && b, {idle: true}) |
115 | return result |
116 | } |
117 | |
118 | function unboxIfNeeded (msg) { |
119 | if (msg.value && typeof msg.value.content === 'string') { |
120 | return api.message.sync.unbox(msg) || msg |
121 | } else { |
122 | return msg |
123 | } |
124 | } |
125 | } |
126 | |
127 | function PreviousKey (collection, item) { |
128 | return computed(collection, (c) => { |
129 | var index = collection.indexOf(item) |
130 | if (~index) { |
131 | var previous = c[index - 1] |
132 | if (previous) { |
133 | return previous.key |
134 | } |
135 | } |
136 | }) |
137 | } |
138 | |
139 | function matchAny (valueOrArray, compare) { |
140 | if (valueOrArray === compare) { |
141 | return true |
142 | } else if (Array.isArray(valueOrArray)) { |
143 | return valueOrArray.includes(compare) |
144 | } |
145 | } |
146 |
Built with git-ssb-web