git ssb

10+

Matt McKegg / patchwork



Tree: fca7e884b005b72ad936e12d9186829ea5e7d0df

Files: fca7e884b005b72ad936e12d9186829ea5e7d0df / modules / feed / html / meta-summary.js

7396 bytesRaw
1var nest = require('depnest')
2var ref = require('ssb-ref')
3var { when, h, Value, computed } = require('mutant')
4
5exports.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
12exports.gives = nest({
13 'feed.html.metaSummary': true
14})
15
16var 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
40exports.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
117function 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
185function 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
206function 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
243function toggleValue () {
244 this.value.set(!this.value())
245}
246
247function 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