Commit 30cc7a8935378d1ee6fddd76239383d8da592a47
rename ThreadCard BlogCard
mix irving committed on 10/16/2017, 3:29:23 AMParent: 46d507d68f853a7a8459be31b98e58fa4e8430aa
Files changed
app/html/blog-card.js | added |
app/html/blog-card.mcss | added |
app/html/thread-card.js | deleted |
app/html/thread-card.mcss | deleted |
app/index.js | changed |
app/page/blogIndex.js | changed |
app/page/channel.js | changed |
app/page/home.js | changed |
app/page/home.mcss | changed |
app/page/userShow.js | changed |
app/html/blog-card.js | ||
---|---|---|
@@ -1,0 +1,131 @@ | ||
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 | + var img = marksum.image(thread.value.content.text) | |
96 | + var m = /\!\[[^]+\]\(([^\)]+)\)/.exec(img) | |
97 | + | |
98 | + if(m) { | |
99 | + //Hey this works! fit an image into a specific size (see thread-card.mcss) | |
100 | + //centered, and scaled to fit the square (works with both landscape and portrait!) | |
101 | + //This is functional css not opinionated css, so all embedded. | |
102 | + img = h('Thumbnail') | |
103 | + img.style = 'background-image: url("'+api.blob.sync.url(m[1])+'"); background-position:center; background-size: cover;' | |
104 | + } | |
105 | + else img = '' | |
106 | + var title = render(marksum.title(thread.value.content.text)) | |
107 | + var summary = render(marksum.summary(thread.value.content.text)) | |
108 | + | |
109 | + var className = thread.unread ? '-unread': '' | |
110 | + return h('BlogCard', { id, className }, [ | |
111 | + h('div.context', [ | |
112 | + api.about.html.avatar(thread.value.author), | |
113 | + ' ', | |
114 | + api.about.obs.name(thread.value.author), | |
115 | + ' ', | |
116 | + humanTime(new Date(thread.value.timestamp)), | |
117 | + ' ', | |
118 | + thread.value.content.channel ? '#'+thread.value.content.channel : null | |
119 | + ]), | |
120 | + h('div.content', {'ev-click': onClick}, [ | |
121 | + img, | |
122 | + h('Text', [ | |
123 | + h('h2', {innerHTML: title}), | |
124 | + h('Summary', {innerHTML: summary}) | |
125 | + ]) | |
126 | + ]) | |
127 | + ]) | |
128 | + }) | |
129 | +} | |
130 | + | |
131 | + |
app/html/blog-card.mcss | ||
---|---|---|
@@ -1,0 +1,71 @@ | ||
1 | +BlogCard { | |
2 | + display: flex | |
3 | +// align-items: center | |
4 | + flex-direction: column | |
5 | + margin-bottom: .5rem | |
6 | + | |
7 | + div.context { | |
8 | + display: flex | |
9 | + margin-right: 1rem | |
10 | + font-weight: inherit | |
11 | + } | |
12 | + | |
13 | + div.content { | |
14 | + display: flex | |
15 | + flex-direction: row | |
16 | + flex-grow: 1 | |
17 | + | |
18 | + cursor: pointer | |
19 | + padding: 1rem | |
20 | + border: 1px solid #ddd | |
21 | + border-radius: 2px | |
22 | + | |
23 | + transition: all .5s ease | |
24 | + | |
25 | + :hover { | |
26 | + background-color: #fff | |
27 | + border: 1px solid #fff | |
28 | + } | |
29 | + div.text { | |
30 | + flow-direction: column | |
31 | + } | |
32 | + | |
33 | + div.subject { | |
34 | + font-size: 1.2rem | |
35 | + margin-bottom: .3rem | |
36 | + | |
37 | + $markdownLarge | |
38 | + } | |
39 | + div.reply { | |
40 | + color: #444 | |
41 | + | |
42 | + display: flex | |
43 | + align-items: center | |
44 | + | |
45 | + i.fa-caret-left { | |
46 | + margin-left: .7rem | |
47 | + margin-right: .5rem | |
48 | + } | |
49 | + | |
50 | + $markdownSmall | |
51 | + } | |
52 | + } | |
53 | + | |
54 | + -unread { | |
55 | + div.content { | |
56 | + background-color: #fff | |
57 | + | |
58 | + div.subject { | |
59 | + $markdownBold | |
60 | + } | |
61 | + } | |
62 | + } | |
63 | +} | |
64 | + | |
65 | +Thumbnail { | |
66 | + border-radius: 5px | |
67 | + min-width: 100px | |
68 | + min-height: 100px | |
69 | + width: 100px | |
70 | + height: 100px | |
71 | +} |
app/html/thread-card.js | ||
---|---|---|
@@ -1,131 +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.threadCard', 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.threadCard', (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 | - var img = marksum.image(thread.value.content.text) | |
96 | - var m = /\!\[[^]+\]\(([^\)]+)\)/.exec(img) | |
97 | - | |
98 | - if(m) { | |
99 | - //Hey this works! fit an image into a specific size (see thread-card.mcss) | |
100 | - //centered, and scaled to fit the square (works with both landscape and portrait!) | |
101 | - //This is functional css not opinionated css, so all embedded. | |
102 | - img = h('Thumbnail') | |
103 | - img.style = 'background-image: url("'+api.blob.sync.url(m[1])+'"); background-position:center; background-size: cover;' | |
104 | - } | |
105 | - else img = '' | |
106 | - var title = render(marksum.title(thread.value.content.text)) | |
107 | - var summary = render(marksum.summary(thread.value.content.text)) | |
108 | - | |
109 | - var className = thread.unread ? '-unread': '' | |
110 | - return h('ThreadCard', { id, className }, [ | |
111 | - h('div.context', [ | |
112 | - api.about.html.avatar(thread.value.author), | |
113 | - ' ', | |
114 | - api.about.obs.name(thread.value.author), | |
115 | - ' ', | |
116 | - humanTime(new Date(thread.value.timestamp)), | |
117 | - ' ', | |
118 | - thread.value.content.channel ? '#'+thread.value.content.channel : null | |
119 | - ]), | |
120 | - h('div.content', {'ev-click': onClick}, [ | |
121 | - img, | |
122 | - h('Text', [ | |
123 | - h('h2', {innerHTML: title}), | |
124 | - h('Summary', {innerHTML: summary}) | |
125 | - ]) | |
126 | - ]) | |
127 | - ]) | |
128 | - }) | |
129 | -} | |
130 | - | |
131 | - |
app/html/thread-card.mcss | ||
---|---|---|
@@ -1,71 +1,0 @@ | ||
1 | -ThreadCard { | |
2 | - display: flex | |
3 | -// align-items: center | |
4 | - flex-direction: column | |
5 | - margin-bottom: .5rem | |
6 | - | |
7 | - div.context { | |
8 | - display: flex | |
9 | - margin-right: 1rem | |
10 | - font-weight: inherit | |
11 | - } | |
12 | - | |
13 | - div.content { | |
14 | - display: flex | |
15 | - flex-direction: row | |
16 | - flex-grow: 1 | |
17 | - | |
18 | - cursor: pointer | |
19 | - padding: 1rem | |
20 | - border: 1px solid #ddd | |
21 | - border-radius: 2px | |
22 | - | |
23 | - transition: all .5s ease | |
24 | - | |
25 | - :hover { | |
26 | - background-color: #fff | |
27 | - border: 1px solid #fff | |
28 | - } | |
29 | - div.text { | |
30 | - flow-direction: column | |
31 | - } | |
32 | - | |
33 | - div.subject { | |
34 | - font-size: 1.2rem | |
35 | - margin-bottom: .3rem | |
36 | - | |
37 | - $markdownLarge | |
38 | - } | |
39 | - div.reply { | |
40 | - color: #444 | |
41 | - | |
42 | - display: flex | |
43 | - align-items: center | |
44 | - | |
45 | - i.fa-caret-left { | |
46 | - margin-left: .7rem | |
47 | - margin-right: .5rem | |
48 | - } | |
49 | - | |
50 | - $markdownSmall | |
51 | - } | |
52 | - } | |
53 | - | |
54 | - -unread { | |
55 | - div.content { | |
56 | - background-color: #fff | |
57 | - | |
58 | - div.subject { | |
59 | - $markdownBold | |
60 | - } | |
61 | - } | |
62 | - } | |
63 | -} | |
64 | - | |
65 | -Thumbnail { | |
66 | - border-radius: 5px | |
67 | - min-width: 100px | |
68 | - min-height: 100px | |
69 | - width: 100px | |
70 | - height: 100px | |
71 | -} |
app/index.js | ||
---|---|---|
@@ -7,9 +7,9 @@ | ||
7 | 7 | context: require('./html/context'), |
8 | 8 | header: require('./html/header'), |
9 | 9 | thread: require('./html/thread'), |
10 | 10 | link: require('./html/link'), |
11 | - threadCard: require('./html/thread-card'), | |
11 | + blogCard: require('./html/thread-card'), | |
12 | 12 | }, |
13 | 13 | page: { |
14 | 14 | blogIndex: require('./page/blogIndex'), |
15 | 15 | blogNew: require('./page/blogNew'), |
app/page/blogIndex.js | ||
---|---|---|
@@ -9,9 +9,9 @@ | ||
9 | 9 | exports.gives = nest('app.page.blogIndex') |
10 | 10 | |
11 | 11 | exports.needs = nest({ |
12 | 12 | 'app.html.context': 'first', |
13 | - 'app.html.threadCard': 'first', | |
13 | + 'app.html.blogCard': 'first', | |
14 | 14 | 'history.sync.push': 'first', |
15 | 15 | 'keys.sync.id': 'first', |
16 | 16 | 'translations.sync.strings': 'first', |
17 | 17 | 'state.obs.threads': 'first', |
@@ -100,9 +100,9 @@ | ||
100 | 100 | var onClick |
101 | 101 | if (channel && !recps) |
102 | 102 | onClick = (ev) => api.history.sync.push({ key: thread.key, page: 'blogShow' }) |
103 | 103 | |
104 | - return api.app.html.threadCard(thread, { onClick }) | |
104 | + return api.app.html.blogCard(thread, { onClick }) | |
105 | 105 | }) |
106 | 106 | ) |
107 | 107 | ) |
108 | 108 |
app/page/channel.js | ||
---|---|---|
@@ -7,9 +7,9 @@ | ||
7 | 7 | exports.gives = nest('app.page.channel') |
8 | 8 | |
9 | 9 | exports.needs = nest({ |
10 | 10 | 'app.html.link': 'first', |
11 | - 'app.html.threadCard': 'first', | |
11 | + 'app.html.blogCard': 'first', | |
12 | 12 | 'history.sync.push': 'first', |
13 | 13 | 'state.obs.channel': 'first', |
14 | 14 | 'translations.sync.strings': 'first', |
15 | 15 | }) |
@@ -41,9 +41,9 @@ | ||
41 | 41 | h('div.threads', Object.keys(threads.roots) |
42 | 42 | .map(id => threads.roots[id]) |
43 | 43 | .filter(thread => get(thread, 'value.content.channel') == channel) |
44 | 44 | .sort((a, b) => latestUpdate(b) - latestUpdate(a)) |
45 | - .map(thread => api.app.html.threadCard(thread)) | |
45 | + .map(thread => api.app.html.blogCard(thread)) | |
46 | 46 | ) |
47 | 47 | ) |
48 | 48 | return updates |
49 | 49 | } |
app/page/home.js | ||
---|---|---|
@@ -11,9 +11,9 @@ | ||
11 | 11 | 'history.sync.push': 'first', |
12 | 12 | 'keys.sync.id': 'first', |
13 | 13 | 'translations.sync.strings': 'first', |
14 | 14 | 'state.obs.threads': 'first', |
15 | - 'app.html.threadCard': 'first', | |
15 | + 'app.html.blogCard': 'first', | |
16 | 16 | 'unread.sync.isUnread': 'first' |
17 | 17 | }) |
18 | 18 | |
19 | 19 | // function toRecpGroup(msg) { |
@@ -122,9 +122,9 @@ | ||
122 | 122 | var onClick |
123 | 123 | if (channel && !recps) |
124 | 124 | onClick = (ev) => api.history.sync.push({ channel }) |
125 | 125 | |
126 | - return api.app.html.threadCard(thread, { onClick }) | |
126 | + return api.app.html.blogCard(thread, { onClick }) | |
127 | 127 | }) |
128 | 128 | ) |
129 | 129 | ) |
130 | 130 |
app/page/home.mcss | ||
---|---|---|
@@ -8,9 +8,9 @@ | ||
8 | 8 | |
9 | 9 | div.threads { |
10 | 10 | min-width: 60% |
11 | 11 | |
12 | - div.ThreadCard { | |
12 | + div.BlogCard { | |
13 | 13 | div.content { |
14 | 14 | div.subject { |
15 | 15 | display: flex |
16 | 16 | |
@@ -30,9 +30,9 @@ | ||
30 | 30 | -channel { |
31 | 31 | $homePageSection |
32 | 32 | |
33 | 33 | div.threads { |
34 | - div.ThreadCard { | |
34 | + div.BlogCard { | |
35 | 35 | div.context { |
36 | 36 | background: #fff |
37 | 37 | min-width: 8rem |
38 | 38 | padding: .1rem .3rem |
@@ -70,7 +70,7 @@ | ||
70 | 70 | margin-bottom: .4rem |
71 | 71 | } |
72 | 72 | |
73 | 73 | div.threads { |
74 | - div.ThreadCard {} | |
74 | + div.BlogCard {} | |
75 | 75 | } |
76 | 76 | } |
app/page/userShow.js | ||
---|---|---|
@@ -8,9 +8,9 @@ | ||
8 | 8 | exports.needs = nest({ |
9 | 9 | 'about.html.image': 'first', |
10 | 10 | 'about.obs.name': 'first', |
11 | 11 | 'app.html.link': 'first', |
12 | - 'app.html.threadCard': 'first', | |
12 | + 'app.html.blogCard': 'first', | |
13 | 13 | 'contact.async.follow': 'first', |
14 | 14 | 'contact.async.unfollow': 'first', |
15 | 15 | 'contact.obs.followers': 'first', |
16 | 16 | 'feed.pull.private': 'first', |
@@ -92,9 +92,9 @@ | ||
92 | 92 | h('div.directMessage', directMessageButton) |
93 | 93 | ]) |
94 | 94 | : '', |
95 | 95 | ]), |
96 | - h('section.blogs', map(threads, api.app.html.threadCard)) | |
96 | + h('section.blogs', map(threads, api.app.html.blogCard)) | |
97 | 97 | ]) |
98 | 98 | ]) |
99 | 99 | } |
100 | 100 | } |
Built with git-ssb-web