exports.create = function (api) { return nest('message.html.render', function about (msg, opts) { if (msg.value.content.type !== 'about') return if (!ref.isFeed(msg.value.content.about)) return var c = msg.value.content var self = msg.value.author === c.about var content = [] if (c.name) { var target = api.profile.html.person(c.about, c.name) content.push(computed([self, api.about.obs.name(c.about), c.name], (self, a, b) => { if (self) { return ['self identifies as "', target, '"'] } else if (a === b) { return ['identified ', api.profile.html.person(c.about)] } else { return ['identifies ', api.profile.html.person(c.about), ' as "', target, '"'] } })) } if (c.image) { if (!content.length) { var imageAction = self ? 'self assigned a display image' : ['assigned a display image to ', api.profile.html.person(c.about)] content.push(imageAction) } content.push(h('a AboutImage', { href: c.about }, [ h('img', {src: api.blob.sync.url(c.image)}) ])) } var elements = [] if (content.length) { var element = api.message.html.layout(msg, extend({ showActions: true, content, layout: 'mini' }, opts)) elements.push(api.message.html.decorate(element, { msg })) } if (c.description) { elements.push(api.message.html.decorate(api.message.html.layout(msg, extend({ showActions: true, content: [ self ? 'self assigned a description' : ['assigned a description to ', api.profile.html.person(c.about)], api.message.html.markdown(c.description) ], layout: 'mini' }, opts)), { msg })) } return elements }) }
exports.create = function (api) { return nest('message.html.render', follow) function follow (msg, opts) { const { type, contact, following, blocking } = msg.value.content if (type !== 'contact') return if (!isFeed(contact)) return const element = api.message.html.layout(msg, extend({ content: renderContent({ contact, following, blocking }), layout: 'mini' }, opts)) return api.message.html.decorate(element, { msg }) } function renderContent ({ contact, following, blocking }) { const name = api.about.html.link(contact) if (blocking !== undefined) { return [ blocking ? 'blocked ' : 'unblocked ', name ] } if (following !== undefined) { return [ following ? 'followed ' : 'unfollowed ', name ] } } }
exports.create = function (api) { return nest('progress.html.render', function (pos, classList) { var pending = computed(pos, x => x > 0 && x < 1) return svg('svg RadialProgress', { viewBox: '-20 -20 240 240', classList }, [ svg('path', { d: 'M100,0 a100,100 0 0 1 0,200 a100,100 0 0 1 0,-200', 'stroke-width': 40, 'stroke': '#CEC', 'fill': 'none' }), svg('path', { d: 'M100,0 a100,100 0 0 1 0,200 a100,100 0 0 1 0,-200', 'stroke-dashoffset': computed(pos, (pos) => { pos = Math.min(Math.max(pos, 0), 1) return (1 - pos) * 629 }), 'style': { transition: when(pending, 'stroke-dashoffset 0.1s', 'stroke-dashoffset 0') }, 'stroke-width': 40, 'stroke-dasharray': 629, 'stroke': '#33DA33', 'fill': 'none' }) ]) }) }
exports.create = function (api) { return nest({ 'app.html.settings': customStyles }) function customStyles () { const customStyles = api.settings.obs.get('patchbay.customStyles', '') const styles = h('textarea', { value: customStyles }) return { title: 'Custom Styles', body: h('CustomStyles', [ h('p', 'Custom MCSS to be applied on this window.'), styles, h('button', {'ev-click': save}, 'Apply Styles') ]) } function save () { api.settings.sync.set({ patchbay: { customStyles: styles.value } }) } } }
exports.create = (api) => { return nest('message.html.decorate', function (element, { msg }) { if (msg.value.content.root) element.dataset.root = msg.value.content.root if (msg.value.content.about) element.dataset.root = msg.value.content.about return element }) }
exports.create = (api) => { return nest('message.html.layout', messageLayout) function messageLayout (msg, opts = {}) { if (!(opts.layout === undefined || opts.layout === 'default')) return const { showUnread = true, showTitle } = opts var { author, timestamp, like, meta, backlinks, quote, reply } = api.message.html var rawMessage = Value(null) var el = h('Message -default', { attributes: { tabindex: '0' } }, // needed to be able to navigate and show focus() [ h('section.left', [ h('div.avatar', {}, api.about.html.avatar(msg.value.author)), h('div.author', {}, author(msg)), h('div.timestamp', {}, timestamp(msg)) ]), h('section.body', [ showTitle ? h('div.title', {}, opts.title) : null, h('div.content', {}, opts.content), h('footer.backlinks', {}, backlinks(msg)), h('div.raw-content', rawMessage) ]), h('section.right', [ h('div.meta', {}, meta(msg, { rawMessage })), // isMsg(msg.key) ? // don't show actions if no msg.key h('div.actions', [ like(msg), quote(msg), reply(msg) ]) ]) ] ) // UnreadFeature (search codebase for this if extracting) if (showUnread && !myMessage(msg)) { api.sbot.async.run(server => { server.unread.isRead(msg.key, (err, isRead) => { if (err) console.error(err) if (!isRead) el.classList.add('-unread') else el.classList.add('-read') }) }) } // ^ this could be in message/html/decorate // but would require opts to be passed to decorators in patchcore return el } function myMessage (msg) { return msg.value.author === api.keys.sync.id() } }
exports.create = function (api) { return nest({ 'app.html.settings': accessibility }) function accessibility () { const invert = api.settings.obs.get('patchbay.accessibility.invert') const saturation = api.settings.obs.get('patchbay.accessibility.saturation') const brightness = api.settings.obs.get('patchbay.accessibility.brightness') const contrast = api.settings.obs.get('patchbay.accessibility.contrast') return { title: 'Accessibility', body: h('AccessibilityStyles', [ h('div', { 'ev-click': () => invert.set(!invert()) }, [ h('label', 'Invert colors'), h('i.fa', { className: when(invert, 'fa-check-square', 'fa-square-o') }) ]), h('div', [ h('label', 'Saturation'), h('input', { type: 'range', min: 0, max: 100, value: saturation, 'ev-input': ev => saturation.set(ev.target.value) }) ]), h('div', [ h('label', 'Brightness'), h('input', { type: 'range', min: 0, max: 100, value: brightness, 'ev-input': ev => brightness.set(ev.target.value) }) ]), h('div', [ h('label', 'Contrast'), h('input', { type: 'range', min: 0, max: 100, value: contrast, 'ev-input': ev => contrast.set(ev.target.value) }) ]) ]) } } }
exports.create = function (api) { return nest('blob.sync.url', function (id) { // return id return 'http://localhost:8989/blobs/get/' + id }) }
exports.create = (api) => { return nest('app.sync.externalHandler', function (msg) { if (!gitMessageTypes.includes(msg.value.content.type)) return return function gitHandler (id) { shell.openExternal(`${viewer}/${encodeURIComponent(id)}`) } }) }
function overrideConfig (config) { return [{ gives: nest('config.sync.load'), create: function (api) { return nest('config.sync.load', () => config) } }] }
exports.create = function (api) { return nest('message.html.author', messageAuthor) function messageAuthor (msg) { return h('div', {title: msg.value.author}, [ api.about.html.link(msg.value.author) ]) } }
exports.create = (api) => { return nest('message.html.meta', privateMeta) function privateMeta (msg) { if (msg.value.private) { return h('i.fa.fa-lock', { title: 'Private' }) } } }
exports.create = function (api) { return nest('app.sync.initialise', initialiseSettings) function initialiseSettings () { const { get, set } = api.settings.sync const settings = merge({}, defaults, get()) settings.filter.defaults = defaults.filter set(settings) } }
exports.create = function (api) { return nest('app.html.search', function (setView) { var getProfileSuggestions = api.profile.async.suggest() var getChannelSuggestions = api.channel.async.suggest() var searchTimer = null var searchBox = h('input.search', { type: 'search', placeholder: 'word, @key, #channel', 'ev-suggestselect': (ev) => { setView(ev.detail.id) searchBox.value = ev.detail.id }, 'ev-input': (ev) => { clearTimeout(searchTimer) searchTimer = setTimeout(doSearch, 500) }, 'ev-focus': (ev) => { if (searchBox.value) { doSearch() } } }) setImmediate(() => { addSuggest(searchBox, (inputText, cb) => { if (inputText[0] === '@') { cb(null, getProfileSuggestions(inputText.slice(1)), {idOnly: true}) } else if (inputText[0] === '#') { cb(null, getChannelSuggestions(inputText.slice(1))) } }, {cls: 'SuggestBox'}) }) return searchBox function doSearch () { var value = searchBox.value.trim() if (value.startsWith('/') || value.startsWith('?') || value.startsWith('@') || value.startsWith('#') || value.startsWith('%')) { if (value.startsWith('@') && value.length < 30) { return // probably not a key } else if (value.length > 2) { setView(value) } } else if (value.trim()) { if (value.length > 2) { setView(`?${value.trim()}`) } } else { setView('/public') } } }) }
exports.create = function (api) { return nest('styles.css', css) function css (sofar = {}) { const mcssObj = api.styles.mcss() const mixinObj = api.styles.mixins() const mcssMixinsStr = mixinsToMcss(mixinObj) const cssObj = mcssToCss(mcssObj, mcssMixinsStr) return assign(sofar, cssObj) } }
exports.create = function (api) { var suggestions = null var recentSuggestions = null return nest('profile.async.suggest', function () { loadSuggestions() return function (word) { if (!word) { return recentSuggestions() } else { return suggestions().filter((item) => { return item.title.toLowerCase().startsWith(word.toLowerCase()) }) } } }) function loadSuggestions () { if (!suggestions) { var id = api.keys.sync.id() var following = api.contact.obs.following(id) var recentlyUpdated = api.profile.obs.recentlyUpdated() var contacts = computed([following, recentlyUpdated], function (a, b) { var result = Array.from(a) b.forEach((item, i) => { if (!result.includes(item)) { result.push(item) } }) return result }) recentSuggestions = map(computed(recentlyUpdated, (items) => Array.from(items).slice(0, 10)), suggestion, {idle: true}) suggestions = map(contacts, suggestion, {idle: true}) watch(recentSuggestions) watch(suggestions) } } function suggestion (id) { var name = api.about.obs.name(id) return Struct({ title: name, id, subtitle: id.substring(0, 10), value: computed([name, id], mention), image: api.about.obs.imageUrl(id), showBoth: true }) } }
exports.create = (api) => { return nest('router.async.normalise', normalise) function normalise (location, cb) { if (typeof location === 'object') { cb(null, location) return true } // if someone has given you an annoying html encoded location if (location.match(/^%25.*%3D.sha256$/)) { location = decodeURIComponent(location) } if (location.startsWith('ssb:')) { try { location = ssbUri.toSigilLink(location) } catch (err) { cb(err) } } var link = parseLink(location) if (link && isMsg(link.link)) { var params = { id: link.link } if (link.query && link.query.unbox) { params.private = true params.unbox = link.query.unbox } api.sbot.async.get(params, function (err, value) { if (err) cb(err) else { if (typeof value.content === 'string') value = api.message.sync.unbox(value) cb(null, { key: link.link, value }) } }) } else if (isBlobLink(location)) { // handles public & private blobs // TODO - parse into link and query? cb(null, { blob: location }) } else if (isChannelMulti(location)) cb(null, { channels: location.split('+') }) else if (isChannel(location)) cb(null, { channel: location }) else if (isFeed(location)) cb(null, { feed: location }) else if (isPage(location)) cb(null, { page: location.substring(1) }) return true } }
exports.create = function (api) { return nest('app.sync.locationId', locationId) function locationId (location) { if (typeof location === 'string') return location if (isMsg(location.key)) { // for all messages make the thread root key the 'locationId' const key = get(location, 'value.content.root', location.key) return JSON.stringify({ key }) } return JSON.stringify(location) } }
exports.create = function (api) { return nest('feed.pull.private', function (opts) { // HACK: handle lt/gt if (opts.lt != null) { opts.query = [ {$filter: { timestamp: {$gte: 0, $lt: opts.lt} }} ] delete opts.lt } return StreamWhenConnected(api.sbot.obs.connection, (sbot) => sbot.private.read(opts)) }) }
exports.create = function (api) { return nest('app.sync.catchKeyboardShortcut', catchKeyboardShortcut) function catchKeyboardShortcut (root) { var tabs = api.app.html.tabs() var search = api.app.html.searchBar() var goTo = api.app.sync.goTo root.addEventListener('keydown', (ev) => { isTextFieldEvent(ev) ? textFieldShortcuts(ev) : genericShortcuts(ev, { tabs, search, goTo }) }) } }
exports.create = function (api) { return nest('app.html.progressNotifier', function (id) { var progress = api.progress.obs.global() var indexing = computed([ api.progress.obs.query().pending, api.progress.obs.private().pending ], Math.max) var maxQueryPending = 0 var indexProgress = computed(indexing, (pending) => { if (pending === 0 || pending > maxQueryPending) { maxQueryPending = pending } if (pending === 0) { return 1 } else { return (maxQueryPending - pending) / maxQueryPending } }) var downloadProgress = computed([progress.feeds, progress.incompleteFeeds], (feeds, incomplete) => { if (feeds) { return clamp((feeds - incomplete) / feeds) } else { return 1 } }) var hidden = computed([progress.incompleteFeeds, indexing], (incomplete, indexing) => { return incomplete < 5 && !indexing }) return h('div.info', { hidden: sustained(hidden, 2000) }, [ h('div.status', [ h('Loading -small', [ when(computed(progress.incompleteFeeds, (v) => v > 5), ['Downloading new messages', h('progress', { style: {'margin-left': '10px'}, min: 0, max: 1, value: downloadProgress })], when(indexing, [ ['Indexing database', h('progress', { style: {'margin-left': '10px'}, min: 0, max: 1, value: indexProgress })] ], 'Scuttling...') ) ]) ]) ]) }) }
exports.create = (api) => { return nest('message.html.layout', mini) function mini (msg, opts) { if (opts.layout !== 'mini') return var classList = [] var additionalMeta = [] var footer = [] if (opts.showActions) { // HACK: this is used for about messages, which really should have there own layout footer.push( computed(msg.key, (key) => { if (ref.isMsg(key)) { return h('footer', [ h('div.actions', [ api.message.html.action(msg) ]) ]) } }) ) } if (opts.priority >= 2) { classList.push('-new') additionalMeta.push(h('span.flag -new', {title: 'New Message'})) } return h('Message -mini', {classList}, [ h('header', [ h('div.mini', [ api.profile.html.person(msg.value.author), ' ', opts.content ]), h('div.meta', {}, [ api.message.html.meta(msg), api.message.html.timestamp(msg), additionalMeta ]) ]), footer ]) } }
exports.create = function (api) { return nest('about.html.avatar', avatar) function avatar (id, size) { const src = api.about.obs.imageUrl(id) const color = computed(src, src => src.match(/^http/) ? 'rgba(0,0,0,0)' : api.about.obs.color(id)) const avatar = api.about.html.link(id, h('img', { style: { 'background-color': color }, src }) ) avatar.classList.add('Avatar') avatar.style.setProperty('--avatar-size', getSize(size)) return avatar } }
exports.create = function (api) { return nest('message.html.backlinks', function (msg) { if (!ref.isMsg(msg.key)) return [] var backlinks = api.message.obs.backlinks(msg.key) var references = computed([backlinks, msg], onlyReferences) return when(computed(references, hasItems), h('MessageBacklinks', [ h('header', 'Referenced:'), h('ul', [ map(references, (backlink) => { return h('li', [ h('a -backlink', { href: backlink.id, title: backlink.id }, api.message.obs.name(backlink.id)) ]) }) ]) ]) ) }) }
exports.create = (api) => { return nest('message.html.layout', miniLayout) function miniLayout (msg, opts) { if (opts.layout !== 'mini') return var rawMessage = Value(null) return h('Message -mini', { attributes: { tabindex: '0' } }, [ h('section.timestamp', {}, api.message.html.timestamp(msg)), h('header.author', {}, api.message.html.author(msg, { size: 'mini' })), h('section.meta', {}, api.message.html.meta(msg, { rawMessage })), h('section.content', { 'ev-click': () => api.app.sync.goTo(msg) }, opts.content), h('section.raw-content', rawMessage) ]) } }
exports.create = function (api) { return nest('app.page.errors', errorsPage) function errorsPage (location) { var { container, content } = api.app.html.scroller({ className: 'Errors', title: '/errors' }) container.id = JSON.stringify(location) // note this page needs an id assigned as it's not added by addPage function addError (err) { const error = h('Error', [ h('header', err.message), h('pre', err.stack) ]) content.appendChild(error) } return { container, addError } } }
exports.create = function (api) { return nest('message.html.meta', function likes (msg) { if (msg.key) { return computed(api.message.obs.likes(msg.key), likeCount) } }) function likeCount (likes) { if (likes.length) { return [' ', h('span.likes', { title: names(likes) }, ['+', h('strong', `${likes.length}`)])] } } function names (ids) { var items = map(ids, api.about.obs.name) return computed([items], (names) => { return 'Liked by\n' + names.map((n) => `- ${n}`).join('\n') }) } }
exports.create = function (api) { return nest('app.html.externalConfirm', externalConfirm) function externalConfirm (href) { var lb = lightbox() document.body.appendChild(lb) var okay = h('button.okay', { 'ev-click': () => { lb.remove() open(href) }}, 'open' ) var cancel = h('button.cancel.-subtle', { 'ev-click': () => { lb.remove() }}, 'cancel' ) okay.addEventListener('keydown', function (ev) { if (ev.keyCode === 27) cancel.click() // escape }) lb.show(h('ExternalConfirm', [ h('header', 'External link'), h('section.prompt', [ h('div.question', 'Open this link in your external browser?'), h('pre.link', href) ]), h('section.actions', [cancel, okay]) ])) okay.focus() } }
exports.create = function (api) { return nest({ 'app.html.menuItem': menuItem, 'app.page.blogs': blogsPage }) function menuItem () { return h('a', { 'ev-click': () => api.app.sync.goTo({ page: 'blogs' }) }, '/blogs') } function blogsPage (location) { const createStream = (opts) => { const query = [{ $filter: { timestamp: { $gt: 0 }, value: { content: { type: 'blog' } } } }] return api.sbot.pull.stream(server => { return next(server.query.read, Object.assign({}, { limit: 100, query }, opts), ['timestamp']) }) } var page = Scroller({ classList: ['Blogs'], streamToTop: createStream({ live: true, old: false }), streamToBottom: createStream({ reverse: true }), render: api.message.html.render }) page.title = '/blogs' page.scroll = keyscroll(page.querySelector('section.content')) return page } }
exports.create = function (api) { return nest('app.sync.initialise', styles) function styles () { const css = values(api.styles.css()).join('\n') const custom = api.settings.obs.get('patchbay.customStyles') const accessibility = api.settings.obs.get('patchbay.accessibility') document.head.appendChild( h('style', { innerHTML: css }) ) document.head.appendChild( h('style', { innerHTML: computed(custom, compileCss) }) ) document.head.appendChild( h('style', { innerHTML: computed(accessibility, a11y => { return compileCss(accessibilityMcss(a11y)) }) }) ) } }