Files: 35e506868ea54929c5978b29572a923888e2518d / app / html / blog-card.js
3941 bytesRaw
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 | |
131 |
Built with git-ssb-web