Exemplo n.º 1
0
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
  })
}
Exemplo n.º 2
0
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
      ]
    }
  }
}
Exemplo n.º 3
0
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'
      })
    ])
  })
}
Exemplo n.º 4
0
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
        }
      })
    }
  }
}
Exemplo n.º 5
0
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
  })
}
Exemplo n.º 6
0
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()
  }
}
Exemplo n.º 7
0
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) })
        ])
      ])
    }
  }
}
Exemplo n.º 8
0
exports.create = function (api) {
  return nest('blob.sync.url', function (id) {
    // return id

    return 'http://localhost:8989/blobs/get/' + id
  })
}
Exemplo n.º 9
0
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)}`)
    }
  })
}
Exemplo n.º 10
0
function overrideConfig (config) {
  return [{
    gives: nest('config.sync.load'),
    create: function (api) {
      return nest('config.sync.load', () => config)
    }
  }]
}
Exemplo n.º 11
0
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)
    ])
  }
}
Exemplo n.º 12
0
exports.create = (api) => {
  return nest('message.html.meta', privateMeta)

  function privateMeta (msg) {
    if (msg.value.private) {
      return h('i.fa.fa-lock', {
        title: 'Private'
      })
    }
  }
}
Exemplo n.º 13
0
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)
  }
}
Exemplo n.º 14
0
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')
      }
    }
  })
}
Exemplo n.º 15
0
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)
  }
}
Exemplo n.º 16
0
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
    })
  }
}
Exemplo n.º 17
0
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
  }
}
Exemplo n.º 18
0
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)
  }
}
Exemplo n.º 19
0
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))
  })
}
Exemplo n.º 20
0
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 })
    })
  }
}
Exemplo n.º 21
0
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...')
          )
        ])
      ])
    ])
  })
}
Exemplo n.º 22
0
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
    ])
  }
}
Exemplo n.º 23
0
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
  }
}
Exemplo n.º 24
0
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))
            ])
          })
        ])
      ])
    )
  })
}
Exemplo n.º 25
0
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)
    ])
  }
}
Exemplo n.º 26
0
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 }
  }
}
Exemplo n.º 27
0
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')
    })
  }
}
Exemplo n.º 28
0
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()
  }
}
Exemplo n.º 29
0
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
  }
}
Exemplo n.º 30
0
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))
        })
      })
    )
  }
}