git ssb

2+

mixmix / ticktack



Commit d1fb3daa34d11265fab5f4118776a40e96d39062

blogShow basic display. refactor of Avatar.

mix irving committed on 10/18/2017, 3:32:41 AM
Parent: d4471ed9e0be3a61e29df8a0de9c3b36cf52e096

Files changed

about/html/avatar.jschanged
about/html/avatar.mcsschanged
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/index.jschanged
app/page/blogIndex.jschanged
app/page/blogShow.jschanged
app/page/groupFind.mcsschanged
app/page/threadNew.mcsschanged
app/page/userFind.mcsschanged
app/page/userShow.jschanged
app/page/userShow.mcsschanged
app/page/blogShow.mcssadded
message/html/compose.jschanged
message/html/compose.mcsschanged
message/html/channel.jsadded
message/html/timeago.jsadded
message/html/timeago.mcssadded
message/index.jschanged
router/sync/routes.jschanged
styles/button.mcsschanged
styles/global.mcsschanged
styles/mixins.jschanged
about/html/avatar.jsView
@@ -1,22 +1,23 @@
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': () => api.history.sync.push({ page: 'userShow', feed: id })
20+ })
2021 })
2122 }
2223
about/html/avatar.mcssView
@@ -1,5 +1,20 @@
11 Avatar {
2- $avatarSmall
3- margin-right: .5rem
2+ $circleSmall
3+
4+ -tiny {
5+ $circleTiny
6+ }
7+
8+ -small {
9+ $circleSmall
10+ }
11+
12+ -medium {
13+ $circleMedium
14+ }
15+
16+ -large {
17+ $circleLarge
18+ }
419 }
520
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 }, [
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', {'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/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,59 @@
1+const nest = require('depnest')
2+const { h, Array: MutantArray, 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+ 'feed.obs.thread': 'first',
11+ 'keys.sync.id': 'first',
12+ 'message.html.markdown': 'first',
13+ 'unread.sync.markRead': 'first',
14+ 'unread.sync.isUnread': 'first'
15+})
16+
17+exports.create = (api) => {
18+ return nest('app.html.comments', comments)
19+
20+ function comments (root) {
21+ const myId = api.keys.sync.id()
22+ const { messages } = api.feed.obs.thread(root)
23+
24+ return h('Comments',
25+ map(messages, Comment)
26+ )
27+
28+ function Comment (msgObs) {
29+ const msg = resolve(msgObs)
30+ const raw = get(msg, 'value.content.text')
31+ var className = api.unread.sync.isUnread(msg) ? ' -unread' : ' -read'
32+ api.unread.sync.markRead(msg)
33+
34+ if (!get(msg, 'value.content.root')) return
35+
36+ const { author } = msg.value
37+ return h('Comment', { className }, [
38+ h('div.left', api.about.html.avatar(author)),
39+ h('div.right', [
40+ h('section.context', [
41+ h('div.name', api.about.obs.name(author)),
42+ h('div.timeago', '3 hours ago'), //TODO
43+ ]),
44+ h('section.content', api.message.html.markdown(raw)),
45+ h('section.actions', [
46+ h('div.reply', [
47+ h('i.fa.fa-commenting-o'),
48+ ]),
49+ h('div.like', [
50+ Math.random() > 0.5 ? h('i.fa.fa-heart') : h('i.fa.fa-heart-o'), // TODO -obs I like
51+ 3 // TODO -obs like count
52+ ])
53+ ])
54+ ])
55+ ])
56+ }
57+ }
58+}
59+
app/index.jsView
@@ -3,13 +3,14 @@
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'),
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/blogShow.jsView
@@ -1,102 +1,69 @@
11 const nest = require('depnest')
2-const { h, Array: MutantArray, computed, when, map } = require('mutant')
3-const pull = require('pull-stream')
2+const { h, computed, when } = require('mutant')
3+const { title: getTitle } = require('markdown-summary')
4+const last = require('lodash/last')
45 const get = require('lodash/get')
56
6-exports.gives = nest('app.page.userShow')
7+exports.gives = nest('app.page.blogShow')
78
89 exports.needs = nest({
9- 'about.html.image': 'first',
10+ 'about.html.avatar': 'first',
1011 'about.obs.name': 'first',
11- 'app.html.link': 'first',
12- 'app.html.blogCard': 'first',
13- 'contact.async.follow': 'first',
14- 'contact.async.unfollow': 'first',
15- 'contact.obs.followers': 'first',
16- 'feed.pull.private': 'first',
17- 'feed.pull.rollup': 'first',
18- 'keys.sync.id': 'first',
19- 'state.obs.threads': 'first',
20- 'translations.sync.strings': 'first',
12+ 'app.html.comments': 'first',
13+ 'app.html.context': 'first',
14+ 'message.html.channel': 'first',
15+ 'message.html.compose': 'first',
16+ 'message.html.markdown': 'first',
17+ 'message.html.timeago': 'first',
2118 })
2219
2320 exports.create = (api) => {
24- return nest('app.page.userShow', userShow)
21+ return nest('app.page.blogShow', blogShow)
2522
26- function userShow (location) {
23+ function blogShow (blogMsg) {
24+ // blogMsg = a thread (message, may be decorated with replies)
2725
28- const { feed } = location
29- const myId = api.keys.sync.id()
30- const name = api.about.obs.name(feed)
26+ const { author, content } = blogMsg.value
3127
32- const strings = api.translations.sync.strings()
28+ const blog = content.text
29+ const title = content.title || getTitle(blog)
3330
34- const { followers } = api.contact.obs
31+ const comments = api.app.html.comments(blogMsg.key)
3532
36- const youFollowThem = computed(followers(feed), followers => followers.includes(myId))
37- // const theyFollowYou = computed(followers(myId), followers => followers.includes(feed))
38- // const youAreFriends = computed([youFollowThem, theyFollowYou], (a, b) => a && b)
33+ const meta = {
34+ type: 'post',
35+ root: blogMsg.key,
36+ // branch: get(last(blogMsg.replies), 'key'), // TODO - change to match new comments logic
37+ // >> lastId? CHECK THIS LOGIC
38+ channel: content.channel
39+ }
3940
40- // const ourRelationship = computed(
41- // [youAreFriends, youFollowThem, theyFollowYou],
42- // (youAreFriends, youFollowThem, theyFollowYou) => {
43- // if (youAreFriends) return strings.userShow.state.friends
44- // if (theyFollowYou) return strings.userShow.state.theyFollow
45- // if (youFollowThem) return strings.userShow.state.youFollow
46- // }
47- // )
48- const { unfollow, follow } = api.contact.async
49- const followButton = when(followers(myId).sync,
50- when(youFollowThem,
51- h('Button -primary', { 'ev-click': () => unfollow(feed) }, strings.userShow.action.unfollow),
52- h('Button -primary', { 'ev-click': () => follow(feed) }, strings.userShow.action.follow)
53- ),
54- h('Button', { disabled: 'disabled' }, strings.loading )
55- )
41+ const { timeago, channel, markdown, compose } = api.message.html
42+ const composer = compose({ meta, shrink: true })
5643
57- const Link = api.app.html.link
58- const userEditButton = Link({ page: 'userEdit', feed }, h('i.fa.fa-pencil'))
59- const directMessageButton = Link({ page: 'threadNew', feed }, h('Button', strings.userShow.action.directMessage))
60-
61- const threads = MutantArray()
62- pull(
63- // next(api.feed.pull.private, {reverse: true, limit: 100, live: false}, ['value', 'timestamp']),
64- // api.feed.pull.private({reverse: true, limit: 100, live: false}),
65- api.feed.pull.private({reverse: true, live: false}),
66- pull.filter(msg => {
67- const recps = get(msg, 'value.content.recps')
68- if (!recps) return
69-
70- return recps
71- .map(r => typeof r === 'object' ? r.link : r)
72- .includes(feed)
73- }),
74- api.feed.pull.rollup(),
75- pull.drain(threads.push)
76- // Scroller(content, scrollerContent, render, false, false)
77- )
78-
79- return h('Page -userShow', {title: name}, [
44+ return h('Page -blogShow', [
45+ api.app.html.context({ page: 'discover' }), // HACK to highlight discover
8046 h('div.content', [
81- h('section.about', [
82- api.about.html.image(feed),
83- h('h1', [
84- name,
85- feed === myId // Only expose own profile editing right now
86- ? userEditButton
87- : ''
47+ h('header', [
48+ h('div.blog', [
49+ h('h1', title),
50+ timeago(blogMsg),
51+ channel(blogMsg)
8852 ]),
89- feed !== myId
90- ? h('div.actions', [
91- h('div.friendship', followButton),
92- h('div.directMessage', directMessageButton)
93- ])
94- : '',
53+ h('div.author', [
54+ h('div.leftCol', api.about.html.avatar(author, 'medium')),
55+ h('div.rightCol', [
56+ h('div.name', api.about.obs.name(author)),
57+ h('Button', 'Follow')
58+ ]),
59+ ])
9560 ]),
96- h('section.blogs', map(threads, api.app.html.blogCard))
97- ])
61+ h('div.break', h('hr')),
62+ h('section.blog', markdown(blog)),
63+ composer,
64+ comments,
65+ ]),
9866 ])
9967 }
10068 }
10169
102-
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,9 +5,9 @@
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',
1313 'contact.async.follow': 'first',
@@ -78,9 +78,9 @@
7878
7979 return h('Page -userShow', {title: name}, [
8080 h('div.content', [
8181 h('section.about', [
82- api.about.html.image(feed),
82+ api.about.html.avatar(feed, 'large'),
8383 h('h1', [
8484 name,
8585 feed === myId // Only expose own profile editing right now
8686 ? userEditButton
app/page/userShow.mcssView
@@ -8,11 +8,8 @@
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
app/page/blogShow.mcssView
@@ -1,0 +1,82 @@
1+Page -blogShow {
2+ // div.context {}
3+
4+ div.content {
5+ header {
6+ $backgroundPrimaryText
7+ padding: 1rem
8+
9+ display: flex
10+
11+ div.blog {
12+ display: flex
13+ flex-wrap: wrap
14+ flex-grow: 1
15+
16+ h1 {
17+ flex-basis: 100%
18+
19+ font-weight: 300
20+ margin: 0 0 1rem 0
21+ }
22+
23+ div.Timeago {
24+ flex-basis: 100%
25+ margin-bottom: .6rem
26+ }
27+
28+ div.Button.-channel {}
29+ }
30+
31+ div.author {
32+ display: flex
33+
34+ div.leftCol {
35+ margin-right: 1rem
36+ img.Avatar {}
37+ }
38+
39+ div.rightCol {
40+ div.name {
41+ font-size: .9rem
42+ margin-bottom: .5rem
43+ }
44+ div.Button.-follow {} // extract
45+ }
46+ }
47+ }
48+
49+ div.break {
50+ padding: 0 1rem
51+ $backgroundPrimaryText
52+
53+ hr {
54+ margin: 0
55+ border: none
56+ border-bottom: 1px solid gainsboro
57+ }
58+ }
59+
60+ section.blog {
61+ $backgroundPrimaryText
62+ padding: 1rem
63+
64+ margin-bottom: 1.5rem
65+ }
66+
67+ div.Compose {
68+ margin-bottom: 1.5rem
69+
70+ section.actions {
71+ input { display: none }
72+
73+ div.Button {
74+ -preview { display: none }
75+ }
76+ }
77+ }
78+
79+ div.Comments {}
80+ }
81+}
82+
message/html/compose.jsView
@@ -83,9 +83,9 @@
8383 else
8484 fileInput = h('input', { style: {visibility: 'hidden'}})
8585
8686 var showPreview = Value(false)
87- var previewBtn = h('Button',
87+ var previewBtn = h('Button -preview',
8888 {
8989 className: when(showPreview, '-primary'),
9090 'ev-click': () => showPreview.set(!showPreview())
9191 },
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/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,11 @@
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+ subject: require('./html/subject'),
9+ timeago: require('./html/timeago')
810 }
911 }
1012
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/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

Built with git-ssb-web