Files: d1c4f8d0850bbfbb59186b4b79726d7de9011998 / app / page / home.js
6112 bytesRaw
1 | const nest = require('depnest') |
2 | const { h, computed } = require('mutant') |
3 | const {threadReduce} = require('ssb-reduce-stream') |
4 | const pull = require('pull-stream') |
5 | const isObject = require('lodash/isObject') |
6 | const isString = require('lodash/isString') |
7 | const last = require('lodash/last') |
8 | const get = require('lodash/get') |
9 | const More = require('hypermore') |
10 | exports.gives = nest('app.page.home') |
11 | const morphdom = require('morphdom') |
12 | const Next = require('pull-next') |
13 | |
14 | exports.needs = nest({ |
15 | 'about.html.image': 'first', |
16 | 'about.obs.name': 'first', |
17 | 'app.html.nav': 'first', |
18 | 'sbot.pull.log': 'first', |
19 | 'history.sync.push': 'first', |
20 | 'keys.sync.id': 'first', |
21 | 'message.sync.unbox': 'first', |
22 | 'message.html.markdown': 'first' |
23 | }) |
24 | |
25 | var strings = { |
26 | showMore: "Show More", |
27 | channels: "Channels", |
28 | directMessages: "Direct Messages", |
29 | replySymbol: "> " |
30 | } |
31 | |
32 | function firstLine (text) { |
33 | if(text.length < 80 && !~text.indexOf('\n')) return text |
34 | |
35 | var line = '' |
36 | var lineNumber = 0 |
37 | while (line.length === 0) { |
38 | const rawLine = text.split('\n')[lineNumber] |
39 | line = trimLeadingMentions(rawLine) |
40 | |
41 | lineNumber++ |
42 | } |
43 | |
44 | var sample = line.substring(0, 80) |
45 | if (hasBrokenLink(sample)) |
46 | sample = sample + line.substring(81).match(/[^\)]*\)/)[0] |
47 | |
48 | return sample |
49 | |
50 | function trimLeadingMentions (str) { |
51 | return str.replace(/^(\s*\[@[^\)]+\)\s*)*/, '') |
52 | // deletes any number of pattern " [@...) " from start of line |
53 | } |
54 | |
55 | function hasBrokenLink (str) { |
56 | return /\[[^\]]*\]\([^\)]*$/.test(str) |
57 | // matches "[name](start_of_link" |
58 | } |
59 | } |
60 | |
61 | exports.create = (api) => { |
62 | return nest('app.page.home', function (location) { |
63 | // location here can expected to be: { page: 'home' } |
64 | |
65 | var container = h('div.container', []) |
66 | |
67 | function subject (msg) { |
68 | const { subject, text } = msg.value.content |
69 | return api.message.html.markdown(firstLine(subject|| text)) |
70 | } |
71 | |
72 | function link(location) { |
73 | return {'ev-click': () => api.history.sync.push(location)} |
74 | } |
75 | |
76 | function item (context, thread, opts = {}) { |
77 | if(!thread.value) return |
78 | |
79 | const subjectEl = h('div.subject', [ |
80 | opts.nameRecipients |
81 | ? h('div.recps', buildRecipientNames(thread).map(recp => h('div.recp', recp))) |
82 | : null, |
83 | subject(thread) |
84 | ]) |
85 | |
86 | const lastReply = thread.replies && last(thread.replies) |
87 | const replyEl = lastReply |
88 | ? h('div.reply', [ |
89 | h('div.replySymbol', strings.replySymbol), |
90 | subject(lastReply) |
91 | ]) |
92 | : null |
93 | |
94 | |
95 | // REFACTOR: move this to a template? |
96 | function buildRecipientNames (thread) { |
97 | const myId = api.keys.sync.id() |
98 | |
99 | return thread.value.content.recps |
100 | .map(link => isString(link) ? link : link.link) |
101 | .filter(link => link !== myId) |
102 | .map(api.about.obs.name) |
103 | } |
104 | |
105 | return h('div.thread', link(thread), [ |
106 | h('div.context', context), |
107 | h('div.content', [ |
108 | subjectEl, |
109 | replyEl |
110 | ]) |
111 | ]) |
112 | } |
113 | |
114 | function threadGroup (threads, obj, toContext, opts) { |
115 | // threads = a state object for all the types of threads |
116 | // obj = a map of keys to root ids, where key (channel | group | concatenated list of pubkeys) |
117 | // toContext = fn that derives the context of the group |
118 | // opts = { nameRecipients } |
119 | |
120 | var groupEl = h('div.threads') |
121 | for(var k in obj) { |
122 | var id = obj[k] |
123 | var thread = get(threads, ['roots', id]) |
124 | if(thread && thread.value) { |
125 | var el = item(toContext(k, thread), thread, opts) |
126 | if(el) groupEl.appendChild(el) |
127 | } |
128 | } |
129 | return groupEl |
130 | } |
131 | |
132 | var initial |
133 | try { initial = JSON.parse(localStorage.threadsState) } |
134 | catch (_) { } |
135 | var lastTimestamp = initial ? initial.last : Date.now() |
136 | |
137 | var timer |
138 | function update (threadsState) { |
139 | clearTimeout(timer) |
140 | setTimeout(function () { |
141 | threadsState.last = lastTimestamp |
142 | localStorage.threadsState = JSON.stringify(threadsState) |
143 | }, 1000) |
144 | } |
145 | |
146 | var threadsObs = More( |
147 | threadReduce, |
148 | pull( |
149 | Next(function () { |
150 | return api.sbot.pull.log({reverse: true, limit: 500, lte: lastTimestamp}) |
151 | }), |
152 | pull.map(function (data) { |
153 | lastTimestamp = data.timestamp |
154 | if(isObject(data.value.content)) return data |
155 | return api.message.sync.unbox(data) |
156 | }), |
157 | pull.filter(Boolean) |
158 | ), |
159 | function render (threads) { |
160 | update(threads) |
161 | morphdom(container, |
162 | h('div.container', [ |
163 | //private section |
164 | h('section.updates -directMessage', [ |
165 | h('h2', strings.directMessages), |
166 | threadGroup( |
167 | threads, |
168 | threads.private, |
169 | function (_, msg) { |
170 | // NB: msg passed in is actually a 'thread', but only care about root msg |
171 | const myId = api.keys.sync.id() |
172 | |
173 | return msg.value.content.recps |
174 | .map(link => isString(link) ? link : link.link) |
175 | .filter(link => link !== myId) |
176 | .map(api.about.html.image) |
177 | }, |
178 | { nameRecipients: true } |
179 | ) |
180 | ]), |
181 | //channels section |
182 | h('section.updates -channel', [ |
183 | h('h2', strings.channels), |
184 | threadGroup( |
185 | threads, |
186 | threads.channels, |
187 | ch => '#'+ch |
188 | ) |
189 | ]), |
190 | //group section |
191 | h('section.updates -group', [ |
192 | h('h2', 'Groups'), |
193 | 'TODO: complete + enable when groups are live' |
194 | // threadGroup( |
195 | // threads, |
196 | // threads.groups, |
197 | // toName ... |
198 | // ) |
199 | ]) |
200 | ]) |
201 | ) |
202 | return container |
203 | }, |
204 | initial |
205 | ) |
206 | |
207 | return h('Page -home', [ |
208 | h('h1', 'Home'), |
209 | api.app.html.nav(), |
210 | threadsObs, |
211 | h('button', {'ev-click': threadsObs.more}, [strings.showMore]) |
212 | ]) |
213 | }) |
214 | } |
215 | |
216 |
Built with git-ssb-web