git ssb

2+

mixmix / ticktack



Commit f1e1c0f3aedd176590017f411e2a923155169442

start adding compose box, debug threads

mix irving committed on 8/15/2017, 4:21:20 AM
Parent: 8f874fd336bd2bf7721b161985bb987d55bc8179

Files changed

app/html/app.jschanged
app/html/thread-card.jschanged
app/html/thread.jschanged
app/page/channel.jschanged
app/page/home.jschanged
app/page/threadShow.jschanged
blob/sync/url.jschanged
config.jschanged
main.jschanged
package-lock.jsonchanged
package.jsonchanged
about/async/suggest.jsadded
about/index.jsadded
message/html/compose.jsadded
message/html/compose.mcssadded
message/index.jsadded
app/html/app.jsView
@@ -27,8 +27,9 @@
2727 api.history.sync.push(link)
2828 })
2929
3030 api.history.obs.location()(render)
31+ api.history.obs.store()(his => console.log('history', his)) // REMOVE)
3132 api.history.sync.push({ page: 'home' })
3233 }
3334
3435 function render (location) {
app/html/thread-card.jsView
@@ -82,12 +82,13 @@
8282 subject(thread)
8383 ])
8484
8585 const lastReply = thread.replies && maxBy(thread.replies, r => r.timestamp)
86+ const replySample = lastReply ? subject(lastReply) : null
8687
87- var replySample = lastReply ? subject(lastReply) : null
88+ const onClick = opts.onClick || function () { api.history.sync.push(thread) }
8889
89- return h('div.thread', {'ev-click': () => api.history.sync.push(thread)}, [
90+ return h('div.thread', {'ev-click': onClick }, [
9091 h('div.context', threadIcon(thread)),
9192 h('div.content', [
9293 subjectEl,
9394 replySample ? h('div.reply', [
app/html/thread.jsView
@@ -13,13 +13,12 @@
1313
1414 exports.create = (api) => {
1515 return nest('app.html.thread', thread)
1616
17- function thread (id) {
18- // location here can expected to be: { page: 'home' }
19-
17+ function thread (root) {
18+ console.log('thread root', root)
2019 const myId = api.keys.sync.id()
21- const thread = api.feed.obs.thread(id)
20+ const thread = api.feed.obs.thread(root)
2221 const chunkedMessages = buildChunkedMessages(thread.messages)
2322
2423 const threadView = h('Thread',
2524 map(chunkedMessages, chunk => {
app/page/channel.jsView
@@ -26,9 +26,9 @@
2626 return nest('app.page.channel', function (location) {
2727 // location here can expected to be: { page: 'home' }
2828 var strings = api.translations.sync.strings()
2929
30- var container = h('div.container')
30+ var container = h('div.container', [])
3131
3232 var channelObs = api.state.obs.channel(location.channel)
3333
3434 //disable "Show More" button when we are at the last thread.
@@ -68,9 +68,9 @@
6868 api.app.html.nav(),
6969 threadsHtmlObs,
7070 h('button', {
7171 'ev-click': threadsHtmlObs.more,
72- disabled: disableShowMore
72+ disabled: disableShowMore
7373 }, [strings.showMore])
7474 ])
7575 })
7676 }
app/page/home.jsView
@@ -108,18 +108,15 @@
108108 h('section.updates -directMessage', [
109109 h('div.threads',
110110 groupedThreads
111111 .map(function (thread) {
112- var el = api.app.html.threadCard(thread)
113112
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
120117
121- return el
118+ return api.app.html.threadCard(thread, { onClick })
122119 })
123120 )
124121 ]),
125122 ])
app/page/threadShow.jsView
@@ -1,25 +1,39 @@
11 const nest = require('depnest')
22 const { h } = require('mutant')
3+const last = require('lodash/last')
4+const get = require('lodash/get')
35
46 exports.gives = nest('app.page.threadShow')
57
68 exports.needs = nest({
79 'app.html.nav': 'first',
8- 'app.html.thread': 'first'
10+ 'app.html.thread': 'first',
11+ 'message.html.compose': 'first'
912 })
1013
1114 exports.create = (api) => {
1215 return nest('app.page.threadShow', threadShow)
1316
1417 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
1620
17- const thread = api.app.html.thread(location.key)
21+ const thread = api.app.html.thread(root)
1822
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+
1932 return h('Page -threadShow', [
2033 h('h1', 'Private message'),
2134 api.app.html.nav(),
22- h('div.container', thread)
35+ h('div.container', thread),
36+ composer
2337 ])
2438 }
2539 }
blob/sync/url.jsView
@@ -6,13 +6,13 @@
66 'config.sync.load': 'first'
77 })
88
99 exports.create = function (api) {
10- return nest('blob.sync.url', function (id) {
10+ return nest('blob.sync.url', function (link) {
1111 var config = api.config.sync.load()
1212 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
1515 }
16- return `${prefix}/${encodeURIComponent(id)}`
16+ return `${prefix}/${encodeURIComponent(link)}`
1717 })
1818 }
config.jsView
@@ -2,10 +2,11 @@
22 const nest = require('depnest')
33 const ssbKeys = require('ssb-keys')
44 const Path = require('path')
55
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 = {
89 port: 43750,
910 blobsPort: 43751,
1011 ws: {
1112 port: 43751
@@ -16,9 +17,9 @@
1617 exports.create = (api) => {
1718 var config
1819 return nest('config.sync.load', () => {
1920 if (!config) {
20- config = Config(process.env.ssb_appname || appName, opts)
21+ config = Config(appName, opts)
2122 config.keys = ssbKeys.loadOrCreateSync(Path.join(config.path, 'secret'))
2223
2324 // HACK: fix offline on windows by specifying 127.0.0.1 instead of localhost (default)
2425 config.remote = `net:127.0.0.1:${config.port}~shs:${config.keys.id.slice(1).replace('.ed25519', '')}`
main.jsView
@@ -10,12 +10,14 @@
1010
1111 // from more specialized to more general
1212 const sockets = combine(
1313 {
14+ about: require('./about'),
1415 app: require('./app'),
1516 blob: require('./blob'),
1617 //config: require('./ssb-config'),
1718 config: require('./config'),
19+ message: require('./message'),
1820 router: require('./router'),
1921 styles: require('./styles'),
2022 translations: require('./translations/sync'),
2123 state: require('./state/obs'),
package-lock.jsonView
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.jsonView
@@ -19,11 +19,11 @@
1919 "dependencies": {
2020 "cross-script": "^1.0.5",
2121 "depject": "^4.1.0",
2222 "depnest": "^1.3.0",
23- "hypermore": "^2.0.0",
2423 "electron-default-menu": "^1.0.1",
2524 "electron-window-state": "^4.1.1",
25+ "hypermore": "^2.0.0",
2626 "insert-css": "^2.0.0",
2727 "libnested": "^1.2.1",
2828 "lodash": "^4.17.4",
2929 "micro-css": "^2.0.1",
@@ -38,19 +38,21 @@
3838 "pull-stream": "^3.6.0",
3939 "read-directory": "^2.1.0",
4040 "scuttlebot": "^10.4.4",
4141 "setimmediate": "^1.0.5",
42- "ssb-reduce-stream": "^1.0.1",
4342 "ssb-about": "^0.1.0",
4443 "ssb-backlinks": "^0.4.0",
4544 "ssb-blobs": "^1.1.3",
4645 "ssb-contacts": "0.0.2",
4746 "ssb-friends": "^2.2.1",
4847 "ssb-keys": "^7.0.10",
48+ "ssb-mentions": "^0.4.0",
4949 "ssb-private": "^0.1.2",
5050 "ssb-query": "^0.1.2",
51+ "ssb-reduce-stream": "^1.0.1",
5152 "ssb-ref": "^2.7.1",
5253 "ssb-ws": "^1.0.3",
54+ "suggest-box": "^2.2.3",
5355 "url": "^0.11.0"
5456 },
5557 "devDependencies": {
5658 "electron": "~1.7.5"
about/async/suggest.jsView
@@ -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.jsView
@@ -1,0 +1,5 @@
1+module.exports = {
2+ async: {
3+ suggest: require('./async/suggest')
4+ }
5+}
message/html/compose.jsView
@@ -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.mcssView
@@ -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+
message/index.jsView
@@ -1,0 +1,6 @@
1+module.exports = {
2+ html: {
3+ compose: require('./html/compose')
4+ }
5+}
6+

Built with git-ssb-web