Commit 0e154203de9454a9f8f6a7ecc08aa6f661819f6d
Merge pull request #46 from ticktackim/blogShow
blogShow - v1mix 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.js | ||
---|---|---|
@@ -1,22 +1,26 @@ | ||
1 | 1 | const nest = require('depnest') |
2 | 2 | const { h } = require('mutant') |
3 | 3 | |
4 | +exports.gives = nest('about.html.avatar') | |
5 | + | |
4 | 6 | 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' | |
7 | 10 | }) |
8 | 11 | |
9 | -exports.gives = nest('about.html.avatar') | |
10 | - | |
11 | 12 | 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 | + }) | |
20 | 24 | }) |
21 | 25 | } |
22 | 26 |
about/html/avatar.mcss | ||
---|---|---|
@@ -1,5 +1,21 @@ | ||
1 | 1 | 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 | + } | |
4 | 20 | } |
5 | 21 |
app/async/catch-link-click.js | ||
---|---|---|
@@ -1,9 +1,14 @@ | ||
1 | 1 | const nest = require('depnest') |
2 | 2 | const Url = require('url') |
3 | +const { isMsg } = require('ssb-ref') | |
3 | 4 | |
4 | 5 | exports.gives = nest('app.async.catchLinkClick') |
5 | 6 | |
7 | +exports.needs = nest({ | |
8 | + 'sbot.async.get': 'first' | |
9 | +}) | |
10 | + | |
6 | 11 | exports.create = function (api) { |
7 | 12 | return nest('app.async.catchLinkClick', catchLinkClick) |
8 | 13 | |
9 | 14 | function catchLinkClick (root, cb) { |
@@ -29,8 +34,16 @@ | ||
29 | 34 | const opts = { |
30 | 35 | isExternal: !!url.host |
31 | 36 | } |
32 | 37 | |
38 | + if (isMsg(href)) { | |
39 | + api.sbot.async.get(href, (err, data) => { | |
40 | + // NOTE the catchLinkClick cb has signature (link, opts) | |
41 | + cb(err || data, opts) | |
42 | + }) | |
43 | + return | |
44 | + } | |
45 | + | |
33 | 46 | cb(href, opts) |
34 | 47 | }) |
35 | 48 | } |
36 | 49 | } |
app/html/app.js | ||
---|---|---|
@@ -1,79 +1,19 @@ | ||
1 | 1 | const nest = require('depnest') |
2 | -const values = require('lodash/values') | |
3 | -const insertCss = require('insert-css') | |
4 | -const openExternal = require('open-external') | |
5 | 2 | |
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') | |
9 | 4 | |
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 | - | |
17 | 5 | 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' | |
27 | 8 | }) |
28 | 9 | |
29 | 10 | exports.create = (api) => { |
30 | - var nav = null | |
31 | - | |
32 | 11 | return nest({ |
33 | 12 | 'app.html.app': function app () { |
13 | + api.app.sync.initialize() | |
34 | 14 | |
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 | + } | |
75 | 17 | }) |
76 | 18 | } |
77 | 19 | |
78 | - | |
79 | - |
app/html/context.js | ||
---|---|---|
@@ -51,33 +51,39 @@ | ||
51 | 51 | LevelTwoContext() |
52 | 52 | ]) |
53 | 53 | |
54 | 54 | function LevelOneContext () { |
55 | - const PAGES_UNDER_DISCOVER = ['blogIndex', 'blogShow', 'home'] | |
55 | + function isDiscoverContext (loc) { | |
56 | + const PAGES_UNDER_DISCOVER = ['blogIndex', 'blogShow', 'home'] | |
56 | 57 | |
58 | + return PAGES_UNDER_DISCOVER.includes(location.page) | |
59 | + || get(location, 'value.private') === undefined | |
60 | + } | |
61 | + | |
57 | 62 | return h('div.level.-one', [ |
58 | 63 | // Nearby |
59 | 64 | computed(nearby, n => !isEmpty(n) ? h('header', strings.peopleNearby) : null), |
60 | 65 | map(nearby, feedId => Option({ |
61 | 66 | 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'), | |
63 | 68 | label: api.about.obs.name(feedId), |
64 | 69 | selected: location.feed === feedId, |
65 | 70 | location: computed(recentPeersContacted, recent => { |
66 | 71 | const lastMsg = recent[feedId] |
67 | 72 | return lastMsg |
68 | 73 | ? Object.assign(lastMsg, { feed: feedId }) |
69 | 74 | : { page: 'threadNew', feed: feedId } |
70 | 75 | }), |
71 | - })), | |
76 | + }), { comparer: (a, b) => a === b }), | |
77 | + | |
72 | 78 | computed(nearby, n => !isEmpty(n) ? h('hr') : null), |
73 | 79 | |
74 | 80 | // Discover |
75 | 81 | Option({ |
76 | 82 | notifications: Math.floor(Math.random()*5+1), |
77 | 83 | imageEl: h('i.fa.fa-binoculars'), |
78 | 84 | label: strings.blogIndex.title, |
79 | - selected: PAGES_UNDER_DISCOVER.includes(location.page), | |
85 | + selected: isDiscoverContext(location), | |
80 | 86 | location: { page: 'blogIndex' }, |
81 | 87 | }), |
82 | 88 | |
83 | 89 | // Recent Messages |
@@ -92,9 +98,9 @@ | ||
92 | 98 | label: api.about.obs.name(feedId), |
93 | 99 | selected: location.feed === feedId, |
94 | 100 | location: Object.assign({}, lastMsg, { feed: feedId }) // TODO make obs? |
95 | 101 | }) |
96 | - }) | |
102 | + }, { comparer: (a, b) => a === b }) | |
97 | 103 | ]) |
98 | 104 | } |
99 | 105 | |
100 | 106 | function LevelTwoContext () { |
@@ -126,9 +132,9 @@ | ||
126 | 132 | label: api.message.html.subject(thread), |
127 | 133 | selected: thread.key === root, |
128 | 134 | location: Object.assign(thread, { feed: targetUser }), |
129 | 135 | }) |
130 | - }) | |
136 | + }, { comparer: (a, b) => a === b }) | |
131 | 137 | ]) |
132 | 138 | } |
133 | 139 | |
134 | 140 | function Option ({ notifications = 0, imageEl, label, location, selected }) { |
app/html/context.mcss | ||
---|---|---|
@@ -1,9 +1,9 @@ | ||
1 | 1 | Context { |
2 | 2 | flex-shrink: 0 |
3 | 3 | flex-grow: 0 |
4 | 4 | overflow: hidden |
5 | - background-color: #fff | |
5 | + $backgroundPrimaryText | |
6 | 6 | |
7 | 7 | display: flex |
8 | 8 | |
9 | 9 | div.level { |
@@ -74,9 +74,9 @@ | ||
74 | 74 | a img { |
75 | 75 | |
76 | 76 | } |
77 | 77 | i { |
78 | - $avatarSmall | |
78 | + $circleSmall | |
79 | 79 | $colorPrimary |
80 | 80 | font-size: 1.3rem |
81 | 81 | display: flex |
82 | 82 | justify-content: center |
app/html/thread.js | ||
---|---|---|
@@ -1,8 +1,9 @@ | ||
1 | 1 | const nest = require('depnest') |
2 | 2 | const { h, Array: MutantArray, map, computed, when } = require('mutant') |
3 | 3 | const get = require('lodash/get') |
4 | 4 | |
5 | +// TODO - rename threadPrivate | |
5 | 6 | exports.gives = nest('app.html.thread') |
6 | 7 | |
7 | 8 | exports.needs = nest({ |
8 | 9 | 'about.html.avatar': 'first', |
@@ -26,18 +27,18 @@ | ||
26 | 27 | const author = computed([chunk], chunk => get(chunk, '[0].value.author')) |
27 | 28 | |
28 | 29 | return author() === myId |
29 | 30 | ? h('div.my-chunk', [ |
30 | - h('div.avatar'), | |
31 | + h('Avatar -small'), | |
31 | 32 | h('div.msgs', map(chunk, msg => { |
32 | 33 | return h('div.msg-row', [ |
33 | 34 | h('div.spacer'), |
34 | 35 | message(msg) |
35 | 36 | ]) |
36 | 37 | })) |
37 | 38 | ]) |
38 | 39 | : h('div.other-chunk', [ |
39 | - h('div.avatar', when(author, api.about.html.avatar(author()))), | |
40 | + when(author, api.about.html.avatar(author()), 'small'), | |
40 | 41 | h('div.msgs', map(chunk, msg => { |
41 | 42 | return h('div.msg-row', [ |
42 | 43 | message(msg), |
43 | 44 | h('div.spacer') |
@@ -96,11 +97,4 @@ | ||
96 | 97 | // TODO (mix) use lodash/get |
97 | 98 | return msgA.value.author === msgB.value.author |
98 | 99 | } |
99 | 100 | |
100 | - | |
101 | - | |
102 | - | |
103 | - | |
104 | - | |
105 | - | |
106 | - |
app/html/thread.mcss | ||
---|---|---|
@@ -9,9 +9,9 @@ | ||
9 | 9 | $chunk |
10 | 10 | |
11 | 11 | justify-content: space-between |
12 | 12 | |
13 | - div.avatar { | |
13 | + img.Avatar { | |
14 | 14 | visibility: hidden |
15 | 15 | } |
16 | 16 | |
17 | 17 | div.msgs { |
@@ -42,16 +42,9 @@ | ||
42 | 42 | $chunk { |
43 | 43 | display: flex |
44 | 44 | margin-bottom: .5rem |
45 | 45 | |
46 | - div.avatar { | |
47 | - background-color: #333 | |
48 | - $avatarSmall | |
49 | - | |
50 | - img { | |
51 | - $avatarSmall | |
52 | - } | |
53 | - | |
46 | + img.Avatar { | |
54 | 47 | margin-right: 1rem |
55 | 48 | } |
56 | 49 | |
57 | 50 | div.msgs { |
@@ -69,9 +62,9 @@ | ||
69 | 62 | } |
70 | 63 | |
71 | 64 | div.msg { |
72 | 65 | line-height: 1.2 |
73 | - background-color: #fff | |
66 | + $backgroundPrimaryText | |
74 | 67 | padding: 0 .7rem |
75 | 68 | border-radius: 4px |
76 | 69 | } |
77 | 70 | div.spacer { |
app/html/blog-card.js | ||
---|---|---|
@@ -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.js | ||
---|---|---|
@@ -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.mcss | ||
---|---|---|
@@ -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.mcss | ||
---|---|---|
@@ -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.js | ||
---|---|---|
@@ -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.mcss | ||
---|---|---|
@@ -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.js | ||
---|---|---|
@@ -3,17 +3,19 @@ | ||
3 | 3 | catchLinkClick: require('./async/catch-link-click'), |
4 | 4 | }, |
5 | 5 | html: { |
6 | 6 | app: require('./html/app'), |
7 | + comments: require('./html/comments'), | |
7 | 8 | context: require('./html/context'), |
8 | 9 | header: require('./html/header'), |
9 | 10 | thread: require('./html/thread'), |
10 | 11 | link: require('./html/link'), |
11 | - blogCard: require('./html/blog-card'), | |
12 | + blogCard: require('./html/blogCard'), | |
12 | 13 | }, |
13 | 14 | page: { |
14 | 15 | blogIndex: require('./page/blogIndex'), |
15 | 16 | blogNew: require('./page/blogNew'), |
17 | + blogShow: require('./page/blogShow'), | |
16 | 18 | error: require('./page/error'), |
17 | 19 | settings: require('./page/settings'), |
18 | 20 | // channel: require('./page/channel'), |
19 | 21 | // image: require('./page/image'), |
@@ -27,7 +29,15 @@ | ||
27 | 29 | // userFind: require('./page/userFind'), |
28 | 30 | userShow: require('./page/userShow'), |
29 | 31 | threadNew: require('./page/threadNew'), |
30 | 32 | 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'), | |
31 | 41 | } |
32 | 42 | } |
33 | 43 |
app/page/blogIndex.js | ||
---|---|---|
@@ -98,9 +98,9 @@ | ||
98 | 98 | groupedThreads.map(thread => { |
99 | 99 | const { recps, channel } = thread.value.content |
100 | 100 | var onClick |
101 | 101 | 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' })) | |
103 | 103 | |
104 | 104 | return api.app.html.blogCard(thread, { onClick }) |
105 | 105 | }) |
106 | 106 | ) |
app/page/error.js | ||
---|---|---|
@@ -13,13 +13,10 @@ | ||
13 | 13 | return nest('app.page.error', error) |
14 | 14 | |
15 | 15 | function error (location) { |
16 | 16 | return h('Page -error', {title: strings.error}, [ |
17 | - strings.errorNotFound, | |
17 | + h('div.message', strings.errorNotFound), | |
18 | 18 | h('pre', [JSON.stringify(location, null, 2)]) |
19 | 19 | ]) |
20 | 20 | } |
21 | 21 | } |
22 | 22 | |
23 | - | |
24 | - | |
25 | - |
app/page/groupFind.mcss | ||
---|---|---|
@@ -1,9 +1,9 @@ | ||
1 | 1 | Page -groupFind { |
2 | 2 | div.content { |
3 | 3 | $maxWidthSmaller |
4 | 4 | div.search { |
5 | - background-color: #fff | |
5 | + $backgroundPrimaryText | |
6 | 6 | |
7 | 7 | margin-bottom: 1rem |
8 | 8 | |
9 | 9 | display: flex |
@@ -29,17 +29,17 @@ | ||
29 | 29 | $maxWidthSmaller |
30 | 30 | |
31 | 31 | div.Link { |
32 | 32 | div.result { |
33 | - background-color: #fff | |
33 | + $backgroundPrimaryText | |
34 | 34 | |
35 | 35 | padding: .5rem |
36 | 36 | |
37 | 37 | display: flex |
38 | 38 | align-items: center |
39 | 39 | |
40 | 40 | img { |
41 | - $avatarSmall | |
41 | + $circleSmall | |
42 | 42 | margin-right: 1rem |
43 | 43 | } |
44 | 44 | |
45 | 45 | div.alias { |
app/page/threadNew.mcss | ||
---|---|---|
@@ -33,9 +33,9 @@ | ||
33 | 33 | padding: .3rem |
34 | 34 | min-width: 5rem |
35 | 35 | $borderSubtle |
36 | 36 | border-radius: 6rem |
37 | - background-color: #fff | |
37 | + $backgroundPrimaryText | |
38 | 38 | |
39 | 39 | margin-right: 1rem |
40 | 40 | |
41 | 41 | display: flex |
app/page/userFind.mcss | ||
---|---|---|
@@ -1,9 +1,9 @@ | ||
1 | 1 | Page -userFind { |
2 | 2 | div.content { |
3 | 3 | $maxWidthSmaller |
4 | 4 | div.search { |
5 | - background-color: #fff | |
5 | + $backgroundPrimaryText | |
6 | 6 | |
7 | 7 | margin-bottom: 1rem |
8 | 8 | |
9 | 9 | display: flex |
@@ -29,17 +29,17 @@ | ||
29 | 29 | $maxWidthSmaller |
30 | 30 | |
31 | 31 | div.Link { |
32 | 32 | div.result { |
33 | - background-color: #fff | |
33 | + $backgroundPrimaryText | |
34 | 34 | |
35 | 35 | padding: .5rem |
36 | 36 | |
37 | 37 | display: flex |
38 | 38 | align-items: center |
39 | 39 | |
40 | 40 | img { |
41 | - $avatarSmall | |
41 | + $circleSmall | |
42 | 42 | margin-right: 1rem |
43 | 43 | } |
44 | 44 | |
45 | 45 | div.alias { |
app/page/userShow.js | ||
---|---|---|
@@ -5,15 +5,14 @@ | ||
5 | 5 | |
6 | 6 | exports.gives = nest('app.page.userShow') |
7 | 7 | |
8 | 8 | exports.needs = nest({ |
9 | - 'about.html.image': 'first', | |
9 | + 'about.html.avatar': 'first', | |
10 | 10 | 'about.obs.name': 'first', |
11 | 11 | 'app.html.link': 'first', |
12 | 12 | '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', | |
16 | 15 | 'sbot.pull.userFeed': 'first', |
17 | 16 | 'keys.sync.id': 'first', |
18 | 17 | 'translations.sync.strings': 'first', |
19 | 18 | }) |
@@ -28,11 +27,11 @@ | ||
28 | 27 | const name = api.about.obs.name(feed) |
29 | 28 | |
30 | 29 | const strings = api.translations.sync.strings() |
31 | 30 | |
32 | - const { followers } = api.contact.obs | |
31 | + // const { followers } = api.contact.obs | |
33 | 32 | |
34 | - const youFollowThem = computed(followers(feed), followers => followers.includes(myId)) | |
33 | + // const youFollowThem = computed(followers(feed), followers => followers.includes(myId)) | |
35 | 34 | // const theyFollowYou = computed(followers(myId), followers => followers.includes(feed)) |
36 | 35 | // const youAreFriends = computed([youFollowThem, theyFollowYou], (a, b) => a && b) |
37 | 36 | |
38 | 37 | // const ourRelationship = computed( |
@@ -42,16 +41,8 @@ | ||
42 | 41 | // if (theyFollowYou) return strings.userShow.state.theyFollow |
43 | 42 | // if (youFollowThem) return strings.userShow.state.youFollow |
44 | 43 | // } |
45 | 44 | // ) |
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 | - ) | |
54 | 45 | |
55 | 46 | const Link = api.app.html.link |
56 | 47 | const userEditButton = Link({ page: 'userEdit', feed }, h('i.fa.fa-pencil')) |
57 | 48 | const directMessageButton = Link({ page: 'threadNew', feed }, h('Button', strings.userShow.action.directMessage)) |
@@ -70,18 +61,18 @@ | ||
70 | 61 | |
71 | 62 | return h('Page -userShow', {title: name}, [ |
72 | 63 | h('div.content', [ |
73 | 64 | h('section.about', [ |
74 | - api.about.html.image(feed), | |
65 | + api.about.html.avatar(feed, 'large'), | |
75 | 66 | h('h1', [ |
76 | 67 | name, |
77 | 68 | feed === myId // Only expose own profile editing right now |
78 | 69 | ? userEditButton |
79 | 70 | : '' |
80 | 71 | ]), |
81 | 72 | feed !== myId |
82 | 73 | ? h('div.actions', [ |
83 | - h('div.friendship', followButton), | |
74 | + api.contact.html.follow(feed), | |
84 | 75 | h('div.directMessage', directMessageButton) |
85 | 76 | ]) |
86 | 77 | : '', |
87 | 78 | ]), |
app/page/userShow.mcss | ||
---|---|---|
@@ -8,22 +8,24 @@ | ||
8 | 8 | flex-direction: column |
9 | 9 | align-items: center |
10 | 10 | |
11 | 11 | img.Avatar { |
12 | - width: 5rem | |
13 | - height: 5rem | |
14 | - border-radius: 3rem | |
15 | 12 | } |
16 | 13 | |
17 | 14 | h1 { |
18 | 15 | font-weight: 300 |
19 | 16 | font-size: 1rem |
17 | + | |
18 | + display: flex | |
19 | + div.Link { | |
20 | + margin-left: .5rem | |
21 | + } | |
20 | 22 | } |
21 | 23 | |
22 | 24 | div.actions { |
23 | 25 | display: flex |
24 | 26 | |
25 | - div.friendship { | |
27 | + div.Follow { | |
26 | 28 | margin-right: 1rem |
27 | 29 | } |
28 | 30 | |
29 | 31 | div.directMessage { |
app/page/blogShow.js | ||
---|---|---|
@@ -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.mcss | ||
---|---|---|
@@ -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/sync/initialize/clickHandler.js | ||
---|---|---|
@@ -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.js | ||
---|---|---|
@@ -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.js | ||
---|---|---|
@@ -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.js | ||
---|---|---|
@@ -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.js | ||
---|---|---|
@@ -18,8 +18,9 @@ | ||
18 | 18 | { |
19 | 19 | about: require('./about'), |
20 | 20 | app: require('./app'), |
21 | 21 | blob: require('./blob'), |
22 | + contact: require('./contact'), | |
22 | 23 | //config: require('./ssb-config'), |
23 | 24 | config: require('./config'), |
24 | 25 | // group: require('./group'), |
25 | 26 | message: require('./message'), |
message/html/compose.js | ||
---|---|---|
@@ -21,9 +21,17 @@ | ||
21 | 21 | |
22 | 22 | exports.create = function (api) { |
23 | 23 | return nest('message.html.compose', compose) |
24 | 24 | |
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 | + | |
26 | 34 | const strings = api.translations.sync.strings() |
27 | 35 | const getProfileSuggestions = api.about.async.suggest() |
28 | 36 | const getChannelSuggestions = api.channel.async.suggest() |
29 | 37 | const getEmojiSuggestions = api.emoji.async.suggest() |
@@ -80,12 +88,12 @@ | ||
80 | 88 | } |
81 | 89 | // if fileInput is null, send button moves to the left side |
82 | 90 | // and we don't want that. |
83 | 91 | else |
84 | - fileInput = h('input', { style: {visibility: 'hidden'}}) | |
92 | + fileInput = h('input', { style: {visibility: 'hidden'} }) | |
85 | 93 | |
86 | 94 | var showPreview = Value(false) |
87 | - var previewBtn = h('Button', | |
95 | + var previewBtn = h('Button -preview', | |
88 | 96 | { |
89 | 97 | className: when(showPreview, '-primary'), |
90 | 98 | 'ev-click': () => showPreview.set(!showPreview()) |
91 | 99 | }, |
@@ -95,10 +103,10 @@ | ||
95 | 103 | |
96 | 104 | var publishBtn = h('Button -primary', { 'ev-click': publish }, strings.sendMessage) |
97 | 105 | |
98 | 106 | var actions = h('section.actions', [ |
99 | - fileInput, | |
100 | - previewBtn, | |
107 | + canAttach ? fileInput : '', | |
108 | + canPreview ? previewBtn : '', | |
101 | 109 | publishBtn |
102 | 110 | ]) |
103 | 111 | |
104 | 112 | var composer = h('Compose', { |
message/html/compose.mcss | ||
---|---|---|
@@ -6,36 +6,23 @@ | ||
6 | 6 | |
7 | 7 | textarea { |
8 | 8 | $fontBasic |
9 | 9 | |
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 | |
15 | 14 | |
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 | - | |
23 | 15 | :focus { |
24 | 16 | outline: none |
25 | - box-shadow: none | |
26 | 17 | } |
27 | - :disabled { | |
28 | - background-color: #f1f1f1 | |
29 | - cursor: not-allowed | |
30 | - } | |
31 | 18 | } |
32 | 19 | |
33 | 20 | section.actions { |
34 | 21 | display: flex |
35 | 22 | flex-direction: row |
36 | 23 | align-items: baseline |
37 | - justify-content: space-between | |
24 | + justify-content: flex-end | |
38 | 25 | |
39 | 26 | margin-top: .4rem |
40 | 27 | |
41 | 28 | input { flex-grow: 1 } |
message/html/channel.js | ||
---|---|---|
@@ -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.js | ||
---|---|---|
@@ -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/timeago.js | ||
---|---|---|
@@ -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/index.js | ||
---|---|---|
@@ -2,9 +2,12 @@ | ||
2 | 2 | async: { |
3 | 3 | publish: require('./async/publish'), |
4 | 4 | }, |
5 | 5 | html: { |
6 | + channel: require('./html/channel'), | |
6 | 7 | compose: require('./html/compose'), |
7 | - subject: require('./html/subject') | |
8 | + likes: require('./html/likes'), | |
9 | + subject: require('./html/subject'), | |
10 | + timeago: require('./html/timeago') | |
8 | 11 | } |
9 | 12 | } |
10 | 13 |
router/sync/routes.js | ||
---|---|---|
@@ -8,8 +8,9 @@ | ||
8 | 8 | exports.needs = nest({ |
9 | 9 | 'app.page.error': 'first', |
10 | 10 | 'app.page.blogIndex': 'first', |
11 | 11 | 'app.page.blogNew': 'first', |
12 | + 'app.page.blogShow': 'first', | |
12 | 13 | 'app.page.settings': 'first', |
13 | 14 | // 'app.page.channel': 'first', |
14 | 15 | // 'app.page.groupFind': 'first', |
15 | 16 | // 'app.page.groupIndex': 'first', |
@@ -30,9 +31,22 @@ | ||
30 | 31 | // route format: [ routeValidator, routeFunction ] |
31 | 32 | |
32 | 33 | const routes = [ |
33 | 34 | |
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 | |
35 | 49 | // [ location => location.page === 'threadNew' && location.channel, pages.threadNew ], |
36 | 50 | [ location => location.page === 'threadNew' && isFeed(location.feed), pages.threadNew ], |
37 | 51 | [ location => isMsg(location.key), pages.threadShow ], |
38 | 52 | |
@@ -47,14 +61,8 @@ | ||
47 | 61 | // [ location => location.page === 'groupNew', pages.groupNew ], |
48 | 62 | // // [ location => location.type === 'groupShow' && isMsg(location.key), pages.groupShow ], |
49 | 63 | // [ location => location.channel , pages.channel ], |
50 | 64 | |
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 | - | |
57 | 65 | [ location => location.page === 'settings', pages.settings ], |
58 | 66 | |
59 | 67 | // [ location => isBlob(location.blob), pages.image ], |
60 | 68 | [ location => isBlob(location.blob), (location) => { |
styles/button.mcss | ||
---|---|---|
@@ -1,10 +1,10 @@ | ||
1 | 1 | Button { |
2 | 2 | font-family: arial |
3 | - background-color: #fff | |
3 | + $backgroundPrimaryText | |
4 | 4 | |
5 | 5 | min-width: 6rem |
6 | - height: 1.2rem | |
6 | + height: 1.2em | |
7 | 7 | padding: .2rem 1rem |
8 | 8 | |
9 | 9 | border: 1px #b9b9b9 solid |
10 | 10 | border-radius: 10rem |
@@ -23,15 +23,23 @@ | ||
23 | 23 | } |
24 | 24 | |
25 | 25 | -primary { |
26 | 26 | $colorPrimary |
27 | + $font | |
27 | 28 | $borderPrimary |
28 | 29 | |
29 | 30 | :hover { |
30 | 31 | opacity: .9 |
31 | 32 | } |
32 | 33 | } |
33 | 34 | |
35 | + -channel { | |
36 | + $backgroundPrimary | |
37 | + $colorFontPrimary | |
38 | + font-size: .9rem | |
39 | + min-width: initial | |
40 | + } | |
41 | + | |
34 | 42 | -showMore { |
35 | 43 | width: 100% |
36 | 44 | |
37 | 45 | padding: .2rem 0 |
styles/global.mcss | ||
---|---|---|
@@ -1,7 +1,7 @@ | ||
1 | 1 | body { |
2 | 2 | $fontBasic |
3 | - background-color: #fff | |
3 | + $backgroundPrimaryText | |
4 | 4 | |
5 | 5 | margin: 0 |
6 | 6 | |
7 | 7 | // different to Page |
styles/markdown.mcss | ||
---|---|---|
@@ -5,8 +5,18 @@ | ||
5 | 5 | margin: .5rem 0 |
6 | 6 | border-radius: .5rem |
7 | 7 | } |
8 | 8 | |
9 | + // center blog images | |
10 | + p { | |
11 | + a { | |
12 | + img { | |
13 | + display: block | |
14 | + margin: auto | |
15 | + } | |
16 | + } | |
17 | + } | |
18 | + | |
9 | 19 | (img.emoji) { |
10 | 20 | margin: 0 |
11 | 21 | } |
12 | 22 | } |
styles/mixins.js | ||
---|---|---|
@@ -44,10 +44,10 @@ | ||
44 | 44 | $colorFontBasic { |
45 | 45 | color: #222 |
46 | 46 | } |
47 | 47 | |
48 | -$colorPrimaryFG { | |
49 | - color: #fff | |
48 | +$colorFontPrimary { | |
49 | + color: #5c6bc0 | |
50 | 50 | } |
51 | 51 | |
52 | 52 | $colorSubtle { |
53 | 53 | color: #999 |
@@ -56,28 +56,52 @@ | ||
56 | 56 | $backgroundPrimary { |
57 | 57 | background-color: #f5f6f7 |
58 | 58 | } |
59 | 59 | |
60 | +$backgroundPrimaryText { | |
61 | + background-color: #fff | |
62 | +} | |
63 | + | |
60 | 64 | $backgroundSelected { |
61 | 65 | background-color: #f0f1f2 |
62 | 66 | } |
63 | 67 | |
64 | 68 | $borderPrimary { |
65 | 69 | border: 1px #2f63ad solid |
66 | 70 | } |
67 | 71 | |
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 | |
72 | 78 | } |
73 | 79 | |
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 | |
78 | 86 | } |
79 | 87 | |
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 | + | |
80 | 104 | $markdownSmall { |
81 | 105 | div.Markdown { |
82 | 106 | h1, h2, h3, h4, h5, h6, p { |
83 | 107 | font-size: .9rem |
contact/html/follow.js | ||
---|---|---|
@@ -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 | + |
Built with git-ssb-web