Commit f1e1c0f3aedd176590017f411e2a923155169442
start adding compose box, debug threads
mix irving committed on 8/15/2017, 4:21:20 AMParent: 8f874fd336bd2bf7721b161985bb987d55bc8179
Files changed
app/html/app.js | changed |
app/html/thread-card.js | changed |
app/html/thread.js | changed |
app/page/channel.js | changed |
app/page/home.js | changed |
app/page/threadShow.js | changed |
blob/sync/url.js | changed |
config.js | changed |
main.js | changed |
package-lock.json | changed |
package.json | changed |
about/async/suggest.js | added |
about/index.js | added |
message/html/compose.js | added |
message/html/compose.mcss | added |
message/index.js | added |
app/html/app.js | ||
---|---|---|
@@ -27,8 +27,9 @@ | ||
27 | 27 | api.history.sync.push(link) |
28 | 28 | }) |
29 | 29 | |
30 | 30 | api.history.obs.location()(render) |
31 | + api.history.obs.store()(his => console.log('history', his)) // REMOVE) | |
31 | 32 | api.history.sync.push({ page: 'home' }) |
32 | 33 | } |
33 | 34 | |
34 | 35 | function render (location) { |
app/html/thread-card.js | ||
---|---|---|
@@ -82,12 +82,13 @@ | ||
82 | 82 | subject(thread) |
83 | 83 | ]) |
84 | 84 | |
85 | 85 | const lastReply = thread.replies && maxBy(thread.replies, r => r.timestamp) |
86 | + const replySample = lastReply ? subject(lastReply) : null | |
86 | 87 | |
87 | - var replySample = lastReply ? subject(lastReply) : null | |
88 | + const onClick = opts.onClick || function () { api.history.sync.push(thread) } | |
88 | 89 | |
89 | - return h('div.thread', {'ev-click': () => api.history.sync.push(thread)}, [ | |
90 | + return h('div.thread', {'ev-click': onClick }, [ | |
90 | 91 | h('div.context', threadIcon(thread)), |
91 | 92 | h('div.content', [ |
92 | 93 | subjectEl, |
93 | 94 | replySample ? h('div.reply', [ |
app/html/thread.js | ||
---|---|---|
@@ -13,13 +13,12 @@ | ||
13 | 13 | |
14 | 14 | exports.create = (api) => { |
15 | 15 | return nest('app.html.thread', thread) |
16 | 16 | |
17 | - function thread (id) { | |
18 | - // location here can expected to be: { page: 'home' } | |
19 | - | |
17 | + function thread (root) { | |
18 | + console.log('thread root', root) | |
20 | 19 | const myId = api.keys.sync.id() |
21 | - const thread = api.feed.obs.thread(id) | |
20 | + const thread = api.feed.obs.thread(root) | |
22 | 21 | const chunkedMessages = buildChunkedMessages(thread.messages) |
23 | 22 | |
24 | 23 | const threadView = h('Thread', |
25 | 24 | map(chunkedMessages, chunk => { |
app/page/channel.js | ||
---|---|---|
@@ -26,9 +26,9 @@ | ||
26 | 26 | return nest('app.page.channel', function (location) { |
27 | 27 | // location here can expected to be: { page: 'home' } |
28 | 28 | var strings = api.translations.sync.strings() |
29 | 29 | |
30 | - var container = h('div.container') | |
30 | + var container = h('div.container', []) | |
31 | 31 | |
32 | 32 | var channelObs = api.state.obs.channel(location.channel) |
33 | 33 | |
34 | 34 | //disable "Show More" button when we are at the last thread. |
@@ -68,9 +68,9 @@ | ||
68 | 68 | api.app.html.nav(), |
69 | 69 | threadsHtmlObs, |
70 | 70 | h('button', { |
71 | 71 | 'ev-click': threadsHtmlObs.more, |
72 | - disabled: disableShowMore | |
72 | + disabled: disableShowMore | |
73 | 73 | }, [strings.showMore]) |
74 | 74 | ]) |
75 | 75 | }) |
76 | 76 | } |
app/page/home.js | ||
---|---|---|
@@ -108,18 +108,15 @@ | ||
108 | 108 | h('section.updates -directMessage', [ |
109 | 109 | h('div.threads', |
110 | 110 | groupedThreads |
111 | 111 | .map(function (thread) { |
112 | - var el = api.app.html.threadCard(thread) | |
113 | 112 | |
114 | - if(thread.value.content.channel) { | |
115 | - el.onclick = function (ev) { | |
116 | - api.history.sync.push({channel: thread.value.content.channel}) | |
117 | - ev.preventDefault() | |
118 | - } | |
119 | - } | |
113 | + const channel = thread.value.content.channel | |
114 | + const onClick = channel | |
115 | + ? (ev) => api.history.sync.push({ channel }) | |
116 | + : null | |
120 | 117 | |
121 | - return el | |
118 | + return api.app.html.threadCard(thread, { onClick }) | |
122 | 119 | }) |
123 | 120 | ) |
124 | 121 | ]), |
125 | 122 | ]) |
app/page/threadShow.js | ||
---|---|---|
@@ -1,25 +1,39 @@ | ||
1 | 1 | const nest = require('depnest') |
2 | 2 | const { h } = require('mutant') |
3 | +const last = require('lodash/last') | |
4 | +const get = require('lodash/get') | |
3 | 5 | |
4 | 6 | exports.gives = nest('app.page.threadShow') |
5 | 7 | |
6 | 8 | exports.needs = nest({ |
7 | 9 | 'app.html.nav': 'first', |
8 | - 'app.html.thread': 'first' | |
10 | + 'app.html.thread': 'first', | |
11 | + 'message.html.compose': 'first' | |
9 | 12 | }) |
10 | 13 | |
11 | 14 | exports.create = (api) => { |
12 | 15 | return nest('app.page.threadShow', threadShow) |
13 | 16 | |
14 | 17 | function threadShow (location) { |
15 | - // location here can expected to be an ssb-message | |
18 | + // location = a thread (message decorated with replies) | |
19 | + const { key: root, replies, channel } = location | |
16 | 20 | |
17 | - const thread = api.app.html.thread(location.key) | |
21 | + const thread = api.app.html.thread(root) | |
18 | 22 | |
23 | + const meta = { | |
24 | + type: 'post', | |
25 | + root, | |
26 | + branch: get(last(location.replies), 'key'), // >> lastId? CHECK THIS LOGIC | |
27 | + channel, | |
28 | + recps: get(location, 'value.content.recps') | |
29 | + } | |
30 | + const composer = api.message.html.compose({ meta }) | |
31 | + | |
19 | 32 | return h('Page -threadShow', [ |
20 | 33 | h('h1', 'Private message'), |
21 | 34 | api.app.html.nav(), |
22 | - h('div.container', thread) | |
35 | + h('div.container', thread), | |
36 | + composer | |
23 | 37 | ]) |
24 | 38 | } |
25 | 39 | } |
blob/sync/url.js | ||
---|---|---|
@@ -6,13 +6,13 @@ | ||
6 | 6 | 'config.sync.load': 'first' |
7 | 7 | }) |
8 | 8 | |
9 | 9 | exports.create = function (api) { |
10 | - return nest('blob.sync.url', function (id) { | |
10 | + return nest('blob.sync.url', function (link) { | |
11 | 11 | var config = api.config.sync.load() |
12 | 12 | var prefix = config.blobsPrefix != null ? config.blobsPrefix : `http://localhost:${config.ws.port}/blobs/get` |
13 | - if (id && typeof id.link === 'string') { | |
14 | - id = id.link | |
13 | + if (link && typeof link.link === 'string') { | |
14 | + link = link.link | |
15 | 15 | } |
16 | - return `${prefix}/${encodeURIComponent(id)}` | |
16 | + return `${prefix}/${encodeURIComponent(link)}` | |
17 | 17 | }) |
18 | 18 | } |
config.js | ||
---|---|---|
@@ -2,10 +2,11 @@ | ||
2 | 2 | const nest = require('depnest') |
3 | 3 | const ssbKeys = require('ssb-keys') |
4 | 4 | const Path = require('path') |
5 | 5 | |
6 | -const appName = 'ssb' //'ticktack-ssb' | |
7 | -const opts = process.env.ssb_appname== 'ssb' ? {} :{ | |
6 | +const appName = process.env.ssb_appname || 'ticktack-ssb' | |
7 | +// const opts = process.env.ssb_appname== 'ssb' ? {} :{ | |
8 | +const opts = { | |
8 | 9 | port: 43750, |
9 | 10 | blobsPort: 43751, |
10 | 11 | ws: { |
11 | 12 | port: 43751 |
@@ -16,9 +17,9 @@ | ||
16 | 17 | exports.create = (api) => { |
17 | 18 | var config |
18 | 19 | return nest('config.sync.load', () => { |
19 | 20 | if (!config) { |
20 | - config = Config(process.env.ssb_appname || appName, opts) | |
21 | + config = Config(appName, opts) | |
21 | 22 | config.keys = ssbKeys.loadOrCreateSync(Path.join(config.path, 'secret')) |
22 | 23 | |
23 | 24 | // HACK: fix offline on windows by specifying 127.0.0.1 instead of localhost (default) |
24 | 25 | config.remote = `net:127.0.0.1:${config.port}~shs:${config.keys.id.slice(1).replace('.ed25519', '')}` |
main.js | ||
---|---|---|
@@ -10,12 +10,14 @@ | ||
10 | 10 | |
11 | 11 | // from more specialized to more general |
12 | 12 | const sockets = combine( |
13 | 13 | { |
14 | + about: require('./about'), | |
14 | 15 | app: require('./app'), |
15 | 16 | blob: require('./blob'), |
16 | 17 | //config: require('./ssb-config'), |
17 | 18 | config: require('./config'), |
19 | + message: require('./message'), | |
18 | 20 | router: require('./router'), |
19 | 21 | styles: require('./styles'), |
20 | 22 | translations: require('./translations/sync'), |
21 | 23 | state: require('./state/obs'), |
package-lock.json | ||
---|---|---|
The diff is too large to show. Use a local git client to view these changes. Old file size: 190988 bytes New file size: 191797 bytes |
package.json | ||
---|---|---|
@@ -19,11 +19,11 @@ | ||
19 | 19 | "dependencies": { |
20 | 20 | "cross-script": "^1.0.5", |
21 | 21 | "depject": "^4.1.0", |
22 | 22 | "depnest": "^1.3.0", |
23 | - "hypermore": "^2.0.0", | |
24 | 23 | "electron-default-menu": "^1.0.1", |
25 | 24 | "electron-window-state": "^4.1.1", |
25 | + "hypermore": "^2.0.0", | |
26 | 26 | "insert-css": "^2.0.0", |
27 | 27 | "libnested": "^1.2.1", |
28 | 28 | "lodash": "^4.17.4", |
29 | 29 | "micro-css": "^2.0.1", |
@@ -38,19 +38,21 @@ | ||
38 | 38 | "pull-stream": "^3.6.0", |
39 | 39 | "read-directory": "^2.1.0", |
40 | 40 | "scuttlebot": "^10.4.4", |
41 | 41 | "setimmediate": "^1.0.5", |
42 | - "ssb-reduce-stream": "^1.0.1", | |
43 | 42 | "ssb-about": "^0.1.0", |
44 | 43 | "ssb-backlinks": "^0.4.0", |
45 | 44 | "ssb-blobs": "^1.1.3", |
46 | 45 | "ssb-contacts": "0.0.2", |
47 | 46 | "ssb-friends": "^2.2.1", |
48 | 47 | "ssb-keys": "^7.0.10", |
48 | + "ssb-mentions": "^0.4.0", | |
49 | 49 | "ssb-private": "^0.1.2", |
50 | 50 | "ssb-query": "^0.1.2", |
51 | + "ssb-reduce-stream": "^1.0.1", | |
51 | 52 | "ssb-ref": "^2.7.1", |
52 | 53 | "ssb-ws": "^1.0.3", |
54 | + "suggest-box": "^2.2.3", | |
53 | 55 | "url": "^0.11.0" |
54 | 56 | }, |
55 | 57 | "devDependencies": { |
56 | 58 | "electron": "~1.7.5" |
about/async/suggest.js | ||
---|---|---|
@@ -1,0 +1,103 @@ | ||
1 | +const nest = require('depnest') | |
2 | +const { Struct, map, concat, dictToCollection, computed, lookup, watch, keys, resolve } = require('mutant') | |
3 | + | |
4 | +exports.gives = nest('about.async.suggest') | |
5 | + | |
6 | +exports.needs = nest({ | |
7 | + 'about.obs.groupedValues': 'first', | |
8 | + 'about.obs.name': 'first', | |
9 | + 'about.obs.imageUrl': 'first', | |
10 | + 'contact.obs.following': 'first', | |
11 | + 'feed.obs.recent': 'first', | |
12 | + 'keys.sync.id': 'first' | |
13 | +}) | |
14 | + | |
15 | +exports.create = function (api) { | |
16 | + var suggestions = null | |
17 | + var recentSuggestions = null | |
18 | + | |
19 | + return nest('about.async.suggest', suggest) | |
20 | + | |
21 | + function suggest () { | |
22 | + loadSuggestions() | |
23 | + return function (word) { | |
24 | + if (!word) return recentSuggestions() | |
25 | + | |
26 | + return suggestions() | |
27 | + .filter((item) => { | |
28 | + return item.title.toLowerCase().startsWith(word.toLowerCase()) | |
29 | + }) | |
30 | + .reverse() | |
31 | + } | |
32 | + } | |
33 | + | |
34 | + function loadSuggestions () { | |
35 | + if (suggestions) return | |
36 | + | |
37 | + var id = api.keys.sync.id() | |
38 | + var following = api.contact.obs.following(id) | |
39 | + var recentlyUpdated = api.feed.obs.recent() | |
40 | + var contacts = computed([following, recentlyUpdated], (a, b) => { | |
41 | + var result = new Set (a) | |
42 | + b.forEach(item => result.add(item)) | |
43 | + | |
44 | + return Array.from(result) | |
45 | + }) | |
46 | + | |
47 | + recentSuggestions = map( | |
48 | + computed(recentlyUpdated, (items) => Array.from(items).slice(0, 10)), | |
49 | + suggestion, | |
50 | + {idle: true} | |
51 | + ) | |
52 | + | |
53 | + const suggestionsRecord = lookup(contacts, contact => { | |
54 | + return [contact, keys(api.about.obs.groupedValues(contact, 'name'))] | |
55 | + }) | |
56 | + | |
57 | + suggestions = concat( | |
58 | + map(dictToCollection(suggestionsRecord), pluralSuggestions, {idle: true}) | |
59 | + ) | |
60 | + | |
61 | + watch(recentSuggestions) | |
62 | + watch(suggestions) | |
63 | + } | |
64 | + | |
65 | + function pluralSuggestions (item) { | |
66 | + const id = resolve(item.key) | |
67 | + return map(item.value, name => { | |
68 | + return Struct({ | |
69 | + id, | |
70 | + title: name, | |
71 | + subtitle: subtitle(id, name), | |
72 | + value: computed([name, id], mention), | |
73 | + image: api.about.obs.imageUrl(id), | |
74 | + showBoth: true | |
75 | + }) | |
76 | + }) | |
77 | + } | |
78 | + | |
79 | + function suggestion (id) { | |
80 | + var name = api.about.obs.name(id) | |
81 | + return Struct({ | |
82 | + title: name, | |
83 | + id, | |
84 | + subtitle: id.substring(0, 10), | |
85 | + value: computed([name, id], mention), | |
86 | + image: api.about.obs.imageUrl(id), | |
87 | + showBoth: true | |
88 | + }) | |
89 | + } | |
90 | + | |
91 | + function subtitle (id, name) { | |
92 | + return computed([api.about.obs.name(id)], commonName => { | |
93 | + return name.toLowerCase() === commonName.toLowerCase() | |
94 | + ? id.substring(0, 10) | |
95 | + : `${commonName} ${id.substring(0, 10)}` | |
96 | + }) | |
97 | + } | |
98 | +} | |
99 | + | |
100 | +function mention (name, id) { | |
101 | + return `[@${name}](${id})` | |
102 | +} | |
103 | + |
about/index.js | ||
---|---|---|
@@ -1,0 +1,5 @@ | ||
1 | +module.exports = { | |
2 | + async: { | |
3 | + suggest: require('./async/suggest') | |
4 | + } | |
5 | +} |
message/html/compose.js | ||
---|---|---|
@@ -1,0 +1,187 @@ | ||
1 | +const nest = require('depnest') | |
2 | +const { h, when, send, resolve, Value, computed } = require('mutant') | |
3 | +const assign = require('lodash/assign') | |
4 | +const ssbMentions = require('ssb-mentions') | |
5 | +const addSuggest = require('suggest-box') | |
6 | + | |
7 | +exports.gives = nest('message.html.compose') | |
8 | + | |
9 | +exports.needs = nest({ | |
10 | + 'about.async.suggest': 'first', | |
11 | + 'blob.html.input': 'first', | |
12 | + // 'channel.async.suggest': 'first', | |
13 | + 'emoji.sync.names': 'first', | |
14 | + 'emoji.sync.url': 'first', | |
15 | + 'message.async.publish': 'first', | |
16 | + // 'message.html.confirm': 'first' | |
17 | +}) | |
18 | + | |
19 | +exports.create = function (api) { | |
20 | + return nest('message.html.compose', compose) | |
21 | + | |
22 | + function compose ({ shrink = true, meta, prepublish, placeholder = 'Write a message' }, cb) { | |
23 | + var files = [] | |
24 | + var filesById = {} | |
25 | + var channelInputFocused = Value(false) | |
26 | + var textAreaFocused = Value(false) | |
27 | + var focused = computed([channelInputFocused, textAreaFocused], (a, b) => a || b) | |
28 | + var hasContent = Value(false) | |
29 | + var getProfileSuggestions = api.about.async.suggest() | |
30 | + // var getChannelSuggestions = api.channel.async.suggest() | |
31 | + | |
32 | + var blurTimeout = null | |
33 | + | |
34 | + var expanded = computed([shrink, focused, hasContent], (shrink, focused, hasContent) => { | |
35 | + if (!shrink || hasContent) return true | |
36 | + | |
37 | + return focused | |
38 | + }) | |
39 | + | |
40 | + // var channelInput = h('input.channel', { | |
41 | + // 'ev-input': () => hasContent.set(!!channelInput.value), | |
42 | + // 'ev-keyup': ev => { | |
43 | + // ev.target.value = ev.target.value.replace(/^#*([\w@%&])/, '#$1') | |
44 | + // }, | |
45 | + // 'ev-blur': () => { | |
46 | + // clearTimeout(blurTimeout) | |
47 | + // blurTimeout = setTimeout(() => channelInputFocused.set(false), 200) | |
48 | + // }, | |
49 | + // 'ev-focus': send(channelInputFocused.set, true), | |
50 | + // placeholder: '#channel (optional)', | |
51 | + // value: computed(meta.channel, ch => ch ? '#' + ch : null), | |
52 | + // disabled: when(meta.channel, true), | |
53 | + // title: when(meta.channel, 'Reply is in same channel as original message') | |
54 | + // }) | |
55 | + | |
56 | + var textArea = h('textarea', { | |
57 | + 'ev-input': () => hasContent.set(!!textArea.value), | |
58 | + 'ev-blur': () => { | |
59 | + clearTimeout(blurTimeout) | |
60 | + blurTimeout = setTimeout(() => textAreaFocused.set(false), 200) | |
61 | + }, | |
62 | + 'ev-focus': send(textAreaFocused.set, true), | |
63 | + placeholder | |
64 | + }) | |
65 | + textArea.publish = publish // TODO: fix - clunky api for the keyboard shortcut to target | |
66 | + | |
67 | + var fileInput = api.blob.html.input(file => { | |
68 | + files.push(file) | |
69 | + filesById[file.link] = file | |
70 | + | |
71 | + var embed = file.type.match(/^image/) ? '!' : '' | |
72 | + var spacer = embed ? '\n' : ' ' | |
73 | + var insertLink = spacer + embed + '[' + file.name + ']' + '(' + file.link + ')' + spacer | |
74 | + | |
75 | + var pos = textArea.selectionStart | |
76 | + textArea.value = textArea.value.slice(0, pos) + insertLink + textArea.value.slice(pos) | |
77 | + | |
78 | + console.log('added:', file) | |
79 | + }) | |
80 | + | |
81 | + fileInput.onclick = () => hasContent.set(true) | |
82 | + | |
83 | + var publishBtn = h('button', { 'ev-click': publish }, 'Publish') | |
84 | + | |
85 | + var actions = h('section.actions', [ | |
86 | + fileInput, | |
87 | + publishBtn | |
88 | + ]) | |
89 | + | |
90 | + var composer = h('Compose', { | |
91 | + classList: when(expanded, '-expanded', '-contracted') | |
92 | + }, [ | |
93 | + // channelInput, | |
94 | + textArea, | |
95 | + actions | |
96 | + ]) | |
97 | + | |
98 | + // addSuggest(channelInput, (inputText, cb) => { | |
99 | + // if (inputText[0] === '#') { | |
100 | + // cb(null, getChannelSuggestions(inputText.slice(1))) | |
101 | + // } | |
102 | + // }, {cls: 'SuggestBox'}) | |
103 | + // channelInput.addEventListener('suggestselect', ev => { | |
104 | + // channelInput.value = ev.detail.id // HACK : this over-rides the markdown value | |
105 | + // }) | |
106 | + | |
107 | + addSuggest(textArea, (inputText, cb) => { | |
108 | + if (inputText[0] === '@') { | |
109 | + cb(null, getProfileSuggestions(inputText.slice(1))) | |
110 | + // } else if (inputText[0] === '#') { | |
111 | + // cb(null, getChannelSuggestions(inputText.slice(1))) | |
112 | + } else if (inputText[0] === ':') { | |
113 | + // suggest emojis | |
114 | + var word = inputText.slice(1) | |
115 | + if (word[word.length - 1] === ':') { | |
116 | + word = word.slice(0, -1) | |
117 | + } | |
118 | + // TODO: when no emoji typed, list some default ones | |
119 | + cb(null, api.emoji.sync.names().filter(function (name) { | |
120 | + return name.slice(0, word.length) === word | |
121 | + }).slice(0, 100).map(function (emoji) { | |
122 | + return { | |
123 | + image: api.emoji.sync.url(emoji), | |
124 | + title: emoji, | |
125 | + subtitle: emoji, | |
126 | + value: ':' + emoji + ':' | |
127 | + } | |
128 | + })) | |
129 | + } | |
130 | + }, {cls: 'SuggestBox'}) | |
131 | + | |
132 | + return composer | |
133 | + | |
134 | + // scoped | |
135 | + | |
136 | + function publish () { | |
137 | + publishBtn.disabled = true | |
138 | + | |
139 | + // const channel = channelInput.value.startsWith('#') | |
140 | + // ? channelInput.value.substr(1).trim() | |
141 | + // : channelInput.value.trim() | |
142 | + const mentions = ssbMentions(textArea.value).map(mention => { | |
143 | + // merge markdown-detected mention with file info | |
144 | + var file = filesById[mention.link] | |
145 | + if (file) { | |
146 | + if (file.type) mention.type = file.type | |
147 | + if (file.size) mention.size = file.size | |
148 | + } | |
149 | + return mention | |
150 | + }) | |
151 | + | |
152 | + var content = assign({}, resolve(meta), { | |
153 | + text: textArea.value, | |
154 | + // channel, | |
155 | + mentions | |
156 | + }) | |
157 | + | |
158 | + // if (!channel) delete content.channel | |
159 | + if (!content.channel) delete content.channel | |
160 | + if (!mentions.length) delete content.mentions | |
161 | + if (content.recps && content.recps.length === 0) delete content.recps | |
162 | + | |
163 | + try { | |
164 | + if (typeof prepublish === 'function') { | |
165 | + content = prepublish(content) | |
166 | + } | |
167 | + } catch (err) { | |
168 | + publishBtn.disabled = false | |
169 | + if (cb) cb(err) | |
170 | + else throw err | |
171 | + } | |
172 | + | |
173 | + debugger | |
174 | + | |
175 | + api.message.async.publish(content, done) | |
176 | + // return api.message.html.confirm(content, done) | |
177 | + | |
178 | + function done (err, msg) { | |
179 | + publishBtn.disabled = false | |
180 | + if (err) throw err | |
181 | + else if (msg) textArea.value = '' | |
182 | + if (cb) cb(err, msg) | |
183 | + } | |
184 | + } | |
185 | + } | |
186 | +} | |
187 | + |
message/html/compose.mcss | ||
---|---|---|
@@ -1,0 +1,118 @@ | ||
1 | +Compose { | |
2 | + display: flex | |
3 | + flex-direction: column | |
4 | + | |
5 | + padding: .5rem .5rem 1rem 6rem | |
6 | + | |
7 | + textarea { | |
8 | + border: 1px solid gainsboro | |
9 | + border-top-left-radius: 0 | |
10 | + border-top-right-radius: 0 | |
11 | + } | |
12 | + | |
13 | + input.channel { | |
14 | + border: 1px solid gainsboro | |
15 | + border-bottom: none | |
16 | + border-bottom-left-radius: 0 | |
17 | + border-bottom-right-radius: 0 | |
18 | + padding: .5rem | |
19 | + | |
20 | + :focus { | |
21 | + outline: none | |
22 | + box-shadow: none | |
23 | + } | |
24 | + :disabled { | |
25 | + background-color: #f1f1f1 | |
26 | + cursor: not-allowed | |
27 | + } | |
28 | + } | |
29 | + | |
30 | + section.actions { | |
31 | + display: flex | |
32 | + flex-direction: row | |
33 | + align-items: baseline | |
34 | + justify-content: space-between | |
35 | + | |
36 | + margin-top: .4rem | |
37 | + | |
38 | + input[type="file"] { | |
39 | + | |
40 | + padding: .5rem 0 | |
41 | + | |
42 | + width: 2.5rem | |
43 | + height: 1.5rem | |
44 | + color: transparent | |
45 | + | |
46 | + ::-webkit-file-upload-button { | |
47 | + visibility: hidden | |
48 | + } | |
49 | + | |
50 | + ::before { | |
51 | + $composeButton | |
52 | + padding-top: .3rem | |
53 | + | |
54 | + content: '๐' | |
55 | + font-size: 1rem | |
56 | + | |
57 | + outline: none | |
58 | + white-space: nowrap | |
59 | + -webkit-user-select: none | |
60 | + } | |
61 | + | |
62 | + :active, :focus { | |
63 | + outline: none | |
64 | + box-shadow: none | |
65 | + } | |
66 | + } | |
67 | + | |
68 | + button { | |
69 | + $composeButton | |
70 | + | |
71 | + text-transform: uppercase | |
72 | + font-weight: bold | |
73 | + font-size: .7rem | |
74 | + } | |
75 | + } | |
76 | + | |
77 | + -expanded { | |
78 | + textarea { | |
79 | + height: 200px | |
80 | + transition: height .15s ease-out | |
81 | + } | |
82 | + | |
83 | + input.channel { | |
84 | + display: flex | |
85 | + } | |
86 | + | |
87 | + section.actions { | |
88 | + display: flex | |
89 | + } | |
90 | + } | |
91 | + | |
92 | + -contracted { | |
93 | + textarea { | |
94 | + height: 50px | |
95 | + transition: height .15s ease-in | |
96 | + } | |
97 | + | |
98 | + input.channel { | |
99 | + display: none | |
100 | + } | |
101 | + | |
102 | + section.actions { | |
103 | + display: none | |
104 | + } | |
105 | + } | |
106 | + | |
107 | +} | |
108 | + | |
109 | +$composeButton { | |
110 | + background: #fff | |
111 | + color: #666 | |
112 | + border: 1px solid #bbb | |
113 | + border-radius: .5rem | |
114 | + padding: .5rem | |
115 | + margin: 0 | |
116 | + cursor: pointer | |
117 | +} | |
118 | + |
Built with git-ssb-web