Files: fca7e884b005b72ad936e12d9186829ea5e7d0df / modules / feed / html / meta-summary.js
7396 bytesRaw
1 | var nest = require('depnest') |
2 | var ref = require('ssb-ref') |
3 | var { when, h, Value, computed } = require('mutant') |
4 | |
5 | exports.needs = nest({ |
6 | 'intl.sync.i18n': 'first', |
7 | 'intl.sync.i18n_n': 'first', |
8 | 'about.html.image': 'first', |
9 | 'about.obs.name': 'first' |
10 | }) |
11 | |
12 | exports.gives = nest({ |
13 | 'feed.html.metaSummary': true |
14 | }) |
15 | |
16 | var i18nActions = { |
17 | from: { |
18 | followed: 'followed %s people', |
19 | unfollowed: 'unfollowed %s people', |
20 | subscribed: 'subscribed to %s channels', |
21 | unsubscribed: 'unsubscribed from %s channels', |
22 | identified: 'identified %s people' |
23 | }, |
24 | to: { |
25 | followed: '%s people followed', |
26 | unfollowed: '%s people unfollowed', |
27 | subscribed: '%s people subscribed to', |
28 | unsubscribed: '%s unsubscribed from', |
29 | identified: '%s people identified' |
30 | }, |
31 | one: { |
32 | followed: 'followed', |
33 | unfollowed: 'unfollowed', |
34 | subscribed: 'subscribed to', |
35 | unsubscribed: 'unsubscribed from', |
36 | identified: 'identified' |
37 | } |
38 | } |
39 | |
40 | exports.create = function (api) { |
41 | const i18n = api.intl.sync.i18n |
42 | const plural = api.intl.sync.i18n_n |
43 | |
44 | return nest('feed.html', {metaSummary}) |
45 | |
46 | function metaSummary (group, renderItem, opts) { |
47 | var expanded = Value(false) |
48 | var actions = getActions(group.msgs) |
49 | var counts = getActionCounts(actions) |
50 | var reduced = reduceActions(counts) |
51 | |
52 | var contentSummary = h('FeedMetaSummary', [ |
53 | reduced.map(item => { |
54 | return h('div -' + item.action, [ |
55 | h('div -left', item.from.slice(0, 10).map(avatarFormatter)), |
56 | h('span.action', {title: actionDescription(item)}), |
57 | h('div -right', item.to.slice(0, 10).map(avatarFormatter)) |
58 | ]) |
59 | }) |
60 | ]) |
61 | |
62 | return h('FeedEvent -group', { |
63 | classList: [ when(expanded, '-expanded') ] |
64 | }, [ |
65 | contentSummary, |
66 | when(expanded, h('div.items', group.msgs.map(msg => renderItem(msg, opts)))), |
67 | h('a.expand', { |
68 | 'tab-index': 0, |
69 | 'ev-click': {handleEvent: toggleValue, value: expanded} |
70 | }, [ |
71 | when(expanded, |
72 | [i18n('Hide details')], |
73 | [i18n('Show details') + ' (', group.msgs.length, ')'] |
74 | ) |
75 | ]) |
76 | ]) |
77 | } |
78 | |
79 | function avatarFormatter (id) { |
80 | if (id.startsWith('#')) { |
81 | return h('a -channel', {href: id}, `#${ref.normalizeChannel(id)}`) |
82 | } else { |
83 | return h('a', {href: id}, api.about.html.image(id)) |
84 | } |
85 | } |
86 | |
87 | function actionDescription (item) { |
88 | if (item.from.length === item.to.length) { |
89 | if (item.action === 'identified' && item.from[0] === item.to[0]) { |
90 | return computed([ |
91 | getName(item.from[0]) |
92 | ], (name) => { |
93 | return name + ' ' + i18n('updated their profile') |
94 | }) |
95 | } else { |
96 | return computed([ |
97 | getName(item.from[0]), |
98 | getName(item.to[0]) |
99 | ], (a, b) => { |
100 | return a + ' ' + i18n(i18nActions.one[item.action]) + ' ' + b |
101 | }) |
102 | } |
103 | } else if (item.from.length < item.to.length) { |
104 | let name = getName(item.from[0]) |
105 | return computed([name, item.to.length], (name, count) => name + ' ' + plural(i18nActions.from[item.action], count)) |
106 | } else { |
107 | let name = getName(item.to[0]) |
108 | return computed([name, item.from.length], (name, count) => plural(i18nActions.to[item.action], count) + ' ' + name) |
109 | } |
110 | } |
111 | |
112 | function getName (id) { |
113 | return id.startsWith('#') ? id : api.about.obs.name(id) |
114 | } |
115 | } |
116 | |
117 | function getActions (msgs) { |
118 | var actions = {} |
119 | |
120 | // reduce down actions for each contact change (de-duplicate, remove redundency) |
121 | // e.g. if a person follows someone then unfollows, ignore both actions |
122 | msgs.forEach(msg => { |
123 | var content = msg.value.content |
124 | let from = msg.value.author |
125 | if (content.type === 'contact') { |
126 | if (ref.isFeed(content.contact)) { |
127 | let to = content.contact |
128 | let key = `${from}:${to}` |
129 | if (content.following === true) { |
130 | if (actions[key] === 'unfollowed') { |
131 | delete actions[key] |
132 | } else { |
133 | actions[key] = 'followed' |
134 | } |
135 | } else if (content.blocking === true) { |
136 | if (actions[key] === 'unblocked') { |
137 | delete actions[key] |
138 | } else { |
139 | actions[key] = 'blocked' |
140 | } |
141 | } else if (content.blocking === false) { |
142 | if (actions[key] === 'blocked') { |
143 | delete actions[key] |
144 | } else { |
145 | actions[key] = 'unblocked' |
146 | } |
147 | } else if (content.following === false) { |
148 | if (actions[key] === 'followed') { |
149 | delete actions[key] |
150 | } else { |
151 | actions[key] = 'unfollowed' |
152 | } |
153 | } |
154 | } |
155 | } else if (content.type === 'channel') { |
156 | if (typeof content.channel === 'string') { // TODO: better channel check |
157 | let to = '#' + content.channel |
158 | let key = `${from}:${to}` |
159 | if (content.subscribed === true) { |
160 | if (actions[key] === 'unsubscribed') { |
161 | delete actions[key] |
162 | } else { |
163 | actions[key] = 'subscribed' |
164 | } |
165 | } else if (content.subscribed === false) { |
166 | if (actions[key] === 'subscribed') { |
167 | delete actions[key] |
168 | } else { |
169 | actions[key] = 'unsubscribed' |
170 | } |
171 | } |
172 | } |
173 | } else if (content.type === 'about') { |
174 | if (ref.isFeed(content.about)) { |
175 | let to = content.about |
176 | let key = `${from}:${to}` |
177 | actions[key] = 'identified' |
178 | } |
179 | } |
180 | }) |
181 | |
182 | return actions |
183 | } |
184 | |
185 | function getActionCounts (actions) { |
186 | var actionCounts = {} |
187 | |
188 | // get actions performed on and by profiles |
189 | // collect who did what and has what done on them |
190 | for (var key in actions) { |
191 | var action = actions[key] |
192 | var {from, to} = splitKey(key) |
193 | actionCounts[from] = actionCounts[from] || {from: {}, to: {}} |
194 | actionCounts[to] = actionCounts[to] || {from: {}, to: {}} |
195 | |
196 | actionCounts[from].from[action] = actionCounts[from].from[action] || [] |
197 | actionCounts[to].to[action] = actionCounts[to].to[action] || [] |
198 | |
199 | actionCounts[from].from[action].push(to) |
200 | actionCounts[to].to[action].push(from) |
201 | } |
202 | |
203 | return actionCounts |
204 | } |
205 | |
206 | function reduceActions (actionCounts) { |
207 | var actions = [] |
208 | |
209 | for (let key in actionCounts) { |
210 | var value = actionCounts[key] |
211 | for (let action in value.from) { |
212 | actions.push({from: [key], action, to: value.from[action], rank: value.from[action].length}) |
213 | } |
214 | for (let action in value.to) { |
215 | actions.push({from: value.to[action], action, to: [key], rank: value.to[action].length}) |
216 | } |
217 | } |
218 | |
219 | // sort desc by most targets per action |
220 | actions.sort((a, b) => Math.max(b.rank - a.rank)) |
221 | |
222 | // remove duplicate actions, and return! |
223 | var used = new Set() |
224 | return actions.filter(action => { |
225 | // only add a particular action once! |
226 | if (action.from.length > action.to.length) { |
227 | action.from = action.from.filter(from => action.to.some(to => !used.has(`${from}:${to}`))) |
228 | } else { |
229 | action.to = action.to.filter(to => action.from.some(from => !used.has(`${from}:${to}`))) |
230 | } |
231 | |
232 | action.from.forEach(from => { |
233 | action.to.forEach(to => { |
234 | used.add(`${from}:${to}`) |
235 | }) |
236 | }) |
237 | |
238 | // // only return if has targets |
239 | return action.from.length && action.to.length |
240 | }) |
241 | } |
242 | |
243 | function toggleValue () { |
244 | this.value.set(!this.value()) |
245 | } |
246 | |
247 | function splitKey (key) { |
248 | var mid = key.indexOf(':') |
249 | return { |
250 | from: key.slice(0, mid), |
251 | to: key.slice(mid + 1) |
252 | } |
253 | } |
254 |
Built with git-ssb-web