Files: d69600e6a6f16e5bda4f2ab3f10499558811ad23 / contact / obs.js
3597 bytesRaw
1 | |
2 | |
3 | var nest = require('depnest') |
4 | var { Value, computed } = require('mutant') |
5 | var pull = require('pull-stream') |
6 | var ref = require('ssb-ref') |
7 | |
8 | exports.needs = nest({ |
9 | 'sbot.pull.stream': 'first' |
10 | }) |
11 | |
12 | exports.gives = nest({ |
13 | 'contact.obs': ['following', 'followers', 'blocking', 'blockers'], |
14 | 'sbot.hook.publish': true |
15 | }) |
16 | |
17 | exports.create = function (api) { |
18 | var cacheLoading = false |
19 | var cache = {} |
20 | var reverseCache = {} |
21 | |
22 | var sync = Value(false) |
23 | |
24 | return nest({ |
25 | 'contact.obs': { |
26 | |
27 | // states: |
28 | // true = following, |
29 | // null = neutral (may have unfollowed), |
30 | // false = blocking |
31 | |
32 | following: (key) => matchingValueKeys(get(key, cache), true), |
33 | followers: (key) => matchingValueKeys(get(key, reverseCache), true), |
34 | blocking: (key) => matchingValueKeys(get(key, cache), false), |
35 | blockers: (key) => matchingValueKeys(get(key, reverseCache), false) |
36 | }, |
37 | 'sbot.hook.publish': function (msg) { |
38 | if (!isContact(msg)) return |
39 | |
40 | // HACK: make interface more responsive when sbot is busy |
41 | var source = msg.value.author |
42 | var dest = msg.value.content.contact |
43 | var tristate = ( // from ssb-friends |
44 | msg.value.content.following ? true |
45 | : msg.value.content.flagged || msg.value.content.blocking ? false |
46 | : null |
47 | ) |
48 | |
49 | update(source, { [dest]: tristate }, cache) |
50 | update(dest, { [source]: tristate }, reverseCache) |
51 | } |
52 | }) |
53 | |
54 | function matchingValueKeys (state, value) { |
55 | var obs = computed(state, state => { |
56 | return Object.keys(state).filter(key => { |
57 | return state[key] === value |
58 | }) |
59 | }) |
60 | |
61 | obs.sync = sync |
62 | return obs |
63 | } |
64 | |
65 | function loadCache () { |
66 | pull( |
67 | api.sbot.pull.stream(sbot => sbot.friends.stream({live: true})), |
68 | pull.drain(item => { |
69 | if (!sync()) { |
70 | // populate observable cache |
71 | var reverse = {} |
72 | for (var source in item) { |
73 | if (ref.isFeed(source)) { |
74 | update(source, item[source], cache) |
75 | |
76 | // generate reverse lookup |
77 | for (let dest in item[source]) { |
78 | reverse[dest] = reverse[dest] || {} |
79 | reverse[dest][source] = item[source][dest] |
80 | } |
81 | } |
82 | } |
83 | |
84 | // populate reverse observable cache |
85 | for (let dest in reverse) { |
86 | update(dest, reverse[dest], reverseCache) |
87 | } |
88 | |
89 | sync.set(true) |
90 | } else if (item && ref.isFeed(item.from) && ref.isFeed(item.to)) { |
91 | // handle realtime updates |
92 | update(item.from, {[item.to]: item.value}, cache) |
93 | update(item.to, {[item.from]: item.value}, reverseCache) |
94 | } |
95 | }) |
96 | ) |
97 | } |
98 | |
99 | function update (sourceId, values, lookup) { |
100 | // ssb-friends: values = { |
101 | // keyA: true|null|false (friend, neutral, block) |
102 | // keyB: true|null|false (friend, neutral, block) |
103 | // } |
104 | var state = get(sourceId, lookup) |
105 | var lastState = state() |
106 | var changed = false |
107 | |
108 | for (var targetId in values) { |
109 | if (values[targetId] !== lastState[targetId]) { |
110 | lastState[targetId] = values[targetId] |
111 | changed = true |
112 | } |
113 | } |
114 | |
115 | if (changed) { |
116 | state.set(lastState) |
117 | } |
118 | } |
119 | |
120 | function get (id, lookup) { |
121 | if (!ref.isFeed(id)) throw new Error('Contact state requires an id!') |
122 | if (!cacheLoading) { |
123 | cacheLoading = true |
124 | loadCache() |
125 | } |
126 | if (!lookup[id]) { |
127 | lookup[id] = Value({}) |
128 | } |
129 | return lookup[id] |
130 | } |
131 | } |
132 | |
133 | function isContact (msg) { |
134 | return msg.value && msg.value.content && msg.value.content.type === 'contact' |
135 | } |
136 |
Built with git-ssb-web