git ssb

2+

mixmix / ticktack



Commit 36aff4f6d5e62e8ff308689bdfb6baea9413881e

Merge pull request #81 from ticktackim/feature-channel-subscriptions

WIP - channel subscriptions
Andre Alves Garzia authored on 2/2/2018, 1:25:33 AM
GitHub committed on 2/2/2018, 1:25:33 AM
Parent: 9660479589cb3e6a2d8fe371854da4ce7a38522b
Parent: 9b889230e5350365ea198b03786a51efe3ffe969

Files changed

app/html/sideNav/sideNavDiscovery.jschanged
app/html/channelCard.jsadded
app/html/channelCard.mcssadded
app/index.jschanged
app/page/channelShow.jsadded
app/page/channelShow.mcssadded
app/page/channelSubscriptions.jsadded
app/page/channelSubscriptions.mcssadded
main.jschanged
router/sync/routes.jschanged
translations/en.jschanged
channel/async.jsadded
channel/index.jsadded
channel/sync.jsadded
app/html/sideNav/sideNavDiscovery.jsView
@@ -129,8 +129,28 @@
129129 ]),
130130 label: strings.blogIndex.title,
131131 selected: isDiscoverLocation(location),
132132 location: { page: 'blogIndex' },
133+ }),
134+
135+ // My subscriptions
136+ Option({
137+ imageEl: h('i', [
138+ h('img', { src: path.join(__dirname, '../../../assets', 'discover.png') })
139+ ]),
140+ label: "My subscriptions",
141+ selected: isDiscoverSideNav(location),
142+ location: { page: 'channelSubscriptions', scope: 'user' },
143+ }),
144+
145+ // Friends subscriptions
146+ Option({
147+ imageEl: h('i', [
148+ h('img', { src: path.join(__dirname, '../../../assets', 'discover.png') })
149+ ]),
150+ label: "Friends subscriptions",
151+ selected: isDiscoverSideNav(location),
152+ location: { page: 'channelSubscriptions', scope: 'friends' },
133153 })
134154 ]
135155
136156 return api.app.html.scroller({
app/html/channelCard.jsView
@@ -1,0 +1,59 @@
1+var nest = require('depnest')
2+const { h, map, when, Value } = require('mutant')
3+var isString= require('lodash/isString')
4+var maxBy= require('lodash/maxBy')
5+var markdown = require('ssb-markdown')
6+var ref = require('ssb-ref')
7+var htmlEscape = require('html-escape')
8+
9+exports.gives = nest('app.html.channelCard')
10+
11+exports.needs = nest({
12+ 'keys.sync.id': 'first',
13+ 'history.sync.push': 'first',
14+ 'translations.sync.strings': 'first',
15+ 'channel.obs.subscribed': 'first',
16+ 'channel.async.subscribe': 'first',
17+ 'channel.async.unsubscribe': 'first',
18+ 'channel.sync.isSubscribedTo': 'first',
19+})
20+
21+exports.create = function (api) {
22+
23+ return nest('app.html.channelCard', (channel) => {
24+ var strings = api.translations.sync.strings()
25+
26+ const myId = api.keys.sync.id()
27+ const { subscribed } = api.channel.obs
28+ const { subscribe, unsubscribe } = api.channel.async
29+ const { isSubscribedTo } = api.channel.sync
30+ const myChannels = subscribed(myId)
31+ let cs = myChannels().values()
32+ const youSubscribe = Value(isSubscribedTo(channel, myId))
33+
34+ let cb = () => {
35+ youSubscribe.set(isSubscribedTo(channel, myId))
36+ }
37+
38+ const goToChannel = (e, channel) => {
39+ e.stopPropagation()
40+
41+ api.history.sync.push({ page: 'channelShow', channel: channel })
42+ }
43+
44+ var b = h('ChannelCard', [
45+ h('div.content', [
46+ h('div.text', [
47+ h('h2', {'ev-click': ev => goToChannel(ev, channel)}, channel),
48+ when(youSubscribe,
49+ h('Button', { 'ev-click': () => unsubscribe(channel, cb) }, strings.channelShow.action.unsubscribe),
50+ h('Button', { 'ev-click': () => subscribe(channel, cb) }, strings.channelShow.action.subscribe)
51+ ),
52+ ])
53+ ])
54+ ])
55+
56+ return b
57+ })
58+}
59+
app/html/channelCard.mcssView
@@ -1,0 +1,47 @@
1+ChannelCard {
2+ $backgroundPrimaryText
3+ padding: 4rem 1.5rem 4rem 1.5rem
4+ width: 100%
5+
6+ border-bottom: 1px solid rgba(0,0,0, .1)
7+ transition: all .5s ease
8+
9+ display: flex
10+ flex-direction: column
11+
12+ div.content {
13+ display: flex
14+ flex-direction: row
15+ flex-grow: 1
16+
17+ cursor: pointer
18+
19+ div.text {
20+ display: flex
21+ flex-wrap: wrap
22+ justify-content: space-between;
23+ width: 100%;
24+
25+ h2 {
26+ $markdownLarge
27+ margin: 0 .5rem 0 0
28+ }
29+ div.Button {
30+ margin-left: auto
31+ }
32+ div.summary {
33+ flex-basis: 100%
34+ }
35+ }
36+ }
37+ background-color: #fff
38+
39+ -unread {
40+ div.content {
41+ font-weight: bold
42+ }
43+ background-color: #fff
44+ }
45+}
46+
47+
app/index.jsView
@@ -9,8 +9,9 @@
99 thread: require('./html/thread'),
1010 link: require('./html/link'),
1111 lightbox: require('./html/lightbox'),
1212 blogCard: require('./html/blogCard'),
13+ channelCard: require('./html/channelCard'),
1314 topNav: {
1415 topNavAddressBook: require('./html/topNav/topNavAddressBook'),
1516 topNavBlog: require('./html/topNav/topNavBlog'),
1617 topNavBack: require('./html/topNav/zz_topNavBack'),
@@ -26,10 +27,12 @@
2627 blogIndex: require('./page/blogIndex'),
2728 blogNew: require('./page/blogNew'),
2829 blogSearch: require('./page/blogSearch'),
2930 blogShow: require('./page/blogShow'),
31+ channelShow: require('./page/channelShow'),
3032 error: require('./page/error'),
3133 settings: require('./page/settings'),
34+ channelSubscriptions: require('./page/channelSubscriptions'),
3235 // channel: require('./page/channel'),
3336 // image: require('./page/image'),
3437 // groupFind: require('./page/groupFind'),
3538 // groupIndex: require('./page/groupIndex'),
app/page/channelShow.jsView
@@ -1,0 +1,105 @@
1+const nest = require('depnest')
2+const { h, Value, computed, map, when, resolve } = require('mutant')
3+const pull = require('pull-stream')
4+
5+exports.gives = nest('app.page.channelShow')
6+
7+exports.needs = nest({
8+ 'app.html.sideNav': 'first',
9+ 'app.html.topNav': 'first',
10+ 'app.html.scroller': 'first',
11+ 'app.html.blogCard': 'first',
12+ 'channel.obs.recent': 'first',
13+ 'feed.pull.channel': 'first',
14+ 'feed.pull.public': 'first',
15+ 'history.sync.push': 'first',
16+ 'keys.sync.id': 'first',
17+ 'message.html.channel': 'first',
18+ 'translations.sync.strings': 'first',
19+ 'unread.sync.isUnread': 'first',
20+ 'channel.obs.subscribed': 'first',
21+ 'channel.async.subscribe': 'first',
22+ 'channel.async.unsubscribe': 'first',
23+ 'channel.sync.isSubscribedTo': 'first'
24+})
25+
26+exports.create = (api) => {
27+ return nest('app.page.channelShow', channelShow)
28+
29+ function channelShow(location) {
30+
31+ var strings = api.translations.sync.strings()
32+ const myId = api.keys.sync.id()
33+ const { subscribed } = api.channel.obs
34+ const { subscribe, unsubscribe } = api.channel.async
35+ const { isSubscribedTo } = api.channel.sync
36+ const myChannels = subscribed(myId)
37+ let cs = myChannels().values()
38+ const youSubscribe = Value(isSubscribedTo(location.channel, myId))
39+
40+ let cb = () => {
41+ youSubscribe.set(isSubscribedTo(location.channel, myId))
42+ }
43+
44+ var searchVal = resolve(location.channel)
45+ var searchResults = computed([api.channel.obs.recent(), searchVal], (channels, val) => {
46+ if (val.length < 2) return []
47+
48+ return channels.filter(c => c.toLowerCase().indexOf(val.toLowerCase()) > -1)
49+ })
50+
51+
52+
53+ createStream = api.feed.pull.channel(location.channel)
54+
55+
56+ const prepend = [
57+ api.app.html.topNav(location),
58+ h('section.about', [
59+ h('h1', location.channel),
60+ h('div.actions', [
61+ when(youSubscribe,
62+ h('Button', { 'ev-click': () => subscribe(location.channel, cb) }, strings.channelShow.action.unsubscribe),
63+ h('Button', { 'ev-click': () => unsubscribe(location.channel, cb) }, strings.channelShow.action.subscribe)
64+ )
65+ ])
66+ ]),
67+ ]
68+
69+ var channelPosts = api.app.html.scroller({
70+ classList: ['content'],
71+ prepend,
72+ stream: createStream,
73+ filter: () => pull(
74+ pull.filter(msg => {
75+ const type = msg.value.content.type
76+ return type === 'post' || type === 'blog'
77+ }),
78+ pull.filter(msg => !msg.value.content.root) // show only root messages
79+ ),
80+ // FUTURE : if we need better perf, we can add a persistent cache. At the moment this page is fast enough though.
81+ // See implementation of app.html.sideNav for example
82+ // store: recentMsgCache,
83+ // updateTop: updateRecentMsgCache,
84+ // updateBottom: updateRecentMsgCache,
85+ render
86+ })
87+
88+ return h('Page -channelShow', { title: strings.home }, [
89+ api.app.html.sideNav(location),
90+ channelPosts
91+ ])
92+ }
93+
94+
95+ function render(blog) {
96+ const { recps, channel } = blog.value.content
97+ var onClick
98+ if (channel && !recps)
99+ onClick = (ev) => api.history.sync.push(Object.assign({}, blog, { page: 'blogShow' }))
100+
101+ return api.app.html.blogCard(blog, { onClick })
102+ }
103+}
104+
105+
app/page/channelShow.mcssView
@@ -1,0 +1,67 @@
1+Page -channelShow {
2+ div.content { padding: 0 }
3+
4+ div.Scroller.content {
5+
6+ section.top {
7+ left: 0
8+ right: 0
9+ top: 0
10+ z-index: 99
11+
12+
13+
14+ section.about {
15+
16+ display: flex
17+ flex-direction: column
18+ align-items: center
19+ padding-bottom: 1rem
20+
21+
22+ h1 {
23+ font-weight: bold
24+ font-size: 1.5rem
25+ padding: 1rem
26+
27+ margin: auto
28+
29+ }
30+
31+ div.actions {
32+ display: flex
33+
34+ div.Button {
35+ margin: auto
36+ }
37+
38+ }
39+ }
40+
41+ }
42+
43+ section.content {
44+ background-color: #fff
45+ $maxWidth
46+ margin: auto
47+ padding: .5rem 2rem
48+
49+ display: flex
50+ flex-wrap: wrap
51+
52+ div.BlogCard {
53+ flex-basis: 100%
54+
55+ border-bottom: 1px solid gainsboro
56+ }
57+ }
58+
59+ section.bottom {
60+ div.Button {
61+ margin: 1rem 0
62+ }
63+ }
64+ }
65+}
66+
67+
app/page/channelSubscriptions.jsView
@@ -1,0 +1,89 @@
1+const nest = require('depnest')
2+const { h, watch, when, computed, Value, Set: MutantSet } = require('mutant')
3+const pull = require('pull-stream')
4+const Pushable = require('pull-pushable')
5+const ref = require('ssb-ref')
6+const throttle = require('mutant/throttle')
7+const MutantPullReduce = require('mutant-pull-reduce')
8+
9+
10+exports.gives = nest('app.page.channelSubscriptions')
11+
12+exports.needs = nest({
13+ 'app.html.sideNav': 'first',
14+ 'app.html.topNav': 'first',
15+ 'app.html.scroller': 'first',
16+ 'app.html.channelCard': 'first',
17+
18+ 'history.sync.push': 'first',
19+ 'keys.sync.id': 'first',
20+ 'channel.obs.subscribed': 'first',
21+ 'channel.html.link': 'first',
22+ 'translations.sync.strings': 'first',
23+ 'sbot.async.friendsGet': 'first',
24+ 'sbot.pull.userFeed': 'first'
25+})
26+
27+exports.create = (api) => {
28+ return nest('app.page.channelSubscriptions', function (location) {
29+ const strings = api.translations.sync.strings()
30+ const myId = api.keys.sync.id()
31+ const { subscribed } = api.channel.obs
32+ let myChannels, displaySubscriptions
33+
34+ if (location.scope === "user") {
35+ myChannels = subscribed(myId)
36+ displaySubscriptions = () => [...myChannels().values()].map(c => api.app.html.channelCard(c))
37+
38+ return h('Page -channelSubscriptions', { title: strings.home }, [
39+ api.app.html.sideNav(location),
40+ h('div.content', [
41+ //api.app.html.topNav(location),
42+ when(myChannels, displaySubscriptions, h("p", "Loading..."))
43+ ])
44+ ])
45+
46+ }
47+
48+ if (location.scope === "friends") {
49+
50+ function createStream() {
51+ var p = Pushable(true) // optionally pass `onDone` after it
52+
53+ api.sbot.async.friendsGet({ dest: myId }, (err, friends) => {
54+ for (f in friends) {
55+ var s = subscribed(f)
56+ s(c => [...c].map(x => p.push(x)))
57+ }
58+ })
59+
60+ return p.source
61+ }
62+
63+ var stream = createStream()
64+ var opts = {
65+ startValue: new Set(),
66+ nextTick: true
67+ }
68+
69+ var channelList = api.app.html.scroller({
70+ classList: ['content'],
71+ stream: createStream,
72+ render
73+ })
74+
75+ function render(channel) {
76+ return api.app.html.channelCard(channel)
77+ }
78+
79+ return h('Page -channelSubscriptions', { title: strings.home }, [
80+ api.app.html.sideNav(location),
81+ channelList
82+ ])
83+ }
84+ })
85+}
86+
87+
88+
89+
app/page/channelSubscriptions.mcssView
@@ -1,0 +1,21 @@
1+Page -channelSubscriptions {
2+
3+ div.content {
4+
5+ background-color: #fff
6+ $maxWidth
7+ margin: .8rem auto
8+ padding: .5rem 2rem
9+
10+ display: flex
11+ flex-wrap: wrap
12+
13+ div.BlogCard {
14+ flex-basis: 100%
15+ border-bottom: 1px solid rgba(0,0,0, .1)
16+ }
17+ }
18+
19+ }
20+}
21+
main.jsView
@@ -29,8 +29,9 @@
2929 router: require('./router'),
3030 styles: require('./styles'),
3131 state: require('./state/obs'),
3232 unread: require('./unread'),
33+ channel: require('./channel')
3334 },
3435 {
3536 profile: require('patch-profile'),
3637 history: require('patch-history'),
router/sync/routes.jsView
@@ -12,8 +12,10 @@
1212 'app.page.blogNew': 'first',
1313 'app.page.blogSearch': 'first',
1414 'app.page.blogShow': 'first',
1515 'app.page.settings': 'first',
16+ 'app.page.channelSubscriptions': 'first',
17+ 'app.page.channelShow': 'first',
1618 // 'app.page.channel': 'first',
1719 // 'app.page.groupFind': 'first',
1820 // 'app.page.groupIndex': 'first',
1921 // 'app.page.groupNew': 'first',
@@ -47,8 +49,13 @@
4749 && get(location, 'value.content.type') === 'post'
4850 && !get(location, 'value.private') // treats public posts as 'blogs'
4951 }, pages.blogShow ],
5052
53+ // Channel related pages
54+ [ location => location.page === 'channelSubscriptions', pages.channelSubscriptions],
55+ [ location => location.page === 'channelShow', pages.channelShow ],
56+
57+
5158 // AddressBook pages
5259 [ location => location.page === 'addressBook', pages.addressBook ],
5360
5461 // Private Thread pages
translations/en.jsView
@@ -130,8 +130,14 @@
130130 follow: "Follow",
131131 friendsInCommon: 'friends in common'
132132 }
133133 },
134+ channelShow: {
135+ action: {
136+ subscribe: 'Subscribe',
137+ unsubscribe: 'Unsubscribe'
138+ }
139+ },
134140 languages: {
135141 en: 'English',
136142 zh: '中文'
137143 }
channel/async.jsView
@@ -1,0 +1,37 @@
1+var nest = require('depnest')
2+var ref = require('ssb-ref')
3+
4+exports.needs = nest({
5+ 'keys.sync.id': 'first',
6+ 'sbot.async.publish': 'first',
7+ 'channel.obs.subscribed': 'first',
8+})
9+
10+exports.gives = nest({
11+ 'channel.async': ['subscribe', 'unsubscribe']
12+})
13+
14+exports.create = function (api) {
15+ return nest({
16+ 'channel.async': {subscribe, unsubscribe}
17+ })
18+
19+ function subscribe (channel, cb) {
20+ if (!channel) throw new Error('a channel must be specified')
21+ api.sbot.async.publish({
22+ type: 'channel',
23+ channel: channel,
24+ subscribed: true
25+ }, cb)
26+ }
27+
28+ function unsubscribe (channel, cb) {
29+ if (!channel) throw new Error('a channel must be specified')
30+ api.sbot.async.publish({
31+ type: 'channel',
32+ channel: channel,
33+ subscribed: false
34+ }, cb)
35+ }
36+
37+}
channel/index.jsView
@@ -1,0 +1,6 @@
1+module.exports = {
2+ async: require('./async'),
3+ sync: require('./sync')
4+}
5+
6+
channel/sync.jsView
@@ -1,0 +1,24 @@
1+var nest = require('depnest')
2+var ref = require('ssb-ref')
3+
4+exports.needs = nest({
5+ 'keys.sync.id': 'first',
6+ 'channel.obs.subscribed': 'first',
7+})
8+
9+exports.gives = nest('channel.sync.isSubscribedTo')
10+
11+exports.create = function (api) {
12+ return nest('channel.sync.isSubscribedTo', isSubscribedTo)
13+
14+ function isSubscribedTo (channel, id) {
15+ if (!ref.isFeed(id)) {
16+ id = api.keys.sync.id()
17+ }
18+
19+ const { subscribed } = api.channel.obs
20+ const myChannels = subscribed(id)
21+ let v = myChannels().values()
22+ return [...v].includes(channel)
23+ }
24+}

Built with git-ssb-web