├── .gitignore
├── .travis.yml
├── CHANGELOG.md
├── CONTRIBUTING.md
├── LICENSE.md
├── README.md
├── app
├── command.js
├── desktop.js
├── elements
│ ├── base-element.js
│ ├── channels.js
│ ├── composer.js
│ ├── input-prompt.js
│ ├── messages.js
│ ├── status.js
│ └── users.js
├── index.js
├── signature.js
├── style
│ ├── button.styl
│ └── index.styl
├── util.js
├── web.js
└── windows
│ ├── git-help.html
│ └── git-help.js
├── config.js
├── index.html
├── index.js
├── package.json
├── pkg.js
└── static
├── Icon.icns
├── Icon.png
├── Icon@2x.png
├── friends-logo.psd
├── logo.png
├── screenshot.png
├── startup.ogg
└── verified.svg
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | app/style/*.css
3 | friendsdb/
4 | node_modules/
5 | pkg/
6 | public-keys/
7 | Friends.app
8 | *.log
9 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | node_js:
3 | - '4'
4 | - '5'
5 | install: npm install standard
6 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Friends change log
2 |
3 | All notable changes to this project will be documented in this file.
4 | This project adheres to [Semantic Versioning](http://semver.org/).
5 |
6 | ## Unreleased
7 | * engage
8 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing Guidelines
2 |
3 | Contributions welcome!
4 |
5 | **Before spending lots of time on something, ask for feedback on your idea first!**
6 |
7 | Please search issues and pull requests before adding something new to avoid duplicating efforts and conversations.
8 |
9 | This project welcomes non-code contributions, too! The following types of contributions are welcome:
10 |
11 | - **Ideas**: participate in an issue thread or start your own to have your voice heard.
12 | - **Writing**: contribute your expertise in an area by helping expand the included content.
13 | - **Copy editing**: fix typos, clarify language, and generally improve the quality of the content.
14 | - **Formatting**: help keep content easy to read with consistent formatting.
15 |
16 | ## Code Style
17 |
18 | [![standard][standard-image]][standard-url]
19 |
20 | This repository uses [`standard`][standard-url] to maintain code style and consistency, and to avoid style arguments. `npm test` runs `standard` automatically, so you don't have to!
21 |
22 | [standard-image]: https://cdn.rawgit.com/feross/standard/master/badge.svg
23 | [standard-url]: https://github.com/feross/standard
24 |
25 | # Project Governance
26 |
27 | **This is an [OPEN Open Source Project](http://openopensource.org/).**
28 |
29 | Individuals making significant and valuable contributions are given commit-access to the project to contribute as they see fit. This project is more like an open wiki than a standard guarded open source project.
30 |
31 | ## Rules
32 |
33 | There are a few basic ground-rules for contributors:
34 |
35 | 1. **No `--force` pushes** or modifying the Git history in any way.
36 | 1. **Non-master branches** ought to be used for ongoing work.
37 | 1. **External API changes and significant modifications** ought to be subject to an **internal pull-request** to solicit feedback from other contributors.
38 | 1. Internal pull-requests to solicit feedback are *encouraged* for any other non-trivial contribution but left to the discretion of the contributor.
39 | 1. Contributors should attempt to adhere to the prevailing code style.
40 |
41 | ## Releases
42 |
43 | Declaring formal releases remains the prerogative of the project maintainer.
44 |
45 | ## Changes to this arrangement
46 |
47 | This is an experiment and feedback is welcome! This document may also be subject to pull-requests or changes by contributors where you believe you have something valuable to add or change.
48 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | # [MIT License](https://spdx.org/licenses/MIT)
2 |
3 | Copyright (c) 2015-2016 [MOOSE Team](http://moose-team.github.io)
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy of
6 | this software and associated documentation files (the "Software"), to deal in
7 | the Software without restriction, including without limitation the rights to
8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
9 | the Software, and to permit persons to whom the Software is furnished to do so,
10 | subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Friends
2 |
3 | > P2P chat powered by the Web.
4 |
5 | [![travis][travis-image]][travis-url]
6 | [![david][david-image]][david-url]
7 | [![javascript style guide][standard-image]][standard-url]
8 |
9 | [travis-image]: https://img.shields.io/travis/moose-team/friends/master.svg
10 | [travis-url]: https://travis-ci.org/moose-team/friends
11 | [david-image]: https://img.shields.io/david/moose-team/friends.svg
12 | [david-url]: https://david-dm.org/moose-team/friends
13 | [standard-image]: https://img.shields.io/badge/code_style-standard-brightgreen.svg
14 | [standard-url]: https://standardjs.com
15 |
16 | 
17 |
18 | **This project is alpha quality.** You probably only want to use this if you like to send pull requests fixing things :)
19 |
20 | ## How it works
21 |
22 | See [our site](http://moose-team.github.io/friends/) or the `gh-pages` branch.
23 |
24 | ## Install
25 |
26 | ### Prerequisites
27 |
28 | You'll need [Node.js](https://nodejs.org) (`>= 4`) and [npm](https://www.npmjs.com/package/npm) (`>= 2.8.3`).
29 |
30 | ### Build
31 |
32 | Clone the source locally:
33 |
34 | ```sh
35 | $ git clone https://github.com/moose-team/friends
36 | $ cd friends
37 | ```
38 |
39 | Install project dependencies:
40 |
41 | ```sh
42 | $ npm install
43 | ```
44 |
45 | Compile leveldown for [electron](http://electron.atom.io/):
46 |
47 | ```sh
48 | $ npm run rebuild-leveldb
49 | ```
50 |
51 | If you are not on 64-bit architecture, you will have to modify the command in package.json:
52 |
53 | ```
54 | "rebuild-leveldb": "cd node_modules/leveldown && set HOME=~/.electron-gyp && node-gyp rebuild --target=$(../../version.js) --arch=x64 --dist-url=https://atom.io/download/atom-shell"
55 | ```
56 |
57 | to use `--arch=ia32`.
58 |
59 | ## Usage
60 |
61 | ### GitHub Login
62 |
63 | Friends currently uses your git and github configuration for authentication.
64 |
65 | If you don't already have a public key on GitHub and corresponding private key on your machine, you'll need to [set that up first](https://help.github.com/articles/generating-ssh-keys/). Make sure your github username is also set, using `git config --global github.user yourusername`.
66 |
67 | If you're having trouble getting this part to work, do this to get debug information:
68 |
69 | ```
70 | $ npm i github-current-user -g
71 | $ DEBUG=* github-current-user
72 | ```
73 |
74 | and then report an [issue](https://github.com/moose-team/friends/issues).
75 |
76 | **Note**: DSA keys are not supported. You should switch to RSA anyway for security reasons.
77 |
78 | If it can't verify you, try doing `ssh-add ~/.ssh/id_rsa`. Your key should show up when you run `ssh-add -l`.
79 |
80 | ### Run
81 |
82 | To run from the command line, execute `npm start`.
83 |
84 | To create a distributable app, run `npm run package`.
85 |
86 | ## Contributing
87 |
88 | Contributions welcome! Please read the [contributing guidelines](CONTRIBUTING.md) before getting started.
89 |
90 | ## License
91 |
92 | [MIT](LICENSE.md)
93 |
--------------------------------------------------------------------------------
/app/command.js:
--------------------------------------------------------------------------------
1 | function command (self, db) {
2 | return function (commandStr) {
3 | var words = commandStr.split(' ')
4 | var command = words[0].substring(1, words[0].length).toLowerCase()
5 |
6 | switch (command) {
7 | case 'join':
8 | words.shift()
9 | var channel = words.join(' ')
10 | self.emit('addChannel', channel)
11 | break
12 | case 'wc':
13 | case 'part':
14 | case 'leave':
15 | self.emit('leaveChannel', self.data.activeChannel.name)
16 | break
17 | case 'wcall':
18 | case 'partall':
19 | case 'leaveall':
20 | self.data.channels.forEach(function (channel) {
21 | self.emit('leaveChannel', channel.name)
22 | })
23 | break
24 | case 'alias':
25 | var aliasName = words[1]
26 | var aliasCommand = words.splice(2, words.length - 1).join(' ')
27 | db.aliases.put(aliasName, aliasCommand)
28 | break
29 | case 'rmalias':
30 | var aliasN = words[1]
31 | db.aliases.del(aliasN)
32 | break
33 | default:
34 | db.aliases.get(command, function (err, alias) {
35 | if (err === null) {
36 | self.emit('executeCommand', alias)
37 | } else {
38 | console.log('Unrecognized command: ' + command + ' (in "' + commandStr + '")')
39 | self.emit('sendMessage', commandStr)
40 | }
41 | })
42 | break
43 | }
44 | }
45 | }
46 |
47 | module.exports = command
48 |
--------------------------------------------------------------------------------
/app/desktop.js:
--------------------------------------------------------------------------------
1 | module.exports = Desktop
2 |
3 | var inherits = require('util').inherits
4 | var remote = require('remote')
5 | var app = remote.require('app')
6 | var shell = require('shell')
7 | var BrowserWindow = remote.require('browser-window')
8 |
9 | var App = require('./')
10 | var config = require('../config')
11 |
12 | inherits(Desktop, App)
13 |
14 | function Desktop () {
15 | if (!(this instanceof Desktop)) return new Desktop()
16 |
17 | // get current window when app is instantiated
18 | var currentWindow = remote.getCurrentWindow()
19 | var self = this
20 |
21 | App.call(this, document.body, currentWindow)
22 |
23 | // defensively remove all listeners as errors will occur on reload
24 | // see https://github.com/atom/electron/issues/3778
25 | currentWindow.removeAllListeners()
26 |
27 | // clear notifications on focus.
28 | // TODO: only clear notifications in current channel when we have that
29 | currentWindow.on('focus', function () {
30 | self.setBadge(false)
31 | })
32 |
33 | this.on('showGitHelp', this.showGitHelp.bind(this))
34 | this.on('setBadge', this.setBadge.bind(this))
35 | this.on('openUrl', function (url) {
36 | shell.openExternal(url)
37 | })
38 | }
39 |
40 | Desktop.prototype.showGitHelp = function () {
41 | var gitHelp = new BrowserWindow({
42 | width: 600,
43 | height: 525,
44 | show: false,
45 | center: true,
46 | resizable: false
47 | })
48 |
49 | gitHelp.on('closed', function () {
50 | gitHelp = null
51 | })
52 |
53 | gitHelp.loadURL(config.GIT_HELP)
54 | gitHelp.show()
55 | }
56 |
57 | Desktop.prototype.setBadge = function (num) {
58 | if (!app.dock) return
59 | if (num === false) return app.dock.setBadge('')
60 | if (num == null) this._notifications++
61 | else this._notifications = num
62 | app.dock.setBadge(this._notifications.toString())
63 | }
64 |
--------------------------------------------------------------------------------
/app/elements/base-element.js:
--------------------------------------------------------------------------------
1 | module.exports = BaseElement
2 |
3 | var inherits = require('util').inherits
4 | var EE = require('events').EventEmitter
5 |
6 | function BaseElement (target) {
7 | EE.call(this)
8 | this.target = target || null
9 | }
10 | inherits(BaseElement, EE)
11 |
12 | BaseElement.prototype.send = function () {
13 | if (this.target && typeof this.target.emit === 'function') {
14 | this.target.emit.apply(this.target, arguments)
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/app/elements/channels.js:
--------------------------------------------------------------------------------
1 | module.exports = Channels
2 |
3 | var InputPrompt = require('./input-prompt')
4 | var inherits = require('util').inherits
5 | var BaseElement = require('./base-element')
6 | var yo = require('yo-yo')
7 |
8 | function Channels (target) {
9 | var self = this
10 | BaseElement.call(this, target)
11 |
12 | self.addChannelPrompt = new InputPrompt({
13 | className: 'add-channel',
14 | prompt: '+ Join Channel',
15 | placeholder: 'Channel name',
16 | onsubmit: function (channelName) {
17 | self.send('addChannel', channelName)
18 | },
19 | onupdate: function () {
20 | self.send('render')
21 | }
22 | })
23 | }
24 | inherits(Channels, BaseElement)
25 |
26 | Channels.prototype.render = function (channels) {
27 | var self = this
28 |
29 | channels = channels.map(function (channel) {
30 | var className = channel.active ? 'active' : ''
31 |
32 | function onclick () {
33 | self.send('selectChannel', channel.name)
34 | }
35 |
36 | return yo`
37 |
38 |
39 |
40 | `
41 | })
42 |
43 | return yo`
44 |
45 |
Channels
46 |
${channels}
47 | ${self.addChannelPrompt.render()}
48 |
49 | `
50 | }
51 |
--------------------------------------------------------------------------------
/app/elements/composer.js:
--------------------------------------------------------------------------------
1 | module.exports = Composer
2 |
3 | var inherits = require('util').inherits
4 | var uniq = require('lodash.uniq')
5 | var BaseElement = require('./base-element')
6 | var yo = require('yo-yo')
7 |
8 | function Composer (target) {
9 | BaseElement.call(this, target)
10 | // List of words available to autocomplete
11 | this.autocompletes = []
12 | // Whether we should show the autocomplete box
13 | this.autocompleting = []
14 | // Which item in the autocomplete list we are selecting
15 | this.autocompleteIndex = 0
16 | }
17 | inherits(Composer, BaseElement)
18 |
19 | var TAB_KEY = 9
20 | var ENTER_KEY = 13
21 |
22 | // the height taken up by padding, margin, border combined
23 | Composer.prototype.minimumHeight = 48 // the default height of the composer element in pixels is one row + mimimum
24 | Composer.prototype.defaultHeight = 17 + this.minimumHeight
25 |
26 | Composer.prototype.render = function (data) {
27 | var self = this
28 | data = data || {}
29 | var ownUsername = data.username
30 |
31 | function onkeydown (e) {
32 | if (e.keyCode === TAB_KEY) {
33 | e.preventDefault()
34 |
35 | // if there are no matches try matching again
36 | if (!self.autocompleting.length) {
37 | self.resetAutocomplete()
38 | self.autocomplete(self.node.value, ownUsername)
39 | }
40 |
41 | self.insertAutocomplete(e.target)
42 | } else if (e.keyCode === ENTER_KEY && !e.shiftKey) {
43 | e.preventDefault()
44 | self.submit(e.target)
45 | }
46 |
47 | // reset the completions if the user submits or changes the text to be
48 | // completed
49 | if (e.keyCode !== TAB_KEY) {
50 | self.resetAutocomplete()
51 | }
52 | }
53 |
54 | function oninput (e) {
55 | self.resize(e.target)
56 | }
57 |
58 | self.node = yo`
59 |
67 | `
68 |
69 | this.initAutoExpander()
70 |
71 | return self.node
72 | }
73 |
74 | Composer.prototype.autocomplete = function (text, ownUsername) {
75 | if (!text || text.length < 1) {
76 | this.resetAutocomplete()
77 | return
78 | }
79 |
80 | this.autocompleting = uniq(this.autocompletes.filter(function (candidate) {
81 | return candidate.toLowerCase().indexOf(text.toLowerCase()) === 0 &&
82 | candidate.toLowerCase() !== ownUsername.toLowerCase()
83 | }))
84 | }
85 |
86 | Composer.prototype.submit = function (node) {
87 | var self = this
88 | if (node.value.charAt(0) === '/') {
89 | self.send('executeCommand', node.value)
90 | } else {
91 | self.send('sendMessage', node.value)
92 | }
93 | node.innerHTML = ''
94 | self.resize(node)
95 | }
96 |
97 | Composer.prototype.insertAutocomplete = function (node) {
98 | if (this.autocompleting.length < 1) return
99 | if (this.autocompleting[this.autocompleteIndex]) {
100 | node.value = this.autocompleting[this.autocompleteIndex] + ': '
101 | }
102 | this.autocompleteIndex = (this.autocompleteIndex + 1) % this.autocompleting.length
103 | }
104 |
105 | Composer.prototype.resetAutocomplete = function () {
106 | this.autocompleting = []
107 | this.autocompleteIndex = 0
108 | }
109 |
110 | Composer.prototype.focus = function () {
111 | this.node.focus()
112 | }
113 |
114 | Composer.prototype.initAutoExpander = function () {
115 | var self = this
116 |
117 | setTimeout(function () {
118 | var savedValue = self.node.value
119 | self.node.value = ''
120 | self.node.baseScrollHeight = self.node.scrollHeight
121 | self.node.value = savedValue
122 | })
123 | }
124 |
125 | Composer.prototype.resize = function () {
126 | var oldrows = this.node.rows
127 | this.node.rows = 1
128 | var rows = Math.ceil((this.node.scrollHeight - this.node.baseScrollHeight) / 17)
129 | this.node.rows = 1 + rows
130 |
131 | // only dispatch an event if the rows count actually changed
132 | if (oldrows !== this.node.rows) {
133 | this.send('resizeComposer', (rows * 17) + this.minimumHeight)
134 | }
135 | }
136 |
--------------------------------------------------------------------------------
/app/elements/input-prompt.js:
--------------------------------------------------------------------------------
1 | module.exports = InputPrompt
2 |
3 | var inherits = require('util').inherits
4 | var BaseElement = require('./base-element')
5 | var yo = require('yo-yo')
6 |
7 | function InputPrompt (params) {
8 | BaseElement.call(this)
9 | if (!params.onsubmit) throw new Error('param `onsubmit` required')
10 | if (!params.prompt) throw new Error('param `prompt` required')
11 |
12 | this.params = params
13 | this.showInput = false
14 | }
15 | inherits(InputPrompt, BaseElement)
16 |
17 | InputPrompt.prototype.render = function () {
18 | var self = this
19 | var view
20 |
21 | function onsubmit (e) {
22 | e.preventDefault()
23 | var input = this.querySelector('input')
24 | self.params.onsubmit(input.value)
25 | self.showInput = false
26 | self.params.onupdate()
27 | }
28 |
29 | function onblur (e) {
30 | self.showInput = false
31 | self.params.onupdate()
32 | }
33 |
34 | function onclick (e) {
35 | e.preventDefault()
36 | self.showInput = true
37 | self.params.onupdate()
38 | }
39 |
40 | if (self.showInput) {
41 | view = yo`
42 |
48 | `
49 | } else {
50 | view = yo``
51 | }
52 |
53 | self.node = yo`
${view}
`
54 |
55 | return self.node
56 | }
57 |
--------------------------------------------------------------------------------
/app/elements/messages.js:
--------------------------------------------------------------------------------
1 | module.exports = Messages
2 |
3 | var inherits = require('util').inherits
4 | var BaseElement = require('./base-element')
5 | var yo = require('yo-yo')
6 |
7 | function Messages (target) {
8 | BaseElement.call(this, target)
9 | this.shouldAutoScroll = true
10 | // keep track of the scrollTop property here to avoid odd behavior when
11 | // changing scrollHeight while rerendering
12 | this.scrollTop = 0
13 | this.composerHeight = 48
14 | }
15 | inherits(Messages, BaseElement)
16 |
17 | Messages.prototype.render = function (channel, users) {
18 | var self = this
19 | var childViews = renderChildViews.call(self, channel, users)
20 |
21 | function leaveChannel () {
22 | if (channel.name === 'friends') return null
23 |
24 | function onclick () {
25 | if (!channel || channel.name === 'friends') return
26 | self.send('leaveChannel', channel.name)
27 | }
28 |
29 | return yo``
30 | }
31 |
32 | var channelName = `#${channel ? channel.name : 'friends'}`
33 | var numPeers = yo`${channel.peers} peer${channel.peers === 1 ? '' : 's'}`
34 |
35 | return yo`
36 |
37 |
38 |
${channelName} ${numPeers}
39 | ${leaveChannel()}
40 |
41 | ${childViews}
42 |
43 | `
44 | }
45 |
46 | function renderChildViews (channel, users) {
47 | if (channel && channel.messages.length === 0) {
48 | var starterMessage = 'This is a new channel. Send a message to start things off!'
49 | return yo`
${starterMessage}
`
50 | }
51 |
52 | var self = this
53 | var messages = (channel ? channel.messages : []).map(function (msg) {
54 | var user = users[msg.username]
55 | if (user && user.blocked) return null
56 |
57 | var isVerified = !/Anonymous/i.test(msg.username) && msg.valid
58 | var userUrl = isVerified ? `http://github.com/${msg.username}` : '#'
59 |
60 | function avatar () {
61 | function onclick (e) {
62 | if (userUrl !== '#') { self.send('openUrl', userUrl) }
63 | }
64 |
65 | return yo``
66 | }
67 |
68 | function username () {
69 | return yo`${msg.username}`
70 | }
71 |
72 | function verified () {
73 | return isVerified ? yo`` : null
74 | }
75 |
76 | function timestamp () {
77 | return yo`${msg.timeago}`
78 | }
79 |
80 | function message () {
81 | var el = yo``
82 | el.innerHTML = msg.html || msg.text
83 | return el
84 | }
85 |
86 | return yo`
87 |