git ssb

2+

mixmix / ticktack



Commit 0e154203de9454a9f8f6a7ecc08aa6f661819f6d

Merge pull request #46 from ticktackim/blogShow

blogShow - v1
mix irving authored on 11/18/2017, 12:17:48 AM
GitHub committed on 11/18/2017, 12:17:48 AM
Parent: b97fb441a1afa288d695acd468056c823a35bf00
Parent: 9fff2bb9c8cf22a5430644eaf74038867de5071d

Files changed

about/html/avatar.jschanged
about/html/avatar.mcsschanged
app/async/catch-link-click.jschanged
app/html/app.jschanged
app/html/context.jschanged
app/html/context.mcsschanged
app/html/thread.jschanged
app/html/thread.mcsschanged
app/html/blog-card.jsdeleted
app/html/blogCard.jsadded
app/html/blog-card.mcssdeleted
app/html/blogCard.mcssadded
app/html/comments.jsadded
app/html/comments.mcssadded
app/index.jschanged
app/page/blogIndex.jschanged
app/page/error.jschanged
app/page/groupFind.mcsschanged
app/page/threadNew.mcsschanged
app/page/userFind.mcsschanged
app/page/userShow.jschanged
app/page/userShow.mcsschanged
app/page/blogShow.jsadded
app/page/blogShow.mcssadded
app/page/error.mcssadded
app/sync/initialize/clickHandler.jsadded
app/sync/initialize/styles.jsadded
app/sync/initialize/suggests.jsadded
app/sync/nav-history.jsadded
main.jschanged
message/html/compose.jschanged
message/html/compose.mcsschanged
message/html/channel.jsadded
message/html/likes.jsadded
message/html/likes.mcssadded
message/html/timeago.jsadded
message/html/timeago.mcssadded
message/index.jschanged
router/sync/routes.jschanged
styles/button.mcsschanged
styles/global.mcsschanged
styles/markdown.mcsschanged
styles/mixins.jschanged
contact/html/follow.jsadded
contact/index.jsadded
about/html/avatar.jsView
@@ -1,22 +1,26 @@
11 const nest = require('depnest')
22 const { h } = require('mutant')
33
4+exports.gives = nest('about.html.avatar')
5+
46 exports.needs = nest({
5- 'about.html.image': 'first',
6- 'app.html.link': 'first'
7+ 'about.obs.imageUrl': 'first',
8+ 'about.obs.color': 'first',
9+ 'history.sync.push': 'first'
710 })
811
9-exports.gives = nest('about.html.avatar')
10-
1112 exports.create = function (api) {
12- return nest('about.html.avatar', feed => {
13- const Link = api.app.html.link
14-
15- return Link(
16- { page: 'userShow', feed },
17- api.about.html.image(feed)
18- )
19-
13+ return nest('about.html.avatar', function (id, size = 'small') {
14+ return h('img', {
15+ classList: `Avatar -${size}`,
16+ style: { 'background-color': api.about.obs.color(id) },
17+ src: api.about.obs.imageUrl(id),
18+ title: id,
19+ 'ev-click': e => {
20+ e.stopPropagation()
21+ api.history.sync.push({ page: 'userShow', feed: id })
22+ }
23+ })
2024 })
2125 }
2226
about/html/avatar.mcssView
@@ -1,5 +1,21 @@
11 Avatar {
2- $avatarSmall
3- margin-right: .5rem
2+ cursor: pointer
3+ $circleSmall
4+
5+ -tiny {
6+ $circleTiny
7+ }
8+
9+ -small {
10+ $circleSmall
11+ }
12+
13+ -medium {
14+ $circleMedium
15+ }
16+
17+ -large {
18+ $circleLarge
19+ }
420 }
521
app/async/catch-link-click.jsView
@@ -1,9 +1,14 @@
@@ -29,8 +34,16 @@
app/html/app.jsView
@@ -1,79 +1,19 @@
11 const nest = require('depnest')
2-const values = require('lodash/values')
3-const insertCss = require('insert-css')
4-const openExternal = require('open-external')
52
6-const HyperNav = require('hyper-nav')
7-const computed = require('mutant/computed')
8-const h = require('mutant/h')
3+exports.gives = nest('app.html.app')
94
10-exports.gives = nest({
11- 'app.html.app': true,
12- 'history.obs.history': true,
13- 'history.sync.push': true,
14- 'history.sync.back': true,
15-})
16-
175 exports.needs = nest({
18- 'about.async.suggest': 'first',
19- 'app.html.header': 'first',
20- 'app.async.catchLinkClick': 'first',
21- 'channel.async.suggest': 'first',
22- 'keys.sync.id': 'first',
23- 'router.sync.router': 'first',
24- 'settings.sync.get': 'first',
25- 'settings.sync.set': 'first',
26- 'styles.css': 'reduce',
6+ 'app.sync.initialize': 'map',
7+ 'app.sync.nav': 'first'
278 })
289
2910 exports.create = (api) => {
30- var nav = null
31-
3211 return nest({
3312 'app.html.app': function app () {
13+ api.app.sync.initialize()
3414
35- // DIRTY HACK - initializes the suggestion indexes
36- api.about.async.suggest()
37- api.channel.async.suggest()
38-
39- const css = values(api.styles.css()).join('\n')
40- insertCss(css)
41-
42- api.app.async.catchLinkClick(document.body, (link, { isExternal }) => {
43- if (isExternal) return openExternal(link)
44- nav.push(link)
45- })
46-
47- nav = HyperNav(
48- api.router.sync.router,
49- api.app.html.header
50- )
51-
52- const isOnboarded = api.settings.sync.get('onboarded')
53- if (isOnboarded)
54- nav.push({page: 'home'})
55- else {
56- nav.push({
57- page:'userEdit',
58- feed: api.keys.sync.id(),
59- callback: (err, didEdit) => {
60- if (err) throw new Error ('Error editing profile', err)
61-
62- if (didEdit)
63- api.settings.sync.set({ onboarded: true })
64-
65- nav.push({ page: 'home' })
66- }
67- })
68- }
69-
70- return nav
71- },
72- 'history.sync.push': (location) => nav.push(location),
73- 'history.sync.back': () => nav.back(),
74- 'history.obs.history': () => nav.history,
15+ return api.app.sync.nav()
16+ }
7517 })
7618 }
7719
78-
79-
app/html/context.jsView
@@ -51,33 +51,39 @@
5151 LevelTwoContext()
5252 ])
5353
5454 function LevelOneContext () {
55- const PAGES_UNDER_DISCOVER = ['blogIndex', 'blogShow', 'home']
55+ function isDiscoverContext (loc) {
56+ const PAGES_UNDER_DISCOVER = ['blogIndex', 'blogShow', 'home']
5657
58+ return PAGES_UNDER_DISCOVER.includes(location.page)
59+ || get(location, 'value.private') === undefined
60+ }
61+
5762 return h('div.level.-one', [
5863 // Nearby
5964 computed(nearby, n => !isEmpty(n) ? h('header', strings.peopleNearby) : null),
6065 map(nearby, feedId => Option({
6166 notifications: Math.random() > 0.7 ? Math.floor(Math.random()*9+1) : 0, // TODO
62- imageEl: api.about.html.avatar(feedId),
67+ imageEl: api.about.html.avatar(feedId, 'small'),
6368 label: api.about.obs.name(feedId),
6469 selected: location.feed === feedId,
6570 location: computed(recentPeersContacted, recent => {
6671 const lastMsg = recent[feedId]
6772 return lastMsg
6873 ? Object.assign(lastMsg, { feed: feedId })
6974 : { page: 'threadNew', feed: feedId }
7075 }),
71- })),
76+ }), { comparer: (a, b) => a === b }),
77+
7278 computed(nearby, n => !isEmpty(n) ? h('hr') : null),
7379
7480 // Discover
7581 Option({
7682 notifications: Math.floor(Math.random()*5+1),
7783 imageEl: h('i.fa.fa-binoculars'),
7884 label: strings.blogIndex.title,
79- selected: PAGES_UNDER_DISCOVER.includes(location.page),
85+ selected: isDiscoverContext(location),
8086 location: { page: 'blogIndex' },
8187 }),
8288
8389 // Recent Messages
@@ -92,9 +98,9 @@
9298 label: api.about.obs.name(feedId),
9399 selected: location.feed === feedId,
94100 location: Object.assign({}, lastMsg, { feed: feedId }) // TODO make obs?
95101 })
96- })
102+ }, { comparer: (a, b) => a === b })
97103 ])
98104 }
99105
100106 function LevelTwoContext () {
@@ -126,9 +132,9 @@
126132 label: api.message.html.subject(thread),
127133 selected: thread.key === root,
128134 location: Object.assign(thread, { feed: targetUser }),
129135 })
130- })
136+ }, { comparer: (a, b) => a === b })
131137 ])
132138 }
133139
134140 function Option ({ notifications = 0, imageEl, label, location, selected }) {
app/html/context.mcssView
@@ -1,9 +1,9 @@
11 Context {
22 flex-shrink: 0
33 flex-grow: 0
44 overflow: hidden
5- background-color: #fff
5+ $backgroundPrimaryText
66
77 display: flex
88
99 div.level {
@@ -74,9 +74,9 @@
7474 a img {
7575
7676 }
7777 i {
78- $avatarSmall
78+ $circleSmall
7979 $colorPrimary
8080 font-size: 1.3rem
8181 display: flex
8282 justify-content: center
app/html/thread.jsView
@@ -1,8 +1,9 @@
11 const nest = require('depnest')
22 const { h, Array: MutantArray, map, computed, when } = require('mutant')
33 const get = require('lodash/get')
44
5+// TODO - rename threadPrivate
56 exports.gives = nest('app.html.thread')
67
78 exports.needs = nest({
89 'about.html.avatar': 'first',
@@ -26,18 +27,18 @@
2627 const author = computed([chunk], chunk => get(chunk, '[0].value.author'))
2728
2829 return author() === myId
2930 ? h('div.my-chunk', [
30- h('div.avatar'),
31+ h('Avatar -small'),
3132 h('div.msgs', map(chunk, msg => {
3233 return h('div.msg-row', [
3334 h('div.spacer'),
3435 message(msg)
3536 ])
3637 }))
3738 ])
3839 : h('div.other-chunk', [
39- h('div.avatar', when(author, api.about.html.avatar(author()))),
40+ when(author, api.about.html.avatar(author()), 'small'),
4041 h('div.msgs', map(chunk, msg => {
4142 return h('div.msg-row', [
4243 message(msg),
4344 h('div.spacer')
@@ -96,11 +97,4 @@
9697 // TODO (mix) use lodash/get
9798 return msgA.value.author === msgB.value.author
9899 }
99100
100-
101-
102-
103-
104-
105-
106-
app/html/thread.mcssView
@@ -9,9 +9,9 @@
99 $chunk
1010
1111 justify-content: space-between
1212
13- div.avatar {
13+ img.Avatar {
1414 visibility: hidden
1515 }
1616
1717 div.msgs {
@@ -42,16 +42,9 @@
4242 $chunk {
4343 display: flex
4444 margin-bottom: .5rem
4545
46- div.avatar {
47- background-color: #333
48- $avatarSmall
49-
50- img {
51- $avatarSmall
52- }
53-
46+ img.Avatar {
5447 margin-right: 1rem
5548 }
5649
5750 div.msgs {
@@ -69,9 +62,9 @@
6962 }
7063
7164 div.msg {
7265 line-height: 1.2
73- background-color: #fff
66+ $backgroundPrimaryText
7467 padding: 0 .7rem
7568 border-radius: 4px
7669 }
7770 div.spacer {
app/html/blog-card.jsView
@@ -1,130 +1,0 @@
1-var nest = require('depnest')
2-var h = require('mutant/h')
3-var isString= require('lodash/isString')
4-var maxBy= require('lodash/maxBy')
5-var humanTime = require('human-time')
6-var marksum = require('markdown-summary')
7-var markdown = require('ssb-markdown')
8-var ref = require('ssb-ref')
9-var htmlEscape = require('html-escape')
10-
11-function renderEmoji (emoji, url) {
12- if (!url) return ':' + emoji + ':'
13- return `
14- <img
15- src="${htmlEscape(url)}"
16- alt=":${htmlEscape(emoji)}:"
17- title=":${htmlEscape(emoji)}:"
18- class="emoji"
19- >
20- `
21-}
22-
23-exports.gives = nest('app.html.blogCard', true)
24-
25-exports.needs = nest({
26- 'keys.sync.id': 'first',
27- 'history.sync.push': 'first',
28- 'about.obs.name': 'first',
29- 'about.html.avatar': 'first',
30- 'translations.sync.strings': 'first',
31- 'unread.sync.isUnread': 'first',
32- 'message.html.markdown': 'first',
33- 'blob.sync.url': 'first',
34- 'emoji.sync.url': 'first'
35-})
36-
37-exports.create = function (api) {
38-
39- //render markdown, but don't support patchwork@2 style mentions or custom emoji right now.
40- function render (source) {
41- return markdown.block(source, {
42- emoji: (emoji) => {
43- return renderEmoji(emoji, api.emoji.sync.url(emoji))
44- },
45- toUrl: (id) => {
46- if (ref.isBlob(id)) return api.blob.sync.url(id)
47- return id
48- },
49- imageLink: (id) => id
50- })
51- }
52-
53-
54- //render the icon for a thread.
55- //it would be more depjecty to split this
56- //into two methods, one in a private plugin
57- //one in a channel plugin
58- function threadIcon (msg) {
59- if(msg.value.private) {
60- const myId = api.keys.sync.id()
61-
62- return msg.value.content.recps
63- .map(link => isString(link) ? link : link.link)
64- .filter(link => link !== myId)
65- .map(api.about.html.avatar)
66- }
67- else if(msg.value.content.channel)
68- return '#'+msg.value.content.channel
69- }
70-
71-
72- // REFACTOR: move this to a template?
73- function buildRecipientNames (thread) {
74- const myId = api.keys.sync.id()
75-
76- return thread.value.content.recps
77- .map(link => isString(link) ? link : link.link)
78- .filter(link => link !== myId)
79- .map(api.about.obs.name)
80- }
81-
82- return nest('app.html.blogCard', (thread, opts = {}) => {
83- var strings = api.translations.sync.strings()
84- const { subject } = api.message.html
85-
86- if(!thread.value) return
87- if('string' !== typeof thread.value.content.text) return
88-
89- const lastReply = thread.replies && maxBy(thread.replies, r => r.timestamp)
90-
91- const onClick = opts.onClick || function () { api.history.sync.push(thread) }
92- const id = `${thread.key.replace(/[^a-z0-9]/gi, '')}` //-${JSON.stringify(opts)}`
93- // id is only here to help morphdom morph accurately
94-
95- const { content, author, timestamp } = thread.value
96-
97- var img = h('Thumbnail')
98- var m = /\!\[[^]+\]\(([^\)]+)\)/.exec(marksum.image(content.text))
99- if(m) {
100- //Hey this works! fit an image into a specific size (see thread-card.mcss)
101- //centered, and scaled to fit the square (works with both landscape and portrait!)
102- //This is functional css not opinionated css, so all embedded.
103- img.style = 'background-image: url("'+api.blob.sync.url(m[1])+'"); background-position:center; background-size: cover;'
104- }
105-
106- const title = render(marksum.title(content.text))
107- const summary = render(marksum.summary(content.text))
108-
109- const className = thread.unread ? '-unread': ''
110-
111- return h('BlogCard', { id, className }, [
112- h('div.context', [
113- api.about.html.avatar(author),
114- h('div.name', api.about.obs.name(author)),
115- h('div.timeago', humanTime(new Date(timestamp))),
116- ]),
117- h('div.content', {'ev-click': onClick}, [
118- img,
119- h('div.text', [
120- h('h2', {innerHTML: title}),
121- content.channel
122- ? h('Button -channel', '#'+content.channel)
123- : '',
124- h('div.summary', {innerHTML: summary})
125- ])
126- ])
127- ])
128- })
129-}
130-
app/html/blogCard.jsView
@@ -1,0 +1,130 @@
1+var nest = require('depnest')
2+var h = require('mutant/h')
3+var isString= require('lodash/isString')
4+var maxBy= require('lodash/maxBy')
5+var marksum = require('markdown-summary')
6+var markdown = require('ssb-markdown')
7+var ref = require('ssb-ref')
8+var htmlEscape = require('html-escape')
9+
10+function renderEmoji (emoji, url) {
11+ if (!url) return ':' + emoji + ':'
12+ return `
13+ <img
14+ src="${htmlEscape(url)}"
15+ alt=":${htmlEscape(emoji)}:"
16+ title=":${htmlEscape(emoji)}:"
17+ class="emoji"
18+ >
19+ `
20+}
21+
22+exports.gives = nest('app.html.blogCard', true)
23+
24+exports.needs = nest({
25+ 'keys.sync.id': 'first',
26+ 'history.sync.push': 'first',
27+ 'about.obs.name': 'first',
28+ 'about.html.avatar': 'first',
29+ 'translations.sync.strings': 'first',
30+ 'unread.sync.isUnread': 'first',
31+ // 'message.html.markdown': 'first',
32+ 'message.html.timeago': 'first',
33+ 'blob.sync.url': 'first',
34+ 'emoji.sync.url': 'first'
35+})
36+
37+exports.create = function (api) {
38+
39+ //render markdown, but don't support patchwork@2 style mentions or custom emoji right now.
40+ function render (source) {
41+ return markdown.block(source, {
42+ emoji: (emoji) => {
43+ return renderEmoji(emoji, api.emoji.sync.url(emoji))
44+ },
45+ toUrl: (id) => {
46+ if (ref.isBlob(id)) return api.blob.sync.url(id)
47+ return id
48+ },
49+ imageLink: (id) => id
50+ })
51+ }
52+
53+
54+ //render the icon for a blog.
55+ //it would be more depjecty to split this
56+ //into two methods, one in a private plugin
57+ //one in a channel plugin
58+ function blogIcon (msg) {
59+ if(msg.value.private) {
60+ const myId = api.keys.sync.id()
61+
62+ return msg.value.content.recps
63+ .map(link => isString(link) ? link : link.link)
64+ .filter(link => link !== myId)
65+ .map(link => api.about.html.avatar)
66+ }
67+ else if(msg.value.content.channel)
68+ return '#'+msg.value.content.channel
69+ }
70+
71+
72+ // REFACTOR: move this to a template?
73+ function buildRecipientNames (blog) {
74+ const myId = api.keys.sync.id()
75+
76+ return blog.value.content.recps
77+ .map(link => isString(link) ? link : link.link)
78+ .filter(link => link !== myId)
79+ .map(api.about.obs.name)
80+ }
81+
82+ return nest('app.html.blogCard', (blog, opts = {}) => {
83+ var strings = api.translations.sync.strings()
84+
85+ if(!blog.value) return
86+ if('string' !== typeof blog.value.content.text) return
87+
88+ const lastReply = blog.replies && maxBy(blog.replies, r => r.timestamp)
89+
90+ const goToBlog = () => api.history.sync.push(blog)
91+ const onClick = opts.onClick || goToBlog
92+ const id = `${blog.key.replace(/[^a-z0-9]/gi, '')}` //-${JSON.stringify(opts)}`
93+ // id is only here to help morphdom morph accurately
94+
95+ const { content, author } = blog.value
96+
97+ var img = h('Thumbnail')
98+ var m = /\!\[[^]+\]\(([^\)]+)\)/.exec(marksum.image(content.text))
99+ if(m) {
100+ //Hey this works! fit an image into a specific size (see blog-card.mcss)
101+ //centered, and scaled to fit the square (works with both landscape and portrait!)
102+ //This is functional css not opinionated css, so all embedded.
103+ img.style = 'background-image: url("'+api.blob.sync.url(m[1])+'"); background-position:center; background-size: cover;'
104+ }
105+
106+ const title = render(marksum.title(content.text))
107+ const summary = render(marksum.summary(content.text))
108+
109+ const className = blog.unread ? '-unread': ''
110+
111+ return h('BlogCard', { id, className, 'ev-click': onClick }, [
112+ h('div.context', [
113+ api.about.html.avatar(author, 'tiny'),
114+ h('div.name', api.about.obs.name(author)),
115+ api.message.html.timeago(blog)
116+ ]),
117+ h('div.content', [
118+ img,
119+ h('div.text', [
120+ h('h2', {innerHTML: title}),
121+ content.channel
122+ ? h('Button -channel', '#'+content.channel)
123+ : '',
124+ h('div.summary', {innerHTML: summary})
125+ ])
126+ ])
127+ ])
128+ })
129+}
130+
app/html/blog-card.mcssView
@@ -1,84 +1,0 @@
1-BlogCard {
2- padding: 1rem
3- background-color: #fff
4-
5- border: 1px solid #fff
6- transition: all .5s ease
7-
8- :hover {
9- border: 1px solid gainsboro
10- box-shadow: gainsboro 2px 2px 10px
11- }
12-
13- display: flex
14- flex-direction: column
15-
16- div.context {
17- font-size: .8rem
18- margin-bottom: 1rem
19-
20- display: flex
21- align-items: center
22-
23- div.Link {
24- height: 2rem
25- img.Avatar {
26- width: 2rem
27- height: 2rem
28- }
29- }
30-
31- div.name {
32- margin-right: 1rem
33- }
34- div.timeago {
35- $colorSubtle
36- }
37- }
38-
39- div.content {
40- display: flex
41- flex-direction: row
42- flex-grow: 1
43-
44- cursor: pointer
45-
46-
47- div.Thumbnail {
48- margin-right: 1rem
49- }
50-
51- div.text {
52- display: flex
53- flex-wrap: wrap
54-
55- h2 {
56- $markdownLarge
57- margin: 0 .5rem 0 0
58- }
59- div.Button.-channel {}
60- div.summary {
61- flex-basis: 100%
62- }
63- }
64- }
65-
66- -unread {
67- div.content {
68- background-color: #fff
69-
70- div.subject {
71- $markdownBold
72- }
73- }
74- }
75-}
76-
77-Thumbnail {
78- border-radius: .5rem
79- min-width: 8rem
80- min-height: 6rem
81- width: 8rem
82- height: 6rem
83-}
84-
app/html/blogCard.mcssView
@@ -1,0 +1,70 @@
1+BlogCard {
2+ padding: 1rem
3+ $backgroundPrimaryText
4+
5+ border: 1px solid #fff
6+ transition: all .5s ease
7+
8+ :hover {
9+ border: 1px solid gainsboro
10+ box-shadow: gainsboro 2px 2px 10px
11+ }
12+
13+ display: flex
14+ flex-direction: column
15+
16+ div.context {
17+ font-size: .8rem
18+ margin-bottom: 1rem
19+
20+ display: flex
21+ align-items: center
22+
23+ img.Avatar {
24+ margin-right: .5rem
25+ }
26+
27+ div.name {
28+ margin-right: 1rem
29+ }
30+
31+ div.Timeago {}
32+ }
33+
34+ div.content {
35+ display: flex
36+ flex-direction: row
37+ flex-grow: 1
38+
39+ cursor: pointer
40+
41+
42+ div.Thumbnail {
43+ margin-right: 1rem
44+ }
45+
46+ div.text {
47+ display: flex
48+ flex-wrap: wrap
49+
50+ h2 {
51+ $markdownLarge
52+ margin: 0 .5rem 0 0
53+ }
54+ div.Button.-channel {}
55+ div.summary {
56+ flex-basis: 100%
57+ }
58+ }
59+ }
60+
61+}
62+
63+Thumbnail {
64+ border-radius: .5rem
65+ min-width: 8rem
66+ min-height: 6rem
67+ width: 8rem
68+ height: 6rem
69+}
70+
app/html/comments.jsView
@@ -1,0 +1,140 @@
1+const nest = require('depnest')
2+const { h, Array: MutantArray, Value, map, computed, when, resolve } = require('mutant')
3+const get = require('lodash/get')
4+
5+exports.gives = nest('app.html.comments')
6+
7+exports.needs = nest({
8+ 'about.html.avatar': 'first',
9+ 'about.obs.name': 'first',
10+ 'backlinks.obs.for': 'first',
11+ 'feed.obs.thread': 'first',
12+ '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+})
19+
20+exports.create = (api) => {
21+ return nest('app.html.comments', comments)
22+
23+ function comments (root) {
24+ const { messages, channel, lastId: branch } = api.feed.obs.thread(root)
25+
26+ // TODO - move this up into Patchcore
27+ const messagesTree = computed(messages, msgs => {
28+ return msgs
29+ .filter(msg => forkOf(msg) === undefined)
30+ .map(threadMsg => {
31+ const nestedReplies = msgs.filter(msg => forkOf(msg) === threadMsg.key)
32+ threadMsg.replies = nestedReplies
33+ return threadMsg
34+ })
35+ })
36+
37+ const meta = {
38+ type: 'post',
39+ root,
40+ branch,
41+ channel
42+ }
43+ const twoComposers = computed(messages, messages => {
44+ return messages.length > 5
45+ })
46+ const { compose } = api.message.html
47+
48+
49+ return h('Comments', [
50+ when(twoComposers, compose({ meta, shrink: true, canAttach: false })),
51+ map(messagesTree, msg => Comment(msg, root, branch)),
52+ compose({ meta, shrink: false, canAttach: false }),
53+ ])
54+ }
55+
56+ function Comment (msgObs, root, branch) {
57+ const msg = resolve(msgObs)
58+
59+ const raw = get(msg, 'value.content.text')
60+ var className = api.unread.sync.isUnread(msg) ? ' -unread' : ' -read'
61+ api.unread.sync.markRead(msg)
62+
63+ if (!get(msg, 'value.content.root')) return
64+
65+ const { author, content } = msg.value
66+
67+ // // TODO - move this upstream into patchcore:feed.obs.thread ??
68+ // // OR change strategy to use forks
69+ // const backlinks = api.backlinks.obs.for(msg.key)
70+ // const nestedReplies = computed(backlinks, backlinks => {
71+ // return backlinks.filter(backlinker => {
72+ // const { type, root } = backlinker.value.content
73+ // return type === 'post' && root === msg.key
74+ // })
75+ // })
76+
77+ var nestedReplyCompose = Value(false)
78+ const toggleCompose = () => nestedReplyCompose.set(!nestedReplyCompose())
79+ const nestedReplyComposer = api.message.html.compose({
80+ meta: {
81+ type: 'post',
82+ root,
83+ fork: msg.key,
84+ branch,
85+ channel: content.channel
86+ },
87+ shrink: false,
88+ canAttach: false,
89+ canPreview: false
90+ }, toggleCompose)
91+
92+ return h('Comment', { className }, [
93+ h('div.left', api.about.html.avatar(author, 'tiny')),
94+ h('div.right', [
95+ h('section.context', [
96+ h('div.name', api.about.obs.name(author)),
97+ api.message.html.timeago(msg)
98+ ]),
99+ h('section.content', api.message.html.markdown(raw)),
100+ when(msgObs.replies,
101+ h('section.replies',
102+ map(msgObs.replies, NestedComment)
103+ )
104+ ),
105+ h('section.actions', [
106+ h('div.reply', { 'ev-click': toggleCompose }, [
107+ h('i.fa.fa-commenting-o'),
108+ ]),
109+ api.message.html.likes(msg)
110+ ]),
111+ when(nestedReplyCompose, nestedReplyComposer),
112+ ])
113+ ])
114+ }
115+
116+ function NestedComment (msgObs) {
117+ const msg = resolve(msgObs)
118+ const raw = get(msg, 'value.content.text')
119+ if (!raw) return
120+
121+ const { author } = msg.value
122+
123+ return h('Comment -nested', [
124+ h('div.left'),
125+ h('div.right', [
126+ h('section.context', [
127+ h('div.name', api.about.obs.name(author)),
128+ api.message.html.timeago(msg)
129+ ]),
130+ h('section.content', api.message.html.markdown(raw)),
131+ ])
132+ ])
133+
134+ api.message.html.markdown(raw)
135+ }
136+}
137+
138+function forkOf (msg) {
139+ return get(msg, 'value.content.fork')
140+}
app/html/comments.mcssView
@@ -1,0 +1,92 @@
1+Comments {
2+ margin: 0 1.5rem
3+
4+ div.Comment {}
5+
6+ div.Compose {
7+ margin-bottom: 1.5rem
8+ }
9+}
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+ border-bottom: 1px solid gainsboro
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: .9rem
40+ line-height: 1.4
41+ div.Markdown {}
42+ }
43+
44+ section.actions {
45+ font-size: 1.2rem
46+ margin-right: .5rem
47+
48+ display: flex
49+ justify-content: flex-end
50+ align-items: baseline
51+
52+ div.reply {
53+ cursor: pointer
54+
55+ margin-right: 1.5rem
56+ i.fa {}
57+ }
58+
59+ div.Likes {
60+ min-width: 2.5rem
61+
62+ display: flex
63+ align-items: center
64+
65+ i.fa { margin-right: .3rem }
66+ div.count {}
67+ }
68+ }
69+
70+ div.Compose {
71+ margin: 1rem 0
72+ }
73+ }
74+}
75+
76+Comment -nested {
77+ padding: 1rem 1rem 0 1rem
78+ $backgroundPrimaryText
79+ $roundTop
80+ $roundBottom
81+
82+ margin-bottom: 1rem
83+
84+ div.left { margin: 0 }
85+ div.right {
86+ padding: 0
87+ border: 0
88+ margin: 0
89+
90+ }
91+}
92+
app/index.jsView
@@ -3,17 +3,19 @@
33 catchLinkClick: require('./async/catch-link-click'),
44 },
55 html: {
66 app: require('./html/app'),
7+ comments: require('./html/comments'),
78 context: require('./html/context'),
89 header: require('./html/header'),
910 thread: require('./html/thread'),
1011 link: require('./html/link'),
11- blogCard: require('./html/blog-card'),
12+ blogCard: require('./html/blogCard'),
1213 },
1314 page: {
1415 blogIndex: require('./page/blogIndex'),
1516 blogNew: require('./page/blogNew'),
17+ blogShow: require('./page/blogShow'),
1618 error: require('./page/error'),
1719 settings: require('./page/settings'),
1820 // channel: require('./page/channel'),
1921 // image: require('./page/image'),
@@ -27,7 +29,15 @@
2729 // userFind: require('./page/userFind'),
2830 userShow: require('./page/userShow'),
2931 threadNew: require('./page/threadNew'),
3032 threadShow: require('./page/threadShow'),
33+ },
34+ sync: {
35+ initialize: {
36+ clickHandler: require('./sync/initialize/clickHandler'),
37+ styles: require('./sync/initialize/styles'),
38+ suggests: require('./sync/initialize/suggests'),
39+ },
40+ navHistory: require('./sync/nav-history'),
3141 }
3242 }
3343
app/page/blogIndex.jsView
@@ -98,9 +98,9 @@
9898 groupedThreads.map(thread => {
9999 const { recps, channel } = thread.value.content
100100 var onClick
101101 if (channel && !recps)
102- onClick = (ev) => api.history.sync.push({ key: thread.key, page: 'blogShow' })
102+ onClick = (ev) => api.history.sync.push(Object.assign({}, thread, { page: 'blogShow' }))
103103
104104 return api.app.html.blogCard(thread, { onClick })
105105 })
106106 )
app/page/error.jsView
@@ -13,13 +13,10 @@
1313 return nest('app.page.error', error)
1414
1515 function error (location) {
1616 return h('Page -error', {title: strings.error}, [
17- strings.errorNotFound,
17+ h('div.message', strings.errorNotFound),
1818 h('pre', [JSON.stringify(location, null, 2)])
1919 ])
2020 }
2121 }
2222
23-
24-
25-
app/page/groupFind.mcssView
@@ -1,9 +1,9 @@
11 Page -groupFind {
22 div.content {
33 $maxWidthSmaller
44 div.search {
5- background-color: #fff
5+ $backgroundPrimaryText
66
77 margin-bottom: 1rem
88
99 display: flex
@@ -29,17 +29,17 @@
2929 $maxWidthSmaller
3030
3131 div.Link {
3232 div.result {
33- background-color: #fff
33+ $backgroundPrimaryText
3434
3535 padding: .5rem
3636
3737 display: flex
3838 align-items: center
3939
4040 img {
41- $avatarSmall
41+ $circleSmall
4242 margin-right: 1rem
4343 }
4444
4545 div.alias {
app/page/threadNew.mcssView
@@ -33,9 +33,9 @@
3333 padding: .3rem
3434 min-width: 5rem
3535 $borderSubtle
3636 border-radius: 6rem
37- background-color: #fff
37+ $backgroundPrimaryText
3838
3939 margin-right: 1rem
4040
4141 display: flex
app/page/userFind.mcssView
@@ -1,9 +1,9 @@
11 Page -userFind {
22 div.content {
33 $maxWidthSmaller
44 div.search {
5- background-color: #fff
5+ $backgroundPrimaryText
66
77 margin-bottom: 1rem
88
99 display: flex
@@ -29,17 +29,17 @@
2929 $maxWidthSmaller
3030
3131 div.Link {
3232 div.result {
33- background-color: #fff
33+ $backgroundPrimaryText
3434
3535 padding: .5rem
3636
3737 display: flex
3838 align-items: center
3939
4040 img {
41- $avatarSmall
41+ $circleSmall
4242 margin-right: 1rem
4343 }
4444
4545 div.alias {
app/page/userShow.jsView
@@ -5,15 +5,14 @@
55
66 exports.gives = nest('app.page.userShow')
77
88 exports.needs = nest({
9- 'about.html.image': 'first',
9+ 'about.html.avatar': 'first',
1010 'about.obs.name': 'first',
1111 'app.html.link': 'first',
1212 'app.html.blogCard': 'first',
13- 'contact.async.follow': 'first',
14- 'contact.async.unfollow': 'first',
15- 'contact.obs.followers': 'first',
13+ 'contact.html.follow': 'first',
14+ 'feed.pull.private': 'first',
1615 'sbot.pull.userFeed': 'first',
1716 'keys.sync.id': 'first',
1817 'translations.sync.strings': 'first',
1918 })
@@ -28,11 +27,11 @@
2827 const name = api.about.obs.name(feed)
2928
3029 const strings = api.translations.sync.strings()
3130
32- const { followers } = api.contact.obs
31+ // const { followers } = api.contact.obs
3332
34- const youFollowThem = computed(followers(feed), followers => followers.includes(myId))
33+ // const youFollowThem = computed(followers(feed), followers => followers.includes(myId))
3534 // const theyFollowYou = computed(followers(myId), followers => followers.includes(feed))
3635 // const youAreFriends = computed([youFollowThem, theyFollowYou], (a, b) => a && b)
3736
3837 // const ourRelationship = computed(
@@ -42,16 +41,8 @@
4241 // if (theyFollowYou) return strings.userShow.state.theyFollow
4342 // if (youFollowThem) return strings.userShow.state.youFollow
4443 // }
4544 // )
46- const { unfollow, follow } = api.contact.async
47- const followButton = when(followers(myId).sync,
48- when(youFollowThem,
49- h('Button -primary', { 'ev-click': () => unfollow(feed) }, strings.userShow.action.unfollow),
50- h('Button -primary', { 'ev-click': () => follow(feed) }, strings.userShow.action.follow)
51- ),
52- h('Button', { disabled: 'disabled' }, strings.loading )
53- )
5445
5546 const Link = api.app.html.link
5647 const userEditButton = Link({ page: 'userEdit', feed }, h('i.fa.fa-pencil'))
5748 const directMessageButton = Link({ page: 'threadNew', feed }, h('Button', strings.userShow.action.directMessage))
@@ -70,18 +61,18 @@
7061
7162 return h('Page -userShow', {title: name}, [
7263 h('div.content', [
7364 h('section.about', [
74- api.about.html.image(feed),
65+ api.about.html.avatar(feed, 'large'),
7566 h('h1', [
7667 name,
7768 feed === myId // Only expose own profile editing right now
7869 ? userEditButton
7970 : ''
8071 ]),
8172 feed !== myId
8273 ? h('div.actions', [
83- h('div.friendship', followButton),
74+ api.contact.html.follow(feed),
8475 h('div.directMessage', directMessageButton)
8576 ])
8677 : '',
8778 ]),
app/page/userShow.mcssView
@@ -8,22 +8,24 @@
88 flex-direction: column
99 align-items: center
1010
1111 img.Avatar {
12- width: 5rem
13- height: 5rem
14- border-radius: 3rem
1512 }
1613
1714 h1 {
1815 font-weight: 300
1916 font-size: 1rem
17+
18+ display: flex
19+ div.Link {
20+ margin-left: .5rem
21+ }
2022 }
2123
2224 div.actions {
2325 display: flex
2426
25- div.friendship {
27+ div.Follow {
2628 margin-right: 1rem
2729 }
2830
2931 div.directMessage {
app/page/blogShow.jsView
@@ -1,0 +1,62 @@
1+const nest = require('depnest')
2+const { h, computed, when } = require('mutant')
3+const { title: getTitle } = require('markdown-summary')
4+const last = require('lodash/last')
5+const get = require('lodash/get')
6+
7+exports.gives = nest('app.page.blogShow')
8+
9+exports.needs = nest({
10+ 'about.html.avatar': 'first',
11+ 'about.obs.name': 'first',
12+ 'app.html.comments': 'first',
13+ 'app.html.context': 'first',
14+ 'contact.html.follow': 'first',
15+ 'message.html.channel': 'first',
16+ 'message.html.markdown': 'first',
17+ 'message.html.timeago': 'first',
18+ 'feed.obs.thread': 'first'
19+})
20+
21+exports.create = (api) => {
22+ return nest('app.page.blogShow', blogShow)
23+
24+ function blogShow (blogMsg) {
25+ // blogMsg = a thread (message, may be decorated with replies)
26+
27+ const { author, content } = blogMsg.value
28+
29+ const blog = content.text
30+ const title = api.message.html.markdown(content.title || getTitle(blog))
31+
32+ const comments = api.app.html.comments(blogMsg.key)
33+
34+ const { lastId: branch } = api.feed.obs.thread(blogMsg.key)
35+
36+ const { timeago, channel, markdown, compose } = api.message.html
37+
38+ return h('Page -blogShow', [
39+ api.app.html.context({ page: 'discover' }), // HACK to highlight discover
40+ h('div.content', [
41+ h('header', [
42+ h('div.blog', [
43+ h('h1', title),
44+ timeago(blogMsg),
45+ channel(blogMsg)
46+ ]),
47+ h('div.author', [
48+ h('div.leftCol', api.about.html.avatar(author, 'medium')),
49+ h('div.rightCol', [
50+ h('div.name', api.about.obs.name(author)),
51+ api.contact.html.follow(author)
52+ ]),
53+ ])
54+ ]),
55+ h('div.break', h('hr')),
56+ h('section.blog', markdown(blog)),
57+ comments,
58+ ]),
59+ ])
60+ }
61+}
62+
app/page/blogShow.mcssView
@@ -1,0 +1,79 @@
1+Page -blogShow {
2+ // div.context {}
3+
4+ div.content {
5+ header, div, section {
6+ $maxWidth
7+ margin-left: auto
8+ margin-right: auto
9+ }
10+
11+ header {
12+ $backgroundPrimaryText
13+ padding: 1rem
14+
15+ display: flex
16+
17+ div.blog {
18+ display: flex
19+ flex-wrap: wrap
20+ flex-grow: 1
21+
22+ h1 {
23+ flex-basis: 100%
24+
25+ $markdownLarge
26+ font-size: 2rem
27+ font-weight: 300
28+ margin: 0 0 1rem 0
29+ }
30+
31+ div.Timeago {
32+ flex-basis: 100%
33+ margin-bottom: .6rem
34+ }
35+
36+ div.Button.-channel {}
37+ }
38+
39+ div.author {
40+ display: flex
41+
42+ div.leftCol {
43+ margin-right: 1rem
44+ img.Avatar {}
45+ }
46+
47+ div.rightCol {
48+ div.name {
49+ font-size: .9rem
50+ margin-bottom: .5rem
51+ }
52+ div.Button.-follow {} // extract
53+ }
54+ }
55+ }
56+
57+ div.break {
58+ padding: 0 1rem
59+ $backgroundPrimaryText
60+
61+ hr {
62+ margin: 0
63+ border: none
64+ border-bottom: 1px solid gainsboro
65+ }
66+ }
67+
68+ section.blog {
69+ $backgroundPrimaryText
70+ padding: 1rem
71+
72+ margin-bottom: 1.5rem
73+ }
74+
75+ div.Comments {
76+ }
77+ }
78+}
79+
app/page/error.mcssView
@@ -1,0 +1,6 @@
1+Page -error {
2+ padding: 1rem
3+
4+ flex-direction: column
5+}
6+
app/sync/initialize/clickHandler.jsView
@@ -1,0 +1,23 @@
1+const nest = require('depnest')
2+
3+exports.gives = nest('app.sync.initialize')
4+
5+exports.needs = nest({
6+ 'app.async.catchLinkClick': 'first',
7+ 'history.sync.push': 'first',
8+})
9+
10+exports.create = (api) => {
11+ return nest({
12+ 'app.sync.initialize': function initializeClickHandling () {
13+ const target = document.body
14+
15+ api.app.async.catchLinkClick(target, (link, { isExternal }) => {
16+ if (isExternal) return openExternal(link)
17+
18+ api.history.sync.push(link)
19+ })
20+ }
21+ })
22+}
23+
app/sync/initialize/styles.jsView
@@ -1,0 +1,19 @@
1+const nest = require('depnest')
2+const insertCss = require('insert-css')
3+const values = require('lodash/values')
4+
5+exports.gives = nest('app.sync.initialize')
6+
7+exports.needs = nest({
8+ 'styles.css': 'reduce'
9+})
10+
11+exports.create = (api) => {
12+ return nest({
13+ 'app.sync.initialize': function initializeStyles () {
14+ const css = values(api.styles.css()).join('\n')
15+ insertCss(css)
16+ }
17+ })
18+}
19+
app/sync/initialize/suggests.jsView
@@ -1,0 +1,20 @@
1+const nest = require('depnest')
2+
3+exports.gives = nest('app.sync.initialize')
4+
5+exports.needs = nest({
6+ 'about.async.suggest': 'first',
7+ 'channel.async.suggest': 'first'
8+})
9+
10+exports.create = (api) => {
11+ var nav = null
12+
13+ return nest({
14+ 'app.sync.initialize': function initializeSuggests () {
15+ api.about.async.suggest()
16+ api.channel.async.suggest()
17+ }
18+ })
19+}
20+
app/sync/nav-history.jsView
@@ -1,0 +1,57 @@
1+const nest = require('depnest')
2+const HyperNav = require('hyper-nav')
3+const { h } = require('mutant')
4+
5+exports.gives = nest({
6+ 'app.sync.nav': true,
7+ 'history.obs.history': true,
8+ 'history.sync.push': true,
9+ 'history.sync.back': true,
10+})
11+
12+exports.needs = nest({
13+ 'app.html.header': 'first',
14+ 'keys.sync.id': 'first',
15+ 'router.sync.router': 'first',
16+ 'settings.sync.get': 'first',
17+ 'settings.sync.set': 'first',
18+})
19+
20+exports.create = (api) => {
21+ var nav = null
22+
23+ return nest({
24+ 'app.sync.nav': function getNav () {
25+ if (nav) return nav
26+
27+ nav = HyperNav(
28+ api.router.sync.router,
29+ api.app.html.header
30+ )
31+
32+ const isOnboarded = api.settings.sync.get('onboarded')
33+ if (isOnboarded)
34+ nav.push({page: 'home'})
35+ else {
36+ nav.push({
37+ page:'userEdit',
38+ feed: api.keys.sync.id(),
39+ callback: (err, didEdit) => {
40+ if (err) throw new Error ('Error editing profile', err)
41+
42+ if (didEdit)
43+ api.settings.sync.set({ onboarded: true })
44+
45+ nav.push({ page: 'home' })
46+ }
47+ })
48+ }
49+
50+ return nav
51+ },
52+ 'history.sync.push': (location) => nav.push(location),
53+ 'history.sync.back': () => nav.back(),
54+ 'history.obs.history': () => nav.history,
55+ })
56+}
57+
main.jsView
@@ -18,8 +18,9 @@
1818 {
1919 about: require('./about'),
2020 app: require('./app'),
2121 blob: require('./blob'),
22+ contact: require('./contact'),
2223 //config: require('./ssb-config'),
2324 config: require('./config'),
2425 // group: require('./group'),
2526 message: require('./message'),
message/html/compose.jsView
@@ -21,9 +21,17 @@
2121
2222 exports.create = function (api) {
2323 return nest('message.html.compose', compose)
2424
25- function compose ({ shrink = true, meta, prepublish, placeholder }, cb) {
25+ function compose (options, cb) {
26+ var {
27+ meta, // required
28+ placeholder,
29+ shrink = true,
30+ canAttach = true, canPreview = true,
31+ prepublish
32+ } = options
33+
2634 const strings = api.translations.sync.strings()
2735 const getProfileSuggestions = api.about.async.suggest()
2836 const getChannelSuggestions = api.channel.async.suggest()
2937 const getEmojiSuggestions = api.emoji.async.suggest()
@@ -80,12 +88,12 @@
8088 }
8189 // if fileInput is null, send button moves to the left side
8290 // and we don't want that.
8391 else
84- fileInput = h('input', { style: {visibility: 'hidden'}})
92+ fileInput = h('input', { style: {visibility: 'hidden'} })
8593
8694 var showPreview = Value(false)
87- var previewBtn = h('Button',
95+ var previewBtn = h('Button -preview',
8896 {
8997 className: when(showPreview, '-primary'),
9098 'ev-click': () => showPreview.set(!showPreview())
9199 },
@@ -95,10 +103,10 @@
95103
96104 var publishBtn = h('Button -primary', { 'ev-click': publish }, strings.sendMessage)
97105
98106 var actions = h('section.actions', [
99- fileInput,
100- previewBtn,
107+ canAttach ? fileInput : '',
108+ canPreview ? previewBtn : '',
101109 publishBtn
102110 ])
103111
104112 var composer = h('Compose', {
message/html/compose.mcssView
@@ -6,36 +6,23 @@
66
77 textarea {
88 $fontBasic
99
10- padding: .6rem
11- $borderSubtle
12- border-top-left-radius: 0
13- border-top-right-radius: 0
14- }
10+ padding: 1rem
11+ border-radius: 1rem
12+ border: none
13+ margin-bottom: .5rem
1514
16- input.channel {
17- $borderSubtle
18- border-bottom: none
19- border-bottom-left-radius: 0
20- border-bottom-right-radius: 0
21- padding: .5rem
22-
2315 :focus {
2416 outline: none
25- box-shadow: none
2617 }
27- :disabled {
28- background-color: #f1f1f1
29- cursor: not-allowed
30- }
3118 }
3219
3320 section.actions {
3421 display: flex
3522 flex-direction: row
3623 align-items: baseline
37- justify-content: space-between
24+ justify-content: flex-end
3825
3926 margin-top: .4rem
4027
4128 input { flex-grow: 1 }
message/html/channel.jsView
@@ -1,0 +1,24 @@
1+const nest = require('depnest')
2+const { h } = require('mutant')
3+
4+exports.gives = nest('message.html.channel')
5+
6+exports.needs = nest({
7+ 'history.sync.push': 'first'
8+})
9+
10+exports.create = function (api) {
11+ return nest('message.html.channel', channel)
12+
13+ function channel (msg) {
14+ const { channel } = msg.value.content
15+
16+ if (!channel) return
17+
18+ return h('Button -channel', {
19+ // 'ev-click': () => history.sync.push({ page: 'channelIndex', channel }) // TODO
20+ }, channel)
21+ }
22+}
23+
24+
message/html/likes.jsView
@@ -1,0 +1,44 @@
1+var { h, computed, when } = require('mutant')
2+var nest = require('depnest')
3+
4+exports.needs = nest({
5+ 'keys.sync.id': 'first',
6+ 'message.obs.likes': 'first',
7+ 'sbot.async.publish': 'first'
8+})
9+
10+exports.gives = nest('message.html.likes')
11+
12+exports.create = (api) => {
13+ return nest('message.html.likes', function likes (msg) {
14+ var id = api.keys.sync.id()
15+ var likes = api.message.obs.likes(msg.key)
16+ var iLike = computed(likes, likes => likes.includes(id))
17+ var count = computed(likes, likes => likes.length ? likes.length : '')
18+
19+ return h('Likes', {'ev-click': () => publishLike(msg, !iLike())}, [
20+ h('i.fa', { className: when(iLike, 'fa-heart', 'fa-heart-o') }),
21+ h('div.count', count)
22+ ])
23+ })
24+
25+ function publishLike (msg, status = true) {
26+ var like = status ? {
27+ type: 'vote',
28+ channel: msg.value.content.channel,
29+ vote: { link: msg.key, value: 1, expression: 'Like' }
30+ } : {
31+ type: 'vote',
32+ channel: msg.value.content.channel,
33+ vote: { link: msg.key, value: 0, expression: 'Unlike' }
34+ }
35+ if (msg.value.content.recps) {
36+ like.recps = msg.value.content.recps.map(function (e) {
37+ return e && typeof e !== 'string' ? e.link : e
38+ })
39+ like.private = true
40+ }
41+ api.sbot.async.publish(like)
42+ }
43+}
44+
message/html/likes.mcssView
@@ -1,0 +1,4 @@
1+Likes {
2+ cursor: pointer
3+}
4+
message/html/timeago.jsView
@@ -1,0 +1,18 @@
1+const nest = require('depnest')
2+const { h } = require('mutant')
3+const humanTime = require('human-time')
4+
5+exports.gives = nest('message.html.timeago')
6+
7+exports.create = function (api) {
8+ return nest('message.html.timeago', timeago)
9+
10+ function timeago (msg) {
11+ const { timestamp } = msg.value
12+
13+ // TODO implement the light auto-updating of this app-wide
14+ // perhaps by adding an initializer which sweeps for data-timestamp elements and updates them
15+ return h('Timeago', humanTime(new Date(timestamp)))
16+ }
17+}
18+
message/html/timeago.mcssView
@@ -1,0 +1,5 @@
1+Timeago {
2+ font-size: .8rem
3+ $colorSubtle
4+}
5+
message/index.jsView
@@ -2,9 +2,12 @@
22 async: {
33 publish: require('./async/publish'),
44 },
55 html: {
6+ channel: require('./html/channel'),
67 compose: require('./html/compose'),
7- subject: require('./html/subject')
8+ likes: require('./html/likes'),
9+ subject: require('./html/subject'),
10+ timeago: require('./html/timeago')
811 }
912 }
1013
router/sync/routes.jsView
@@ -8,8 +8,9 @@
88 exports.needs = nest({
99 'app.page.error': 'first',
1010 'app.page.blogIndex': 'first',
1111 'app.page.blogNew': 'first',
12+ 'app.page.blogShow': 'first',
1213 'app.page.settings': 'first',
1314 // 'app.page.channel': 'first',
1415 // 'app.page.groupFind': 'first',
1516 // 'app.page.groupIndex': 'first',
@@ -30,9 +31,22 @@
3031 // route format: [ routeValidator, routeFunction ]
3132
3233 const routes = [
3334
34- // Thread pages
35+ // Blog pages
36+ [ location => location.page === 'home', pages.blogIndex ],
37+ [ location => location.page === 'discovery', pages.blogIndex ],
38+ [ location => location.page === 'blogIndex', pages.blogIndex ],
39+ [ location => location.page === 'blogNew', pages.blogNew ],
40+ [ location => location.page === 'blogShow', pages.blogShow ],
41+ [ location => isMsg(location.key) && get(location, 'value.content.type') === 'blog', pages.blogShow ],
42+ [ location => {
43+ return isMsg(location.key)
44+ && get(location, 'value.content.type') === 'post'
45+ && !get(location, 'value.private') // treats public posts as 'blogs'
46+ }, pages.blogShow ],
47+
48+ // Private Thread pages
3549 // [ location => location.page === 'threadNew' && location.channel, pages.threadNew ],
3650 [ location => location.page === 'threadNew' && isFeed(location.feed), pages.threadNew ],
3751 [ location => isMsg(location.key), pages.threadShow ],
3852
@@ -47,14 +61,8 @@
4761 // [ location => location.page === 'groupNew', pages.groupNew ],
4862 // // [ location => location.type === 'groupShow' && isMsg(location.key), pages.groupShow ],
4963 // [ location => location.channel , pages.channel ],
5064
51- // Blog pages
52- [ location => location.page === 'home', pages.blogIndex ],
53- [ location => location.page === 'discovery', pages.blogIndex ],
54- [ location => location.page === 'blogIndex', pages.blogIndex ],
55- [ location => location.page === 'blogNew', pages.blogNew ],
56-
5765 [ location => location.page === 'settings', pages.settings ],
5866
5967 // [ location => isBlob(location.blob), pages.image ],
6068 [ location => isBlob(location.blob), (location) => {
styles/button.mcssView
@@ -1,10 +1,10 @@
11 Button {
22 font-family: arial
3- background-color: #fff
3+ $backgroundPrimaryText
44
55 min-width: 6rem
6- height: 1.2rem
6+ height: 1.2em
77 padding: .2rem 1rem
88
99 border: 1px #b9b9b9 solid
1010 border-radius: 10rem
@@ -23,15 +23,23 @@
2323 }
2424
2525 -primary {
2626 $colorPrimary
27+ $font
2728 $borderPrimary
2829
2930 :hover {
3031 opacity: .9
3132 }
3233 }
3334
35+ -channel {
36+ $backgroundPrimary
37+ $colorFontPrimary
38+ font-size: .9rem
39+ min-width: initial
40+ }
41+
3442 -showMore {
3543 width: 100%
3644
3745 padding: .2rem 0
styles/global.mcssView
@@ -1,7 +1,7 @@
11 body {
22 $fontBasic
3- background-color: #fff
3+ $backgroundPrimaryText
44
55 margin: 0
66
77 // different to Page
styles/markdown.mcssView
@@ -5,8 +5,18 @@
55 margin: .5rem 0
66 border-radius: .5rem
77 }
88
9+ // center blog images
10+ p {
11+ a {
12+ img {
13+ display: block
14+ margin: auto
15+ }
16+ }
17+ }
18+
919 (img.emoji) {
1020 margin: 0
1121 }
1222 }
styles/mixins.jsView
@@ -44,10 +44,10 @@
4444 $colorFontBasic {
4545 color: #222
4646 }
4747
48-$colorPrimaryFG {
49- color: #fff
48+$colorFontPrimary {
49+ color: #5c6bc0
5050 }
5151
5252 $colorSubtle {
5353 color: #999
@@ -56,28 +56,52 @@
5656 $backgroundPrimary {
5757 background-color: #f5f6f7
5858 }
5959
60+$backgroundPrimaryText {
61+ background-color: #fff
62+}
63+
6064 $backgroundSelected {
6165 background-color: #f0f1f2
6266 }
6367
6468 $borderPrimary {
6569 border: 1px #2f63ad solid
6670 }
6771
68-$avatarSmall {
69- width: 3rem
70- height: 3rem
71- border-radius: 1.5rem
72+$circleTiny {
73+ min-width: 2rem
74+ min-height: 2rem
75+ width: 2rem
76+ height: 2rem
77+ border-radius: 1rem
7278 }
7379
74-$avatarLarge {
75- width: 6rem
76- height: 6rem
77- border-radius: 3rem
80+$circleSmall {
81+ min-width: 2.8rem
82+ min-height: 2.8rem
83+ width: 2.8rem
84+ height: 2.8rem
85+ border-radius: 1.4rem
7886 }
7987
88+$circleMedium {
89+ min-width: 3.5rem
90+ min-height: 3.5rem
91+ width: 3.5rem
92+ height: 3.5rem
93+ border-radius: 1.75rem
94+}
95+
96+$circlelarge {
97+ min-width: 5rem
98+ min-height: 5rem
99+ width: 5rem
100+ height: 5rem
101+ border-radius: 2.5rem
102+}
103+
80104 $markdownSmall {
81105 div.Markdown {
82106 h1, h2, h3, h4, h5, h6, p {
83107 font-size: .9rem
contact/html/follow.jsView
@@ -1,0 +1,41 @@
1+const nest = require('depnest')
2+const { h, Array: MutantArray, computed, when, map } = require('mutant')
3+
4+exports.gives = nest('contact.html.follow')
5+
6+exports.needs = nest({
7+ 'contact.async.follow': 'first',
8+ 'contact.async.unfollow': 'first',
9+ 'contact.obs.followers': 'first',
10+ 'keys.sync.id': 'first',
11+ 'translations.sync.strings': 'first',
12+})
13+
14+exports.create = (api) => {
15+ return nest('contact.html.follow', follow)
16+
17+ function follow (feed) {
18+ const strings = api.translations.sync.strings()
19+ const myId = api.keys.sync.id()
20+
21+ if (feed === myId) return
22+
23+ const { followers } = api.contact.obs
24+ const theirFollowers = followers(feed)
25+ const youFollowThem = computed(theirFollowers, followers => followers.includes(myId))
26+
27+ const { unfollow, follow } = api.contact.async
28+ const className = when(youFollowThem, '-following')
29+
30+ return h('Follow', { className },
31+ when(theirFollowers.sync,
32+ when(youFollowThem,
33+ h('Button', { 'ev-click': () => unfollow(feed) }, strings.userShow.action.unfollow),
34+ h('Button', { 'ev-click': () => follow(feed) }, strings.userShow.action.follow)
35+ ),
36+ h('Button', { disabled: 'disabled' }, strings.loading )
37+ )
38+ )
39+ }
40+}
41+
contact/index.jsView
@@ -1,0 +1,7 @@
1+module.exports = {
2+ html: {
3+ follow: require('./html/follow'),
4+ }
5+}
6+
7+

Built with git-ssb-web