├── .eslintignore ├── .eslintrc ├── .floo ├── .flooignore ├── .gitignore ├── .travis.yml ├── LICENSE ├── NOTICE ├── README.md ├── keymaps └── floobits.cson ├── lib ├── atom_listener.js ├── atom_utils.js ├── common │ ├── api.js │ ├── buffer_action.js │ ├── buffer_model.js │ ├── button_model.js │ ├── constants.js │ ├── editor_action.js │ ├── emitter.js │ ├── ext_messaging.js │ ├── extern │ │ ├── term.js │ │ └── webrtc │ │ │ ├── gain_controller.js │ │ │ ├── get_user_media.js │ │ │ ├── hark.js │ │ │ ├── peer_connection.js │ │ │ ├── webrtc.js │ │ │ ├── webrtc_support.js │ │ │ └── wild_emitter.js │ ├── filetree_model.js │ ├── floop.js │ ├── floorc.js │ ├── handlers │ │ ├── account.js │ │ └── floohandler.js │ ├── ignore.js │ ├── message_action.js │ ├── message_model.js │ ├── msg_model.js │ ├── mugshot.js │ ├── permission_model.js │ ├── persistentjson.js │ ├── sound_effects.js │ ├── terminal_model.js │ ├── transport.js │ ├── userPref_model.js │ ├── user_model.js │ ├── utils.js │ ├── webrtc.js │ └── webrtc_action.js ├── floobits.js ├── floodmp.js ├── floourl.js ├── follow_view.js ├── media_sources.js ├── modal.js ├── react_wrapper.js ├── recentworkspaceview.js ├── select_directory_to_share.js ├── select_list_view.js ├── terminal_manager.js └── transport.js ├── menus └── floobits.cson ├── package.json ├── resources ├── anonymous.png ├── ascii_town.png ├── dark_noise.png ├── disc.png ├── edit_icon.png ├── email_icon.png ├── fl_logo146_30.png ├── fonts │ ├── glyphicons-halflings │ │ ├── glyphicons-halflings-regular.eot │ │ ├── glyphicons-halflings-regular.svg │ │ ├── glyphicons-halflings-regular.ttf │ │ └── glyphicons-halflings-regular.woff │ ├── montserrat │ │ ├── Montserrat-Bold.ttf │ │ ├── Montserrat-Regular.ttf │ │ └── OFL.txt │ ├── proxima_nova │ │ ├── ProximaNova-Bold.otf │ │ ├── ProximaNova-Extrabold.otf │ │ ├── ProximaNova-Light_0.otf │ │ ├── ProximaNova-Regular.otf │ │ ├── ProximaNova-Semibold.otf │ │ ├── ProximaNovaCond-Light.otf │ │ ├── ProximaNovaCond-RegularIt.otf │ │ └── ProximaNovaCond-Semibold.otf │ ├── robotoslab_bold_macroman │ │ ├── RobotoSlab-Bold-webfont.eot │ │ ├── RobotoSlab-Bold-webfont.svg │ │ ├── RobotoSlab-Bold-webfont.ttf │ │ ├── RobotoSlab-Bold-webfont.woff │ │ └── stylesheet.css │ ├── robotoslab_light_macroman │ │ ├── RobotoSlab-Light-webfont.eot │ │ ├── RobotoSlab-Light-webfont.svg │ │ ├── RobotoSlab-Light-webfont.ttf │ │ └── RobotoSlab-Light-webfont.woff │ ├── robotoslab_regular_macroman │ │ ├── RobotoSlab-Regular-webfont.eot │ │ ├── RobotoSlab-Regular-webfont.svg │ │ ├── RobotoSlab-Regular-webfont.ttf │ │ └── RobotoSlab-Regular-webfont.woff │ └── robotoslab_thin_macroman │ │ ├── RobotoSlab-Thin-webfont.eot │ │ ├── RobotoSlab-Thin-webfont.svg │ │ ├── RobotoSlab-Thin-webfont.ttf │ │ └── RobotoSlab-Thin-webfont.woff ├── icon_64x64.png ├── owner_icon.png ├── password_conf_icon.png ├── password_icon.png ├── settings_icon.png ├── sideways_infinity.png ├── sliderbg.png ├── sublime_demo.png ├── supported_bg.png ├── title_bg.png ├── username_icon.png ├── vim_demo.png └── workspace_icon.png ├── spec ├── floobits-spec.coffee └── floobits-view-spec.coffee ├── styles ├── colors.less ├── edit_permissions_wizard.less ├── floobits.atom-text-editor.less ├── fonts.less ├── icons.less ├── join.less ├── messages.less ├── modal.less ├── permissions.less ├── signin.less ├── status_bar.less ├── terminal.less ├── user_list_pane.less ├── userlist.less ├── util.less └── welcome.less └── templates ├── chat.js ├── code_review.js ├── conflicts.js ├── create_workspace.js ├── edit_perms_view.js ├── handle_request_perm.js ├── join.js ├── messages_view.js ├── mixins.js ├── pane.coffee ├── permission_view.js ├── status_bar.js ├── user_list_pane.js ├── user_view.js ├── webview.js └── yes_no_cancel.js /.eslintignore: -------------------------------------------------------------------------------- 1 | lib/build 2 | lib/common/extern 3 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es6": true, 5 | "node": true 6 | }, 7 | "extends": ["eslint-config-floobits", "plugin:react/recommended"], 8 | "globals": { 9 | "atom": false, 10 | "document": false, 11 | "MediaStreamTrack": false, 12 | "window": false 13 | }, 14 | "plugins": [ 15 | "react" 16 | ], 17 | "rules": { 18 | "consistent-this": [1, "that"], 19 | "react/no-deprecated": [1], 20 | "react/no-is-mounted": [1], 21 | "react/prop-types": [0], 22 | "strict": [0], 23 | }, 24 | "settings": { 25 | "ecmascript": 6, 26 | "jsx": true 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /.floo: -------------------------------------------------------------------------------- 1 | { 2 | "url": "https://floobits.com/Floobits/atom" 3 | } -------------------------------------------------------------------------------- /.flooignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | tmp 3 | vendor 4 | lib/build -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .idea 3 | npm-debug.log 4 | node_modules 5 | lib/build/ 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | sudo: false 3 | 4 | branches: 5 | only: 6 | - master 7 | 8 | node_js: 9 | - "4" 10 | - "5" 11 | - "6" 12 | 13 | script: 14 | - npm run lint && npm test 15 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | Floobits plugin for Atom 2 | Copyright 2015 Floobits, Inc. 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # [Floobits](https://floobits.com/) for Atom 2 | 3 | ### Remote pair programming across editors. 4 | 5 | ### Development status: Beta 6 | 7 | [![Floobits Status](https://floobits.com/Floobits/atom.svg)](https://floobits.com/Floobits/atom/redirect) 8 | 9 | [![Build Status](https://travis-ci.org/Floobits/floobits-atom.svg?branch=master)](https://travis-ci.org/Floobits/floobits-atom) 10 | 11 | [![#floobits on Freenode](https://img.shields.io/Freenode/%23floobits.png)](https://webchat.freenode.net/?channels=floobits) 12 | 13 | Floobits adds support for real-time collaborative editing to text editors and IDEs. In addition to Atom, Floobits also supports [Sublime Text](https://github.com/Floobits/floobits-sublime), [Emacs](https://github.com/Floobits/floobits-emacs), [Vim](https://github.com/Floobits/floobits-vim), and [IntelliJ](https://github.com/Floobits/floobits-intellij). 14 | 15 | This plugin also allows collaborators to video chat using WebRTC and to share terminals via the [term3 plugin](https://atom.io/packages/term3) without leaving Atom. 16 | 17 | For a demo of this plugin in action, see [this video](https://www.youtube.com/watch?v=liwChJKd4og) of [ggreer](https://github.com/ggreer) and [btipling](https://github.com/btipling) pairing on [ag](https://github.com/ggreer/the_silver_searcher). 18 | 19 | [Documentation](https://floobits.com/help/plugins/atom) is on the Floobits website. 20 | 21 | --- 22 | 23 | ### Video chat in Atom 24 | 25 | Joining a Floobits workspace in Atom 26 | 27 | ### Collaboratively edit 28 | 29 | Editing together in Atom 30 | 31 | ### Share terminals 32 | 33 | Sharing terminals in Atom 34 | 35 | --- 36 | 37 | ## Configuration 38 | 39 | On first usage, the plugin will guide you through the setup process. If you don't already have a Floobits account, it will help you create one for free. 40 | 41 | 42 | ## Help 43 | 44 | If you have trouble setting up or using this plugin, please [contact us](https://floobits.com/help#support). 45 | -------------------------------------------------------------------------------- /keymaps/floobits.cson: -------------------------------------------------------------------------------- 1 | # Keybindings require three things to be fully defined: A selector that is 2 | # matched against the focused element, the keystroke and the command to 3 | # execute. 4 | # 5 | # Below is a basic keybinding which registers on all platforms by applying to 6 | # the root workspace element. 7 | 8 | # For more detailed documentation see 9 | # https://atom.io/docs/latest/advanced/keymaps 10 | 11 | 'atom-workspace': 12 | # 'ctrl-alt-o': 'Floobits: Join Recent Workspace' 13 | 'ctrl-shift-j': 'Floobits: Join Workspace' 14 | 'ctrl-shift-alt-a': 'Floobits: Toggle Follow Mode' 15 | 'ctrl-shift-alt-s': 'Floobits: Summon' 16 | 17 | 18 | '.floobits-nativize webview': 19 | 'tab': 'native!' 20 | 'shift-tab': 'native!' 21 | 'click': 'native!' 22 | 'enter': 'native!' 23 | 'backspace': 'native!' 24 | 'shift-backspace': 'native!' 25 | 'delete': 'native!' 26 | 'up': 'native!' 27 | 'down': 'native!' 28 | 'shift-up': 'native!' 29 | 'shift-down': 'native!' 30 | 'alt-up': 'native!' 31 | 'alt-down': 'native!' 32 | 'alt-shift-up': 'native!' 33 | 'alt-shift-down': 'native!' 34 | 'cmd-up': 'native!' 35 | 'cmd-down': 'native!' 36 | 'cmd-shift-up': 'native!' 37 | 'cmd-shift-down': 'native!' 38 | 'ctrl-up': 'native!' 39 | 'ctrl-down': 'native!' 40 | 'ctrl-shift-up': 'native!' 41 | 'ctrl-shift-down': 'native!' 42 | 'left': 'native!' 43 | 'right': 'native!' 44 | 'shift-left': 'native!' 45 | 'shift-right': 'native!' 46 | 'alt-left': 'native!' 47 | 'alt-right': 'native!' 48 | 'alt-shift-left': 'native!' 49 | 'alt-shift-right': 'native!' 50 | 'cmd-left': 'native!' 51 | 'cmd-right': 'native!' 52 | 'cmd-shift-left': 'native!' 53 | 'cmd-shift-right': 'native!' 54 | 'ctrl-left': 'native!' 55 | 'ctrl-right': 'native!' 56 | 'ctrl-shift-left': 'native!' 57 | 'ctrl-shift-right': 'native!' 58 | 'ctrl-b': 'native!' 59 | 'ctrl-f': 'native!' 60 | 'ctrl-F': 'native!' 61 | 'ctrl-B': 'native!' 62 | 'ctrl-h': 'native!' 63 | 'ctrl-d': 'native!' 64 | -------------------------------------------------------------------------------- /lib/atom_utils.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const _ = require("lodash"); 4 | const AtomRange = require("atom").Range; 5 | const wrapper = require("./react_wrapper"); 6 | 7 | module.exports = { 8 | rangeFromEditor: function (editor) { 9 | const selections = editor.selections; 10 | 11 | if (selections.length <= 0) { 12 | return [0, 0]; 13 | } 14 | 15 | return _.map(selections, function (selection) { 16 | const range = selection.getBufferRange(); 17 | const start = editor.buffer.characterIndexForPosition(range.start); 18 | const end = editor.buffer.characterIndexForPosition(range.end); 19 | return [start, end]; 20 | }); 21 | }, 22 | 23 | offsetToBufferRange: function (buffer, start, end) { 24 | if (!buffer) { 25 | return null; 26 | } 27 | if (!buffer.positionForCharacterIndex) { 28 | console.error("no buffer.positionForCharacterIndex()!"); 29 | return null; 30 | } 31 | const startPoint = buffer.positionForCharacterIndex(start); 32 | const endPoint = buffer.positionForCharacterIndex(end); 33 | return new AtomRange(startPoint, endPoint); 34 | }, 35 | addRightPanel: function (name, view, styles) { 36 | const DOMNode = wrapper.create_node(name, view, styles); 37 | const pane = atom.workspace.addRightPanel({item: DOMNode}); 38 | DOMNode.onDestroy(pane); 39 | return pane; 40 | }, 41 | addModalPanel: function (element_name, view) { 42 | const DOMNode = wrapper.create_node(element_name, view, {width: "100%", height: "100%", overflow: "auto"}); 43 | const pane = atom.workspace.addModalPanel({item: DOMNode}); 44 | DOMNode.onDestroy(pane); 45 | } 46 | }; 47 | -------------------------------------------------------------------------------- /lib/common/api.js: -------------------------------------------------------------------------------- 1 | /*global fl */ 2 | "use strict"; 3 | "use babel"; 4 | 5 | const _ = require("lodash"); 6 | const request = require("request"); 7 | const floorc = require("./floorc"); 8 | const utils = require("./utils"); 9 | 10 | const USER_AGENT = `Floobits Plugin ${fl.PLUGIN_VERSION} Atom-${process.version} ${process.platform} node-${process.versions.node}`; 11 | 12 | let ERRORS_SENT = 0; 13 | let MAX_ERROR_REPORTS = 5; 14 | let ERROR_COUNT = 0; 15 | 16 | function api_request (host, path, data, method, cb, opts) { 17 | const creds = floorc.auth[host]; 18 | const options = { 19 | headers: { 20 | "Accept": "application/json", 21 | "User-Agent": USER_AGENT, 22 | }, 23 | strictSSL: false, 24 | }; 25 | 26 | if (creds) { 27 | options.auth = { 28 | user: creds.api_key || creds.username, 29 | pass: creds.secret, 30 | sendImmediately: true 31 | }; 32 | } 33 | 34 | if (data) { 35 | options.json = data; 36 | } 37 | 38 | if (opts) { 39 | _.merge(options, opts); 40 | } 41 | 42 | const url = "https://" + host + path; 43 | try { 44 | request[method](url, options, function (err, res, body) { 45 | if (err) { 46 | return cb(err, res, body); 47 | } 48 | 49 | if (res.statusCode >= 300) { 50 | let message; 51 | try { 52 | message = JSON.parse(body).detail; 53 | return cb(message, res); 54 | } catch (ignored) { 55 | return cb(res.statusMessage || res.statusCode, res); 56 | } 57 | } 58 | if (data) { 59 | return cb(null, res, body); 60 | } 61 | try { 62 | return cb(null, res, JSON.parse(body)); 63 | } catch (e) { 64 | return cb(e, res, body); 65 | } 66 | }); 67 | } catch (e) { 68 | return cb(e); 69 | } 70 | } 71 | 72 | module.exports = { 73 | post_code_review: function (host, owner, workspace, description, cb) { 74 | const path = `/api/workspace/${owner}/${workspace}/review`; 75 | return api_request(host, path, {description: description}, "post", cb); 76 | }, 77 | get_credentials: function (host, username, password, cb) { 78 | return api_request(host, `/api/user/credentials/`, null, "get", cb, { 79 | auth: { 80 | user: username, 81 | pass: password 82 | } 83 | }); 84 | }, 85 | create_workspace: function (host, name, owner, perms, cb) { 86 | const path = `/api/workspace`; 87 | const post_data = { 88 | name: name, 89 | owner: owner, 90 | perms: perms 91 | }; 92 | return api_request(host, path, post_data, "post", cb); 93 | }, 94 | delete_workspace: function (host, owner, workspace, cb) { 95 | const path = `/api/workspace/${owner}/${workspace}`; 96 | return api_request(host, path, null, "DELETE", cb); 97 | }, 98 | update_workspace: function (workspace_url, data, cb) { 99 | var result = utils.parse_url(workspace_url); 100 | const path = `/api/workspace/${result.owner}/${result.workspace}`; 101 | return api_request(result.host, path, data, "put", cb); 102 | }, 103 | get_workspace_by_url: function (url, cb) { 104 | var result = utils.parse_url(url); 105 | const path = `/api/workspace/${result.owner}/${result.workspace}`; 106 | return api_request(result.host, path, null, "get", cb); 107 | }, 108 | get_workspace: function (host, owner, workspace, cb) { 109 | const path = `/api/workspace/${owner}/${workspace}`; 110 | return api_request(host, path, null, "get", cb); 111 | }, 112 | get_workspaces: function (host, cb) { 113 | const path = `/api/workspaces/can/view`; 114 | return api_request(host, path, null, "get", cb); 115 | }, 116 | get_orgs: function (host, cb) { 117 | const path = `/api/orgs`; 118 | return api_request(host, path, null, "get", cb); 119 | }, 120 | get_orgs_can_admin: function (host, cb) { 121 | const path = `/api/orgs/can/admin`; 122 | return api_request(host, path, null, "get", cb); 123 | }, 124 | send_error: function (description, error) { 125 | ERROR_COUNT += 1; 126 | let data = { 127 | jsondump: { 128 | error_count: ERROR_COUNT 129 | }, 130 | message: {}, 131 | dir: fl.base_path, 132 | }; 133 | 134 | let stack = ""; 135 | 136 | if (error) { 137 | stack = error.stack; 138 | description = description || error.message; 139 | } 140 | 141 | data.message = { 142 | description: description, 143 | stack: stack 144 | }; 145 | 146 | console.log("Floobits plugin error! Sending exception report: ", data.message); 147 | if (ERRORS_SENT >= MAX_ERROR_REPORTS) { 148 | console.warn("Already sent ", ERRORS_SENT, " errors this session. Not sending any more.\n", description, error, stack); 149 | return; 150 | } 151 | 152 | let floourl = require("../floobits").floourl; 153 | 154 | let host; 155 | 156 | if (floourl) { 157 | data.owner = floourl.owner; 158 | data.workspace = floourl.workspace; 159 | data.username = "????"; 160 | host = floourl.host; 161 | } else { 162 | host = "floobits.com"; 163 | } 164 | 165 | try { 166 | let api_url = `https://${host}/api/log`; 167 | ERRORS_SENT += 1; 168 | api_request(host, api_url, data, "get", function (err) { 169 | if (err) { 170 | console.error(err); 171 | } 172 | }); 173 | 174 | } catch (e) { 175 | console.error(e); 176 | } 177 | } 178 | }; 179 | 180 | 181 | 182 | // function send_errors (f) { 183 | // @wraps(f) 184 | // def wrapped(*args, **kwargs): 185 | // try: 186 | // return f(*args, **kwargs) 187 | 188 | // except Exception as e: 189 | // send_error(None, e) 190 | // raise 191 | // return wrapped 192 | // } 193 | -------------------------------------------------------------------------------- /lib/common/buffer_action.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var flux = require("flukes"); 4 | 5 | const Actions = flux.createActions({ 6 | changed: function (buf, constCharPointer, patches, username) { 7 | return [buf, constCharPointer || [buf.buf], patches, username]; 8 | }, 9 | deleted: function (buf, unlink) { 10 | return [buf, unlink]; 11 | }, 12 | saved: function (buf) { 13 | return buf; 14 | }, 15 | rename: function (buf, oldPath, newPath) { 16 | return [buf, oldPath, newPath]; 17 | }, 18 | created: function (buf, username, connID) { 19 | return [buf, username, connID]; 20 | }, 21 | // pseudo event for on RI 22 | add: function (buf) { 23 | return buf; 24 | }, 25 | }); 26 | 27 | module.exports = new Actions(); 28 | -------------------------------------------------------------------------------- /lib/common/button_model.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const flux = require("flukes"); 4 | 5 | const ButtonModel = flux.createModel({ 6 | modelName: "Button", 7 | fieldTypes: { 8 | name: flux.FieldTypes.string, 9 | action: flux.FieldTypes.func, 10 | classNames: flux.FieldTypes.arrayOf(flux.FieldTypes.string), 11 | }, 12 | getDefaultFields: function () { 13 | return { 14 | action: function () { return; }, 15 | classNames: [], 16 | }; 17 | } 18 | }); 19 | 20 | const Buttons = flux.createCollection({ 21 | modelName: "Buttons", 22 | model: ButtonModel, 23 | }); 24 | 25 | 26 | module.exports = { 27 | Button: ButtonModel, 28 | Buttons: Buttons, 29 | }; 30 | -------------------------------------------------------------------------------- /lib/common/constants.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | module.exports = { 4 | // HOST: "dev.fixtheco.de", 5 | HOST: "floobits.com", 6 | PORT: 3448, 7 | PLUGIN_VERSION: "0.0.1", 8 | VERSION: "0.11" 9 | }; 10 | -------------------------------------------------------------------------------- /lib/common/editor_action.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const _ = require("lodash"); 4 | const flux = require("flukes"); 5 | 6 | const floop = require("./floop"); 7 | 8 | const Actions = flux.createActions({ 9 | clear_highlights: function () { 10 | return; 11 | }, 12 | handle_conflicts: function (newFiles, different, missing, ignored, tooBig, justUpload) { 13 | return { 14 | newFiles: newFiles, 15 | different: different, 16 | missing: missing, 17 | ignored: ignored, 18 | tooBig: tooBig, 19 | justUpload: justUpload 20 | }; 21 | }, 22 | jump_to_user: function (username) { 23 | return username; 24 | }, 25 | kick: function (connectionIDorUsername) { 26 | let msg; 27 | if (_.isNumber(connectionIDorUsername)) { 28 | msg = {user_id: connectionIDorUsername}; 29 | } else { 30 | msg = {username: connectionIDorUsername}; 31 | } 32 | floop.send_kick(msg); 33 | return connectionIDorUsername; 34 | }, 35 | pref: function (name, value, force) { 36 | if (this.prefs[name] === value && !force) { 37 | return new Error("Preference " + name + " already has value " + value); 38 | } 39 | this.prefs[name] = value; 40 | // XXXX: hard-coded fl.editor_settings.prefs_path 41 | this.prefs.save("editor"); 42 | return [this.prefs, name]; 43 | }, 44 | follow: function (username) { 45 | let following; 46 | if (username) { 47 | following = this.prefs.followUsers.toggle(username); 48 | } else { 49 | following = this.prefs.following = !this.prefs.following; 50 | } 51 | this.prefs.save("editor"); 52 | if (this.prefs.isFollowing()) { 53 | this.jump_to_user(username); 54 | } 55 | return following; 56 | }, 57 | }); 58 | 59 | Actions.prototype.set = function (buffers, prefs) { 60 | this.buffers = buffers; 61 | this.prefs = prefs; 62 | }; 63 | 64 | module.exports = new Actions(); 65 | -------------------------------------------------------------------------------- /lib/common/emitter.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var _ = require("lodash"); 4 | 5 | /** 6 | * @constructor 7 | */ 8 | function Emitter () { 9 | /** 10 | * @type {number} 11 | * @private 12 | */ 13 | this.count = 0; 14 | /** 15 | * @type {Object} 16 | * @private 17 | */ 18 | this.on_ = {}; 19 | /** 20 | * @type {Object} 21 | * @private 22 | */ 23 | this.all_ = {}; 24 | } 25 | 26 | /** 27 | * @param {string} opt_event 28 | * @param {Function} listener 29 | * @param {Object=} opt_thisArg 30 | */ 31 | Emitter.prototype.on = function (opt_event, listener, opt_thisArg) { 32 | if (_.isFunction(opt_event)) { 33 | this.all_[++this.count] = listener ? opt_event.bind(listener) : opt_event; 34 | return this.count; 35 | } 36 | 37 | if (!_.has(this.on_, opt_event)) { 38 | this.on_[opt_event] = {}; 39 | } 40 | this.on_[opt_event][++this.count] = opt_thisArg ? listener.bind(opt_thisArg) : listener; 41 | return this.count; 42 | }; 43 | 44 | /** 45 | * @param {string} event 46 | */ 47 | Emitter.prototype.emit = function (event) { 48 | var args; 49 | 50 | args = _.toArray(arguments); 51 | 52 | _.each(this.all_, (l) => { 53 | l.apply(null, args); 54 | }); 55 | 56 | args.shift(); 57 | _.each((this.on_[event] || {}), (l) => { 58 | l.apply(null, args); 59 | }); 60 | }; 61 | 62 | Emitter.prototype.emitAsync = function (cb, event, args) { 63 | var events = this.on_[event] || {}, async = _.size(events) + _.size(this.all_); 64 | 65 | args = [args].concat(function popAsync() { 66 | if (async === 0) { 67 | throw new Error("cb fired more than once"); 68 | } 69 | async -= 1; 70 | if (async === 0) { 71 | return cb && cb(); 72 | } 73 | }); 74 | 75 | _.each(this.all_, (l) => { 76 | l.apply(null, args); 77 | }); 78 | 79 | _.each(events, (l) => { 80 | l.apply(null, args); 81 | }); 82 | }; 83 | 84 | /** 85 | * @param {string} event 86 | * @param {number} id 87 | */ 88 | Emitter.prototype.off = function (opt_event_or_id) { 89 | if (_.isNumber(opt_event_or_id)) { 90 | delete this.all_[opt_event_or_id]; 91 | // TODO: I don't think this does what it's supposed to do --ggreer 92 | _.each(this.on_, function (events) { 93 | delete events[opt_event_or_id]; 94 | }); 95 | return; 96 | } 97 | if (_.isString(opt_event_or_id)) { 98 | delete this.on_[opt_event_or_id]; 99 | return; 100 | } 101 | if (_.isUndefined(opt_event_or_id)) { 102 | this.all_ = {}; 103 | this.on_ = {}; 104 | return; 105 | } 106 | throw new Error("WTF is " + opt_event_or_id + " ?"); 107 | }; 108 | 109 | 110 | module.exports = Emitter; 111 | -------------------------------------------------------------------------------- /lib/common/ext_messaging.js: -------------------------------------------------------------------------------- 1 | /*global fl */ 2 | "use strict"; 3 | 4 | const _ = require("lodash"); 5 | const $ = require("atom-space-pen-views").$; 6 | 7 | const floop = require("./floop"); 8 | const webrtcAction = require("./webrtc_action"); 9 | 10 | var callback; 11 | 12 | /* 13 | * @param {Object} event 14 | */ 15 | function handleMessage(event) { 16 | event = event.originalEvent; 17 | if (!event.data) { 18 | return; 19 | } 20 | switch(event.data.name) { 21 | case "flooScreenShareResponse": 22 | console.log("Got content script stuff", event); 23 | callback(event.data.id); 24 | break; 25 | case "flooScreenHasExtension": 26 | console.log("Website: user has extension"); 27 | webrtcAction.can_share_screen(true); 28 | break; 29 | default: 30 | // no-op 31 | } 32 | } 33 | 34 | /** 35 | * @param {Function} cb 36 | */ 37 | function getSharedScreenId(cb) { 38 | callback = cb; 39 | setTimeout(function () { 40 | console.log("Getting screen share id"); 41 | window.postMessage({text: "flooScreenShare"}, "*"); 42 | }, 1); 43 | } 44 | 45 | /** 46 | * Let's ask to see if we can share the screen. 47 | */ 48 | function getCanShare() { 49 | setTimeout(function () { 50 | console.log("Checking to see if we have a chrome extension."); 51 | window.postMessage({text: "flooScreenDoWeHaveAnExtension"}, "*"); 52 | }, 0); 53 | } 54 | 55 | function init() { 56 | console.log("Initing ext msg."); 57 | $(window).on("message", handleMessage); 58 | 59 | function changeListener (evt) { 60 | var evt_data, 61 | origin, 62 | parse_url; 63 | 64 | parse_url = function (url) { 65 | var a = document.createElement("a"); 66 | a.href = url; 67 | return { 68 | hostname: a.hostname, 69 | port: a.port, 70 | protocol: a.protocol 71 | }; 72 | }; 73 | origin = parse_url(evt.origin); 74 | if (origin.protocol !== "https:" || !origin.hostname.match(/\.googleusercontent\.com$/)) { 75 | console.log("discarding because event origin is", origin); 76 | return; 77 | } 78 | console.log("got event", evt.data); 79 | try { 80 | evt_data = JSON.parse(evt.data); 81 | } catch (e) { 82 | console.log("Couldn't parse event json:", e); 83 | return; 84 | } 85 | if (evt_data.temp_data) { 86 | fl.editor_settings.temp_data = _.extend(fl.editor_settings.temp_data, 87 | evt_data.temp_data); 88 | try { 89 | floop.set_temp_data(evt_data.temp_data); 90 | } catch (e) { 91 | console.log(e); 92 | } 93 | } 94 | } 95 | 96 | if (window.parent) { 97 | if (window.addEventListener) { 98 | window.addEventListener("message", changeListener, false); 99 | } else if (window.attachEvent) { 100 | window.attachEvent("onmessage", changeListener); 101 | } 102 | window.parent.postMessage(JSON.stringify({ 103 | owner: fl.editor_settings.room_owner, 104 | room: fl.editor_settings.room 105 | }), 106 | "*"); 107 | } 108 | } 109 | 110 | module.exports = { 111 | getSharedScreenId: getSharedScreenId, 112 | getCanShare: getCanShare, 113 | init: init 114 | }; 115 | -------------------------------------------------------------------------------- /lib/common/extern/webrtc/gain_controller.js: -------------------------------------------------------------------------------- 1 | var webrtcsupport = require("./webrtc_support"); 2 | 3 | function GainController(stream) { 4 | this.support = webrtcsupport.webAudio && webrtcsupport.mediaStream; 5 | 6 | // set our starting value 7 | this.gain = 1; 8 | 9 | if (this.support) { 10 | var context = this.context = new webrtcsupport.AudioContext(); 11 | this.microphone = context.createMediaStreamSource(stream); 12 | this.gainFilter = context.createGain(); 13 | this.destination = context.createMediaStreamDestination(); 14 | this.outputStream = this.destination.stream; 15 | this.microphone.connect(this.gainFilter); 16 | this.gainFilter.connect(this.destination); 17 | stream.removeTrack(stream.getAudioTracks()[0]); 18 | stream.addTrack(this.outputStream.getAudioTracks()[0]); 19 | } 20 | this.stream = stream; 21 | } 22 | 23 | // setting 24 | GainController.prototype.setGain = function (val) { 25 | // check for support 26 | if (!this.support) return; 27 | this.gainFilter.gain.value = val; 28 | this.gain = val; 29 | }; 30 | 31 | GainController.prototype.getGain = function () { 32 | return this.gain; 33 | }; 34 | 35 | GainController.prototype.off = function () { 36 | return this.setGain(0); 37 | }; 38 | 39 | GainController.prototype.on = function () { 40 | this.setGain(1); 41 | }; 42 | 43 | module.exports = GainController; -------------------------------------------------------------------------------- /lib/common/extern/webrtc/get_user_media.js: -------------------------------------------------------------------------------- 1 | // getUserMedia helper by @HenrikJoreteg 2 | // 3 | 4 | var func = (navigator.getUserMedia || 5 | navigator.webkitGetUserMedia || 6 | navigator.mozGetUserMedia || 7 | navigator.msGetUserMedia); 8 | 9 | 10 | module.exports = function (constraints, cb) { 11 | var options; 12 | var haveOpts = arguments.length === 2; 13 | var defaultOpts = {video: true, audio: true}; 14 | var error; 15 | var denied = 'PERMISSION_DENIED'; 16 | var notSatified = 'CONSTRAINT_NOT_SATISFIED'; 17 | 18 | // make constraints optional 19 | if (!haveOpts) { 20 | cb = constraints; 21 | constraints = defaultOpts; 22 | } 23 | 24 | // treat lack of browser support like an error 25 | if (!func) { 26 | // throw proper error per spec 27 | error = new Error('NavigatorUserMediaError'); 28 | error.name = 'NOT_SUPPORTED_ERROR'; 29 | return cb(error); 30 | } 31 | 32 | func.call(navigator, constraints, function (stream) { 33 | cb(null, stream); 34 | }, function (err) { 35 | var error; 36 | // coerce into an error object since FF gives us a string 37 | // there are only two valid names according to the spec 38 | // we coerce all non-denied to "constraint not satisfied". 39 | if (typeof err === 'string') { 40 | error = new Error('NavigatorUserMediaError'); 41 | if (err === denied) { 42 | error.name = denied; 43 | } else { 44 | error.name = notSatified; 45 | } 46 | } else { 47 | // if we get an error object make sure '.name' property is set 48 | // according to spec: http://dev.w3.org/2011/webrtc/editor/getusermedia.html#navigatorusermediaerror-and-navigatorusermediaerrorcallback 49 | error = err; 50 | if (!error.name) { 51 | // this is likely chrome which 52 | // sets a property called "ERROR_DENIED" on the error object 53 | // if so we make sure to set a name 54 | if (error[denied]) { 55 | err.name = denied; 56 | } else { 57 | err.name = notSatified; 58 | } 59 | } 60 | } 61 | 62 | cb(error); 63 | }); 64 | }; 65 | -------------------------------------------------------------------------------- /lib/common/extern/webrtc/hark.js: -------------------------------------------------------------------------------- 1 | var WildEmitter = require("./wild_emitter"); 2 | 3 | function getMaxVolume (analyser, fftBins) { 4 | var maxVolume = -Infinity; 5 | analyser.getFloatFrequencyData(fftBins); 6 | 7 | for(var i=0, ii=fftBins.length; i < ii; i++) { 8 | if (fftBins[i] > maxVolume && fftBins[i] < 0) { 9 | maxVolume = fftBins[i]; 10 | } 11 | }; 12 | 13 | return maxVolume; 14 | } 15 | 16 | 17 | module.exports = function (stream, options) { 18 | var harker = new WildEmitter(); 19 | 20 | var audioContextType = window.webkitAudioContext || window.AudioContext; 21 | 22 | // make it not break in non-supported browsers 23 | if (!audioContextType) return harker; 24 | 25 | //Config 26 | var options = options || {}, 27 | smoothing = (options.smoothing || 0.5), 28 | interval = (options.interval || 100), 29 | threshold = options.threshold, 30 | play = options.play; 31 | 32 | //Setup Audio Context 33 | var audioContext = new audioContextType(); 34 | var sourceNode, fftBins, analyser; 35 | 36 | analyser = audioContext.createAnalyser(); 37 | analyser.fftSize = 512; 38 | analyser.smoothingTimeConstant = smoothing; 39 | fftBins = new Float32Array(analyser.fftSize); 40 | 41 | if (stream.jquery) stream = stream[0]; 42 | if (stream instanceof HTMLAudioElement) { 43 | //Audio Tag 44 | sourceNode = audioContext.createMediaElementSource(stream); 45 | if (typeof play === 'undefined') play = true; 46 | threshold = threshold || -65; 47 | } else { 48 | //WebRTC Stream 49 | sourceNode = audioContext.createMediaStreamSource(stream); 50 | threshold = threshold || -45; 51 | } 52 | 53 | sourceNode.connect(analyser); 54 | if (play) analyser.connect(audioContext.destination); 55 | 56 | harker.speaking = false; 57 | 58 | harker.setThreshold = function (t) { 59 | threshold = t; 60 | }; 61 | 62 | harker.setInterval = function (i) { 63 | interval = i; 64 | }; 65 | 66 | // Poll the analyser node to determine if speaking 67 | // and emit events if changed 68 | var looper = function () { 69 | setTimeout(function () { 70 | var currentVolume = getMaxVolume(analyser, fftBins); 71 | 72 | harker.emit('volume_change', currentVolume, threshold); 73 | 74 | if (currentVolume > threshold) { 75 | if (!harker.speaking) { 76 | harker.speaking = true; 77 | harker.emit('speaking'); 78 | } 79 | } else { 80 | if (harker.speaking) { 81 | harker.speaking = false; 82 | harker.emit('stopped_speaking'); 83 | } 84 | } 85 | 86 | looper(); 87 | }, interval); 88 | }; 89 | looper(); 90 | 91 | 92 | return harker; 93 | } 94 | -------------------------------------------------------------------------------- /lib/common/extern/webrtc/peer_connection.js: -------------------------------------------------------------------------------- 1 | var webrtcsupport = require("./webrtc_support"); 2 | var WildEmitter = require("./wild_emitter"); 3 | 4 | function PeerConnection(config, constraints) { 5 | var item; 6 | this.pc = new webrtcsupport.PeerConnection(config, constraints); 7 | WildEmitter.call(this); 8 | 9 | // proxy some events directly 10 | this.pc.onremovestream = this.emit.bind(this, 'removeStream'); 11 | this.pc.onnegotiationneeded = this.emit.bind(this, 'negotiationNeeded'); 12 | this.pc.oniceconnectionstatechange = this.emit.bind(this, 'iceConnectionStateChange'); 13 | this.pc.onsignalingstatechange = this.emit.bind(this, 'signalingStateChange'); 14 | 15 | // handle incoming ice and data channel events 16 | this.pc.onaddstream = this._onAddStream.bind(this); 17 | this.pc.onicecandidate = this._onIce.bind(this); 18 | this.pc.ondatachannel = this._onDataChannel.bind(this); 19 | 20 | // whether to use SDP hack for faster data transfer 21 | this.config = { 22 | debug: false, 23 | sdpHack: true 24 | }; 25 | 26 | // apply our config 27 | console.log("applying config", config); 28 | for (item in config) { 29 | console.log("item", item, config[item]); 30 | this.config[item] = config[item]; 31 | } 32 | 33 | if (this.config.debug) { 34 | this.on('*', function (eventName, event) { 35 | var logger = config.logger || console; 36 | logger.log('PeerConnection event:', arguments); 37 | }); 38 | } 39 | } 40 | 41 | PeerConnection.prototype = Object.create(WildEmitter.prototype, { 42 | constructor: { 43 | value: PeerConnection 44 | } 45 | }); 46 | 47 | // Add a stream to the peer connection object 48 | PeerConnection.prototype.addStream = function (stream) { 49 | this.localStream = stream; 50 | this.pc.addStream(stream); 51 | }; 52 | 53 | 54 | // Init and add ice candidate object with correct constructor 55 | PeerConnection.prototype.processIce = function (candidate) { 56 | this.pc.addIceCandidate(new webrtcsupport.IceCandidate(candidate)); 57 | }; 58 | 59 | // Generate and emit an offer with the given constraints 60 | PeerConnection.prototype.offer = function (constraints, cb) { 61 | var self = this; 62 | var hasConstraints = arguments.length === 2; 63 | var mediaConstraints = hasConstraints ? constraints : { 64 | mandatory: { 65 | OfferToReceiveAudio: true, 66 | OfferToReceiveVideo: true 67 | } 68 | }; 69 | var callback = hasConstraints ? cb : constraints; 70 | 71 | // Actually generate the offer 72 | this.pc.createOffer( 73 | function (offer) { 74 | offer.sdp = self._applySdpHack(offer.sdp); 75 | self.pc.setLocalDescription(offer); 76 | self.emit('offer', offer); 77 | if (callback) callback(null, offer); 78 | }, 79 | function (err) { 80 | self.emit('error', err); 81 | if (callback) callback(err); 82 | }, 83 | mediaConstraints 84 | ); 85 | }; 86 | 87 | // Answer an offer with audio only 88 | PeerConnection.prototype.answerAudioOnly = function (offer, cb) { 89 | var mediaConstraints = { 90 | mandatory: { 91 | OfferToReceiveAudio: true, 92 | OfferToReceiveVideo: false 93 | } 94 | }; 95 | this._answer(offer, mediaConstraints, cb); 96 | }; 97 | 98 | // Answer an offer without offering to recieve 99 | PeerConnection.prototype.answerBroadcastOnly = function (offer, cb) { 100 | var mediaConstraints = { 101 | mandatory: { 102 | OfferToReceiveAudio: false, 103 | OfferToReceiveVideo: false 104 | } 105 | }; 106 | this._answer(offer, mediaConstraints, cb); 107 | }; 108 | 109 | // Answer an offer with given constraints default is audio/video 110 | PeerConnection.prototype.answer = function (offer, constraints, cb) { 111 | var self = this; 112 | var hasConstraints = arguments.length === 3; 113 | var callback = hasConstraints ? cb : constraints; 114 | var mediaConstraints = hasConstraints ? constraints : { 115 | mandatory: { 116 | OfferToReceiveAudio: true, 117 | OfferToReceiveVideo: true 118 | } 119 | }; 120 | 121 | this._answer(offer, mediaConstraints, callback); 122 | }; 123 | 124 | // Process an answer 125 | PeerConnection.prototype.handleAnswer = function (answer) { 126 | this.pc.setRemoteDescription(new webrtcsupport.SessionDescription(answer)); 127 | }; 128 | 129 | // Close the peer connection 130 | PeerConnection.prototype.close = function () { 131 | this.pc.close(); 132 | this.emit('close'); 133 | }; 134 | 135 | // Internal code sharing for various types of answer methods 136 | PeerConnection.prototype._answer = function (offer, constraints, cb) { 137 | var self = this; 138 | this.pc.setRemoteDescription(new webrtcsupport.SessionDescription(offer), 139 | function () { 140 | self.pc.createAnswer( 141 | function (answer) { 142 | answer.sdp = self._applySdpHack(answer.sdp); 143 | self.pc.setLocalDescription(answer); 144 | self.emit('answer', answer); 145 | if (cb) cb(null, answer); 146 | }, function (err) { 147 | self.emit('error', err); 148 | if (cb) cb(err); 149 | }, 150 | constraints 151 | ); 152 | }, 153 | function (err) { 154 | console.log("setRemoteDescription error", err); 155 | cb(err); 156 | } 157 | ); 158 | }; 159 | 160 | // Internal method for emitting ice candidates on our peer object 161 | PeerConnection.prototype._onIce = function (event) { 162 | if (event.candidate) { 163 | this.emit('ice', event.candidate); 164 | } else { 165 | this.emit('endOfCandidates'); 166 | } 167 | }; 168 | 169 | // Internal method for processing a new data channel being added by the 170 | // other peer. 171 | PeerConnection.prototype._onDataChannel = function (event) { 172 | this.emit('addChannel', event.channel); 173 | }; 174 | 175 | // Internal handling of adding stream 176 | PeerConnection.prototype._onAddStream = function (event) { 177 | this.remoteStream = event.stream; 178 | this.emit('addStream', event); 179 | }; 180 | 181 | // SDP hack for increasing AS (application specific) data transfer speed allowed in chrome 182 | PeerConnection.prototype._applySdpHack = function (sdp) { 183 | var i, 184 | limits = this.config.limits || {}, 185 | line, 186 | sdp_lines; 187 | 188 | 189 | sdp_lines = sdp.split("\r\n"); // sdp lines have windows-style newlines 190 | console.log("sdp lines:", sdp_lines.join("\r\n")); 191 | if (!this.config.sdpHack) { 192 | console.log("sdp hack disabled."); 193 | return sdp; 194 | } 195 | 196 | for (i = 0; i < sdp_lines.length; i++) { 197 | line = sdp_lines[i]; 198 | if (line.search("mid:audio") >= 0) { 199 | // Audio max is 50kbit/sec 200 | sdp_lines.splice(i + 1, 0, "b=AS:" + limits.audio || "50"); 201 | console.log("set audio bandwidth"); 202 | continue; 203 | } 204 | if (line.search("mid:video") >= 0) { 205 | // Max video bandwidth is 150kbit/sec because we use tiny resolution 206 | sdp_lines.splice(i + 1, 0, "b=AS:" + limits.video || "200"); 207 | console.log("set video bandwidth"); 208 | continue; 209 | } 210 | } 211 | console.log("sdp lines after:", sdp_lines.join("\r\n")); 212 | return sdp_lines.join("\r\n"); 213 | }; 214 | 215 | // Create a data channel spec reference: 216 | // http://dev.w3.org/2011/webrtc/editor/webrtc.html#idl-def-RTCDataChannelInit 217 | PeerConnection.prototype.createDataChannel = function (name, opts) { 218 | opts = opts || {}; 219 | var reliable = !!opts.reliable; 220 | var protocol = opts.protocol || 'text/plain'; 221 | var negotiated = !!(opts.negotiated || opts.preset); 222 | var settings; 223 | var channel; 224 | // firefox is a bit more finnicky 225 | if (webrtcsupport.prefix === 'moz') { 226 | if (reliable) { 227 | settings = { 228 | protocol: protocol, 229 | preset: negotiated, 230 | stream: name 231 | }; 232 | } else { 233 | settings = {}; 234 | } 235 | channel = this.pc.createDataChannel(name, settings); 236 | channel.binaryType = 'blob'; 237 | } else { 238 | if (reliable) { 239 | settings = { 240 | reliable: true 241 | }; 242 | } else { 243 | settings = {reliable: false}; 244 | } 245 | channel = this.pc.createDataChannel(name, settings); 246 | } 247 | return channel; 248 | }; 249 | 250 | module.exports = PeerConnection; 251 | -------------------------------------------------------------------------------- /lib/common/extern/webrtc/webrtc_support.js: -------------------------------------------------------------------------------- 1 | // created by @HenrikJoreteg 2 | 3 | var prefix; 4 | var isChrome = false; 5 | var isFirefox = false; 6 | var ua = navigator.userAgent.toLowerCase(); 7 | 8 | // basic sniffing 9 | if (ua.indexOf('firefox') !== -1) { 10 | prefix = 'moz'; 11 | isFirefox = true; 12 | } else if (ua.indexOf('chrome') !== -1) { 13 | prefix = 'webkit'; 14 | isChrome = true; 15 | } 16 | 17 | var PC = window.mozRTCPeerConnection || window.webkitRTCPeerConnection; 18 | var IceCandidate = window.mozRTCIceCandidate || window.RTCIceCandidate; 19 | var SessionDescription = window.mozRTCSessionDescription || window.RTCSessionDescription; 20 | var MediaStream = window.webkitMediaStream || window.MediaStream; 21 | var screenSharing = navigator.userAgent.match('Chrome') && parseInt(navigator.userAgent.match(/Chrome\/(.*) /)[1], 10) >= 26; 22 | var AudioContext = window.AudioContext || window.webkitAudioContext; 23 | 24 | 25 | // export support flags and constructors.prototype && PC 26 | module.exports = { 27 | support: !!PC, 28 | dataChannel: isChrome || isFirefox || (PC && PC.prototype && PC.prototype.createDataChannel), 29 | prefix: prefix, 30 | webAudio: !!(AudioContext && AudioContext.prototype.createMediaStreamSource), 31 | mediaStream: !!(MediaStream && MediaStream.prototype.removeTrack), 32 | screenSharing: !!screenSharing, 33 | AudioContext: AudioContext, 34 | PeerConnection: PC, 35 | SessionDescription: SessionDescription, 36 | IceCandidate: IceCandidate 37 | }; 38 | -------------------------------------------------------------------------------- /lib/common/extern/webrtc/wild_emitter.js: -------------------------------------------------------------------------------- 1 | /* 2 | WildEmitter.js is a slim little event emitter by @henrikjoreteg largely based 3 | on @visionmedia's Emitter from UI Kit. 4 | 5 | Why? I wanted it standalone. 6 | 7 | I also wanted support for wildcard emitters like this: 8 | 9 | emitter.on('*', function (eventName, other, event, payloads) { 10 | 11 | }); 12 | 13 | emitter.on('somenamespace*', function (eventName, payloads) { 14 | 15 | }); 16 | 17 | Please note that callbacks triggered by wildcard registered events also get 18 | the event name as the first argument. 19 | */ 20 | 21 | function WildEmitter() { 22 | this.callbacks = {}; 23 | } 24 | 25 | // Listen on the given `event` with `fn`. Store a group name if present. 26 | WildEmitter.prototype.on = function (event, groupName, fn) { 27 | var hasGroup = (arguments.length === 3), 28 | group = hasGroup ? arguments[1] : undefined, 29 | func = hasGroup ? arguments[2] : arguments[1]; 30 | func._groupName = group; 31 | (this.callbacks[event] = this.callbacks[event] || []).push(func); 32 | return this; 33 | }; 34 | 35 | // Adds an `event` listener that will be invoked a single 36 | // time then automatically removed. 37 | WildEmitter.prototype.once = function (event, groupName, fn) { 38 | var self = this, 39 | hasGroup = (arguments.length === 3), 40 | group = hasGroup ? arguments[1] : undefined, 41 | func = hasGroup ? arguments[2] : arguments[1]; 42 | function on() { 43 | self.off(event, on); 44 | func.apply(this, arguments); 45 | } 46 | this.on(event, group, on); 47 | return this; 48 | }; 49 | 50 | // Unbinds an entire group 51 | WildEmitter.prototype.releaseGroup = function (groupName) { 52 | var item, i, len, handlers; 53 | for (item in this.callbacks) { 54 | handlers = this.callbacks[item]; 55 | for (i = 0, len = handlers.length; i < len; i++) { 56 | if (handlers[i]._groupName === groupName) { 57 | //console.log('removing'); 58 | // remove it and shorten the array we're looping through 59 | handlers.splice(i, 1); 60 | i--; 61 | len--; 62 | } 63 | } 64 | } 65 | return this; 66 | }; 67 | 68 | // Remove the given callback for `event` or all 69 | // registered callbacks. 70 | WildEmitter.prototype.off = function (event, fn) { 71 | var callbacks = this.callbacks[event], 72 | i; 73 | 74 | if (!callbacks) return this; 75 | 76 | // remove all handlers 77 | if (arguments.length === 1) { 78 | delete this.callbacks[event]; 79 | return this; 80 | } 81 | 82 | // remove specific handler 83 | i = callbacks.indexOf(fn); 84 | callbacks.splice(i, 1); 85 | return this; 86 | }; 87 | 88 | // Emit `event` with the given args. 89 | // also calls any `*` handlers 90 | WildEmitter.prototype.emit = function (event) { 91 | var args = [].slice.call(arguments, 1), 92 | callbacks = this.callbacks[event], 93 | specialCallbacks = this.getWildcardCallbacks(event), 94 | i, 95 | len, 96 | item; 97 | 98 | if (callbacks) { 99 | for (i = 0, len = callbacks.length; i < len; ++i) { 100 | if (callbacks[i]) { 101 | callbacks[i].apply(this, args); 102 | } else { 103 | break; 104 | } 105 | } 106 | } 107 | 108 | if (specialCallbacks) { 109 | for (i = 0, len = specialCallbacks.length; i < len; ++i) { 110 | if (specialCallbacks[i]) { 111 | specialCallbacks[i].apply(this, [event].concat(args)); 112 | } else { 113 | break; 114 | } 115 | } 116 | } 117 | 118 | return this; 119 | }; 120 | 121 | // Helper for for finding special wildcard event handlers that match the event 122 | WildEmitter.prototype.getWildcardCallbacks = function (eventName) { 123 | var item, 124 | split, 125 | result = []; 126 | 127 | for (item in this.callbacks) { 128 | split = item.split('*'); 129 | if (item === '*' || (split.length === 2 && eventName.slice(0, split[1].length) === split[1])) { 130 | result = result.concat(this.callbacks[item]); 131 | } 132 | } 133 | return result; 134 | }; 135 | 136 | module.exports = WildEmitter; 137 | -------------------------------------------------------------------------------- /lib/common/filetree_model.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var id = 0; 4 | const _ = require("lodash"); 5 | const flux = require("flukes"); 6 | 7 | /** 8 | * @extends {flux.DataModel} 9 | * @constructor 10 | */ 11 | const Filetree = flux.createModel({ 12 | modelName: "tree", 13 | fieldTypes: { 14 | id: flux.FieldTypes.number, 15 | tree: flux.FieldTypes.object, 16 | }, 17 | getDefaultFields: function () { 18 | return { 19 | id: ++id, 20 | }; 21 | }, 22 | addBuf: function (buf) { 23 | var node = this.tree, 24 | paths = buf.path.split("/"), 25 | name = paths.pop(); 26 | 27 | try { 28 | _.each(paths, function (p) { 29 | var n = node[p]; 30 | if (!n) { 31 | n = {}; 32 | node[p] = n; 33 | } 34 | node = n; 35 | }); 36 | node[name] = buf.id; 37 | } catch (e) { 38 | console.error("Path conflict for", buf.path); 39 | return; 40 | } 41 | 42 | this.update(); 43 | }, 44 | removePath: function (bufPath) { 45 | var i, name, 46 | node = this.tree, 47 | paths = bufPath.split("/"); 48 | 49 | name = paths.pop(); 50 | for (i = 0; i < paths.length; i++) { 51 | node = node[paths[i]]; 52 | if (!node) { 53 | console.error("Couldn't find filetree node for path", bufPath); 54 | return; 55 | } 56 | } 57 | delete node[name]; 58 | if (!paths.length) { 59 | this.update(); 60 | return; 61 | } 62 | if (_.size(node) === 0) { 63 | this.removePath(paths.join("/")); 64 | return; 65 | } 66 | this.update(); 67 | } 68 | }); 69 | 70 | Filetree.Event = { 71 | OPEN: "open" 72 | }; 73 | 74 | module.exports = Filetree; 75 | -------------------------------------------------------------------------------- /lib/common/floop.js: -------------------------------------------------------------------------------- 1 | /* global */ 2 | "use strict"; 3 | 4 | /** 5 | * @fileOverview Connects to floobits via something. 6 | */ 7 | 8 | let authorized = false; 9 | 10 | const _ = require("lodash"); 11 | const flux = require("flukes"); 12 | const perms = require("./permission_model"); 13 | 14 | var EVENTS, actions, 15 | // messageAction = require("./editor/message_action"), 16 | reqCallbacks = {}; 17 | 18 | actions = { 19 | socket_: null, 20 | capturedEvents: null, 21 | captureEvents: null, 22 | requestId: 0, 23 | reconnectTimeout: null, 24 | engineIoURL: null 25 | }; 26 | 27 | EVENTS = { 28 | IN: [ 29 | "ack", 30 | "create_buf", 31 | "create_term", 32 | "datamsg", 33 | "delete_buf", 34 | "delete_temp_data", 35 | "delete_term", 36 | "error", 37 | "get_buf", 38 | "highlight", 39 | "join", 40 | "kick", 41 | "msg", 42 | "part", 43 | "patch", 44 | "perms", 45 | "ping", 46 | "pong", 47 | "rename_buf", 48 | "request_perms", 49 | "room_info", 50 | "saved", 51 | "set_temp_data", 52 | "solicit", 53 | "sync", 54 | "term_stdin", 55 | "term_stdout", 56 | "update_term", 57 | "user_info", 58 | "webrtc", 59 | // TODO: something better than all events for all connections 60 | "create_user" 61 | ], 62 | OUT: [ 63 | "auth", 64 | "create_buf", 65 | "create_term", 66 | "datamsg", 67 | "delete_buf", 68 | "delete_term", 69 | "delete_temp_data", 70 | "get_buf", 71 | "highlight", 72 | "kick", 73 | "msg", 74 | "patch", 75 | "perms", 76 | "ping", 77 | "pong", 78 | "pull_repo", 79 | "rename_buf", 80 | "request_perms", 81 | "saved", 82 | "set_buf", 83 | "set_temp_data", 84 | "solicit", 85 | "term_stdin", 86 | "term_stdout", 87 | "update_term", 88 | "webrtc", 89 | ] 90 | }; 91 | 92 | _.each(EVENTS.OUT, function (name) { 93 | actions["send_" + name] = function actionOut (data, on_write, on_response) { 94 | const err = this.send_(name, data, on_write, on_response); 95 | return err || data; 96 | }; 97 | }); 98 | 99 | function actionIn (data) { 100 | var id = data.res_id, 101 | f = reqCallbacks[id]; 102 | 103 | if (!f) { 104 | return data; 105 | } 106 | delete reqCallbacks[id]; 107 | if (data.name === "error") { 108 | console.error(data.msg); 109 | f(data); 110 | return data; 111 | } 112 | f(null, data); 113 | return data; 114 | } 115 | 116 | _.each(EVENTS.IN, function (name) { 117 | actions[name] = actionIn; 118 | }); 119 | 120 | actions.auth = function (auth) { 121 | this.connected = true; 122 | return this.transport.write(null, auth); 123 | }; 124 | 125 | actions.disconnect = function (msg) { 126 | authorized = false; 127 | if (!this.transport) { 128 | return msg; 129 | } 130 | try { 131 | this.transport.removeListener("connected", this.onConnected_); 132 | this.transport.removeListener("disconnected", this.onDisconnected_); 133 | this.transport.removeListener("messaged", this.onMessaged_); 134 | } catch (e) { 135 | // Unused 136 | } 137 | 138 | try { 139 | this.transport.disconnect(); 140 | this.transport = null; 141 | } catch (e) { 142 | // Unused 143 | } 144 | 145 | return msg; 146 | }; 147 | 148 | actions.connect = function (transport, auth_blob) { 149 | var that = this; 150 | this.transport = transport; 151 | transport.connect(); 152 | that.onConnected_ = this.auth.bind(this, auth_blob); 153 | transport.on("connected", that.onConnected_); 154 | that.onDisconnected_ = function () { 155 | that.connected = false; 156 | authorized = false; 157 | }; 158 | transport.on("disconnected", that.onDisconnected_); 159 | that.onMessaged_ = function (name, data) { 160 | if (name === "room_info") { 161 | console.log("Now authorized."); 162 | authorized = true; 163 | } 164 | 165 | const f = that[name]; 166 | if (f) { 167 | f(data); 168 | } 169 | }; 170 | transport.on("messaged", that.onMessaged_); 171 | }; 172 | 173 | actions.ping = function () { 174 | if (!this.transport) { 175 | console.log("There is no socket to send with. We are currently disconnected."); 176 | return; 177 | } 178 | this.transport.write("pong", {}); 179 | }; 180 | 181 | actions.send_get_buf = function (id, on_response) { 182 | return this.send_("get_buf", {id: id}, null, on_response); 183 | }; 184 | 185 | /** 186 | * @param {*} data 187 | * @param {Array.} to 188 | * @return {number} request id. 189 | */ 190 | actions.emitDataMessage = function (data, to) { 191 | console.log("emitting data message", data, to); 192 | if (!this.transport) { 193 | console.log("There is no socket to send with. We are currently disconnected."); 194 | return; 195 | } 196 | this.transport.write("datamsg", { 197 | data: data, 198 | to: to 199 | }); 200 | }; 201 | 202 | actions.connected = false; 203 | 204 | let Actions = flux.createActions(actions); 205 | 206 | Actions.prototype.send_ = function (name, data, on_write, on_response) { 207 | if (!this.transport) { 208 | console.log("There is no socket to send with. We are currently disconnected."); 209 | return new Error("Floobits is not connected to a workspace."); 210 | } 211 | 212 | if (name !== "auth" && !authorized) { 213 | console.log("Not authorized yet."); 214 | return new Error("You are not authorized to do that (yet, maybe)."); 215 | } 216 | 217 | if (!("req_id" in data || "res_id" in data)) { 218 | data.req_id = ++this.requestId; 219 | } 220 | 221 | if (name && perms.indexOf(name) === -1) { 222 | console.error("OMG no permission to send", name); 223 | return new Error("You don't have permission to do that (%s)." % (name)); 224 | } 225 | 226 | this.transport.write(name, data, on_write); 227 | 228 | if (on_response) { 229 | reqCallbacks[data.req_id] = on_response; 230 | } 231 | }; 232 | 233 | module.exports = new Actions(); 234 | -------------------------------------------------------------------------------- /lib/common/floorc.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const fs = require("fs"); 4 | const path = require("path"); 5 | 6 | const _ = require("lodash"); 7 | 8 | const p = path.join(process.env[(process.platform === "win32") ? "USERPROFILE" : "HOME"], ".floorc.json"); 9 | let floorc = {}; 10 | 11 | function reloadFloorc() { 12 | let data = {auth: {}}; 13 | try { 14 | /*eslint-disable no-sync */ 15 | data = JSON.parse(fs.readFileSync(p, { 16 | encoding: "utf8", 17 | })); 18 | /*eslint-enable no-sync */ 19 | } catch (e) { 20 | console.warn(e); 21 | } 22 | try { 23 | _.merge(floorc, data); 24 | } catch (e) { 25 | console.error(e); 26 | return; 27 | } 28 | } 29 | 30 | floorc.__write = function () { 31 | /*eslint-disable no-sync */ 32 | fs.writeFileSync(p, JSON.stringify(floorc, null, 4)); 33 | /*eslint-enable no-sync */ 34 | }; 35 | 36 | try { 37 | fs.watch(p, reloadFloorc); 38 | } catch (e) { 39 | console.warn(e); 40 | } 41 | 42 | reloadFloorc(); 43 | 44 | module.exports = floorc; 45 | -------------------------------------------------------------------------------- /lib/common/handlers/account.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | "use babel"; 3 | 4 | const floop = require("../floop"); 5 | const constants = require("../constants"); 6 | const Transport = require("../../transport"); 7 | 8 | function AccountHandler(username, password, email) { 9 | this.username = username; 10 | this.password = password; 11 | this.email = email; 12 | } 13 | 14 | AccountHandler.prototype.start = function () { 15 | floop.onCREATE_USER(function (msg) { 16 | // TODO 17 | console.debug("User created:", msg); 18 | }); 19 | floop.onDISCONNECT(this.on_disconnect.bind(this)); 20 | floop.onERROR(this.on_error.bind(this)); 21 | floop.connect(new Transport(constants.HOST, constants.PORT), { 22 | name: "create_user", 23 | username: this.username, 24 | password: this.password, 25 | email: this.email, 26 | version: constants.VERSION, 27 | client: "Atom", 28 | platform: process.platform, 29 | }); 30 | }; 31 | 32 | AccountHandler.prototype.stop = function () { 33 | floop.off(); 34 | floop.disconnect(); 35 | }; 36 | 37 | AccountHandler.prototype.on_disconnect = function (d) { 38 | console.error("You were disconnected because", d.reason); 39 | }; 40 | 41 | AccountHandler.prototype.on_error = function (d) { 42 | console.error("You were disconnected because", d.reason); 43 | }; 44 | 45 | module.exports = AccountHandler; 46 | -------------------------------------------------------------------------------- /lib/common/ignore.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const fs = require("fs"); 4 | const path = require("path"); 5 | 6 | const _ = require("lodash"); 7 | const Minimatch = require("minimatch").Minimatch; 8 | 9 | const utils = require("./utils"); 10 | 11 | const IGNORE_FILE = ".flooignore"; 12 | 13 | 14 | // const HIDDEN_WHITELIST = [".floo"] + IGNORE_FILES; 15 | const BLACKLIST = [ 16 | ".DS_Store", 17 | ".git", 18 | ".svn", 19 | ".hg", 20 | ]; 21 | 22 | const DEFAULT_IGNORES = [ 23 | "#*", 24 | "*.o", 25 | "*.pyc", 26 | "*~", 27 | "extern/", 28 | "heroku.yml", 29 | "node_modules/", 30 | "tmp", 31 | "vendor/", 32 | ]; 33 | 34 | const MAX_FILE_SIZE = 1024 * 1024 * 5; 35 | 36 | 37 | // const IS_IG_IGNORED = 1; 38 | // const IS_IG_CHECK_CHILD = 2; 39 | 40 | function Ignore() { 41 | this.ignores = []; 42 | this.unignores = []; 43 | this.repo = null; 44 | } 45 | 46 | Ignore.prototype.init = function (directory, cb) { 47 | /* eslint-disable no-sync */ 48 | this.dir_path = directory.getRealPathSync(); 49 | const pretend_path = path.join(this.dir_path, IGNORE_FILE); 50 | /* eslint-enable no-sync */ 51 | _.each(BLACKLIST, this.add_ignore_entry_.bind(this, pretend_path)); 52 | this.create_flooignore_(this.dir_path); 53 | atom.project.repositoryForDirectory(directory).then((repo) => { 54 | this.repo = repo; 55 | return cb(); 56 | }, cb); 57 | }; 58 | 59 | Ignore.prototype.create_flooignore_ = function () { 60 | const flooignore = path.join(this.dir_path, IGNORE_FILE); 61 | try { 62 | /*eslint-disable no-sync */ 63 | if (!fs.existsSync(flooignore)) { 64 | fs.writeFileSync(flooignore, DEFAULT_IGNORES.join("\n")); 65 | } 66 | /*eslint-enable no-sync */ 67 | } catch (e) { 68 | console.error(e); 69 | } 70 | try { 71 | /*eslint-disable no-sync */ 72 | const data = fs.readFileSync(flooignore, { 73 | encoding: "utf8", 74 | }); 75 | /*eslint-enable no-sync */ 76 | _.each(data.split(/\n/), this.add_ignore_entry_.bind(this, flooignore)); 77 | } catch (e) { 78 | console.error(e); 79 | } 80 | }; 81 | 82 | Ignore.prototype.is_unignored_ = function (filePath) { 83 | for (let i in this.unignores) { 84 | if (this.unignores[i].match(filePath)) { 85 | return true; 86 | } 87 | } 88 | return false; 89 | }; 90 | 91 | Ignore.prototype.is_ignored_ = function (filePath) { 92 | for (let i in this.ignores) { 93 | if (this.ignores[i].match(filePath)) { 94 | return true; 95 | } 96 | } 97 | return false; 98 | }; 99 | 100 | Ignore.prototype.is_ignored = function (absOSPath) { 101 | const gitIgnored = this.repo && this.repo.isPathIgnored(absOSPath) && absOSPath !== this.dir_path; 102 | const flooIgnored = this.is_ignored_(absOSPath); 103 | if (gitIgnored || flooIgnored) { 104 | if (this.is_unignored_(absOSPath)) { 105 | return false; 106 | } 107 | return true; 108 | } 109 | return false; 110 | }; 111 | 112 | Ignore.prototype.getSize = function (filePath) { 113 | try { 114 | /*eslint-disable no-sync */ 115 | const stats = fs.statSync(filePath); 116 | /*eslint-enable no-sync */ 117 | return stats.size; 118 | } catch (e) { 119 | console.error(e); 120 | return 0; 121 | } 122 | }; 123 | 124 | Ignore.prototype.is_too_big = function (size) { 125 | return size > MAX_FILE_SIZE; 126 | }; 127 | 128 | Ignore.prototype.add_ignore_entry_ = function (filePath, line) { 129 | if (!line) { 130 | return; 131 | } 132 | const dir = path.dirname(filePath); 133 | const rel = utils.to_rel_path(dir); 134 | 135 | // console.log("s:", line); 136 | const negate = line[0] === "!"; 137 | if (negate) { 138 | line = line.slice(1); 139 | } 140 | if (line[0] === "/") { 141 | line = path.join(dir, line); 142 | } else { 143 | line = path.join(rel, "**", line); 144 | } 145 | 146 | if (line[line.length - 1] === "/") { 147 | line += "**"; 148 | } 149 | 150 | const match = new Minimatch(line); 151 | if (!_.isFunction(match.match)) { 152 | console.warn(`Floobits: Could not parse ignore match from ${line}`); 153 | return; 154 | } 155 | 156 | if (negate) { 157 | this.unignores.push(match); 158 | } else { 159 | this.ignores.push(match); 160 | } 161 | }; 162 | 163 | Ignore.prototype.add_ignore = function (file) { 164 | const name = file.getBaseName(); 165 | if (name !== IGNORE_FILE) { 166 | return; 167 | } 168 | 169 | let data; 170 | try { 171 | /*eslint-disable no-sync */ 172 | data = file.readSync(); 173 | /*eslint-enable no-sync */ 174 | } catch (e) { 175 | console.error(e); 176 | return; 177 | } 178 | 179 | /* eslint-disable no-sync */ 180 | const p = file.getRealPathSync(); 181 | /* eslint-enable no-sync */ 182 | _.each(data.split(/\n/), this.add_ignore_entry_.bind(this, p)); 183 | }; 184 | 185 | module.exports = new Ignore(); 186 | -------------------------------------------------------------------------------- /lib/common/message_action.js: -------------------------------------------------------------------------------- 1 | /* @flow weak */ 2 | /*global self, fl, Notification */ 3 | "use strict"; 4 | 5 | const _ = require("lodash"); 6 | const flux = require("flukes"); 7 | 8 | const buttonModel = require("./button_model"); 9 | const messagesModel = require("./message_model"); 10 | const prefs = require("./userPref_model"); 11 | 12 | const NOTIFICATION_DURATION = 5 * 1000; 13 | const notifications = []; 14 | 15 | 16 | // Clear notifications on window close 17 | self.addEventListener("beforeunload", function () { 18 | _.each(notifications, function (n) { 19 | n.close(); 20 | }); 21 | }); 22 | 23 | const Actions = flux.createActions({ 24 | notify: function (msg, force) { 25 | var n; 26 | if (force !== true) { 27 | if (!prefs.showNotifications || !prefs.canNotify) { 28 | return; 29 | } 30 | // if (window.document.hasFocus && window.document.hasFocus()) { 31 | // return; 32 | // } 33 | } 34 | if (fl.editor_settings) { 35 | msg = fl.editor_settings.room + ": " + msg; 36 | } 37 | n = new Notification("floobits", { 38 | body: msg, 39 | icon: "atom://floobits/resources/icon_64x64.png", 40 | }); 41 | 42 | notifications.push(n); 43 | 44 | n.onshow = function () { 45 | _.delay(function () { 46 | n.close(); 47 | notifications.splice(notifications.indexOf(n), 1); 48 | }, NOTIFICATION_DURATION); 49 | }; 50 | }, 51 | log: function (msg, opt_alertLvl, notify) { 52 | var msgObject; 53 | opt_alertLvl = opt_alertLvl || messagesModel.LEVEL.INFO; 54 | msgObject = new messagesModel.Message({ 55 | msg: msg, 56 | type: "log", 57 | level: opt_alertLvl, 58 | }); 59 | this.messages.insert(msgObject, 0); 60 | if (notify === false) { 61 | return msgObject; 62 | } 63 | if (notify === true || opt_alertLvl >= messagesModel.LEVEL.WARNING) { 64 | this.notify(msg); 65 | } 66 | return msgObject; 67 | }, 68 | info: function (msg, notify) { 69 | return this.log(msg, messagesModel.LEVEL.INFO, notify); 70 | }, 71 | success: function (msg, notify) { 72 | return this.log(msg, messagesModel.LEVEL.SUCCESS, notify); 73 | }, 74 | warn: function (msg, notify) { 75 | return this.log(msg, messagesModel.LEVEL.WARNING, notify); 76 | }, 77 | error: function (msg, notify) { 78 | return this.log(msg, messagesModel.LEVEL.DANGER, notify); 79 | }, 80 | modal: function (msg) { 81 | return msg; 82 | }, 83 | interactive: function (msg, buttons) { 84 | var msgObject = new messagesModel.Message({ 85 | msg: msg, 86 | type: "interactive", 87 | buttons: buttons.map(function (b) { 88 | return new buttonModel.Button(b); 89 | }), 90 | }); 91 | this.messages.insert(msgObject, 0); 92 | return msgObject; 93 | }, 94 | user: function (username, msg, time) { 95 | var msgObject = new messagesModel.Message({ 96 | type: "user", 97 | username: username, 98 | msg: msg, 99 | time: time * 1000 || Date.now(), 100 | }); 101 | this.messages.insert(msgObject, 0); 102 | if (username !== fl.username) { 103 | this.notify("<" + username + "> " + msg); 104 | } 105 | return msgObject; 106 | }, 107 | messages: messagesModel.Messages, 108 | }); 109 | 110 | module.exports = new Actions(); 111 | -------------------------------------------------------------------------------- /lib/common/message_model.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | "use babel"; 3 | /* @flow weak */ 4 | 5 | var Message, 6 | Messages, 7 | LEVEL, 8 | LEVEL_TO_STRING = {}, 9 | id = 0; 10 | 11 | const _ = require("lodash"); 12 | const flux = require("flukes"); 13 | const utils = require("./utils"); 14 | 15 | LEVEL = { 16 | INFO: 0, 17 | SUCCESS: 1, 18 | WARNING: 2, 19 | DANGER: 3, 20 | }; 21 | 22 | _.each(LEVEL, function (v, k) { 23 | LEVEL_TO_STRING[v] = k.toLowerCase(); 24 | }); 25 | 26 | /** 27 | * @param {Object} data 28 | * @param {number} id 29 | * @param {string} owner 30 | * @param {string} workspace 31 | * @constructor 32 | */ 33 | 34 | Message = flux.createModel({ 35 | modelName: "Message", 36 | fieldTypes: { 37 | id: flux.FieldTypes.number, 38 | username: flux.FieldTypes.string, 39 | msg: flux.FieldTypes.string, 40 | type: flux.FieldTypes.string, 41 | level: flux.FieldTypes.number, 42 | time: flux.FieldTypes.number, 43 | buttons: flux.FieldTypes.collection, 44 | }, 45 | getDefaultFields: function () { 46 | return { 47 | id: ++id, 48 | time: Date.now() 49 | }; 50 | }, 51 | }); 52 | 53 | Object.defineProperty(Message.prototype, "levelName", { 54 | get: function () { 55 | return LEVEL_TO_STRING[this.level]; 56 | } 57 | }); 58 | 59 | Object.defineProperty(Message.prototype, "prettyTime", { 60 | get: function () { 61 | var d = utils.formatDate(new Date(this.time)); 62 | return `${d.hour}:${d.minute} ${d.meridian}`; 63 | } 64 | }); 65 | 66 | Messages = flux.createCollection({ 67 | modelName: "Messages", 68 | model: Message, 69 | didUpdate: function () { 70 | // Limit to 1k messages max 71 | this.splice(1000); 72 | } 73 | }); 74 | 75 | module.exports = { 76 | LEVEL: LEVEL, 77 | Message: Message, 78 | Messages: new Messages(), 79 | }; 80 | -------------------------------------------------------------------------------- /lib/common/msg_model.js: -------------------------------------------------------------------------------- 1 | /* @flow weak */ 2 | /** @jsx React.DOM */ 3 | /** @fileOverview User preferences. */ 4 | "use strict"; 5 | 6 | const flux = require("flukes"); 7 | 8 | module.exports = new flux.List(); 9 | -------------------------------------------------------------------------------- /lib/common/mugshot.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const _ = require("lodash"); 4 | const getUserMedia = require("./extern/webrtc/get_user_media"); 5 | 6 | // const handlerActions = require("./handler_action"); 7 | const messageAction = require("./message_action"); 8 | const perms = require("./permission_model"); 9 | const prefs = require("./userPref_model"); 10 | const WebRTC = require("./webrtc"); 11 | 12 | const THUMB_WIDTH = 228; 13 | const THUMB_HEIGHT = 228; 14 | 15 | function MugShot() { 16 | this.snapshotInterval = null; 17 | this.drawTimeouts = {}; 18 | this.stream = null; 19 | } 20 | 21 | MugShot.prototype.start = function (users, myConn) { 22 | if (perms.indexOf("patch") === -1) { 23 | messageAction.info("Mugshots require edit permission."); 24 | return; 25 | } 26 | this.stop(); 27 | this.users = users; 28 | this.myConn = myConn; 29 | this.snapshotInterval = setInterval(_.bind(this.snapshot, this), 60000); 30 | this.snapshot(); 31 | }; 32 | 33 | MugShot.prototype.stop = function () { 34 | clearInterval(this.snapshotInterval); 35 | this.snapshotInterval = null; 36 | this.clearDrawTimeouts(); 37 | this.stopStream(); 38 | if (this.myConn) { 39 | this.myConn.image = null; 40 | } 41 | }; 42 | 43 | MugShot.prototype.snapshot = function () { 44 | console.log("Creating snapshot."); 45 | 46 | const options = { 47 | audio: false, 48 | video: { optional: [ 49 | { maxWidth: THUMB_WIDTH }, 50 | { maxHeight: THUMB_HEIGHT }, 51 | ]} 52 | }; 53 | 54 | if (prefs.source_video_id) { 55 | options.video.optional.push({ sourceId: prefs.source_video_id }); 56 | } else { 57 | options.video.optional.push({ facingMode: "user" }); 58 | } 59 | 60 | getUserMedia(options, _.bind(this.gotUserMedia, this)); 61 | }; 62 | 63 | MugShot.prototype.clearDrawTimeouts = function () { 64 | _.each(this.drawTimeouts, function (timeout) { 65 | clearTimeout(timeout); 66 | }); 67 | this.drawTimeouts = {}; 68 | }; 69 | 70 | MugShot.prototype.stopStream = function () { 71 | if (this.stream) { 72 | try { 73 | WebRTC.stopStream(this.stream); 74 | } catch (e) { 75 | console.error("Error stopping stream in stopStream:", e); 76 | } 77 | this.stream = null; 78 | } 79 | 80 | this.clearDrawTimeouts(); 81 | }; 82 | 83 | /** 84 | * @param {Object} media 85 | */ 86 | MugShot.prototype.gotUserMedia = function (err, stream) { 87 | if (err) { 88 | console.error("Error getting snapshot!", err); 89 | return; 90 | } 91 | 92 | if (this.stream) { 93 | console.error("Mug shot stream already exists! Killing old stream"); 94 | try { 95 | this.stream.stop(); 96 | } catch (e) { 97 | console.error("Error stopping stream in gotUserMedia:", e); 98 | } 99 | } 100 | 101 | this.stream = stream; 102 | 103 | this.getSnapshotFromStream(stream, function (snapErr, data) { 104 | if (snapErr) { 105 | console.error("Error getting snapshot from stream:", snapErr); 106 | } 107 | this.users.broadcast_data_message_for_perm({ 108 | name: "user_image", 109 | image: data, 110 | }, "get_buf"); 111 | this.myConn.image = data; 112 | this.stopStream(); 113 | }.bind(this)); 114 | }; 115 | 116 | // TODO: remove this.drawTimeout. right now capturing two mugshots from different streams will break 117 | MugShot.prototype.getSnapshotFromStream = function (stream, cb) { 118 | const self = this; 119 | 120 | let video = document.createElement("video"); 121 | video.setAttribute("width", THUMB_WIDTH); 122 | video.setAttribute("height", THUMB_HEIGHT); 123 | 124 | const canvas = document.createElement("canvas"); 125 | canvas.setAttribute("width", THUMB_WIDTH); 126 | canvas.setAttribute("height", THUMB_HEIGHT); 127 | 128 | let canvasContext = canvas.getContext("2d"); 129 | 130 | // I think the window stuff is to grab fullscreen video? 131 | video.src = (window.URL && window.URL.createObjectURL(stream)) || stream; 132 | video.play(); 133 | 134 | function done(err, result) { 135 | try { 136 | // Please oh please oh please get GC'd! 137 | video.pause(); 138 | video.src = ""; 139 | video = null; 140 | } catch (e) { 141 | console.error("Mugshot draw: Error pausing video:", e); 142 | } 143 | try { 144 | delete self.drawTimeouts[stream]; 145 | } catch (unused) { 146 | // Ignore 147 | } 148 | return cb(err, result); 149 | } 150 | 151 | // Some cameras take time to warm up. Check image data for values besides 0 before displaying. 152 | // Typical framerate is 30fps, so give up after 1.5 seconds 153 | let i = 45; 154 | function draw() { 155 | var data; 156 | i--; 157 | try { 158 | canvasContext.clearRect(0, 0, canvas.width, canvas.height); 159 | canvasContext.drawImage(video, 0, 0, video.width, video.height); 160 | } catch (e) { 161 | console.log("Capture self shot failed.", e); 162 | } 163 | data = self.handleFrame_(canvas, canvasContext, false); 164 | if (data) { 165 | console.log("handled snapshot"); 166 | return done(null, data); 167 | } 168 | if (i <= 0) { 169 | console.log("snapshot still not ready. forcing"); 170 | data = self.handleFrame_(canvas, canvasContext, true); 171 | return done(null, data); 172 | } 173 | // 30fps === 33.3...ms per frame 174 | self.drawTimeouts[stream] = setTimeout(draw, 34); 175 | } 176 | _.defer(draw.bind(this)); 177 | }; 178 | 179 | MugShot.prototype.handleFrame_ = function (canvas, canvasContext, force) { 180 | var data, 181 | i, r, g, b, 182 | imageData = canvasContext.getImageData(0, 0, canvas.width, canvas.height), 183 | goodPixels = 0; 184 | 185 | data = new Array(imageData.data.length / 4); 186 | for (i = 0; i < data.length; i++) { 187 | r = imageData.data[i * 4]; 188 | g = imageData.data[i * 4 + 1]; 189 | b = imageData.data[i * 4 + 2]; 190 | data[i] = Math.round(0.21 * r + 0.72 * g + 0.07 * b); 191 | imageData.data[i * 4] = data[i]; 192 | imageData.data[i * 4 + 1] = data[i]; 193 | imageData.data[i * 4 + 2] = data[i]; 194 | if (data[i] > 20 && data[i] < 240) { 195 | goodPixels++; 196 | } 197 | } 198 | if (goodPixels < (imageData.data.length * 0.10) && !force) { 199 | return null; 200 | } 201 | canvasContext.putImageData(imageData, 0, 0); 202 | data = { 203 | width: canvas.width, 204 | height: canvas.height, 205 | data: canvas.toDataURL(), 206 | }; 207 | 208 | return data; 209 | }; 210 | 211 | module.exports = new MugShot(); 212 | -------------------------------------------------------------------------------- /lib/common/permission_model.js: -------------------------------------------------------------------------------- 1 | /* @flow weak */ 2 | "use strict"; 3 | 4 | var flux = require("flukes"); 5 | 6 | module.exports = new flux.List([]); 7 | -------------------------------------------------------------------------------- /lib/common/persistentjson.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const utils = require("./utils"); 4 | const _ = require("lodash"); 5 | const fs = require("fs-plus"); 6 | 7 | function PersistentJson() { 8 | this.data = {}; 9 | const floorc = require("./floorc"); 10 | if (floorc.share_dir) { 11 | this.path = fs.absolute(`${floorc.share_dir}/persistent.json`); 12 | } else { 13 | this.path = fs.absolute("~/floobits/persistent.json"); 14 | } 15 | } 16 | 17 | PersistentJson.prototype.load = function () { 18 | let d; 19 | try { 20 | /*eslint-disable no-sync */ 21 | d = fs.readFileSync(this.path, {encoding: "utf8"}); 22 | /*eslint-enable no-sync */ 23 | this.data = JSON.parse(d); 24 | } catch (e) { 25 | console.error(e); 26 | this.data = {}; 27 | } 28 | 29 | return this.data; 30 | }; 31 | 32 | PersistentJson.prototype.update = function (path, url) { 33 | const recent_workspaces = this.data.recent_workspaces || []; 34 | let index = -1; 35 | _.each(recent_workspaces, function (w, i) { 36 | if (w.url === url) { 37 | index = i; 38 | return false; 39 | } 40 | }); 41 | 42 | if (index >= 0) { 43 | recent_workspaces.splice(index, 1); 44 | } 45 | recent_workspaces.unshift({url: url}); 46 | this.data.recent_workspaces = recent_workspaces; 47 | 48 | const floourl = utils.parse_url(url); 49 | if (!this.data.workspaces) { 50 | this.data.workspaces = {}; 51 | } 52 | const workspaces = this.data.workspaces; 53 | if (!(floourl.owner in workspaces)) { 54 | workspaces[floourl.owner] = {}; 55 | } 56 | const owner = workspaces[floourl.owner]; 57 | if (!(floourl.workspace in owner)) { 58 | owner[floourl.workspace] = {}; 59 | } 60 | const workspace = owner[floourl.workspace]; 61 | workspace.path = path; 62 | workspace.url = url; 63 | }; 64 | 65 | PersistentJson.prototype.write = function () { 66 | /*eslint-disable no-sync */ 67 | fs.writeFileSync(this.path, JSON.stringify(this.data, null, 4), {encoding: "utf8"}); 68 | /*eslint-enable no-sync */ 69 | }; 70 | 71 | module.exports = PersistentJson; 72 | -------------------------------------------------------------------------------- /lib/common/sound_effects.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const _ = require("lodash"); 4 | const utils = require("./utils"); 5 | 6 | const prefs = require("./userPref_model"); 7 | 8 | var semitone, harmonicMinor, tempo, duration; 9 | 10 | window.AudioContext = window.AudioContext || window.webkitAudioContext; 11 | window.PannerNode = window.PannerNode || window.webkitAudioPannerNode; 12 | 13 | semitone = 1.0594545454545454; 14 | harmonicMinor = [0, 2, 3, 5, 7, 9, 11, 12]; 15 | tempo = 0.1; 16 | duration = { 17 | whole: tempo * 4, 18 | half: tempo * 2, 19 | quarter: tempo, 20 | eighth: tempo / 2 21 | }; 22 | 23 | function SoundEffects () { 24 | // var toggle = $("#toggle_sound_effects a"); 25 | if (!window.AudioContext) { 26 | return; 27 | } 28 | /** 29 | * Audio context to manipulate audio with. 30 | * @type {window.AudioContext} 31 | */ 32 | try { 33 | this.context = new window.AudioContext(); 34 | } catch (e) { 35 | console.error("Unable to create AudioContext for sound effects. :( Exception:", e); 36 | return; 37 | } 38 | 39 | try { 40 | this.gainNode = this.context.createGain(); 41 | this.gainNode.gain.value = 0.2; 42 | this.gainNode.connect(this.context.destination); 43 | } catch (e) { 44 | console.log("Your browser can't createGain(). Things might be loud. :( Exception:", e); 45 | this.gainNode = this.context.destination; 46 | } 47 | 48 | try { 49 | this.panner = this.context.createPanner(); 50 | this.panner.panningModel = window.PannerNode.HRTF; 51 | this.panner.setPosition(0, 1, 1); 52 | this.panner.connect(this.gainNode); 53 | } catch (e) { 54 | console.log("Your browser can't createPanner(). Things might sound weird. :( Exception:", e); 55 | this.panner = this.gainNode; 56 | } 57 | 58 | this.waveform = "square"; 59 | this.scale = this.makeScale(harmonicMinor, 220, 15); 60 | this.suspendTimeout = null; 61 | console.log("sound effects initialized"); 62 | // Prevent CPU usage by suspending everything until we need it. 63 | this.context.suspend(); 64 | } 65 | 66 | /** 67 | * @param {string} type 68 | * @param {number} frequency 69 | * @param {number} duration 70 | * @param {number} offset 71 | */ 72 | SoundEffects.prototype.playNote = function (type, frequency, noteDuration, offset) { 73 | if (!this.context) { 74 | return; 75 | } 76 | if (!prefs.sound) { 77 | console.debug("sound disabled. not playing", frequency, "for", noteDuration); 78 | return; 79 | } 80 | 81 | const play = () => { 82 | console.debug("playing", frequency, "for", noteDuration); 83 | const osc = this.context.createOscillator(); 84 | offset = offset || 0; 85 | 86 | osc.type = type; 87 | osc.frequency.value = frequency; 88 | 89 | osc.connect(this.panner); 90 | osc.start(this.context.currentTime + offset); 91 | osc.stop(this.context.currentTime + offset + noteDuration); 92 | }; 93 | 94 | clearTimeout(this.suspendTimeout); 95 | if (this.context.state === 'running') { 96 | play(); 97 | return; 98 | } 99 | this.context.resume().then(() => { 100 | clearTimeout(this.suspendTimeout); 101 | play(); 102 | this.suspendTimeout = setTimeout(() => { 103 | this.context.suspend(); 104 | }, noteDuration + offset + 10); 105 | }); 106 | }; 107 | 108 | /** 109 | * Used to track audio analysis. 110 | * @type {object} 111 | */ 112 | SoundEffects.prototype.audioAnalyseTracker = {}; 113 | 114 | /** 115 | * @param {Array.} scale 116 | * @param {number} root 117 | * @param {number} len 118 | */ 119 | SoundEffects.prototype.makeScale = function (scale, root, len) { 120 | var s, current, position; 121 | s = []; 122 | current = root; 123 | for (position = 0; s.length < len; position++) { 124 | if (_.includes(scale, position % 12)) { 125 | s.push(current); 126 | } 127 | current = current * semitone; 128 | } 129 | return s; 130 | }; 131 | 132 | /** 133 | * @param {string} username 134 | */ 135 | SoundEffects.prototype.join = function (username) { 136 | var i, note; 137 | i = _.reduce(utils.md5(username), function (memo, c) { 138 | return memo + c.charCodeAt(0); 139 | }, 0); 140 | note = this.scale[i % _.size(this.scale)]; 141 | this.playNote(this.waveform, this.scale[0], duration.quarter, 0); 142 | this.playNote(this.waveform, note, duration.quarter, duration.quarter); 143 | }; 144 | 145 | /** 146 | * @param {string} username 147 | */ 148 | SoundEffects.prototype.leave = function (username) { 149 | var i, note; 150 | i = _.reduce(utils.md5(username), function (memo, c) { 151 | return memo + c.charCodeAt(0); 152 | }, 0); 153 | note = this.scale[i % _.size(this.scale)]; 154 | this.playNote(this.waveform, note, duration.quarter, 0); 155 | this.playNote(this.waveform, this.scale[0], duration.quarter, duration.quarter); 156 | }; 157 | 158 | module.exports = new SoundEffects(); 159 | -------------------------------------------------------------------------------- /lib/common/terminal_model.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | // const _ = require("lodash"); 4 | const flux = require("flukes"); 5 | 6 | // const Socket = require("./floop"); 7 | // const editorAction = require("./editor_action"); 8 | 9 | const TerminalModel = flux.createModel({ 10 | modelName: "Terminal", 11 | fieldTypes: { 12 | id: flux.FieldTypes.number, 13 | local: flux.FieldTypes.bool.ephemeral(), 14 | term: flux.FieldTypes.object.ephemeral(), 15 | username: flux.FieldTypes.string, 16 | title: flux.FieldTypes.string, 17 | deleted: flux.FieldTypes.bool.ephemeral(), 18 | } 19 | }); 20 | 21 | const Terminals = flux.createCollection({ 22 | model: TerminalModel, 23 | modelName: "Terminals", 24 | users: null, 25 | getTerm_: function (id) { 26 | var term = this.get(id); 27 | return term && term.term; 28 | }, 29 | init: function (args, args2) { 30 | this.users = args2.users; 31 | 32 | // editorAction.onCLOSE_TERM(function (term) { 33 | // if (!term.deleted) { 34 | // return; 35 | // } 36 | // this.remove(term.id); 37 | // term.term.destroy(); 38 | // }, this); 39 | 40 | // Socket.onCREATE_TERM(function (term) { 41 | // var user = this.users.getByConnectionID(term.owner); 42 | // // this.addTerminal(term.id, term.term_name, term.size[0], term.size[1], user.id); 43 | // }, this); 44 | 45 | // Socket.onDELETE_TERM(function (data) { 46 | // const term = this.get(data.id); 47 | // if (!term || !term.term) { 48 | // return; 49 | // } 50 | // term.term.write("\r\n *** TERMINAL CLOSED *** \r\n"); 51 | // term.deleted = true; 52 | // }, this); 53 | 54 | // Socket.onTERM_STDOUT(function (data) { 55 | // var term = this.getTerm_(data.id); 56 | // if (!term || !term.children) { 57 | // return; 58 | // } 59 | // try { 60 | // term.write(new Buffer(data.data, "base64").toString("utf-8")); 61 | // } catch (e) { 62 | // console.warn(e); 63 | // } 64 | // }, this); 65 | 66 | // Socket.onUPDATE_TERM(function (data) { 67 | // var term = this.getTerm_(data.id); 68 | // console.log("update term", data); 69 | // if (term && data.size) { 70 | // term.resize(data.size[0], data.size[1]); 71 | // } 72 | // }, this); 73 | 74 | // // XXX Clean this up. 75 | // Socket.onSYNC(function (terms) { 76 | // console.log("Attempting to sync..."); 77 | // _.each(terms, function (data, id) { 78 | // var term = this.getTerm_(id); 79 | // if (!term) { 80 | // return; 81 | // } 82 | // term.resize(data.cols, data.rows); 83 | // }, this); 84 | // }, this); 85 | }, 86 | }); 87 | 88 | 89 | module.exports = { 90 | Terminals: Terminals 91 | }; 92 | -------------------------------------------------------------------------------- /lib/common/transport.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var util = require("util"); 4 | var messageAction = require("./message_action"); 5 | var Emitter = require("./emitter"); 6 | 7 | var STARTING_TIMEOUT = 200; 8 | var MAX_TIMEOUT = 10000; 9 | 10 | function Transport() { 11 | Emitter.call(this); 12 | 13 | this.timeout = STARTING_TIMEOUT; 14 | this.reconnectTimeout = null; 15 | this.on("connected", this.reset_timeout.bind(this)); 16 | } 17 | 18 | util.inherits(Transport, Emitter); 19 | 20 | Transport.prototype.connect = function () { 21 | throw new Error("not implemented"); 22 | }; 23 | 24 | Transport.prototype.disconnect = function () { 25 | this.reconnectTimeout = clearTimeout(this.reconnectTimeout); 26 | }; 27 | 28 | Transport.prototype.write = function (name, msg, cb, context) { 29 | console.debug(name, msg, cb, context); 30 | throw new Error("not implemented"); 31 | }; 32 | 33 | Transport.prototype.reset_timeout = function () { 34 | this.timeout = STARTING_TIMEOUT; 35 | this.reconnectTimeout = clearTimeout(this.reconnectTimeout); 36 | }; 37 | 38 | Transport.prototype.reconnect_ = function () { 39 | if (this.reconnectTimeout) { 40 | return; 41 | } 42 | 43 | this.reconnectTimeout = setTimeout(function () { 44 | this.reconnectTimeout = null; 45 | this.connect(); 46 | }.bind(this), this.timeout); 47 | messageAction.log(`Reconnecting in ${this.timeout}ms...`); 48 | this.timeout = Math.min(this.timeout * 2, MAX_TIMEOUT); 49 | }; 50 | 51 | module.exports = Transport; 52 | -------------------------------------------------------------------------------- /lib/common/userPref_model.js: -------------------------------------------------------------------------------- 1 | /* @flow weak */ 2 | /** @jsx React.DOM */ 3 | /*global self, Notification */ 4 | /** @fileOverview User preferences. */ 5 | "use strict"; 6 | 7 | const flux = require("flukes"); 8 | const _ = require("lodash"); 9 | let fieldTypes = flux.FieldTypes; 10 | var prefs; 11 | 12 | var UserPref = flux.createModel({ 13 | // TODO: post prefs back to django 14 | backend: flux.backends.local, 15 | followPaused: false, 16 | modelName: "UserPref", 17 | fieldTypes: { 18 | id: fieldTypes.string.defaults("UserPref"), 19 | theme: fieldTypes.string, 20 | sound: fieldTypes.bool.defaults(true), 21 | path: fieldTypes.string, 22 | following: fieldTypes.bool, 23 | followUsers: fieldTypes.list, 24 | logLevel: fieldTypes.number, 25 | showNotifications: fieldTypes.bool, 26 | showImages: fieldTypes.bool.defaults(true), 27 | dnd: fieldTypes.bool, 28 | mugshots: fieldTypes.bool.defaults(true), 29 | audioOnly: fieldTypes.bool, 30 | canNotify: fieldTypes.bool.ephemeral(), 31 | source_audio_id: fieldTypes.string.defaults(""), 32 | source_audio_name: fieldTypes.string.defaults("Default"), 33 | source_video_id: fieldTypes.string.defaults(""), 34 | source_video_name: fieldTypes.string.defaults("Default"), 35 | }, 36 | // TODO: save as a user pref in django-land 37 | // getDefaultFields: function () { 38 | // // var editorSettings = fl.editor_settings; 39 | // return { 40 | // theme: editorSettings.theme, 41 | // sound: !!editorSettings.sound, 42 | // following: !!editorSettings.follow_mode, 43 | // logLevel: editorSettings.logLevel, 44 | // }; 45 | // }, 46 | isFollowing: function (username) { 47 | if (this.followPaused) { 48 | return false; 49 | } 50 | if (_.isUndefined(username)) { 51 | if (this.followUsers.length > 0) { 52 | return true; 53 | } 54 | return this.following; 55 | } 56 | if(this.followUsers.indexOf(username) !== -1) { 57 | return true; 58 | } 59 | return this.following; 60 | }, 61 | pauseFollowMode: function (duration) { 62 | var that = this; 63 | 64 | function resetFollowMode () { 65 | that.followPaused = false; 66 | delete that.pauseFollowTimeout; 67 | } 68 | 69 | if (!this.following && !this.followUsers.length) { 70 | return; 71 | } 72 | 73 | clearTimeout(this.pauseFollowTimeout); 74 | this.followPaused = true; 75 | this.pauseFollowTimeout = setTimeout(resetFollowMode, duration); 76 | }, 77 | didUpdate: function (field) { 78 | if (_.isString(field) && field !== "showNotifications") { 79 | return; 80 | } 81 | if (_.isArray(field) && field.indexOf("showNotifications") === -1) { 82 | return; 83 | } 84 | this.requestNotificationPermission(); 85 | }, 86 | onNotificationPermission_: function (permission) { 87 | var canNotify = permission === "granted"; 88 | 89 | if (canNotify === this.canNotify) { 90 | return; 91 | } 92 | this.set({canNotify: canNotify}, {silent: true}); 93 | try { 94 | this.save(); 95 | } catch (ignore) { 96 | // Squelch 97 | } 98 | }, 99 | requestNotificationPermission: function () { 100 | if (!self.Notification || !_.isFunction(Notification.requestPermission) || !this.showNotifications) { 101 | return; 102 | } 103 | Notification.requestPermission(this.onNotificationPermission_.bind(this)); 104 | } 105 | }); 106 | 107 | // prefs = new UserPref(fl.editor_settings); 108 | prefs = new UserPref(); 109 | module.exports = prefs; 110 | -------------------------------------------------------------------------------- /lib/common/user_model.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const _ = require("lodash"); 4 | const flux = require("flukes"); 5 | 6 | const floop = require("./floop"); 7 | const utils = require("./utils"); 8 | const editorAction = require("./editor_action"); 9 | 10 | let context = null; 11 | try { 12 | context = window.AudioContext && new window.AudioContext(); 13 | } catch (e) { 14 | console.log("Unable to create context.", e); 15 | } 16 | const Visualizer = flux.createActions({ 17 | visualize: function (width) { return width; } 18 | }); 19 | 20 | 21 | const Connection = flux.createModel({ 22 | modelName: "Connection", 23 | fieldTypes: { 24 | id: flux.FieldTypes.number, 25 | path: flux.FieldTypes.string, 26 | bufId: flux.FieldTypes.number, 27 | client: flux.FieldTypes.string, 28 | platform: flux.FieldTypes.string, 29 | version: flux.FieldTypes.string, 30 | connected: flux.FieldTypes.bool, 31 | isMe: flux.FieldTypes.bool, 32 | // B&W thumbnail 33 | image: flux.FieldTypes.object.defaults(null), 34 | // Video or audio stream if chatting 35 | streamURL: flux.FieldTypes.string, 36 | audioOnly: flux.FieldTypes.bool, 37 | inVideoChat: flux.FieldTypes.bool, 38 | screenStreamURL: flux.FieldTypes.string 39 | }, 40 | init: function () { 41 | this.visualizer = new Visualizer(); 42 | }, 43 | visualizer: null, 44 | jsProcessor: null, 45 | stream: null, 46 | screenStream: null, 47 | isWeb: function () { 48 | return _.includes(utils.BROWSER_CLIENTS, this.client); 49 | }, 50 | onSoundEvent: function (analyser) { 51 | var data = new Uint8Array(analyser.frequencyBinCount), 52 | max = 0; 53 | analyser.getByteFrequencyData(data); 54 | if (!data.length) { 55 | return; 56 | } 57 | _.each(data, function (frame) { 58 | max = Math.max(max, frame); 59 | }); 60 | max = max / 7.96875; // 255 / 32 61 | this.visualizer.visualize(Math.min(max, 32)); 62 | }, 63 | stopStream: function () { 64 | if (this.jsProcessor) { 65 | this.jsProcessor.disconnect(); 66 | } 67 | this.jsProcessor = null; 68 | }, 69 | processStream: function (stream) { 70 | if (!context || !context.createAnalyser || !context.createScriptProcessor) { 71 | console.log("Browser has no audio analyser."); 72 | return; 73 | } 74 | console.log("begin audio thing"); 75 | let audioSource = null; 76 | try { 77 | audioSource = context.createMediaStreamSource(stream); 78 | } catch (e) { 79 | console.log("Unable to create media stream source.", e); 80 | return; 81 | } 82 | const analyser = context.createAnalyser(); 83 | analyser.fftSize = 32; 84 | analyser.smoothingTimeConstant = 0.3; 85 | const jsProcessor = context.createScriptProcessor(2048, 1, 1); // buffer size, input channels, output channels 86 | jsProcessor.onaudioprocess = this.onSoundEvent.bind(this, analyser); 87 | audioSource.connect(analyser); 88 | analyser.connect(jsProcessor); 89 | jsProcessor.connect(context.destination); 90 | this.stopStream(); 91 | this.jsProcessor = jsProcessor; 92 | } 93 | }); 94 | 95 | const Connections = flux.createCollection({ 96 | modelName: "Connections", 97 | model: Connection, 98 | sort: function (a, b) { 99 | // Web clients (ones with video chat) at the top, otherwise highest conn id wins 100 | var aIsWeb = a.isWeb(), 101 | bIsWeb = b.isWeb(); 102 | 103 | if (a.image && !b.image) { 104 | return -1; 105 | } 106 | if (b.image && !a.image) { 107 | return 1; 108 | } 109 | if (aIsWeb && !bIsWeb) { 110 | return -1; 111 | } 112 | if (bIsWeb && !aIsWeb) { 113 | return 1; 114 | } 115 | if (a.id > b.id) { 116 | return -1; 117 | } 118 | return 1; 119 | } 120 | }); 121 | 122 | function User () { 123 | User.super_.apply(this, arguments); 124 | } 125 | 126 | 127 | flux.inherit(User, flux.createModel({ 128 | modelName: "User", 129 | fieldTypes: { 130 | connections: Connections, 131 | id: flux.FieldTypes.string, 132 | permissions: flux.FieldTypes.list, 133 | isMe: flux.FieldTypes.bool, 134 | client: flux.FieldTypes.string, 135 | platform: flux.FieldTypes.string, 136 | isAnon: flux.FieldTypes.bool, 137 | version: flux.FieldTypes.string, 138 | can_contract: flux.FieldTypes.bool, 139 | rate: flux.FieldTypes.number, 140 | gravatar: flux.FieldTypes.string, 141 | }, 142 | init: function (args) { 143 | if (args && args.color_) { 144 | this.color_ = args.color_; 145 | } 146 | }, 147 | color_: null, 148 | getDefaultFields: function () { 149 | return { 150 | isAnon: true, 151 | permissions: new flux.List(), 152 | connections: new Connections() 153 | }; 154 | } 155 | })); 156 | 157 | Object.defineProperty(User.prototype, "color", { 158 | get: function id() { 159 | if (this.color_) { 160 | return this.color_; 161 | } 162 | this.color_ = utils.user_color(this.id); 163 | return this.color_; 164 | } 165 | }); 166 | 167 | Object.defineProperty(User.prototype, "isAdmin", { 168 | get: function isAdmin () { 169 | return this.permissions.indexOf("kick") !== -1; 170 | } 171 | }); 172 | 173 | Object.defineProperty(User.prototype, "username", { 174 | get: function getUsername() { 175 | return this.id; 176 | } 177 | }); 178 | 179 | User.prototype.getConnectionID = function () { 180 | var conn = this.connections.valueOf()[0]; 181 | return conn && conn.id; 182 | }; 183 | 184 | User.prototype.createConnection = function (connectionId, client, platform, version, isMe) { 185 | var conn = new Connection({ 186 | id: connectionId, 187 | path: "", 188 | bufId: 0, 189 | client: client, 190 | platform: platform, 191 | version: version, 192 | connected: true, 193 | isMe: isMe, 194 | inVideoChat: false, 195 | }); 196 | this.connections.add(conn); 197 | return conn; 198 | }; 199 | 200 | User.prototype.kick = function () { 201 | this.connections.forEach(function (conn) { 202 | console.log("kicking", conn); 203 | editorAction.kick(conn.id); 204 | }); 205 | }; 206 | 207 | User.prototype.getMyConnection = function () { 208 | var conn = _.find(this.connections.valueOf(), function (c) { 209 | return c.isMe; 210 | }); 211 | return conn && this.connections.get(conn.id); 212 | }; 213 | 214 | function Users () { 215 | Users.super_.apply(this, arguments); 216 | floop.onDATAMSG(function (msg) { 217 | if (msg.data.name === "user_image") { 218 | const user = this.getByConnectionID(msg.user_id); 219 | if (!user) { 220 | return; 221 | } 222 | user.connections.get(msg.user_id).image = msg.data.image; 223 | } 224 | }, this); 225 | } 226 | 227 | flux.inherit(Users, flux.createCollection({ 228 | modelName: "Users", 229 | model: User 230 | })); 231 | 232 | Users.prototype.getByConnectionID = function (connectionId) { 233 | return _.find(this.data.collection, function (user) { 234 | return user.connections.get(connectionId); 235 | }); 236 | }; 237 | 238 | Users.prototype.getConnectionByConnectionID = function (connectionId) { 239 | var user = this.getByConnectionID(connectionId); 240 | return user && user.connections.get(connectionId); 241 | }; 242 | 243 | Users.prototype.broadcast_data_message_for_perm = function (datamsg, perm) { 244 | const ids = []; 245 | this.forEach(function (user) { 246 | user.connections.forEach(function (conn) { 247 | if (!conn.isMe && _.includes(utils.BROWSER_CLIENTS, conn.client) && user.permissions.indexOf(perm) > -1) { 248 | ids.push(conn.id); 249 | } 250 | }); 251 | }); 252 | 253 | if (!ids.length) { 254 | return; 255 | } 256 | 257 | floop.emitDataMessage(datamsg, ids); 258 | }; 259 | 260 | module.exports = { 261 | User, 262 | Users, 263 | }; 264 | -------------------------------------------------------------------------------- /lib/common/webrtc_action.js: -------------------------------------------------------------------------------- 1 | /* @flow weak */ 2 | /*global chrome, self */ 3 | "use strict"; 4 | 5 | var canShareScreen = false, 6 | cantStartVideoChat = false; 7 | 8 | const _ = require("lodash"); 9 | const flux = require("flukes"); 10 | 11 | const Modal = require("../modal"); 12 | const editorAction = require("./editor_action"); 13 | const messageAction = require("./message_action"); 14 | const perms = require("./permission_model"); 15 | const prefs = require("./userPref_model"); 16 | 17 | 18 | const Actions = flux.createActions({ 19 | start_video_chat: function (connId) { 20 | if (cantStartVideoChat) { 21 | return new Error("Can't start video yet."); 22 | } 23 | if (prefs.dnd) { 24 | editorAction.pref("dnd", false); 25 | messageAction.info("Do not disturb disabled."); 26 | } 27 | if (perms.indexOf("patch") === -1) { 28 | messageAction.info("You need edit permissions to video chat."); 29 | return new Error("No permission to video chat."); 30 | } 31 | // Kinda hacky but whatever. 32 | if (prefs.audioOnly) { 33 | this.start_audio_chat(connId); 34 | return new Error("Starting audio chat."); 35 | } 36 | return connId; 37 | }, 38 | stop_video_chat: function (connId) { 39 | // Kinda hacky but whatever. 40 | if (prefs.audioOnly) { 41 | this.stop_audio_chat(connId); 42 | return new Error("Stopping audio chat."); 43 | } 44 | return connId; 45 | }, 46 | start_audio_chat: function (connId) { 47 | return connId; 48 | }, 49 | stop_audio_chat: function (connId) { 50 | return connId; 51 | }, 52 | start_screen: function (connId) { 53 | var errMsg; 54 | if (canShareScreen) { 55 | if (prefs.dnd) { 56 | editorAction.pref("dnd", false); 57 | messageAction.info("Do not disturb disabled."); 58 | } 59 | return connId; 60 | } 61 | 62 | if (!self.chrome || !chrome.app) { 63 | errMsg = "Screen sharing requires Google Chrome and the Floobits screen sharing extension."; 64 | Modal.showWithText(errMsg, "Can't Share Screen"); 65 | messageAction.warn(errMsg); 66 | return new Error(errMsg); 67 | } 68 | 69 | try { 70 | chrome.webstore.install("https://chrome.google.com/webstore/detail/lmojaknpofhmdnbpanagbbeinbjmbodo", function () { 71 | self.location.reload(); 72 | }); 73 | } catch (e) { 74 | self.open("https://chrome.google.com/webstore/detail/lmojaknpofhmdnbpanagbbeinbjmbodo"); 75 | } 76 | return new Error("User may install extension"); 77 | }, 78 | stop_screen: function (connId) { 79 | return connId; 80 | }, 81 | can_share_screen: function (can) { 82 | canShareScreen = can; 83 | return can; 84 | }, 85 | get_constraints: function (type, baseContraints, cb) { 86 | const constraints = _.extend({}, baseContraints); 87 | 88 | const getSource = function (sources, _type) { 89 | const id = prefs["source_" + _type + "_id"]; 90 | 91 | let source = _.find(sources, function (s) { 92 | return s.id === id && s.kind === _type; 93 | }); 94 | 95 | if (source) { 96 | return source.id; 97 | } 98 | 99 | const name = prefs["source_" + _type + "_name"]; 100 | 101 | console.warn("Could not find video source", id); 102 | source = _.find(sources, function (s) { 103 | return s.label === name && s.kind === _type; 104 | }); 105 | 106 | if (source) { 107 | console.warn("Found source with the same name", name); 108 | return source.id; 109 | } 110 | return null; 111 | }; 112 | 113 | if (!MediaStreamTrack || !MediaStreamTrack.getSources || type === "screen") { 114 | return cb(null, constraints); 115 | } 116 | 117 | MediaStreamTrack.getSources(function (sources) { 118 | let getAudioDevice = type === "audio"; 119 | if (type === "video") { 120 | if (_.isBoolean(baseContraints.audio)) { 121 | getAudioDevice = baseContraints.audio; 122 | } else { 123 | getAudioDevice = _.isObject(baseContraints.audio); 124 | } 125 | } 126 | if (getAudioDevice) { 127 | const id = getSource(sources, "audio"); 128 | if (id) { 129 | let c = constraints.audio; 130 | if (_.isBoolean(c)) { 131 | c = constraints.audio = {}; 132 | } 133 | if (!c.optional) { 134 | c.optional = []; 135 | } 136 | c.optional.push({sourceId: id}); 137 | } 138 | } 139 | if (type === "video") { 140 | const id = getSource(sources, type); 141 | if (id) { 142 | constraints[type].optional.push({sourceId: id}); 143 | } 144 | } 145 | console.log("constraints for", type, constraints); 146 | return cb(null, constraints); 147 | }); 148 | }, 149 | closedStreams: function () { 150 | if (cantStartVideoChat) { 151 | return; 152 | } 153 | cantStartVideoChat = true; 154 | _.delay(function () { 155 | cantStartVideoChat = false; 156 | }, 2000); 157 | }, 158 | }); 159 | 160 | module.exports = new Actions(); 161 | -------------------------------------------------------------------------------- /lib/floodmp.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const util = require("util"); 4 | 5 | const dmp = require("dmp"); 6 | 7 | function FlooDMP() { 8 | dmp.call(this); 9 | } 10 | 11 | util.inherits(FlooDMP, dmp); 12 | 13 | FlooDMP.prototype.patch_apply = function (patches, text) { 14 | if (patches.length === 0) { 15 | return [text, []]; 16 | } 17 | 18 | // Deep copy the patches so that no changes are made to originals. 19 | patches = this.patch_deepCopy(patches); 20 | 21 | let nullPadding = this.patch_addPadding(patches); 22 | text = nullPadding + text + nullPadding; 23 | 24 | this.patch_splitMax(patches); 25 | // delta keeps track of the offset between the expected and actual location 26 | // of the previous patch. If there are patches expected at positions 10 and 27 | // 20, but the first patch was found at 12, delta is 2 and the second patch 28 | // has an effective expected position of 22. 29 | let delta = 0; 30 | let results = []; 31 | let positions = []; 32 | for (let x = 0; x < patches.length; x++) { 33 | let position = [3, 0, ""]; 34 | let expected_loc = patches[x].start2 + delta; 35 | let text1 = this.diff_text1(patches[x].diffs); 36 | let start_loc; 37 | let replacement_str; 38 | let end_loc = -1; 39 | if (text1.length > this.Match_MaxBits) { 40 | // patch_splitMax will only provide an oversized pattern in the case of 41 | // a monster delete. 42 | start_loc = this.match_main(text, text1.substring(0, this.Match_MaxBits), 43 | expected_loc); 44 | if (start_loc !== -1) { 45 | end_loc = this.match_main(text, 46 | text1.substring(text1.length - this.Match_MaxBits), 47 | expected_loc + text1.length - this.Match_MaxBits); 48 | if (end_loc === -1 || start_loc >= end_loc) { 49 | // Can't find valid trailing context. Drop this patch. 50 | start_loc = -1; 51 | } 52 | } 53 | } else { 54 | start_loc = this.match_main(text, text1, expected_loc); 55 | } 56 | if (start_loc === -1) { 57 | // No match found. :( 58 | results[x] = false; 59 | // Subtract the delta for this failed patch from subsequent patches. 60 | delta -= patches[x].length2 - patches[x].length1; 61 | } else { 62 | // Found a match. :) 63 | results[x] = true; 64 | delta = start_loc - expected_loc; 65 | let text2; 66 | if (end_loc === -1) { 67 | text2 = text.substring(start_loc, start_loc + text1.length); 68 | } else { 69 | text2 = text.substring(start_loc, end_loc + this.Match_MaxBits); 70 | } 71 | if (text1 === text2) { 72 | // Perfect match, just shove the replacement text in. 73 | replacement_str = this.diff_text2(patches[x].diffs); 74 | text = text.substring(0, start_loc) + 75 | replacement_str + 76 | text.substring(start_loc + text1.length); 77 | position = [start_loc, text1.length, replacement_str]; 78 | } else { 79 | // Imperfect match. Run a diff to get a framework of equivalent 80 | // indices. 81 | let diffs = this.diff_main(text1, text2, false); 82 | if (text1.length > this.Match_MaxBits && 83 | this.diff_levenshtein(diffs) / text1.length > 84 | this.Patch_DeleteThreshold) { 85 | // The end points match, but the content is unacceptably bad. 86 | results[x] = false; 87 | } else { 88 | this.diff_cleanupSemanticLossless(diffs); 89 | let index1 = 0; 90 | let index2; 91 | let delete_len = 0; 92 | let inserted_text = ""; 93 | for (let y = 0; y < patches[x].diffs.length; y++) { 94 | let mod = patches[x].diffs[y]; 95 | if (mod[0] !== dmp.DIFF_EQUAL) { 96 | index2 = this.diff_xIndex(diffs, index1); 97 | } 98 | if (mod[0] === dmp.DIFF_INSERT) { // Insertion 99 | text = text.substring(0, start_loc + index2) + mod[1] + 100 | text.substring(start_loc + index2); 101 | inserted_text += mod[1]; 102 | } else if (mod[0] === dmp.DIFF_DELETE) { // Deletion 103 | let diff_index = this.diff_xIndex(diffs, 104 | index1 + mod[1].length); 105 | // self.diff_xIndex(diffs, index1 + len(data)); 106 | text = text.substring(0, start_loc + index2) + 107 | text.substring(start_loc + diff_index); 108 | delete_len += (diff_index - index2); 109 | } 110 | if (mod[0] !== dmp.DIFF_DELETE) { 111 | index1 += mod[1].length; 112 | } 113 | } 114 | position = [start_loc, delete_len, inserted_text]; 115 | } 116 | } 117 | } 118 | let np_len = nullPadding.length; 119 | if (position[0] < np_len){ 120 | position[1] -= np_len - position[0]; 121 | position[2] = position[2].substring(np_len - position[0]); 122 | position[0] = 0; 123 | }else{ 124 | position[0] -= np_len; 125 | } 126 | 127 | let too_close = (position[0] + position[2].length) - (text.length - 2 * np_len); 128 | if (too_close > 0) { 129 | position[2] = position[2].substring(0, position[2].length - too_close); 130 | } 131 | positions.push(position); 132 | } 133 | // Strip the padding off. 134 | text = text.substring(nullPadding.length, text.length - nullPadding.length); 135 | return [text, results, positions]; 136 | }; 137 | 138 | exports.FlooDMP = FlooDMP; 139 | -------------------------------------------------------------------------------- /lib/floourl.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var util = require("util"); 4 | 5 | function FlooUrl(owner, workspace, host, port) { 6 | this.owner = owner; 7 | this.workspace = workspace; 8 | this.host = host; 9 | this.port = port; 10 | } 11 | 12 | FlooUrl.prototype.toAPIString = function () { 13 | return util.format("https://%s/api/room/%s/%s", this.host, this.owner, this.workspace); 14 | }; 15 | 16 | FlooUrl.prototype.toString = function () { 17 | let port = parseInt(this.port, 10); 18 | return util.format("https://%s%s/%s/%s", this.host, port === 3448 ? "" : ":" + this.port, this.owner, this.workspace); 19 | }; 20 | 21 | exports.FlooUrl = FlooUrl; 22 | -------------------------------------------------------------------------------- /lib/follow_view.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | "use babel"; 3 | 4 | const $$ = require("atom-space-pen-views").$$; 5 | 6 | const editorAction = require("./common/editor_action"); 7 | const prefs = require("./common/userPref_model"); 8 | const SelectListView = require("./select_list_view"); 9 | const utils = require("./common/utils"); 10 | const EVERYONE = "all changes"; 11 | 12 | var FollowView = function (me, users) { 13 | this.items_ = {}; 14 | this.users = users; 15 | this.me = me; 16 | SelectListView.call(this); 17 | }; 18 | 19 | utils.inherits(FollowView, SelectListView); 20 | 21 | FollowView.prototype.initialize = function () { 22 | SelectListView.prototype.initialize.apply(this, arguments); 23 | this.addClass("modal overlay from-top"); 24 | let myUsername = this.me.username; 25 | let items = this.users.map(function (u) { 26 | let username = u.username; 27 | this.items_[username] = prefs.followUsers.indexOf(username) !== -1; 28 | return u.username; 29 | }, this).filter(function (username) { 30 | return username !== myUsername; 31 | }); 32 | items.unshift(EVERYONE); 33 | this.items_[EVERYONE] = prefs.following; 34 | this.setItems(items); 35 | }; 36 | 37 | FollowView.prototype.viewForItem = function (name) { 38 | var items = this.items_; 39 | return $$(function () { 40 | var that = this; 41 | this.li({"class": ""}, function () { 42 | that.div({"class": "primary-line icon icon-" + (items[name] ? "mute" : "unmute")}, (items[name] ? "Stop following " : "Follow ") + name); 43 | }); 44 | }); 45 | }; 46 | 47 | FollowView.prototype.confirmed = function (name) { 48 | SelectListView.prototype.confirmed.call(this, name); 49 | if (name === EVERYONE) { 50 | name = ""; 51 | } 52 | editorAction.follow(name); 53 | }; 54 | 55 | module.exports = FollowView; 56 | -------------------------------------------------------------------------------- /lib/media_sources.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const $$ = require("atom-space-pen-views").$$; 4 | const _ = require("lodash"); 5 | 6 | const utils = require("./common/utils"); 7 | const SelectListView = require("./select_list_view"); 8 | 9 | function MediaSources (sources, cb) { 10 | SelectListView.call(this, cb); 11 | this.sources = sources; 12 | const items = _.map(this.sources, function (source) { 13 | return source.label; 14 | }); 15 | this.setItems(items); 16 | } 17 | 18 | utils.inherits(MediaSources, SelectListView); 19 | 20 | MediaSources.prototype.viewForItem = function (label) { 21 | return $$(function () { 22 | this.li(label); 23 | }); 24 | }; 25 | 26 | MediaSources.prototype.confirmed = function (label) { 27 | const source = _.filter(this.sources, {label: label})[0]; 28 | SelectListView.prototype.confirmed.call(this, source); 29 | }; 30 | 31 | module.exports = MediaSources; 32 | -------------------------------------------------------------------------------- /lib/modal.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const atomUtils = require("./atom_utils"); 3 | 4 | module.exports = { 5 | showView: function (view) { 6 | atomUtils.addModalPanel("handle-request-perm", view); 7 | }, 8 | showWithText: function (title, msg) { 9 | atom.confirm({ 10 | message: "Floobits: " + title, 11 | detailedMessage: msg, 12 | buttons: ["OK"] 13 | }); 14 | }, 15 | }; 16 | -------------------------------------------------------------------------------- /lib/react_wrapper.js: -------------------------------------------------------------------------------- 1 | /* global HTMLElement */ 2 | "use strict"; 3 | "use babel"; 4 | 5 | const _ = require("lodash"); 6 | const React = require("react-atom-fork"); 7 | 8 | const ELEMENTS = {}; 9 | 10 | const create_wrapper = function (name) { 11 | var Proto = Object.create(HTMLElement.prototype); 12 | 13 | Proto.init = function (component, styles) { 14 | this.reactNode = null; 15 | this.component = component; 16 | _.each(styles, (v, k) => { 17 | this.style[k] = v; 18 | }); 19 | return this; 20 | }; 21 | 22 | Proto.createdCallback = function () { 23 | }; 24 | 25 | Proto.attachedCallback = function () { 26 | this.reactNode = React.renderComponent(this.component, this); 27 | }; 28 | 29 | // Proto.attributeChangedCallback = function (attrName, oldVal, newVal) { 30 | // return; 31 | // }; 32 | 33 | Proto.detachedCallback = function () { 34 | return; 35 | }; 36 | 37 | Proto.onDestroy = function (pane) { 38 | this.pane = pane; 39 | }; 40 | 41 | Proto.destroy = function () { 42 | if (!this.pane) { 43 | return; 44 | } 45 | this.pane.destroy(); 46 | this.pane = null; 47 | }; 48 | 49 | return document.registerElement(name, {prototype: Proto}); 50 | }; 51 | 52 | // Warning! If you choose the same node name as another plugin, this will throw! 53 | const create_node_unsafe = function (name, component, style) { 54 | let node; 55 | if (name in ELEMENTS) { 56 | node = new ELEMENTS[name](); 57 | } else { 58 | let Element = create_wrapper(name); 59 | ELEMENTS[name] = Element; 60 | node = new Element(); 61 | } 62 | node.init(component, style); 63 | return node; 64 | }; 65 | 66 | module.exports = { 67 | create_node: function (name, component, style) { 68 | return create_node_unsafe("floobits-" + name, component, style); 69 | }, 70 | create_node_unsafe: create_node_unsafe, 71 | create_wrapper: create_wrapper, 72 | }; 73 | -------------------------------------------------------------------------------- /lib/recentworkspaceview.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const $$ = require("atom-space-pen-views").$$; 4 | const _ = require("lodash"); 5 | 6 | const utils = require("./common/utils"); 7 | const PersistentJson = require("./common/persistentjson"); 8 | const SelectListView = require("./select_list_view"); 9 | 10 | function RecentWorkspaceView () { 11 | SelectListView.call(this); 12 | } 13 | 14 | utils.inherits(RecentWorkspaceView, SelectListView); 15 | 16 | RecentWorkspaceView.prototype.initialize = function () { 17 | SelectListView.prototype.initialize.apply(this, arguments); 18 | 19 | this.addClass("overlay from-top"); 20 | const pj = new PersistentJson().load(); 21 | const recent_workspaces = _.pluck(pj.recent_workspaces, "url"); 22 | this.items_ = {}; 23 | const items = recent_workspaces.map((workspace) => { 24 | const stuff = utils.parse_url(workspace); 25 | let p; 26 | try{ 27 | p = pj.workspaces[stuff.owner][stuff.workspace].path; 28 | } catch(e) { 29 | p = "?"; 30 | } 31 | this.items_[p] = workspace; 32 | return p; 33 | }); 34 | this.setItems(items); 35 | }; 36 | 37 | RecentWorkspaceView.prototype.viewForItem = function (path) { 38 | const items = this.items_; 39 | return $$(function () { 40 | const that = this; 41 | this.li({"class": "two-lines"}, function () { 42 | that.div({"class": "primary-line file icon icon-file-text"}, path); 43 | that.div({"class": "secondary-line path no-icon"}, items[path]); 44 | }); 45 | }); 46 | }; 47 | 48 | RecentWorkspaceView.prototype.confirmed = function (path) { 49 | // if (d && d.getRealPathSync() === path) { 50 | // return require("./floobits").join_workspace(this.items_[path]); 51 | // } 52 | 53 | atom.config.set("floobits.atoms-api-sucks-url", this.items_[path]); 54 | atom.config.set("floobits.atoms-api-sucks-path", path); 55 | atom.open({pathsToOpen: [path], newWindow: true}); 56 | }; 57 | 58 | exports.RecentWorkspaceView = RecentWorkspaceView; 59 | -------------------------------------------------------------------------------- /lib/select_directory_to_share.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const $$ = require("atom-space-pen-views").$$; 4 | 5 | const utils = require("./common/utils"); 6 | const SelectListView = require("./select_list_view"); 7 | 8 | function DirectorySelectorView (directories, cb) { 9 | SelectListView.call(this, cb); 10 | this.setItems(directories); 11 | } 12 | 13 | utils.inherits(DirectorySelectorView, SelectListView); 14 | 15 | DirectorySelectorView.prototype.viewForItem = function (directory) { 16 | return $$(function () { 17 | this.li(directory.getBaseName()); 18 | }); 19 | }; 20 | 21 | module.exports = DirectorySelectorView; 22 | -------------------------------------------------------------------------------- /lib/select_list_view.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | "use babel"; 3 | 4 | const AtomSelectListView = require("atom-space-pen-views").SelectListView; 5 | 6 | const utils = require("./common/utils"); 7 | 8 | 9 | function SelectListView (cb) { 10 | this.cb = cb; 11 | AtomSelectListView.call(this); 12 | // this nextTick is needed for some reason... :( 13 | process.nextTick(function () { 14 | this.panel = atom.workspace.addModalPanel({item: this}); 15 | this.storeFocusedElement(); 16 | this.focusFilterEditor(); 17 | }.bind(this)); 18 | } 19 | 20 | utils.inherits(SelectListView, AtomSelectListView); 21 | 22 | SelectListView.prototype.initialize = function () { 23 | AtomSelectListView.prototype.initialize.apply(this, arguments); 24 | this.addClass("overlay from-top"); 25 | }; 26 | 27 | SelectListView.prototype.cancel = function () { 28 | AtomSelectListView.prototype.cancel.apply(this); 29 | this.panel && this.panel.destroy(); 30 | }; 31 | 32 | SelectListView.prototype.confirmed = function (arg) { 33 | console.debug(`${arg} selected`); 34 | this.cb && this.cb(null, arg); 35 | this.cancel(); 36 | }; 37 | 38 | module.exports = SelectListView; 39 | -------------------------------------------------------------------------------- /lib/terminal_manager.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const Socket = require("./common/floop"); 3 | const _ = require("lodash"); 4 | // const editorAction = require("./common/editor_action"); 5 | const CompositeDisposable = require('event-kit').CompositeDisposable; 6 | const prefs = require("./common/userPref_model"); 7 | 8 | function TerminalManager () { 9 | // floobits term id to this.Term3 TermialView 10 | this.terms = {}; 11 | 12 | this.term3_name_to_ids = {}; 13 | 14 | this.term3 = null; 15 | this.username = null; 16 | this.floobits_terms = null; 17 | } 18 | 19 | TerminalManager.prototype.send_create_term = function (t) { 20 | if (!t.getForked()) { 21 | console.log("not sending create event for remotely driven terminal"); 22 | return; 23 | } 24 | const d = t.getDimensions(); 25 | const name = this.nameForTerm(t); 26 | this.term3_name_to_ids[name] = t.id; 27 | Socket.send_create_term({term_name: name, size: [d.cols, d.rows]}); 28 | }; 29 | 30 | TerminalManager.prototype.nameForTerm = function (term) { 31 | return `atom-${this.username.replace('.','-')}-${term.id}-${Math.floor(Math.random() * 10000)}`; 32 | }; 33 | 34 | TerminalManager.prototype.on_floobits = function(username, floobits_terms) { 35 | this.username = username; 36 | this.floobits_terms = floobits_terms; 37 | 38 | if (!this.term3) { 39 | return; 40 | } 41 | this.start_(); 42 | }; 43 | 44 | TerminalManager.prototype.on_term3_service = function(term3) { 45 | this.term3 = term3; 46 | 47 | if (!this.floobits_terms || !this.username) { 48 | return; 49 | } 50 | this.start_(); 51 | }; 52 | 53 | TerminalManager.prototype.start_ = function () { 54 | const that = this; 55 | 56 | this.subs = new CompositeDisposable(); 57 | 58 | const disposable = that.term3.onTerm(that.send_create_term.bind(that)); 59 | 60 | this.subs.add(disposable); 61 | 62 | that.term3.getTerminals().forEach(that.send_create_term.bind(that)); 63 | 64 | _.each(that.floobits_terms, function (t, termID) { 65 | const term = that.term3.newTerm(false, t.size[1], t.size[0], t.owner); 66 | that.terms[termID] = term; 67 | 68 | const subs = new CompositeDisposable(); 69 | 70 | subs.add(term.onSTDIN(function (d) { 71 | Socket.send_term_stdin({'data': new Buffer(d).toString("base64"), id: termID}); 72 | })); 73 | 74 | subs.add(term.onExit(function () { 75 | if (that.subs) { 76 | that.subs.remove(subs); 77 | } 78 | subs.dispose(); 79 | delete that.terms[termID]; 80 | })); 81 | 82 | that.subs.add(subs); 83 | }); 84 | 85 | Socket.onCREATE_TERM(function (req) { 86 | const name = req.term_name; 87 | const termID = req.id; 88 | 89 | const term3_ID = that.term3_name_to_ids[name]; 90 | const term = _.find(that.term3.getTerminals(), function (t) { 91 | return t.id === term3_ID; 92 | }); 93 | 94 | if (term) { 95 | that.terms[termID] = term; 96 | } else { 97 | that.terms[termID] = that.term3.newTerm(false); 98 | that.subs.add(that.terms[termID].onSTDIN(function (d) { 99 | Socket.send_term_stdin({'data': new Buffer(d).toString("base64"), id: termID}); 100 | })); 101 | return; 102 | } 103 | 104 | const d = term.getDimensions(); 105 | Socket.send_update_term({id: termID, size: [d.cols, d.rows]}); 106 | 107 | const subs = new CompositeDisposable(); 108 | 109 | subs.add(term.onSTDOUT(function (d) { 110 | Socket.send_term_stdout({data: new Buffer(d).toString("base64"), id: termID}); 111 | })); 112 | 113 | subs.add(term.onResize(function (size) { 114 | Socket.send_update_term({id: termID, size: [size.cols, size.rows]}); 115 | })); 116 | 117 | subs.add(term.onExit(function () { 118 | Socket.send_delete_term({id: termID}); 119 | if (that.subs) { 120 | that.subs.remove(subs); 121 | } 122 | subs.dispose(); 123 | delete that.terms[termID]; 124 | })); 125 | 126 | that.subs.add(subs); 127 | }); 128 | 129 | Socket.onTERM_STDIN(function (data) { 130 | const term = that.terms[data.id]; 131 | if (!term) { 132 | return; 133 | } 134 | 135 | if (prefs.isFollowing(data.username)) { 136 | term.focusPane(); 137 | } 138 | 139 | let term_data = data.data; 140 | if (!term_data.length) { 141 | return; 142 | } 143 | 144 | term_data = new Buffer(term_data, "base64").toString("utf-8"); 145 | 146 | // TODO: allow terminals to be unsafe 147 | if (true) { 148 | /*eslint-disable no-control-regex */ 149 | term_data = term_data.replace(/[\x04\x07\n\r]/g, ""); 150 | /*eslint-enable no-control-regex */ 151 | } 152 | 153 | try { 154 | term.input(term_data); 155 | } catch (e) { 156 | console.warn(e); 157 | } 158 | }); 159 | 160 | Socket.onDELETE_TERM(function (data) { 161 | const term = that.terms[data.id]; 162 | 163 | if (!term) { 164 | return; 165 | } 166 | 167 | term.input("\r\n *** TERMINAL CLOSED *** \r\n"); 168 | }); 169 | 170 | Socket.onTERM_STDOUT(function (data) { 171 | const term = that.terms[data.id]; 172 | 173 | if (!term) { 174 | return; 175 | } 176 | 177 | try { 178 | term.input(new Buffer(data.data, "base64").toString("utf-8")); 179 | } catch (e) { 180 | console.warn(e); 181 | } 182 | }); 183 | 184 | Socket.onUPDATE_TERM(function (data) { 185 | const term = that.terms[data.id]; 186 | 187 | if (!term) { 188 | return; 189 | } 190 | 191 | console.log("update term", data); 192 | if (data.size) { 193 | term.resize(data.size[0], data.size[1]); 194 | } 195 | }); 196 | 197 | Socket.onSYNC(function (terms) { 198 | console.log("Attempting to sync..."); 199 | _.each(terms, function (data, id) { 200 | const term = that.terms[id]; 201 | 202 | if (!term) { 203 | return; 204 | } 205 | 206 | term.resize(data.cols, data.rows); 207 | }); 208 | }); 209 | }; 210 | 211 | TerminalManager.prototype.stop = function(term3_disposed) { 212 | if (this.subs) { 213 | this.subs.dispose(); 214 | this.subs = null; 215 | } 216 | // we may not get another term3... 217 | if (term3_disposed) { 218 | this.term3 = null; 219 | } 220 | this.floobits_terms = null; 221 | this.username = null; 222 | }; 223 | 224 | module.exports = new TerminalManager(); 225 | -------------------------------------------------------------------------------- /lib/transport.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const util = require("util"); 4 | const tls = require("tls"); 5 | const CommonTransport = require("./common/transport"); 6 | 7 | function Transport(host, port) { 8 | CommonTransport.call(this); 9 | this.conn_ = null; 10 | this.conn_buf = ""; 11 | this.host = host; 12 | this.port = port; 13 | } 14 | 15 | util.inherits(Transport, CommonTransport); 16 | 17 | Transport.prototype.data_handler_ = function (d) { 18 | var msg, newline_index; 19 | 20 | this.conn_buf += d; 21 | 22 | newline_index = this.conn_buf.indexOf("\n"); 23 | while (newline_index !== -1) { 24 | msg = this.conn_buf.slice(0, newline_index); 25 | this.conn_buf = this.conn_buf.slice(newline_index + 1); 26 | newline_index = this.conn_buf.indexOf("\n"); 27 | msg = JSON.parse(msg); 28 | 29 | if (!msg.name) { 30 | continue; 31 | } 32 | 33 | this.emit("messaged", msg.name, msg); 34 | } 35 | }; 36 | 37 | Transport.prototype.reconnect_ = function () { 38 | try { 39 | this.conn_.off(); 40 | this.conn_.close(); 41 | } catch (ignore) { 42 | // ignore 43 | } 44 | 45 | CommonTransport.prototype.reconnect_.call(this); 46 | }; 47 | 48 | Transport.prototype.connect = function () { 49 | var that = this; 50 | 51 | that.conn_buf = ""; 52 | that.conn_ = tls.connect({ 53 | host: this.host, 54 | port: this.port, 55 | }, function () { 56 | that.emit("connected"); 57 | }); 58 | that.conn_.setEncoding("utf8"); 59 | that.onEnd_ = function () { 60 | console.warn("socket is gone"); 61 | that.emit("disconnected"); 62 | that.reconnect_(); 63 | }; 64 | that.conn_.once("end", that.onEnd_); 65 | that.onData_ = function (data) { 66 | that.data_handler_(data); 67 | }; 68 | that.conn_.on("data", that.onData_); 69 | that.onError_ = function (err) { 70 | console.error("Connection error:", err); 71 | that.emit("disconnected", err); 72 | that.reconnect_(); 73 | }; 74 | that.conn_.once("error", that.onError_); 75 | }; 76 | 77 | Transport.prototype.disconnect = function (msg) { 78 | if (!this.conn_) { 79 | return msg; 80 | } 81 | 82 | CommonTransport.prototype.disconnect.call(this); 83 | 84 | try { 85 | this.conn_.removeListener("end", this.onEnd_); 86 | this.conn_.removeListener("data", this.onData_); 87 | this.conn_.removeListener("error", this.onError_); 88 | this.onEnd_ = null; 89 | this.onData_ = null; 90 | this.onError_ = null; 91 | } catch (e) { 92 | //ignore 93 | } 94 | 95 | try { 96 | this.conn_.end(); 97 | this.conn_.destroy(); 98 | } catch (e) { 99 | //ignore 100 | } 101 | 102 | this.conn_buf = ""; 103 | this.conn_ = null; 104 | return msg; 105 | }; 106 | 107 | Transport.prototype.write = function (name, msg, cb, context) { 108 | var str; 109 | 110 | if (!this.conn_) { 111 | return cb && cb("not connected"); 112 | } 113 | 114 | if (name) { 115 | msg.name = name; 116 | } 117 | 118 | str = util.format("%s\n", JSON.stringify(msg)); 119 | 120 | if (str.length > 1000) { 121 | console.info("writing to conn:", str.slice(0, 1000) + '...'); 122 | } else { 123 | console.info("writing to conn:", str); 124 | } 125 | 126 | 127 | try { 128 | this.conn_.write(str, cb && cb.bind(context)); 129 | } catch (e) { 130 | console.error("error writing to client:", e, "disconnecting"); 131 | } 132 | }; 133 | 134 | module.exports = Transport; 135 | -------------------------------------------------------------------------------- /menus/floobits.cson: -------------------------------------------------------------------------------- 1 | # See https://atom.io/docs/latest/creating-a-package#menus for more details 2 | 'context-menu': 3 | 'atom-workspace': [ 4 | # { 'label': 'Enable Floobits', 'command': 'Floobits: Join Workspace' } 5 | # { 'label': 'Enable Floobits', 'command': 'Floobits: Join Recent Workspace' } 6 | ] 7 | 8 | 'menu': [ 9 | { 10 | 'label': 'Packages' 11 | 'submenu': [ 12 | 'label': 'Floobits' 13 | 'submenu': [ 14 | { 'label': 'Settings', 'command': 'Floobits: Settings' } 15 | { 'label': 'Join Workspace', 'command': 'Floobits: Join Workspace' } 16 | # { 'label': 'Join Recent Workspace', 'command': 'Floobits: Join Recent Workspace' } 17 | { 'label': 'Leave Workspace', 'command': 'Floobits: Leave Workspace' } 18 | { 'label': 'Refresh Workspace', 'command': 'Floobits: Refresh Workspace' } 19 | { 'label': 'Create Workspace', 'command': 'Floobits: Create Workspace' } 20 | { 'label': 'Add Current File', 'command': 'Floobits: Add Current File' } 21 | { 'label': 'Toggle User List', 'command': 'Floobits: Toggle User List Panel' } 22 | { 'label': 'Request Code Review', 'command': 'Floobits: Request Code Review' } 23 | ] 24 | ] 25 | } 26 | ] 27 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "floobits", 3 | "main": "./lib/floobits", 4 | "version": "0.32.12", 5 | "description": "The Floobits pair programming plugin for Atom. This plugin adds support for real-time collaborative editing, video chatting, and sharing terminals via the Term3 plugin. A Floobits account is required, but the plugin will guide you.", 6 | "maintainers": [ 7 | { 8 | "name": "Geoff Greer", 9 | "web": "http://geoff.greer.fm/" 10 | }, 11 | { 12 | "name": "Matt Kaniaris", 13 | "web": "https://github.com/kans" 14 | } 15 | ], 16 | "contributors": [ 17 | { 18 | "name": "Geoff Greer", 19 | "web": "http://geoff.greer.fm/" 20 | }, 21 | { 22 | "name": "Matt Kaniaris", 23 | "web": "https://github.com/kans" 24 | }, 25 | { 26 | "name": "Bjorn Tipling", 27 | "web": "https://github.com/btipling" 28 | } 29 | ], 30 | "repository": "https://github.com/Floobits/floobits-atom", 31 | "license": "Apache-2.0", 32 | "engines": { 33 | "atom": ">=1.0" 34 | }, 35 | "dependencies": { 36 | "async": "1.x", 37 | "atom-space-pen-views": "2.2.x", 38 | "dmp": "1.x", 39 | "flukes": ">=0.1.4", 40 | "fs-plus": "2.x", 41 | "lodash": "4.x", 42 | "minimatch": "3.x", 43 | "mkdirp": "0.5.x", 44 | "open": "0.x", 45 | "react-atom-fork": "0.11.x", 46 | "react-tools": "0.11.x", 47 | "reactionary-atom-fork": "1.x", 48 | "request": "2.x", 49 | "event-kit": "1.5.x", 50 | "chokidar": "1.x" 51 | }, 52 | "consumedServices": { 53 | "term3-service": { 54 | "versions": { 55 | "0.1.3": "term3-service" 56 | } 57 | } 58 | }, 59 | "devDependencies": { 60 | "eslint": "3.x", 61 | "eslint-config-floobits": "*", 62 | "eslint-plugin-react": "5.x" 63 | }, 64 | "scripts": { 65 | "build": "npm run install", 66 | "lint": "eslint lib templates", 67 | "watch": "jsx --watch --harmony templates lib/build", 68 | "install": "jsx --harmony templates lib/build" 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /resources/anonymous.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Floobits/floobits-atom/64c044009c1591d686958b5a6af1362528aa038e/resources/anonymous.png -------------------------------------------------------------------------------- /resources/ascii_town.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Floobits/floobits-atom/64c044009c1591d686958b5a6af1362528aa038e/resources/ascii_town.png -------------------------------------------------------------------------------- /resources/dark_noise.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Floobits/floobits-atom/64c044009c1591d686958b5a6af1362528aa038e/resources/dark_noise.png -------------------------------------------------------------------------------- /resources/disc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Floobits/floobits-atom/64c044009c1591d686958b5a6af1362528aa038e/resources/disc.png -------------------------------------------------------------------------------- /resources/edit_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Floobits/floobits-atom/64c044009c1591d686958b5a6af1362528aa038e/resources/edit_icon.png -------------------------------------------------------------------------------- /resources/email_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Floobits/floobits-atom/64c044009c1591d686958b5a6af1362528aa038e/resources/email_icon.png -------------------------------------------------------------------------------- /resources/fl_logo146_30.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Floobits/floobits-atom/64c044009c1591d686958b5a6af1362528aa038e/resources/fl_logo146_30.png -------------------------------------------------------------------------------- /resources/fonts/glyphicons-halflings/glyphicons-halflings-regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Floobits/floobits-atom/64c044009c1591d686958b5a6af1362528aa038e/resources/fonts/glyphicons-halflings/glyphicons-halflings-regular.eot -------------------------------------------------------------------------------- /resources/fonts/glyphicons-halflings/glyphicons-halflings-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Floobits/floobits-atom/64c044009c1591d686958b5a6af1362528aa038e/resources/fonts/glyphicons-halflings/glyphicons-halflings-regular.ttf -------------------------------------------------------------------------------- /resources/fonts/glyphicons-halflings/glyphicons-halflings-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Floobits/floobits-atom/64c044009c1591d686958b5a6af1362528aa038e/resources/fonts/glyphicons-halflings/glyphicons-halflings-regular.woff -------------------------------------------------------------------------------- /resources/fonts/montserrat/Montserrat-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Floobits/floobits-atom/64c044009c1591d686958b5a6af1362528aa038e/resources/fonts/montserrat/Montserrat-Bold.ttf -------------------------------------------------------------------------------- /resources/fonts/montserrat/Montserrat-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Floobits/floobits-atom/64c044009c1591d686958b5a6af1362528aa038e/resources/fonts/montserrat/Montserrat-Regular.ttf -------------------------------------------------------------------------------- /resources/fonts/montserrat/OFL.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2011-2012, Julieta Ulanovsky (julieta.ulanovsky@gmail.com), with Reserved Font Names 'Montserrat' 2 | This Font Software is licensed under the SIL Open Font License, Version 1.1. 3 | This license is copied below, and is also available with a FAQ at: 4 | http://scripts.sil.org/OFL 5 | 6 | 7 | ----------------------------------------------------------- 8 | SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 9 | ----------------------------------------------------------- 10 | 11 | PREAMBLE 12 | The goals of the Open Font License (OFL) are to stimulate worldwide 13 | development of collaborative font projects, to support the font creation 14 | efforts of academic and linguistic communities, and to provide a free and 15 | open framework in which fonts may be shared and improved in partnership 16 | with others. 17 | 18 | The OFL allows the licensed fonts to be used, studied, modified and 19 | redistributed freely as long as they are not sold by themselves. The 20 | fonts, including any derivative works, can be bundled, embedded, 21 | redistributed and/or sold with any software provided that any reserved 22 | names are not used by derivative works. The fonts and derivatives, 23 | however, cannot be released under any other type of license. The 24 | requirement for fonts to remain under this license does not apply 25 | to any document created using the fonts or their derivatives. 26 | 27 | DEFINITIONS 28 | "Font Software" refers to the set of files released by the Copyright 29 | Holder(s) under this license and clearly marked as such. This may 30 | include source files, build scripts and documentation. 31 | 32 | "Reserved Font Name" refers to any names specified as such after the 33 | copyright statement(s). 34 | 35 | "Original Version" refers to the collection of Font Software components as 36 | distributed by the Copyright Holder(s). 37 | 38 | "Modified Version" refers to any derivative made by adding to, deleting, 39 | or substituting -- in part or in whole -- any of the components of the 40 | Original Version, by changing formats or by porting the Font Software to a 41 | new environment. 42 | 43 | "Author" refers to any designer, engineer, programmer, technical 44 | writer or other person who contributed to the Font Software. 45 | 46 | PERMISSION & CONDITIONS 47 | Permission is hereby granted, free of charge, to any person obtaining 48 | a copy of the Font Software, to use, study, copy, merge, embed, modify, 49 | redistribute, and sell modified and unmodified copies of the Font 50 | Software, subject to the following conditions: 51 | 52 | 1) Neither the Font Software nor any of its individual components, 53 | in Original or Modified Versions, may be sold by itself. 54 | 55 | 2) Original or Modified Versions of the Font Software may be bundled, 56 | redistributed and/or sold with any software, provided that each copy 57 | contains the above copyright notice and this license. These can be 58 | included either as stand-alone text files, human-readable headers or 59 | in the appropriate machine-readable metadata fields within text or 60 | binary files as long as those fields can be easily viewed by the user. 61 | 62 | 3) No Modified Version of the Font Software may use the Reserved Font 63 | Name(s) unless explicit written permission is granted by the corresponding 64 | Copyright Holder. This restriction only applies to the primary font name as 65 | presented to the users. 66 | 67 | 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font 68 | Software shall not be used to promote, endorse or advertise any 69 | Modified Version, except to acknowledge the contribution(s) of the 70 | Copyright Holder(s) and the Author(s) or with their explicit written 71 | permission. 72 | 73 | 5) The Font Software, modified or unmodified, in part or in whole, 74 | must be distributed entirely under this license, and must not be 75 | distributed under any other license. The requirement for fonts to 76 | remain under this license does not apply to any document created 77 | using the Font Software. 78 | 79 | TERMINATION 80 | This license becomes null and void if any of the above conditions are 81 | not met. 82 | 83 | DISCLAIMER 84 | THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 85 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF 86 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT 87 | OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE 88 | COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 89 | INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL 90 | DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 91 | FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM 92 | OTHER DEALINGS IN THE FONT SOFTWARE. 93 | -------------------------------------------------------------------------------- /resources/fonts/proxima_nova/ProximaNova-Bold.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Floobits/floobits-atom/64c044009c1591d686958b5a6af1362528aa038e/resources/fonts/proxima_nova/ProximaNova-Bold.otf -------------------------------------------------------------------------------- /resources/fonts/proxima_nova/ProximaNova-Extrabold.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Floobits/floobits-atom/64c044009c1591d686958b5a6af1362528aa038e/resources/fonts/proxima_nova/ProximaNova-Extrabold.otf -------------------------------------------------------------------------------- /resources/fonts/proxima_nova/ProximaNova-Light_0.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Floobits/floobits-atom/64c044009c1591d686958b5a6af1362528aa038e/resources/fonts/proxima_nova/ProximaNova-Light_0.otf -------------------------------------------------------------------------------- /resources/fonts/proxima_nova/ProximaNova-Regular.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Floobits/floobits-atom/64c044009c1591d686958b5a6af1362528aa038e/resources/fonts/proxima_nova/ProximaNova-Regular.otf -------------------------------------------------------------------------------- /resources/fonts/proxima_nova/ProximaNova-Semibold.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Floobits/floobits-atom/64c044009c1591d686958b5a6af1362528aa038e/resources/fonts/proxima_nova/ProximaNova-Semibold.otf -------------------------------------------------------------------------------- /resources/fonts/proxima_nova/ProximaNovaCond-Light.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Floobits/floobits-atom/64c044009c1591d686958b5a6af1362528aa038e/resources/fonts/proxima_nova/ProximaNovaCond-Light.otf -------------------------------------------------------------------------------- /resources/fonts/proxima_nova/ProximaNovaCond-RegularIt.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Floobits/floobits-atom/64c044009c1591d686958b5a6af1362528aa038e/resources/fonts/proxima_nova/ProximaNovaCond-RegularIt.otf -------------------------------------------------------------------------------- /resources/fonts/proxima_nova/ProximaNovaCond-Semibold.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Floobits/floobits-atom/64c044009c1591d686958b5a6af1362528aa038e/resources/fonts/proxima_nova/ProximaNovaCond-Semibold.otf -------------------------------------------------------------------------------- /resources/fonts/robotoslab_bold_macroman/RobotoSlab-Bold-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Floobits/floobits-atom/64c044009c1591d686958b5a6af1362528aa038e/resources/fonts/robotoslab_bold_macroman/RobotoSlab-Bold-webfont.eot -------------------------------------------------------------------------------- /resources/fonts/robotoslab_bold_macroman/RobotoSlab-Bold-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Floobits/floobits-atom/64c044009c1591d686958b5a6af1362528aa038e/resources/fonts/robotoslab_bold_macroman/RobotoSlab-Bold-webfont.ttf -------------------------------------------------------------------------------- /resources/fonts/robotoslab_bold_macroman/RobotoSlab-Bold-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Floobits/floobits-atom/64c044009c1591d686958b5a6af1362528aa038e/resources/fonts/robotoslab_bold_macroman/RobotoSlab-Bold-webfont.woff -------------------------------------------------------------------------------- /resources/fonts/robotoslab_bold_macroman/stylesheet.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: 'roboto_slabbold'; 3 | src: url('RobotoSlab-Bold-webfont.eot'); 4 | src: url('RobotoSlab-Bold-webfont.eot?#iefix') format('embedded-opentype'), 5 | url('RobotoSlab-Bold-webfont.woff') format('woff'), 6 | url('RobotoSlab-Bold-webfont.ttf') format('truetype'), 7 | url('RobotoSlab-Bold-webfont.svg#roboto_slabbold') format('svg'); 8 | font-weight: normal; 9 | font-style: normal; 10 | 11 | } 12 | 13 | -------------------------------------------------------------------------------- /resources/fonts/robotoslab_light_macroman/RobotoSlab-Light-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Floobits/floobits-atom/64c044009c1591d686958b5a6af1362528aa038e/resources/fonts/robotoslab_light_macroman/RobotoSlab-Light-webfont.eot -------------------------------------------------------------------------------- /resources/fonts/robotoslab_light_macroman/RobotoSlab-Light-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Floobits/floobits-atom/64c044009c1591d686958b5a6af1362528aa038e/resources/fonts/robotoslab_light_macroman/RobotoSlab-Light-webfont.ttf -------------------------------------------------------------------------------- /resources/fonts/robotoslab_light_macroman/RobotoSlab-Light-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Floobits/floobits-atom/64c044009c1591d686958b5a6af1362528aa038e/resources/fonts/robotoslab_light_macroman/RobotoSlab-Light-webfont.woff -------------------------------------------------------------------------------- /resources/fonts/robotoslab_regular_macroman/RobotoSlab-Regular-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Floobits/floobits-atom/64c044009c1591d686958b5a6af1362528aa038e/resources/fonts/robotoslab_regular_macroman/RobotoSlab-Regular-webfont.eot -------------------------------------------------------------------------------- /resources/fonts/robotoslab_regular_macroman/RobotoSlab-Regular-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Floobits/floobits-atom/64c044009c1591d686958b5a6af1362528aa038e/resources/fonts/robotoslab_regular_macroman/RobotoSlab-Regular-webfont.ttf -------------------------------------------------------------------------------- /resources/fonts/robotoslab_regular_macroman/RobotoSlab-Regular-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Floobits/floobits-atom/64c044009c1591d686958b5a6af1362528aa038e/resources/fonts/robotoslab_regular_macroman/RobotoSlab-Regular-webfont.woff -------------------------------------------------------------------------------- /resources/fonts/robotoslab_thin_macroman/RobotoSlab-Thin-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Floobits/floobits-atom/64c044009c1591d686958b5a6af1362528aa038e/resources/fonts/robotoslab_thin_macroman/RobotoSlab-Thin-webfont.eot -------------------------------------------------------------------------------- /resources/fonts/robotoslab_thin_macroman/RobotoSlab-Thin-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Floobits/floobits-atom/64c044009c1591d686958b5a6af1362528aa038e/resources/fonts/robotoslab_thin_macroman/RobotoSlab-Thin-webfont.ttf -------------------------------------------------------------------------------- /resources/fonts/robotoslab_thin_macroman/RobotoSlab-Thin-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Floobits/floobits-atom/64c044009c1591d686958b5a6af1362528aa038e/resources/fonts/robotoslab_thin_macroman/RobotoSlab-Thin-webfont.woff -------------------------------------------------------------------------------- /resources/icon_64x64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Floobits/floobits-atom/64c044009c1591d686958b5a6af1362528aa038e/resources/icon_64x64.png -------------------------------------------------------------------------------- /resources/owner_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Floobits/floobits-atom/64c044009c1591d686958b5a6af1362528aa038e/resources/owner_icon.png -------------------------------------------------------------------------------- /resources/password_conf_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Floobits/floobits-atom/64c044009c1591d686958b5a6af1362528aa038e/resources/password_conf_icon.png -------------------------------------------------------------------------------- /resources/password_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Floobits/floobits-atom/64c044009c1591d686958b5a6af1362528aa038e/resources/password_icon.png -------------------------------------------------------------------------------- /resources/settings_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Floobits/floobits-atom/64c044009c1591d686958b5a6af1362528aa038e/resources/settings_icon.png -------------------------------------------------------------------------------- /resources/sideways_infinity.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Floobits/floobits-atom/64c044009c1591d686958b5a6af1362528aa038e/resources/sideways_infinity.png -------------------------------------------------------------------------------- /resources/sliderbg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Floobits/floobits-atom/64c044009c1591d686958b5a6af1362528aa038e/resources/sliderbg.png -------------------------------------------------------------------------------- /resources/sublime_demo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Floobits/floobits-atom/64c044009c1591d686958b5a6af1362528aa038e/resources/sublime_demo.png -------------------------------------------------------------------------------- /resources/supported_bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Floobits/floobits-atom/64c044009c1591d686958b5a6af1362528aa038e/resources/supported_bg.png -------------------------------------------------------------------------------- /resources/title_bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Floobits/floobits-atom/64c044009c1591d686958b5a6af1362528aa038e/resources/title_bg.png -------------------------------------------------------------------------------- /resources/username_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Floobits/floobits-atom/64c044009c1591d686958b5a6af1362528aa038e/resources/username_icon.png -------------------------------------------------------------------------------- /resources/vim_demo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Floobits/floobits-atom/64c044009c1591d686958b5a6af1362528aa038e/resources/vim_demo.png -------------------------------------------------------------------------------- /resources/workspace_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Floobits/floobits-atom/64c044009c1591d686958b5a6af1362528aa038e/resources/workspace_icon.png -------------------------------------------------------------------------------- /spec/floobits-spec.coffee: -------------------------------------------------------------------------------- 1 | Floobits = require '../lib/floobits' 2 | 3 | # Use the command `window:run-package-specs` (cmd-alt-ctrl-p) to run specs. 4 | # 5 | # To run a specific `it` or `describe` block add an `f` to the front (e.g. `fit` 6 | # or `fdescribe`). Remove the `f` to unfocus the block. 7 | 8 | describe "Floobits", -> 9 | activationPromise = null 10 | 11 | beforeEach -> 12 | atom.workspaceView = new WorkspaceView 13 | activationPromise = atom.packages.activatePackage('floobits') 14 | 15 | describe "when the floobits:toggle event is triggered", -> 16 | it "attaches and then detaches the view", -> 17 | expect(atom.workspaceView.find('.floobits')).not.toExist() 18 | 19 | # This is an activation event, triggering it will cause the package to be 20 | # activated. 21 | atom.workspaceView.trigger 'floobits:toggle' 22 | 23 | waitsForPromise -> 24 | activationPromise 25 | 26 | runs -> 27 | expect(atom.workspaceView.find('.floobits')).toExist() 28 | atom.workspaceView.trigger 'floobits:toggle' 29 | expect(atom.workspaceView.find('.floobits')).not.toExist() 30 | -------------------------------------------------------------------------------- /spec/floobits-view-spec.coffee: -------------------------------------------------------------------------------- 1 | FloobitsView = require '../lib/floobits-view' 2 | {WorkspaceView} = require 'atom' 3 | 4 | describe "FloobitsView", -> 5 | it "has one valid test", -> 6 | expect("life").toBe "easy" 7 | -------------------------------------------------------------------------------- /styles/colors.less: -------------------------------------------------------------------------------- 1 | @red: rgb(226, 82, 82); 2 | @red-text: rgb(224, 83, 83); 3 | @dark-red: rgb(210, 80, 83); 4 | @darker-red: rgb(175, 26, 31); 5 | @green: rgb(130, 198, 157); 6 | @teal-green: rgb(86, 172, 130); 7 | @maroon: rgb(117, 84, 84); 8 | @dark-blue: rgb(53, 58, 79); 9 | @darker-blue: rgb(223, 35, 28); 10 | @midnight-blue: rgb(34, 38, 54); 11 | @plan-blue: rgb(177, 223, 255); 12 | @light-blue: rgb(191, 198, 228); 13 | @baby-blue: rgb(178, 250, 255); 14 | @teal-blue: rgb(62, 198, 209); 15 | @red-link: rgb(221, 78, 78); 16 | @light-teal: rgb(61, 154, 162); 17 | @pale-teal: rgb(74, 166, 172); 18 | @lighter-teal: rgb(99, 194, 201); 19 | @dark-teal: rgb(53, 89, 105); 20 | @so-much-teal: rgb(49, 211, 240); 21 | @btn-active-teal: rgb(0, 181, 208); 22 | @tab-teal: rgb(103, 196, 203); 23 | @light-purple: rgb(114, 106, 189); 24 | @btn-active-purple: rgb(150, 142, 222); 25 | @dark-purple: rgb(98, 107, 143); 26 | @pink: rgb(251, 210, 255); 27 | @pastel-pink: rgb(254, 173, 255); 28 | @violet: rgb(184, 180, 219); 29 | @dark-violet: rgb(141, 137, 176); 30 | @light-violet: rgb(159, 154, 200); 31 | @fl-flat-purple: rgb(165, 155, 171); 32 | @fl-white: rgb(255, 255, 255); 33 | @fl-yellow: rgb(222, 212, 194); 34 | @fl-dark-yellow: rgb(255, 236, 45); 35 | @fl-pale-yellow: rgb(254, 255, 208); 36 | @fl-bg-yellow: rgb(255, 251, 222); 37 | @fl-light-yellow: rgb(254, 255, 232); 38 | @fl-tan: rgba(159, 173, 143, .75); 39 | @fl-black: rgb(0, 0, 0); 40 | @fl-bg-black: rgb(49, 46, 52); 41 | @fl-light-black: rgb(51, 51, 51); 42 | @fl-med-black: rgb(39, 36, 41); 43 | @fl-gray: rgb(190, 190, 190); 44 | @fl-light-gray: rgb(234, 235, 237); 45 | @fl-dark-gray: rgb(49, 46, 52); 46 | @fl-gray-text: rgb(102, 102, 102); 47 | @fl-green: rgb(115, 184, 147); 48 | @fl-shaded-green: rgb(88, 173, 129); 49 | @fl-dark-green: rgb(91, 174, 130); 50 | @fl-red: rgb(225, 83, 83); 51 | @fl-brown: rgb(153, 133, 133); 52 | 53 | @sublime-green: rgb(147, 211, 125); 54 | @vim-purple: @violet; 55 | @emacs-blue: @lighter-teal; 56 | @intellij-gold: rgb(213, 205, 125); 57 | 58 | @fl-soft-black-text: rgb(49, 46, 52); 59 | @fl-box-shadow: rgba(0, 0, 0, .31); 60 | @fl-input-border: rgba(0, 0, 0, .23); 61 | @faded-border: rgba(51, 51, 51, .5); 62 | @grad-dark-point: rgba(125, 109, 113, .24); 63 | @grad-light-point: rgba(255, 255, 255, .24); 64 | @text-shadow-color: rgba(186, 155, 125, .75); 65 | @text-shadow-color-dark: rgba(107, 89, 72, .75); 66 | @separator-color: rgb(214, 206, 206); 67 | @light-border: rgba(255, 255, 255, .15); 68 | 69 | @plan-green: rgb(147, 211, 125); 70 | @plan-orange: rgb(252, 186, 71); 71 | @plan-red: rgb(255, 125, 125); 72 | @plan-violet: rgb(202, 173, 255); 73 | 74 | 75 | @lightest_gray: #f5f5f5; 76 | @lighter_gray: #ededed; 77 | @light_gray: #dcdcdc; 78 | @mid_gray: #ccc; 79 | @darkMidGray: rgba(175, 175, 175, 1); 80 | @lighterGray: rgba(240, 240, 240, 1); 81 | @dark_gray: #555; 82 | @darker_gray: #333; 83 | 84 | @thumbnailGray: rgba(228, 228, 228, 0.8); 85 | @thumbnailGrayShadow: rgba(96, 96, 96, 0.5); 86 | 87 | @mid_blue: #6493be; 88 | 89 | @mid_orange: #f70; 90 | 91 | @floobits_color: #d00; 92 | 93 | @red: #f00; 94 | 95 | @dark: rgba(62, 62, 62, 1); 96 | @darkHover: rgba(40, 40, 40, 1); 97 | @darker: rgba(10, 10, 10, 1); 98 | @light: rgba(255, 255, 255, 1); 99 | @white: white; 100 | @black: black; 101 | -------------------------------------------------------------------------------- /styles/edit_permissions_wizard.less: -------------------------------------------------------------------------------- 1 | @import "colors.less"; 2 | @import "util.less"; 3 | 4 | input.autocomplete { 5 | margin-bottom: 0; 6 | width: 20em; 7 | } 8 | 9 | ul.autocomplete_results { 10 | border: 1px solid @fl-gray; 11 | list-style: none; 12 | margin-top: -2px; 13 | margin-left: 0; 14 | padding-left: 0; 15 | li { 16 | cursor: pointer; 17 | margin: 2px 0 0 0; 18 | .syntax--ellipsis; 19 | padding: 2px 2px 2px 4px; 20 | } 21 | li:first-child{ 22 | padding-top: 4px; 23 | } 24 | li:last-child{ 25 | padding-bottom: 4px; 26 | } 27 | li:hover, li.selected { 28 | color: @fl-light-gray; 29 | background-color: @dark-blue; 30 | } 31 | } 32 | 33 | .workspace-wizard { 34 | text-align: center; 35 | font-size: 15px; 36 | a { 37 | text-decoration: underline; 38 | color: @floobits_color; 39 | } 40 | .wizard-section { 41 | padding-bottom: 20px; 42 | margin-bottom: 20px; 43 | border: 0px solid @lighter_gray; 44 | border-bottom-width: 1px; 45 | } 46 | .wizard-section-no-border { 47 | .wizard-section; 48 | border: 0; 49 | } 50 | table, label { 51 | font-size: 15px; 52 | } 53 | label { 54 | display: inline-block; 55 | margin-right: 10px; 56 | } 57 | } 58 | .wizard-button-bar { 59 | margin-top: 20px; 60 | text-align: right; 61 | } 62 | 63 | #edit-perms-content-wizard { 64 | .everyone-can { 65 | text-align: center; 66 | table { 67 | margin: 0 auto; 68 | } 69 | } 70 | td { 71 | padding: 5px; 72 | text-align: left; 73 | label { 74 | font-size: 12px; 75 | } 76 | } 77 | #all-perms-content { 78 | label { 79 | font-size: 12px; 80 | } 81 | } 82 | input[type=text] { 83 | border: 1px solid @darkMidGray; 84 | width: 200px; 85 | outline: none; 86 | box-shadow: none; 87 | &:focus { 88 | box-shadow: none; 89 | outline: none; 90 | } 91 | } 92 | .autocomplete-content { 93 | position: relative; 94 | .edit-perms-autocomplete-results { 95 | position: absolute; 96 | top: 0px; 97 | left: 0px; 98 | width: 200px; 99 | background-color: #FFF; 100 | border: 1px solid @darkMidGray; 101 | max-height: 200px; 102 | overflow-y: auto; 103 | overflow-x: hidden; 104 | } 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /styles/floobits.atom-text-editor.less: -------------------------------------------------------------------------------- 1 | // The ui-variables file is provided by base themes provided by Atom. 2 | // 3 | // See https://github.com/atom/atom-dark-ui/blob/master/stylesheets/ui-variables.less 4 | // for a full listing of what's available. 5 | @import "ui-variables"; 6 | @import "./edit_permissions_wizard"; 7 | 8 | @transition-time: 0.5s; 9 | @size: 2px; 10 | 11 | @-webkit-keyframes floobits-highlight-animation { 12 | 0% {opacity: .5;} 13 | 100% {opacity: 0;} 14 | } 15 | 16 | .make_highlight_dark(@hlcolor) { 17 | .floobits-highlight-dark_@{hlcolor} .region { 18 | background-color: @hlcolor; 19 | opacity: .3; 20 | } 21 | .floobits-border-dark_@{hlcolor} .region { 22 | background-color: @hlcolor; 23 | -webkit-animation: floobits-highlight-animation 2s ease 1; 24 | opacity: 0; 25 | } 26 | .floobits-highlight-dark_@{hlcolor}.line-number:before { 27 | border-right: @size solid @hlcolor; 28 | content: " "; 29 | } 30 | } 31 | 32 | .floobits-highlight-light(@hlcolor) { 33 | .floobits-highlight_@{hlcolor}.region { 34 | background-color: @hlcolor; 35 | opacity: .8; 36 | } 37 | .floobits-border-light_@{hlcolor} .region { 38 | background-color: @hlcolor; 39 | -webkit-animation: floobits-highlight-animation 2s ease 1; 40 | opacity: 0; 41 | } 42 | .floobits-highlight_@{hlcolor}.line-number:before { 43 | border-right: @size solid @hlcolor; 44 | content: " "; 45 | } 46 | } 47 | .floobits { 48 | a { 49 | //color: #428BCA; 50 | outline: none !important; 51 | } 52 | } 53 | 54 | .make_highlight_dark(e("black")); 55 | .make_highlight_dark(e("blue")); 56 | .make_highlight_dark(e("darkblue")); 57 | .make_highlight_dark(e("fuchsia")); 58 | .make_highlight_dark(e("gray")); 59 | .make_highlight_dark(e("green")); 60 | .make_highlight_dark(e("greenyellow")); 61 | .make_highlight_dark(e("indigo")); 62 | .make_highlight_dark(e("lime")); 63 | .make_highlight_dark(e("magenta")); 64 | .make_highlight_dark(e("maroon")); 65 | .make_highlight_dark(e("midnightblue")); 66 | .make_highlight_dark(e("orange")); 67 | .make_highlight_dark(e("orangered")); 68 | .make_highlight_dark(e("purple")); 69 | .make_highlight_dark(e("red")); 70 | .make_highlight_dark(e("teal")); 71 | .make_highlight_dark(e("yellow")); 72 | 73 | floobits-conflicts div { 74 | padding: 10px; 75 | // color: white; 76 | } 77 | 78 | input.floobits-submit { 79 | color: black; 80 | display: inline-block; 81 | min-width: 125px; 82 | content: 'Select Directory'; 83 | display: inline-block; 84 | background: -webkit-linear-gradient(top, #f9f9f9, #e3e3e3); 85 | border: 1px solid #999; 86 | border-radius: 3px; 87 | padding: 5px 8px; 88 | outline: none; 89 | white-space: nowrap; 90 | -webkit-user-select: none; 91 | cursor: pointer; 92 | text-shadow: 1px 1px #fff; 93 | font-weight: 700; 94 | font-size: 10pt; 95 | float: right; 96 | } 97 | -------------------------------------------------------------------------------- /styles/fonts.less: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: 'RobotoSlabRegular'; 3 | src: url('atom://floobits/resources/fonts/robotoslab_regular_macroman/RobotoSlab-Regular-webfont.eot?iefix') format('eot'), 4 | url('atom://floobits/resources/fonts/robotoslab_regular_macroman/RobotoSlab-Regular-webfont.woff') format('woff'), 5 | url('atom://floobits/resources/fonts/robotoslab_regular_macroman/RobotoSlab-Regular-webfont.ttf') format('truetype'), 6 | url('atom://floobits/resources/fonts/robotoslab_regular_macroman/RobotoSlab-Regular-webfont.svg#webfont') format('svg'); 7 | } 8 | @font-face { 9 | font-family: 'RobotoSlabBold'; 10 | src: url('atom://floobits/resources/fonts/robotoslab_bold_macroman/RobotoSlab-Bold-webfont.eot?iefix') format('eot'), 11 | url('atom://floobits/resources/fonts/robotoslab_bold_macroman/RobotoSlab-Bold-webfont.woff') format('woff'), 12 | url('atom://floobits/resources/fonts/robotoslab_bold_macroman/RobotoSlab-Bold-webfont.ttf') format('truetype'), 13 | url('atom://floobits/resources/fonts/robotoslab_bold_macroman/RobotoSlab-Bold-webfont.svg#webfont') format('svg'); 14 | } 15 | @font-face { 16 | font-family: 'RobotoSlabLight'; 17 | src: url('atom://floobits/resources/fonts/robotoslab_light_macroman/RobotoSlab-Light-webfont.eot?iefix') format('eot'), 18 | url('atom://floobits/resources/fonts/robotoslab_light_macroman/RobotoSlab-Light-webfont.woff') format('woff'), 19 | url('atom://floobits/resources/fonts/robotoslab_light_macroman/RobotoSlab-Light-webfont.ttf') format('truetype'), 20 | url('atom://floobits/resources/fonts/robotoslab_light_macroman/RobotoSlab-Light-webfont.svg#webfont') format('svg'); 21 | } 22 | @font-face { 23 | font-family: 'ProximaNovaExtraBold'; 24 | src: url('atom://floobits/resources/fonts/proxima_nova/ProximaNova-Extrabold.otf') format('opentype'); 25 | } 26 | @font-face { 27 | font-family: 'ProximaNova'; 28 | src: url('atom://floobits/resources/fonts/proxima_nova/ProximaNova-Semibold.otf') format('opentype'); 29 | } 30 | @font-face { 31 | font-family: 'ProximaNovaLight'; 32 | src: url('atom://floobits/resources/fonts/proxima_nova/ProximaNova-Light_0.otf') format('opentype'); 33 | } 34 | -------------------------------------------------------------------------------- /styles/icons.less: -------------------------------------------------------------------------------- 1 | @import "octicon-mixins"; 2 | 3 | .make_icon(@octiconicon, @size: 16px) { 4 | .octicon(@octiconicon, @size); 5 | margin: 0 auto; 6 | text-align: center; 7 | z-index: 100000; 8 | cursor: default; 9 | } 10 | 11 | .floobits-close-icon { 12 | .make_icon(x, 24px); 13 | position: absolute; 14 | top: 0px; 15 | right: 0px; 16 | cursor: pointer; 17 | } 18 | 19 | .floobits-close-icon-small { 20 | .make_icon(x, 16px); 21 | position: absolute; 22 | top: 4px; 23 | right: 2px; 24 | cursor: pointer; 25 | } 26 | 27 | .icon-floobits-conflicts { 28 | padding-left: 22px; 29 | background: transparent url("atom://floobits/resources/icon_64x64.png") no-repeat; 30 | background-size: 20px; 31 | background-position-y: center; 32 | } 33 | 34 | .floobits-icon-remove { 35 | .make_icon(x); 36 | } 37 | 38 | .floobits-arrow-up-icon { 39 | .make_icon(three-bars); 40 | } 41 | 42 | .floobits-arrow-down-icon { 43 | .make_icon(three-bars); 44 | } 45 | 46 | .floobits-eject-icon { 47 | .make_icon(jump-down); 48 | } 49 | 50 | .floobits-video-icon { 51 | .make_icon(device-camera-video); 52 | } 53 | 54 | .floobits-follow-icon { 55 | .make_icon(radio-tower); 56 | } 57 | 58 | .floobits-info-icon { 59 | .make_icon(info); 60 | } 61 | 62 | .floobits-permissions-icon { 63 | .make_icon(gear); 64 | } -------------------------------------------------------------------------------- /styles/join.less: -------------------------------------------------------------------------------- 1 | floobits-join-workspace { 2 | h2 { 3 | text-align: center; 4 | width: 100%; 5 | //color: white; 6 | font-size: 1.5em; 7 | padding-bottom: 10px; 8 | } 9 | 10 | .well { 11 | background-color: rgba(170, 170, 170, 0.15); 12 | padding: 15px; 13 | overflow: auto; 14 | } 15 | 16 | .input-group { 17 | margin-bottom: 15px; 18 | } 19 | 20 | input.floobits-file { 21 | //color: white; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /styles/messages.less: -------------------------------------------------------------------------------- 1 | @import "colors.less"; 2 | @import "util.less"; 3 | 4 | @time-width: 60px; 5 | 6 | 7 | .floobits-messages-container { 8 | .chat-input-container { 9 | form { 10 | input.chat-input { 11 | width: 100%; 12 | border: none; 13 | color: black; 14 | padding: 5px; 15 | } 16 | } 17 | } 18 | .messages-list { 19 | overflow-x: hidden; 20 | overflow-y: auto; 21 | .message { 22 | font-size: 13px; 23 | border: 0px solid; 24 | &.alert { 25 | .nomar_nopad; 26 | background-color: inherit; 27 | } 28 | .message-content { 29 | font-size: 13px; 30 | padding: 8px 0; 31 | margin: 0px; 32 | text-align: left; 33 | a { 34 | text-decoration: underline; 35 | } 36 | img { 37 | max-width: 100%; 38 | max-height: 400px; 39 | display: block; 40 | } 41 | .floobits-square { 42 | display: inline-block; 43 | height: 13px; 44 | width: 13px; 45 | margin: 0 5px 1px 0px; 46 | vertical-align: middle; 47 | } 48 | .messages-image-container { 49 | position: relative; 50 | .messages-remove-image { 51 | position: absolute; 52 | top: 0px; 53 | left: 0px; 54 | //color: @black; 55 | opacity: 0.45; 56 | } 57 | img { 58 | position: relative; 59 | left: 20px; 60 | } 61 | } 62 | } 63 | .message-log-repeat { 64 | font-size: 9px; 65 | font-weight: bold; 66 | } 67 | .message-text { 68 | margin-left: @time-width; 69 | line-height: 18px; 70 | } 71 | .message-username { 72 | font-weight: bold; 73 | } 74 | .message-timestamp { 75 | width: @time-width; 76 | margin-left: 2px; 77 | float: left; 78 | line-height: 18px; 79 | font-size: 11px; 80 | color: #919191; 81 | } 82 | } 83 | } 84 | } -------------------------------------------------------------------------------- /styles/modal.less: -------------------------------------------------------------------------------- 1 | .btn-group { 2 | button { 3 | height: 35px; 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /styles/permissions.less: -------------------------------------------------------------------------------- 1 | #floobits-permissions { 2 | label { 3 | padding-right: 10px; 4 | } 5 | .field_wrapper { 6 | padding-bottom: 20px; 7 | } 8 | h2 { 9 | text-align: center; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /styles/signin.less: -------------------------------------------------------------------------------- 1 | @import "./util.less"; 2 | 3 | .fl-signin-form-container { 4 | font-family: "ProximaNova", sans-serif; 5 | a { 6 | color: @fl-green; 7 | outline: none !important; 8 | &:hover { 9 | color: @red-text; 10 | } 11 | button.btn { 12 | color: @fl-white; 13 | } 14 | text-decoration: none; 15 | } 16 | text-align: center; 17 | .signin-title { 18 | color: @fl-green; 19 | font-size: 30px; 20 | font-family: "RobotoSlabRegular", serif; 21 | text-align: center; 22 | margin: 25px 0 5px; 23 | } 24 | form { 25 | width: 325px; 26 | div.signup-input-container { 27 | text-align: left; 28 | display: inline-block; 29 | width: 275px; 30 | height: 42px; 31 | &:nth-child(2) { 32 | margin: 15px 0 0; 33 | } 34 | padding-top: 0px !important; 35 | padding: 0px; 36 | } 37 | div.signin-other-container { 38 | .signin-btn { 39 | margin: 10px 0; 40 | display: inline-block; 41 | height: 45px; 42 | width: 275px; 43 | border: 1px solid @dark-red; 44 | border-bottom-color: @darker-red; 45 | .border-image(@dark-red, @darker-red); 46 | &:hover { 47 | border-color: @dark-teal; 48 | border-image: none; 49 | } 50 | &:focus { 51 | border-color: @dark-teal; 52 | border-image: none; 53 | } 54 | } 55 | } 56 | .signin-github { 57 | margin: 15px 0 70px; 58 | } 59 | } 60 | .signin-forgot-password { 61 | position: absolute; 62 | left: 0px; 63 | bottom: 0px; 64 | right: 0px; 65 | background-color: @fl-light-gray; 66 | text-align: center; 67 | padding: 15px 0; 68 | a { 69 | color: @fl-light-black; 70 | font-size: 12px; 71 | } 72 | } 73 | .modal-content { 74 | border-radius: 0; 75 | width: 325px; 76 | .signin-close { 77 | color: @fl-gray; 78 | position: absolute; 79 | top: 10px; 80 | right: 10px; 81 | cursor: pointer; 82 | } 83 | } 84 | } 85 | div.signup-input-container, div.signup-github { 86 | background-color: @fl-white; 87 | padding: 5px 20px; 88 | span.signup-icon { 89 | display: inline-block; 90 | background-image: -webkit-linear-gradient(top, rgba(255, 255, 255, 0.1) 0%, rgba(0, 0, 0, 0.1) 100%); 91 | background-image: -o-linear-gradient(top, rgba(255, 255, 255, 0.1) 0%, rgba(0, 0, 0, 0.1) 100%); 92 | background-image: linear-gradient(to bottom, rgba(255, 255, 255, 0.1) 0%, rgba(0, 0, 0, 0.1) 100%); 93 | background-repeat: repeat-x; 94 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#1affffff', endColorstr='#1a000000', GradientType=0); 95 | width: 40px; 96 | height: 40px; 97 | vertical-align: top; 98 | border: 0px solid @fl-gray; 99 | border-right-width: 1px; 100 | position: relative; 101 | &::after { 102 | opacity: 0.5; 103 | content: " "; 104 | position: absolute; 105 | top: 0px; 106 | left: 0px; 107 | right: 0px; 108 | bottom: 0px; 109 | } 110 | &.signup-username-icon::after { 111 | background: transparent url("atom://floobits/resources/username_icon.png") no-repeat 11px 10px; 112 | } 113 | &.signup-email-icon::after { 114 | background: transparent url("atom://floobits/resources/email_icon.png") no-repeat 10px 11px; 115 | } 116 | &.signup-password-icon::after { 117 | background: transparent url("atom://floobits/resources/password_icon.png") no-repeat 10px 11px; 118 | } 119 | &.signup-password-conf-icon::after { 120 | background: transparent url("atom://floobits/resources/password_conf_icon.png") no-repeat 10px 9px; 121 | } 122 | 123 | } 124 | &.active-signup-icon { 125 | div { 126 | border-color: @fl-dark-gray; 127 | span.signup-icon { 128 | border-color: @fl-dark-gray; 129 | &::after { 130 | opacity: 1 !important; 131 | } 132 | } 133 | } 134 | } 135 | input { 136 | display: inline-block; 137 | height: 40px; 138 | width: 80%; 139 | border: 1px; 140 | line-height: 150%; 141 | font-size: 12px; 142 | padding: 10px 5px; 143 | } 144 | @inputBorderPadding: 30px; 145 | &:nth-child(2) { 146 | padding-top: @inputBorderPadding; 147 | } 148 | &:nth-child(5) { 149 | padding-bottom: @inputBorderPadding; 150 | } 151 | div { 152 | border: 1px solid @fl-input-border; 153 | white-space: nowrap; 154 | overflow: hidden; 155 | } 156 | div > input:focus { 157 | border-color: @fl-light-black; 158 | } 159 | } 160 | .red-btn { 161 | color: @fl-white; 162 | border-radius: 1px; 163 | /*#gradient > .vertical(@end-color: rgb(211, 65, 65); @start-color: rgb(231, 92, 92));*/ 164 | &:hover { 165 | color: @fl-white; 166 | border-color: rgb(44, 167, 177); 167 | /*#gradient > .vertical(@end-color: rgb(44, 167, 177); @start-color: rgb(53, 188, 198));*/ 168 | } 169 | &:focus { 170 | color: @fl-white; 171 | border-color: rgb(44, 167, 177); 172 | /*#gradient > .vertical(@end-color: rgb(44, 167, 177); @start-color: rgb(53, 188, 198));*/ 173 | } 174 | } 175 | .signup-btn { 176 | font-size: 18px; 177 | display: block; 178 | width: 100%; 179 | border: 0px solid rgb(231, 92, 92); 180 | border-top-width: 1px; 181 | height: 65px; 182 | .red-btn; 183 | background-image: linear-gradient(to bottom, #e75c5c 0%, #d34141 100%); 184 | background-repeat: repeat-x; 185 | 186 | &:hover { 187 | color: #ffffff; 188 | border-color: #2ca7b1; 189 | background-image: -webkit-linear-gradient(top, #35bcc6 0%, #2ca7b1 100%); 190 | background-image: -o-linear-gradient(top, #35bcc6 0%, #2ca7b1 100%); 191 | background-image: linear-gradient(to bottom, #35bcc6 0%, #2ca7b1 100%); 192 | background-repeat: repeat-x; 193 | } 194 | } 195 | a.signup-btn { 196 | display: inline-block; 197 | vertical-align: middle; 198 | padding-top: 17px; 199 | &:hover { 200 | color: @fl-white; 201 | text-decoration: none; 202 | } 203 | width: 250px; 204 | margin: 70px 0 50px 0; 205 | font-size: 20px; 206 | } 207 | -------------------------------------------------------------------------------- /styles/status_bar.less: -------------------------------------------------------------------------------- 1 | @import "ui-variables"; 2 | 3 | floobits-status-bar { 4 | .floobits-status-bar { 5 | font-size: 11px; 6 | // This fixes styling with many popular themes. 7 | padding: 2px @component-line-height/2; 8 | background: inherit; 9 | } 10 | // This fixes styling with many popular themes. 11 | padding-left: 0 !important; 12 | display: inline-block; 13 | width: 100%; 14 | } 15 | -------------------------------------------------------------------------------- /styles/terminal.less: -------------------------------------------------------------------------------- 1 | @import "./util.less"; 2 | 3 | .floobits-terminal { 4 | padding: 8px; 5 | .syntax--monospace; 6 | div { 7 | white-space: nowrap; 8 | } 9 | } 10 | 11 | floobits-terminal-list-view { 12 | display: flex; 13 | margin-top: 10px; 14 | .header { 15 | img { 16 | width: 22px; 17 | padding-right: 5px; 18 | } 19 | margin-left:20px; 20 | } 21 | ul { 22 | margin-left: -10px; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /styles/user_list_pane.less: -------------------------------------------------------------------------------- 1 | #user-list-pane { 2 | height: 100%; 3 | overflow-x: hidden; 4 | overflow-y: auto; 5 | ::-webkit-scrollbar { 6 | display: none; 7 | } 8 | background-color: black; 9 | #user-list-pane-header { 10 | width: 100%; 11 | background-color: black; 12 | border: 1px solid black; 13 | height: 30px; 14 | img { 15 | width: 25px; 16 | height: 25px; 17 | margin: 3px; 18 | } 19 | span { 20 | margin: 0 5px 0 0; 21 | } 22 | } 23 | #user-list-close { 24 | cursor: pointer; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /styles/userlist.less: -------------------------------------------------------------------------------- 1 | @import "./util.less"; 2 | @import "./icons.less"; 3 | 4 | @userBarHeight: 32px; 5 | @iconFontSize: 20px; 6 | @iconPadding: 3px; 7 | 8 | .userlist-icon { 9 | border: 1px solid; 10 | opacity: 0.45; 11 | margin-right: 5px; 12 | } 13 | 14 | @userBarHeight: 32px; 15 | @iconFontSize: 20px; 16 | 17 | .user-thumbnail-background { 18 | background-color: rgba(0, 0, 0, 0.36); 19 | } 20 | 21 | .user-thumbnail-text { 22 | color: @thumbnailGray; 23 | } 24 | 25 | .user-list { 26 | width: @userThumbWidth; 27 | overflow-x: hidden; 28 | .user { 29 | height: @userThumbHeight; 30 | width: @userThumbWidth; 31 | vertical-align: top; 32 | display: inline-block; 33 | position: relative; 34 | overflow: hidden; 35 | i.user-indicator { 36 | position: absolute; 37 | cursor: pointer; 38 | font-size: @iconFontSize; 39 | color: @white; 40 | opacity: 0.5; 41 | &.enabled, &:hover { 42 | opacity: 1; 43 | } 44 | border: 1px solid @thumbnailGrayShadow; 45 | background-color: @thumbnailGrayShadow; 46 | } 47 | .visualizer-container { 48 | position: absolute; 49 | top: -2px; 50 | left: -2px; 51 | height: @userBarHeight; 52 | width: @userBarHeight; 53 | display: flex; 54 | align-items: center; 55 | justify-content: center; 56 | .visualizer { 57 | background-color: @white; 58 | border-radius: 50%; 59 | opacity: 0.45; 60 | z-index: @zPlane1; 61 | } 62 | } 63 | i.user-stop { 64 | top: 3px; 65 | right: 3px; 66 | z-index: @zPlane1; 67 | .floobits-icon-remove; 68 | } 69 | i.in-video { 70 | top: auto; 71 | bottom: @userBarHeight + 3; 72 | left: 3px; 73 | .floobits-video-icon; 74 | } 75 | i.user-following { 76 | top: auto; 77 | bottom: @userBarHeight + 3; 78 | right: 3px; 79 | .floobits-follow-icon; 80 | } 81 | .user-thumb { 82 | cursor: pointer; 83 | background-color: black; 84 | height: @userThumbHeight; 85 | width: @userThumbWidth; 86 | &.user-my-conn { 87 | transform: scaleX(-1); 88 | } 89 | &.audio-only { 90 | transform: scaleX(1); 91 | } 92 | } 93 | .user-face { 94 | .click-to-video { 95 | .noselect; 96 | .pos(0, 0, @userBarHeight, 0); 97 | .user-thumbnail-background; 98 | .user-thumbnail-text; 99 | display: none; 100 | font-size: @iconFontSize; 101 | padding-top: (@userThumbHeight - @userBarHeight) / 2; 102 | text-align: center; 103 | cursor: pointer; 104 | // In Chrome, transform properties mess with z-index. This is a workaround. 105 | z-index: @zPlane1; 106 | .glyphicon { 107 | top: @iconPadding; 108 | } 109 | } 110 | &:hover .click-to-video { 111 | display: block; 112 | } 113 | } 114 | &.opened { 115 | .user-thumb { 116 | .blur(5px); 117 | } 118 | .user-info { 119 | top: 0; 120 | opacity: 1; 121 | } 122 | .user-bar i.user-arrow { 123 | .floobits-arrow-down-icon; 124 | } 125 | } 126 | .user-bar { 127 | cursor: pointer; 128 | padding: 4px 5px; 129 | .user-thumbnail-text; 130 | .user-thumbnail-background; 131 | .pos_bottom; 132 | .noselect; 133 | font-size: 18px; 134 | height: @userBarHeight; 135 | i.user-arrow { 136 | position: absolute; 137 | top: 7px; 138 | right: 7px; 139 | .floobits-arrow-up-icon; 140 | } 141 | } 142 | .user-info { 143 | opacity: 0; 144 | hr { 145 | margin: 5px 0; 146 | border-color: #333; 147 | } 148 | .stack-up { 149 | padding: 3px 5px; 150 | .stack-up-content { 151 | padding: 0 3px; 152 | a { 153 | color: #73B893; 154 | } 155 | } 156 | } 157 | .user-conn { 158 | clear: both; 159 | } 160 | .user-thumbnail-text; 161 | .user-thumbnail-background; 162 | position: absolute; 163 | left: 0; 164 | right: 0; 165 | top: @userThumbHeight - @userBarHeight; 166 | bottom: @userBarHeight; 167 | transition: top 0.25s, opacity 0s; 168 | -webkit-transition: top 0.25s, opacity 0s; 169 | height: @userThumbHeight - @userBarHeight; 170 | text-align: left; 171 | overflow-x: hidden; 172 | overflow-y: auto; 173 | width: 100%; 174 | } 175 | } 176 | } 177 | -------------------------------------------------------------------------------- /styles/util.less: -------------------------------------------------------------------------------- 1 | //@import "bootstrap_v3.2.0/variables.less"; 2 | //@import "bootstrap_v3.2.0/mixins/alerts.less"; 3 | //@import "bootstrap_v3.2.0/alerts.less"; 4 | @import "./colors.less"; 5 | 6 | //.alert-error { 7 | // .alert-danger; 8 | //} 9 | 10 | .fl-progress { 11 | background-color: inherit; 12 | border: 1px solid; 13 | box-shadow: inset 0 1px 2px rgba(0,0,0,.1); 14 | color: #fff; 15 | height: 20px; 16 | margin: 20px 0; 17 | overflow: hidden; 18 | padding: 0; 19 | text-align: center; 20 | position: relative; 21 | .fl-progress-bar { 22 | background-color: #337ab7; 23 | box-shadow: inset 0 -1px 0 rgba(0,0,0,.15); 24 | color: #fff; 25 | float: left; 26 | font-size: 12px; 27 | height: 100%; 28 | line-height: 20px; 29 | padding: 0; 30 | text-align: center; 31 | transition: width .6s ease; 32 | width: 0%; 33 | } 34 | .fl-progress-text { 35 | position: absolute; 36 | display: block; 37 | width: 100%; 38 | } 39 | } 40 | 41 | .nomar_nopad() { 42 | margin: 0; 43 | padding: 0; 44 | } 45 | 46 | .pos(@top: 0, @right: 0, @bottom: 0, @left: 0) { 47 | position: absolute; 48 | top: @top; 49 | right: @right; 50 | bottom: @bottom; 51 | left: @left; 52 | } 53 | 54 | .pos_bottom { 55 | position: absolute; 56 | bottom: 0; 57 | left: 0; 58 | right: 0; 59 | } 60 | 61 | .sans { 62 | font-family: "HelveticaNeue", Helvetica, Verdana, Arial, sans-serif; 63 | } 64 | 65 | .header_font() { 66 | .sans; 67 | font-weight: 500; 68 | .a_hover; 69 | } 70 | 71 | .syntax--monospace { 72 | font-family: Menlo, "Deja Vu Sans Mono", Consolas, monospace; 73 | } 74 | 75 | #toc { 76 | background-color: @lightest_gray; 77 | padding: 0.5em 1em; 78 | width: 12em; 79 | } 80 | 81 | .a_nocolor { 82 | a { 83 | color: inherit; 84 | } 85 | } 86 | 87 | .a_stealth { 88 | .a_nocolor; 89 | a, a:hover { 90 | text-decoration: none; 91 | } 92 | } 93 | 94 | .a_hover { 95 | a { 96 | text-decoration: none; 97 | } 98 | a:hover { 99 | text-decoration: underline; 100 | } 101 | } 102 | 103 | .syntax--ellipsis { 104 | overflow: hidden; 105 | text-overflow: ellipsis; 106 | white-space: nowrap; 107 | } 108 | 109 | .noselect { 110 | -webkit-touch-callout: none; 111 | -webkit-user-select: none; 112 | -khtml-user-select: none; 113 | -moz-user-select: none; 114 | -ms-user-select: none; 115 | user-select: none; 116 | } 117 | 118 | .blur(@radius) { 119 | filter: blur(@radius); 120 | -webkit-filter: blur(@radius); 121 | } 122 | 123 | hr.syntax--separator { 124 | padding-bottom: 2em; 125 | } 126 | div.syntax--separator{ 127 | padding-bottom: 4em; 128 | } 129 | 130 | @zPlane1: 5; 131 | @zPlane2: 90; 132 | @zPlane3: 100; 133 | @zPlane4: 200; 134 | @zPlane5: 300; 135 | @zPlaneTop: 1000; 136 | 137 | @userThumbHeight: 228px; 138 | @userThumbWidth: 228px; 139 | @panelHandleWidth: 7px; 140 | @tabs_height: 30px; 141 | 142 | .dropdown { 143 | &:hover > ul.dropdown-menu { 144 | display: block; 145 | } 146 | ul.dropdown-menu { 147 | margin-top: -1px; 148 | } 149 | } 150 | 151 | .user-color-square { 152 | display: inline-block; 153 | height: 12px; 154 | width: 12px; 155 | margin: 0 5px 1px 2px; 156 | vertical-align: middle; 157 | border: 1px solid @thumbnailGrayShadow; 158 | opacity: 0.6; 159 | } 160 | 161 | .border-image (@color1, @color2) { 162 | border-image: -webkit-linear-gradient(-90deg, @color1, @color2) 20 stretch; 163 | border-image: -moz-linear-gradient(-90deg, @color1, @color2) 20 stretch; 164 | border-image: -ms-linear-gradient(-90deg, @color1, @color2) 20 stretch; 165 | border-image: -linear-gradient(-90deg, @color1, @color2) 20 stretch; 166 | } 167 | -------------------------------------------------------------------------------- /templates/chat.js: -------------------------------------------------------------------------------- 1 | /** @jsx React.DOM */ 2 | "use strict"; 3 | 4 | const React = require('react-atom-fork'); 5 | const utils = require("../common/utils"); 6 | const floop = require("../common/floop"); 7 | const flux = require("flukes"); 8 | const message_action = require("../common/message_action"); 9 | 10 | module.exports = React.createClass({ 11 | mixins: [flux.createAutoBinder(['msgs'])], 12 | handleMessage_: function (event) { 13 | event.preventDefault(); 14 | const input = this.refs.newMessage.getDOMNode(); 15 | let txt = input.value; 16 | input.value = ""; 17 | const ret = floop.send_msg({data: txt}); 18 | if (ret) { 19 | const error = ret.message || ret.toString(); 20 | console.error(error); 21 | txt = error; 22 | } 23 | message_action.user(this.props.username, txt, Date.now()); 24 | }, 25 | componentDidMount: function () { 26 | this.focus(); 27 | }, 28 | focus: function () { 29 | this.refs.newMessage.getDOMNode().focus(); 30 | }, 31 | render: function () { 32 | const msgs = this.props.msgs.map(function (msg) { 33 | const userColor = utils.user_color(msg.username); 34 | return ( 35 |
36 |
37 |
{msg.prettyTime}
38 |
39 | 40 | 41 | {msg.username}:  42 | 43 | {msg.data} 44 |
45 |
46 |
47 | ); 48 | }); 49 | return ( 50 |
54 |
55 |
56 | 57 |
58 |
59 |
60 | {msgs} 61 |
62 |
63 | ); 64 | } 65 | }); 66 | -------------------------------------------------------------------------------- /templates/code_review.js: -------------------------------------------------------------------------------- 1 | /** @jsx React.DOM */ 2 | 3 | "use strict"; 4 | 5 | const React = require('react-atom-fork'); 6 | const mixins = require("./mixins"); 7 | const atomUtils = require("../atom_utils"); 8 | 9 | const CodeReview = React.createClass({ 10 | mixins: [mixins.ReactUnwrapper, mixins.FormMixin], 11 | onSubmit: function (state) { 12 | const cb = this.props.cb.bind({}, null, state, this.refs.description.getDOMNode().value); 13 | setTimeout(cb, 0); 14 | this.destroy(); 15 | }, 16 | componentDidMount: function () { 17 | this.refs.description.getDOMNode().focus(); 18 | }, 19 | render: function () { 20 | return ( 21 |
22 |

Code Review

23 |
24 |
25 |
26 | Please describe your problem. A human will look at your code and try to help you. 27 | 29 |
30 |
31 | 32 |
33 |
34 |
35 | 36 |   37 | 38 |
39 |
40 |
41 |
42 | ); 43 | } 44 | }); 45 | 46 | module.exports = function (cb) { 47 | const view = CodeReview({cb: cb}); 48 | atomUtils.addModalPanel('code-review', view); 49 | }; -------------------------------------------------------------------------------- /templates/conflicts.js: -------------------------------------------------------------------------------- 1 | /** @jsx React.DOM */ 2 | /*global fl */ 3 | "use strict"; 4 | const fs = require("fs"); 5 | const path = require("path"); 6 | 7 | const _ = require("lodash"); 8 | const $ = require('atom-space-pen-views').$; 9 | const React = require('react-atom-fork'); 10 | 11 | const floop = require("../common/floop"); 12 | const utils = require("../common/utils"); 13 | 14 | module.exports = React.createClass({ 15 | treeize_: function (obj) { 16 | let node = {}; 17 | let tree = {}; 18 | _.each(obj, function (p) { 19 | node = tree; 20 | p.split(path.sep).forEach(function (p) { 21 | if (p in node) { 22 | node = node[p]; 23 | return; 24 | } 25 | node[p] = {}; 26 | node = node[p]; 27 | }); 28 | }); 29 | return tree; 30 | }, 31 | getInitialState: function () { 32 | return { 33 | enabled: true, 34 | clicked: "", 35 | totalFiles: _.size(this.props.different) + _.size(this.props.newFiles) + _.size(this.props.missing), 36 | missing: new Set(), 37 | different: new Set(), 38 | newFiles: new Set(), 39 | }; 40 | }, 41 | componentDidMount: function () { 42 | if (this.props.justUpload) { 43 | const upload = this.remote_; 44 | setTimeout(function () { 45 | upload(); 46 | }, 0); 47 | } 48 | 49 | const local = this.refs.local; 50 | if (!local) { 51 | return; 52 | } 53 | $(local.getDOMNode()).focus(); 54 | }, 55 | onClick: function (id) { 56 | console.log(id); 57 | }, 58 | remote_: function () { 59 | this.setState({enabled: false}); 60 | _.each(this.props.different, (b, id) => { 61 | let encoding = b.encoding || "utf8"; 62 | floop.send_set_buf({ 63 | id, encoding, 64 | buf: b.txt.toString(encoding), 65 | md5: b.md5, 66 | }, null, (err) => { 67 | if (!err) { 68 | this.setState({different: this.state.different.add(id)}); 69 | floop.send_saved({id: id}); 70 | } 71 | }); 72 | }); 73 | 74 | _.each(this.props.missing, (b, id) => { 75 | floop.send_delete_buf({id}, null, () => { 76 | // TODO: check err 77 | this.setState({missing: this.state.missing.add(id)}); 78 | }); 79 | }); 80 | 81 | _.each(this.props.newFiles, (b, rel) => { 82 | fs.readFile(b.path, (err, data) => { 83 | if (err) { 84 | console.log(err); 85 | return; 86 | } 87 | 88 | const encoding = utils.is_binary(data, data.length) ? "base64" : "utf8"; 89 | floop.send_create_buf({ 90 | path: rel, 91 | buf: data.toString(encoding), 92 | encoding: encoding, 93 | md5: utils.md5(data), 94 | }, null, () => { 95 | this.setState({newFiles: this.state.newFiles.add(rel)}); 96 | }); 97 | }); 98 | }); 99 | this.props.onHandledConflicts({}); 100 | }, 101 | local_: function () { 102 | this.setState({ 103 | enabled: false, 104 | newFiles: new Set(_.keys(this.props.newFiles)), 105 | }); 106 | _.each(this.props.missing, (b, id) => { 107 | floop.send_get_buf(id, () => this.setState({missing: this.state.missing.add(id)})); 108 | }); 109 | _.each(this.props.different, (b, id) => { 110 | floop.send_get_buf(id, () => this.setState({different: this.state.different.add(id)})); 111 | }); 112 | const toFetch = _.merge({}, this.props.missing, this.props.different); 113 | this.props.onHandledConflicts(toFetch); 114 | }, 115 | cancel_: function () { 116 | this.setState({enabled: false}); 117 | require("../floobits").leave_workspace(); 118 | }, 119 | render_: function (title, name) { 120 | const items = this.props[name]; 121 | const completed = this.state[name]; 122 | if (!_.size(items)) { 123 | return ""; 124 | } 125 | return ( 126 |
127 |

{title}

128 |
    129 | { 130 | _.map(items, (b, id) => { 131 | const path = b.path; 132 | const checked = completed.has(id) ? "✓" : ""; 133 | return (
  1. {path}  {checked}
  2. ); 134 | }) 135 | } 136 |
137 |
138 | ); 139 | }, 140 | render_progress: function () { 141 | if (this.state.enabled) { 142 | return false; 143 | } 144 | const state = this.state; 145 | const width = parseInt((state.different.size + state.newFiles.size + state.missing.size) / state.totalFiles * 100, 10); 146 | const progressWidth = `${width}%`; 147 | return ( 148 |
149 |
150 | {progressWidth} 151 |
152 |
153 |

154 | All done syncing files! 155 |

156 |
157 | ); 158 | }, 159 | render_created_workspace: function () { 160 | const newFiles = this.render_("Uploading:", "newFiles"); 161 | const progress = this.render_progress(); 162 | return (
163 |

Created {fl.floourl ? fl.floourl.toString() : "the workspace"}

164 | { progress } 165 | { newFiles } 166 |
); 167 | }, 168 | render_conflicts: function () { 169 | const missing = this.render_("Missing", "missing"); 170 | const different = this.render_("Different", "different"); 171 | const newFiles = this.render_("New", "newFiles"); 172 | const ignored = _.map(this.props.ignored, function (p) { 173 | return
  • {p}
  • ; 174 | }); 175 | 176 | const tooBig = _.map(this.props.tooBig, function (size, p) { 177 | return
  • {p} {utils.formatBytes(size)}
  • ; 178 | }); 179 | 180 | const state = this.state; 181 | const progress = this.render_progress(); 182 | 183 | return (
    184 |

    Your local files are different from the workspace.

    185 | 186 | 187 | 188 | 189 | {progress} 190 | 191 | {missing} 192 | {different} 193 | {newFiles} 194 | {!this.props.ignored.length ? "" : 195 |
    196 |

    Ignored

    197 |
      198 | { ignored } 199 |
    200 |
    201 | } 202 | {!tooBig.length ? "" : 203 |
    204 |

    Too Big

    205 |
      206 | { tooBig } 207 |
    208 |
    209 | } 210 |
    ); 211 | }, 212 | render: function () { 213 | const body = this.props.justUpload ? this.render_created_workspace() : this.render_conflicts(); 214 | return ( 215 |
    216 | {body} 217 |
    218 | ); 219 | } 220 | }); 221 | -------------------------------------------------------------------------------- /templates/handle_request_perm.js: -------------------------------------------------------------------------------- 1 | /** @jsx React.DOM */ 2 | 3 | "use strict"; 4 | 5 | const React = require('react-atom-fork'); 6 | const floop = require("../common/floop"); 7 | const permsEvent = {}; 8 | 9 | const HandleRequestPermView = React.createClass({ 10 | destroy: function () { 11 | this.getDOMNode().parentNode.destroy(); 12 | }, 13 | grant: function () { 14 | permsEvent.action = "add"; 15 | this.send(); 16 | }, 17 | deny: function () { 18 | permsEvent.action = "reject"; 19 | this.send(); 20 | }, 21 | send: function () { 22 | floop.send_perms(permsEvent); 23 | this.destroy(); 24 | }, 25 | render: function () { 26 | permsEvent.user_id = this.props.userId; 27 | permsEvent.perms = this.props.perms; 28 | return ( 29 |
    30 |
    31 |

    {this.props.username} wants to edit this workspace

    32 |
    33 |
    34 |
    35 |
    36 | 37 | 38 | 39 |
    40 |
    41 |
    42 |
    43 | ); 44 | } 45 | }); 46 | 47 | module.exports = HandleRequestPermView; 48 | -------------------------------------------------------------------------------- /templates/join.js: -------------------------------------------------------------------------------- 1 | /** @jsx React.DOM */ 2 | 3 | "use strict"; 4 | 5 | const $ = require('atom-space-pen-views').$; 6 | const React = require('react-atom-fork'); 7 | const utils = require("../common/utils"); 8 | const mixins = require("./mixins"); 9 | 10 | const JoinWorkspace = React.createClass({ 11 | mixins: [mixins.ReactUnwrapper, mixins.FormMixin], 12 | getInitialState: function () { 13 | return { 14 | path: this.props.path, 15 | url: this.props.url, 16 | }; 17 | }, 18 | onSubmit: function (event) { 19 | if (event) { 20 | event.preventDefault(); 21 | } 22 | setTimeout(function () { 23 | this.props.on_url(this.state.path, this.refs.url.getDOMNode().value); 24 | }.bind(this), 0); 25 | this.destroy(); 26 | }, 27 | onChange_: function (event) { 28 | const files = event.target.files; 29 | if (!files.length) { 30 | return; 31 | } 32 | const path = files[0].path; 33 | const state = {path: path}; 34 | if (this.refs.url.getDOMNode().value === this.props.url) { 35 | const dotFloo = utils.load_floo(path); 36 | if (dotFloo.url && dotFloo.url.length) { 37 | state.url = dotFloo.url; 38 | } 39 | } 40 | this.setState(state); 41 | }, 42 | onDidStuff: function () { 43 | if (this.state.path) { 44 | return; 45 | } 46 | $("#ultra-secret-hidden-file-input").attr("webkitdirectory", true); 47 | }, 48 | componentDidUpdate: function () { 49 | this.onDidStuff(); 50 | }, 51 | componentDidMount: function () { 52 | this.onDidStuff(); 53 | 54 | const that = this; 55 | setTimeout(function () { 56 | const url = that.refs.url.getDOMNode(); 57 | const length = url.value.length; 58 | url.setSelectionRange(length, length); 59 | url.focus(); 60 | }, 0); 61 | 62 | }, 63 | onTyping: function (event) { 64 | const path = event.target.value; 65 | if (path === this.state.path) { 66 | return; 67 | } 68 | this.setState({path: path}); 69 | }, 70 | focusFileInput: function () { 71 | if (this.state.path) { 72 | return; 73 | } 74 | $("#ultra-secret-hidden-file-input").trigger('click'); 75 | this.setState({showMessage: true}); 76 | }, 77 | updateURL: function (e) { 78 | this.setState({url: e.target.value}); 79 | }, 80 | render: function () { 81 | return ( 82 |
    83 |

    Join Workspace

    84 |
    85 |
    86 | 87 | 88 |
    89 |
    90 |
    91 | URL 92 | 93 |
    94 |
    95 |
    96 | 97 |
    98 |
    99 |
    100 | Select Directory 101 | 103 |
    104 |
    105 |
    106 | 107 | { this.props.path && 108 |
    109 |
    110 |

    111 | Atom's API for managing windows is currently broken. If you'd like to open the workspace in a different window, 112 | please open the window and then call floobits::join/create in that window. 113 |

    114 |
    115 |
    116 | } 117 |
    118 |
    119 | 120 | 121 |
    122 |
    123 |
    124 |
    125 |
    126 | ); 127 | } 128 | }); 129 | 130 | module.exports = JoinWorkspace; 131 | -------------------------------------------------------------------------------- /templates/messages_view.js: -------------------------------------------------------------------------------- 1 | /** @jsx React.DOM */ 2 | "use strict"; 3 | 4 | const flux = require("flukes"); 5 | const React = require('react-atom-fork'); 6 | const _ = require("lodash"); 7 | const utils = require("../common/utils"); 8 | const floop = require("../common/floop"); 9 | const messageAction = require("../common/message_action"); 10 | 11 | 12 | const LogMessageView = React.createClass({ 13 | render: function () { 14 | const message = this.props.message; 15 | let repeatCountHTML = ""; 16 | if (this.props.repeatCount > 0) { 17 | repeatCountHTML = ( 18 | x{this.props.repeatCount + 1} 19 | ); 20 | } 21 | return ( 22 |
    23 |
    24 |
    {message.prettyTime}
    25 |
    {message.msg} {repeatCountHTML}
    26 |
    27 |
    28 | ); 29 | } 30 | }); 31 | 32 | const UserMessageView = React.createClass({ 33 | getInitialState: function () { 34 | return { 35 | ignoredURLs: [], 36 | }; 37 | }, 38 | ignoreURL: function (url, event) { 39 | if (event) { 40 | event.stopPropagation(); 41 | event.preventDefault(); 42 | } 43 | const ignoredURLs = this.state.ignoredURLs; 44 | ignoredURLs.push(url); 45 | this.setState({ignoredURLs: ignoredURLs}); 46 | }, 47 | render: function () { 48 | var message = this.props.message, 49 | urlRegexp = /https?:\/\/\S+/g, 50 | userColor = utils.user_color(message.username), 51 | result, 52 | msgTxt = [], 53 | before, 54 | after, 55 | key = 0, 56 | prevIndex = 0; 57 | 58 | while (true) { 59 | result = urlRegexp.exec(message.msg); 60 | if (!result) { 61 | msgTxt.push(message.msg.slice(prevIndex)); 62 | break; 63 | } 64 | before = message.msg.slice(prevIndex, result.index); 65 | prevIndex = result.index + result[0].length; 66 | after = message.msg.slice(prevIndex, urlRegexp.lastIndex); 67 | let imgOrTxt = result[0]; 68 | if (this.state.ignoredURLs.indexOf(imgOrTxt) === -1 && utils.image_mime_from_extension(imgOrTxt)) { 69 | imgOrTxt = ( 70 |
    71 | 72 | 73 |
    ); 74 | } 75 | 76 | msgTxt.push( 77 | 78 | {before} 79 | {imgOrTxt} 80 | {after} 81 | 82 | ); 83 | } 84 | 85 | return ( 86 |
    87 |
    88 |
    {message.prettyTime}
    89 |
    90 | 91 | 92 | {message.username || message.type}:  93 | 94 | {msgTxt} 95 |
    96 |
    97 |
    98 | ); 99 | } 100 | }); 101 | 102 | const InteractiveMessageView = React.createClass({ 103 | getInitialState: function () { 104 | return { 105 | clicked: null 106 | }; 107 | }, 108 | onClick: function (button) { 109 | if (this.state.clicked !== null) { 110 | return; 111 | } 112 | button.action(); 113 | this.setState({clicked: button.id}); 114 | }, 115 | render: function () { 116 | var message = this.props.message, 117 | buttons = message.buttons || []; 118 | 119 | buttons = buttons.map(function (b) { 120 | var classes = "btn ", 121 | clicked = this.state.clicked; 122 | 123 | if (clicked === null || clicked === b.id) { 124 | classes += b.classNames.join(" "); 125 | } 126 | if (clicked === b.id) { 127 | classes += " dim"; 128 | } 129 | return ( 130 | 131 | ); 132 | }, this); 133 | 134 | return ( 135 |
    136 |
    137 |
    {message.prettyTime}
    138 |
    139 | {message.msg} 140 | {buttons.length && 141 |
    {buttons}
    142 | } 143 |
    144 |
    145 |
    146 | ); 147 | } 148 | }); 149 | 150 | const MessagesView = React.createClass({ 151 | mixins: [flux.createAutoBinder(["messages"])], 152 | handleMessage_: function (event) { 153 | event.preventDefault(); 154 | const input = this.refs.newMessage.getDOMNode(); 155 | const value = input.value; 156 | let ret = floop.send_msg({data: value}); 157 | if (ret) { 158 | ret = ret.message || ret.toString(); 159 | messageAction.error(ret, false); 160 | return; 161 | } 162 | input.value = ""; 163 | messageAction.user(this.props.username, value, Date.now() / 1000); 164 | }, 165 | componentDidMount: function () { 166 | // focus in chat but not editor proxy :( 167 | if (this.props.focus && this.refs.newMessage) { 168 | this.focus(); 169 | } 170 | }, 171 | getMessages: function () { 172 | const messages = []; 173 | let prevLogMessage = null; 174 | this.props.messages.forEach(function (message) { 175 | if (message.type !== "log") { 176 | prevLogMessage = null; 177 | messages.push({message}); 178 | return; 179 | } 180 | if (prevLogMessage === message.msg) { 181 | _.last(messages).repeatCount += 1; 182 | return; 183 | } 184 | messages.push({message, repeatCount: 0}); 185 | prevLogMessage = message.msg; 186 | }); 187 | return messages; 188 | }, 189 | focus: function () { 190 | this.refs.newMessage.getDOMNode().focus(); 191 | }, 192 | render: function () { 193 | let chatInput = ""; 194 | const nodes = this.getMessages().map(function (messageObj) { 195 | const message = messageObj.message; 196 | switch (message.type) { 197 | case "user": 198 | return ; 199 | case "log": 200 | return ; 201 | case "interactive": 202 | return ; 203 | default: 204 | console.error("Unknown message type:", message.type); 205 | break; 206 | } 207 | }, this); 208 | if (!this.props.hideChat) { 209 | chatInput = ( 210 |
    211 |
    212 | 213 |
    214 |
    215 | ); 216 | } 217 | 218 | return ( 219 |
    223 |
    224 | {chatInput} 225 |
    226 | {nodes} 227 |
    228 |
    229 |
    230 | ); 231 | } 232 | }); 233 | 234 | module.exports = { 235 | InteractiveMessageView, 236 | LogMessageView, 237 | MessagesView, 238 | UserMessageView, 239 | }; 240 | -------------------------------------------------------------------------------- /templates/mixins.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const ReactUnwrapper = { 4 | destroy: function (e) { 5 | if (e) { 6 | e.preventDefault(); 7 | } 8 | if (!this.isMounted()) { 9 | return; 10 | } 11 | this.getDOMNode().parentNode.destroy(); 12 | } 13 | }; 14 | 15 | const $ = require('atom-space-pen-views').$; 16 | 17 | const FormMixin = { 18 | componentDidMount: function () { 19 | const that = this; 20 | $(this.getDOMNode()).keydown(function (k) { 21 | switch (k.keyCode) { 22 | case 27: // escape 23 | that.destroy(k); 24 | return; 25 | case 13: // enter 26 | that.onSubmit(k); 27 | return; 28 | default: 29 | break; 30 | } 31 | }); 32 | }, 33 | componentWillUnmount: function () { 34 | $(this.getDOMNode()).off("keydown"); 35 | }, 36 | }; 37 | 38 | module.exports = { 39 | ReactUnwrapper: ReactUnwrapper, 40 | FormMixin: FormMixin, 41 | }; 42 | -------------------------------------------------------------------------------- /templates/pane.coffee: -------------------------------------------------------------------------------- 1 | {$, View} = require 'atom-space-pen-views' 2 | 3 | class PaneView extends View 4 | focus: -> @input.focus() 5 | initialize: (@pane) => 6 | @pane.setView @ 7 | this[0].appendChild(@pane.inner) 8 | 9 | detached: -> 10 | console.log "detached" 11 | @pane.onDetached?() 12 | 13 | @content: (params) -> 14 | @div 'style': "overflow: auto;", "class": "native-key-bindings" 15 | 16 | class Pane 17 | constructor: (@title, @iconName, @inner, @onDetached) -> 18 | getViewClass: -> PaneView 19 | getView: -> @view 20 | setView: (@view) -> 21 | getTitle: () => 22 | @title 23 | getIconName: () => 24 | @iconName 25 | 26 | module.exports = Pane 27 | -------------------------------------------------------------------------------- /templates/permission_view.js: -------------------------------------------------------------------------------- 1 | /** @jsx React.DOM */ 2 | "use strict"; 3 | 4 | var React = require('react-atom-fork'); 5 | var floop = require("../common/floop"); 6 | var mixins = require("./mixins"); 7 | var _ = require("lodash"); 8 | 9 | var PermissionView = React.createClass({ 10 | mixins: [mixins.ReactUnwrapper, mixins.FormMixin], 11 | getInitialState: function () { 12 | var permissions; 13 | permissions = this.props.user.permissions; 14 | return { 15 | admin_room: permissions.indexOf("kick") !== -1, 16 | view_room: permissions.indexOf("get_buf") !== -1, 17 | edit_room: permissions.indexOf("set_buf") !== -1, 18 | request_perms: permissions.indexOf("request_perms") !== -1, 19 | }; 20 | }, 21 | componentDidMount: function () { 22 | // TODO. this is a bootstrap-ism 23 | // this.props.user.on("change:connected", modal.killModal); 24 | return; 25 | }, 26 | renderField: function (label, name, description) { 27 | return ( 28 |
    29 | 30 | 31 |
    32 | {description} 33 |
    34 |
    35 | ); 36 | }, 37 | /** 38 | * @param {Event} event 39 | * @private 40 | */ 41 | onSubmit: function (event) { 42 | var newPerms; 43 | event.preventDefault(); 44 | newPerms = ["view_room", "edit_room", "request_perms", "admin_room"].filter(function (perm) { 45 | return this.state[perm]; 46 | }, this); 47 | floop.send_perms({ 48 | user_id: this.props.user.getConnectionID(), 49 | perms: newPerms, 50 | action: "set", 51 | }); 52 | this.destroy(); 53 | }, 54 | /** 55 | * @param {string} type 56 | * @param {Object} event 57 | * @private 58 | */ 59 | handleChange_: function (type, event) { 60 | var index, permissions, state, checked; 61 | permissions = ["view_room", "request_perms", "edit_room", "admin_room"]; 62 | checked = event.target.checked; 63 | index = permissions.indexOf(type); 64 | state = {}; 65 | 66 | permissions.forEach(function (perm, i) { 67 | if (i < index && checked) { 68 | state[perm] = checked; 69 | } else if (i > index && !checked) { 70 | state[perm] = checked; 71 | } else if (i === index) { 72 | state[perm] = checked; 73 | } else { 74 | state[perm] = this.state[perm]; 75 | } 76 | }, this); 77 | this.setState(state); 78 | }, 79 | render: function () { 80 | var fields = [ 81 | ["View", "view_room", "view files and terminals in this workspace"], 82 | ["Request permissions", "request_perms", "ask admins for permission to edit files in this workspace"], 83 | ["Edit", "edit_room", "edit files in this workspace"], 84 | ["Administer", "admin_room", "set permissions and type in all terminals"], 85 | ].map(function (perm) { 86 | return this.renderField.apply(this, perm); 87 | }, this); 88 | 89 | 90 | return ( 91 |
    92 |

    Permissions for {this.props.user.id}

    93 | 94 |
    95 |
    96 | {fields} 97 |
    98 |
    99 | 100 |
    101 |
    102 | 103 | 104 |
    105 |
    106 |
    107 | ); 108 | } 109 | }); 110 | 111 | module.exports = PermissionView; 112 | 113 | -------------------------------------------------------------------------------- /templates/status_bar.js: -------------------------------------------------------------------------------- 1 | /** @jsx React.DOM */ 2 | "use strict"; 3 | 4 | const React = require("react-atom-fork"); 5 | const flux = require("flukes"); 6 | const prefs = require("../common/userPref_model"); 7 | 8 | const floop = require("../common/floop"); 9 | 10 | const StatusBarView = React.createClass({ 11 | mixins: [flux.createAutoBinder(["me"], [prefs])], 12 | getInitialState: function () { 13 | return { 14 | conn_status: "Connecting...", 15 | }; 16 | }, 17 | componentWillMount: function () { 18 | const that = this; 19 | floop.onROOM_INFO(function () { 20 | that.setState({conn_status: "Connected."}); 21 | }); 22 | floop.onDISCONNECT(function () { 23 | that.setState({conn_status: "Disconnected."}); 24 | }); 25 | }, 26 | render: function () { 27 | let workspace = `${this.props.floourl.owner}/${this.props.floourl.workspace}`; 28 | let following_status; 29 | if (prefs.following) { 30 | following_status = " Following changes."; 31 | } else if (prefs.followUsers.length) { 32 | following_status = `Following ${prefs.followUsers.join(", ")}`; 33 | } 34 | return ( 35 |
    36 | {this.props.me.id}@{workspace}: {this.state.conn_status} {following_status} 37 |
    38 | ); 39 | } 40 | }); 41 | 42 | module.exports = StatusBarView; 43 | -------------------------------------------------------------------------------- /templates/user_list_pane.js: -------------------------------------------------------------------------------- 1 | /** @jsx React.DOM */ 2 | "use strict"; 3 | "use babel"; 4 | 5 | const React = require("react-atom-fork"); 6 | const mixins = require("./mixins"); 7 | const UserlistView = require("./user_view").UserlistView; 8 | 9 | const UserlistPane = React.createClass({ 10 | mixins: [mixins.ReactUnwrapper], 11 | render: function () { 12 | return ( 13 |
    14 |
    15 | Floobits 16 | 17 |
    18 | 19 |
    20 | ); 21 | } 22 | }); 23 | 24 | module.exports = UserlistPane; 25 | -------------------------------------------------------------------------------- /templates/webview.js: -------------------------------------------------------------------------------- 1 | /*global HTMLElement: true */ 2 | 3 | "use strict"; 4 | 5 | const _ = require("lodash"); 6 | const floorc = require("../common/floorc"); 7 | const message_action = require("../common/message_action"); 8 | const Pane = require("../../templates/pane"); 9 | 10 | const Proto = Object.create(HTMLElement.prototype); 11 | 12 | Proto.createdCallback = function () { 13 | const frame = document.createElement("webview"); 14 | frame.setAttribute("plugins", "on"); 15 | frame.setAttribute("disablewebsecurity", "on"); 16 | frame.setAttribute("allowPointerLock", 'on'); 17 | frame.setAttribute("allowfileaccessfromfiles", 'on'); 18 | frame.style.border = "0 none"; 19 | frame.style.width = "100%"; 20 | frame.style.height = "100%"; 21 | frame.className = "native-key-bindings"; 22 | 23 | frame.addEventListener("console-message",function (e) { 24 | let m = e.message; 25 | const prefix = "::atom-floobits-ipc::"; 26 | if (!m.startsWith(prefix)) { 27 | return; 28 | } 29 | m = m.slice(prefix.length); 30 | console.log(m); 31 | try { 32 | m = JSON.parse(m); 33 | } catch (e) { 34 | return; 35 | } 36 | this.onCredentials(m.auth); 37 | return; 38 | }.bind(this)); 39 | this.frame = frame; 40 | }; 41 | 42 | Proto.onCredentials = function (blob) { 43 | if (!_.has(floorc, "auth")) { 44 | floorc.auth = {}; 45 | } 46 | 47 | const host = _.keys(blob)[0]; 48 | const auth_data = blob[host]; 49 | 50 | if (!_.has(floorc.auth, host)) { 51 | floorc.auth[host] = {}; 52 | } 53 | 54 | const floorc_auth = floorc.auth[host]; 55 | 56 | floorc_auth.username = auth_data.username; 57 | floorc_auth.api_key = auth_data.api_key; 58 | floorc_auth.secret = auth_data.secret; 59 | 60 | try { 61 | floorc.__write(); 62 | } catch (e) { 63 | return message_action.error(e); 64 | } 65 | }; 66 | 67 | Proto.load = function (host) { 68 | this.host = host; 69 | this.pane = new Pane("Floobits", "", this); 70 | atom.workspace.getActivePane().activateItem(this.pane); 71 | this.frame.src = `https://${host}/signup/atom`; 72 | }; 73 | 74 | Proto.attachedCallback = function () { 75 | this.className = "floobits-nativize"; 76 | this.appendChild(this.frame); 77 | }; 78 | 79 | Proto.detachedCallback = function () { 80 | this.destroy(); 81 | }; 82 | 83 | Proto.destroy = function () { 84 | if (!this.pane) { 85 | return; 86 | } 87 | try { 88 | this.pane.destroy(); 89 | } catch (e) { 90 | console.warn(e); 91 | } 92 | this.pane = null; 93 | }; 94 | 95 | module.exports = document.registerElement("floobits-welcome_web_view", {prototype: Proto}); 96 | -------------------------------------------------------------------------------- /templates/yes_no_cancel.js: -------------------------------------------------------------------------------- 1 | /** @jsx React.DOM */ 2 | 3 | "use strict"; 4 | 5 | const React = require('react-atom-fork'); 6 | const mixins = require("./mixins"); 7 | const atomUtils = require("../atom_utils"); 8 | 9 | const YesNoCancel = React.createClass({ 10 | mixins: [mixins.ReactUnwrapper, mixins.FormMixin], 11 | onSubmit: function (type) { 12 | if (!type) { 13 | type = "yes"; 14 | } else if (type.target) { 15 | type = type.target.name; 16 | } 17 | const cb = this.props.cb.bind({}, null, type); 18 | setTimeout(cb, 0); 19 | this.destroy(); 20 | }, 21 | componentDidMount: function () { 22 | this.refs.yes.getDOMNode().focus(); 23 | }, 24 | render: function () { 25 | const yes = this.props.yes || "Yes"; 26 | const no = this.props.no || "No"; 27 | const cancel = this.props.cancel || "Cancel"; 28 | return ( 29 |
    30 |

    {this.props.title}

    31 |
    32 |
    33 |
    34 |

    35 | {this.props.body} 36 |

    37 |
    38 |
    39 | 40 |
    41 |
    42 | 43 |
    44 |
    45 | 46 |   47 | 48 |
    49 |
    50 |
    51 |
    52 | ); 53 | } 54 | }); 55 | 56 | module.exports = function (title, body, opts, cb) { 57 | if (!cb) { 58 | cb = opts; 59 | opts = {}; 60 | } 61 | 62 | opts.title = title; 63 | opts.body = body; 64 | opts.cb = cb; 65 | 66 | const view = YesNoCancel(opts); 67 | atomUtils.addModalPanel('yes-no-cancel', view); 68 | }; --------------------------------------------------------------------------------