Commit e421e6f554c1320df904394b27f186dbdac65de5
Merge pull request #138 from ticktackim/notification-hanger
Notifications / StatsAndre Alves Garzia authored on 5/8/2018, 7:00:52 PM
GitHub committed on 5/8/2018, 7:00:52 PM
Parent: 7132de8591e3a3e7dde989835cf9c5fb52382791
Parent: 996a5056487e4eca14e036e0f7175b60e587e77c
Files changed
app/html/comments.js | changed |
app/html/comments.mcss | changed |
app/html/header.js | changed |
app/html/header.mcss | changed |
app/html/scroller.js | changed |
app/html/scroller.mcss | changed |
app/html/sideNav/sideNavNotifications.js | added |
app/html/sideNav/sideNavNotifications.mcss | added |
app/index.js | changed |
app/page/addressBook.js | changed |
app/page/statsShow.js | changed |
app/page/notifications.js | added |
app/page/notifications.mcss | added |
background-process.js | changed |
message/html/comment.js | added |
message/html/comment.mcss | added |
message/html/notification.js | added |
message/html/notification.mcss | added |
message/index.js | changed |
package.json | changed |
router/sync/routes.js | changed |
translations/en.js | changed |
ssb-server-blog-stats.js | deleted |
ssb-server-ticktack.js | added |
app/html/comments.js | ||
---|---|---|
@@ -1,21 +1,13 @@ | ||
1 | 1 | const nest = require('depnest') |
2 | -const { h, Value, map, computed, when, resolve, throttle } = require('mutant') | |
2 | +const { h, map, Struct, computed, throttle } = require('mutant') | |
3 | 3 | const get = require('lodash/get') |
4 | 4 | |
5 | 5 | exports.gives = nest('app.html.comments') |
6 | 6 | |
7 | 7 | exports.needs = nest({ |
8 | - 'about.html.avatar': 'first', | |
9 | - 'about.obs.name': 'first', | |
10 | - 'backlinks.obs.for': 'first', | |
11 | - 'feed.obs.thread': 'first', | |
8 | + 'message.html.comment': 'first', | |
12 | 9 | 'message.html.compose': 'first', |
13 | - 'message.html.markdown': 'first', | |
14 | - 'message.html.timeago': 'first', | |
15 | - 'message.html.likes': 'first', | |
16 | - 'unread.sync.markRead': 'first', | |
17 | - 'unread.sync.isUnread': 'first', | |
18 | 10 | 'translations.sync.strings': 'first' |
19 | 11 | }) |
20 | 12 | |
21 | 13 | exports.create = (api) => { |
@@ -27,13 +19,15 @@ | ||
27 | 19 | |
28 | 20 | // TODO - move this up into Patchcore |
29 | 21 | const messagesTree = computed(throttle(messages, 200), msgs => { |
30 | 22 | return msgs |
31 | - .filter(msg => forkOf(msg) === undefined) | |
32 | - .map(threadMsg => { | |
33 | - const nestedReplies = msgs.filter(msg => forkOf(msg) === threadMsg.key) | |
34 | - threadMsg.replies = nestedReplies | |
35 | - return threadMsg | |
23 | + .filter(msg => forkOf(msg) === undefined) // exclude nested replies / forks | |
24 | + .filter(msg => msg.value.content.root) // exclude root message / blog | |
25 | + .map(comment => { | |
26 | + return Struct({ | |
27 | + comment, | |
28 | + replies: msgs.filter(msg => forkOf(msg) === comment.key) | |
29 | + }) | |
36 | 30 | }) |
37 | 31 | }) |
38 | 32 | |
39 | 33 | const root = computed(messages, ary => ary[0].key) |
@@ -54,95 +48,22 @@ | ||
54 | 48 | }) |
55 | 49 | |
56 | 50 | return h('Comments', [ |
57 | 51 | // when(twoComposers, compose({ meta, shrink: true, canAttach: false })), |
58 | - map(messagesTree, msg => Comment(msg, root, branch)), | |
52 | + map( | |
53 | + messagesTree, | |
54 | + msg => api.message.html.comment({ comment: msg.comment, replies: msg.replies, branch }), | |
55 | + { | |
56 | + comparer: (a, b) => { | |
57 | + if (a === undefined || b === undefined) return false | |
58 | + | |
59 | + return a.comment().key === b.comment().key && a.replies().length === b.replies().length | |
60 | + } | |
61 | + } | |
62 | + ), | |
59 | 63 | compose({ meta, feedIdsInThread, shrink: false, canAttach: true, placeholder: strings.writeComment }) |
60 | 64 | ]) |
61 | 65 | } |
62 | - | |
63 | - function Comment (msgObs, root, branch) { | |
64 | - const strings = api.translations.sync.strings() | |
65 | - const msg = resolve(msgObs) | |
66 | - | |
67 | - const raw = get(msg, 'value.content.text') | |
68 | - var className = api.unread.sync.isUnread(msg) ? ' -unread' : ' -read' | |
69 | - api.unread.sync.markRead(msg) | |
70 | - | |
71 | - if (!get(msg, 'value.content.root')) return | |
72 | - | |
73 | - const { author, content } = msg.value | |
74 | - | |
75 | - // // TODO - move this upstream into patchcore:feed.obs.thread ?? | |
76 | - // // OR change strategy to use forks | |
77 | - // const backlinks = api.backlinks.obs.for(msg.key) | |
78 | - // const nestedReplies = computed(backlinks, backlinks => { | |
79 | - // return backlinks.filter(backlinker => { | |
80 | - // const { type, root } = backlinker.value.content | |
81 | - // return type === 'post' && root === msg.key | |
82 | - // }) | |
83 | - // }) | |
84 | - | |
85 | - var nestedReplyCompose = Value(false) | |
86 | - const toggleCompose = () => nestedReplyCompose.set(!nestedReplyCompose()) | |
87 | - const nestedReplyComposer = api.message.html.compose({ | |
88 | - meta: { | |
89 | - type: 'post', | |
90 | - root, | |
91 | - fork: msg.key, | |
92 | - branch, | |
93 | - channel: content.channel | |
94 | - }, | |
95 | - shrink: false, | |
96 | - canAttach: true, | |
97 | - canPreview: false, | |
98 | - placeholder: strings.writeComment | |
99 | - }, toggleCompose) | |
100 | - | |
101 | - return h('Comment', { className }, [ | |
102 | - h('div.left', api.about.html.avatar(author, 'tiny')), | |
103 | - h('div.right', [ | |
104 | - h('section.context', [ | |
105 | - h('div.name', api.about.obs.name(author)), | |
106 | - api.message.html.timeago(msg) | |
107 | - ]), | |
108 | - h('section.content', api.message.html.markdown(raw)), | |
109 | - when(msgObs.replies, | |
110 | - h('section.replies', | |
111 | - map(msgObs.replies, NestedComment) | |
112 | - ) | |
113 | - ), | |
114 | - h('section.actions', [ | |
115 | - h('div.reply', { 'ev-click': toggleCompose }, [ | |
116 | - h('i.fa.fa-commenting-o') | |
117 | - ]), | |
118 | - api.message.html.likes(msg) | |
119 | - ]), | |
120 | - when(nestedReplyCompose, nestedReplyComposer) | |
121 | - ]) | |
122 | - ]) | |
123 | - } | |
124 | - | |
125 | - function NestedComment (msgObs) { | |
126 | - const msg = resolve(msgObs) | |
127 | - const raw = get(msg, 'value.content.text') | |
128 | - if (!raw) return | |
129 | - | |
130 | - const { author } = msg.value | |
131 | - | |
132 | - return h('Comment -nested', [ | |
133 | - h('div.left'), | |
134 | - h('div.right', [ | |
135 | - h('section.context', [ | |
136 | - h('div.name', api.about.obs.name(author)), | |
137 | - api.message.html.timeago(msg) | |
138 | - ]), | |
139 | - h('section.content', api.message.html.markdown(raw)) | |
140 | - ]) | |
141 | - ]) | |
142 | - | |
143 | - api.message.html.markdown(raw) | |
144 | - } | |
145 | 66 | } |
146 | 67 | |
147 | 68 | function forkOf (msg) { |
148 | 69 | return get(msg, 'value.content.fork') |
app/html/comments.mcss | ||
---|---|---|
@@ -7,79 +7,4 @@ | ||
7 | 7 | margin-bottom: 1.5rem |
8 | 8 | } |
9 | 9 | } |
10 | 10 | |
11 | -Comment { | |
12 | - display: flex | |
13 | - | |
14 | - div.left { | |
15 | - margin-right: 1rem | |
16 | - | |
17 | - div.Avatar {} | |
18 | - } | |
19 | - | |
20 | - div.right { | |
21 | - flex-grow: 1 | |
22 | - | |
23 | - $borderBottomLight | |
24 | - padding-bottom: 1rem | |
25 | - margin-bottom: 1rem | |
26 | - | |
27 | - section.context { | |
28 | - display: flex | |
29 | - align-items: baseline | |
30 | - | |
31 | - div.name { | |
32 | - font-size: 1.2rem | |
33 | - margin-right: 1rem | |
34 | - } | |
35 | - div.Timeago {} | |
36 | - } | |
37 | - | |
38 | - section.content { | |
39 | - font-size: .95rem | |
40 | - line-height: 1.4 | |
41 | - div.Markdown {} | |
42 | - } | |
43 | - | |
44 | - section.actions { | |
45 | - font-size: 1rem | |
46 | - color: #555 | |
47 | - margin-right: .5rem | |
48 | - | |
49 | - display: flex | |
50 | - justify-content: flex-end | |
51 | - align-items: baseline | |
52 | - | |
53 | - div.reply { | |
54 | - cursor: pointer | |
55 | - | |
56 | - margin-right: 1.5rem | |
57 | - i.fa {} | |
58 | - } | |
59 | - | |
60 | - div.Likes { } | |
61 | - } | |
62 | - | |
63 | - div.Compose { | |
64 | - margin: 1rem 0 | |
65 | - } | |
66 | - } | |
67 | -} | |
68 | - | |
69 | -Comment -nested { | |
70 | - padding: 1rem 1rem 0 1rem | |
71 | - $backgroundPrimaryText | |
72 | - $roundTop | |
73 | - $roundBottom | |
74 | - | |
75 | - margin-bottom: 1rem | |
76 | - | |
77 | - div.left { margin: 0 } | |
78 | - div.right { | |
79 | - padding: 0 | |
80 | - border: 0 | |
81 | - margin: 0 | |
82 | - | |
83 | - } | |
84 | -} | |
85 | - |
app/html/header.js | ||
---|---|---|
@@ -22,9 +22,10 @@ | ||
22 | 22 | if (loc().page === 'splash') return |
23 | 23 | |
24 | 24 | const isSettings = computed(loc, loc => SETTINGS_PAGES.includes(loc.page)) |
25 | 25 | const isAddressBook = computed(loc, loc => loc.page === 'addressBook') |
26 | - const isFeed = computed([isAddressBook, isSettings], (p, s) => !p && !s) | |
26 | + const isNotifications = computed(loc, loc => loc.page === 'notifications' || loc.page === 'statsShow') | |
27 | + const isFeed = computed([isAddressBook, isSettings, isNotifications], (p, s, n) => !p && !s && !n) | |
27 | 28 | |
28 | 29 | return h('Header', [ |
29 | 30 | windowControls(), |
30 | 31 | h('nav', [ |
@@ -39,9 +40,10 @@ | ||
39 | 40 | h('img.settings', { |
40 | 41 | src: when(isSettings, assetPath('settings_on.png'), assetPath('settings.png')), |
41 | 42 | 'ev-click': () => push({page: 'settings'}) |
42 | 43 | }), |
43 | - h('i.fa.fa-bell', { | |
44 | + h('i.fa', { | |
45 | + className: when(isNotifications, 'fa-bell', 'fa-bell-o'), | |
44 | 46 | 'ev-click': () => push({page: 'statsShow'}) |
45 | 47 | }) |
46 | 48 | ]) |
47 | 49 | ]) |
@@ -77,5 +79,4 @@ | ||
77 | 79 | |
78 | 80 | function assetPath (name) { |
79 | 81 | return path.join(__dirname, '../../assets', name) |
80 | 82 | } |
81 | - |
app/html/header.mcss | ||
---|---|---|
@@ -35,8 +35,11 @@ | ||
35 | 35 | |
36 | 36 | margin: 0 2rem |
37 | 37 | } |
38 | 38 | |
39 | + i { font-size: 1.4rem } | |
40 | + i.fa-bell-o { color: #7da9ea } | |
41 | + | |
39 | 42 | (a) { |
40 | 43 | color: #222 |
41 | 44 | margin-right: 1rem |
42 | 45 |
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/html/sideNav/sideNavNotifications.js | ||
---|---|---|
@@ -1,0 +1,69 @@ | ||
1 | +const nest = require('depnest') | |
2 | +const { h, computed } = require('mutant') | |
3 | + | |
4 | +exports.gives = nest({ | |
5 | + 'app.html.sideNav': true | |
6 | +}) | |
7 | + | |
8 | +exports.needs = nest({ | |
9 | + 'history.sync.push': 'first', | |
10 | + 'translations.sync.strings': 'first' | |
11 | +}) | |
12 | + | |
13 | +const SECTIONS = ['comments', 'likes', 'shares'] | |
14 | + | |
15 | +const ICONS = { | |
16 | + stats: 'bar-chart', | |
17 | + comments: 'commenting-o', | |
18 | + likes: 'heart-o', | |
19 | + shares: 'share-alt' | |
20 | +} | |
21 | + | |
22 | +exports.create = (api) => { | |
23 | + return nest({ | |
24 | + 'app.html.sideNav': sideNav | |
25 | + }) | |
26 | + | |
27 | + function sideNav (location) { | |
28 | + if (location.page !== 'statsShow' && location.page !== 'notifications') return | |
29 | + if (location.page === 'notifications' && !SECTIONS.includes(location.section)) return | |
30 | + | |
31 | + const strings = api.translations.sync.strings() | |
32 | + const goTo = (loc) => () => api.history.sync.push(loc) | |
33 | + | |
34 | + return h('SideNav -notifications', [ | |
35 | + LevelOneSideNav() | |
36 | + ]) | |
37 | + | |
38 | + function LevelOneSideNav () { | |
39 | + return h('div.level.-one', [ | |
40 | + h('section', [ | |
41 | + h('Option', | |
42 | + { | |
43 | + className: location.page === 'statsShow' ? '-selected' : '', | |
44 | + 'ev-click': goTo({page: 'statsShow'}) | |
45 | + }, | |
46 | + [ | |
47 | + h('i.fa', { className: `fa-${ICONS['stats']}` }), | |
48 | + strings['stats'] | |
49 | + ] | |
50 | + ), | |
51 | + SECTIONS.map(section => SectionOption(section)) | |
52 | + ]) | |
53 | + ]) | |
54 | + } | |
55 | + | |
56 | + function SectionOption (section) { | |
57 | + return h('Option', | |
58 | + { | |
59 | + className: location.section === section ? '-selected' : '', | |
60 | + 'ev-click': goTo({page: 'notifications', section}) | |
61 | + }, | |
62 | + [ | |
63 | + h('i.fa', { className: `fa-${ICONS[section]}` }), | |
64 | + strings[section] | |
65 | + ] | |
66 | + ) | |
67 | + } | |
68 | + } | |
69 | +} |
app/html/sideNav/sideNavNotifications.mcss | ||
---|---|---|
@@ -1,0 +1,35 @@ | ||
1 | +SideNav -notifications { | |
2 | + div.level { | |
3 | + section{ | |
4 | + margin-top: 1rem | |
5 | + | |
6 | + header { | |
7 | + font-size: .8rem | |
8 | + } | |
9 | + | |
10 | + div.Option { | |
11 | + padding: 1rem | |
12 | + padding-left: 1.5rem | |
13 | + | |
14 | + display: flex | |
15 | + | |
16 | + div.Button { | |
17 | + flex-grow: 1 | |
18 | + margin: .5rem 0 | |
19 | + } | |
20 | + | |
21 | + i.fa { | |
22 | + margin-right: .5rem | |
23 | + } | |
24 | + | |
25 | + div.count { | |
26 | + flex-grow: 1 | |
27 | + | |
28 | + display: flex | |
29 | + justify-content: flex-end | |
30 | + } | |
31 | + } | |
32 | + } | |
33 | + } | |
34 | +} | |
35 | + |
app/index.js | ||
---|---|---|
@@ -18,9 +18,10 @@ | ||
18 | 18 | }, |
19 | 19 | scroller: require('./html/scroller'), |
20 | 20 | sideNav: { |
21 | 21 | addressBook: require('./html/sideNav/sideNavAddressBook'), |
22 | - discovery: require('./html/sideNav/sideNavDiscovery') | |
22 | + discovery: require('./html/sideNav/sideNavDiscovery'), | |
23 | + notifications: require('./html/sideNav/sideNavNotifications') | |
23 | 24 | }, |
24 | 25 | warning: require('./html/warning') |
25 | 26 | }, |
26 | 27 | obs: { |
@@ -32,21 +33,22 @@ | ||
32 | 33 | blogNew: require('./page/blogNew'), |
33 | 34 | blogSearch: require('./page/blogSearch'), |
34 | 35 | blogShow: require('./page/blogShow'), |
35 | 36 | channelShow: require('./page/channelShow'), |
36 | - error: require('./page/error'), | |
37 | - settings: require('./page/settings'), | |
38 | 37 | channelSubscriptions: require('./page/channelSubscriptions'), |
39 | 38 | // channel: require('./page/channel'), |
40 | 39 | // image: require('./page/image'), |
41 | 40 | // groupFind: require('./page/groupFind'), |
42 | 41 | // groupIndex: require('./page/groupIndex'), |
43 | 42 | // groupNew: require('./page/groupNew'), |
44 | 43 | // groupShow: require('./page/groupShow'), |
45 | 44 | // threadShow: require('./page/threadShow'), |
45 | + notifications: require('./page/notifications'), | |
46 | + error: require('./page/error'), | |
46 | 47 | userEdit: require('./page/userEdit'), |
47 | 48 | // userFind: require('./page/userFind'), |
48 | 49 | userShow: require('./page/userShow'), |
50 | + settings: require('./page/settings'), | |
49 | 51 | splash: require('./page/splash'), |
50 | 52 | statsShow: require('./page/statsShow'), |
51 | 53 | threadNew: require('./page/threadNew'), |
52 | 54 | threadShow: require('./page/threadShow') |
app/page/addressBook.js | ||
---|---|---|
@@ -1,7 +1,6 @@ | ||
1 | 1 | const nest = require('depnest') |
2 | 2 | const { h, Value, computed, map } = require('mutant') |
3 | -const pull = require('pull-stream') | |
4 | 3 | |
5 | 4 | exports.gives = nest('app.page.addressBook') |
6 | 5 | |
7 | 6 | // declare consts to avoid magic-string errors |
@@ -13,10 +12,8 @@ | ||
13 | 12 | exports.needs = nest({ |
14 | 13 | 'about.html.avatar': 'first', |
15 | 14 | 'about.async.suggest': 'first', |
16 | 15 | 'about.obs.name': 'first', |
17 | - 'app.html.topNav': 'first', | |
18 | - // 'app.html.scroller': 'first', | |
19 | 16 | 'app.html.sideNav': 'first', |
20 | 17 | 'app.html.topNav': 'first', |
21 | 18 | 'contact.html.follow': 'first', |
22 | 19 | 'contact.obs.relationships': 'first', |
@@ -26,9 +23,9 @@ | ||
26 | 23 | }) |
27 | 24 | |
28 | 25 | exports.create = (api) => { |
29 | 26 | return nest('app.page.addressBook', function (location) { |
30 | - // location here can expected to be: { page: 'addressBook'} | |
27 | + // location here can expected to be: { page: 'addressBook', section: '...'} | |
31 | 28 | |
32 | 29 | const strings = api.translations.sync.strings() |
33 | 30 | const myKey = api.keys.sync.id() |
34 | 31 | const relationships = api.contact.obs.relationships(myKey) |
app/page/statsShow.js | ||
---|---|---|
@@ -11,11 +11,12 @@ | ||
11 | 11 | |
12 | 12 | exports.gives = nest('app.page.statsShow') |
13 | 13 | |
14 | 14 | exports.needs = nest({ |
15 | - 'sbot.obs.connection': 'first', | |
15 | + 'app.html.sideNav': 'first', | |
16 | 16 | 'history.sync.push': 'first', |
17 | 17 | 'message.html.markdown': 'first', |
18 | + 'sbot.obs.connection': 'first', | |
18 | 19 | 'translations.sync.strings': 'first' |
19 | 20 | }) |
20 | 21 | |
21 | 22 | const COMMENTS = 'comments' |
@@ -93,8 +94,9 @@ | ||
93 | 94 | |
94 | 95 | const canvas = h('canvas', { height: 200, width: 600, style: { height: '200px', width: '600px' } }) |
95 | 96 | |
96 | 97 | const page = h('Page -statsShow', [ |
98 | + api.app.html.sideNav(location), | |
97 | 99 | h('Scroller.content', [ |
98 | 100 | h('div.content', [ |
99 | 101 | h('h1', t.title), |
100 | 102 | h('section.totals', [COMMENTS, LIKES, SHARES].map(focus => { |
@@ -184,9 +186,9 @@ | ||
184 | 186 | |
185 | 187 | function fetchBlogData ({ server, store }) { |
186 | 188 | const myKey = server.id |
187 | 189 | |
188 | - server.blogStats.getBlogs({}, (err, blogs) => { | |
190 | + server.ticktack.getBlogs({}, (err, blogs) => { | |
189 | 191 | if (err) console.error(err) |
190 | 192 | |
191 | 193 | // TODO - change this once merge in the new notifications-hanger work |
192 | 194 | // i.e. do one query for ALL comments on my blogs as opposed to N queries |
@@ -203,9 +205,9 @@ | ||
203 | 205 | function fetchComments ({ server, store, blog }) { |
204 | 206 | if (!store.comments.has(blog.key)) store.comments.put(blog.key, MutantArray()) |
205 | 207 | |
206 | 208 | pull( |
207 | - server.blogStats.readComments(blog), | |
209 | + server.ticktack.readComments(blog), | |
208 | 210 | pull.drain(msg => { |
209 | 211 | if (msg.value.author === myKey) return |
210 | 212 | store.comments.get(blog.key).push(msg) |
211 | 213 | }) |
@@ -215,9 +217,9 @@ | ||
215 | 217 | function fetchLikes ({ server, store, blog }) { |
216 | 218 | if (!store.likes.has(blog.key)) store.likes.put(blog.key, MutantArray()) |
217 | 219 | |
218 | 220 | pull( |
219 | - server.blogStats.readLikes(blog), | |
221 | + server.ticktack.readLikes(blog), | |
220 | 222 | pull.drain(msg => { |
221 | 223 | if (msg.value.author === myKey) return |
222 | 224 | |
223 | 225 | const isUnlike = get(msg, 'value.content.vote.value', 1) < 1 |
app/page/notifications.js | ||
---|---|---|
@@ -1,0 +1,69 @@ | ||
1 | +const nest = require('depnest') | |
2 | +const { h, onceTrue } = require('mutant') | |
3 | +const defer = require('pull-defer') | |
4 | + | |
5 | +exports.gives = nest('app.page.notifications') | |
6 | + | |
7 | +exports.needs = nest({ | |
8 | + 'app.html.scroller': 'first', | |
9 | + 'app.html.sideNav': 'first', | |
10 | + 'message.html.comment': 'first', | |
11 | + 'message.html.notification': 'first', | |
12 | + 'sbot.obs.connection': 'first', | |
13 | + 'translations.sync.strings': 'first' | |
14 | +}) | |
15 | + | |
16 | +const SOURCES = { | |
17 | + comments: 'readAllComments', | |
18 | + likes: 'readAllLikes', | |
19 | + shares: 'readAllShares' | |
20 | +} | |
21 | + | |
22 | +exports.create = (api) => { | |
23 | + return nest('app.page.notifications', function (location) { | |
24 | + // location here can expected to be: { page: 'notifications', section: * } | |
25 | + if (!Object.keys(SOURCES).includes(location.section)) return | |
26 | + | |
27 | + var scroller = api.app.html.scroller({ | |
28 | + classList: ['content'], | |
29 | + stream: createCreateStream(location.section), | |
30 | + render: createRender(location.section) | |
31 | + }) | |
32 | + | |
33 | + return h('Page -notifications', [ | |
34 | + api.app.html.sideNav(location), | |
35 | + scroller | |
36 | + ]) | |
37 | + | |
38 | + function createCreateStream (section) { | |
39 | + return function (opts) { | |
40 | + const source = defer.source() | |
41 | + var resolved = false | |
42 | + | |
43 | + onceTrue(api.sbot.obs.connection, server => { | |
44 | + if (resolved) return | |
45 | + | |
46 | + source.resolve(server.ticktack[SOURCES[section]](opts)) | |
47 | + resolved = true | |
48 | + }) | |
49 | + | |
50 | + return source | |
51 | + } | |
52 | + } | |
53 | + | |
54 | + function createRender (section) { | |
55 | + return function (msg) { | |
56 | + switch (section) { | |
57 | + case 'comments': | |
58 | + return api.message.html.comment({ comment: msg, showRootLink: true }) | |
59 | + | |
60 | + case 'likes': | |
61 | + return api.message.html.notification(msg) | |
62 | + | |
63 | + case 'shares': | |
64 | + return api.message.html.notification(msg) | |
65 | + } | |
66 | + } | |
67 | + } | |
68 | + }) | |
69 | +} |
app/page/notifications.mcss | ||
---|---|---|
@@ -1,0 +1,25 @@ | ||
1 | +Page -notifications { | |
2 | + div.Scroller.content { | |
3 | + | |
4 | + section.content { | |
5 | + padding-top: 2rem | |
6 | + | |
7 | + div.Comment { | |
8 | + flex-basis: 100% | |
9 | + | |
10 | + div.right { | |
11 | + section.replies { | |
12 | + | |
13 | + div.Comment.-nested { | |
14 | + $backgroundPrimary | |
15 | + } | |
16 | + } | |
17 | + } | |
18 | + } | |
19 | + | |
20 | + div.Notification { | |
21 | + flex-basis: 100% | |
22 | + } | |
23 | + } | |
24 | + } | |
25 | +} |
background-process.js | ||
---|---|---|
@@ -21,9 +21,9 @@ | ||
21 | 21 | .use(require('ssb-about')) |
22 | 22 | // .use(require('ssb-ebt')) |
23 | 23 | .use(require('ssb-ws')) |
24 | 24 | .use(require('ssb-server-channel')) |
25 | - .use(require('./ssb-server-blog-stats')) | |
25 | + .use(require('./ssb-server-ticktack')) | |
26 | 26 | |
27 | 27 | Client(config.keys, config, (err, ssbServer) => { |
28 | 28 | if (err) { |
29 | 29 | console.log('> starting sbot') |
message/html/comment.js | ||
---|---|---|
@@ -1,0 +1,135 @@ | ||
1 | +const nest = require('depnest') | |
2 | +const { h, Value, map, when, resolve, computed, onceTrue } = require('mutant') | |
3 | +const get = require('lodash/get') | |
4 | +const { heads } = require('ssb-sort') | |
5 | + | |
6 | +exports.gives = nest('message.html.comment') | |
7 | + | |
8 | +exports.needs = nest({ | |
9 | + 'about.html.avatar': 'first', | |
10 | + 'about.obs.name': 'first', | |
11 | + 'backlinks.obs.for': 'first', | |
12 | + 'blog.html.title': 'first', | |
13 | + 'message.html.compose': 'first', | |
14 | + 'message.html.markdown': 'first', | |
15 | + 'message.html.timeago': 'first', | |
16 | + 'message.html.likes': 'first', | |
17 | + 'unread.sync.markRead': 'first', | |
18 | + 'unread.sync.isUnread': 'first', | |
19 | + 'sbot.obs.connection': 'first', | |
20 | + 'translations.sync.strings': 'first' | |
21 | +}) | |
22 | + | |
23 | +exports.create = (api) => { | |
24 | + return nest('message.html.comment', Comment) | |
25 | + | |
26 | + function Comment ({ comment, replies, branch, showRootLink = false }) { | |
27 | + const strings = api.translations.sync.strings() | |
28 | + const msg = resolve(comment) | |
29 | + var root = get(msg, 'value.content.root') | |
30 | + if (!root) return | |
31 | + | |
32 | + const { author, content } = msg.value | |
33 | + | |
34 | + if (!replies) { | |
35 | + replies = computed(api.backlinks.obs.for(msg.key), backlinks => { | |
36 | + return backlinks.filter(backlinker => { | |
37 | + const { type, root: _root, fork } = backlinker.value.content | |
38 | + return type === 'post' && fork === msg.key && _root === root | |
39 | + }) | |
40 | + }) | |
41 | + } | |
42 | + if (!branch) { | |
43 | + branch = computed(api.backlinks.obs.for(root), backlinks => { | |
44 | + return heads(backlinks) | |
45 | + }) | |
46 | + } | |
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 | + | |
59 | + var nestedReplyCompose = Value(false) | |
60 | + const toggleCompose = () => nestedReplyCompose.set(!nestedReplyCompose()) | |
61 | + const nestedReplyComposer = api.message.html.compose({ | |
62 | + meta: { | |
63 | + type: 'post', | |
64 | + root, | |
65 | + fork: msg.key, | |
66 | + branch, | |
67 | + channel: content.channel | |
68 | + }, | |
69 | + shrink: false, | |
70 | + canAttach: true, | |
71 | + canPreview: false, | |
72 | + placeholder: strings.writeComment | |
73 | + }, toggleCompose) | |
74 | + | |
75 | + return h('Comment', { className }, [ | |
76 | + h('div.left', api.about.html.avatar(author, 'tiny')), | |
77 | + h('div.right', [ | |
78 | + h('section.context', [ | |
79 | + h('div.name', api.about.obs.name(author)), | |
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! | |
83 | + ]), | |
84 | + h('section.content', api.message.html.markdown(get(msg, 'value.content.text'))), | |
85 | + h('section.actions', [ | |
86 | + h('div.reply', { 'ev-click': toggleCompose }, [ | |
87 | + h('i.fa.fa-commenting-o') | |
88 | + ]), | |
89 | + api.message.html.likes(msg) | |
90 | + ]), | |
91 | + when(replies, | |
92 | + h('section.replies', | |
93 | + map( | |
94 | + replies, | |
95 | + NestedComment, | |
96 | + { | |
97 | + comparer: (a, b) => { | |
98 | + if (a === undefined || b === undefined) return false | |
99 | + return a.key === b.key | |
100 | + } | |
101 | + } | |
102 | + ) | |
103 | + ) | |
104 | + ), | |
105 | + when(nestedReplyCompose, nestedReplyComposer) | |
106 | + ]) | |
107 | + ]) | |
108 | + } | |
109 | + | |
110 | + function NestedComment (comment) { | |
111 | + const msg = resolve(comment) | |
112 | + const raw = get(msg, 'value.content.text') | |
113 | + if (!raw) return | |
114 | + | |
115 | + const { author } = msg.value | |
116 | + | |
117 | + return h('Comment -nested', [ | |
118 | + h('div.left'), | |
119 | + h('div.right', [ | |
120 | + h('section.context', [ | |
121 | + h('div.name', api.about.obs.name(author)), | |
122 | + api.message.html.timeago(msg) | |
123 | + ]), | |
124 | + h('section.content', api.message.html.markdown(raw)) | |
125 | + ]) | |
126 | + ]) | |
127 | + } | |
128 | + | |
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 | + } | |
135 | +} |
message/html/comment.mcss | ||
---|---|---|
@@ -1,0 +1,85 @@ | ||
1 | +Comment { | |
2 | + display: flex | |
3 | + | |
4 | + div.left { | |
5 | + margin-right: 1rem | |
6 | + | |
7 | + div.Avatar {} | |
8 | + } | |
9 | + | |
10 | + div.right { | |
11 | + flex-grow: 1 | |
12 | + | |
13 | + $borderBottomLight | |
14 | + padding-bottom: 1rem | |
15 | + margin-bottom: 1rem | |
16 | + | |
17 | + section.context { | |
18 | + display: flex | |
19 | + align-items: baseline | |
20 | + | |
21 | + div.name { | |
22 | + font-size: 1.2rem | |
23 | + margin-right: 1rem | |
24 | + } | |
25 | + div.Timeago { | |
26 | + margin-right: 1.5rem | |
27 | + } | |
28 | + a.rootLink { | |
29 | + font-size: .8rem | |
30 | + } | |
31 | + } | |
32 | + | |
33 | + section.content { | |
34 | + font-size: .95rem | |
35 | + line-height: 1.4 | |
36 | + div.Markdown {} | |
37 | + } | |
38 | + | |
39 | + section.actions { | |
40 | + font-size: 1rem | |
41 | + color: #555 | |
42 | + margin-right: .5rem | |
43 | + | |
44 | + display: flex | |
45 | + justify-content: flex-end | |
46 | + align-items: baseline | |
47 | + | |
48 | + div.reply { | |
49 | + cursor: pointer | |
50 | + | |
51 | + margin-right: 1.5rem | |
52 | + i.fa {} | |
53 | + } | |
54 | + | |
55 | + div.Likes { } | |
56 | + } | |
57 | + | |
58 | + section.replies { | |
59 | + margin-top: 1rem | |
60 | + } | |
61 | + | |
62 | + div.Compose { | |
63 | + margin: 1rem 0 | |
64 | + } | |
65 | + } | |
66 | +} | |
67 | + | |
68 | +Comment -nested { | |
69 | + padding: 1rem 1rem 0 1rem | |
70 | + $backgroundPrimaryText | |
71 | + $roundTop | |
72 | + $roundBottom | |
73 | + | |
74 | + margin-bottom: 1rem | |
75 | + | |
76 | + div.left { margin: 0 } | |
77 | + div.right { | |
78 | + padding: 0 | |
79 | + border: 0 | |
80 | + margin: 0 | |
81 | + | |
82 | + } | |
83 | +} | |
84 | + | |
85 | + |
message/html/notification.js | ||
---|---|---|
@@ -1,0 +1,69 @@ | ||
1 | +const nest = require('depnest') | |
2 | +const { h, Value, onceTrue } = require('mutant') | |
3 | +const get = require('lodash/get') | |
4 | +const getType = (msg) => get(msg, 'value.content.type') | |
5 | +const getLikeRoot = (msg) => get(msg, 'value.content.vote.link') | |
6 | +const getShareRoot = (msg) => get(msg, 'value.content.share.link') | |
7 | +const getRoot = (msg) => { | |
8 | + switch (getType(msg)) { | |
9 | + case 'vote': | |
10 | + return getLikeRoot(msg) | |
11 | + case 'share': | |
12 | + return getShareRoot(msg) | |
13 | + } | |
14 | +} | |
15 | + | |
16 | +exports.gives = nest('message.html.notification') | |
17 | + | |
18 | +exports.needs = nest({ | |
19 | + 'about.html.avatar': 'first', | |
20 | + 'about.obs.name': 'first', | |
21 | + 'blog.html.title': 'first', | |
22 | + // 'message.html.markdown': 'first', | |
23 | + 'message.html.timeago': 'first', | |
24 | + 'message.html.likes': 'first', | |
25 | + 'unread.sync.markRead': 'first', | |
26 | + 'unread.sync.isUnread': 'first', | |
27 | + 'sbot.obs.connection': 'first', | |
28 | + 'translations.sync.strings': 'first' | |
29 | +}) | |
30 | + | |
31 | +exports.create = (api) => { | |
32 | + return nest('message.html.notification', Notification) | |
33 | + | |
34 | + function Notification (msg) { | |
35 | + var root = getRoot(msg) | |
36 | + if (!root) return | |
37 | + | |
38 | + const { author } = msg.value | |
39 | + | |
40 | + var className = api.unread.sync.isUnread(msg) ? ' -unread' : ' -read' | |
41 | + api.unread.sync.markRead(msg) | |
42 | + | |
43 | + var title = Value() | |
44 | + processMessage(root, msg => { | |
45 | + var t = api.blog.html.title(msg) | |
46 | + title.set(t.innerText ? t.innerText : t) | |
47 | + }) | |
48 | + | |
49 | + return h('Notification', { className }, [ | |
50 | + h('div.left', api.about.html.avatar(author, 'tiny')), | |
51 | + h('div.right', [ | |
52 | + h('section.context', [ | |
53 | + h('div.name', api.about.obs.name(author)), | |
54 | + api.message.html.timeago(msg), | |
55 | + h('a.rootLink', {href: root}, ['<< ', title, ' >>']) | |
56 | + ]) | |
57 | + // TODO display the share text ?? | |
58 | + // h('section.content', api.message.html.markdown(get(msg, 'value.content.text'))), | |
59 | + ]) | |
60 | + ]) | |
61 | + } | |
62 | + | |
63 | + function processMessage (key, fn) { | |
64 | + onceTrue(api.sbot.obs.connection, server => server.get(key, (err, value) => { | |
65 | + if (err) return console.error(err) | |
66 | + fn({ key, value }) | |
67 | + })) | |
68 | + } | |
69 | +} |
message/html/notification.mcss | ||
---|---|---|
@@ -1,0 +1,42 @@ | ||
1 | +Notification { | |
2 | + display: flex | |
3 | + | |
4 | + div.left { | |
5 | + padding-top: 1rem | |
6 | + margin-right: 1rem | |
7 | + | |
8 | + div.Avatar {} | |
9 | + } | |
10 | + | |
11 | + div.right { | |
12 | + flex-grow: 1 | |
13 | + | |
14 | + padding-top: 1rem | |
15 | + padding-bottom: 2rem | |
16 | + $borderBottomLight | |
17 | + margin-bottom: 1rem | |
18 | + | |
19 | + section.context { | |
20 | + display: flex | |
21 | + align-items: baseline | |
22 | + | |
23 | + div.name { | |
24 | + font-size: 1.2rem | |
25 | + margin-right: 1rem | |
26 | + } | |
27 | + div.Timeago { | |
28 | + margin-right: 1.5rem | |
29 | + } | |
30 | + a.rootLink { | |
31 | + font-size: .8rem | |
32 | + } | |
33 | + } | |
34 | + | |
35 | + section.content { | |
36 | + font-size: .95rem | |
37 | + line-height: 1.4 | |
38 | + div.Markdown {} | |
39 | + } | |
40 | + | |
41 | + } | |
42 | +} |
message/index.js | ||
---|---|---|
@@ -3,10 +3,12 @@ | ||
3 | 3 | publish: require('./async/publish') |
4 | 4 | }, |
5 | 5 | html: { |
6 | 6 | channel: require('./html/channel'), |
7 | + comment: require('./html/comment'), | |
7 | 8 | compose: require('./html/compose'), |
8 | 9 | likes: require('./html/likes'), |
10 | + notification: require('./html/notification'), | |
9 | 11 | shares: require('./html/shares'), |
10 | 12 | webshares: require('./html/webshares'), |
11 | 13 | subject: require('./html/subject'), |
12 | 14 | timeago: require('./html/timeago') |
package.json | ||
---|---|---|
@@ -40,17 +40,18 @@ | ||
40 | 40 | "medium-editor": "^5.23.3", |
41 | 41 | "medium-editor-markdown": "^2.6.0", |
42 | 42 | "micro-css": "^2.0.1", |
43 | 43 | "morphdom": "^2.3.3", |
44 | - "mutant": "^3.21.2", | |
44 | + "mutant": "^3.22.1", | |
45 | 45 | "mutant-scroll": "0.0.5", |
46 | 46 | "open-external": "^0.1.1", |
47 | 47 | "patch-drafts": "0.0.6", |
48 | 48 | "patch-history": "^1.0.0", |
49 | 49 | "patch-profile": "^1.0.4", |
50 | 50 | "patch-settings": "^1.1.2", |
51 | 51 | "patch-suggest": "^1.1.0", |
52 | 52 | "patchcore": "^1.23.3", |
53 | + "pull-defer": "^0.2.2", | |
53 | 54 | "pull-next": "^1.0.1", |
54 | 55 | "pull-next-step": "^1.0.0", |
55 | 56 | "pull-obv": "^1.3.0", |
56 | 57 | "pull-stream": "^3.6.0", |
router/sync/routes.js | ||
---|---|---|
@@ -14,8 +14,9 @@ | ||
14 | 14 | 'app.page.blogShow': 'first', |
15 | 15 | 'app.page.settings': 'first', |
16 | 16 | 'app.page.channelSubscriptions': 'first', |
17 | 17 | 'app.page.channelShow': 'first', |
18 | + 'app.page.notifications': 'first', | |
18 | 19 | // 'app.page.channel': 'first', |
19 | 20 | // 'app.page.groupFind': 'first', |
20 | 21 | // 'app.page.groupIndex': 'first', |
21 | 22 | // 'app.page.groupNew': 'first', |
@@ -55,10 +56,11 @@ | ||
55 | 56 | [ location => location.page === 'channelSubscriptions', pages.channelSubscriptions ], |
56 | 57 | [ location => location.page === 'channelShow', pages.channelShow ], |
57 | 58 | [ location => location.channel, pages.channelShow ], |
58 | 59 | |
59 | - // Stats pages | |
60 | + // Stats / Notifications pages | |
60 | 61 | [ location => location.page === 'statsShow', pages.statsShow ], |
62 | + [ location => location.page === 'notifications', pages.notifications ], | |
61 | 63 | |
62 | 64 | // AddressBook pages |
63 | 65 | [ location => location.page === 'addressBook', pages.addressBook ], |
64 | 66 |
translations/en.js | ||
---|---|---|
@@ -1,5 +1,6 @@ | ||
1 | 1 | module.exports = { |
2 | + stats: 'Stats', | |
2 | 3 | comments: 'Comments', |
3 | 4 | likes: 'Likes', |
4 | 5 | shares: 'Shares', |
5 | 6 | splash: { |
ssb-server-blog-stats.js | ||
---|---|---|
@@ -1,146 +1,0 @@ | ||
1 | -const FlumeView = require('flumeview-level') | |
2 | -const get = require('lodash/get') | |
3 | -const pull = require('pull-stream') | |
4 | -const isBlog = require('scuttle-blog/isBlog') | |
5 | -const { isMsg: isMsgRef } = require('ssb-ref') | |
6 | - | |
7 | -const getType = (msg) => get(msg, 'value.content.type') | |
8 | -const getAuthor = (msg) => get(msg, 'value.author') | |
9 | -const getCommentRoot = (msg) => get(msg, 'value.content.root') | |
10 | -const getLikeRoot = (msg) => get(msg, 'value.content.vote.link') | |
11 | -const getTimestamp = (msg) => get(msg, 'value.timestamp') | |
12 | - | |
13 | -const FLUME_VIEW_VERSION = 1 | |
14 | - | |
15 | -module.exports = { | |
16 | - name: 'blogStats', | |
17 | - version: 1, | |
18 | - manifest: { | |
19 | - get: 'async', | |
20 | - read: 'source', | |
21 | - readBlogs: 'source', | |
22 | - getBlogs: 'async', | |
23 | - readComments: 'source', | |
24 | - readLikes: 'source' | |
25 | - }, | |
26 | - init: (server, config) => { | |
27 | - console.log('initialising blog-stats plugin') | |
28 | - const myKey = server.keys.id | |
29 | - | |
30 | - const view = server._flumeUse( | |
31 | - 'internalblogStats', | |
32 | - FlumeView(FLUME_VIEW_VERSION, map) | |
33 | - ) | |
34 | - | |
35 | - return { | |
36 | - get: view.get, | |
37 | - read: view.read, | |
38 | - readBlogs, | |
39 | - getBlogs, | |
40 | - readComments, | |
41 | - readLikes | |
42 | - // readShares | |
43 | - } | |
44 | - | |
45 | - function map (msg, seq) { | |
46 | - var root | |
47 | - | |
48 | - switch (getType(msg)) { | |
49 | - case 'blog': | |
50 | - if (isBlog(msg) && isMyMsg(msg)) return [['B', msg.key, getTimestamp(msg)]] | |
51 | - else return [] | |
52 | - | |
53 | - case 'vote': | |
54 | - root = getLikeRoot(msg) | |
55 | - // TODO figure out how to only store likes I care about | |
56 | - if (root) return [['L', root, getTimestamp(msg)]] | |
57 | - else return [] | |
58 | - | |
59 | - // Note this catches: | |
60 | - // - all likes, on all things D: | |
61 | - // - likes AND unlikes | |
62 | - | |
63 | - case 'post': | |
64 | - root = getCommentRoot(msg) | |
65 | - // TODO figure out how to only store comments I care about | |
66 | - if (!root && isMyMsg(msg) && isPlog(msg)) return [['B', msg.key, getTimestamp(msg)]] | |
67 | - else if (root) return [['C', root, getTimestamp(msg)]] | |
68 | - else return [] | |
69 | - | |
70 | - // Note this catches: | |
71 | - // - all comments, on all things D: | |
72 | - | |
73 | - default: | |
74 | - return [] | |
75 | - } | |
76 | - } | |
77 | - | |
78 | - // a Plog is a Blog shaped Post! | |
79 | - function isPlog (msg) { | |
80 | - // if (get(msg, 'value.content.text', '').length >= 2500) console.log(get(msg, 'value.content.text', '').length) | |
81 | - return get(msg, 'value.content.text', '').length >= 2500 | |
82 | - } | |
83 | - | |
84 | - function readBlogs (options = {}) { | |
85 | - const query = Object.assign({}, { | |
86 | - gte: ['B', null, null], | |
87 | - // null is the 'minimum' structure in bytewise ordering | |
88 | - lte: ['B', undefined, undefined], | |
89 | - reverse: true, | |
90 | - values: true, | |
91 | - keys: false, | |
92 | - seqs: false | |
93 | - }, options) | |
94 | - | |
95 | - return view.read(query) | |
96 | - } | |
97 | - | |
98 | - function getBlogs (options, cb) { | |
99 | - pull( | |
100 | - readBlogs(options), | |
101 | - pull.collect(cb) | |
102 | - ) | |
103 | - } | |
104 | - | |
105 | - function readComments (blog, options = {}) { | |
106 | - var key = getBlogKey(blog) | |
107 | - | |
108 | - const query = Object.assign({}, { | |
109 | - gt: ['C', key, null], | |
110 | - lt: ['C', key, undefined], | |
111 | - // undefined is the 'maximum' structure in bytewise ordering https://www.npmjs.com/package/bytewise#order-of-supported-structures | |
112 | - reverse: true, | |
113 | - values: true, | |
114 | - keys: false, | |
115 | - seqs: false | |
116 | - }, options) | |
117 | - | |
118 | - return view.read(query) | |
119 | - } | |
120 | - | |
121 | - function readLikes (blog, options = {}) { | |
122 | - var key = getBlogKey(blog) | |
123 | - | |
124 | - const query = Object.assign({}, { | |
125 | - gt: ['L', key, null], | |
126 | - lt: ['L', key, undefined], | |
127 | - reverse: true, | |
128 | - values: true, | |
129 | - keys: false, | |
130 | - seqs: false | |
131 | - }, options) | |
132 | - | |
133 | - return view.read(query) | |
134 | - } | |
135 | - | |
136 | - function getBlogKey (blog) { | |
137 | - if (isMsgRef(blog)) return blog | |
138 | - // else if (isMsgRef(blog.key) && isBlog(blog)) return blog.key | |
139 | - else if (isMsgRef(blog.key) && (isBlog(blog) || isPlog(blog))) return blog.key | |
140 | - } | |
141 | - | |
142 | - function isMyMsg (msg) { | |
143 | - return getAuthor(msg) === myKey | |
144 | - } | |
145 | - } | |
146 | -} |
ssb-server-ticktack.js | ||
---|---|---|
@@ -1,0 +1,222 @@ | ||
1 | +const FlumeView = require('flumeview-level') | |
2 | +const get = require('lodash/get') | |
3 | +const pull = require('pull-stream') | |
4 | +const defer = require('pull-defer') | |
5 | +const isBlog = require('scuttle-blog/isBlog') | |
6 | +const { isMsg: isMsgRef } = require('ssb-ref') | |
7 | + | |
8 | +const getType = (msg) => get(msg, 'value.content.type') | |
9 | +const getAuthor = (msg) => get(msg, 'value.author') | |
10 | +const getCommentRoot = (msg) => get(msg, 'value.content.root') | |
11 | +const getLikeRoot = (msg) => get(msg, 'value.content.vote.link') | |
12 | +const getShareRoot = (msg) => get(msg, 'value.content.share.link') | |
13 | +const getTimestamp = (msg) => get(msg, 'value.timestamp') | |
14 | + | |
15 | +const FLUME_VIEW_VERSION = 1 | |
16 | + | |
17 | +module.exports = { | |
18 | + name: 'ticktack', | |
19 | + version: 1, | |
20 | + manifest: { | |
21 | + get: 'async', | |
22 | + read: 'source', | |
23 | + readBlogs: 'source', | |
24 | + getBlogs: 'async', | |
25 | + readComments: 'source', | |
26 | + readAllComments: 'source', | |
27 | + readAllLikes: 'source', | |
28 | + readAllShares: 'source', | |
29 | + readLikes: 'source' | |
30 | + }, | |
31 | + init: (server, config) => { | |
32 | + console.log('> initialising ticktack plugin') | |
33 | + const myKey = server.keys.id | |
34 | + | |
35 | + const view = server._flumeUse( | |
36 | + 'ticktack', | |
37 | + FlumeView(FLUME_VIEW_VERSION, map) | |
38 | + ) | |
39 | + | |
40 | + return { | |
41 | + get: view.get, | |
42 | + read: view.read, | |
43 | + readBlogs, | |
44 | + getBlogs, | |
45 | + readComments, | |
46 | + readAllComments, | |
47 | + readAllLikes, | |
48 | + readAllShares, | |
49 | + readLikes | |
50 | + // readShares | |
51 | + } | |
52 | + | |
53 | + function map (msg, seq) { | |
54 | + var root | |
55 | + | |
56 | + switch (getType(msg)) { | |
57 | + case 'blog': | |
58 | + if (isBlog(msg) && isMyMsg(msg)) return [['B', msg.key, getTimestamp(msg)]] | |
59 | + else return [] | |
60 | + | |
61 | + case 'vote': | |
62 | + root = getLikeRoot(msg) | |
63 | + // TODO figure out how to only store likes I care about | |
64 | + if (root) return [['L', root, getTimestamp(msg)]] | |
65 | + else return [] | |
66 | + | |
67 | + // Note this catches: | |
68 | + // - all likes, on all things D: | |
69 | + // - likes AND unlikes | |
70 | + | |
71 | + case 'post': | |
72 | + root = getCommentRoot(msg) | |
73 | + // TODO figure out how to only store comments I care about | |
74 | + if (!root && isMyMsg(msg) && isPlog(msg)) return [['B', msg.key, getTimestamp(msg)]] | |
75 | + else if (root) return [['C', root, getTimestamp(msg)]] | |
76 | + else return [] | |
77 | + | |
78 | + // Note this catches: | |
79 | + // - all comments, on all things D: | |
80 | + | |
81 | + default: | |
82 | + return [] | |
83 | + } | |
84 | + } | |
85 | + | |
86 | + function readBlogs (options = {}) { | |
87 | + const query = Object.assign({}, { | |
88 | + gte: ['B', null, null], | |
89 | + // null is the 'minimum' structure in bytewise ordering | |
90 | + lte: ['B', undefined, undefined], | |
91 | + reverse: true, | |
92 | + values: true, | |
93 | + keys: false, | |
94 | + seqs: false | |
95 | + }, options) | |
96 | + | |
97 | + return view.read(query) | |
98 | + } | |
99 | + | |
100 | + function getBlogs (options, cb) { | |
101 | + if (typeof options === 'function') { | |
102 | + cb = options | |
103 | + options = {} | |
104 | + } | |
105 | + | |
106 | + pull( | |
107 | + readBlogs(options), | |
108 | + pull.collect(cb) | |
109 | + ) | |
110 | + } | |
111 | + | |
112 | + function readComments (blog, options = {}) { | |
113 | + var key = getBlogKey(blog) | |
114 | + | |
115 | + const query = Object.assign({}, { | |
116 | + gt: ['C', key, null], | |
117 | + lt: ['C', key, undefined], | |
118 | + // undefined is the 'maximum' structure in bytewise ordering https://www.npmjs.com/package/bytewise#order-of-supported-structures | |
119 | + reverse: true, | |
120 | + values: true, | |
121 | + keys: false, | |
122 | + seqs: false | |
123 | + }, options) | |
124 | + | |
125 | + return view.read(query) | |
126 | + } | |
127 | + | |
128 | + function readLikes (blog, options = {}) { | |
129 | + var key = getBlogKey(blog) | |
130 | + | |
131 | + const query = Object.assign({}, { | |
132 | + gt: ['L', key, null], | |
133 | + lt: ['L', key, undefined], | |
134 | + reverse: true, | |
135 | + values: true, | |
136 | + keys: false, | |
137 | + seqs: false | |
138 | + }, options) | |
139 | + | |
140 | + return view.read(query) | |
141 | + } | |
142 | + | |
143 | + function readAllComments (opts = {}) { | |
144 | + return readAllSource({ | |
145 | + type: 'post', | |
146 | + makeFilter: blogIds => msg => { | |
147 | + if (getAuthor(msg) === myKey) return false // exclude my posts | |
148 | + if (getCommentRoot(msg) === undefined) return false // want only 'comments' (reply posts) | |
149 | + // NOTE - this one will get nested replies too | |
150 | + | |
151 | + return blogIds.includes(getCommentRoot(msg)) // is about one of my blogs | |
152 | + }, | |
153 | + opts | |
154 | + }) | |
155 | + } | |
156 | + | |
157 | + function readAllLikes (opts = {}) { | |
158 | + return readAllSource({ | |
159 | + type: 'vote', | |
160 | + makeFilter: blogIds => msg => { | |
161 | + if (getAuthor(msg) === myKey) return false // exclude my likes | |
162 | + | |
163 | + return blogIds.includes(getLikeRoot(msg)) // is about one of my blogs | |
164 | + }, | |
165 | + opts | |
166 | + }) | |
167 | + } | |
168 | + | |
169 | + function readAllShares (opts = {}) { | |
170 | + return readAllSource({ | |
171 | + type: 'share', | |
172 | + makeFilter: (blogIds) => msg => { | |
173 | + if (getAuthor(msg) === myKey) return false // exclude my shares | |
174 | + | |
175 | + return blogIds.includes(getShareRoot(msg)) // is about one of my blogs | |
176 | + }, | |
177 | + opts | |
178 | + }) | |
179 | + } | |
180 | + | |
181 | + function readAllSource ({ type, makeFilter, opts = {} }) { | |
182 | + var source = defer.source() | |
183 | + | |
184 | + getBlogs({ keys: true, values: false }, (err, data) => { | |
185 | + if (err) throw err | |
186 | + | |
187 | + const blogIds = data.map(d => d[1]) | |
188 | + | |
189 | + opts.type = type | |
190 | + var limit = opts.limit | |
191 | + delete opts.limit | |
192 | + // have to remove limit from the query otherwise Next stalls out if it doesn't get a new result | |
193 | + | |
194 | + const _source = pull( | |
195 | + server.messagesByType(opts), | |
196 | + pull.filter(makeFilter(blogIds)), | |
197 | + limit ? pull.take(limit) : true | |
198 | + ) | |
199 | + | |
200 | + source.resolve(_source) | |
201 | + }) | |
202 | + | |
203 | + return source | |
204 | + } | |
205 | + | |
206 | + function isMyMsg (msg) { | |
207 | + return getAuthor(msg) === myKey | |
208 | + } | |
209 | + } | |
210 | +} | |
211 | + | |
212 | +function getBlogKey (blog) { | |
213 | + if (isMsgRef(blog)) return blog | |
214 | + // else if (isMsgRef(blog.key) && isBlog(blog)) return blog.key | |
215 | + else if (isMsgRef(blog.key) && (isBlog(blog) || isPlog(blog))) return blog.key | |
216 | +} | |
217 | + | |
218 | +// a Plog is a Blog shaped Post! | |
219 | +function isPlog (msg) { | |
220 | + // if (get(msg, 'value.content.text', '').length >= 2500) console.log(get(msg, 'value.content.text', '').length) | |
221 | + return get(msg, 'value.content.text', '').length >= 2500 | |
222 | +} |
Built with git-ssb-web