├── .gitignore ├── LICENSE.md ├── README.md ├── elements └── logo.js ├── index.js └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # node-waf configuration 20 | .lock-wscript 21 | 22 | # Compiled binary addons (http://nodejs.org/api/addons.html) 23 | build/Release 24 | 25 | # Dependency directory 26 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git 27 | node_modules 28 | config.yml -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # [MIT License](https://spdx.org/licenses/MIT) 2 | 3 | Copyright (c) 2017 Joe Hand 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # hyperirc-web 2 | 3 | (WIP) read [hyperirc](https://github.com/mafintosh/hyperirc) over websockets. 4 | 5 | [![npm][npm-image]][npm-url] 6 | [![travis][travis-image]][travis-url] 7 | [![standard][standard-image]][standard-url] 8 | 9 | ## License 10 | 11 | [MIT](LICENSE.md) 12 | 13 | [npm-image]: https://img.shields.io/npm/v/hyperirc-dash.svg?style=flat-square 14 | [npm-url]: https://www.npmjs.com/package/hyperirc-dash 15 | [travis-image]: https://img.shields.io/travis/joehand/hyperirc-dash.svg?style=flat-square 16 | [travis-url]: https://travis-ci.org/joehand/hyperirc-dash 17 | [standard-image]: https://img.shields.io/badge/code%20style-standard-brightgreen.svg?style=flat-square 18 | [standard-url]: http://npm.im/standard 19 | -------------------------------------------------------------------------------- /elements/logo.js: -------------------------------------------------------------------------------- 1 | module.exports = ` 2 | 3 | 4 | 5 | ` 6 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var html = require('choo/html') 2 | var choo = require('choo') 3 | var log = require('choo-log') 4 | var css = require('sheetify') 5 | var wss = require('websocket-stream') 6 | var hypercore = require('hypercore') 7 | var ram = require('random-access-memory') 8 | var pump = require('pump') 9 | var moment = require('moment') 10 | var Autolinker = require('autolinker') 11 | var debounce = require('lodash/debounce') 12 | var logo = require('./elements/logo') 13 | 14 | css('tachyons') 15 | css` 16 | .pulse-circle { 17 | width: 14px; 18 | height: 14px; 19 | border-radius: 50%; 20 | box-shadow: 0 0 0 rgba(205, 236, 255, 0.6); 21 | animation: pulse 4s 2s infinite; 22 | } 23 | .pulse-circle:hover { 24 | animation: none; 25 | } 26 | @-webkit-keyframes pulse { 27 | 0% { 28 | -webkit-box-shadow: 0 0 0 0 rgba(205, 236, 255, 1); 29 | } 30 | 70% { 31 | -webkit-box-shadow: 0 0 0 10px rgba(205, 236, 255, 0); 32 | } 33 | 100% { 34 | -webkit-box-shadow: 0 0 0 0 rgba(205, 236, 255, 0); 35 | } 36 | } 37 | @keyframes pulse { 38 | 0% { 39 | -moz-box-shadow: 0 0 0 0 rgba(205, 236, 255, 1); 40 | box-shadow: 0 0 0 0 rgba(205, 236, 255, 1); 41 | } 42 | 70% { 43 | -moz-box-shadow: 0 0 0 20px rgba(205, 236, 255, 0); 44 | box-shadow: 0 0 0 20px rgba(205, 236, 255, 0); 45 | } 46 | 100% { 47 | -moz-box-shadow: 0 0 0 0 rgba(205, 236, 255, 0); 48 | box-shadow: 0 0 0 0 rgba(205, 236, 255, 0); 49 | } 50 | } 51 | 52 | .logo { 53 | height:42px; 54 | } 55 | 56 | .logo svg { 57 | width:auto; 58 | height:42px; 59 | } 60 | 61 | footer { 62 | background-color: #293648; 63 | } 64 | ` 65 | 66 | var app = choo() 67 | app.use(log()) 68 | app.use(connectWs) 69 | app.use(updateTimestamps) 70 | app.route('/', mainView) 71 | app.mount('body') 72 | 73 | function mainView (state, emit) { 74 | var onScrollDebounced = debounce(onScroll, 50) 75 | var logoEl = html`` 76 | logoEl.innerHTML = logo 77 | var footer = html` 78 | 89 | ` 90 | 91 | return html` 92 | 93 | ${footer} 94 |
95 | 101 |
102 |
103 |
104 | ${state.channel} 105 |
106 |

107 | ${state.connected ? html`connected` : 'Connecting...'} 108 |

109 |
110 | ${state.messages.length ? messages() : loading() } 111 |
112 |
113 | 114 | ` 115 | 116 | function messages () { 117 | return state.messages.map(data => { 118 | var msgEl = html`

` 119 | msgEl.innerHTML = data.html 120 | 121 | return html` 122 |
123 |
124 | ${data.from}${data.moment.fromNow()} 125 |
${data.gitter ? 'via gitter ' : ''} 126 |
127 | ${msgEl} 128 |
129 | ` 130 | }) 131 | } 132 | 133 | function loading () { 134 | return html` 135 |
136 |
137 |
138 |

loading...

139 |
140 | ` 141 | } 142 | 143 | function onScroll (e) { 144 | // emit at bottom 145 | if ((window.innerHeight + window.scrollY + 250) >= document.body.offsetHeight) { 146 | emit('scroll') 147 | } 148 | } 149 | } 150 | 151 | function updateTimestamps (state, emitter) { 152 | // render on inactivty to update timestamps 153 | var activityTimeout = setTimeout(inActive, 5000) 154 | emitter.on('render', function () { 155 | clearTimeout(activityTimeout) 156 | activityTimeout = setTimeout(inActive, 5000) 157 | }) 158 | 159 | function inActive () { 160 | emitter.emit('render') 161 | } 162 | } 163 | 164 | function connectWs (state, emitter) { 165 | state = Object.assign(state, { 166 | channel: '#dat', 167 | key: '227d9212ee85c0f14416885c5390f2d270ba372252e781bf45a6b7056bb0a1b5', 168 | feed: null, 169 | messages: [], 170 | connected: false, 171 | startIndex: 1, 172 | wsUrl: 'ws://archiver.jhand.space' // TODO: configure ws endpoint? 173 | }) 174 | 175 | if (!state.feed) createFeed(state.key) 176 | 177 | emitter.on('scroll', function () { 178 | var loadNum = 7 179 | var msgs = [] 180 | state.startIndex = Math.max(state.startIndex - loadNum, 1) 181 | 182 | var stream = state.feed.createReadStream({live: false, start: state.startIndex, end: state.startIndex + loadNum}) 183 | stream.on('data', function (data) { 184 | msgs.unshift(parseMessage(data)) 185 | }) 186 | stream.on('end', function () { 187 | state.messages = state.messages.concat(msgs) 188 | emitter.emit('render') 189 | }) 190 | }) 191 | 192 | emitter.on('message', function (data) { 193 | var msg = parseMessage(data) 194 | state.messages.unshift(msg) 195 | emitter.emit('render') 196 | }) 197 | 198 | function parseMessage (data) { 199 | if (data.from === 'dat-gitter') { 200 | var split = data.message.split(/(\([\S]*\))/) 201 | data.gitter = true 202 | // long gitter messages don't have username at beginning 203 | if (split.length > 1) { 204 | data.from = split[1].slice(1,split[1].length - 1) 205 | data.message = split[2] 206 | } else { 207 | // continued message, get previous message user 208 | data.from = state.messages[0].from 209 | data.message = split[0] 210 | } 211 | } 212 | 213 | data.moment = moment(data.timestamp) 214 | data.html = Autolinker.link(data.message) 215 | return data 216 | } 217 | 218 | function createFeed () { 219 | var feed = hypercore(ram, state.key, {sparse: true, valueEncoding: 'json'}) 220 | 221 | feed.on('ready', function () { 222 | state.connected = true 223 | state.feed = feed 224 | emitter.emit('log:info', 'feed ready') 225 | emitter.emit('log:info', feed.length) 226 | 227 | feed.update(function () { 228 | state.startIndex = Math.max(feed.length - 20, 1) 229 | feed.createReadStream({live: true, start: state.startIndex}).on('data', function (data) { 230 | emitter.emit('message', data) 231 | }) 232 | }) 233 | }) 234 | 235 | feed.on('download', function () { 236 | state.connected = true 237 | }) 238 | 239 | replicate() 240 | 241 | function replicate () { 242 | var ws = wss(state.wsUrl) 243 | ws.on('connect', function () { 244 | state.connected = true 245 | }) 246 | pump(ws, feed.replicate({live: true}), ws, function (err) { 247 | emitter.emit('log:error', err) 248 | state.connected = false 249 | replicate() // again if it closes? 250 | }) 251 | } 252 | } 253 | } 254 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hyperirc-web", 3 | "description": "read hyperirc over websockets", 4 | "version": "1.0.0", 5 | "author": "Joe Hand ", 6 | "bugs": { 7 | "url": "https://github.com/joehand/hyperirc-web/issues" 8 | }, 9 | "devDependencies": { 10 | "standard": "*", 11 | "tap-spec": "^4.0.2", 12 | "tape": "^4.0.0" 13 | }, 14 | "homepage": "https://github.com/joehand/hyperirc-web", 15 | "keywords": [ 16 | "dat", 17 | "hypercore", 18 | "irc" 19 | ], 20 | "license": "MIT", 21 | "main": "index.js", 22 | "repository": { 23 | "type": "git", 24 | "url": "https://github.com/joehand/hyperirc-web.git" 25 | }, 26 | "scripts": { 27 | "build": "bankai build index.js dist/", 28 | "start": "bankai index.js -w" 29 | }, 30 | "dependencies": { 31 | "autolinker": "^1.4.3", 32 | "bankai": "^8.1.1", 33 | "choo": "^5.6.2", 34 | "choo-log": "^6.1.2", 35 | "hypercore": "^6.6.3", 36 | "lodash": "^4.17.4", 37 | "moment": "^2.18.1", 38 | "pump": "^1.0.2", 39 | "random-access-memory": "^2.4.0", 40 | "sheetify": "^6.1.0", 41 | "tachyons": "^4.7.4", 42 | "websocket-stream": "^5.0.0" 43 | } 44 | } 45 | --------------------------------------------------------------------------------