From 4ae0d8af9f88a6687c41dd4d8aae1a89fa4081d5 Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Sat, 6 Jul 2013 13:54:44 -0700 Subject: [PATCH 01/62] Use tantamount for _.isEqual() implementation This code was needed in telepath so it was pulled out as a standalone module with the exact same behavior as was previously in Atom. --- package.json | 1 + src/stdlib/underscore-extensions.coffee | 50 +------------------------ 2 files changed, 2 insertions(+), 49 deletions(-) diff --git a/package.json b/package.json index 2779ef254..5c71491d4 100644 --- a/package.json +++ b/package.json @@ -34,6 +34,7 @@ "humanize-plus": "1.1.0", "semver": "1.1.4", "guid": "0.0.10", + "tantamount": "0.3.0", "c-tmbundle": "1.0.0", "coffee-script-tmbundle": "2.0.0", "css-tmbundle": "1.0.0", diff --git a/src/stdlib/underscore-extensions.coffee b/src/stdlib/underscore-extensions.coffee index 55a9d1435..4eac66b8d 100644 --- a/src/stdlib/underscore-extensions.coffee +++ b/src/stdlib/underscore-extensions.coffee @@ -180,52 +180,4 @@ _.mixin newObject[key] = value if value? newObject -originalIsEqual = _.isEqual -extendedIsEqual = (a, b, aStack=[], bStack=[]) -> - return originalIsEqual(a, b) if a is b - return originalIsEqual(a, b) if _.isFunction(a) or _.isFunction(b) - return a.isEqual(b) if _.isFunction(a?.isEqual) - return b.isEqual(a) if _.isFunction(b?.isEqual) - - stackIndex = aStack.length - while stackIndex-- - return bStack[stackIndex] is b if aStack[stackIndex] is a - aStack.push(a) - bStack.push(b) - - equal = false - if _.isArray(a) and _.isArray(b) and a.length is b.length - equal = true - for aElement, i in a - unless extendedIsEqual(aElement, b[i], aStack, bStack) - equal = false - break - else if _.isObject(a) and _.isObject(b) - aCtor = a.constructor - bCtor = b.constructor - aCtorValid = _.isFunction(aCtor) and aCtor instanceof aCtor - bCtorValid = _.isFunction(bCtor) and bCtor instanceof bCtor - if aCtor isnt bCtor and not (aCtorValid and bCtorValid) - equal = false - else - aKeyCount = 0 - equal = true - for key, aValue of a - continue unless _.has(a, key) - aKeyCount++ - unless _.has(b, key) and extendedIsEqual(aValue, b[key], aStack, bStack) - equal = false - break - if equal - bKeyCount = 0 - for key, bValue of b - bKeyCount++ if _.has(b, key) - equal = aKeyCount is bKeyCount - else - equal = originalIsEqual(a, b) - - aStack.pop() - bStack.pop() - equal - -_.isEqual = (a, b) -> extendedIsEqual(a, b) +_.isEqual = require 'tantamount' From adebae6c470d4344d28e3741ad29cda8898e319b Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Sat, 6 Jul 2013 14:03:13 -0700 Subject: [PATCH 02/62] Remove jasmine.js lines from displayed stack trace This removes the noise from the setup and compare lines that occur in jasmine.js and only displays the lines that are generated from within the failing spec. --- spec/atom-reporter.coffee | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/spec/atom-reporter.coffee b/spec/atom-reporter.coffee index 5636647ca..90e15106e 100644 --- a/spec/atom-reporter.coffee +++ b/spec/atom-reporter.coffee @@ -166,13 +166,26 @@ class SpecResultView extends View @description.html @spec.description for result in @spec.results().getItems() when not result.passed() + stackTrace = @formatStackTrace(result.trace.stack) @specFailures.append $$ -> @div result.message, class: 'resultMessage fail' - @div result.trace.stack, class: 'stackTrace' if result.trace.stack + @div stackTrace, class: 'stackTrace' if stackTrace attach: -> @parentSuiteView().append this + formatStackTrace: (stackTrace) -> + return stackTrace unless stackTrace + + jasminePath = require.resolve('jasmine') + jasminePattern = new RegExp("\\(#{_.escapeRegExp(jasminePath)}:\\d+:\\d+\\)\\s*$") + convertedLines = [] + for line in stackTrace.split('\n') + unless jasminePattern.test(line) + convertedLines.push(line) + + convertedLines.join('\n') + parentSuiteView: -> if not suiteView = $(".suite-view-#{@spec.suite.id}").view() suiteView = new SuiteResultView(@spec.suite) From e34d8e4c42b9c23bb8b95385c21b8e4abbcaacd8 Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Sat, 6 Jul 2013 14:15:04 -0700 Subject: [PATCH 03/62] Display CoffeeScript line numbers in stack traces Use coffeestack to convert stack traces to have CoffeeScript line and column numbers in the output instead of JavaScript line and column numbers. --- package.json | 1 + spec/atom-reporter.coffee | 30 ++++++++++++++++-------------- 2 files changed, 17 insertions(+), 14 deletions(-) diff --git a/package.json b/package.json index 5c71491d4..c80db2ffd 100644 --- a/package.json +++ b/package.json @@ -35,6 +35,7 @@ "semver": "1.1.4", "guid": "0.0.10", "tantamount": "0.3.0", + "coffeestack": "0.4.0", "c-tmbundle": "1.0.0", "coffee-script-tmbundle": "2.0.0", "css-tmbundle": "1.0.0", diff --git a/spec/atom-reporter.coffee b/spec/atom-reporter.coffee index 90e15106e..106f71519 100644 --- a/spec/atom-reporter.coffee +++ b/spec/atom-reporter.coffee @@ -1,6 +1,19 @@ $ = require 'jquery' {View, $$} = require 'space-pen' _ = require 'underscore' +{convertStackTrace} = require 'coffeestack' + +sourceMaps = {} +formatStackTrace = (stackTrace) -> + return stackTrace unless stackTrace + + jasminePath = require.resolve('jasmine') + jasminePattern = new RegExp("\\(#{_.escapeRegExp(jasminePath)}:\\d+:\\d+\\)\\s*$") + convertedLines = [] + for line in stackTrace.split('\n') + convertedLines.push(line) unless jasminePattern.test(line) + + convertStackTrace(convertedLines.join('\n'), sourceMaps) module.exports = class AtomReporter extends View @@ -42,6 +55,7 @@ class AtomReporter extends View reportSpecResults: (spec) -> @completeSpecCount++ + spec.endedAt = new Date().getTime() @specComplete(spec) @updateStatusView(spec) @@ -99,7 +113,7 @@ class AtomReporter extends View rootSuite = rootSuite.parentSuite while rootSuite.parentSuite @message.text rootSuite.description - time = "#{Math.round((new Date().getTime() - @startedAt.getTime()) / 10)}" + time = "#{Math.round((spec.endedAt - @startedAt.getTime()) / 10)}" time = "0#{time}" if time.length < 3 @time.text "#{time[0...-2]}.#{time[-2..]}s" @@ -166,7 +180,7 @@ class SpecResultView extends View @description.html @spec.description for result in @spec.results().getItems() when not result.passed() - stackTrace = @formatStackTrace(result.trace.stack) + stackTrace = formatStackTrace(result.trace.stack) @specFailures.append $$ -> @div result.message, class: 'resultMessage fail' @div stackTrace, class: 'stackTrace' if stackTrace @@ -174,18 +188,6 @@ class SpecResultView extends View attach: -> @parentSuiteView().append this - formatStackTrace: (stackTrace) -> - return stackTrace unless stackTrace - - jasminePath = require.resolve('jasmine') - jasminePattern = new RegExp("\\(#{_.escapeRegExp(jasminePath)}:\\d+:\\d+\\)\\s*$") - convertedLines = [] - for line in stackTrace.split('\n') - unless jasminePattern.test(line) - convertedLines.push(line) - - convertedLines.join('\n') - parentSuiteView: -> if not suiteView = $(".suite-view-#{@spec.suite.id}").view() suiteView = new SuiteResultView(@spec.suite) From b3b501ef07bbfc9c40a7fb52ebfe887865ec9876 Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Sat, 6 Jul 2013 14:19:25 -0700 Subject: [PATCH 04/62] Set stack trace overflow to auto This keeps the stack trace text inside the spec box when the window in narrower than the stack trace lines. --- static/jasmine.less | 1 + 1 file changed, 1 insertion(+) diff --git a/static/jasmine.less b/static/jasmine.less index 33e886f45..310d15669 100644 --- a/static/jasmine.less +++ b/static/jasmine.less @@ -158,4 +158,5 @@ body { border: 1px solid #ddd; background: white; white-space: pre; + overflow: auto; } From acb69a42211d35e7839e3c35b5e7f507752b5ab6 Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Sat, 6 Jul 2013 14:29:16 -0700 Subject: [PATCH 05/62] Format stack traces in console reporter This generates valid CoffeeScript line/column numbers and also removes noisy lines from jasmine.js and from the grunt stack portion for the initial test process spawn. --- vendor/jasmine-console-reporter.js | 30 +++++++++++++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/vendor/jasmine-console-reporter.js b/vendor/jasmine-console-reporter.js index 3d1faad26..28dbcfded 100644 --- a/vendor/jasmine-console-reporter.js +++ b/vendor/jasmine-console-reporter.js @@ -1,3 +1,31 @@ +var _ = require('underscore'); +var convertStackTrace = require('coffeestack').convertStackTrace; + +var sourceMaps = {}; +var formatStackTrace = function(stackTrace) { + if (!stackTrace) + return stackTrace; + + // Remove all lines containing jasmine.js path + var jasminePath = require.resolve('jasmine'); + var jasminePattern = new RegExp("\\(" + _.escapeRegExp(jasminePath) + ":\\d+:\\d+\\)\\s*$"); + var convertedLines = []; + var lines = stackTrace.split('\n'); + for (var i = 0; i < lines.length; i++) + if (!jasminePattern.test(lines[i])) + convertedLines.push(lines[i]); + + //Remove last util.spawn.callDone line and all lines after it + var gruntSpawnPattern = /^\s*at util\.spawn\.callDone\s*\(.*\/grunt\/util\.js:\d+:\d+\)\s*$/ + for (var i = convertedLines.length - 1; i > 0; i--) + if (gruntSpawnPattern.test(convertedLines[i])) { + convertedLines = convertedLines.slice(0, i); + break; + } + + return convertStackTrace(convertedLines.join('\n'), sourceMaps); +} + jasmine.ConsoleReporter = function(doc, logErrors) { this.logErrors = logErrors == false ? false : true }; @@ -35,7 +63,7 @@ jasmine.ConsoleReporter.prototype.reportSpecResults = function(spec) { console.log("\n\n" + message) console.log((new Array(message.length + 1)).join('-')) if (result.trace.stack) { - console.log(result.trace.stack) + console.log(formatStackTrace(result.trace.stack)); } else { console.log(result.message) From 3366c4d6a5e4e99c74b4d7e5bd8fbb8caaf2f4ee Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Mon, 8 Jul 2013 11:29:28 -0700 Subject: [PATCH 06/62] Remove node_modules before bootstrapping --- script/constructicon/build | 1 + 1 file changed, 1 insertion(+) diff --git a/script/constructicon/build b/script/constructicon/build index 8abf2180f..e3d40044b 100755 --- a/script/constructicon/build +++ b/script/constructicon/build @@ -6,6 +6,7 @@ set -ex # xcode cd "$(dirname "$0")/../.." +rm -fr node_modules ./script/bootstrap ./node_modules/.bin/grunt --build-dir="$BUILT_PRODUCTS_DIR" deploy From 2efbe9ce4e1a15cc9e36d98343467a23bd1c18d1 Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Mon, 8 Jul 2013 11:29:46 -0700 Subject: [PATCH 07/62] Run partial-clean during deploy task --- Gruntfile.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gruntfile.coffee b/Gruntfile.coffee index 2d0e1da1a..10e3fae1a 100644 --- a/Gruntfile.coffee +++ b/Gruntfile.coffee @@ -113,5 +113,5 @@ module.exports = (grunt) -> grunt.registerTask('compile', ['coffee', 'less', 'cson']) grunt.registerTask('lint', ['coffeelint', 'csslint', 'lesslint']) grunt.registerTask('ci', ['lint', 'partial-clean', 'update-atom-shell', 'build', 'test']) - grunt.registerTask('deploy', ['update-atom-shell', 'build', 'codesign']) + grunt.registerTask('deploy', ['partial-clean', 'update-atom-shell', 'build', 'codesign']) grunt.registerTask('default', ['update-atom-shell', 'build', 'set-development-version', 'install']) From f1bdcaedc18b3f7aa9060bd1dae58a2891a7cd97 Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Mon, 8 Jul 2013 17:06:16 -0700 Subject: [PATCH 08/62] Support moving up/down in tree view with k/j keys --- src/packages/tree-view/keymaps/tree-view.cson | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/packages/tree-view/keymaps/tree-view.cson b/src/packages/tree-view/keymaps/tree-view.cson index 5dd1a928c..1be64e05c 100644 --- a/src/packages/tree-view/keymaps/tree-view.cson +++ b/src/packages/tree-view/keymaps/tree-view.cson @@ -12,6 +12,8 @@ 'a': 'tree-view:add' 'delete': 'tree-view:remove' 'backspace': 'tree-view:remove' + 'k': 'core:move-up' + 'j': 'core:move-down' '.tree-view-dialog .mini.editor': 'enter': 'core:confirm' From ed1f51b987d0f18d67720aca89759923ceb30add Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Mon, 8 Jul 2013 18:57:58 -0700 Subject: [PATCH 09/62] Map k/j to up/down in archive-view --- src/packages/archive-view/keymaps/archive-view.cson | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 src/packages/archive-view/keymaps/archive-view.cson diff --git a/src/packages/archive-view/keymaps/archive-view.cson b/src/packages/archive-view/keymaps/archive-view.cson new file mode 100644 index 000000000..6369a1bae --- /dev/null +++ b/src/packages/archive-view/keymaps/archive-view.cson @@ -0,0 +1,3 @@ +'.archive-view': + 'k': 'core:move-up' + 'j': 'core:move-down' From 0fee962faa989ecd6a479a184b9b4b9ef6d12ca9 Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Tue, 9 Jul 2013 09:22:35 -0700 Subject: [PATCH 10/62] Mention j/k support --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 16da93e55..37ce8e2e9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,5 @@ +* Added: j/k now can be used to navigate the tree view and archive editor + * Fixed: Atom can now be launched when ~/.atom/config.cson doesn't exist * Added: Initial collaboration sessions * Fixed: Empty lines being deleted via uppercase/downcase command From 753b11cf15b7302efd5cd8884f107478f53f48d6 Mon Sep 17 00:00:00 2001 From: Cheng Zhao Date: Wed, 10 Jul 2013 18:57:26 +0800 Subject: [PATCH 11/62] Register Atom to handle atom:// scheme URLs. --- resources/mac/atom-Info.plist | 11 +++++++++++ src/main.coffee | 8 ++++++++ 2 files changed, 19 insertions(+) diff --git a/resources/mac/atom-Info.plist b/resources/mac/atom-Info.plist index 9fa80ea8e..d3865ab50 100644 --- a/resources/mac/atom-Info.plist +++ b/resources/mac/atom-Info.plist @@ -36,6 +36,17 @@ speakeasy.pem SUScheduledCheckInterval 3600 + CFBundleURLTypes + + + CFBundleURLSchemes + + atom + + CFBundleURLName + Atom Shared Session Protocol + + CFBundleDocumentTypes diff --git a/src/main.coffee b/src/main.coffee index d2f2bd05b..69f1c8115 100644 --- a/src/main.coffee +++ b/src/main.coffee @@ -7,6 +7,7 @@ fs = require 'fs' path = require 'path' optimist = require 'optimist' nslog = require 'nslog' +dialog = require 'dialog' _ = require 'underscore' console.log = (args...) -> @@ -21,6 +22,13 @@ delegate.browserMainParts.preMainMessageLoopRun = -> event.preventDefault() args.pathsToOpen.push(filePath) + app.on 'open-url', (event, url) => + event.preventDefault() + dialog.showMessageBox + message: 'Atom opened with URL' + detail: url + buttons: ['OK'] + app.on 'open-file', addPathToOpen app.on 'will-finish-launching', -> From 12580bce83926c7ad6535bfa9d85559f0cd951b3 Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Wed, 10 Jul 2013 08:54:15 -0700 Subject: [PATCH 12/62] Guard against missing file path in getScore() --- CHANGELOG.md | 2 ++ spec/app/syntax-spec.coffee | 6 ++++++ src/app/text-mate-grammar.coffee | 5 ++--- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 37ce8e2e9..b642ddc91 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,5 @@ +* Fixed: Error selecting a grammar for an untitled editor + * Added: j/k now can be used to navigate the tree view and archive editor * Fixed: Atom can now be launched when ~/.atom/config.cson doesn't exist diff --git a/spec/app/syntax-spec.coffee b/spec/app/syntax-spec.coffee index 497bb88ff..0cca30ed1 100644 --- a/spec/app/syntax-spec.coffee +++ b/spec/app/syntax-spec.coffee @@ -75,6 +75,12 @@ describe "the `syntax` global", -> expect(syntax.selectGrammar('more.test', '')).toBe grammar1 + describe "when there is no file path", -> + it "does not throw an exception (regression)", -> + expect(-> syntax.selectGrammar(null, '#!/usr/bin/ruby')).not.toThrow() + expect(-> syntax.selectGrammar(null, '')).not.toThrow() + expect(-> syntax.selectGrammar(null, null)).not.toThrow() + describe ".removeGrammar(grammar)", -> it "removes the grammar, so it won't be returned by selectGrammar", -> grammar = syntax.selectGrammar('foo.js') diff --git a/src/app/text-mate-grammar.coffee b/src/app/text-mate-grammar.coffee index 5796631b0..ce5e0c60b 100644 --- a/src/app/text-mate-grammar.coffee +++ b/src/app/text-mate-grammar.coffee @@ -74,11 +74,10 @@ class TextMateGrammar getScore: (filePath, contents) -> contents = fsUtils.read(filePath) if not contents? and fsUtils.isFileSync(filePath) - if syntax.grammarOverrideForPath(filePath) is @scopeName - 2 + filePath.length + 2 + (filePath?.length ? 0) else if @matchesContents(contents) - 1 + filePath.length + 1 + (filePath?.length ? 0) else @getPathScore(filePath) From 94e2dbbc2caf266c3c17e0112c921eb8a44d82f9 Mon Sep 17 00:00:00 2001 From: Coby Chapple Date: Thu, 11 Jul 2013 10:20:29 +0100 Subject: [PATCH 13/62] extraneous paren in package docs --- docs/packages/authoring-packages.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/packages/authoring-packages.md b/docs/packages/authoring-packages.md index 6e6c1af7a..f174a276d 100644 --- a/docs/packages/authoring-packages.md +++ b/docs/packages/authoring-packages.md @@ -29,7 +29,7 @@ on creating your first package. ## package.json -Similar to [npm packages](http://en.wikipedia.org/wiki/Npm_(software)), Atom packages +Similar to [npm packages](http://en.wikipedia.org/wiki/Npm_(software), Atom packages can contain a _package.json_ file in their top-level directory. This file contains metadata about the package, such as the path to its "main" module, library dependencies, and manifests specifying the order in which its resources should be loaded. From 16095c80864deb1c22704b679d84765deb771f98 Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Thu, 11 Jul 2013 20:22:43 -0700 Subject: [PATCH 14/62] Add New Window to File menu Closes #626 --- CHANGELOG.md | 2 ++ src/atom-application.coffee | 1 + 2 files changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b642ddc91..8a770bcf5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,5 @@ +* Fixed: cmd-n now works when no windows are open + * Fixed: Error selecting a grammar for an untitled editor * Added: j/k now can be used to navigate the tree view and archive editor diff --git a/src/atom-application.coffee b/src/atom-application.coffee index 993807ed4..520389efb 100644 --- a/src/atom-application.coffee +++ b/src/atom-application.coffee @@ -124,6 +124,7 @@ class AtomApplication menus.push label: 'File' submenu: [ + { label: 'New Window', accelerator: 'Command+N', click: => @openPath() } { label: 'Open...', accelerator: 'Command+O', click: => @promptForPath() } { label: 'Open In Dev Mode...', accelerator: 'Command+Shift+O', click: => @promptForPath(devMode: true) } ] From 270d17814e2329c87ff97c04c651c147e8416ea7 Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Fri, 12 Jul 2013 13:59:52 -0700 Subject: [PATCH 15/62] Move right border to tree view resizer This allows the border to still show when the tree view scrolls horizontally. Closes #622 --- themes/atom-light-ui/tree-view.less | 3 +++ 1 file changed, 3 insertions(+) diff --git a/themes/atom-light-ui/tree-view.less b/themes/atom-light-ui/tree-view.less index 5ab39df36..452cabb28 100644 --- a/themes/atom-light-ui/tree-view.less +++ b/themes/atom-light-ui/tree-view.less @@ -1,5 +1,8 @@ .tree-view { background: #dde3e8; +} + +.tree-view-resizer { border-right: 1px solid #989898; } From 041f52aaaa6ab24771255500f350b936f3ce1c2d Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Fri, 12 Jul 2013 14:07:03 -0700 Subject: [PATCH 16/62] Upgrade apm --- vendor/apm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vendor/apm b/vendor/apm index 1e03f08ee..ac0e54801 160000 --- a/vendor/apm +++ b/vendor/apm @@ -1 +1 @@ -Subproject commit 1e03f08ee07bd946b1f557c73e1c8eb8277c28e4 +Subproject commit ac0e54801b8fc1389c1810f0d4ac6670ff8a5602 From d1f372e439693fd56311b66f130934b9f90c4abe Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Fri, 12 Jul 2013 14:44:58 -0700 Subject: [PATCH 17/62] Rebuild native modules when atom-shell is upgraded Spawn an apm rebuild when the atom shell version changes after running the update-atom-shell script. Closes #618 --- tasks/update-atom-shell-task.coffee | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/tasks/update-atom-shell-task.coffee b/tasks/update-atom-shell-task.coffee index 4e95df45e..2c7091bf1 100644 --- a/tasks/update-atom-shell-task.coffee +++ b/tasks/update-atom-shell-task.coffee @@ -1,6 +1,26 @@ +path = require 'path' + module.exports = (grunt) -> {spawn} = require('./task-helpers')(grunt) + getAtomShellVersion = -> + versionPath = path.join('atom-shell', 'version') + if grunt.file.isFile(versionPath) + grunt.file.read(versionPath).trim() + else + null + grunt.registerTask 'update-atom-shell', 'Update atom-shell', -> done = @async() - spawn cmd: 'script/update-atom-shell', (error) -> done(error) + currentVersion = getAtomShellVersion() + spawn cmd: 'script/update-atom-shell', (error) -> + if error? + done(error) + else + newVersion = getAtomShellVersion() + if newVersion and currentVersion isnt newVersion + grunt.log.writeln("Rebuilding native modules for new atom-shell version #{newVersion.cyan}.") + cmd = path.join('node_modules', '.bin', 'apm') + spawn {cmd, args: ['rebuild']}, (error) -> done(error) + else + done() From ff70ae633d1a4c55a6462686072624a53dfe7a4f Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Fri, 12 Jul 2013 17:29:11 -0700 Subject: [PATCH 18/62] Correct broken link to npm --- docs/packages/authoring-packages.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/packages/authoring-packages.md b/docs/packages/authoring-packages.md index f174a276d..677df5fa8 100644 --- a/docs/packages/authoring-packages.md +++ b/docs/packages/authoring-packages.md @@ -29,7 +29,7 @@ on creating your first package. ## package.json -Similar to [npm packages](http://en.wikipedia.org/wiki/Npm_(software), Atom packages +Similar to [npm packages](http://en.wikipedia.org/wiki/Npm_(software\)), Atom packages can contain a _package.json_ file in their top-level directory. This file contains metadata about the package, such as the path to its "main" module, library dependencies, and manifests specifying the order in which its resources should be loaded. From 1a76e3dc9dc3fed7803c2013c3f3c8bcfeb6f359 Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Mon, 15 Jul 2013 12:18:50 -0700 Subject: [PATCH 19/62] Add missing '..' to node directory path The path has changed and one more parent directory needs to be traversed to find the bundled path to node for spawning child processes. --- CHANGELOG.md | 2 ++ src/stdlib/buffered-process.coffee | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8a770bcf5..61765affb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,5 @@ +* Fixed: Search never completing in the command panel + * Fixed: cmd-n now works when no windows are open * Fixed: Error selecting a grammar for an untitled editor diff --git a/src/stdlib/buffered-process.coffee b/src/stdlib/buffered-process.coffee index 94c75137c..a24381999 100644 --- a/src/stdlib/buffered-process.coffee +++ b/src/stdlib/buffered-process.coffee @@ -43,7 +43,7 @@ class BufferedProcess addNodeDirectoryToPath: (options) -> options.env ?= process.env pathSegments = [] - nodeDirectoryPath = path.resolve(process.execPath, '..', '..', '..', '..', 'Resources') + nodeDirectoryPath = path.resolve(process.execPath, '..', '..', '..', '..', '..', 'Resources') pathSegments.push(nodeDirectoryPath) pathSegments.push(options.env.PATH) if options.env.PATH options.env = _.extend({}, options.env, PATH: pathSegments.join(path.delimiter)) From 6bbcc58542b9d2d5af40eb0f128ae848667c2fab Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Mon, 15 Jul 2013 13:40:51 -0700 Subject: [PATCH 20/62] Call atom.focus() in the root beforeEach Async events are not currently firing in specs and this appears to cause them fire. --- spec/spec-helper.coffee | 1 + 1 file changed, 1 insertion(+) diff --git a/spec/spec-helper.coffee b/spec/spec-helper.coffee index 057986f20..171e80509 100644 --- a/spec/spec-helper.coffee +++ b/spec/spec-helper.coffee @@ -78,6 +78,7 @@ beforeEach -> spyOn(clipboard, 'readText').andCallFake -> pasteboardContent addCustomMatchers(this) + atom.focus() # Hack to get async events to fire afterEach -> keymap.bindingSets = bindingSetsToRestore From 9d733a2da905f4761e82dfc8d09982d2485dce3e Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Mon, 15 Jul 2013 13:56:18 -0700 Subject: [PATCH 21/62] Don't grab native window focus Call $(window) instead of atom.focus() so the native window doesn't regain focus on each spec run preventing it from running in the background. --- spec/spec-helper.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/spec-helper.coffee b/spec/spec-helper.coffee index 171e80509..0a1d1a904 100644 --- a/spec/spec-helper.coffee +++ b/spec/spec-helper.coffee @@ -78,7 +78,7 @@ beforeEach -> spyOn(clipboard, 'readText').andCallFake -> pasteboardContent addCustomMatchers(this) - atom.focus() # Hack to get async events to fire + $(window).focus() # Hack to get async events to fire afterEach -> keymap.bindingSets = bindingSetsToRestore From 17cecda23efed77a05b3d44526a20e9ccf0d7ef1 Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Tue, 16 Jul 2013 09:48:15 -0700 Subject: [PATCH 22/62] Use atom-shell 23dd5b4da8 The release past this build is causing issues with async callbacks not firing. --- script/update-atom-shell | 2 +- spec/spec-helper.coffee | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/script/update-atom-shell b/script/update-atom-shell index 256e18698..1d42c3ffc 100755 --- a/script/update-atom-shell +++ b/script/update-atom-shell @@ -5,7 +5,7 @@ cd "$(dirname "${BASH_SOURCE[0]}" )/.." TARGET=${1:-atom-shell} DISTURL="https://gh-contractor-zcbenz.s3.amazonaws.com/atom-shell" CURRENT_VERSION=$(cat "${TARGET}/version" 2>&1) -LATEST_VERSION=$(curl -fsSkL $DISTURL/version) +LATEST_VERSION=23dd5b4da8019d37eb0d4992d933f1351ece5a59 if [ -z "${LATEST_VERSION}" ] ; then echo "Could determine lastest version of atom-shell" >&2 diff --git a/spec/spec-helper.coffee b/spec/spec-helper.coffee index 0a1d1a904..057986f20 100644 --- a/spec/spec-helper.coffee +++ b/spec/spec-helper.coffee @@ -78,7 +78,6 @@ beforeEach -> spyOn(clipboard, 'readText').andCallFake -> pasteboardContent addCustomMatchers(this) - $(window).focus() # Hack to get async events to fire afterEach -> keymap.bindingSets = bindingSetsToRestore From d1812d74d66b96d634a1bca7a6d280f66f9257da Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Mon, 8 Jul 2013 11:04:06 -0700 Subject: [PATCH 23/62] Vendor pusher.js --- src/packages/collaboration/vendor/pusher.js | 3611 +++++++++++++++++++ 1 file changed, 3611 insertions(+) create mode 100644 src/packages/collaboration/vendor/pusher.js diff --git a/src/packages/collaboration/vendor/pusher.js b/src/packages/collaboration/vendor/pusher.js new file mode 100644 index 000000000..37d4fec7b --- /dev/null +++ b/src/packages/collaboration/vendor/pusher.js @@ -0,0 +1,3611 @@ +/*! + * Pusher JavaScript Library v2.1.1 + * http://pusherapp.com/ + * + * Copyright 2013, Pusher + * Released under the MIT licence. + */ + +;(function() { + function Pusher(app_key, options) { + options = options || {}; + + var self = this; + + this.key = app_key; + this.config = Pusher.Util.extend( + Pusher.getGlobalConfig(), + options.cluster ? Pusher.getClusterConfig(options.cluster) : {}, + options + ); + + this.channels = new Pusher.Channels(); + this.global_emitter = new Pusher.EventsDispatcher(); + this.sessionID = Math.floor(Math.random() * 1000000000); + + checkAppKey(this.key); + + var getStrategy = function(options) { + return Pusher.StrategyBuilder.build( + Pusher.getDefaultStrategy(self.config), + Pusher.Util.extend({}, self.config, options) + ); + }; + var getTimeline = function() { + return new Pusher.Timeline(self.key, self.sessionID, { + features: Pusher.Util.getClientFeatures(), + params: self.config.timelineParams || {}, + limit: 50, + level: Pusher.Timeline.INFO, + version: Pusher.VERSION + }); + }; + var getTimelineSender = function(timeline, options) { + if (self.config.disableStats) { + return null; + } + return new Pusher.TimelineSender(timeline, { + encrypted: self.isEncrypted() || !!options.encrypted, + host: self.config.statsHost, + path: "/timeline" + }); + }; + + this.connection = new Pusher.ConnectionManager( + this.key, + Pusher.Util.extend( + { getStrategy: getStrategy, + getTimeline: getTimeline, + getTimelineSender: getTimelineSender, + activityTimeout: this.config.activity_timeout, + pongTimeout: this.config.pong_timeout, + unavailableTimeout: this.config.unavailable_timeout + }, + this.config, + { encrypted: this.isEncrypted() } + ) + ); + + this.connection.bind('connected', function() { + self.subscribeAll(); + }); + this.connection.bind('message', function(params) { + var internal = (params.event.indexOf('pusher_internal:') === 0); + if (params.channel) { + var channel = self.channel(params.channel); + if (channel) { + channel.handleEvent(params.event, params.data); + } + } + // Emit globaly [deprecated] + if (!internal) self.global_emitter.emit(params.event, params.data); + }); + this.connection.bind('disconnected', function() { + self.channels.disconnect(); + }); + this.connection.bind('error', function(err) { + Pusher.warn('Error', err); + }); + + Pusher.instances.push(this); + + if (Pusher.isReady) self.connect(); + } + var prototype = Pusher.prototype; + + Pusher.instances = []; + Pusher.isReady = false; + + // To receive log output provide a Pusher.log function, for example + // Pusher.log = function(m){console.log(m)} + Pusher.debug = function() { + if (!Pusher.log) { + return; + } + Pusher.log(Pusher.Util.stringify.apply(this, arguments)); + }; + + Pusher.warn = function() { + var message = Pusher.Util.stringify.apply(this, arguments); + if (window.console) { + if (window.console.warn) { + window.console.warn(message); + } else if (window.console.log) { + window.console.log(message); + } + } + if (Pusher.log) { + Pusher.log(message); + } + }; + + Pusher.ready = function() { + Pusher.isReady = true; + for (var i = 0, l = Pusher.instances.length; i < l; i++) { + Pusher.instances[i].connect(); + } + }; + + prototype.channel = function(name) { + return this.channels.find(name); + }; + + prototype.connect = function() { + this.connection.connect(); + }; + + prototype.disconnect = function() { + this.connection.disconnect(); + }; + + prototype.bind = function(event_name, callback) { + this.global_emitter.bind(event_name, callback); + return this; + }; + + prototype.bind_all = function(callback) { + this.global_emitter.bind_all(callback); + return this; + }; + + prototype.subscribeAll = function() { + var channelName; + for (channelName in this.channels.channels) { + if (this.channels.channels.hasOwnProperty(channelName)) { + this.subscribe(channelName); + } + } + }; + + prototype.subscribe = function(channel_name) { + var self = this; + var channel = this.channels.add(channel_name, this); + + if (this.connection.state === 'connected') { + channel.authorize(this.connection.socket_id, function(err, data) { + if (err) { + channel.handleEvent('pusher:subscription_error', data); + } else { + self.send_event('pusher:subscribe', { + channel: channel_name, + auth: data.auth, + channel_data: data.channel_data + }); + } + }); + } + return channel; + }; + + prototype.unsubscribe = function(channel_name) { + this.channels.remove(channel_name); + if (this.connection.state === 'connected') { + this.send_event('pusher:unsubscribe', { + channel: channel_name + }); + } + }; + + prototype.send_event = function(event_name, data, channel) { + return this.connection.send_event(event_name, data, channel); + }; + + prototype.isEncrypted = function() { + if (Pusher.Util.getDocumentLocation().protocol === "https:") { + return true; + } else { + return !!this.config.encrypted; + } + }; + + function checkAppKey(key) { + if (key === null || key === undefined) { + Pusher.warn( + 'Warning', 'You must pass your app key when you instantiate Pusher.' + ); + } + } + + this.Pusher = Pusher; +}).call(this); + +;(function() { + Pusher.Util = { + now: function() { + if (Date.now) { + return Date.now(); + } else { + return new Date().valueOf(); + } + }, + + /** Merges multiple objects into the target argument. + * + * For properties that are plain Objects, performs a deep-merge. For the + * rest it just copies the value of the property. + * + * To extend prototypes use it as following: + * Pusher.Util.extend(Target.prototype, Base.prototype) + * + * You can also use it to merge objects without altering them: + * Pusher.Util.extend({}, object1, object2) + * + * @param {Object} target + * @return {Object} the target argument + */ + extend: function(target) { + for (var i = 1; i < arguments.length; i++) { + var extensions = arguments[i]; + for (var property in extensions) { + if (extensions[property] && extensions[property].constructor && + extensions[property].constructor === Object) { + target[property] = Pusher.Util.extend( + target[property] || {}, extensions[property] + ); + } else { + target[property] = extensions[property]; + } + } + } + return target; + }, + + stringify: function() { + var m = ["Pusher"]; + for (var i = 0; i < arguments.length; i++) { + if (typeof arguments[i] === "string") { + m.push(arguments[i]); + } else { + if (window.JSON === undefined) { + m.push(arguments[i].toString()); + } else { + m.push(JSON.stringify(arguments[i])); + } + } + } + return m.join(" : "); + }, + + arrayIndexOf: function(array, item) { // MSIE doesn't have array.indexOf + var nativeIndexOf = Array.prototype.indexOf; + if (array === null) { + return -1; + } + if (nativeIndexOf && array.indexOf === nativeIndexOf) { + return array.indexOf(item); + } + for (var i = 0, l = array.length; i < l; i++) { + if (array[i] === item) { + return i; + } + } + return -1; + }, + + keys: function(object) { + var result = []; + for (var key in object) { + if (Object.prototype.hasOwnProperty.call(object, key)) { + result.push(key); + } + } + return result; + }, + + /** Applies a function f to all elements of an array. + * + * Function f gets 3 arguments passed: + * - element from the array + * - index of the element + * - reference to the array + * + * @param {Array} array + * @param {Function} f + */ + apply: function(array, f) { + for (var i = 0; i < array.length; i++) { + f(array[i], i, array); + } + }, + + /** Applies a function f to all properties of an object. + * + * Function f gets 3 arguments passed: + * - element from the object + * - key of the element + * - reference to the object + * + * @param {Object} object + * @param {Function} f + */ + objectApply: function(object, f) { + for (var key in object) { + if (Object.prototype.hasOwnProperty.call(object, key)) { + f(object[key], key, object); + } + } + }, + + /** Maps all elements of the array and returns the result. + * + * Function f gets 4 arguments passed: + * - element from the array + * - index of the element + * - reference to the source array + * - reference to the destination array + * + * @param {Array} array + * @param {Function} f + */ + map: function(array, f) { + var result = []; + for (var i = 0; i < array.length; i++) { + result.push(f(array[i], i, array, result)); + } + return result; + }, + + /** Maps all elements of the object and returns the result. + * + * Function f gets 4 arguments passed: + * - element from the object + * - key of the element + * - reference to the source object + * - reference to the destination object + * + * @param {Object} object + * @param {Function} f + */ + mapObject: function(object, f) { + var result = {}; + for (var key in object) { + if (Object.prototype.hasOwnProperty.call(object, key)) { + result[key] = f(object[key]); + } + } + return result; + }, + + /** Filters elements of the array using a test function. + * + * Function test gets 4 arguments passed: + * - element from the array + * - index of the element + * - reference to the source array + * - reference to the destination array + * + * @param {Array} array + * @param {Function} f + */ + filter: function(array, test) { + test = test || function(value) { return !!value; }; + + var result = []; + for (var i = 0; i < array.length; i++) { + if (test(array[i], i, array, result)) { + result.push(array[i]); + } + } + return result; + }, + + /** Filters properties of the object using a test function. + * + * Function test gets 4 arguments passed: + * - element from the object + * - key of the element + * - reference to the source object + * - reference to the destination object + * + * @param {Object} object + * @param {Function} f + */ + filterObject: function(object, test) { + test = test || function(value) { return !!value; }; + + var result = {}; + for (var key in object) { + if (Object.prototype.hasOwnProperty.call(object, key)) { + if (test(object[key], key, object, result)) { + result[key] = object[key]; + } + } + } + return result; + }, + + /** Flattens an object into a two-dimensional array. + * + * @param {Object} object + * @return {Array} resulting array of [key, value] pairs + */ + flatten: function(object) { + var result = []; + for (var key in object) { + if (Object.prototype.hasOwnProperty.call(object, key)) { + result.push([key, object[key]]); + } + } + return result; + }, + + /** Checks whether any element of the array passes the test. + * + * Function test gets 3 arguments passed: + * - element from the array + * - index of the element + * - reference to the source array + * + * @param {Array} array + * @param {Function} f + */ + any: function(array, test) { + for (var i = 0; i < array.length; i++) { + if (test(array[i], i, array)) { + return true; + } + } + return false; + }, + + /** Checks whether all elements of the array pass the test. + * + * Function test gets 3 arguments passed: + * - element from the array + * - index of the element + * - reference to the source array + * + * @param {Array} array + * @param {Function} f + */ + all: function(array, test) { + for (var i = 0; i < array.length; i++) { + if (!test(array[i], i, array)) { + return false; + } + } + return true; + }, + + /** Builds a function that will proxy a method call to its first argument. + * + * Allows partial application of arguments, so additional arguments are + * prepended to the argument list. + * + * @param {String} name method name + * @return {Function} proxy function + */ + method: function(name) { + var boundArguments = Array.prototype.slice.call(arguments, 1); + return function(object) { + return object[name].apply(object, boundArguments.concat(arguments)); + }; + }, + + getDocument: function() { + return document; + }, + + getDocumentLocation: function() { + return Pusher.Util.getDocument().location; + }, + + getLocalStorage: function() { + try { + return window.localStorage; + } catch (e) { + return undefined; + } + }, + + getClientFeatures: function() { + return Pusher.Util.keys( + Pusher.Util.filterObject( + { "ws": Pusher.WSTransport, "flash": Pusher.FlashTransport }, + function (t) { return t.isSupported(); } + ) + ); + } + }; +}).call(this); + +;(function() { + Pusher.VERSION = '2.1.1'; + Pusher.PROTOCOL = 6; + + // DEPRECATED: WS connection parameters + Pusher.host = 'ws.pusherapp.com'; + Pusher.ws_port = 80; + Pusher.wss_port = 443; + // DEPRECATED: SockJS fallback parameters + Pusher.sockjs_host = 'sockjs.pusher.com'; + Pusher.sockjs_http_port = 80; + Pusher.sockjs_https_port = 443; + Pusher.sockjs_path = "/pusher"; + // DEPRECATED: Stats + Pusher.stats_host = 'stats.pusher.com'; + // DEPRECATED: Other settings + Pusher.channel_auth_endpoint = '/pusher/auth'; + Pusher.channel_auth_transport = 'ajax'; + Pusher.activity_timeout = 120000; + Pusher.pong_timeout = 30000; + Pusher.unavailable_timeout = 10000; + // CDN configuration + Pusher.cdn_http = 'http://js.pusher.com/'; + Pusher.cdn_https = 'https://d3dy5gmtp8yhk7.cloudfront.net/'; + Pusher.dependency_suffix = ''; + + Pusher.getDefaultStrategy = function(config) { + return [ + [":def", "ws_options", { + hostUnencrypted: config.wsHost + ":" + config.wsPort, + hostEncrypted: config.wsHost + ":" + config.wssPort + }], + [":def", "sockjs_options", { + hostUnencrypted: config.httpHost + ":" + config.httpPort, + hostEncrypted: config.httpHost + ":" + config.httpsPort + }], + [":def", "timeouts", { + loop: true, + timeout: 15000, + timeoutLimit: 60000 + }], + + [":def", "ws_manager", [":transport_manager", { + lives: 2, + minPingDelay: 10000, + maxPingDelay: config.activity_timeout + }]], + + [":def_transport", "ws", "ws", 3, ":ws_options", ":ws_manager"], + [":def_transport", "flash", "flash", 2, ":ws_options", ":ws_manager"], + [":def_transport", "sockjs", "sockjs", 1, ":sockjs_options"], + [":def", "ws_loop", [":sequential", ":timeouts", ":ws"]], + [":def", "flash_loop", [":sequential", ":timeouts", ":flash"]], + [":def", "sockjs_loop", [":sequential", ":timeouts", ":sockjs"]], + + [":def", "strategy", + [":cached", 1800000, + [":first_connected", + [":if", [":is_supported", ":ws"], [ + ":best_connected_ever", ":ws_loop", [":delayed", 2000, [":sockjs_loop"]] + ], [":if", [":is_supported", ":flash"], [ + ":best_connected_ever", ":flash_loop", [":delayed", 2000, [":sockjs_loop"]] + ], [ + ":sockjs_loop" + ] + ]] + ] + ] + ] + ]; + }; +}).call(this); + +;(function() { + Pusher.getGlobalConfig = function() { + return { + wsHost: Pusher.host, + wsPort: Pusher.ws_port, + wssPort: Pusher.wss_port, + httpHost: Pusher.sockjs_host, + httpPort: Pusher.sockjs_http_port, + httpsPort: Pusher.sockjs_https_port, + httpPath: Pusher.sockjs_path, + statsHost: Pusher.stats_host, + authEndpoint: Pusher.channel_auth_endpoint, + authTransport: Pusher.channel_auth_transport, + // TODO make this consistent with other options in next major version + activity_timeout: Pusher.activity_timeout, + pong_timeout: Pusher.pong_timeout, + unavailable_timeout: Pusher.unavailable_timeout + }; + }; + + Pusher.getClusterConfig = function(clusterName) { + return { + wsHost: "ws-" + clusterName + ".pusher.com", + httpHost: "sockjs-" + clusterName + ".pusher.com" + }; + }; +}).call(this); + +;(function() { + function buildExceptionClass(name) { + var klass = function(message) { + Error.call(this, message); + this.name = name; + }; + Pusher.Util.extend(klass.prototype, Error.prototype); + + return klass; + } + + /** Error classes used throughout pusher-js library. */ + Pusher.Errors = { + UnsupportedTransport: buildExceptionClass("UnsupportedTransport"), + UnsupportedStrategy: buildExceptionClass("UnsupportedStrategy"), + TransportPriorityTooLow: buildExceptionClass("TransportPriorityTooLow"), + TransportClosed: buildExceptionClass("TransportClosed") + }; +}).call(this); + +;(function() { + /** Manages callback bindings and event emitting. + * + * @param Function failThrough called when no listeners are bound to an event + */ + function EventsDispatcher(failThrough) { + this.callbacks = new CallbackRegistry(); + this.global_callbacks = []; + this.failThrough = failThrough; + } + var prototype = EventsDispatcher.prototype; + + prototype.bind = function(eventName, callback) { + this.callbacks.add(eventName, callback); + return this; + }; + + prototype.bind_all = function(callback) { + this.global_callbacks.push(callback); + return this; + }; + + prototype.unbind = function(eventName, callback) { + this.callbacks.remove(eventName, callback); + return this; + }; + + prototype.emit = function(eventName, data) { + var i; + + for (i = 0; i < this.global_callbacks.length; i++) { + this.global_callbacks[i](eventName, data); + } + + var callbacks = this.callbacks.get(eventName); + if (callbacks && callbacks.length > 0) { + for (i = 0; i < callbacks.length; i++) { + callbacks[i](data); + } + } else if (this.failThrough) { + this.failThrough(eventName, data); + } + + return this; + }; + + /** Callback registry helper. */ + + function CallbackRegistry() { + this._callbacks = {}; + } + + CallbackRegistry.prototype.get = function(eventName) { + return this._callbacks[this._prefix(eventName)]; + }; + + CallbackRegistry.prototype.add = function(eventName, callback) { + var prefixedEventName = this._prefix(eventName); + this._callbacks[prefixedEventName] = this._callbacks[prefixedEventName] || []; + this._callbacks[prefixedEventName].push(callback); + }; + + CallbackRegistry.prototype.remove = function(eventName, callback) { + if(this.get(eventName)) { + var index = Pusher.Util.arrayIndexOf(this.get(eventName), callback); + if (index !== -1){ + var callbacksCopy = this._callbacks[this._prefix(eventName)].slice(0); + callbacksCopy.splice(index, 1); + this._callbacks[this._prefix(eventName)] = callbacksCopy; + } + } + }; + + CallbackRegistry.prototype._prefix = function(eventName) { + return "_" + eventName; + }; + + Pusher.EventsDispatcher = EventsDispatcher; +}).call(this); + +;(function() { + /** Handles loading dependency files. + * + * Options: + * - cdn_http - url to HTTP CND + * - cdn_https - url to HTTPS CDN + * - version - version of pusher-js + * - suffix - suffix appended to all names of dependency files + * + * @param {Object} options + */ + function DependencyLoader(options) { + this.options = options; + this.loading = {}; + this.loaded = {}; + } + var prototype = DependencyLoader.prototype; + + /** Loads the dependency from CDN. + * + * @param {String} name + * @param {Function} callback + */ + prototype.load = function(name, callback) { + var self = this; + + if (this.loaded[name]) { + callback(); + return; + } + + if (!this.loading[name]) { + this.loading[name] = []; + } + this.loading[name].push(callback); + if (this.loading[name].length > 1) { + return; + } + + require(this.getPath(name), function() { + for (var i = 0; i < self.loading[name].length; i++) { + self.loading[name][i](); + } + delete self.loading[name]; + self.loaded[name] = true; + }); + }; + + /** Returns a root URL for pusher-js CDN. + * + * @returns {String} + */ + prototype.getRoot = function(options) { + var cdn; + var protocol = Pusher.Util.getDocumentLocation().protocol; + if ((options && options.encrypted) || protocol === "https:") { + cdn = this.options.cdn_https; + } else { + cdn = this.options.cdn_http; + } + // make sure there are no double slashes + return cdn.replace(/\/*$/, "") + "/" + this.options.version; + }; + + /** Returns a full path to a dependency file. + * + * @param {String} name + * @returns {String} + */ + prototype.getPath = function(name, options) { + return this.getRoot(options) + '/' + name + this.options.suffix + '.js'; + }; + + function handleScriptLoaded(elem, callback) { + if (Pusher.Util.getDocument().addEventListener) { + elem.addEventListener('load', callback, false); + } else { + elem.attachEvent('onreadystatechange', function () { + if (elem.readyState === 'loaded' || elem.readyState === 'complete') { + callback(); + } + }); + } + } + + function require(src, callback) { + var document = Pusher.Util.getDocument(); + var head = document.getElementsByTagName('head')[0]; + var script = document.createElement('script'); + + script.setAttribute('src', src); + script.setAttribute("type","text/javascript"); + script.setAttribute('async', true); + + handleScriptLoaded(script, function() { + // workaround for an Opera issue + setTimeout(callback, 0); + }); + + head.appendChild(script); + } + + Pusher.DependencyLoader = DependencyLoader; +}).call(this); + +;(function() { + Pusher.Dependencies = new Pusher.DependencyLoader({ + cdn_http: Pusher.cdn_http, + cdn_https: Pusher.cdn_https, + version: Pusher.VERSION, + suffix: Pusher.dependency_suffix + }); + + // Support Firefox versions which prefix WebSocket + if (!window.WebSocket && window.MozWebSocket) { + window.WebSocket = window.MozWebSocket; + } + + function initialize() { + Pusher.ready(); + } + + // Allows calling a function when the document body is available + function onDocumentBody(callback) { + if (document.body) { + callback(); + } else { + setTimeout(function() { + onDocumentBody(callback); + }, 0); + } + } + + function initializeOnDocumentBody() { + onDocumentBody(initialize); + } + + if (!window.JSON) { + Pusher.Dependencies.load("json2", initializeOnDocumentBody); + } else { + initializeOnDocumentBody(); + } +})(); + +;(function() { + /** Cross-browser compatible timer abstraction. + * + * @param {Number} delay + * @param {Function} callback + */ + function Timer(delay, callback) { + var self = this; + + this.timeout = setTimeout(function() { + if (self.timeout !== null) { + callback(); + self.timeout = null; + } + }, delay); + } + var prototype = Timer.prototype; + + /** Returns whether the timer is still running. + * + * @return {Boolean} + */ + prototype.isRunning = function() { + return this.timeout !== null; + }; + + /** Aborts a timer when it's running. */ + prototype.ensureAborted = function() { + if (this.timeout) { + clearTimeout(this.timeout); + this.timeout = null; + } + }; + + Pusher.Timer = Timer; +}).call(this); + +(function() { + + var Base64 = { + encode: function (s) { + return btoa(utob(s)); + } + }; + + var fromCharCode = String.fromCharCode; + + var b64chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'; + var b64tab = {}; + + for (var i = 0, l = b64chars.length; i < l; i++) { + b64tab[b64chars.charAt(i)] = i; + } + + var cb_utob = function(c) { + var cc = c.charCodeAt(0); + return cc < 0x80 ? c + : cc < 0x800 ? fromCharCode(0xc0 | (cc >>> 6)) + + fromCharCode(0x80 | (cc & 0x3f)) + : fromCharCode(0xe0 | ((cc >>> 12) & 0x0f)) + + fromCharCode(0x80 | ((cc >>> 6) & 0x3f)) + + fromCharCode(0x80 | ( cc & 0x3f)); + }; + + var utob = function(u) { + return u.replace(/[^\x00-\x7F]/g, cb_utob); + }; + + var cb_encode = function(ccc) { + var padlen = [0, 2, 1][ccc.length % 3]; + var ord = ccc.charCodeAt(0) << 16 + | ((ccc.length > 1 ? ccc.charCodeAt(1) : 0) << 8) + | ((ccc.length > 2 ? ccc.charCodeAt(2) : 0)); + var chars = [ + b64chars.charAt( ord >>> 18), + b64chars.charAt((ord >>> 12) & 63), + padlen >= 2 ? '=' : b64chars.charAt((ord >>> 6) & 63), + padlen >= 1 ? '=' : b64chars.charAt(ord & 63) + ]; + return chars.join(''); + }; + + var btoa = window.btoa || function(b) { + return b.replace(/[\s\S]{1,3}/g, cb_encode); + }; + + Pusher.Base64 = Base64; + +}).call(this); + +(function() { + + function JSONPRequest(options) { + this.options = options; + } + + JSONPRequest.send = function(options, callback) { + var request = new Pusher.JSONPRequest({ + url: options.url, + receiver: options.receiverName, + tagPrefix: options.tagPrefix + }); + var id = options.receiver.register(function(error, result) { + request.cleanup(); + callback(error, result); + }); + + return request.send(id, options.data, function(error) { + var callback = options.receiver.unregister(id); + if (callback) { + callback(error); + } + }); + }; + + var prototype = JSONPRequest.prototype; + + prototype.send = function(id, data, callback) { + if (this.script) { + return false; + } + + var tagPrefix = this.options.tagPrefix || "_pusher_jsonp_"; + + var params = Pusher.Util.extend( + {}, data, { receiver: this.options.receiver } + ); + var query = Pusher.Util.map( + Pusher.Util.flatten( + encodeData( + Pusher.Util.filterObject(params, function(value) { + return value !== undefined; + }) + ) + ), + Pusher.Util.method("join", "=") + ).join("&"); + + this.script = document.createElement("script"); + this.script.id = tagPrefix + id; + this.script.src = this.options.url + "/" + id + "?" + query; + this.script.type = "text/javascript"; + this.script.charset = "UTF-8"; + this.script.onerror = this.script.onload = callback; + + // Opera<11.6 hack for missing onerror callback + if (this.script.async === undefined && document.attachEvent) { + if (/opera/i.test(navigator.userAgent)) { + var receiverName = this.options.receiver || "Pusher.JSONP.receive"; + this.errorScript = document.createElement("script"); + this.errorScript.text = receiverName + "(" + id + ", true);"; + this.script.async = this.errorScript.async = false; + } + } + + var self = this; + this.script.onreadystatechange = function() { + if (self.script && /loaded|complete/.test(self.script.readyState)) { + callback(true); + } + }; + + var head = document.getElementsByTagName('head')[0]; + head.insertBefore(this.script, head.firstChild); + if (this.errorScript) { + head.insertBefore(this.errorScript, this.script.nextSibling); + } + + return true; + }; + + prototype.cleanup = function() { + if (this.script && this.script.parentNode) { + this.script.parentNode.removeChild(this.script); + this.script = null; + } + if (this.errorScript && this.errorScript.parentNode) { + this.errorScript.parentNode.removeChild(this.errorScript); + this.errorScript = null; + } + }; + + function encodeData(data) { + return Pusher.Util.mapObject(data, function(value) { + if (typeof value === "object") { + value = JSON.stringify(value); + } + return encodeURIComponent(Pusher.Base64.encode(value.toString())); + }); + } + + Pusher.JSONPRequest = JSONPRequest; + +}).call(this); + +(function() { + + function JSONPReceiver() { + this.lastId = 0; + this.callbacks = {}; + } + + var prototype = JSONPReceiver.prototype; + + prototype.register = function(callback) { + this.lastId++; + var id = this.lastId; + this.callbacks[id] = callback; + return id; + }; + + prototype.unregister = function(id) { + if (this.callbacks[id]) { + var callback = this.callbacks[id]; + delete this.callbacks[id]; + return callback; + } else { + return null; + } + }; + + prototype.receive = function(id, error, data) { + var callback = this.unregister(id); + if (callback) { + callback(error, data); + } + }; + + Pusher.JSONPReceiver = JSONPReceiver; + Pusher.JSONP = new JSONPReceiver(); + +}).call(this); + +(function() { + function Timeline(key, session, options) { + this.key = key; + this.session = session; + this.events = []; + this.options = options || {}; + this.sent = 0; + this.uniqueID = 0; + } + var prototype = Timeline.prototype; + + // Log levels + Timeline.ERROR = 3; + Timeline.INFO = 6; + Timeline.DEBUG = 7; + + prototype.log = function(level, event) { + if (this.options.level === undefined || level <= this.options.level) { + this.events.push( + Pusher.Util.extend({}, event, { + timestamp: Pusher.Util.now(), + level: level + }) + ); + if (this.options.limit && this.events.length > this.options.limit) { + this.events.shift(); + } + } + }; + + prototype.error = function(event) { + this.log(Timeline.ERROR, event); + }; + + prototype.info = function(event) { + this.log(Timeline.INFO, event); + }; + + prototype.debug = function(event) { + this.log(Timeline.DEBUG, event); + }; + + prototype.isEmpty = function() { + return this.events.length === 0; + }; + + prototype.send = function(sendJSONP, callback) { + var self = this; + + var data = {}; + if (this.sent === 0) { + data = Pusher.Util.extend({ + key: this.key, + features: this.options.features, + version: this.options.version + }, this.options.params || {}); + } + data.session = this.session; + data.timeline = this.events; + data = Pusher.Util.filterObject(data, function(v) { + return v !== undefined; + }); + + this.events = []; + sendJSONP(data, function(error, result) { + if (!error) { + self.sent++; + } + callback(error, result); + }); + + return true; + }; + + prototype.generateUniqueID = function() { + this.uniqueID++; + return this.uniqueID; + }; + + Pusher.Timeline = Timeline; +}).call(this); + +(function() { + function TimelineSender(timeline, options) { + this.timeline = timeline; + this.options = options || {}; + } + var prototype = TimelineSender.prototype; + + prototype.send = function(callback) { + if (this.timeline.isEmpty()) { + return; + } + + var options = this.options; + var scheme = "http" + (this.isEncrypted() ? "s" : "") + "://"; + + var sendJSONP = function(data, callback) { + return Pusher.JSONPRequest.send({ + data: data, + url: scheme + options.host + options.path, + receiver: Pusher.JSONP + }, callback); + }; + this.timeline.send(sendJSONP, callback); + }; + + prototype.isEncrypted = function() { + return !!this.options.encrypted; + }; + + Pusher.TimelineSender = TimelineSender; +}).call(this); + +;(function() { + /** Launches all substrategies and emits prioritized connected transports. + * + * @param {Array} strategies + */ + function BestConnectedEverStrategy(strategies) { + this.strategies = strategies; + } + var prototype = BestConnectedEverStrategy.prototype; + + prototype.isSupported = function() { + return Pusher.Util.any(this.strategies, Pusher.Util.method("isSupported")); + }; + + prototype.connect = function(minPriority, callback) { + return connect(this.strategies, minPriority, function(i, runners) { + return function(error, handshake) { + runners[i].error = error; + if (error) { + if (allRunnersFailed(runners)) { + callback(true); + } + return; + } + Pusher.Util.apply(runners, function(runner) { + runner.forceMinPriority(handshake.transport.priority); + }); + callback(null, handshake); + }; + }); + }; + + /** Connects to all strategies in parallel. + * + * Callback builder should be a function that takes two arguments: index + * and a list of runners. It should return another function that will be + * passed to the substrategy with given index. Runners can be aborted using + * abortRunner(s) functions from this class. + * + * @param {Array} strategies + * @param {Function} callbackBuilder + * @return {Object} strategy runner + */ + function connect(strategies, minPriority, callbackBuilder) { + var runners = Pusher.Util.map(strategies, function(strategy, i, _, rs) { + return strategy.connect(minPriority, callbackBuilder(i, rs)); + }); + return { + abort: function() { + Pusher.Util.apply(runners, abortRunner); + }, + forceMinPriority: function(p) { + Pusher.Util.apply(runners, function(runner) { + runner.forceMinPriority(p); + }); + } + }; + } + + function allRunnersFailed(runners) { + return Pusher.Util.all(runners, function(runner) { + return Boolean(runner.error); + }); + } + + function abortRunner(runner) { + if (!runner.error && !runner.aborted) { + runner.abort(); + runner.aborted = true; + } + } + + Pusher.BestConnectedEverStrategy = BestConnectedEverStrategy; +}).call(this); + +;(function() { + /** Caches last successful transport and uses it for following attempts. + * + * @param {Strategy} strategy + * @param {Object} transports + * @param {Object} options + */ + function CachedStrategy(strategy, transports, options) { + this.strategy = strategy; + this.transports = transports; + this.ttl = options.ttl || 1800*1000; + this.timeline = options.timeline; + } + var prototype = CachedStrategy.prototype; + + prototype.isSupported = function() { + return this.strategy.isSupported(); + }; + + prototype.connect = function(minPriority, callback) { + var info = fetchTransportInfo(); + + var strategies = [this.strategy]; + if (info && info.timestamp + this.ttl >= Pusher.Util.now()) { + var transport = this.transports[info.transport]; + if (transport) { + this.timeline.info({ cached: true, transport: info.transport }); + strategies.push(new Pusher.SequentialStrategy([transport], { + timeout: info.latency * 2, + failFast: true + })); + } + } + + var startTimestamp = Pusher.Util.now(); + var runner = strategies.pop().connect( + minPriority, + function cb(error, handshake) { + if (error) { + flushTransportInfo(); + if (strategies.length > 0) { + startTimestamp = Pusher.Util.now(); + runner = strategies.pop().connect(minPriority, cb); + } else { + callback(error); + } + } else { + var latency = Pusher.Util.now() - startTimestamp; + storeTransportInfo(handshake.transport.name, latency); + callback(null, handshake); + } + } + ); + + return { + abort: function() { + runner.abort(); + }, + forceMinPriority: function(p) { + minPriority = p; + if (runner) { + runner.forceMinPriority(p); + } + } + }; + }; + + function fetchTransportInfo() { + var storage = Pusher.Util.getLocalStorage(); + if (storage) { + var info = storage.pusherTransport; + if (info) { + return JSON.parse(storage.pusherTransport); + } + } + return null; + } + + function storeTransportInfo(transport, latency) { + var storage = Pusher.Util.getLocalStorage(); + if (storage) { + try { + storage.pusherTransport = JSON.stringify({ + timestamp: Pusher.Util.now(), + transport: transport, + latency: latency + }); + } catch(e) { + // catch over quota exceptions raised by localStorage + } + } + } + + function flushTransportInfo() { + var storage = Pusher.Util.getLocalStorage(); + if (storage && storage.pusherTransport) { + try { + delete storage.pusherTransport; + } catch(e) { + storage.pusherTransport = undefined; + } + } + } + + Pusher.CachedStrategy = CachedStrategy; +}).call(this); + +;(function() { + /** Runs substrategy after specified delay. + * + * Options: + * - delay - time in miliseconds to delay the substrategy attempt + * + * @param {Strategy} strategy + * @param {Object} options + */ + function DelayedStrategy(strategy, options) { + this.strategy = strategy; + this.options = { delay: options.delay }; + } + var prototype = DelayedStrategy.prototype; + + prototype.isSupported = function() { + return this.strategy.isSupported(); + }; + + prototype.connect = function(minPriority, callback) { + var strategy = this.strategy; + var runner; + var timer = new Pusher.Timer(this.options.delay, function() { + runner = strategy.connect(minPriority, callback); + }); + + return { + abort: function() { + timer.ensureAborted(); + if (runner) { + runner.abort(); + } + }, + forceMinPriority: function(p) { + minPriority = p; + if (runner) { + runner.forceMinPriority(p); + } + } + }; + }; + + Pusher.DelayedStrategy = DelayedStrategy; +}).call(this); + +;(function() { + /** Launches the substrategy and terminates on the first open connection. + * + * @param {Strategy} strategy + */ + function FirstConnectedStrategy(strategy) { + this.strategy = strategy; + } + var prototype = FirstConnectedStrategy.prototype; + + prototype.isSupported = function() { + return this.strategy.isSupported(); + }; + + prototype.connect = function(minPriority, callback) { + var runner = this.strategy.connect( + minPriority, + function(error, handshake) { + if (handshake) { + runner.abort(); + } + callback(error, handshake); + } + ); + return runner; + }; + + Pusher.FirstConnectedStrategy = FirstConnectedStrategy; +}).call(this); + +;(function() { + /** Proxies method calls to one of substrategies basing on the test function. + * + * @param {Function} test + * @param {Strategy} trueBranch strategy used when test returns true + * @param {Strategy} falseBranch strategy used when test returns false + */ + function IfStrategy(test, trueBranch, falseBranch) { + this.test = test; + this.trueBranch = trueBranch; + this.falseBranch = falseBranch; + } + var prototype = IfStrategy.prototype; + + prototype.isSupported = function() { + var branch = this.test() ? this.trueBranch : this.falseBranch; + return branch.isSupported(); + }; + + prototype.connect = function(minPriority, callback) { + var branch = this.test() ? this.trueBranch : this.falseBranch; + return branch.connect(minPriority, callback); + }; + + Pusher.IfStrategy = IfStrategy; +}).call(this); + +;(function() { + /** Loops through strategies with optional timeouts. + * + * Options: + * - loop - whether it should loop through the substrategy list + * - timeout - initial timeout for a single substrategy + * - timeoutLimit - maximum timeout + * + * @param {Strategy[]} strategies + * @param {Object} options + */ + function SequentialStrategy(strategies, options) { + this.strategies = strategies; + this.loop = Boolean(options.loop); + this.failFast = Boolean(options.failFast); + this.timeout = options.timeout; + this.timeoutLimit = options.timeoutLimit; + } + var prototype = SequentialStrategy.prototype; + + prototype.isSupported = function() { + return Pusher.Util.any(this.strategies, Pusher.Util.method("isSupported")); + }; + + prototype.connect = function(minPriority, callback) { + var self = this; + + var strategies = this.strategies; + var current = 0; + var timeout = this.timeout; + var runner = null; + + var tryNextStrategy = function(error, handshake) { + if (handshake) { + callback(null, handshake); + } else { + current = current + 1; + if (self.loop) { + current = current % strategies.length; + } + + if (current < strategies.length) { + if (timeout) { + timeout = timeout * 2; + if (self.timeoutLimit) { + timeout = Math.min(timeout, self.timeoutLimit); + } + } + runner = self.tryStrategy( + strategies[current], + minPriority, + { timeout: timeout, failFast: self.failFast }, + tryNextStrategy + ); + } else { + callback(true); + } + } + }; + + runner = this.tryStrategy( + strategies[current], + minPriority, + { timeout: timeout, failFast: this.failFast }, + tryNextStrategy + ); + + return { + abort: function() { + runner.abort(); + }, + forceMinPriority: function(p) { + minPriority = p; + if (runner) { + runner.forceMinPriority(p); + } + } + }; + }; + + /** @private */ + prototype.tryStrategy = function(strategy, minPriority, options, callback) { + var timer = null; + var runner = null; + + runner = strategy.connect(minPriority, function(error, handshake) { + if (error && timer && timer.isRunning() && !options.failFast) { + // advance to the next strategy after the timeout + return; + } + if (timer) { + timer.ensureAborted(); + } + callback(error, handshake); + }); + + if (options.timeout > 0) { + timer = new Pusher.Timer(options.timeout, function() { + runner.abort(); + callback(true); + }); + } + + return { + abort: function() { + if (timer) { + timer.ensureAborted(); + } + runner.abort(); + }, + forceMinPriority: function(p) { + runner.forceMinPriority(p); + } + }; + }; + + Pusher.SequentialStrategy = SequentialStrategy; +}).call(this); + +;(function() { + /** Provides a strategy interface for transports. + * + * @param {String} name + * @param {Number} priority + * @param {Class} transport + * @param {Object} options + */ + function TransportStrategy(name, priority, transport, options) { + this.name = name; + this.priority = priority; + this.transport = transport; + this.options = options || {}; + } + var prototype = TransportStrategy.prototype; + + /** Returns whether the transport is supported in the browser. + * + * @returns {Boolean} + */ + prototype.isSupported = function() { + return this.transport.isSupported({ + disableFlash: !!this.options.disableFlash + }); + }; + + /** Launches a connection attempt and returns a strategy runner. + * + * @param {Function} callback + * @return {Object} strategy runner + */ + prototype.connect = function(minPriority, callback) { + if (!this.transport.isSupported()) { + return failAttempt(new Pusher.Errors.UnsupportedStrategy(), callback); + } else if (this.priority < minPriority) { + return failAttempt(new Pusher.Errors.TransportPriorityTooLow(), callback); + } + + var self = this; + var connected = false; + + var transport = this.transport.createConnection( + this.name, this.priority, this.options.key, this.options + ); + var handshake = null; + + var onInitialized = function() { + transport.unbind("initialized", onInitialized); + transport.connect(); + }; + var onOpen = function() { + handshake = new Pusher.Handshake(transport, function(result) { + connected = true; + unbindListeners(); + callback(null, result); + }); + }; + var onError = function(error) { + unbindListeners(); + callback(error); + }; + var onClosed = function() { + unbindListeners(); + callback(new Pusher.Errors.TransportClosed(transport)); + }; + + var unbindListeners = function() { + transport.unbind("initialized", onInitialized); + transport.unbind("open", onOpen); + transport.unbind("error", onError); + transport.unbind("closed", onClosed); + }; + + transport.bind("initialized", onInitialized); + transport.bind("open", onOpen); + transport.bind("error", onError); + transport.bind("closed", onClosed); + + // connect will be called automatically after initialization + transport.initialize(); + + return { + abort: function() { + if (connected) { + return; + } + unbindListeners(); + if (handshake) { + handshake.close(); + } else { + transport.close(); + } + }, + forceMinPriority: function(p) { + if (connected) { + return; + } + if (self.priority < p) { + if (handshake) { + handshake.close(); + } else { + transport.close(); + } + } + } + }; + }; + + function failAttempt(error, callback) { + new Pusher.Timer(0, function() { + callback(error); + }); + return { + abort: function() {}, + forceMinPriority: function() {} + }; + } + + Pusher.TransportStrategy = TransportStrategy; +}).call(this); + +;(function() { + /** Handles common logic for all transports. + * + * Transport is a low-level connection object that wraps a connection method + * and exposes a simple evented interface for the connection state and + * messaging. It does not implement Pusher-specific WebSocket protocol. + * + * Additionally, it fetches resources needed for transport to work and exposes + * an interface for querying transport support and its features. + * + * This is an abstract class, please do not instantiate it. + * + * States: + * - new - initial state after constructing the object + * - initializing - during initialization phase, usually fetching resources + * - intialized - ready to establish a connection + * - connection - when connection is being established + * - open - when connection ready to be used + * - closed - after connection was closed be either side + * + * Emits: + * - error - after the connection raised an error + * + * Options: + * - encrypted - whether connection should use ssl + * - hostEncrypted - host to connect to when connection is encrypted + * - hostUnencrypted - host to connect to when connection is not encrypted + * + * @param {String} key application key + * @param {Object} options + */ + function AbstractTransport(name, priority, key, options) { + Pusher.EventsDispatcher.call(this); + + this.name = name; + this.priority = priority; + this.key = key; + this.state = "new"; + this.timeline = options.timeline; + this.id = this.timeline.generateUniqueID(); + + this.options = { + encrypted: Boolean(options.encrypted), + hostUnencrypted: options.hostUnencrypted, + hostEncrypted: options.hostEncrypted + }; + } + var prototype = AbstractTransport.prototype; + Pusher.Util.extend(prototype, Pusher.EventsDispatcher.prototype); + + /** Checks whether the transport is supported in the browser. + * + * @returns {Boolean} + */ + AbstractTransport.isSupported = function() { + return false; + }; + + /** Checks whether the transport handles ping/pong on itself. + * + * @return {Boolean} + */ + prototype.supportsPing = function() { + return false; + }; + + /** Initializes the transport. + * + * Fetches resources if needed and then transitions to initialized. + */ + prototype.initialize = function() { + this.timeline.info(this.buildTimelineMessage({ + transport: this.name + (this.options.encrypted ? "s" : "") + })); + this.timeline.debug(this.buildTimelineMessage({ method: "initialize" })); + + this.changeState("initialized"); + }; + + /** Tries to establish a connection. + * + * @returns {Boolean} false if transport is in invalid state + */ + prototype.connect = function() { + var url = this.getURL(this.key, this.options); + this.timeline.debug(this.buildTimelineMessage({ + method: "connect", + url: url + })); + + if (this.socket || this.state !== "initialized") { + return false; + } + + try { + this.socket = this.createSocket(url); + } catch (e) { + var self = this; + new Pusher.Timer(0, function() { + self.onError(e); + self.changeState("closed"); + }); + return false; + } + + this.bindListeners(); + + Pusher.debug("Connecting", { transport: this.name, url: url }); + this.changeState("connecting"); + return true; + }; + + /** Closes the connection. + * + * @return {Boolean} true if there was a connection to close + */ + prototype.close = function() { + this.timeline.debug(this.buildTimelineMessage({ method: "close" })); + + if (this.socket) { + this.socket.close(); + return true; + } else { + return false; + } + }; + + /** Sends data over the open connection. + * + * @param {String} data + * @return {Boolean} true only when in the "open" state + */ + prototype.send = function(data) { + this.timeline.debug(this.buildTimelineMessage({ + method: "send", + data: data + })); + + if (this.state === "open") { + // Workaround for MobileSafari bug (see https://gist.github.com/2052006) + var self = this; + setTimeout(function() { + self.socket.send(data); + }, 0); + return true; + } else { + return false; + } + }; + + prototype.requestPing = function() { + this.emit("ping_request"); + }; + + /** @protected */ + prototype.onOpen = function() { + this.changeState("open"); + this.socket.onopen = undefined; + }; + + /** @protected */ + prototype.onError = function(error) { + this.emit("error", { type: 'WebSocketError', error: error }); + this.timeline.error(this.buildTimelineMessage({})); + }; + + /** @protected */ + prototype.onClose = function(closeEvent) { + if (closeEvent) { + this.changeState("closed", { + code: closeEvent.code, + reason: closeEvent.reason, + wasClean: closeEvent.wasClean + }); + } else { + this.changeState("closed"); + } + this.socket = undefined; + }; + + /** @protected */ + prototype.onMessage = function(message) { + this.timeline.debug(this.buildTimelineMessage({ message: message.data })); + this.emit("message", message); + }; + + /** @protected */ + prototype.bindListeners = function() { + var self = this; + + this.socket.onopen = function() { self.onOpen(); }; + this.socket.onerror = function(error) { self.onError(error); }; + this.socket.onclose = function(closeEvent) { self.onClose(closeEvent); }; + this.socket.onmessage = function(message) { self.onMessage(message); }; + }; + + /** @protected */ + prototype.createSocket = function(url) { + return null; + }; + + /** @protected */ + prototype.getScheme = function() { + return this.options.encrypted ? "wss" : "ws"; + }; + + /** @protected */ + prototype.getBaseURL = function() { + var host; + if (this.options.encrypted) { + host = this.options.hostEncrypted; + } else { + host = this.options.hostUnencrypted; + } + return this.getScheme() + "://" + host; + }; + + /** @protected */ + prototype.getPath = function() { + return "/app/" + this.key; + }; + + /** @protected */ + prototype.getQueryString = function() { + return "?protocol=" + Pusher.PROTOCOL + + "&client=js&version=" + Pusher.VERSION; + }; + + /** @protected */ + prototype.getURL = function() { + return this.getBaseURL() + this.getPath() + this.getQueryString(); + }; + + /** @protected */ + prototype.changeState = function(state, params) { + this.state = state; + this.timeline.info(this.buildTimelineMessage({ + state: state, + params: params + })); + this.emit(state, params); + }; + + /** @protected */ + prototype.buildTimelineMessage = function(message) { + return Pusher.Util.extend({ cid: this.id }, message); + }; + + Pusher.AbstractTransport = AbstractTransport; +}).call(this); + +;(function() { + /** Transport using Flash to emulate WebSockets. + * + * @see AbstractTransport + */ + function FlashTransport(name, priority, key, options) { + Pusher.AbstractTransport.call(this, name, priority, key, options); + } + var prototype = FlashTransport.prototype; + Pusher.Util.extend(prototype, Pusher.AbstractTransport.prototype); + + /** Creates a new instance of FlashTransport. + * + * @param {String} key + * @param {Object} options + * @return {FlashTransport} + */ + FlashTransport.createConnection = function(name, priority, key, options) { + return new FlashTransport(name, priority, key, options); + }; + + /** Checks whether Flash is supported in the browser. + * + * It is possible to disable flash by passing an envrionment object with the + * disableFlash property set to true. + * + * @see AbstractTransport.isSupported + * @param {Object} environment + * @returns {Boolean} + */ + FlashTransport.isSupported = function(environment) { + if (environment && environment.disableFlash) { + return false; + } + try { + return Boolean(new ActiveXObject('ShockwaveFlash.ShockwaveFlash')); + } catch (e) { + return Boolean( + navigator && + navigator.mimeTypes && + navigator.mimeTypes["application/x-shockwave-flash"] !== undefined + ); + } + }; + + /** Fetches flashfallback dependency if needed. + * + * Sets WEB_SOCKET_SUPPRESS_CROSS_DOMAIN_SWF_ERROR to true (if not set before) + * and WEB_SOCKET_SWF_LOCATION to Pusher's cdn before loading Flash resources. + * + * @see AbstractTransport.prototype.initialize + */ + prototype.initialize = function() { + var self = this; + + this.timeline.info(this.buildTimelineMessage({ + transport: this.name + (this.options.encrypted ? "s" : "") + })); + this.timeline.debug(this.buildTimelineMessage({ method: "initialize" })); + this.changeState("initializing"); + + if (window.WEB_SOCKET_SUPPRESS_CROSS_DOMAIN_SWF_ERROR === undefined) { + window.WEB_SOCKET_SUPPRESS_CROSS_DOMAIN_SWF_ERROR = true; + } + window.WEB_SOCKET_SWF_LOCATION = Pusher.Dependencies.getRoot() + + "/WebSocketMain.swf"; + Pusher.Dependencies.load("flashfallback", function() { + self.changeState("initialized"); + }); + }; + + /** @protected */ + prototype.createSocket = function(url) { + return new FlashWebSocket(url); + }; + + /** @protected */ + prototype.getQueryString = function() { + return Pusher.AbstractTransport.prototype.getQueryString.call(this) + + "&flash=true"; + }; + + Pusher.FlashTransport = FlashTransport; +}).call(this); + +;(function() { + /** Fallback transport using SockJS. + * + * @see AbstractTransport + */ + function SockJSTransport(name, priority, key, options) { + Pusher.AbstractTransport.call(this, name, priority, key, options); + this.options.ignoreNullOrigin = options.ignoreNullOrigin; + } + var prototype = SockJSTransport.prototype; + Pusher.Util.extend(prototype, Pusher.AbstractTransport.prototype); + + /** Creates a new instance of SockJSTransport. + * + * @param {String} key + * @param {Object} options + * @return {SockJSTransport} + */ + SockJSTransport.createConnection = function(name, priority, key, options) { + return new SockJSTransport(name, priority, key, options); + }; + + /** Assumes that SockJS is always supported. + * + * @returns {Boolean} always true + */ + SockJSTransport.isSupported = function() { + return true; + }; + + /** Fetches sockjs dependency if needed. + * + * @see AbstractTransport.prototype.initialize + */ + prototype.initialize = function() { + var self = this; + + this.timeline.info(this.buildTimelineMessage({ + transport: this.name + (this.options.encrypted ? "s" : "") + })); + this.timeline.debug(this.buildTimelineMessage({ method: "initialize" })); + + this.changeState("initializing"); + Pusher.Dependencies.load("sockjs", function() { + self.changeState("initialized"); + }); + }; + + /** Always returns true, since SockJS handles ping on its own. + * + * @returns {Boolean} always true + */ + prototype.supportsPing = function() { + return true; + }; + + /** @protected */ + prototype.createSocket = function(url) { + return new SockJS(url, null, { + js_path: Pusher.Dependencies.getPath("sockjs", { + encrypted: this.options.encrypted + }), + ignore_null_origin: this.options.ignoreNullOrigin + }); + }; + + /** @protected */ + prototype.getScheme = function() { + return this.options.encrypted ? "https" : "http"; + }; + + /** @protected */ + prototype.getPath = function() { + return this.options.httpPath || "/pusher"; + }; + + /** @protected */ + prototype.getQueryString = function() { + return ""; + }; + + /** Handles opening a SockJS connection to Pusher. + * + * Since SockJS does not handle custom paths, we send it immediately after + * establishing the connection. + * + * @protected + */ + prototype.onOpen = function() { + this.socket.send(JSON.stringify({ + path: Pusher.AbstractTransport.prototype.getPath.call(this) + + Pusher.AbstractTransport.prototype.getQueryString.call(this) + })); + this.changeState("open"); + this.socket.onopen = undefined; + }; + + Pusher.SockJSTransport = SockJSTransport; +}).call(this); + +;(function() { + /** WebSocket transport. + * + * @see AbstractTransport + */ + function WSTransport(name, priority, key, options) { + Pusher.AbstractTransport.call(this, name, priority, key, options); + } + var prototype = WSTransport.prototype; + Pusher.Util.extend(prototype, Pusher.AbstractTransport.prototype); + + /** Creates a new instance of WSTransport. + * + * @param {String} key + * @param {Object} options + * @return {WSTransport} + */ + WSTransport.createConnection = function(name, priority, key, options) { + return new WSTransport(name, priority, key, options); + }; + + /** Checks whether the browser supports WebSockets in any form. + * + * @returns {Boolean} true if browser supports WebSockets + */ + WSTransport.isSupported = function() { + return window.WebSocket !== undefined || window.MozWebSocket !== undefined; + }; + + /** @protected */ + prototype.createSocket = function(url) { + var constructor = window.WebSocket || window.MozWebSocket; + return new constructor(url); + }; + + /** @protected */ + prototype.getQueryString = function() { + return Pusher.AbstractTransport.prototype.getQueryString.call(this) + + "&flash=false"; + }; + + Pusher.WSTransport = WSTransport; +}).call(this); + +;(function() { + function AssistantToTheTransportManager(manager, transport, options) { + this.manager = manager; + this.transport = transport; + this.minPingDelay = options.minPingDelay; + this.maxPingDelay = options.maxPingDelay; + this.pingDelay = null; + } + var prototype = AssistantToTheTransportManager.prototype; + + prototype.createConnection = function(name, priority, key, options) { + var connection = this.transport.createConnection( + name, priority, key, options + ); + + var self = this; + var openTimestamp = null; + var pingTimer = null; + + var onOpen = function() { + connection.unbind("open", onOpen); + + openTimestamp = Pusher.Util.now(); + if (self.pingDelay) { + pingTimer = setInterval(function() { + if (pingTimer) { + connection.requestPing(); + } + }, self.pingDelay); + } + + connection.bind("closed", onClosed); + }; + var onClosed = function(closeEvent) { + connection.unbind("closed", onClosed); + if (pingTimer) { + clearInterval(pingTimer); + pingTimer = null; + } + + if (closeEvent.code === 1002 || closeEvent.code === 1003) { + // we don't want to use transports not obeying the protocol + self.manager.reportDeath(); + } else if (!closeEvent.wasClean && openTimestamp) { + // report deaths only for short-living transport + var lifespan = Pusher.Util.now() - openTimestamp; + if (lifespan < 2 * self.maxPingDelay) { + self.manager.reportDeath(); + self.pingDelay = Math.max(lifespan / 2, self.minPingDelay); + } + } + }; + + connection.bind("open", onOpen); + return connection; + }; + + prototype.isSupported = function(environment) { + return this.manager.isAlive() && this.transport.isSupported(environment); + }; + + Pusher.AssistantToTheTransportManager = AssistantToTheTransportManager; +}).call(this); + +;(function() { + function TransportManager(options) { + this.options = options || {}; + this.livesLeft = this.options.lives || Infinity; + } + var prototype = TransportManager.prototype; + + prototype.getAssistant = function(transport) { + return new Pusher.AssistantToTheTransportManager(this, transport, { + minPingDelay: this.options.minPingDelay, + maxPingDelay: this.options.maxPingDelay + }); + }; + + prototype.isAlive = function() { + return this.livesLeft > 0; + }; + + prototype.reportDeath = function() { + this.livesLeft -= 1; + }; + + Pusher.TransportManager = TransportManager; +}).call(this); + +;(function() { + var StrategyBuilder = { + /** Transforms a JSON scheme to a strategy tree. + * + * @param {Array} scheme JSON strategy scheme + * @param {Object} options a hash of symbols to be included in the scheme + * @returns {Strategy} strategy tree that's represented by the scheme + */ + build: function(scheme, options) { + var context = Pusher.Util.extend({}, globalContext, options); + return evaluate(scheme, context)[1].strategy; + } + }; + + var transports = { + ws: Pusher.WSTransport, + flash: Pusher.FlashTransport, + sockjs: Pusher.SockJSTransport + }; + + // DSL bindings + + function returnWithOriginalContext(f) { + return function(context) { + return [f.apply(this, arguments), context]; + }; + } + + var globalContext = { + def: function(context, name, value) { + if (context[name] !== undefined) { + throw "Redefining symbol " + name; + } + context[name] = value; + return [undefined, context]; + }, + + def_transport: function(context, name, type, priority, options, manager) { + var transportClass = transports[type]; + if (!transportClass) { + throw new Pusher.Errors.UnsupportedTransport(type); + } + var transportOptions = Pusher.Util.extend({}, { + key: context.key, + encrypted: context.encrypted, + timeline: context.timeline, + disableFlash: context.disableFlash, + ignoreNullOrigin: context.ignoreNullOrigin + }, options); + if (manager) { + transportClass = manager.getAssistant(transportClass); + } + var transport = new Pusher.TransportStrategy( + name, priority, transportClass, transportOptions + ); + var newContext = context.def(context, name, transport)[1]; + newContext.transports = context.transports || {}; + newContext.transports[name] = transport; + return [undefined, newContext]; + }, + + transport_manager: returnWithOriginalContext(function(_, options) { + return new Pusher.TransportManager(options); + }), + + sequential: returnWithOriginalContext(function(_, options) { + var strategies = Array.prototype.slice.call(arguments, 2); + return new Pusher.SequentialStrategy(strategies, options); + }), + + cached: returnWithOriginalContext(function(context, ttl, strategy){ + return new Pusher.CachedStrategy(strategy, context.transports, { + ttl: ttl, + timeline: context.timeline + }); + }), + + first_connected: returnWithOriginalContext(function(_, strategy) { + return new Pusher.FirstConnectedStrategy(strategy); + }), + + best_connected_ever: returnWithOriginalContext(function() { + var strategies = Array.prototype.slice.call(arguments, 1); + return new Pusher.BestConnectedEverStrategy(strategies); + }), + + delayed: returnWithOriginalContext(function(_, delay, strategy) { + return new Pusher.DelayedStrategy(strategy, { delay: delay }); + }), + + "if": returnWithOriginalContext(function(_, test, trueBranch, falseBranch) { + return new Pusher.IfStrategy(test, trueBranch, falseBranch); + }), + + is_supported: returnWithOriginalContext(function(_, strategy) { + return function() { + return strategy.isSupported(); + }; + }) + }; + + // DSL interpreter + + function isSymbol(expression) { + return (typeof expression === "string") && expression.charAt(0) === ":"; + } + + function getSymbolValue(expression, context) { + return context[expression.slice(1)]; + } + + function evaluateListOfExpressions(expressions, context) { + if (expressions.length === 0) { + return [[], context]; + } + var head = evaluate(expressions[0], context); + var tail = evaluateListOfExpressions(expressions.slice(1), head[1]); + return [[head[0]].concat(tail[0]), tail[1]]; + } + + function evaluateString(expression, context) { + if (!isSymbol(expression)) { + return [expression, context]; + } + var value = getSymbolValue(expression, context); + if (value === undefined) { + throw "Undefined symbol " + expression; + } + return [value, context]; + } + + function evaluateArray(expression, context) { + if (isSymbol(expression[0])) { + var f = getSymbolValue(expression[0], context); + if (expression.length > 1) { + if (typeof f !== "function") { + throw "Calling non-function " + expression[0]; + } + var args = [Pusher.Util.extend({}, context)].concat( + Pusher.Util.map(expression.slice(1), function(arg) { + return evaluate(arg, Pusher.Util.extend({}, context))[0]; + }) + ); + return f.apply(this, args); + } else { + return [f, context]; + } + } else { + return evaluateListOfExpressions(expression, context); + } + } + + function evaluate(expression, context) { + var expressionType = typeof expression; + if (typeof expression === "string") { + return evaluateString(expression, context); + } else if (typeof expression === "object") { + if (expression instanceof Array && expression.length > 0) { + return evaluateArray(expression, context); + } + } + return [expression, context]; + } + + Pusher.StrategyBuilder = StrategyBuilder; +}).call(this); + +;(function() { + /** + * Provides functions for handling Pusher protocol-specific messages. + */ + Protocol = {}; + + /** + * Decodes a message in a Pusher format. + * + * Throws errors when messages are not parse'able. + * + * @param {Object} message + * @return {Object} + */ + Protocol.decodeMessage = function(message) { + try { + var params = JSON.parse(message.data); + if (typeof params.data === 'string') { + try { + params.data = JSON.parse(params.data); + } catch (e) { + if (!(e instanceof SyntaxError)) { + // TODO looks like unreachable code + // https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/JSON/parse + throw e; + } + } + } + return params; + } catch (e) { + throw { type: 'MessageParseError', error: e, data: message.data}; + } + }; + + /** + * Encodes a message to be sent. + * + * @param {Object} message + * @return {String} + */ + Protocol.encodeMessage = function(message) { + return JSON.stringify(message); + }; + + /** Processes a handshake message and returns appropriate actions. + * + * Returns an object with an 'action' and other action-specific properties. + * + * There are three outcomes when calling this function. First is a successful + * connection attempt, when pusher:connection_established is received, which + * results in a 'connected' action with an 'id' property. When passed a + * pusher:error event, it returns a result with action appropriate to the + * close code and an error. Otherwise, it raises an exception. + * + * @param {String} message + * @result Object + */ + Protocol.processHandshake = function(message) { + message = this.decodeMessage(message); + + if (message.event === "pusher:connection_established") { + return { action: "connected", id: message.data.socket_id }; + } else if (message.event === "pusher:error") { + // From protocol 6 close codes are sent only once, so this only + // happens when connection does not support close codes + return { + action: this.getCloseAction(message.data), + error: this.getCloseError(message.data) + }; + } else { + throw "Invalid handshake"; + } + }; + + /** + * Dispatches the close event and returns an appropriate action name. + * + * See: + * 1. https://developer.mozilla.org/en-US/docs/WebSockets/WebSockets_reference/CloseEvent + * 2. http://pusher.com/docs/pusher_protocol + * + * @param {CloseEvent} closeEvent + * @return {String} close action name + */ + Protocol.getCloseAction = function(closeEvent) { + if (closeEvent.code < 4000) { + // ignore 1000 CLOSE_NORMAL, 1001 CLOSE_GOING_AWAY, + // 1005 CLOSE_NO_STATUS, 1006 CLOSE_ABNORMAL + // ignore 1007...3999 + // handle 1002 CLOSE_PROTOCOL_ERROR, 1003 CLOSE_UNSUPPORTED, + // 1004 CLOSE_TOO_LARGE + if (closeEvent.code >= 1002 && closeEvent.code <= 1004) { + return "backoff"; + } else { + return null; + } + } else if (closeEvent.code === 4000) { + return "ssl_only"; + } else if (closeEvent.code < 4100) { + return "refused"; + } else if (closeEvent.code < 4200) { + return "backoff"; + } else if (closeEvent.code < 4300) { + return "retry"; + } else { + // unknown error + return "refused"; + } + }; + + /** + * Returns an error or null basing on the close event. + * + * Null is returned when connection was closed cleanly. Otherwise, an object + * with error details is returned. + * + * @param {CloseEvent} closeEvent + * @return {Object} error object + */ + Protocol.getCloseError = function(closeEvent) { + if (closeEvent.code !== 1000 && closeEvent.code !== 1001) { + return { + type: 'PusherError', + data: { + code: closeEvent.code, + message: closeEvent.reason || closeEvent.message + } + }; + } else { + return null; + } + }; + + Pusher.Protocol = Protocol; +}).call(this); + +;(function() { + /** + * Provides Pusher protocol interface for transports. + * + * Emits following events: + * - message - on received messages + * - ping - on ping requests + * - pong - on pong responses + * - error - when the transport emits an error + * - closed - after closing the transport + * + * It also emits more events when connection closes with a code. + * See Protocol.getCloseAction to get more details. + * + * @param {Number} id + * @param {AbstractTransport} transport + */ + function Connection(id, transport) { + Pusher.EventsDispatcher.call(this); + + this.id = id; + this.transport = transport; + this.bindListeners(); + } + var prototype = Connection.prototype; + Pusher.Util.extend(prototype, Pusher.EventsDispatcher.prototype); + + /** Returns whether used transport handles ping/pong by itself + * + * @returns {Boolean} true if ping is handled by the transport + */ + prototype.supportsPing = function() { + return this.transport.supportsPing(); + }; + + /** Sends raw data. + * + * @param {String} data + */ + prototype.send = function(data) { + return this.transport.send(data); + }; + + /** Sends an event. + * + * @param {String} name + * @param {String} data + * @param {String} [channel] + * @returns {Boolean} whether message was sent or not + */ + prototype.send_event = function(name, data, channel) { + var message = { event: name, data: data }; + if (channel) { + message.channel = channel; + } + Pusher.debug('Event sent', message); + return this.send(Pusher.Protocol.encodeMessage(message)); + }; + + /** Closes the connection. */ + prototype.close = function() { + this.transport.close(); + }; + + /** @private */ + prototype.bindListeners = function() { + var self = this; + + var onMessage = function(m) { + var message; + try { + message = Pusher.Protocol.decodeMessage(m); + } catch(e) { + self.emit('error', { + type: 'MessageParseError', + error: e, + data: m.data + }); + } + + if (message !== undefined) { + Pusher.debug('Event recd', message); + + switch (message.event) { + case 'pusher:error': + self.emit('error', { type: 'PusherError', data: message.data }); + break; + case 'pusher:ping': + self.emit("ping"); + break; + case 'pusher:pong': + self.emit("pong"); + break; + } + self.emit('message', message); + } + }; + var onPingRequest = function() { + self.emit("ping_request"); + }; + var onError = function(error) { + self.emit("error", { type: "WebSocketError", error: error }); + }; + var onClosed = function(closeEvent) { + unbindListeners(); + + if (closeEvent && closeEvent.code) { + self.handleCloseEvent(closeEvent); + } + + self.transport = null; + self.emit("closed"); + }; + + var unbindListeners = function() { + self.transport.unbind("closed", onClosed); + self.transport.unbind("error", onError); + self.transport.unbind("ping_request", onPingRequest); + self.transport.unbind("message", onMessage); + }; + + self.transport.bind("message", onMessage); + self.transport.bind("ping_request", onPingRequest); + self.transport.bind("error", onError); + self.transport.bind("closed", onClosed); + }; + + /** @private */ + prototype.handleCloseEvent = function(closeEvent) { + var action = Pusher.Protocol.getCloseAction(closeEvent); + var error = Pusher.Protocol.getCloseError(closeEvent); + if (error) { + this.emit('error', error); + } + if (action) { + this.emit(action); + } + }; + + Pusher.Connection = Connection; +}).call(this); + +;(function() { + /** + * Handles Pusher protocol handshakes for transports. + * + * Calls back with a result object after handshake is completed. Results + * always have two fields: + * - action - string describing action to be taken after the handshake + * - transport - the transport object passed to the constructor + * + * Different actions can set different additional properties on the result. + * In the case of 'connected' action, there will be a 'connection' property + * containing a Connection object for the transport. Other actions should + * carry an 'error' property. + * + * @param {AbstractTransport} transport + * @param {Function} callback + */ + function Handshake(transport, callback) { + this.transport = transport; + this.callback = callback; + this.bindListeners(); + } + var prototype = Handshake.prototype; + + prototype.close = function() { + this.unbindListeners(); + this.transport.close(); + }; + + /** @private */ + prototype.bindListeners = function() { + var self = this; + + self.onMessage = function(m) { + self.unbindListeners(); + + try { + var result = Pusher.Protocol.processHandshake(m); + if (result.action === "connected") { + self.finish("connected", { + connection: new Pusher.Connection(result.id, self.transport) + }); + } else { + self.finish(result.action, { error: result.error }); + self.transport.close(); + } + } catch (e) { + self.finish("error", { error: e }); + self.transport.close(); + } + }; + + self.onClosed = function(closeEvent) { + self.unbindListeners(); + + var action = Pusher.Protocol.getCloseAction(closeEvent) || "backoff"; + var error = Pusher.Protocol.getCloseError(closeEvent); + self.finish(action, { error: error }); + }; + + self.transport.bind("message", self.onMessage); + self.transport.bind("closed", self.onClosed); + }; + + /** @private */ + prototype.unbindListeners = function() { + this.transport.unbind("message", this.onMessage); + this.transport.unbind("closed", this.onClosed); + }; + + /** @private */ + prototype.finish = function(action, params) { + this.callback( + Pusher.Util.extend({ transport: this.transport, action: action }, params) + ); + }; + + Pusher.Handshake = Handshake; +}).call(this); + +;(function() { + /** Manages connection to Pusher. + * + * Uses a strategy (currently only default), timers and network availability + * info to establish a connection and export its state. In case of failures, + * manages reconnection attempts. + * + * Exports state changes as following events: + * - "state_change", { previous: p, current: state } + * - state + * + * States: + * - initialized - initial state, never transitioned to + * - connecting - connection is being established + * - connected - connection has been fully established + * - disconnected - on requested disconnection or before reconnecting + * - unavailable - after connection timeout or when there's no network + * + * Options: + * - unavailableTimeout - time to transition to unavailable state + * - activityTimeout - time after which ping message should be sent + * - pongTimeout - time for Pusher to respond with pong before reconnecting + * + * @param {String} key application key + * @param {Object} options + */ + function ConnectionManager(key, options) { + Pusher.EventsDispatcher.call(this); + + this.key = key; + this.options = options || {}; + this.state = "initialized"; + this.connection = null; + this.encrypted = !!options.encrypted; + this.timeline = this.options.getTimeline(); + + this.connectionCallbacks = this.buildConnectionCallbacks(); + this.errorCallbacks = this.buildErrorCallbacks(); + this.handshakeCallbacks = this.buildHandshakeCallbacks(this.errorCallbacks); + + var self = this; + + Pusher.Network.bind("online", function() { + self.timeline.info({ netinfo: "online" }); + if (self.state === "unavailable") { + self.connect(); + } + }); + Pusher.Network.bind("offline", function() { + self.timeline.info({ netinfo: "offline" }); + if (self.shouldRetry()) { + self.disconnect(); + self.updateState("unavailable"); + } + }); + + var sendTimeline = function() { + if (self.timelineSender) { + self.timelineSender.send(function() {}); + } + }; + this.bind("connected", sendTimeline); + setInterval(sendTimeline, 60000); + + this.updateStrategy(); + } + var prototype = ConnectionManager.prototype; + + Pusher.Util.extend(prototype, Pusher.EventsDispatcher.prototype); + + /** Establishes a connection to Pusher. + * + * Does nothing when connection is already established. See top-level doc + * to find events emitted on connection attempts. + */ + prototype.connect = function() { + var self = this; + + if (self.connection) { + return; + } + if (self.state === "connecting") { + return; + } + + if (!self.strategy.isSupported()) { + self.updateState("failed"); + return; + } + if (Pusher.Network.isOnline() === false) { + self.updateState("unavailable"); + return; + } + + self.updateState("connecting"); + self.timelineSender = self.options.getTimelineSender( + self.timeline, + { encrypted: self.encrypted }, + self + ); + + var callback = function(error, handshake) { + if (error) { + self.runner = self.strategy.connect(0, callback); + } else { + if (handshake.action === "error") { + self.timeline.error({ handshakeError: handshake.error }); + } else { + // we don't support switching connections yet + self.runner.abort(); + self.handshakeCallbacks[handshake.action](handshake); + } + } + }; + self.runner = self.strategy.connect(0, callback); + + self.setUnavailableTimer(); + }; + + /** Sends raw data. + * + * @param {String} data + */ + prototype.send = function(data) { + if (this.connection) { + return this.connection.send(data); + } else { + return false; + } + }; + + /** Sends an event. + * + * @param {String} name + * @param {String} data + * @param {String} [channel] + * @returns {Boolean} whether message was sent or not + */ + prototype.send_event = function(name, data, channel) { + if (this.connection) { + return this.connection.send_event(name, data, channel); + } else { + return false; + } + }; + + /** Closes the connection. */ + prototype.disconnect = function() { + if (this.runner) { + this.runner.abort(); + } + this.clearRetryTimer(); + this.clearUnavailableTimer(); + this.stopActivityCheck(); + this.updateState("disconnected"); + // we're in disconnected state, so closing will not cause reconnecting + if (this.connection) { + this.connection.close(); + this.abandonConnection(); + } + }; + + /** @private */ + prototype.updateStrategy = function() { + this.strategy = this.options.getStrategy({ + key: this.key, + timeline: this.timeline, + encrypted: this.encrypted + }); + }; + + /** @private */ + prototype.retryIn = function(delay) { + var self = this; + self.timeline.info({ action: "retry", delay: delay }); + if (delay > 0) { + self.emit("connecting_in", Math.round(delay / 1000)); + } + self.retryTimer = new Pusher.Timer(delay || 0, function() { + self.disconnect(); + self.connect(); + }); + }; + + /** @private */ + prototype.clearRetryTimer = function() { + if (this.retryTimer) { + this.retryTimer.ensureAborted(); + } + }; + + /** @private */ + prototype.setUnavailableTimer = function() { + var self = this; + self.unavailableTimer = new Pusher.Timer( + self.options.unavailableTimeout, + function() { + self.updateState("unavailable"); + } + ); + }; + + /** @private */ + prototype.clearUnavailableTimer = function() { + if (this.unavailableTimer) { + this.unavailableTimer.ensureAborted(); + } + }; + + /** @private */ + prototype.resetActivityCheck = function() { + this.stopActivityCheck(); + // send ping after inactivity + if (!this.connection.supportsPing()) { + var self = this; + self.activityTimer = new Pusher.Timer( + self.options.activityTimeout, + function() { + self.send_event('pusher:ping', {}); + // wait for pong response + self.activityTimer = new Pusher.Timer( + self.options.pongTimeout, + function() { + self.connection.close(); + } + ); + } + ); + } + }; + + /** @private */ + prototype.stopActivityCheck = function() { + if (this.activityTimer) { + this.activityTimer.ensureAborted(); + } + }; + + /** @private */ + prototype.buildConnectionCallbacks = function() { + var self = this; + return { + message: function(message) { + // includes pong messages from server + self.resetActivityCheck(); + self.emit('message', message); + }, + ping: function() { + self.send_event('pusher:pong', {}); + }, + ping_request: function() { + self.send_event('pusher:ping', {}); + }, + error: function(error) { + // just emit error to user - socket will already be closed by browser + self.emit("error", { type: "WebSocketError", error: error }); + }, + closed: function() { + self.abandonConnection(); + if (self.shouldRetry()) { + self.retryIn(1000); + } + } + }; + }; + + /** @private */ + prototype.buildHandshakeCallbacks = function(errorCallbacks) { + var self = this; + return Pusher.Util.extend({}, errorCallbacks, { + connected: function(handshake) { + self.clearUnavailableTimer(); + self.setConnection(handshake.connection); + self.socket_id = self.connection.id; + self.updateState("connected"); + } + }); + }; + + /** @private */ + prototype.buildErrorCallbacks = function() { + var self = this; + + function withErrorEmitted(callback) { + return function(result) { + if (result.error) { + self.emit("error", { type: "WebSocketError", error: result.error }); + } + callback(result); + }; + } + + return { + ssl_only: withErrorEmitted(function() { + self.encrypted = true; + self.updateStrategy(); + self.retryIn(0); + }), + refused: withErrorEmitted(function() { + self.disconnect(); + }), + backoff: withErrorEmitted(function() { + self.retryIn(1000); + }), + retry: withErrorEmitted(function() { + self.retryIn(0); + }) + }; + }; + + /** @private */ + prototype.setConnection = function(connection) { + this.connection = connection; + for (var event in this.connectionCallbacks) { + this.connection.bind(event, this.connectionCallbacks[event]); + } + this.resetActivityCheck(); + }; + + /** @private */ + prototype.abandonConnection = function() { + if (!this.connection) { + return; + } + for (var event in this.connectionCallbacks) { + this.connection.unbind(event, this.connectionCallbacks[event]); + } + this.connection = null; + }; + + /** @private */ + prototype.updateState = function(newState, data) { + var previousState = this.state; + + this.state = newState; + // Only emit when the state changes + if (previousState !== newState) { + Pusher.debug('State changed', previousState + ' -> ' + newState); + + this.timeline.info({ state: newState }); + this.emit('state_change', { previous: previousState, current: newState }); + this.emit(newState, data); + } + }; + + /** @private */ + prototype.shouldRetry = function() { + return this.state === "connecting" || this.state === "connected"; + }; + + Pusher.ConnectionManager = ConnectionManager; +}).call(this); + +;(function() { + /** Really basic interface providing network availability info. + * + * Emits: + * - online - when browser goes online + * - offline - when browser goes offline + */ + function NetInfo() { + Pusher.EventsDispatcher.call(this); + + var self = this; + // This is okay, as IE doesn't support this stuff anyway. + if (window.addEventListener !== undefined) { + window.addEventListener("online", function() { + self.emit('online'); + }, false); + window.addEventListener("offline", function() { + self.emit('offline'); + }, false); + } + } + Pusher.Util.extend(NetInfo.prototype, Pusher.EventsDispatcher.prototype); + + var prototype = NetInfo.prototype; + + /** Returns whether browser is online or not + * + * Offline means definitely offline (no connection to router). + * Inverse does NOT mean definitely online (only currently supported in Safari + * and even there only means the device has a connection to the router). + * + * @return {Boolean} + */ + prototype.isOnline = function() { + if (window.navigator.onLine === undefined) { + return true; + } else { + return window.navigator.onLine; + } + }; + + Pusher.NetInfo = NetInfo; + Pusher.Network = new NetInfo(); +}).call(this); + +;(function() { + /** Represents a collection of members of a presence channel. */ + function Members() { + this.reset(); + } + var prototype = Members.prototype; + + /** Returns member's info for given id. + * + * Resulting object containts two fields - id and info. + * + * @param {Number} id + * @return {Object} member's info or null + */ + prototype.get = function(id) { + if (Object.prototype.hasOwnProperty.call(this.members, id)) { + return { + id: id, + info: this.members[id] + }; + } else { + return null; + } + }; + + /** Calls back for each member in unspecified order. + * + * @param {Function} callback + */ + prototype.each = function(callback) { + var self = this; + Pusher.Util.objectApply(self.members, function(member, id) { + callback(self.get(id)); + }); + }; + + /** Updates the id for connected member. For internal use only. */ + prototype.setMyID = function(id) { + this.myID = id; + }; + + /** Handles subscription data. For internal use only. */ + prototype.onSubscription = function(subscriptionData) { + this.members = subscriptionData.presence.hash; + this.count = subscriptionData.presence.count; + this.me = this.get(this.myID); + }; + + /** Adds a new member to the collection. For internal use only. */ + prototype.addMember = function(memberData) { + if (this.get(memberData.user_id) === null) { + this.count++; + } + this.members[memberData.user_id] = memberData.user_info; + return this.get(memberData.user_id); + }; + + /** Adds a member from the collection. For internal use only. */ + prototype.removeMember = function(memberData) { + var member = this.get(memberData.user_id); + if (member) { + delete this.members[memberData.user_id]; + this.count--; + } + return member; + }; + + /** Resets the collection to the initial state. For internal use only. */ + prototype.reset = function() { + this.members = {}; + this.count = 0; + this.myID = null; + this.me = null; + }; + + Pusher.Members = Members; +}).call(this); + +;(function() { + /** Provides base public channel interface with an event emitter. + * + * Emits: + * - pusher:subscription_succeeded - after subscribing successfully + * - other non-internal events + * + * @param {String} name + * @param {Pusher} pusher + */ + function Channel(name, pusher) { + Pusher.EventsDispatcher.call(this, function(event, data) { + Pusher.debug('No callbacks on ' + name + ' for ' + event); + }); + + this.name = name; + this.pusher = pusher; + this.subscribed = false; + } + var prototype = Channel.prototype; + Pusher.Util.extend(prototype, Pusher.EventsDispatcher.prototype); + + /** Skips authorization, since public channels don't require it. + * + * @param {Function} callback + */ + prototype.authorize = function(socketId, callback) { + return callback(false, {}); + }; + + /** Triggers an event */ + prototype.trigger = function(event, data) { + return this.pusher.send_event(event, data, this.name); + }; + + /** Signals disconnection to the channel. For internal use only. */ + prototype.disconnect = function() { + this.subscribed = false; + }; + + /** Handles an event. For internal use only. + * + * @param {String} event + * @param {*} data + */ + prototype.handleEvent = function(event, data) { + if (event.indexOf("pusher_internal:") === 0) { + if (event === "pusher_internal:subscription_succeeded") { + this.subscribed = true; + this.emit("pusher:subscription_succeeded", data); + } + } else { + this.emit(event, data); + } + }; + + Pusher.Channel = Channel; +}).call(this); + +;(function() { + /** Extends public channels to provide private channel interface. + * + * @param {String} name + * @param {Pusher} pusher + */ + function PrivateChannel(name, pusher) { + Pusher.Channel.call(this, name, pusher); + } + var prototype = PrivateChannel.prototype; + Pusher.Util.extend(prototype, Pusher.Channel.prototype); + + /** Authorizes the connection to use the channel. + * + * @param {String} socketId + * @param {Function} callback + */ + prototype.authorize = function(socketId, callback) { + var authorizer = new Pusher.Channel.Authorizer(this, this.pusher.config); + return authorizer.authorize(socketId, callback); + }; + + Pusher.PrivateChannel = PrivateChannel; +}).call(this); + +;(function() { + /** Adds presence channel functionality to private channels. + * + * @param {String} name + * @param {Pusher} pusher + */ + function PresenceChannel(name, pusher) { + Pusher.PrivateChannel.call(this, name, pusher); + this.members = new Pusher.Members(); + } + var prototype = PresenceChannel.prototype; + Pusher.Util.extend(prototype, Pusher.PrivateChannel.prototype); + + /** Authenticates the connection as a member of the channel. + * + * @param {String} socketId + * @param {Function} callback + */ + prototype.authorize = function(socketId, callback) { + var _super = Pusher.PrivateChannel.prototype.authorize; + var self = this; + _super.call(self, socketId, function(error, authData) { + if (!error) { + if (authData.channel_data === undefined) { + Pusher.warn( + "Invalid auth response for channel '" + + self.name + + "', expected 'channel_data' field" + ); + callback("Invalid auth response"); + return; + } + var channelData = JSON.parse(authData.channel_data); + self.members.setMyID(channelData.user_id); + } + callback(error, authData); + }); + }; + + /** Handles presence and subscription events. For internal use only. + * + * @param {String} event + * @param {*} data + */ + prototype.handleEvent = function(event, data) { + switch (event) { + case "pusher_internal:subscription_succeeded": + this.members.onSubscription(data); + this.subscribed = true; + this.emit("pusher:subscription_succeeded", this.members); + break; + case "pusher_internal:member_added": + var addedMember = this.members.addMember(data); + this.emit('pusher:member_added', addedMember); + break; + case "pusher_internal:member_removed": + var removedMember = this.members.removeMember(data); + if (removedMember) { + this.emit('pusher:member_removed', removedMember); + } + break; + default: + Pusher.PrivateChannel.prototype.handleEvent.call(this, event, data); + } + }; + + /** Resets the channel state, including members map. For internal use only. */ + prototype.disconnect = function() { + this.members.reset(); + Pusher.PrivateChannel.prototype.disconnect.call(this); + }; + + Pusher.PresenceChannel = PresenceChannel; +}).call(this); + +;(function() { + /** Handles a channel map. */ + function Channels() { + this.channels = {}; + } + var prototype = Channels.prototype; + + /** Creates or retrieves an existing channel by its name. + * + * @param {String} name + * @param {Pusher} pusher + * @return {Channel} + */ + prototype.add = function(name, pusher) { + if (!this.channels[name]) { + this.channels[name] = createChannel(name, pusher); + } + return this.channels[name]; + }; + + /** Finds a channel by its name. + * + * @param {String} name + * @return {Channel} channel or null if it doesn't exist + */ + prototype.find = function(name) { + return this.channels[name]; + }; + + /** Removes a channel from the map. + * + * @param {String} name + */ + prototype.remove = function(name) { + delete this.channels[name]; + }; + + /** Proxies disconnection signal to all channels. */ + prototype.disconnect = function() { + Pusher.Util.objectApply(this.channels, function(channel) { + channel.disconnect(); + }); + }; + + function createChannel(name, pusher) { + if (name.indexOf('private-') === 0) { + return new Pusher.PrivateChannel(name, pusher); + } else if (name.indexOf('presence-') === 0) { + return new Pusher.PresenceChannel(name, pusher); + } else { + return new Pusher.Channel(name, pusher); + } + } + + Pusher.Channels = Channels; +}).call(this); + +;(function() { + Pusher.Channel.Authorizer = function(channel, options) { + this.channel = channel; + this.type = options.authTransport; + + this.options = options; + this.authOptions = (options || {}).auth || {}; + }; + + Pusher.Channel.Authorizer.prototype = { + composeQuery: function(socketId) { + var query = '&socket_id=' + encodeURIComponent(socketId) + + '&channel_name=' + encodeURIComponent(this.channel.name); + + for(var i in this.authOptions.params) { + query += "&" + encodeURIComponent(i) + "=" + encodeURIComponent(this.authOptions.params[i]); + } + + return query; + }, + + authorize: function(socketId, callback) { + return Pusher.authorizers[this.type].call(this, socketId, callback); + } + }; + + var nextAuthCallbackID = 1; + + Pusher.auth_callbacks = {}; + Pusher.authorizers = { + ajax: function(socketId, callback){ + var self = this, xhr; + + if (Pusher.XHR) { + xhr = new Pusher.XHR(); + } else { + xhr = (window.XMLHttpRequest ? new window.XMLHttpRequest() : new ActiveXObject("Microsoft.XMLHTTP")); + } + + xhr.open("POST", self.options.authEndpoint, true); + + // add request headers + xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded"); + for(var headerName in this.authOptions.headers) { + xhr.setRequestHeader(headerName, this.authOptions.headers[headerName]); + } + + xhr.onreadystatechange = function() { + if (xhr.readyState == 4) { + if (xhr.status == 200) { + var data, parsed = false; + + try { + data = JSON.parse(xhr.responseText); + parsed = true; + } catch (e) { + callback(true, 'JSON returned from webapp was invalid, yet status code was 200. Data was: ' + xhr.responseText); + } + + if (parsed) { // prevents double execution. + callback(false, data); + } + } else { + Pusher.warn("Couldn't get auth info from your webapp", xhr.status); + callback(true, xhr.status); + } + } + }; + + xhr.send(this.composeQuery(socketId)); + return xhr; + }, + + jsonp: function(socketId, callback){ + if(this.authOptions.headers !== undefined) { + Pusher.warn("Warn", "To send headers with the auth request, you must use AJAX, rather than JSONP."); + } + + var callbackName = nextAuthCallbackID.toString(); + nextAuthCallbackID++; + + var script = document.createElement("script"); + // Hacked wrapper. + Pusher.auth_callbacks[callbackName] = function(data) { + callback(false, data); + }; + + var callback_name = "Pusher.auth_callbacks['" + callbackName + "']"; + script.src = this.options.authEndpoint + + '?callback=' + + encodeURIComponent(callback_name) + + this.composeQuery(socketId); + + var head = document.getElementsByTagName("head")[0] || document.documentElement; + head.insertBefore( script, head.firstChild ); + } + }; +}).call(this); From e7ba9e1c9d7541319146c2039d417568ae5cec99 Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Mon, 8 Jul 2013 14:18:54 -0700 Subject: [PATCH 24/62] Add Git.getConfigValue() --- src/app/git.coffee | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/app/git.coffee b/src/app/git.coffee index 19cee55cc..cc0eb2404 100644 --- a/src/app/git.coffee +++ b/src/app/git.coffee @@ -213,6 +213,8 @@ class Git # Returns an object with two keys, `ahead` and `behind`. These will always be greater than zero. getLineDiffs: (path, text) -> @getRepo().getLineDiffs(@relativize(path), text) + getConfigValue: (key) -> @getRepo().getConfigValue(key) + ### Internal ### refreshStatus: -> From 4fde8f0753640fe86eaa8aa8b35515b6ff1957cb Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Mon, 8 Jul 2013 14:25:47 -0700 Subject: [PATCH 25/62] Export Pusher class --- src/packages/collaboration/vendor/pusher.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/packages/collaboration/vendor/pusher.js b/src/packages/collaboration/vendor/pusher.js index 37d4fec7b..a6c015221 100644 --- a/src/packages/collaboration/vendor/pusher.js +++ b/src/packages/collaboration/vendor/pusher.js @@ -210,6 +210,7 @@ }).call(this); ;(function() { + Pusher = this.Pusher; Pusher.Util = { now: function() { if (Date.now) { @@ -3609,3 +3610,5 @@ } }; }).call(this); + +module.exports = this.Pusher; From b127492c9f462e316338cacf46396b0fff6b7031 Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Mon, 8 Jul 2013 14:26:17 -0700 Subject: [PATCH 26/62] Add initial buddy list --- .../collaboration/lib/buddy-list.coffee | 27 ++++++++++++++ .../collaboration/lib/collaboration.coffee | 6 +++ .../collaboration/lib/presence-utils.coffee | 37 +++++++++++++++++++ 3 files changed, 70 insertions(+) create mode 100644 src/packages/collaboration/lib/buddy-list.coffee create mode 100644 src/packages/collaboration/lib/presence-utils.coffee diff --git a/src/packages/collaboration/lib/buddy-list.coffee b/src/packages/collaboration/lib/buddy-list.coffee new file mode 100644 index 000000000..6f5c8eee9 --- /dev/null +++ b/src/packages/collaboration/lib/buddy-list.coffee @@ -0,0 +1,27 @@ +{$$} = require 'space-pen' +SelectList = require 'select-list' +{getAvailablePeople} = require './presence-utils' + +module.exports = +class BuddyList extends SelectList + @viewClass: -> "#{super} peoples-view overlay from-top" + + filterKey: 'name' + + initialize: -> + super + + @setArray(getAvailablePeople()) + @attach() + + attach: -> + super + + rootView.append(this) + @miniEditor.focus() + + itemForElement: ({info}) -> + $$ -> + @li class: 'two-lines', => + @div info.login, class: 'primary-line' + @div info.name, class: 'secondary-line' diff --git a/src/packages/collaboration/lib/collaboration.coffee b/src/packages/collaboration/lib/collaboration.coffee index e6a57d520..8d0068b40 100644 --- a/src/packages/collaboration/lib/collaboration.coffee +++ b/src/packages/collaboration/lib/collaboration.coffee @@ -1,6 +1,8 @@ JoinPromptView = require './join-prompt-view' {createSite, Document} = require 'telepath' {createPeer, connectDocument} = require './session-utils' +{advertisePresence} = require './presence-utils' +BuddyList = require './buddy-list' startSession = -> peer = createPeer() @@ -14,8 +16,12 @@ startSession = -> module.exports = activate: -> + advertisePresence() + sessionId = null + rootView.command 'collaboration:buddy-list', -> new BuddyList() + rootView.command 'collaboration:copy-session-id', -> pasteboard.write(sessionId) if sessionId diff --git a/src/packages/collaboration/lib/presence-utils.coffee b/src/packages/collaboration/lib/presence-utils.coffee new file mode 100644 index 000000000..1ac52df96 --- /dev/null +++ b/src/packages/collaboration/lib/presence-utils.coffee @@ -0,0 +1,37 @@ +keytar = require 'keytar' +_ = require 'underscore' +Pusher = require '../vendor/pusher.js' + +availablePeople = {} + +module.exports = + getAvailablePeople: -> _.values(availablePeople) + + advertisePresence: -> + token = keytar.getPassword('github.com', 'github') + return unless token + + pusher = new Pusher '490be67c75616316d386', + encrypted: true + authEndpoint: 'https://fierce-caverns-8387.herokuapp.com/pusher/auth' + auth: + params: + oauth_token: token + channel = pusher.subscribe('presence-atom') + channel.bind 'pusher:subscription_succeeded', (members) -> + availablePeople[members.me.id] = members.me + console.log 'subscribed to presence channel' + event = id: members.me.id + if git? + event.repository = + branch: git.getShortHead() + url: git.getConfigValue('remote.origin.url') + channel.trigger('client-details', event) + + channel.bind 'pusher:member_added', (member) -> + console.log 'member added', member + availablePeople[member.id] = member + + channel.bind 'pusher:member_removed', (member) -> + console.log 'member removed', member + availablePeople.delete(member.id) From 05748cd7dc129783867eaadb0bee7f2b2937a2fa Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Mon, 8 Jul 2013 15:01:19 -0700 Subject: [PATCH 27/62] Display repo and branch in buddy list --- .../collaboration/lib/buddy-list.coffee | 20 ++++++++++++++----- .../collaboration/lib/presence-utils.coffee | 20 +++++++++++++++---- 2 files changed, 31 insertions(+), 9 deletions(-) diff --git a/src/packages/collaboration/lib/buddy-list.coffee b/src/packages/collaboration/lib/buddy-list.coffee index 6f5c8eee9..b14efa803 100644 --- a/src/packages/collaboration/lib/buddy-list.coffee +++ b/src/packages/collaboration/lib/buddy-list.coffee @@ -1,3 +1,4 @@ +url = require 'url' {$$} = require 'space-pen' SelectList = require 'select-list' {getAvailablePeople} = require './presence-utils' @@ -6,12 +7,18 @@ module.exports = class BuddyList extends SelectList @viewClass: -> "#{super} peoples-view overlay from-top" - filterKey: 'name' + filterKey: 'filterText' initialize: -> super - @setArray(getAvailablePeople()) + people = getAvailablePeople() + people.forEach (person) -> + segments = [] + segments.push(person.user.login) + segments.push(person.user.name) if person.user.name + person.filterText = segments.join(' ') + @setArray(people) @attach() attach: -> @@ -20,8 +27,11 @@ class BuddyList extends SelectList rootView.append(this) @miniEditor.focus() - itemForElement: ({info}) -> + itemForElement: ({user, state}) -> $$ -> @li class: 'two-lines', => - @div info.login, class: 'primary-line' - @div info.name, class: 'secondary-line' + @div "#{user.login} (#{user.name})", class: 'primary-line' + if state.repository + [owner, name] = url.parse(state.repository.url).path.split('/')[-2..] + name = name.replace(/\.git$/, '') + @div "#{owner}/#{name}@#{state.repository.branch}", class: 'secondary-line' diff --git a/src/packages/collaboration/lib/presence-utils.coffee b/src/packages/collaboration/lib/presence-utils.coffee index 1ac52df96..b04345dc8 100644 --- a/src/packages/collaboration/lib/presence-utils.coffee +++ b/src/packages/collaboration/lib/presence-utils.coffee @@ -19,19 +19,31 @@ module.exports = oauth_token: token channel = pusher.subscribe('presence-atom') channel.bind 'pusher:subscription_succeeded', (members) -> - availablePeople[members.me.id] = members.me console.log 'subscribed to presence channel' event = id: members.me.id + event.state = {} if git? - event.repository = + event.state.repository = branch: git.getShortHead() url: git.getConfigValue('remote.origin.url') - channel.trigger('client-details', event) + channel.trigger('client-state-changed', event) + + # List self as available for debugging UI when no one else is around + self = + id: members.me.id + user: members.me.info + state: event.state + availablePeople[self.id] = self channel.bind 'pusher:member_added', (member) -> console.log 'member added', member - availablePeople[member.id] = member + availablePeople[member.id] = {user: member.info} channel.bind 'pusher:member_removed', (member) -> console.log 'member removed', member availablePeople.delete(member.id) + + channel.bind 'client-state-changed', (event) -> + console.log 'client state changed', event + if person = availablePeople[event.id] + person.state = event.state From 0fdb15f9a69a93b7826b0256094101c0de21e89d Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Mon, 8 Jul 2013 15:36:30 -0700 Subject: [PATCH 28/62] Move peer.js to vendor directory --- src/packages/collaboration/lib/session-utils.coffee | 2 +- src/packages/collaboration/{lib => vendor}/peer.js | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename src/packages/collaboration/{lib => vendor}/peer.js (100%) diff --git a/src/packages/collaboration/lib/session-utils.coffee b/src/packages/collaboration/lib/session-utils.coffee index 462b6552e..c6bdae7d8 100644 --- a/src/packages/collaboration/lib/session-utils.coffee +++ b/src/packages/collaboration/lib/session-utils.coffee @@ -1,4 +1,4 @@ -Peer = require './peer' +Peer = require '../vendor/peer.js' Guid = require 'guid' module.exports = diff --git a/src/packages/collaboration/lib/peer.js b/src/packages/collaboration/vendor/peer.js similarity index 100% rename from src/packages/collaboration/lib/peer.js rename to src/packages/collaboration/vendor/peer.js From aed7d3ed70aa87ccd44ee4c04ba10340e4527ec6 Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Mon, 8 Jul 2013 15:55:45 -0700 Subject: [PATCH 29/62] Make buddy list a permanent view on the right --- .../collaboration/lib/buddy-list.coffee | 48 ++++++++----------- .../collaboration/lib/buddy-view.coffee | 14 ++++++ .../collaboration/lib/collaboration.coffee | 10 ++-- ...{presence-utils.coffee => presence.coffee} | 34 ++++++++----- themes/atom-dark-ui/buddy-list.less | 7 +++ themes/atom-dark-ui/package.cson | 1 + 6 files changed, 71 insertions(+), 43 deletions(-) create mode 100644 src/packages/collaboration/lib/buddy-view.coffee rename src/packages/collaboration/lib/{presence-utils.coffee => presence.coffee} (61%) create mode 100644 themes/atom-dark-ui/buddy-list.less diff --git a/src/packages/collaboration/lib/buddy-list.coffee b/src/packages/collaboration/lib/buddy-list.coffee index b14efa803..20a8168fc 100644 --- a/src/packages/collaboration/lib/buddy-list.coffee +++ b/src/packages/collaboration/lib/buddy-list.coffee @@ -1,37 +1,31 @@ url = require 'url' {$$} = require 'space-pen' -SelectList = require 'select-list' -{getAvailablePeople} = require './presence-utils' +ScrollView = require 'scroll-view' +BuddyView = require './buddy-view' module.exports = -class BuddyList extends SelectList - @viewClass: -> "#{super} peoples-view overlay from-top" +class BuddyList extends ScrollView + @content: -> + @div class: 'buddy-list', tabindex: -1 - filterKey: 'filterText' - - initialize: -> + initialize: (@presence) -> super - people = getAvailablePeople() - people.forEach (person) -> - segments = [] - segments.push(person.user.login) - segments.push(person.user.name) if person.user.name - person.filterText = segments.join(' ') - @setArray(people) - @attach() + @presence.on 'person-added', -> @updateBuddies() + @presence.on 'person-removed', -> @updateBuddies() + @presence.on 'person-status-changed', -> @updateBuddies() + + toggle: -> + if @hasParent() + @detach() + else + @attach() attach: -> - super + rootView.horizontal.append(this) + @focus() + @updateBuddies() - rootView.append(this) - @miniEditor.focus() - - itemForElement: ({user, state}) -> - $$ -> - @li class: 'two-lines', => - @div "#{user.login} (#{user.name})", class: 'primary-line' - if state.repository - [owner, name] = url.parse(state.repository.url).path.split('/')[-2..] - name = name.replace(/\.git$/, '') - @div "#{owner}/#{name}@#{state.repository.branch}", class: 'secondary-line' + updateBuddies: -> + @empty() + @append(new BuddyView(buddy)) for buddy in @presence.getPeople() diff --git a/src/packages/collaboration/lib/buddy-view.coffee b/src/packages/collaboration/lib/buddy-view.coffee new file mode 100644 index 000000000..d21518291 --- /dev/null +++ b/src/packages/collaboration/lib/buddy-view.coffee @@ -0,0 +1,14 @@ +url = require 'url' +{View} = require 'space-pen' + +module.exports = +class BuddyView extends View + @content: ({user, state}) -> + @div class: 'two-lines', => + @div "#{user.login} (#{user.name})" + if state.repository + [owner, name] = url.parse(state.repository.url).path.split('/')[-2..] + name = name.replace(/\.git$/, '') + @div "#{owner}/#{name}@#{state.repository.branch}" + + initialize: (@buddy) -> diff --git a/src/packages/collaboration/lib/collaboration.coffee b/src/packages/collaboration/lib/collaboration.coffee index 8d0068b40..86aeae6dc 100644 --- a/src/packages/collaboration/lib/collaboration.coffee +++ b/src/packages/collaboration/lib/collaboration.coffee @@ -1,7 +1,7 @@ JoinPromptView = require './join-prompt-view' {createSite, Document} = require 'telepath' {createPeer, connectDocument} = require './session-utils' -{advertisePresence} = require './presence-utils' +Presence = require './presence' BuddyList = require './buddy-list' startSession = -> @@ -16,11 +16,13 @@ startSession = -> module.exports = activate: -> - advertisePresence() - + presence = new Presence() + buddyList = null sessionId = null - rootView.command 'collaboration:buddy-list', -> new BuddyList() + rootView.command 'collaboration:toggle-buddy-list', -> + buddyList ?= new BuddyList(presence) + buddyList.toggle() rootView.command 'collaboration:copy-session-id', -> pasteboard.write(sessionId) if sessionId diff --git a/src/packages/collaboration/lib/presence-utils.coffee b/src/packages/collaboration/lib/presence.coffee similarity index 61% rename from src/packages/collaboration/lib/presence-utils.coffee rename to src/packages/collaboration/lib/presence.coffee index b04345dc8..ce2a54735 100644 --- a/src/packages/collaboration/lib/presence-utils.coffee +++ b/src/packages/collaboration/lib/presence.coffee @@ -2,12 +2,17 @@ keytar = require 'keytar' _ = require 'underscore' Pusher = require '../vendor/pusher.js' -availablePeople = {} - module.exports = - getAvailablePeople: -> _.values(availablePeople) +class Presence + _.extend @prototype, require('event-emitter') - advertisePresence: -> + people: null + + constructor: -> + @people = {} + @connect() + + connect: -> token = keytar.getPassword('github.com', 'github') return unless token @@ -18,7 +23,7 @@ module.exports = params: oauth_token: token channel = pusher.subscribe('presence-atom') - channel.bind 'pusher:subscription_succeeded', (members) -> + channel.bind 'pusher:subscription_succeeded', (members) => console.log 'subscribed to presence channel' event = id: members.me.id event.state = {} @@ -33,17 +38,22 @@ module.exports = id: members.me.id user: members.me.info state: event.state - availablePeople[self.id] = self + @people[self.id] = self - channel.bind 'pusher:member_added', (member) -> + channel.bind 'pusher:member_added', (member) => console.log 'member added', member - availablePeople[member.id] = {user: member.info} + @people[member.id] = {user: member.info} + @trigger 'person-added', @people[member.id] - channel.bind 'pusher:member_removed', (member) -> + channel.bind 'pusher:member_removed', (member) => console.log 'member removed', member - availablePeople.delete(member.id) + @people.delete(member.id) + @trigger 'person-removed' - channel.bind 'client-state-changed', (event) -> + channel.bind 'client-state-changed', (event) => console.log 'client state changed', event - if person = availablePeople[event.id] + if person = @people[event.id] person.state = event.state + @trigger 'person-status-changed', person + + getPeople: -> _.values(@people) diff --git a/themes/atom-dark-ui/buddy-list.less b/themes/atom-dark-ui/buddy-list.less new file mode 100644 index 000000000..4c9279062 --- /dev/null +++ b/themes/atom-dark-ui/buddy-list.less @@ -0,0 +1,7 @@ +.buddy-list { + background: #1b1c1e; + box-shadow: + 1px 0 0 #131516, + inset -1px 0 0 rgba(255, 255, 255, 0.02), + 1px 0 3px rgba(0, 0, 0, 0.2); +} diff --git a/themes/atom-dark-ui/package.cson b/themes/atom-dark-ui/package.cson index bf65ab7d7..a59025250 100644 --- a/themes/atom-dark-ui/package.cson +++ b/themes/atom-dark-ui/package.cson @@ -10,4 +10,5 @@ 'blurred' 'image-view' 'archive-view' + 'buddy-list' ] From 533f91e7ac60ccad08329b7a51d2dcd9ce74a432 Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Mon, 8 Jul 2013 16:12:04 -0700 Subject: [PATCH 30/62] Remove unused class --- src/packages/collaboration/lib/buddy-view.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/packages/collaboration/lib/buddy-view.coffee b/src/packages/collaboration/lib/buddy-view.coffee index d21518291..628f5103b 100644 --- a/src/packages/collaboration/lib/buddy-view.coffee +++ b/src/packages/collaboration/lib/buddy-view.coffee @@ -4,7 +4,7 @@ url = require 'url' module.exports = class BuddyView extends View @content: ({user, state}) -> - @div class: 'two-lines', => + @div => @div "#{user.login} (#{user.name})" if state.repository [owner, name] = url.parse(state.repository.url).path.split('/')[-2..] From aab4d7a78ba5e0e4180d25a64ddd1c17085c5c0f Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Mon, 8 Jul 2013 17:02:03 -0700 Subject: [PATCH 31/62] Add avatar to buddy view --- .../collaboration/lib/buddy-view.coffee | 14 ++++++- .../stylesheets/collaboration.less | 42 +++++++++++++++++++ themes/atom-dark-ui/buddy-list.less | 1 + 3 files changed, 55 insertions(+), 2 deletions(-) create mode 100644 src/packages/collaboration/stylesheets/collaboration.less diff --git a/src/packages/collaboration/lib/buddy-view.coffee b/src/packages/collaboration/lib/buddy-view.coffee index 628f5103b..cd0e41e62 100644 --- a/src/packages/collaboration/lib/buddy-view.coffee +++ b/src/packages/collaboration/lib/buddy-view.coffee @@ -5,10 +5,20 @@ module.exports = class BuddyView extends View @content: ({user, state}) -> @div => - @div "#{user.login} (#{user.name})" + @div class: 'buddy-name', => + @img class: 'avatar', outlet: 'avatar' + @span user.login if state.repository [owner, name] = url.parse(state.repository.url).path.split('/')[-2..] name = name.replace(/\.git$/, '') - @div "#{owner}/#{name}@#{state.repository.branch}" + @div class: 'repo-name', => + @span name + if state.repository.branch + @div class: 'branch-name', => + @span state.repository.branch initialize: (@buddy) -> + if @buddy.user.avatarUrl + @avatar.attr('src', @buddy.user.avatarUrl) + else + @avatar.hide() diff --git a/src/packages/collaboration/stylesheets/collaboration.less b/src/packages/collaboration/stylesheets/collaboration.less new file mode 100644 index 000000000..eff44e42f --- /dev/null +++ b/src/packages/collaboration/stylesheets/collaboration.less @@ -0,0 +1,42 @@ +@import "bootstrap/less/variables.less"; +@import "octicon-mixins.less"; + +.buddy-list { + @item-line-height: @line-height-base * 1.25; + padding: 10px; + -webkit-user-select: none; + cursor: pointer; + + .buddy-name { + line-height: 30px; + margin-bottom: 5px; + + .avatar { + border-radius: 3px; + height: 30px; + width: 30px; + float: left; + margin-right: 5px; + } + } + + .repo-name { + margin-left: 5px; + line-height: @item-line-height; + .mini-icon(public-repo); + + span { + padding-left: 5px; + } + } + + .branch-name { + margin-left: 5px; + line-height: @item-line-height; + .mini-icon(branch); + + span { + padding-left: 5px; + } + } +} diff --git a/themes/atom-dark-ui/buddy-list.less b/themes/atom-dark-ui/buddy-list.less index 4c9279062..b3f0a1862 100644 --- a/themes/atom-dark-ui/buddy-list.less +++ b/themes/atom-dark-ui/buddy-list.less @@ -4,4 +4,5 @@ 1px 0 0 #131516, inset -1px 0 0 rgba(255, 255, 255, 0.02), 1px 0 3px rgba(0, 0, 0, 0.2); + color: #cecece; } From b55e62f2ab0a87e7f827b74045f8d21c262828d8 Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Mon, 8 Jul 2013 17:10:17 -0700 Subject: [PATCH 32/62] Use fat arrow for callbacks --- src/packages/collaboration/lib/buddy-list.coffee | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/packages/collaboration/lib/buddy-list.coffee b/src/packages/collaboration/lib/buddy-list.coffee index 20a8168fc..e103591de 100644 --- a/src/packages/collaboration/lib/buddy-list.coffee +++ b/src/packages/collaboration/lib/buddy-list.coffee @@ -11,9 +11,9 @@ class BuddyList extends ScrollView initialize: (@presence) -> super - @presence.on 'person-added', -> @updateBuddies() - @presence.on 'person-removed', -> @updateBuddies() - @presence.on 'person-status-changed', -> @updateBuddies() + @presence.on 'person-added', => @updateBuddies() + @presence.on 'person-removed', => @updateBuddies() + @presence.on 'person-status-changed', => @updateBuddies() toggle: -> if @hasParent() From 09e73b16feea60e0056e2ae1f01ad3a8dbb200c6 Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Mon, 8 Jul 2013 18:16:55 -0700 Subject: [PATCH 33/62] Show all open windows in buddy list --- .../collaboration/lib/buddy-view.coffee | 21 ++++++----- .../collaboration/lib/presence.coffee | 36 +++++++++++++------ 2 files changed, 38 insertions(+), 19 deletions(-) diff --git a/src/packages/collaboration/lib/buddy-view.coffee b/src/packages/collaboration/lib/buddy-view.coffee index cd0e41e62..974b3497e 100644 --- a/src/packages/collaboration/lib/buddy-view.coffee +++ b/src/packages/collaboration/lib/buddy-view.coffee @@ -3,19 +3,22 @@ url = require 'url' module.exports = class BuddyView extends View - @content: ({user, state}) -> + @content: ({user, windows}) -> @div => @div class: 'buddy-name', => @img class: 'avatar', outlet: 'avatar' @span user.login - if state.repository - [owner, name] = url.parse(state.repository.url).path.split('/')[-2..] - name = name.replace(/\.git$/, '') - @div class: 'repo-name', => - @span name - if state.repository.branch - @div class: 'branch-name', => - @span state.repository.branch + + for id, windowState of windows + {repository} = windowState + continue unless repository? + [owner, name] = url.parse(repository.url).path.split('/')[-2..] + name = name.replace(/\.git$/, '') + @div class: 'repo-name', => + @span name + if repository.branch + @div class: 'branch-name', => + @span repository.branch initialize: (@buddy) -> if @buddy.user.avatarUrl diff --git a/src/packages/collaboration/lib/presence.coffee b/src/packages/collaboration/lib/presence.coffee index ce2a54735..d9a12fea6 100644 --- a/src/packages/collaboration/lib/presence.coffee +++ b/src/packages/collaboration/lib/presence.coffee @@ -1,3 +1,5 @@ +guid = require 'guid' +$ = require 'jquery' keytar = require 'keytar' _ = require 'underscore' Pusher = require '../vendor/pusher.js' @@ -7,9 +9,12 @@ class Presence _.extend @prototype, require('event-emitter') people: null + personId: null + windowId: null constructor: -> @people = {} + @windowId = guid.create().toString() @connect() connect: -> @@ -25,24 +30,26 @@ class Presence channel = pusher.subscribe('presence-atom') channel.bind 'pusher:subscription_succeeded', (members) => console.log 'subscribed to presence channel' - event = id: members.me.id - event.state = {} + @personId = members.me.id + event = id: @personId + event.window = id: @windowId if git? - event.state.repository = + event.window.repository = branch: git.getShortHead() url: git.getConfigValue('remote.origin.url') - channel.trigger('client-state-changed', event) + channel.trigger('client-window-opened', event) # List self as available for debugging UI when no one else is around self = - id: members.me.id + id: @personId user: members.me.info - state: event.state + windows: {} + self.windows[@windowId] = event.window @people[self.id] = self channel.bind 'pusher:member_added', (member) => console.log 'member added', member - @people[member.id] = {user: member.info} + @people[member.id] = {user: member.info, windows: {}} @trigger 'person-added', @people[member.id] channel.bind 'pusher:member_removed', (member) => @@ -50,10 +57,19 @@ class Presence @people.delete(member.id) @trigger 'person-removed' - channel.bind 'client-state-changed', (event) => - console.log 'client state changed', event + channel.bind 'client-window-opened', (event) => + console.log 'window opened', event if person = @people[event.id] - person.state = event.state + person.windows[event.window.id] = event.window @trigger 'person-status-changed', person + channel.bind 'client-window-closed', (event) => + console.log 'window closed', event + if person = @people[event.id] + delete person.windows[event.windowId] + @trigger 'person-status-changed', person + + $(window).on 'beforeunload', => + channel.trigger 'client-window-closed', {id: @personId, @windowId} + getPeople: -> _.values(@people) From 0fd44994eec318928c80834607c41c79d179375b Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Mon, 8 Jul 2013 18:18:03 -0700 Subject: [PATCH 34/62] Trigger status changed for self --- src/packages/collaboration/lib/presence.coffee | 1 + 1 file changed, 1 insertion(+) diff --git a/src/packages/collaboration/lib/presence.coffee b/src/packages/collaboration/lib/presence.coffee index d9a12fea6..bf025aed2 100644 --- a/src/packages/collaboration/lib/presence.coffee +++ b/src/packages/collaboration/lib/presence.coffee @@ -46,6 +46,7 @@ class Presence windows: {} self.windows[@windowId] = event.window @people[self.id] = self + @trigger 'person-status-changed', self channel.bind 'pusher:member_added', (member) => console.log 'member added', member From be078b2b41e8bd3cfbb35f1dd581848122b5053e Mon Sep 17 00:00:00 2001 From: Corey Johnson & Kevin Sawicki Date: Tue, 9 Jul 2013 12:05:35 -0700 Subject: [PATCH 35/62] Add share button to buddy list --- .../collaboration/lib/buddy-list.coffee | 33 ++++++++++++-- .../collaboration/lib/collaboration.coffee | 24 ++++------ .../collaboration/lib/session-utils.coffee | 2 +- .../collaboration/lib/sharing-session.coffee | 44 +++++++++++++++++++ src/packages/collaboration/vendor/peer.js | 2 +- 5 files changed, 83 insertions(+), 22 deletions(-) create mode 100644 src/packages/collaboration/lib/sharing-session.coffee diff --git a/src/packages/collaboration/lib/buddy-list.coffee b/src/packages/collaboration/lib/buddy-list.coffee index e103591de..b681530b0 100644 --- a/src/packages/collaboration/lib/buddy-list.coffee +++ b/src/packages/collaboration/lib/buddy-list.coffee @@ -6,14 +6,39 @@ BuddyView = require './buddy-view' module.exports = class BuddyList extends ScrollView @content: -> - @div class: 'buddy-list', tabindex: -1 + @div class: 'buddy-list', tabindex: -1, => + @button outlet: 'shareButton', type: 'button', class: 'btn btn-default' + @div outlet: 'buddies' - initialize: (@presence) -> + presence: null + sharingSession: null + + initialize: (@presence, @sharingSession) -> super + if @sharingSession.isSharing() + @shareButton.text('Stop') + else + @shareButton.text('Start') + @presence.on 'person-added', => @updateBuddies() @presence.on 'person-removed', => @updateBuddies() @presence.on 'person-status-changed', => @updateBuddies() + @shareButton.on 'click', => + @shareButton.disable() + + if @sharingSession.isSharing() + @shareButton.text('Stopping...') + @sharingSession.stop() + else + @shareButton.text('Starting...') + @sharingSession.start() + + @sharingSession.on 'started', => + @shareButton.text('Stop').enable() + + @sharingSession.on 'stopped', => + @shareButton.text('Start').enable() toggle: -> if @hasParent() @@ -27,5 +52,5 @@ class BuddyList extends ScrollView @updateBuddies() updateBuddies: -> - @empty() - @append(new BuddyView(buddy)) for buddy in @presence.getPeople() + @buddies.empty() + @buddies.append(new BuddyView(buddy)) for buddy in @presence.getPeople() diff --git a/src/packages/collaboration/lib/collaboration.coffee b/src/packages/collaboration/lib/collaboration.coffee index 86aeae6dc..a1ac1ab73 100644 --- a/src/packages/collaboration/lib/collaboration.coffee +++ b/src/packages/collaboration/lib/collaboration.coffee @@ -1,34 +1,24 @@ -JoinPromptView = require './join-prompt-view' -{createSite, Document} = require 'telepath' -{createPeer, connectDocument} = require './session-utils' Presence = require './presence' +SharingSession = require './sharing-session' BuddyList = require './buddy-list' - -startSession = -> - peer = createPeer() - peer.on 'connection', (connection) -> - connection.on 'open', -> - console.log 'sending document' - windowState = atom.getWindowState() - connection.send(windowState.serialize()) - connectDocument(windowState, connection) - peer.id +JoinPromptView = require './join-prompt-view' module.exports = activate: -> presence = new Presence() + sharingSession = new SharingSession() buddyList = null - sessionId = null rootView.command 'collaboration:toggle-buddy-list', -> - buddyList ?= new BuddyList(presence) + buddyList ?= new BuddyList(presence, sharingSession) buddyList.toggle() rootView.command 'collaboration:copy-session-id', -> + sessionId = sharingSession.getId() pasteboard.write(sessionId) if sessionId rootView.command 'collaboration:start-session', -> - if sessionId = startSession() + if sessionId = sharingSession.start() pasteboard.write(sessionId) rootView.command 'collaboration:join-session', -> @@ -38,3 +28,5 @@ module.exports = resourcePath: window.resourcePath sessionId: id atom.openWindow(windowSettings) + + rootView.trigger 'collaboration:toggle-buddy-list' # TEMP diff --git a/src/packages/collaboration/lib/session-utils.coffee b/src/packages/collaboration/lib/session-utils.coffee index c6bdae7d8..706a55d09 100644 --- a/src/packages/collaboration/lib/session-utils.coffee +++ b/src/packages/collaboration/lib/session-utils.coffee @@ -4,7 +4,7 @@ Guid = require 'guid' module.exports = createPeer: -> id = Guid.create().toString() - new Peer(id, host: 'ec2-54-218-51-127.us-west-2.compute.amazonaws.com', port: 8080) + new Peer(id, key: '0njqmaln320dlsor') connectDocument: (doc, connection) -> nextOutputEventId = 1 diff --git a/src/packages/collaboration/lib/sharing-session.coffee b/src/packages/collaboration/lib/sharing-session.coffee new file mode 100644 index 000000000..560031c76 --- /dev/null +++ b/src/packages/collaboration/lib/sharing-session.coffee @@ -0,0 +1,44 @@ +_ = require 'underscore' +{createPeer, connectDocument} = require './session-utils' + +module.exports = +class SharingSession + _.extend @prototype, require('event-emitter') + + peer: null + sharing: false + + start: -> + return if @peer + + @peer = createPeer() + @peer.on 'connection', (connection) -> + connection.on 'open', -> + console.log 'sending document' + windowState = atom.getWindowState() + connection.send(windowState.serialize()) + connectDocument(windowState, connection) + + @peer.on 'open', => + console.log 'sharing session started' + @sharing = true + @trigger 'started' + + @peer.on 'close', => + console.log 'sharing session stopped' + @sharing = false + @trigger 'stopped' + + @getId() + + stop: -> + return unless @peer? + + @peer.destroy() + @peer = null + + getId: -> + @peer.id + + isSharing: -> + @sharing diff --git a/src/packages/collaboration/vendor/peer.js b/src/packages/collaboration/vendor/peer.js index fc63d2717..8e6bd6954 100644 --- a/src/packages/collaboration/vendor/peer.js +++ b/src/packages/collaboration/vendor/peer.js @@ -782,7 +782,7 @@ var util = { global.attachEvent('onmessage', handleMessage); } return setZeroTimeoutPostMessage; - }(this)), + }(window)), blobToArrayBuffer: function(blob, cb){ var fr = new FileReader(); From 40d500949b98a3c9afefda8f0e06e0650858482a Mon Sep 17 00:00:00 2001 From: Corey Johnson & Kevin Sawicki Date: Tue, 9 Jul 2013 15:08:53 -0700 Subject: [PATCH 36/62] Remove presence from collaboration package. --- .../collaboration/lib/buddy-list.coffee | 56 -------------- .../collaboration/lib/buddy-view.coffee | 27 ------- .../lib/collaboration-view.coffee | 38 ++++++++++ .../collaboration/lib/collaboration.coffee | 12 +-- .../collaboration/lib/presence.coffee | 76 ------------------- .../stylesheets/collaboration.less | 37 +++------ themes/atom-dark-ui/buddy-list.less | 2 +- 7 files changed, 50 insertions(+), 198 deletions(-) delete mode 100644 src/packages/collaboration/lib/buddy-list.coffee delete mode 100644 src/packages/collaboration/lib/buddy-view.coffee create mode 100644 src/packages/collaboration/lib/collaboration-view.coffee delete mode 100644 src/packages/collaboration/lib/presence.coffee diff --git a/src/packages/collaboration/lib/buddy-list.coffee b/src/packages/collaboration/lib/buddy-list.coffee deleted file mode 100644 index b681530b0..000000000 --- a/src/packages/collaboration/lib/buddy-list.coffee +++ /dev/null @@ -1,56 +0,0 @@ -url = require 'url' -{$$} = require 'space-pen' -ScrollView = require 'scroll-view' -BuddyView = require './buddy-view' - -module.exports = -class BuddyList extends ScrollView - @content: -> - @div class: 'buddy-list', tabindex: -1, => - @button outlet: 'shareButton', type: 'button', class: 'btn btn-default' - @div outlet: 'buddies' - - presence: null - sharingSession: null - - initialize: (@presence, @sharingSession) -> - super - - if @sharingSession.isSharing() - @shareButton.text('Stop') - else - @shareButton.text('Start') - - @presence.on 'person-added', => @updateBuddies() - @presence.on 'person-removed', => @updateBuddies() - @presence.on 'person-status-changed', => @updateBuddies() - @shareButton.on 'click', => - @shareButton.disable() - - if @sharingSession.isSharing() - @shareButton.text('Stopping...') - @sharingSession.stop() - else - @shareButton.text('Starting...') - @sharingSession.start() - - @sharingSession.on 'started', => - @shareButton.text('Stop').enable() - - @sharingSession.on 'stopped', => - @shareButton.text('Start').enable() - - toggle: -> - if @hasParent() - @detach() - else - @attach() - - attach: -> - rootView.horizontal.append(this) - @focus() - @updateBuddies() - - updateBuddies: -> - @buddies.empty() - @buddies.append(new BuddyView(buddy)) for buddy in @presence.getPeople() diff --git a/src/packages/collaboration/lib/buddy-view.coffee b/src/packages/collaboration/lib/buddy-view.coffee deleted file mode 100644 index 974b3497e..000000000 --- a/src/packages/collaboration/lib/buddy-view.coffee +++ /dev/null @@ -1,27 +0,0 @@ -url = require 'url' -{View} = require 'space-pen' - -module.exports = -class BuddyView extends View - @content: ({user, windows}) -> - @div => - @div class: 'buddy-name', => - @img class: 'avatar', outlet: 'avatar' - @span user.login - - for id, windowState of windows - {repository} = windowState - continue unless repository? - [owner, name] = url.parse(repository.url).path.split('/')[-2..] - name = name.replace(/\.git$/, '') - @div class: 'repo-name', => - @span name - if repository.branch - @div class: 'branch-name', => - @span repository.branch - - initialize: (@buddy) -> - if @buddy.user.avatarUrl - @avatar.attr('src', @buddy.user.avatarUrl) - else - @avatar.hide() diff --git a/src/packages/collaboration/lib/collaboration-view.coffee b/src/packages/collaboration/lib/collaboration-view.coffee new file mode 100644 index 000000000..e527d3d57 --- /dev/null +++ b/src/packages/collaboration/lib/collaboration-view.coffee @@ -0,0 +1,38 @@ +url = require 'url' +{$$, View} = require 'space-pen' + +module.exports = +class CollaborationView extends View + @content: -> + @div class: 'collaboration', tabindex: -1, => + @div outlet: 'share', type: 'button', class: 'share' + @div outlet: 'participants' + + sharingSession: null + + initialize: (@sharingSession) -> + if @sharingSession.isSharing() + @share.addClass('running') + + @share.on 'click', => + @share.disable() + + if @sharingSession.isSharing() + @sharingSession.stop() + else + @sharingSession.start() + + @sharingSession.on 'started stopped', => + @share.toggleClass('running').enable() + + @attach() + + toggle: -> + if @hasParent() + @detach() + else + @attach() + + attach: -> + rootView.horizontal.append(this) + @focus() diff --git a/src/packages/collaboration/lib/collaboration.coffee b/src/packages/collaboration/lib/collaboration.coffee index a1ac1ab73..92bfd7278 100644 --- a/src/packages/collaboration/lib/collaboration.coffee +++ b/src/packages/collaboration/lib/collaboration.coffee @@ -1,23 +1,17 @@ -Presence = require './presence' +CollaborationView = require './collaboration-view' SharingSession = require './sharing-session' -BuddyList = require './buddy-list' JoinPromptView = require './join-prompt-view' module.exports = activate: -> - presence = new Presence() sharingSession = new SharingSession() - buddyList = null - - rootView.command 'collaboration:toggle-buddy-list', -> - buddyList ?= new BuddyList(presence, sharingSession) - buddyList.toggle() rootView.command 'collaboration:copy-session-id', -> sessionId = sharingSession.getId() pasteboard.write(sessionId) if sessionId rootView.command 'collaboration:start-session', -> + new CollaborationView(sharingSession) if sessionId = sharingSession.start() pasteboard.write(sessionId) @@ -28,5 +22,3 @@ module.exports = resourcePath: window.resourcePath sessionId: id atom.openWindow(windowSettings) - - rootView.trigger 'collaboration:toggle-buddy-list' # TEMP diff --git a/src/packages/collaboration/lib/presence.coffee b/src/packages/collaboration/lib/presence.coffee deleted file mode 100644 index bf025aed2..000000000 --- a/src/packages/collaboration/lib/presence.coffee +++ /dev/null @@ -1,76 +0,0 @@ -guid = require 'guid' -$ = require 'jquery' -keytar = require 'keytar' -_ = require 'underscore' -Pusher = require '../vendor/pusher.js' - -module.exports = -class Presence - _.extend @prototype, require('event-emitter') - - people: null - personId: null - windowId: null - - constructor: -> - @people = {} - @windowId = guid.create().toString() - @connect() - - connect: -> - token = keytar.getPassword('github.com', 'github') - return unless token - - pusher = new Pusher '490be67c75616316d386', - encrypted: true - authEndpoint: 'https://fierce-caverns-8387.herokuapp.com/pusher/auth' - auth: - params: - oauth_token: token - channel = pusher.subscribe('presence-atom') - channel.bind 'pusher:subscription_succeeded', (members) => - console.log 'subscribed to presence channel' - @personId = members.me.id - event = id: @personId - event.window = id: @windowId - if git? - event.window.repository = - branch: git.getShortHead() - url: git.getConfigValue('remote.origin.url') - channel.trigger('client-window-opened', event) - - # List self as available for debugging UI when no one else is around - self = - id: @personId - user: members.me.info - windows: {} - self.windows[@windowId] = event.window - @people[self.id] = self - @trigger 'person-status-changed', self - - channel.bind 'pusher:member_added', (member) => - console.log 'member added', member - @people[member.id] = {user: member.info, windows: {}} - @trigger 'person-added', @people[member.id] - - channel.bind 'pusher:member_removed', (member) => - console.log 'member removed', member - @people.delete(member.id) - @trigger 'person-removed' - - channel.bind 'client-window-opened', (event) => - console.log 'window opened', event - if person = @people[event.id] - person.windows[event.window.id] = event.window - @trigger 'person-status-changed', person - - channel.bind 'client-window-closed', (event) => - console.log 'window closed', event - if person = @people[event.id] - delete person.windows[event.windowId] - @trigger 'person-status-changed', person - - $(window).on 'beforeunload', => - channel.trigger 'client-window-closed', {id: @personId, @windowId} - - getPeople: -> _.values(@people) diff --git a/src/packages/collaboration/stylesheets/collaboration.less b/src/packages/collaboration/stylesheets/collaboration.less index eff44e42f..c0e0aa047 100644 --- a/src/packages/collaboration/stylesheets/collaboration.less +++ b/src/packages/collaboration/stylesheets/collaboration.less @@ -1,42 +1,23 @@ @import "bootstrap/less/variables.less"; @import "octicon-mixins.less"; +@runningColor: #99CC99; -.buddy-list { + +.collaboration { @item-line-height: @line-height-base * 1.25; padding: 10px; -webkit-user-select: none; cursor: pointer; - .buddy-name { - line-height: 30px; - margin-bottom: 5px; - - .avatar { - border-radius: 3px; - height: 30px; - width: 30px; - float: left; - margin-right: 5px; - } + .share { + .mini-icon(notifications); } - .repo-name { - margin-left: 5px; - line-height: @item-line-height; - .mini-icon(public-repo); + .running { + color: @runningColor; - span { - padding-left: 5px; - } - } - - .branch-name { - margin-left: 5px; - line-height: @item-line-height; - .mini-icon(branch); - - span { - padding-left: 5px; + &:hover { + color: lighten(@runningColor, 15%); } } } diff --git a/themes/atom-dark-ui/buddy-list.less b/themes/atom-dark-ui/buddy-list.less index b3f0a1862..c63508993 100644 --- a/themes/atom-dark-ui/buddy-list.less +++ b/themes/atom-dark-ui/buddy-list.less @@ -1,4 +1,4 @@ -.buddy-list { +.collaboration { background: #1b1c1e; box-shadow: 1px 0 0 #131516, From 460a09f9ebb3d052a91ebc46f41411fe94148e05 Mon Sep 17 00:00:00 2001 From: Corey Johnson & Kevin Sawicki Date: Wed, 10 Jul 2013 08:42:46 -0700 Subject: [PATCH 37/62] Show participants in the session --- .../collaboration/lib/bootstrap.coffee | 17 ++--- .../lib/collaboration-view.coffee | 38 ----------- .../collaboration/lib/collaboration.coffee | 38 ++++++----- .../collaboration/lib/guest-session.coffee | 30 +++++++++ .../collaboration/lib/guest-view.coffee | 34 ++++++++++ .../collaboration/lib/host-session.coffee | 63 +++++++++++++++++++ .../collaboration/lib/host-view.coffee | 46 ++++++++++++++ .../collaboration/lib/session-utils.coffee | 1 + .../collaboration/lib/sharing-session.coffee | 44 ------------- .../stylesheets/collaboration.less | 6 +- 10 files changed, 205 insertions(+), 112 deletions(-) delete mode 100644 src/packages/collaboration/lib/collaboration-view.coffee create mode 100644 src/packages/collaboration/lib/guest-session.coffee create mode 100644 src/packages/collaboration/lib/guest-view.coffee create mode 100644 src/packages/collaboration/lib/host-session.coffee create mode 100644 src/packages/collaboration/lib/host-view.coffee delete mode 100644 src/packages/collaboration/lib/sharing-session.coffee diff --git a/src/packages/collaboration/lib/bootstrap.coffee b/src/packages/collaboration/lib/bootstrap.coffee index cf09d143e..bf13b8fe9 100644 --- a/src/packages/collaboration/lib/bootstrap.coffee +++ b/src/packages/collaboration/lib/bootstrap.coffee @@ -2,8 +2,7 @@ require 'atom' require 'window' $ = require 'jquery' {$$} = require 'space-pen' -{createPeer, connectDocument} = require './session-utils' -{createSite, Document} = require 'telepath' +GuestSession = require './guest-session' window.setDimensions(width: 350, height: 100) window.setUpEnvironment('editor') @@ -15,13 +14,7 @@ loadingView = $$ -> $(window.rootViewParentSelector).append(loadingView) atom.show() -peer = createPeer() -connection = peer.connect(sessionId, reliable: true) -connection.on 'open', -> - console.log 'connection opened' - connection.once 'data', (data) -> - loadingView.remove() - console.log 'received document' - atom.windowState = Document.deserialize(createSite(peer.id), data) - connectDocument(atom.windowState, connection) - window.startEditorWindow() +atom.guestSession = new GuestSession(sessionId) +atom.guestSession.on 'started', -> + loadingView.remove() + window.startEditorWindow() diff --git a/src/packages/collaboration/lib/collaboration-view.coffee b/src/packages/collaboration/lib/collaboration-view.coffee deleted file mode 100644 index e527d3d57..000000000 --- a/src/packages/collaboration/lib/collaboration-view.coffee +++ /dev/null @@ -1,38 +0,0 @@ -url = require 'url' -{$$, View} = require 'space-pen' - -module.exports = -class CollaborationView extends View - @content: -> - @div class: 'collaboration', tabindex: -1, => - @div outlet: 'share', type: 'button', class: 'share' - @div outlet: 'participants' - - sharingSession: null - - initialize: (@sharingSession) -> - if @sharingSession.isSharing() - @share.addClass('running') - - @share.on 'click', => - @share.disable() - - if @sharingSession.isSharing() - @sharingSession.stop() - else - @sharingSession.start() - - @sharingSession.on 'started stopped', => - @share.toggleClass('running').enable() - - @attach() - - toggle: -> - if @hasParent() - @detach() - else - @attach() - - attach: -> - rootView.horizontal.append(this) - @focus() diff --git a/src/packages/collaboration/lib/collaboration.coffee b/src/packages/collaboration/lib/collaboration.coffee index 92bfd7278..0e7c87116 100644 --- a/src/packages/collaboration/lib/collaboration.coffee +++ b/src/packages/collaboration/lib/collaboration.coffee @@ -1,24 +1,28 @@ -CollaborationView = require './collaboration-view' -SharingSession = require './sharing-session' +GuestView = require './guest-view' +HostView = require './host-view' +HostSession = require './host-session' JoinPromptView = require './join-prompt-view' module.exports = activate: -> - sharingSession = new SharingSession() + if atom.getLoadSettings().sessionId + new GuestView(atom.guestSession) + else + hostSession = new HostSession() - rootView.command 'collaboration:copy-session-id', -> - sessionId = sharingSession.getId() - pasteboard.write(sessionId) if sessionId + rootView.command 'collaboration:copy-session-id', -> + sessionId = hostSession.getId() + pasteboard.write(sessionId) if sessionId - rootView.command 'collaboration:start-session', -> - new CollaborationView(sharingSession) - if sessionId = sharingSession.start() - pasteboard.write(sessionId) + rootView.command 'collaboration:start-session', -> + new HostView(hostSession) + if sessionId = hostSession.start() + pasteboard.write(sessionId) - rootView.command 'collaboration:join-session', -> - new JoinPromptView (id) -> - windowSettings = - bootstrapScript: require.resolve('collaboration/lib/bootstrap') - resourcePath: window.resourcePath - sessionId: id - atom.openWindow(windowSettings) + rootView.command 'collaboration:join-session', -> + new JoinPromptView (id) -> + windowSettings = + bootstrapScript: require.resolve('collaboration/lib/bootstrap') + resourcePath: window.resourcePath + sessionId: id + atom.openWindow(windowSettings) diff --git a/src/packages/collaboration/lib/guest-session.coffee b/src/packages/collaboration/lib/guest-session.coffee new file mode 100644 index 000000000..55d9a7af2 --- /dev/null +++ b/src/packages/collaboration/lib/guest-session.coffee @@ -0,0 +1,30 @@ +_ = require 'underscore' +telepath = require 'telepath' +{connectDocument, createPeer} = require './session-utils' + +module.exports = +class GuestSession + _.extend @prototype, require('event-emitter') + + participants: null + peer: null + + constructor: (sessionId) -> + @peer = createPeer() + connection = @peer.connect(sessionId, {reliable: true, connectionId: @getId()}) + connection.on 'open', => + console.log 'connection opened' + connection.once 'data', (data) => + console.log 'received document' + doc = telepath.Document.deserialize(telepath.createSite(@getId()), data) + atom.windowState = doc.get('windowState') + @participants = doc.get('participants') + connectDocument(doc, connection) + + @trigger 'started' + + @participants.push + id: @getId() + email: git.getConfigValue('user.email') + + getId: -> @peer.id diff --git a/src/packages/collaboration/lib/guest-view.coffee b/src/packages/collaboration/lib/guest-view.coffee new file mode 100644 index 000000000..f499ab9da --- /dev/null +++ b/src/packages/collaboration/lib/guest-view.coffee @@ -0,0 +1,34 @@ +{$$, View} = require 'space-pen' + +module.exports = +class GuestView extends View + @content: -> + @div class: 'collaboration', tabindex: -1, => + @div class: 'guest' + @div outlet: 'participants' + + guestSession: null + + initialize: (@guestSession) -> + @guestSession.on 'participants-changed', (participants) => + @updateParticipants(participants) + + @updateParticipants(@guestSession.participants.toObject()) + + @attach() + + updateParticipants: (participants) -> + @participants.empty() + for {email, id} in participants when id isnt @guestSession.getId() + @participants.append $$ -> + @div email + + toggle: -> + if @hasParent() + @detach() + else + @attach() + + attach: -> + rootView.horizontal.append(this) + @focus() diff --git a/src/packages/collaboration/lib/host-session.coffee b/src/packages/collaboration/lib/host-session.coffee new file mode 100644 index 000000000..24d5d4a47 --- /dev/null +++ b/src/packages/collaboration/lib/host-session.coffee @@ -0,0 +1,63 @@ +_ = require 'underscore' +telepath = require 'telepath' +{createPeer, connectDocument} = require './session-utils' + +module.exports = +class HostSession + _.extend @prototype, require('event-emitter') + + doc: null + participants: null + peer: null + sharing: false + + start: -> + return if @peer? + + @peer = createPeer() + @doc = telepath.Document.create({}, site: telepath.createSite(@getId())) + @doc.set('windowState', atom.windowState) + @doc.set('participants', []) + @participants = @doc.get('participants') + @participants.push + id: @getId() + email: git.getConfigValue('user.email') + @participants.observe => + @trigger 'participants-changed', @participants.toObject() + + @peer.on 'connection', (connection) => + console.log connection + connection.on 'open', => + console.log 'sending document' + connection.send(@doc.serialize()) + connectDocument(@doc, connection) + + connection.on 'close', => + console.log 'conection closed' + @participants.each (participant, index) => + if connection.peer is participant.get('id') + @participants.remove(index) + + @peer.on 'open', => + console.log 'sharing session started' + @sharing = true + @trigger 'started' + + @peer.on 'close', => + console.log 'sharing session stopped' + @sharing = false + @trigger 'stopped' + + @getId() + + stop: -> + return unless @peer? + + @peer.destroy() + @peer = null + + getId: -> + @peer.id + + isSharing: -> + @sharing diff --git a/src/packages/collaboration/lib/host-view.coffee b/src/packages/collaboration/lib/host-view.coffee new file mode 100644 index 000000000..5580a808b --- /dev/null +++ b/src/packages/collaboration/lib/host-view.coffee @@ -0,0 +1,46 @@ +{$$, View} = require 'space-pen' + +module.exports = +class HostView extends View + @content: -> + @div class: 'collaboration', tabindex: -1, => + @div outlet: 'share', type: 'button', class: 'share' + @div outlet: 'participants' + + hostSession: null + + initialize: (@hostSession) -> + if @hostSession.isSharing() + @share.addClass('running') + + @share.on 'click', => + @share.disable() + + if @hostSession.isSharing() + @hostSession.stop() + else + @hostSession.start() + + @hostSession.on 'started stopped', => + @share.toggleClass('running').enable() + + @hostSession.on 'participants-changed', (participants) => + @updateParticipants(participants) + + @attach() + + updateParticipants: (participants) -> + @participants.empty() + for {email, id} in participants when id is @hostSession.getId() + @participants.append $$ -> + @div email + + toggle: -> + if @hasParent() + @detach() + else + @attach() + + attach: -> + rootView.horizontal.append(this) + @focus() diff --git a/src/packages/collaboration/lib/session-utils.coffee b/src/packages/collaboration/lib/session-utils.coffee index 706a55d09..fe5a428be 100644 --- a/src/packages/collaboration/lib/session-utils.coffee +++ b/src/packages/collaboration/lib/session-utils.coffee @@ -9,6 +9,7 @@ module.exports = connectDocument: (doc, connection) -> nextOutputEventId = 1 outputListener = (event) -> + return unless connection.open event.id = nextOutputEventId++ console.log 'sending event', event.id, event connection.send(event) diff --git a/src/packages/collaboration/lib/sharing-session.coffee b/src/packages/collaboration/lib/sharing-session.coffee deleted file mode 100644 index 560031c76..000000000 --- a/src/packages/collaboration/lib/sharing-session.coffee +++ /dev/null @@ -1,44 +0,0 @@ -_ = require 'underscore' -{createPeer, connectDocument} = require './session-utils' - -module.exports = -class SharingSession - _.extend @prototype, require('event-emitter') - - peer: null - sharing: false - - start: -> - return if @peer - - @peer = createPeer() - @peer.on 'connection', (connection) -> - connection.on 'open', -> - console.log 'sending document' - windowState = atom.getWindowState() - connection.send(windowState.serialize()) - connectDocument(windowState, connection) - - @peer.on 'open', => - console.log 'sharing session started' - @sharing = true - @trigger 'started' - - @peer.on 'close', => - console.log 'sharing session stopped' - @sharing = false - @trigger 'stopped' - - @getId() - - stop: -> - return unless @peer? - - @peer.destroy() - @peer = null - - getId: -> - @peer.id - - isSharing: -> - @sharing diff --git a/src/packages/collaboration/stylesheets/collaboration.less b/src/packages/collaboration/stylesheets/collaboration.less index c0e0aa047..810ef6336 100644 --- a/src/packages/collaboration/stylesheets/collaboration.less +++ b/src/packages/collaboration/stylesheets/collaboration.less @@ -2,7 +2,6 @@ @import "octicon-mixins.less"; @runningColor: #99CC99; - .collaboration { @item-line-height: @line-height-base * 1.25; padding: 10px; @@ -13,6 +12,11 @@ .mini-icon(notifications); } + .guest { + .mini-icon(watchers); + color: #96CBFE; + } + .running { color: @runningColor; From 5ce0cf65c456d39c750e5a4d21a584a7556df881 Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Wed, 10 Jul 2013 09:07:16 -0700 Subject: [PATCH 38/62] Unvendor pusher.js --- src/packages/collaboration/vendor/pusher.js | 3614 ------------------- 1 file changed, 3614 deletions(-) delete mode 100644 src/packages/collaboration/vendor/pusher.js diff --git a/src/packages/collaboration/vendor/pusher.js b/src/packages/collaboration/vendor/pusher.js deleted file mode 100644 index a6c015221..000000000 --- a/src/packages/collaboration/vendor/pusher.js +++ /dev/null @@ -1,3614 +0,0 @@ -/*! - * Pusher JavaScript Library v2.1.1 - * http://pusherapp.com/ - * - * Copyright 2013, Pusher - * Released under the MIT licence. - */ - -;(function() { - function Pusher(app_key, options) { - options = options || {}; - - var self = this; - - this.key = app_key; - this.config = Pusher.Util.extend( - Pusher.getGlobalConfig(), - options.cluster ? Pusher.getClusterConfig(options.cluster) : {}, - options - ); - - this.channels = new Pusher.Channels(); - this.global_emitter = new Pusher.EventsDispatcher(); - this.sessionID = Math.floor(Math.random() * 1000000000); - - checkAppKey(this.key); - - var getStrategy = function(options) { - return Pusher.StrategyBuilder.build( - Pusher.getDefaultStrategy(self.config), - Pusher.Util.extend({}, self.config, options) - ); - }; - var getTimeline = function() { - return new Pusher.Timeline(self.key, self.sessionID, { - features: Pusher.Util.getClientFeatures(), - params: self.config.timelineParams || {}, - limit: 50, - level: Pusher.Timeline.INFO, - version: Pusher.VERSION - }); - }; - var getTimelineSender = function(timeline, options) { - if (self.config.disableStats) { - return null; - } - return new Pusher.TimelineSender(timeline, { - encrypted: self.isEncrypted() || !!options.encrypted, - host: self.config.statsHost, - path: "/timeline" - }); - }; - - this.connection = new Pusher.ConnectionManager( - this.key, - Pusher.Util.extend( - { getStrategy: getStrategy, - getTimeline: getTimeline, - getTimelineSender: getTimelineSender, - activityTimeout: this.config.activity_timeout, - pongTimeout: this.config.pong_timeout, - unavailableTimeout: this.config.unavailable_timeout - }, - this.config, - { encrypted: this.isEncrypted() } - ) - ); - - this.connection.bind('connected', function() { - self.subscribeAll(); - }); - this.connection.bind('message', function(params) { - var internal = (params.event.indexOf('pusher_internal:') === 0); - if (params.channel) { - var channel = self.channel(params.channel); - if (channel) { - channel.handleEvent(params.event, params.data); - } - } - // Emit globaly [deprecated] - if (!internal) self.global_emitter.emit(params.event, params.data); - }); - this.connection.bind('disconnected', function() { - self.channels.disconnect(); - }); - this.connection.bind('error', function(err) { - Pusher.warn('Error', err); - }); - - Pusher.instances.push(this); - - if (Pusher.isReady) self.connect(); - } - var prototype = Pusher.prototype; - - Pusher.instances = []; - Pusher.isReady = false; - - // To receive log output provide a Pusher.log function, for example - // Pusher.log = function(m){console.log(m)} - Pusher.debug = function() { - if (!Pusher.log) { - return; - } - Pusher.log(Pusher.Util.stringify.apply(this, arguments)); - }; - - Pusher.warn = function() { - var message = Pusher.Util.stringify.apply(this, arguments); - if (window.console) { - if (window.console.warn) { - window.console.warn(message); - } else if (window.console.log) { - window.console.log(message); - } - } - if (Pusher.log) { - Pusher.log(message); - } - }; - - Pusher.ready = function() { - Pusher.isReady = true; - for (var i = 0, l = Pusher.instances.length; i < l; i++) { - Pusher.instances[i].connect(); - } - }; - - prototype.channel = function(name) { - return this.channels.find(name); - }; - - prototype.connect = function() { - this.connection.connect(); - }; - - prototype.disconnect = function() { - this.connection.disconnect(); - }; - - prototype.bind = function(event_name, callback) { - this.global_emitter.bind(event_name, callback); - return this; - }; - - prototype.bind_all = function(callback) { - this.global_emitter.bind_all(callback); - return this; - }; - - prototype.subscribeAll = function() { - var channelName; - for (channelName in this.channels.channels) { - if (this.channels.channels.hasOwnProperty(channelName)) { - this.subscribe(channelName); - } - } - }; - - prototype.subscribe = function(channel_name) { - var self = this; - var channel = this.channels.add(channel_name, this); - - if (this.connection.state === 'connected') { - channel.authorize(this.connection.socket_id, function(err, data) { - if (err) { - channel.handleEvent('pusher:subscription_error', data); - } else { - self.send_event('pusher:subscribe', { - channel: channel_name, - auth: data.auth, - channel_data: data.channel_data - }); - } - }); - } - return channel; - }; - - prototype.unsubscribe = function(channel_name) { - this.channels.remove(channel_name); - if (this.connection.state === 'connected') { - this.send_event('pusher:unsubscribe', { - channel: channel_name - }); - } - }; - - prototype.send_event = function(event_name, data, channel) { - return this.connection.send_event(event_name, data, channel); - }; - - prototype.isEncrypted = function() { - if (Pusher.Util.getDocumentLocation().protocol === "https:") { - return true; - } else { - return !!this.config.encrypted; - } - }; - - function checkAppKey(key) { - if (key === null || key === undefined) { - Pusher.warn( - 'Warning', 'You must pass your app key when you instantiate Pusher.' - ); - } - } - - this.Pusher = Pusher; -}).call(this); - -;(function() { - Pusher = this.Pusher; - Pusher.Util = { - now: function() { - if (Date.now) { - return Date.now(); - } else { - return new Date().valueOf(); - } - }, - - /** Merges multiple objects into the target argument. - * - * For properties that are plain Objects, performs a deep-merge. For the - * rest it just copies the value of the property. - * - * To extend prototypes use it as following: - * Pusher.Util.extend(Target.prototype, Base.prototype) - * - * You can also use it to merge objects without altering them: - * Pusher.Util.extend({}, object1, object2) - * - * @param {Object} target - * @return {Object} the target argument - */ - extend: function(target) { - for (var i = 1; i < arguments.length; i++) { - var extensions = arguments[i]; - for (var property in extensions) { - if (extensions[property] && extensions[property].constructor && - extensions[property].constructor === Object) { - target[property] = Pusher.Util.extend( - target[property] || {}, extensions[property] - ); - } else { - target[property] = extensions[property]; - } - } - } - return target; - }, - - stringify: function() { - var m = ["Pusher"]; - for (var i = 0; i < arguments.length; i++) { - if (typeof arguments[i] === "string") { - m.push(arguments[i]); - } else { - if (window.JSON === undefined) { - m.push(arguments[i].toString()); - } else { - m.push(JSON.stringify(arguments[i])); - } - } - } - return m.join(" : "); - }, - - arrayIndexOf: function(array, item) { // MSIE doesn't have array.indexOf - var nativeIndexOf = Array.prototype.indexOf; - if (array === null) { - return -1; - } - if (nativeIndexOf && array.indexOf === nativeIndexOf) { - return array.indexOf(item); - } - for (var i = 0, l = array.length; i < l; i++) { - if (array[i] === item) { - return i; - } - } - return -1; - }, - - keys: function(object) { - var result = []; - for (var key in object) { - if (Object.prototype.hasOwnProperty.call(object, key)) { - result.push(key); - } - } - return result; - }, - - /** Applies a function f to all elements of an array. - * - * Function f gets 3 arguments passed: - * - element from the array - * - index of the element - * - reference to the array - * - * @param {Array} array - * @param {Function} f - */ - apply: function(array, f) { - for (var i = 0; i < array.length; i++) { - f(array[i], i, array); - } - }, - - /** Applies a function f to all properties of an object. - * - * Function f gets 3 arguments passed: - * - element from the object - * - key of the element - * - reference to the object - * - * @param {Object} object - * @param {Function} f - */ - objectApply: function(object, f) { - for (var key in object) { - if (Object.prototype.hasOwnProperty.call(object, key)) { - f(object[key], key, object); - } - } - }, - - /** Maps all elements of the array and returns the result. - * - * Function f gets 4 arguments passed: - * - element from the array - * - index of the element - * - reference to the source array - * - reference to the destination array - * - * @param {Array} array - * @param {Function} f - */ - map: function(array, f) { - var result = []; - for (var i = 0; i < array.length; i++) { - result.push(f(array[i], i, array, result)); - } - return result; - }, - - /** Maps all elements of the object and returns the result. - * - * Function f gets 4 arguments passed: - * - element from the object - * - key of the element - * - reference to the source object - * - reference to the destination object - * - * @param {Object} object - * @param {Function} f - */ - mapObject: function(object, f) { - var result = {}; - for (var key in object) { - if (Object.prototype.hasOwnProperty.call(object, key)) { - result[key] = f(object[key]); - } - } - return result; - }, - - /** Filters elements of the array using a test function. - * - * Function test gets 4 arguments passed: - * - element from the array - * - index of the element - * - reference to the source array - * - reference to the destination array - * - * @param {Array} array - * @param {Function} f - */ - filter: function(array, test) { - test = test || function(value) { return !!value; }; - - var result = []; - for (var i = 0; i < array.length; i++) { - if (test(array[i], i, array, result)) { - result.push(array[i]); - } - } - return result; - }, - - /** Filters properties of the object using a test function. - * - * Function test gets 4 arguments passed: - * - element from the object - * - key of the element - * - reference to the source object - * - reference to the destination object - * - * @param {Object} object - * @param {Function} f - */ - filterObject: function(object, test) { - test = test || function(value) { return !!value; }; - - var result = {}; - for (var key in object) { - if (Object.prototype.hasOwnProperty.call(object, key)) { - if (test(object[key], key, object, result)) { - result[key] = object[key]; - } - } - } - return result; - }, - - /** Flattens an object into a two-dimensional array. - * - * @param {Object} object - * @return {Array} resulting array of [key, value] pairs - */ - flatten: function(object) { - var result = []; - for (var key in object) { - if (Object.prototype.hasOwnProperty.call(object, key)) { - result.push([key, object[key]]); - } - } - return result; - }, - - /** Checks whether any element of the array passes the test. - * - * Function test gets 3 arguments passed: - * - element from the array - * - index of the element - * - reference to the source array - * - * @param {Array} array - * @param {Function} f - */ - any: function(array, test) { - for (var i = 0; i < array.length; i++) { - if (test(array[i], i, array)) { - return true; - } - } - return false; - }, - - /** Checks whether all elements of the array pass the test. - * - * Function test gets 3 arguments passed: - * - element from the array - * - index of the element - * - reference to the source array - * - * @param {Array} array - * @param {Function} f - */ - all: function(array, test) { - for (var i = 0; i < array.length; i++) { - if (!test(array[i], i, array)) { - return false; - } - } - return true; - }, - - /** Builds a function that will proxy a method call to its first argument. - * - * Allows partial application of arguments, so additional arguments are - * prepended to the argument list. - * - * @param {String} name method name - * @return {Function} proxy function - */ - method: function(name) { - var boundArguments = Array.prototype.slice.call(arguments, 1); - return function(object) { - return object[name].apply(object, boundArguments.concat(arguments)); - }; - }, - - getDocument: function() { - return document; - }, - - getDocumentLocation: function() { - return Pusher.Util.getDocument().location; - }, - - getLocalStorage: function() { - try { - return window.localStorage; - } catch (e) { - return undefined; - } - }, - - getClientFeatures: function() { - return Pusher.Util.keys( - Pusher.Util.filterObject( - { "ws": Pusher.WSTransport, "flash": Pusher.FlashTransport }, - function (t) { return t.isSupported(); } - ) - ); - } - }; -}).call(this); - -;(function() { - Pusher.VERSION = '2.1.1'; - Pusher.PROTOCOL = 6; - - // DEPRECATED: WS connection parameters - Pusher.host = 'ws.pusherapp.com'; - Pusher.ws_port = 80; - Pusher.wss_port = 443; - // DEPRECATED: SockJS fallback parameters - Pusher.sockjs_host = 'sockjs.pusher.com'; - Pusher.sockjs_http_port = 80; - Pusher.sockjs_https_port = 443; - Pusher.sockjs_path = "/pusher"; - // DEPRECATED: Stats - Pusher.stats_host = 'stats.pusher.com'; - // DEPRECATED: Other settings - Pusher.channel_auth_endpoint = '/pusher/auth'; - Pusher.channel_auth_transport = 'ajax'; - Pusher.activity_timeout = 120000; - Pusher.pong_timeout = 30000; - Pusher.unavailable_timeout = 10000; - // CDN configuration - Pusher.cdn_http = 'http://js.pusher.com/'; - Pusher.cdn_https = 'https://d3dy5gmtp8yhk7.cloudfront.net/'; - Pusher.dependency_suffix = ''; - - Pusher.getDefaultStrategy = function(config) { - return [ - [":def", "ws_options", { - hostUnencrypted: config.wsHost + ":" + config.wsPort, - hostEncrypted: config.wsHost + ":" + config.wssPort - }], - [":def", "sockjs_options", { - hostUnencrypted: config.httpHost + ":" + config.httpPort, - hostEncrypted: config.httpHost + ":" + config.httpsPort - }], - [":def", "timeouts", { - loop: true, - timeout: 15000, - timeoutLimit: 60000 - }], - - [":def", "ws_manager", [":transport_manager", { - lives: 2, - minPingDelay: 10000, - maxPingDelay: config.activity_timeout - }]], - - [":def_transport", "ws", "ws", 3, ":ws_options", ":ws_manager"], - [":def_transport", "flash", "flash", 2, ":ws_options", ":ws_manager"], - [":def_transport", "sockjs", "sockjs", 1, ":sockjs_options"], - [":def", "ws_loop", [":sequential", ":timeouts", ":ws"]], - [":def", "flash_loop", [":sequential", ":timeouts", ":flash"]], - [":def", "sockjs_loop", [":sequential", ":timeouts", ":sockjs"]], - - [":def", "strategy", - [":cached", 1800000, - [":first_connected", - [":if", [":is_supported", ":ws"], [ - ":best_connected_ever", ":ws_loop", [":delayed", 2000, [":sockjs_loop"]] - ], [":if", [":is_supported", ":flash"], [ - ":best_connected_ever", ":flash_loop", [":delayed", 2000, [":sockjs_loop"]] - ], [ - ":sockjs_loop" - ] - ]] - ] - ] - ] - ]; - }; -}).call(this); - -;(function() { - Pusher.getGlobalConfig = function() { - return { - wsHost: Pusher.host, - wsPort: Pusher.ws_port, - wssPort: Pusher.wss_port, - httpHost: Pusher.sockjs_host, - httpPort: Pusher.sockjs_http_port, - httpsPort: Pusher.sockjs_https_port, - httpPath: Pusher.sockjs_path, - statsHost: Pusher.stats_host, - authEndpoint: Pusher.channel_auth_endpoint, - authTransport: Pusher.channel_auth_transport, - // TODO make this consistent with other options in next major version - activity_timeout: Pusher.activity_timeout, - pong_timeout: Pusher.pong_timeout, - unavailable_timeout: Pusher.unavailable_timeout - }; - }; - - Pusher.getClusterConfig = function(clusterName) { - return { - wsHost: "ws-" + clusterName + ".pusher.com", - httpHost: "sockjs-" + clusterName + ".pusher.com" - }; - }; -}).call(this); - -;(function() { - function buildExceptionClass(name) { - var klass = function(message) { - Error.call(this, message); - this.name = name; - }; - Pusher.Util.extend(klass.prototype, Error.prototype); - - return klass; - } - - /** Error classes used throughout pusher-js library. */ - Pusher.Errors = { - UnsupportedTransport: buildExceptionClass("UnsupportedTransport"), - UnsupportedStrategy: buildExceptionClass("UnsupportedStrategy"), - TransportPriorityTooLow: buildExceptionClass("TransportPriorityTooLow"), - TransportClosed: buildExceptionClass("TransportClosed") - }; -}).call(this); - -;(function() { - /** Manages callback bindings and event emitting. - * - * @param Function failThrough called when no listeners are bound to an event - */ - function EventsDispatcher(failThrough) { - this.callbacks = new CallbackRegistry(); - this.global_callbacks = []; - this.failThrough = failThrough; - } - var prototype = EventsDispatcher.prototype; - - prototype.bind = function(eventName, callback) { - this.callbacks.add(eventName, callback); - return this; - }; - - prototype.bind_all = function(callback) { - this.global_callbacks.push(callback); - return this; - }; - - prototype.unbind = function(eventName, callback) { - this.callbacks.remove(eventName, callback); - return this; - }; - - prototype.emit = function(eventName, data) { - var i; - - for (i = 0; i < this.global_callbacks.length; i++) { - this.global_callbacks[i](eventName, data); - } - - var callbacks = this.callbacks.get(eventName); - if (callbacks && callbacks.length > 0) { - for (i = 0; i < callbacks.length; i++) { - callbacks[i](data); - } - } else if (this.failThrough) { - this.failThrough(eventName, data); - } - - return this; - }; - - /** Callback registry helper. */ - - function CallbackRegistry() { - this._callbacks = {}; - } - - CallbackRegistry.prototype.get = function(eventName) { - return this._callbacks[this._prefix(eventName)]; - }; - - CallbackRegistry.prototype.add = function(eventName, callback) { - var prefixedEventName = this._prefix(eventName); - this._callbacks[prefixedEventName] = this._callbacks[prefixedEventName] || []; - this._callbacks[prefixedEventName].push(callback); - }; - - CallbackRegistry.prototype.remove = function(eventName, callback) { - if(this.get(eventName)) { - var index = Pusher.Util.arrayIndexOf(this.get(eventName), callback); - if (index !== -1){ - var callbacksCopy = this._callbacks[this._prefix(eventName)].slice(0); - callbacksCopy.splice(index, 1); - this._callbacks[this._prefix(eventName)] = callbacksCopy; - } - } - }; - - CallbackRegistry.prototype._prefix = function(eventName) { - return "_" + eventName; - }; - - Pusher.EventsDispatcher = EventsDispatcher; -}).call(this); - -;(function() { - /** Handles loading dependency files. - * - * Options: - * - cdn_http - url to HTTP CND - * - cdn_https - url to HTTPS CDN - * - version - version of pusher-js - * - suffix - suffix appended to all names of dependency files - * - * @param {Object} options - */ - function DependencyLoader(options) { - this.options = options; - this.loading = {}; - this.loaded = {}; - } - var prototype = DependencyLoader.prototype; - - /** Loads the dependency from CDN. - * - * @param {String} name - * @param {Function} callback - */ - prototype.load = function(name, callback) { - var self = this; - - if (this.loaded[name]) { - callback(); - return; - } - - if (!this.loading[name]) { - this.loading[name] = []; - } - this.loading[name].push(callback); - if (this.loading[name].length > 1) { - return; - } - - require(this.getPath(name), function() { - for (var i = 0; i < self.loading[name].length; i++) { - self.loading[name][i](); - } - delete self.loading[name]; - self.loaded[name] = true; - }); - }; - - /** Returns a root URL for pusher-js CDN. - * - * @returns {String} - */ - prototype.getRoot = function(options) { - var cdn; - var protocol = Pusher.Util.getDocumentLocation().protocol; - if ((options && options.encrypted) || protocol === "https:") { - cdn = this.options.cdn_https; - } else { - cdn = this.options.cdn_http; - } - // make sure there are no double slashes - return cdn.replace(/\/*$/, "") + "/" + this.options.version; - }; - - /** Returns a full path to a dependency file. - * - * @param {String} name - * @returns {String} - */ - prototype.getPath = function(name, options) { - return this.getRoot(options) + '/' + name + this.options.suffix + '.js'; - }; - - function handleScriptLoaded(elem, callback) { - if (Pusher.Util.getDocument().addEventListener) { - elem.addEventListener('load', callback, false); - } else { - elem.attachEvent('onreadystatechange', function () { - if (elem.readyState === 'loaded' || elem.readyState === 'complete') { - callback(); - } - }); - } - } - - function require(src, callback) { - var document = Pusher.Util.getDocument(); - var head = document.getElementsByTagName('head')[0]; - var script = document.createElement('script'); - - script.setAttribute('src', src); - script.setAttribute("type","text/javascript"); - script.setAttribute('async', true); - - handleScriptLoaded(script, function() { - // workaround for an Opera issue - setTimeout(callback, 0); - }); - - head.appendChild(script); - } - - Pusher.DependencyLoader = DependencyLoader; -}).call(this); - -;(function() { - Pusher.Dependencies = new Pusher.DependencyLoader({ - cdn_http: Pusher.cdn_http, - cdn_https: Pusher.cdn_https, - version: Pusher.VERSION, - suffix: Pusher.dependency_suffix - }); - - // Support Firefox versions which prefix WebSocket - if (!window.WebSocket && window.MozWebSocket) { - window.WebSocket = window.MozWebSocket; - } - - function initialize() { - Pusher.ready(); - } - - // Allows calling a function when the document body is available - function onDocumentBody(callback) { - if (document.body) { - callback(); - } else { - setTimeout(function() { - onDocumentBody(callback); - }, 0); - } - } - - function initializeOnDocumentBody() { - onDocumentBody(initialize); - } - - if (!window.JSON) { - Pusher.Dependencies.load("json2", initializeOnDocumentBody); - } else { - initializeOnDocumentBody(); - } -})(); - -;(function() { - /** Cross-browser compatible timer abstraction. - * - * @param {Number} delay - * @param {Function} callback - */ - function Timer(delay, callback) { - var self = this; - - this.timeout = setTimeout(function() { - if (self.timeout !== null) { - callback(); - self.timeout = null; - } - }, delay); - } - var prototype = Timer.prototype; - - /** Returns whether the timer is still running. - * - * @return {Boolean} - */ - prototype.isRunning = function() { - return this.timeout !== null; - }; - - /** Aborts a timer when it's running. */ - prototype.ensureAborted = function() { - if (this.timeout) { - clearTimeout(this.timeout); - this.timeout = null; - } - }; - - Pusher.Timer = Timer; -}).call(this); - -(function() { - - var Base64 = { - encode: function (s) { - return btoa(utob(s)); - } - }; - - var fromCharCode = String.fromCharCode; - - var b64chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'; - var b64tab = {}; - - for (var i = 0, l = b64chars.length; i < l; i++) { - b64tab[b64chars.charAt(i)] = i; - } - - var cb_utob = function(c) { - var cc = c.charCodeAt(0); - return cc < 0x80 ? c - : cc < 0x800 ? fromCharCode(0xc0 | (cc >>> 6)) + - fromCharCode(0x80 | (cc & 0x3f)) - : fromCharCode(0xe0 | ((cc >>> 12) & 0x0f)) + - fromCharCode(0x80 | ((cc >>> 6) & 0x3f)) + - fromCharCode(0x80 | ( cc & 0x3f)); - }; - - var utob = function(u) { - return u.replace(/[^\x00-\x7F]/g, cb_utob); - }; - - var cb_encode = function(ccc) { - var padlen = [0, 2, 1][ccc.length % 3]; - var ord = ccc.charCodeAt(0) << 16 - | ((ccc.length > 1 ? ccc.charCodeAt(1) : 0) << 8) - | ((ccc.length > 2 ? ccc.charCodeAt(2) : 0)); - var chars = [ - b64chars.charAt( ord >>> 18), - b64chars.charAt((ord >>> 12) & 63), - padlen >= 2 ? '=' : b64chars.charAt((ord >>> 6) & 63), - padlen >= 1 ? '=' : b64chars.charAt(ord & 63) - ]; - return chars.join(''); - }; - - var btoa = window.btoa || function(b) { - return b.replace(/[\s\S]{1,3}/g, cb_encode); - }; - - Pusher.Base64 = Base64; - -}).call(this); - -(function() { - - function JSONPRequest(options) { - this.options = options; - } - - JSONPRequest.send = function(options, callback) { - var request = new Pusher.JSONPRequest({ - url: options.url, - receiver: options.receiverName, - tagPrefix: options.tagPrefix - }); - var id = options.receiver.register(function(error, result) { - request.cleanup(); - callback(error, result); - }); - - return request.send(id, options.data, function(error) { - var callback = options.receiver.unregister(id); - if (callback) { - callback(error); - } - }); - }; - - var prototype = JSONPRequest.prototype; - - prototype.send = function(id, data, callback) { - if (this.script) { - return false; - } - - var tagPrefix = this.options.tagPrefix || "_pusher_jsonp_"; - - var params = Pusher.Util.extend( - {}, data, { receiver: this.options.receiver } - ); - var query = Pusher.Util.map( - Pusher.Util.flatten( - encodeData( - Pusher.Util.filterObject(params, function(value) { - return value !== undefined; - }) - ) - ), - Pusher.Util.method("join", "=") - ).join("&"); - - this.script = document.createElement("script"); - this.script.id = tagPrefix + id; - this.script.src = this.options.url + "/" + id + "?" + query; - this.script.type = "text/javascript"; - this.script.charset = "UTF-8"; - this.script.onerror = this.script.onload = callback; - - // Opera<11.6 hack for missing onerror callback - if (this.script.async === undefined && document.attachEvent) { - if (/opera/i.test(navigator.userAgent)) { - var receiverName = this.options.receiver || "Pusher.JSONP.receive"; - this.errorScript = document.createElement("script"); - this.errorScript.text = receiverName + "(" + id + ", true);"; - this.script.async = this.errorScript.async = false; - } - } - - var self = this; - this.script.onreadystatechange = function() { - if (self.script && /loaded|complete/.test(self.script.readyState)) { - callback(true); - } - }; - - var head = document.getElementsByTagName('head')[0]; - head.insertBefore(this.script, head.firstChild); - if (this.errorScript) { - head.insertBefore(this.errorScript, this.script.nextSibling); - } - - return true; - }; - - prototype.cleanup = function() { - if (this.script && this.script.parentNode) { - this.script.parentNode.removeChild(this.script); - this.script = null; - } - if (this.errorScript && this.errorScript.parentNode) { - this.errorScript.parentNode.removeChild(this.errorScript); - this.errorScript = null; - } - }; - - function encodeData(data) { - return Pusher.Util.mapObject(data, function(value) { - if (typeof value === "object") { - value = JSON.stringify(value); - } - return encodeURIComponent(Pusher.Base64.encode(value.toString())); - }); - } - - Pusher.JSONPRequest = JSONPRequest; - -}).call(this); - -(function() { - - function JSONPReceiver() { - this.lastId = 0; - this.callbacks = {}; - } - - var prototype = JSONPReceiver.prototype; - - prototype.register = function(callback) { - this.lastId++; - var id = this.lastId; - this.callbacks[id] = callback; - return id; - }; - - prototype.unregister = function(id) { - if (this.callbacks[id]) { - var callback = this.callbacks[id]; - delete this.callbacks[id]; - return callback; - } else { - return null; - } - }; - - prototype.receive = function(id, error, data) { - var callback = this.unregister(id); - if (callback) { - callback(error, data); - } - }; - - Pusher.JSONPReceiver = JSONPReceiver; - Pusher.JSONP = new JSONPReceiver(); - -}).call(this); - -(function() { - function Timeline(key, session, options) { - this.key = key; - this.session = session; - this.events = []; - this.options = options || {}; - this.sent = 0; - this.uniqueID = 0; - } - var prototype = Timeline.prototype; - - // Log levels - Timeline.ERROR = 3; - Timeline.INFO = 6; - Timeline.DEBUG = 7; - - prototype.log = function(level, event) { - if (this.options.level === undefined || level <= this.options.level) { - this.events.push( - Pusher.Util.extend({}, event, { - timestamp: Pusher.Util.now(), - level: level - }) - ); - if (this.options.limit && this.events.length > this.options.limit) { - this.events.shift(); - } - } - }; - - prototype.error = function(event) { - this.log(Timeline.ERROR, event); - }; - - prototype.info = function(event) { - this.log(Timeline.INFO, event); - }; - - prototype.debug = function(event) { - this.log(Timeline.DEBUG, event); - }; - - prototype.isEmpty = function() { - return this.events.length === 0; - }; - - prototype.send = function(sendJSONP, callback) { - var self = this; - - var data = {}; - if (this.sent === 0) { - data = Pusher.Util.extend({ - key: this.key, - features: this.options.features, - version: this.options.version - }, this.options.params || {}); - } - data.session = this.session; - data.timeline = this.events; - data = Pusher.Util.filterObject(data, function(v) { - return v !== undefined; - }); - - this.events = []; - sendJSONP(data, function(error, result) { - if (!error) { - self.sent++; - } - callback(error, result); - }); - - return true; - }; - - prototype.generateUniqueID = function() { - this.uniqueID++; - return this.uniqueID; - }; - - Pusher.Timeline = Timeline; -}).call(this); - -(function() { - function TimelineSender(timeline, options) { - this.timeline = timeline; - this.options = options || {}; - } - var prototype = TimelineSender.prototype; - - prototype.send = function(callback) { - if (this.timeline.isEmpty()) { - return; - } - - var options = this.options; - var scheme = "http" + (this.isEncrypted() ? "s" : "") + "://"; - - var sendJSONP = function(data, callback) { - return Pusher.JSONPRequest.send({ - data: data, - url: scheme + options.host + options.path, - receiver: Pusher.JSONP - }, callback); - }; - this.timeline.send(sendJSONP, callback); - }; - - prototype.isEncrypted = function() { - return !!this.options.encrypted; - }; - - Pusher.TimelineSender = TimelineSender; -}).call(this); - -;(function() { - /** Launches all substrategies and emits prioritized connected transports. - * - * @param {Array} strategies - */ - function BestConnectedEverStrategy(strategies) { - this.strategies = strategies; - } - var prototype = BestConnectedEverStrategy.prototype; - - prototype.isSupported = function() { - return Pusher.Util.any(this.strategies, Pusher.Util.method("isSupported")); - }; - - prototype.connect = function(minPriority, callback) { - return connect(this.strategies, minPriority, function(i, runners) { - return function(error, handshake) { - runners[i].error = error; - if (error) { - if (allRunnersFailed(runners)) { - callback(true); - } - return; - } - Pusher.Util.apply(runners, function(runner) { - runner.forceMinPriority(handshake.transport.priority); - }); - callback(null, handshake); - }; - }); - }; - - /** Connects to all strategies in parallel. - * - * Callback builder should be a function that takes two arguments: index - * and a list of runners. It should return another function that will be - * passed to the substrategy with given index. Runners can be aborted using - * abortRunner(s) functions from this class. - * - * @param {Array} strategies - * @param {Function} callbackBuilder - * @return {Object} strategy runner - */ - function connect(strategies, minPriority, callbackBuilder) { - var runners = Pusher.Util.map(strategies, function(strategy, i, _, rs) { - return strategy.connect(minPriority, callbackBuilder(i, rs)); - }); - return { - abort: function() { - Pusher.Util.apply(runners, abortRunner); - }, - forceMinPriority: function(p) { - Pusher.Util.apply(runners, function(runner) { - runner.forceMinPriority(p); - }); - } - }; - } - - function allRunnersFailed(runners) { - return Pusher.Util.all(runners, function(runner) { - return Boolean(runner.error); - }); - } - - function abortRunner(runner) { - if (!runner.error && !runner.aborted) { - runner.abort(); - runner.aborted = true; - } - } - - Pusher.BestConnectedEverStrategy = BestConnectedEverStrategy; -}).call(this); - -;(function() { - /** Caches last successful transport and uses it for following attempts. - * - * @param {Strategy} strategy - * @param {Object} transports - * @param {Object} options - */ - function CachedStrategy(strategy, transports, options) { - this.strategy = strategy; - this.transports = transports; - this.ttl = options.ttl || 1800*1000; - this.timeline = options.timeline; - } - var prototype = CachedStrategy.prototype; - - prototype.isSupported = function() { - return this.strategy.isSupported(); - }; - - prototype.connect = function(minPriority, callback) { - var info = fetchTransportInfo(); - - var strategies = [this.strategy]; - if (info && info.timestamp + this.ttl >= Pusher.Util.now()) { - var transport = this.transports[info.transport]; - if (transport) { - this.timeline.info({ cached: true, transport: info.transport }); - strategies.push(new Pusher.SequentialStrategy([transport], { - timeout: info.latency * 2, - failFast: true - })); - } - } - - var startTimestamp = Pusher.Util.now(); - var runner = strategies.pop().connect( - minPriority, - function cb(error, handshake) { - if (error) { - flushTransportInfo(); - if (strategies.length > 0) { - startTimestamp = Pusher.Util.now(); - runner = strategies.pop().connect(minPriority, cb); - } else { - callback(error); - } - } else { - var latency = Pusher.Util.now() - startTimestamp; - storeTransportInfo(handshake.transport.name, latency); - callback(null, handshake); - } - } - ); - - return { - abort: function() { - runner.abort(); - }, - forceMinPriority: function(p) { - minPriority = p; - if (runner) { - runner.forceMinPriority(p); - } - } - }; - }; - - function fetchTransportInfo() { - var storage = Pusher.Util.getLocalStorage(); - if (storage) { - var info = storage.pusherTransport; - if (info) { - return JSON.parse(storage.pusherTransport); - } - } - return null; - } - - function storeTransportInfo(transport, latency) { - var storage = Pusher.Util.getLocalStorage(); - if (storage) { - try { - storage.pusherTransport = JSON.stringify({ - timestamp: Pusher.Util.now(), - transport: transport, - latency: latency - }); - } catch(e) { - // catch over quota exceptions raised by localStorage - } - } - } - - function flushTransportInfo() { - var storage = Pusher.Util.getLocalStorage(); - if (storage && storage.pusherTransport) { - try { - delete storage.pusherTransport; - } catch(e) { - storage.pusherTransport = undefined; - } - } - } - - Pusher.CachedStrategy = CachedStrategy; -}).call(this); - -;(function() { - /** Runs substrategy after specified delay. - * - * Options: - * - delay - time in miliseconds to delay the substrategy attempt - * - * @param {Strategy} strategy - * @param {Object} options - */ - function DelayedStrategy(strategy, options) { - this.strategy = strategy; - this.options = { delay: options.delay }; - } - var prototype = DelayedStrategy.prototype; - - prototype.isSupported = function() { - return this.strategy.isSupported(); - }; - - prototype.connect = function(minPriority, callback) { - var strategy = this.strategy; - var runner; - var timer = new Pusher.Timer(this.options.delay, function() { - runner = strategy.connect(minPriority, callback); - }); - - return { - abort: function() { - timer.ensureAborted(); - if (runner) { - runner.abort(); - } - }, - forceMinPriority: function(p) { - minPriority = p; - if (runner) { - runner.forceMinPriority(p); - } - } - }; - }; - - Pusher.DelayedStrategy = DelayedStrategy; -}).call(this); - -;(function() { - /** Launches the substrategy and terminates on the first open connection. - * - * @param {Strategy} strategy - */ - function FirstConnectedStrategy(strategy) { - this.strategy = strategy; - } - var prototype = FirstConnectedStrategy.prototype; - - prototype.isSupported = function() { - return this.strategy.isSupported(); - }; - - prototype.connect = function(minPriority, callback) { - var runner = this.strategy.connect( - minPriority, - function(error, handshake) { - if (handshake) { - runner.abort(); - } - callback(error, handshake); - } - ); - return runner; - }; - - Pusher.FirstConnectedStrategy = FirstConnectedStrategy; -}).call(this); - -;(function() { - /** Proxies method calls to one of substrategies basing on the test function. - * - * @param {Function} test - * @param {Strategy} trueBranch strategy used when test returns true - * @param {Strategy} falseBranch strategy used when test returns false - */ - function IfStrategy(test, trueBranch, falseBranch) { - this.test = test; - this.trueBranch = trueBranch; - this.falseBranch = falseBranch; - } - var prototype = IfStrategy.prototype; - - prototype.isSupported = function() { - var branch = this.test() ? this.trueBranch : this.falseBranch; - return branch.isSupported(); - }; - - prototype.connect = function(minPriority, callback) { - var branch = this.test() ? this.trueBranch : this.falseBranch; - return branch.connect(minPriority, callback); - }; - - Pusher.IfStrategy = IfStrategy; -}).call(this); - -;(function() { - /** Loops through strategies with optional timeouts. - * - * Options: - * - loop - whether it should loop through the substrategy list - * - timeout - initial timeout for a single substrategy - * - timeoutLimit - maximum timeout - * - * @param {Strategy[]} strategies - * @param {Object} options - */ - function SequentialStrategy(strategies, options) { - this.strategies = strategies; - this.loop = Boolean(options.loop); - this.failFast = Boolean(options.failFast); - this.timeout = options.timeout; - this.timeoutLimit = options.timeoutLimit; - } - var prototype = SequentialStrategy.prototype; - - prototype.isSupported = function() { - return Pusher.Util.any(this.strategies, Pusher.Util.method("isSupported")); - }; - - prototype.connect = function(minPriority, callback) { - var self = this; - - var strategies = this.strategies; - var current = 0; - var timeout = this.timeout; - var runner = null; - - var tryNextStrategy = function(error, handshake) { - if (handshake) { - callback(null, handshake); - } else { - current = current + 1; - if (self.loop) { - current = current % strategies.length; - } - - if (current < strategies.length) { - if (timeout) { - timeout = timeout * 2; - if (self.timeoutLimit) { - timeout = Math.min(timeout, self.timeoutLimit); - } - } - runner = self.tryStrategy( - strategies[current], - minPriority, - { timeout: timeout, failFast: self.failFast }, - tryNextStrategy - ); - } else { - callback(true); - } - } - }; - - runner = this.tryStrategy( - strategies[current], - minPriority, - { timeout: timeout, failFast: this.failFast }, - tryNextStrategy - ); - - return { - abort: function() { - runner.abort(); - }, - forceMinPriority: function(p) { - minPriority = p; - if (runner) { - runner.forceMinPriority(p); - } - } - }; - }; - - /** @private */ - prototype.tryStrategy = function(strategy, minPriority, options, callback) { - var timer = null; - var runner = null; - - runner = strategy.connect(minPriority, function(error, handshake) { - if (error && timer && timer.isRunning() && !options.failFast) { - // advance to the next strategy after the timeout - return; - } - if (timer) { - timer.ensureAborted(); - } - callback(error, handshake); - }); - - if (options.timeout > 0) { - timer = new Pusher.Timer(options.timeout, function() { - runner.abort(); - callback(true); - }); - } - - return { - abort: function() { - if (timer) { - timer.ensureAborted(); - } - runner.abort(); - }, - forceMinPriority: function(p) { - runner.forceMinPriority(p); - } - }; - }; - - Pusher.SequentialStrategy = SequentialStrategy; -}).call(this); - -;(function() { - /** Provides a strategy interface for transports. - * - * @param {String} name - * @param {Number} priority - * @param {Class} transport - * @param {Object} options - */ - function TransportStrategy(name, priority, transport, options) { - this.name = name; - this.priority = priority; - this.transport = transport; - this.options = options || {}; - } - var prototype = TransportStrategy.prototype; - - /** Returns whether the transport is supported in the browser. - * - * @returns {Boolean} - */ - prototype.isSupported = function() { - return this.transport.isSupported({ - disableFlash: !!this.options.disableFlash - }); - }; - - /** Launches a connection attempt and returns a strategy runner. - * - * @param {Function} callback - * @return {Object} strategy runner - */ - prototype.connect = function(minPriority, callback) { - if (!this.transport.isSupported()) { - return failAttempt(new Pusher.Errors.UnsupportedStrategy(), callback); - } else if (this.priority < minPriority) { - return failAttempt(new Pusher.Errors.TransportPriorityTooLow(), callback); - } - - var self = this; - var connected = false; - - var transport = this.transport.createConnection( - this.name, this.priority, this.options.key, this.options - ); - var handshake = null; - - var onInitialized = function() { - transport.unbind("initialized", onInitialized); - transport.connect(); - }; - var onOpen = function() { - handshake = new Pusher.Handshake(transport, function(result) { - connected = true; - unbindListeners(); - callback(null, result); - }); - }; - var onError = function(error) { - unbindListeners(); - callback(error); - }; - var onClosed = function() { - unbindListeners(); - callback(new Pusher.Errors.TransportClosed(transport)); - }; - - var unbindListeners = function() { - transport.unbind("initialized", onInitialized); - transport.unbind("open", onOpen); - transport.unbind("error", onError); - transport.unbind("closed", onClosed); - }; - - transport.bind("initialized", onInitialized); - transport.bind("open", onOpen); - transport.bind("error", onError); - transport.bind("closed", onClosed); - - // connect will be called automatically after initialization - transport.initialize(); - - return { - abort: function() { - if (connected) { - return; - } - unbindListeners(); - if (handshake) { - handshake.close(); - } else { - transport.close(); - } - }, - forceMinPriority: function(p) { - if (connected) { - return; - } - if (self.priority < p) { - if (handshake) { - handshake.close(); - } else { - transport.close(); - } - } - } - }; - }; - - function failAttempt(error, callback) { - new Pusher.Timer(0, function() { - callback(error); - }); - return { - abort: function() {}, - forceMinPriority: function() {} - }; - } - - Pusher.TransportStrategy = TransportStrategy; -}).call(this); - -;(function() { - /** Handles common logic for all transports. - * - * Transport is a low-level connection object that wraps a connection method - * and exposes a simple evented interface for the connection state and - * messaging. It does not implement Pusher-specific WebSocket protocol. - * - * Additionally, it fetches resources needed for transport to work and exposes - * an interface for querying transport support and its features. - * - * This is an abstract class, please do not instantiate it. - * - * States: - * - new - initial state after constructing the object - * - initializing - during initialization phase, usually fetching resources - * - intialized - ready to establish a connection - * - connection - when connection is being established - * - open - when connection ready to be used - * - closed - after connection was closed be either side - * - * Emits: - * - error - after the connection raised an error - * - * Options: - * - encrypted - whether connection should use ssl - * - hostEncrypted - host to connect to when connection is encrypted - * - hostUnencrypted - host to connect to when connection is not encrypted - * - * @param {String} key application key - * @param {Object} options - */ - function AbstractTransport(name, priority, key, options) { - Pusher.EventsDispatcher.call(this); - - this.name = name; - this.priority = priority; - this.key = key; - this.state = "new"; - this.timeline = options.timeline; - this.id = this.timeline.generateUniqueID(); - - this.options = { - encrypted: Boolean(options.encrypted), - hostUnencrypted: options.hostUnencrypted, - hostEncrypted: options.hostEncrypted - }; - } - var prototype = AbstractTransport.prototype; - Pusher.Util.extend(prototype, Pusher.EventsDispatcher.prototype); - - /** Checks whether the transport is supported in the browser. - * - * @returns {Boolean} - */ - AbstractTransport.isSupported = function() { - return false; - }; - - /** Checks whether the transport handles ping/pong on itself. - * - * @return {Boolean} - */ - prototype.supportsPing = function() { - return false; - }; - - /** Initializes the transport. - * - * Fetches resources if needed and then transitions to initialized. - */ - prototype.initialize = function() { - this.timeline.info(this.buildTimelineMessage({ - transport: this.name + (this.options.encrypted ? "s" : "") - })); - this.timeline.debug(this.buildTimelineMessage({ method: "initialize" })); - - this.changeState("initialized"); - }; - - /** Tries to establish a connection. - * - * @returns {Boolean} false if transport is in invalid state - */ - prototype.connect = function() { - var url = this.getURL(this.key, this.options); - this.timeline.debug(this.buildTimelineMessage({ - method: "connect", - url: url - })); - - if (this.socket || this.state !== "initialized") { - return false; - } - - try { - this.socket = this.createSocket(url); - } catch (e) { - var self = this; - new Pusher.Timer(0, function() { - self.onError(e); - self.changeState("closed"); - }); - return false; - } - - this.bindListeners(); - - Pusher.debug("Connecting", { transport: this.name, url: url }); - this.changeState("connecting"); - return true; - }; - - /** Closes the connection. - * - * @return {Boolean} true if there was a connection to close - */ - prototype.close = function() { - this.timeline.debug(this.buildTimelineMessage({ method: "close" })); - - if (this.socket) { - this.socket.close(); - return true; - } else { - return false; - } - }; - - /** Sends data over the open connection. - * - * @param {String} data - * @return {Boolean} true only when in the "open" state - */ - prototype.send = function(data) { - this.timeline.debug(this.buildTimelineMessage({ - method: "send", - data: data - })); - - if (this.state === "open") { - // Workaround for MobileSafari bug (see https://gist.github.com/2052006) - var self = this; - setTimeout(function() { - self.socket.send(data); - }, 0); - return true; - } else { - return false; - } - }; - - prototype.requestPing = function() { - this.emit("ping_request"); - }; - - /** @protected */ - prototype.onOpen = function() { - this.changeState("open"); - this.socket.onopen = undefined; - }; - - /** @protected */ - prototype.onError = function(error) { - this.emit("error", { type: 'WebSocketError', error: error }); - this.timeline.error(this.buildTimelineMessage({})); - }; - - /** @protected */ - prototype.onClose = function(closeEvent) { - if (closeEvent) { - this.changeState("closed", { - code: closeEvent.code, - reason: closeEvent.reason, - wasClean: closeEvent.wasClean - }); - } else { - this.changeState("closed"); - } - this.socket = undefined; - }; - - /** @protected */ - prototype.onMessage = function(message) { - this.timeline.debug(this.buildTimelineMessage({ message: message.data })); - this.emit("message", message); - }; - - /** @protected */ - prototype.bindListeners = function() { - var self = this; - - this.socket.onopen = function() { self.onOpen(); }; - this.socket.onerror = function(error) { self.onError(error); }; - this.socket.onclose = function(closeEvent) { self.onClose(closeEvent); }; - this.socket.onmessage = function(message) { self.onMessage(message); }; - }; - - /** @protected */ - prototype.createSocket = function(url) { - return null; - }; - - /** @protected */ - prototype.getScheme = function() { - return this.options.encrypted ? "wss" : "ws"; - }; - - /** @protected */ - prototype.getBaseURL = function() { - var host; - if (this.options.encrypted) { - host = this.options.hostEncrypted; - } else { - host = this.options.hostUnencrypted; - } - return this.getScheme() + "://" + host; - }; - - /** @protected */ - prototype.getPath = function() { - return "/app/" + this.key; - }; - - /** @protected */ - prototype.getQueryString = function() { - return "?protocol=" + Pusher.PROTOCOL + - "&client=js&version=" + Pusher.VERSION; - }; - - /** @protected */ - prototype.getURL = function() { - return this.getBaseURL() + this.getPath() + this.getQueryString(); - }; - - /** @protected */ - prototype.changeState = function(state, params) { - this.state = state; - this.timeline.info(this.buildTimelineMessage({ - state: state, - params: params - })); - this.emit(state, params); - }; - - /** @protected */ - prototype.buildTimelineMessage = function(message) { - return Pusher.Util.extend({ cid: this.id }, message); - }; - - Pusher.AbstractTransport = AbstractTransport; -}).call(this); - -;(function() { - /** Transport using Flash to emulate WebSockets. - * - * @see AbstractTransport - */ - function FlashTransport(name, priority, key, options) { - Pusher.AbstractTransport.call(this, name, priority, key, options); - } - var prototype = FlashTransport.prototype; - Pusher.Util.extend(prototype, Pusher.AbstractTransport.prototype); - - /** Creates a new instance of FlashTransport. - * - * @param {String} key - * @param {Object} options - * @return {FlashTransport} - */ - FlashTransport.createConnection = function(name, priority, key, options) { - return new FlashTransport(name, priority, key, options); - }; - - /** Checks whether Flash is supported in the browser. - * - * It is possible to disable flash by passing an envrionment object with the - * disableFlash property set to true. - * - * @see AbstractTransport.isSupported - * @param {Object} environment - * @returns {Boolean} - */ - FlashTransport.isSupported = function(environment) { - if (environment && environment.disableFlash) { - return false; - } - try { - return Boolean(new ActiveXObject('ShockwaveFlash.ShockwaveFlash')); - } catch (e) { - return Boolean( - navigator && - navigator.mimeTypes && - navigator.mimeTypes["application/x-shockwave-flash"] !== undefined - ); - } - }; - - /** Fetches flashfallback dependency if needed. - * - * Sets WEB_SOCKET_SUPPRESS_CROSS_DOMAIN_SWF_ERROR to true (if not set before) - * and WEB_SOCKET_SWF_LOCATION to Pusher's cdn before loading Flash resources. - * - * @see AbstractTransport.prototype.initialize - */ - prototype.initialize = function() { - var self = this; - - this.timeline.info(this.buildTimelineMessage({ - transport: this.name + (this.options.encrypted ? "s" : "") - })); - this.timeline.debug(this.buildTimelineMessage({ method: "initialize" })); - this.changeState("initializing"); - - if (window.WEB_SOCKET_SUPPRESS_CROSS_DOMAIN_SWF_ERROR === undefined) { - window.WEB_SOCKET_SUPPRESS_CROSS_DOMAIN_SWF_ERROR = true; - } - window.WEB_SOCKET_SWF_LOCATION = Pusher.Dependencies.getRoot() + - "/WebSocketMain.swf"; - Pusher.Dependencies.load("flashfallback", function() { - self.changeState("initialized"); - }); - }; - - /** @protected */ - prototype.createSocket = function(url) { - return new FlashWebSocket(url); - }; - - /** @protected */ - prototype.getQueryString = function() { - return Pusher.AbstractTransport.prototype.getQueryString.call(this) + - "&flash=true"; - }; - - Pusher.FlashTransport = FlashTransport; -}).call(this); - -;(function() { - /** Fallback transport using SockJS. - * - * @see AbstractTransport - */ - function SockJSTransport(name, priority, key, options) { - Pusher.AbstractTransport.call(this, name, priority, key, options); - this.options.ignoreNullOrigin = options.ignoreNullOrigin; - } - var prototype = SockJSTransport.prototype; - Pusher.Util.extend(prototype, Pusher.AbstractTransport.prototype); - - /** Creates a new instance of SockJSTransport. - * - * @param {String} key - * @param {Object} options - * @return {SockJSTransport} - */ - SockJSTransport.createConnection = function(name, priority, key, options) { - return new SockJSTransport(name, priority, key, options); - }; - - /** Assumes that SockJS is always supported. - * - * @returns {Boolean} always true - */ - SockJSTransport.isSupported = function() { - return true; - }; - - /** Fetches sockjs dependency if needed. - * - * @see AbstractTransport.prototype.initialize - */ - prototype.initialize = function() { - var self = this; - - this.timeline.info(this.buildTimelineMessage({ - transport: this.name + (this.options.encrypted ? "s" : "") - })); - this.timeline.debug(this.buildTimelineMessage({ method: "initialize" })); - - this.changeState("initializing"); - Pusher.Dependencies.load("sockjs", function() { - self.changeState("initialized"); - }); - }; - - /** Always returns true, since SockJS handles ping on its own. - * - * @returns {Boolean} always true - */ - prototype.supportsPing = function() { - return true; - }; - - /** @protected */ - prototype.createSocket = function(url) { - return new SockJS(url, null, { - js_path: Pusher.Dependencies.getPath("sockjs", { - encrypted: this.options.encrypted - }), - ignore_null_origin: this.options.ignoreNullOrigin - }); - }; - - /** @protected */ - prototype.getScheme = function() { - return this.options.encrypted ? "https" : "http"; - }; - - /** @protected */ - prototype.getPath = function() { - return this.options.httpPath || "/pusher"; - }; - - /** @protected */ - prototype.getQueryString = function() { - return ""; - }; - - /** Handles opening a SockJS connection to Pusher. - * - * Since SockJS does not handle custom paths, we send it immediately after - * establishing the connection. - * - * @protected - */ - prototype.onOpen = function() { - this.socket.send(JSON.stringify({ - path: Pusher.AbstractTransport.prototype.getPath.call(this) + - Pusher.AbstractTransport.prototype.getQueryString.call(this) - })); - this.changeState("open"); - this.socket.onopen = undefined; - }; - - Pusher.SockJSTransport = SockJSTransport; -}).call(this); - -;(function() { - /** WebSocket transport. - * - * @see AbstractTransport - */ - function WSTransport(name, priority, key, options) { - Pusher.AbstractTransport.call(this, name, priority, key, options); - } - var prototype = WSTransport.prototype; - Pusher.Util.extend(prototype, Pusher.AbstractTransport.prototype); - - /** Creates a new instance of WSTransport. - * - * @param {String} key - * @param {Object} options - * @return {WSTransport} - */ - WSTransport.createConnection = function(name, priority, key, options) { - return new WSTransport(name, priority, key, options); - }; - - /** Checks whether the browser supports WebSockets in any form. - * - * @returns {Boolean} true if browser supports WebSockets - */ - WSTransport.isSupported = function() { - return window.WebSocket !== undefined || window.MozWebSocket !== undefined; - }; - - /** @protected */ - prototype.createSocket = function(url) { - var constructor = window.WebSocket || window.MozWebSocket; - return new constructor(url); - }; - - /** @protected */ - prototype.getQueryString = function() { - return Pusher.AbstractTransport.prototype.getQueryString.call(this) + - "&flash=false"; - }; - - Pusher.WSTransport = WSTransport; -}).call(this); - -;(function() { - function AssistantToTheTransportManager(manager, transport, options) { - this.manager = manager; - this.transport = transport; - this.minPingDelay = options.minPingDelay; - this.maxPingDelay = options.maxPingDelay; - this.pingDelay = null; - } - var prototype = AssistantToTheTransportManager.prototype; - - prototype.createConnection = function(name, priority, key, options) { - var connection = this.transport.createConnection( - name, priority, key, options - ); - - var self = this; - var openTimestamp = null; - var pingTimer = null; - - var onOpen = function() { - connection.unbind("open", onOpen); - - openTimestamp = Pusher.Util.now(); - if (self.pingDelay) { - pingTimer = setInterval(function() { - if (pingTimer) { - connection.requestPing(); - } - }, self.pingDelay); - } - - connection.bind("closed", onClosed); - }; - var onClosed = function(closeEvent) { - connection.unbind("closed", onClosed); - if (pingTimer) { - clearInterval(pingTimer); - pingTimer = null; - } - - if (closeEvent.code === 1002 || closeEvent.code === 1003) { - // we don't want to use transports not obeying the protocol - self.manager.reportDeath(); - } else if (!closeEvent.wasClean && openTimestamp) { - // report deaths only for short-living transport - var lifespan = Pusher.Util.now() - openTimestamp; - if (lifespan < 2 * self.maxPingDelay) { - self.manager.reportDeath(); - self.pingDelay = Math.max(lifespan / 2, self.minPingDelay); - } - } - }; - - connection.bind("open", onOpen); - return connection; - }; - - prototype.isSupported = function(environment) { - return this.manager.isAlive() && this.transport.isSupported(environment); - }; - - Pusher.AssistantToTheTransportManager = AssistantToTheTransportManager; -}).call(this); - -;(function() { - function TransportManager(options) { - this.options = options || {}; - this.livesLeft = this.options.lives || Infinity; - } - var prototype = TransportManager.prototype; - - prototype.getAssistant = function(transport) { - return new Pusher.AssistantToTheTransportManager(this, transport, { - minPingDelay: this.options.minPingDelay, - maxPingDelay: this.options.maxPingDelay - }); - }; - - prototype.isAlive = function() { - return this.livesLeft > 0; - }; - - prototype.reportDeath = function() { - this.livesLeft -= 1; - }; - - Pusher.TransportManager = TransportManager; -}).call(this); - -;(function() { - var StrategyBuilder = { - /** Transforms a JSON scheme to a strategy tree. - * - * @param {Array} scheme JSON strategy scheme - * @param {Object} options a hash of symbols to be included in the scheme - * @returns {Strategy} strategy tree that's represented by the scheme - */ - build: function(scheme, options) { - var context = Pusher.Util.extend({}, globalContext, options); - return evaluate(scheme, context)[1].strategy; - } - }; - - var transports = { - ws: Pusher.WSTransport, - flash: Pusher.FlashTransport, - sockjs: Pusher.SockJSTransport - }; - - // DSL bindings - - function returnWithOriginalContext(f) { - return function(context) { - return [f.apply(this, arguments), context]; - }; - } - - var globalContext = { - def: function(context, name, value) { - if (context[name] !== undefined) { - throw "Redefining symbol " + name; - } - context[name] = value; - return [undefined, context]; - }, - - def_transport: function(context, name, type, priority, options, manager) { - var transportClass = transports[type]; - if (!transportClass) { - throw new Pusher.Errors.UnsupportedTransport(type); - } - var transportOptions = Pusher.Util.extend({}, { - key: context.key, - encrypted: context.encrypted, - timeline: context.timeline, - disableFlash: context.disableFlash, - ignoreNullOrigin: context.ignoreNullOrigin - }, options); - if (manager) { - transportClass = manager.getAssistant(transportClass); - } - var transport = new Pusher.TransportStrategy( - name, priority, transportClass, transportOptions - ); - var newContext = context.def(context, name, transport)[1]; - newContext.transports = context.transports || {}; - newContext.transports[name] = transport; - return [undefined, newContext]; - }, - - transport_manager: returnWithOriginalContext(function(_, options) { - return new Pusher.TransportManager(options); - }), - - sequential: returnWithOriginalContext(function(_, options) { - var strategies = Array.prototype.slice.call(arguments, 2); - return new Pusher.SequentialStrategy(strategies, options); - }), - - cached: returnWithOriginalContext(function(context, ttl, strategy){ - return new Pusher.CachedStrategy(strategy, context.transports, { - ttl: ttl, - timeline: context.timeline - }); - }), - - first_connected: returnWithOriginalContext(function(_, strategy) { - return new Pusher.FirstConnectedStrategy(strategy); - }), - - best_connected_ever: returnWithOriginalContext(function() { - var strategies = Array.prototype.slice.call(arguments, 1); - return new Pusher.BestConnectedEverStrategy(strategies); - }), - - delayed: returnWithOriginalContext(function(_, delay, strategy) { - return new Pusher.DelayedStrategy(strategy, { delay: delay }); - }), - - "if": returnWithOriginalContext(function(_, test, trueBranch, falseBranch) { - return new Pusher.IfStrategy(test, trueBranch, falseBranch); - }), - - is_supported: returnWithOriginalContext(function(_, strategy) { - return function() { - return strategy.isSupported(); - }; - }) - }; - - // DSL interpreter - - function isSymbol(expression) { - return (typeof expression === "string") && expression.charAt(0) === ":"; - } - - function getSymbolValue(expression, context) { - return context[expression.slice(1)]; - } - - function evaluateListOfExpressions(expressions, context) { - if (expressions.length === 0) { - return [[], context]; - } - var head = evaluate(expressions[0], context); - var tail = evaluateListOfExpressions(expressions.slice(1), head[1]); - return [[head[0]].concat(tail[0]), tail[1]]; - } - - function evaluateString(expression, context) { - if (!isSymbol(expression)) { - return [expression, context]; - } - var value = getSymbolValue(expression, context); - if (value === undefined) { - throw "Undefined symbol " + expression; - } - return [value, context]; - } - - function evaluateArray(expression, context) { - if (isSymbol(expression[0])) { - var f = getSymbolValue(expression[0], context); - if (expression.length > 1) { - if (typeof f !== "function") { - throw "Calling non-function " + expression[0]; - } - var args = [Pusher.Util.extend({}, context)].concat( - Pusher.Util.map(expression.slice(1), function(arg) { - return evaluate(arg, Pusher.Util.extend({}, context))[0]; - }) - ); - return f.apply(this, args); - } else { - return [f, context]; - } - } else { - return evaluateListOfExpressions(expression, context); - } - } - - function evaluate(expression, context) { - var expressionType = typeof expression; - if (typeof expression === "string") { - return evaluateString(expression, context); - } else if (typeof expression === "object") { - if (expression instanceof Array && expression.length > 0) { - return evaluateArray(expression, context); - } - } - return [expression, context]; - } - - Pusher.StrategyBuilder = StrategyBuilder; -}).call(this); - -;(function() { - /** - * Provides functions for handling Pusher protocol-specific messages. - */ - Protocol = {}; - - /** - * Decodes a message in a Pusher format. - * - * Throws errors when messages are not parse'able. - * - * @param {Object} message - * @return {Object} - */ - Protocol.decodeMessage = function(message) { - try { - var params = JSON.parse(message.data); - if (typeof params.data === 'string') { - try { - params.data = JSON.parse(params.data); - } catch (e) { - if (!(e instanceof SyntaxError)) { - // TODO looks like unreachable code - // https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/JSON/parse - throw e; - } - } - } - return params; - } catch (e) { - throw { type: 'MessageParseError', error: e, data: message.data}; - } - }; - - /** - * Encodes a message to be sent. - * - * @param {Object} message - * @return {String} - */ - Protocol.encodeMessage = function(message) { - return JSON.stringify(message); - }; - - /** Processes a handshake message and returns appropriate actions. - * - * Returns an object with an 'action' and other action-specific properties. - * - * There are three outcomes when calling this function. First is a successful - * connection attempt, when pusher:connection_established is received, which - * results in a 'connected' action with an 'id' property. When passed a - * pusher:error event, it returns a result with action appropriate to the - * close code and an error. Otherwise, it raises an exception. - * - * @param {String} message - * @result Object - */ - Protocol.processHandshake = function(message) { - message = this.decodeMessage(message); - - if (message.event === "pusher:connection_established") { - return { action: "connected", id: message.data.socket_id }; - } else if (message.event === "pusher:error") { - // From protocol 6 close codes are sent only once, so this only - // happens when connection does not support close codes - return { - action: this.getCloseAction(message.data), - error: this.getCloseError(message.data) - }; - } else { - throw "Invalid handshake"; - } - }; - - /** - * Dispatches the close event and returns an appropriate action name. - * - * See: - * 1. https://developer.mozilla.org/en-US/docs/WebSockets/WebSockets_reference/CloseEvent - * 2. http://pusher.com/docs/pusher_protocol - * - * @param {CloseEvent} closeEvent - * @return {String} close action name - */ - Protocol.getCloseAction = function(closeEvent) { - if (closeEvent.code < 4000) { - // ignore 1000 CLOSE_NORMAL, 1001 CLOSE_GOING_AWAY, - // 1005 CLOSE_NO_STATUS, 1006 CLOSE_ABNORMAL - // ignore 1007...3999 - // handle 1002 CLOSE_PROTOCOL_ERROR, 1003 CLOSE_UNSUPPORTED, - // 1004 CLOSE_TOO_LARGE - if (closeEvent.code >= 1002 && closeEvent.code <= 1004) { - return "backoff"; - } else { - return null; - } - } else if (closeEvent.code === 4000) { - return "ssl_only"; - } else if (closeEvent.code < 4100) { - return "refused"; - } else if (closeEvent.code < 4200) { - return "backoff"; - } else if (closeEvent.code < 4300) { - return "retry"; - } else { - // unknown error - return "refused"; - } - }; - - /** - * Returns an error or null basing on the close event. - * - * Null is returned when connection was closed cleanly. Otherwise, an object - * with error details is returned. - * - * @param {CloseEvent} closeEvent - * @return {Object} error object - */ - Protocol.getCloseError = function(closeEvent) { - if (closeEvent.code !== 1000 && closeEvent.code !== 1001) { - return { - type: 'PusherError', - data: { - code: closeEvent.code, - message: closeEvent.reason || closeEvent.message - } - }; - } else { - return null; - } - }; - - Pusher.Protocol = Protocol; -}).call(this); - -;(function() { - /** - * Provides Pusher protocol interface for transports. - * - * Emits following events: - * - message - on received messages - * - ping - on ping requests - * - pong - on pong responses - * - error - when the transport emits an error - * - closed - after closing the transport - * - * It also emits more events when connection closes with a code. - * See Protocol.getCloseAction to get more details. - * - * @param {Number} id - * @param {AbstractTransport} transport - */ - function Connection(id, transport) { - Pusher.EventsDispatcher.call(this); - - this.id = id; - this.transport = transport; - this.bindListeners(); - } - var prototype = Connection.prototype; - Pusher.Util.extend(prototype, Pusher.EventsDispatcher.prototype); - - /** Returns whether used transport handles ping/pong by itself - * - * @returns {Boolean} true if ping is handled by the transport - */ - prototype.supportsPing = function() { - return this.transport.supportsPing(); - }; - - /** Sends raw data. - * - * @param {String} data - */ - prototype.send = function(data) { - return this.transport.send(data); - }; - - /** Sends an event. - * - * @param {String} name - * @param {String} data - * @param {String} [channel] - * @returns {Boolean} whether message was sent or not - */ - prototype.send_event = function(name, data, channel) { - var message = { event: name, data: data }; - if (channel) { - message.channel = channel; - } - Pusher.debug('Event sent', message); - return this.send(Pusher.Protocol.encodeMessage(message)); - }; - - /** Closes the connection. */ - prototype.close = function() { - this.transport.close(); - }; - - /** @private */ - prototype.bindListeners = function() { - var self = this; - - var onMessage = function(m) { - var message; - try { - message = Pusher.Protocol.decodeMessage(m); - } catch(e) { - self.emit('error', { - type: 'MessageParseError', - error: e, - data: m.data - }); - } - - if (message !== undefined) { - Pusher.debug('Event recd', message); - - switch (message.event) { - case 'pusher:error': - self.emit('error', { type: 'PusherError', data: message.data }); - break; - case 'pusher:ping': - self.emit("ping"); - break; - case 'pusher:pong': - self.emit("pong"); - break; - } - self.emit('message', message); - } - }; - var onPingRequest = function() { - self.emit("ping_request"); - }; - var onError = function(error) { - self.emit("error", { type: "WebSocketError", error: error }); - }; - var onClosed = function(closeEvent) { - unbindListeners(); - - if (closeEvent && closeEvent.code) { - self.handleCloseEvent(closeEvent); - } - - self.transport = null; - self.emit("closed"); - }; - - var unbindListeners = function() { - self.transport.unbind("closed", onClosed); - self.transport.unbind("error", onError); - self.transport.unbind("ping_request", onPingRequest); - self.transport.unbind("message", onMessage); - }; - - self.transport.bind("message", onMessage); - self.transport.bind("ping_request", onPingRequest); - self.transport.bind("error", onError); - self.transport.bind("closed", onClosed); - }; - - /** @private */ - prototype.handleCloseEvent = function(closeEvent) { - var action = Pusher.Protocol.getCloseAction(closeEvent); - var error = Pusher.Protocol.getCloseError(closeEvent); - if (error) { - this.emit('error', error); - } - if (action) { - this.emit(action); - } - }; - - Pusher.Connection = Connection; -}).call(this); - -;(function() { - /** - * Handles Pusher protocol handshakes for transports. - * - * Calls back with a result object after handshake is completed. Results - * always have two fields: - * - action - string describing action to be taken after the handshake - * - transport - the transport object passed to the constructor - * - * Different actions can set different additional properties on the result. - * In the case of 'connected' action, there will be a 'connection' property - * containing a Connection object for the transport. Other actions should - * carry an 'error' property. - * - * @param {AbstractTransport} transport - * @param {Function} callback - */ - function Handshake(transport, callback) { - this.transport = transport; - this.callback = callback; - this.bindListeners(); - } - var prototype = Handshake.prototype; - - prototype.close = function() { - this.unbindListeners(); - this.transport.close(); - }; - - /** @private */ - prototype.bindListeners = function() { - var self = this; - - self.onMessage = function(m) { - self.unbindListeners(); - - try { - var result = Pusher.Protocol.processHandshake(m); - if (result.action === "connected") { - self.finish("connected", { - connection: new Pusher.Connection(result.id, self.transport) - }); - } else { - self.finish(result.action, { error: result.error }); - self.transport.close(); - } - } catch (e) { - self.finish("error", { error: e }); - self.transport.close(); - } - }; - - self.onClosed = function(closeEvent) { - self.unbindListeners(); - - var action = Pusher.Protocol.getCloseAction(closeEvent) || "backoff"; - var error = Pusher.Protocol.getCloseError(closeEvent); - self.finish(action, { error: error }); - }; - - self.transport.bind("message", self.onMessage); - self.transport.bind("closed", self.onClosed); - }; - - /** @private */ - prototype.unbindListeners = function() { - this.transport.unbind("message", this.onMessage); - this.transport.unbind("closed", this.onClosed); - }; - - /** @private */ - prototype.finish = function(action, params) { - this.callback( - Pusher.Util.extend({ transport: this.transport, action: action }, params) - ); - }; - - Pusher.Handshake = Handshake; -}).call(this); - -;(function() { - /** Manages connection to Pusher. - * - * Uses a strategy (currently only default), timers and network availability - * info to establish a connection and export its state. In case of failures, - * manages reconnection attempts. - * - * Exports state changes as following events: - * - "state_change", { previous: p, current: state } - * - state - * - * States: - * - initialized - initial state, never transitioned to - * - connecting - connection is being established - * - connected - connection has been fully established - * - disconnected - on requested disconnection or before reconnecting - * - unavailable - after connection timeout or when there's no network - * - * Options: - * - unavailableTimeout - time to transition to unavailable state - * - activityTimeout - time after which ping message should be sent - * - pongTimeout - time for Pusher to respond with pong before reconnecting - * - * @param {String} key application key - * @param {Object} options - */ - function ConnectionManager(key, options) { - Pusher.EventsDispatcher.call(this); - - this.key = key; - this.options = options || {}; - this.state = "initialized"; - this.connection = null; - this.encrypted = !!options.encrypted; - this.timeline = this.options.getTimeline(); - - this.connectionCallbacks = this.buildConnectionCallbacks(); - this.errorCallbacks = this.buildErrorCallbacks(); - this.handshakeCallbacks = this.buildHandshakeCallbacks(this.errorCallbacks); - - var self = this; - - Pusher.Network.bind("online", function() { - self.timeline.info({ netinfo: "online" }); - if (self.state === "unavailable") { - self.connect(); - } - }); - Pusher.Network.bind("offline", function() { - self.timeline.info({ netinfo: "offline" }); - if (self.shouldRetry()) { - self.disconnect(); - self.updateState("unavailable"); - } - }); - - var sendTimeline = function() { - if (self.timelineSender) { - self.timelineSender.send(function() {}); - } - }; - this.bind("connected", sendTimeline); - setInterval(sendTimeline, 60000); - - this.updateStrategy(); - } - var prototype = ConnectionManager.prototype; - - Pusher.Util.extend(prototype, Pusher.EventsDispatcher.prototype); - - /** Establishes a connection to Pusher. - * - * Does nothing when connection is already established. See top-level doc - * to find events emitted on connection attempts. - */ - prototype.connect = function() { - var self = this; - - if (self.connection) { - return; - } - if (self.state === "connecting") { - return; - } - - if (!self.strategy.isSupported()) { - self.updateState("failed"); - return; - } - if (Pusher.Network.isOnline() === false) { - self.updateState("unavailable"); - return; - } - - self.updateState("connecting"); - self.timelineSender = self.options.getTimelineSender( - self.timeline, - { encrypted: self.encrypted }, - self - ); - - var callback = function(error, handshake) { - if (error) { - self.runner = self.strategy.connect(0, callback); - } else { - if (handshake.action === "error") { - self.timeline.error({ handshakeError: handshake.error }); - } else { - // we don't support switching connections yet - self.runner.abort(); - self.handshakeCallbacks[handshake.action](handshake); - } - } - }; - self.runner = self.strategy.connect(0, callback); - - self.setUnavailableTimer(); - }; - - /** Sends raw data. - * - * @param {String} data - */ - prototype.send = function(data) { - if (this.connection) { - return this.connection.send(data); - } else { - return false; - } - }; - - /** Sends an event. - * - * @param {String} name - * @param {String} data - * @param {String} [channel] - * @returns {Boolean} whether message was sent or not - */ - prototype.send_event = function(name, data, channel) { - if (this.connection) { - return this.connection.send_event(name, data, channel); - } else { - return false; - } - }; - - /** Closes the connection. */ - prototype.disconnect = function() { - if (this.runner) { - this.runner.abort(); - } - this.clearRetryTimer(); - this.clearUnavailableTimer(); - this.stopActivityCheck(); - this.updateState("disconnected"); - // we're in disconnected state, so closing will not cause reconnecting - if (this.connection) { - this.connection.close(); - this.abandonConnection(); - } - }; - - /** @private */ - prototype.updateStrategy = function() { - this.strategy = this.options.getStrategy({ - key: this.key, - timeline: this.timeline, - encrypted: this.encrypted - }); - }; - - /** @private */ - prototype.retryIn = function(delay) { - var self = this; - self.timeline.info({ action: "retry", delay: delay }); - if (delay > 0) { - self.emit("connecting_in", Math.round(delay / 1000)); - } - self.retryTimer = new Pusher.Timer(delay || 0, function() { - self.disconnect(); - self.connect(); - }); - }; - - /** @private */ - prototype.clearRetryTimer = function() { - if (this.retryTimer) { - this.retryTimer.ensureAborted(); - } - }; - - /** @private */ - prototype.setUnavailableTimer = function() { - var self = this; - self.unavailableTimer = new Pusher.Timer( - self.options.unavailableTimeout, - function() { - self.updateState("unavailable"); - } - ); - }; - - /** @private */ - prototype.clearUnavailableTimer = function() { - if (this.unavailableTimer) { - this.unavailableTimer.ensureAborted(); - } - }; - - /** @private */ - prototype.resetActivityCheck = function() { - this.stopActivityCheck(); - // send ping after inactivity - if (!this.connection.supportsPing()) { - var self = this; - self.activityTimer = new Pusher.Timer( - self.options.activityTimeout, - function() { - self.send_event('pusher:ping', {}); - // wait for pong response - self.activityTimer = new Pusher.Timer( - self.options.pongTimeout, - function() { - self.connection.close(); - } - ); - } - ); - } - }; - - /** @private */ - prototype.stopActivityCheck = function() { - if (this.activityTimer) { - this.activityTimer.ensureAborted(); - } - }; - - /** @private */ - prototype.buildConnectionCallbacks = function() { - var self = this; - return { - message: function(message) { - // includes pong messages from server - self.resetActivityCheck(); - self.emit('message', message); - }, - ping: function() { - self.send_event('pusher:pong', {}); - }, - ping_request: function() { - self.send_event('pusher:ping', {}); - }, - error: function(error) { - // just emit error to user - socket will already be closed by browser - self.emit("error", { type: "WebSocketError", error: error }); - }, - closed: function() { - self.abandonConnection(); - if (self.shouldRetry()) { - self.retryIn(1000); - } - } - }; - }; - - /** @private */ - prototype.buildHandshakeCallbacks = function(errorCallbacks) { - var self = this; - return Pusher.Util.extend({}, errorCallbacks, { - connected: function(handshake) { - self.clearUnavailableTimer(); - self.setConnection(handshake.connection); - self.socket_id = self.connection.id; - self.updateState("connected"); - } - }); - }; - - /** @private */ - prototype.buildErrorCallbacks = function() { - var self = this; - - function withErrorEmitted(callback) { - return function(result) { - if (result.error) { - self.emit("error", { type: "WebSocketError", error: result.error }); - } - callback(result); - }; - } - - return { - ssl_only: withErrorEmitted(function() { - self.encrypted = true; - self.updateStrategy(); - self.retryIn(0); - }), - refused: withErrorEmitted(function() { - self.disconnect(); - }), - backoff: withErrorEmitted(function() { - self.retryIn(1000); - }), - retry: withErrorEmitted(function() { - self.retryIn(0); - }) - }; - }; - - /** @private */ - prototype.setConnection = function(connection) { - this.connection = connection; - for (var event in this.connectionCallbacks) { - this.connection.bind(event, this.connectionCallbacks[event]); - } - this.resetActivityCheck(); - }; - - /** @private */ - prototype.abandonConnection = function() { - if (!this.connection) { - return; - } - for (var event in this.connectionCallbacks) { - this.connection.unbind(event, this.connectionCallbacks[event]); - } - this.connection = null; - }; - - /** @private */ - prototype.updateState = function(newState, data) { - var previousState = this.state; - - this.state = newState; - // Only emit when the state changes - if (previousState !== newState) { - Pusher.debug('State changed', previousState + ' -> ' + newState); - - this.timeline.info({ state: newState }); - this.emit('state_change', { previous: previousState, current: newState }); - this.emit(newState, data); - } - }; - - /** @private */ - prototype.shouldRetry = function() { - return this.state === "connecting" || this.state === "connected"; - }; - - Pusher.ConnectionManager = ConnectionManager; -}).call(this); - -;(function() { - /** Really basic interface providing network availability info. - * - * Emits: - * - online - when browser goes online - * - offline - when browser goes offline - */ - function NetInfo() { - Pusher.EventsDispatcher.call(this); - - var self = this; - // This is okay, as IE doesn't support this stuff anyway. - if (window.addEventListener !== undefined) { - window.addEventListener("online", function() { - self.emit('online'); - }, false); - window.addEventListener("offline", function() { - self.emit('offline'); - }, false); - } - } - Pusher.Util.extend(NetInfo.prototype, Pusher.EventsDispatcher.prototype); - - var prototype = NetInfo.prototype; - - /** Returns whether browser is online or not - * - * Offline means definitely offline (no connection to router). - * Inverse does NOT mean definitely online (only currently supported in Safari - * and even there only means the device has a connection to the router). - * - * @return {Boolean} - */ - prototype.isOnline = function() { - if (window.navigator.onLine === undefined) { - return true; - } else { - return window.navigator.onLine; - } - }; - - Pusher.NetInfo = NetInfo; - Pusher.Network = new NetInfo(); -}).call(this); - -;(function() { - /** Represents a collection of members of a presence channel. */ - function Members() { - this.reset(); - } - var prototype = Members.prototype; - - /** Returns member's info for given id. - * - * Resulting object containts two fields - id and info. - * - * @param {Number} id - * @return {Object} member's info or null - */ - prototype.get = function(id) { - if (Object.prototype.hasOwnProperty.call(this.members, id)) { - return { - id: id, - info: this.members[id] - }; - } else { - return null; - } - }; - - /** Calls back for each member in unspecified order. - * - * @param {Function} callback - */ - prototype.each = function(callback) { - var self = this; - Pusher.Util.objectApply(self.members, function(member, id) { - callback(self.get(id)); - }); - }; - - /** Updates the id for connected member. For internal use only. */ - prototype.setMyID = function(id) { - this.myID = id; - }; - - /** Handles subscription data. For internal use only. */ - prototype.onSubscription = function(subscriptionData) { - this.members = subscriptionData.presence.hash; - this.count = subscriptionData.presence.count; - this.me = this.get(this.myID); - }; - - /** Adds a new member to the collection. For internal use only. */ - prototype.addMember = function(memberData) { - if (this.get(memberData.user_id) === null) { - this.count++; - } - this.members[memberData.user_id] = memberData.user_info; - return this.get(memberData.user_id); - }; - - /** Adds a member from the collection. For internal use only. */ - prototype.removeMember = function(memberData) { - var member = this.get(memberData.user_id); - if (member) { - delete this.members[memberData.user_id]; - this.count--; - } - return member; - }; - - /** Resets the collection to the initial state. For internal use only. */ - prototype.reset = function() { - this.members = {}; - this.count = 0; - this.myID = null; - this.me = null; - }; - - Pusher.Members = Members; -}).call(this); - -;(function() { - /** Provides base public channel interface with an event emitter. - * - * Emits: - * - pusher:subscription_succeeded - after subscribing successfully - * - other non-internal events - * - * @param {String} name - * @param {Pusher} pusher - */ - function Channel(name, pusher) { - Pusher.EventsDispatcher.call(this, function(event, data) { - Pusher.debug('No callbacks on ' + name + ' for ' + event); - }); - - this.name = name; - this.pusher = pusher; - this.subscribed = false; - } - var prototype = Channel.prototype; - Pusher.Util.extend(prototype, Pusher.EventsDispatcher.prototype); - - /** Skips authorization, since public channels don't require it. - * - * @param {Function} callback - */ - prototype.authorize = function(socketId, callback) { - return callback(false, {}); - }; - - /** Triggers an event */ - prototype.trigger = function(event, data) { - return this.pusher.send_event(event, data, this.name); - }; - - /** Signals disconnection to the channel. For internal use only. */ - prototype.disconnect = function() { - this.subscribed = false; - }; - - /** Handles an event. For internal use only. - * - * @param {String} event - * @param {*} data - */ - prototype.handleEvent = function(event, data) { - if (event.indexOf("pusher_internal:") === 0) { - if (event === "pusher_internal:subscription_succeeded") { - this.subscribed = true; - this.emit("pusher:subscription_succeeded", data); - } - } else { - this.emit(event, data); - } - }; - - Pusher.Channel = Channel; -}).call(this); - -;(function() { - /** Extends public channels to provide private channel interface. - * - * @param {String} name - * @param {Pusher} pusher - */ - function PrivateChannel(name, pusher) { - Pusher.Channel.call(this, name, pusher); - } - var prototype = PrivateChannel.prototype; - Pusher.Util.extend(prototype, Pusher.Channel.prototype); - - /** Authorizes the connection to use the channel. - * - * @param {String} socketId - * @param {Function} callback - */ - prototype.authorize = function(socketId, callback) { - var authorizer = new Pusher.Channel.Authorizer(this, this.pusher.config); - return authorizer.authorize(socketId, callback); - }; - - Pusher.PrivateChannel = PrivateChannel; -}).call(this); - -;(function() { - /** Adds presence channel functionality to private channels. - * - * @param {String} name - * @param {Pusher} pusher - */ - function PresenceChannel(name, pusher) { - Pusher.PrivateChannel.call(this, name, pusher); - this.members = new Pusher.Members(); - } - var prototype = PresenceChannel.prototype; - Pusher.Util.extend(prototype, Pusher.PrivateChannel.prototype); - - /** Authenticates the connection as a member of the channel. - * - * @param {String} socketId - * @param {Function} callback - */ - prototype.authorize = function(socketId, callback) { - var _super = Pusher.PrivateChannel.prototype.authorize; - var self = this; - _super.call(self, socketId, function(error, authData) { - if (!error) { - if (authData.channel_data === undefined) { - Pusher.warn( - "Invalid auth response for channel '" + - self.name + - "', expected 'channel_data' field" - ); - callback("Invalid auth response"); - return; - } - var channelData = JSON.parse(authData.channel_data); - self.members.setMyID(channelData.user_id); - } - callback(error, authData); - }); - }; - - /** Handles presence and subscription events. For internal use only. - * - * @param {String} event - * @param {*} data - */ - prototype.handleEvent = function(event, data) { - switch (event) { - case "pusher_internal:subscription_succeeded": - this.members.onSubscription(data); - this.subscribed = true; - this.emit("pusher:subscription_succeeded", this.members); - break; - case "pusher_internal:member_added": - var addedMember = this.members.addMember(data); - this.emit('pusher:member_added', addedMember); - break; - case "pusher_internal:member_removed": - var removedMember = this.members.removeMember(data); - if (removedMember) { - this.emit('pusher:member_removed', removedMember); - } - break; - default: - Pusher.PrivateChannel.prototype.handleEvent.call(this, event, data); - } - }; - - /** Resets the channel state, including members map. For internal use only. */ - prototype.disconnect = function() { - this.members.reset(); - Pusher.PrivateChannel.prototype.disconnect.call(this); - }; - - Pusher.PresenceChannel = PresenceChannel; -}).call(this); - -;(function() { - /** Handles a channel map. */ - function Channels() { - this.channels = {}; - } - var prototype = Channels.prototype; - - /** Creates or retrieves an existing channel by its name. - * - * @param {String} name - * @param {Pusher} pusher - * @return {Channel} - */ - prototype.add = function(name, pusher) { - if (!this.channels[name]) { - this.channels[name] = createChannel(name, pusher); - } - return this.channels[name]; - }; - - /** Finds a channel by its name. - * - * @param {String} name - * @return {Channel} channel or null if it doesn't exist - */ - prototype.find = function(name) { - return this.channels[name]; - }; - - /** Removes a channel from the map. - * - * @param {String} name - */ - prototype.remove = function(name) { - delete this.channels[name]; - }; - - /** Proxies disconnection signal to all channels. */ - prototype.disconnect = function() { - Pusher.Util.objectApply(this.channels, function(channel) { - channel.disconnect(); - }); - }; - - function createChannel(name, pusher) { - if (name.indexOf('private-') === 0) { - return new Pusher.PrivateChannel(name, pusher); - } else if (name.indexOf('presence-') === 0) { - return new Pusher.PresenceChannel(name, pusher); - } else { - return new Pusher.Channel(name, pusher); - } - } - - Pusher.Channels = Channels; -}).call(this); - -;(function() { - Pusher.Channel.Authorizer = function(channel, options) { - this.channel = channel; - this.type = options.authTransport; - - this.options = options; - this.authOptions = (options || {}).auth || {}; - }; - - Pusher.Channel.Authorizer.prototype = { - composeQuery: function(socketId) { - var query = '&socket_id=' + encodeURIComponent(socketId) + - '&channel_name=' + encodeURIComponent(this.channel.name); - - for(var i in this.authOptions.params) { - query += "&" + encodeURIComponent(i) + "=" + encodeURIComponent(this.authOptions.params[i]); - } - - return query; - }, - - authorize: function(socketId, callback) { - return Pusher.authorizers[this.type].call(this, socketId, callback); - } - }; - - var nextAuthCallbackID = 1; - - Pusher.auth_callbacks = {}; - Pusher.authorizers = { - ajax: function(socketId, callback){ - var self = this, xhr; - - if (Pusher.XHR) { - xhr = new Pusher.XHR(); - } else { - xhr = (window.XMLHttpRequest ? new window.XMLHttpRequest() : new ActiveXObject("Microsoft.XMLHTTP")); - } - - xhr.open("POST", self.options.authEndpoint, true); - - // add request headers - xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded"); - for(var headerName in this.authOptions.headers) { - xhr.setRequestHeader(headerName, this.authOptions.headers[headerName]); - } - - xhr.onreadystatechange = function() { - if (xhr.readyState == 4) { - if (xhr.status == 200) { - var data, parsed = false; - - try { - data = JSON.parse(xhr.responseText); - parsed = true; - } catch (e) { - callback(true, 'JSON returned from webapp was invalid, yet status code was 200. Data was: ' + xhr.responseText); - } - - if (parsed) { // prevents double execution. - callback(false, data); - } - } else { - Pusher.warn("Couldn't get auth info from your webapp", xhr.status); - callback(true, xhr.status); - } - } - }; - - xhr.send(this.composeQuery(socketId)); - return xhr; - }, - - jsonp: function(socketId, callback){ - if(this.authOptions.headers !== undefined) { - Pusher.warn("Warn", "To send headers with the auth request, you must use AJAX, rather than JSONP."); - } - - var callbackName = nextAuthCallbackID.toString(); - nextAuthCallbackID++; - - var script = document.createElement("script"); - // Hacked wrapper. - Pusher.auth_callbacks[callbackName] = function(data) { - callback(false, data); - }; - - var callback_name = "Pusher.auth_callbacks['" + callbackName + "']"; - script.src = this.options.authEndpoint + - '?callback=' + - encodeURIComponent(callback_name) + - this.composeQuery(socketId); - - var head = document.getElementsByTagName("head")[0] || document.documentElement; - head.insertBefore( script, head.firstChild ); - } - }; -}).call(this); - -module.exports = this.Pusher; From b1ca43ac0f823729db3bd6d86ac2f3d7d988c569 Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Wed, 10 Jul 2013 09:15:24 -0700 Subject: [PATCH 39/62] Upgrade telepath --- src/app/edit-session.coffee | 2 +- src/app/pane-axis.coffee | 2 +- src/app/pane-container.coffee | 2 +- src/app/pane.coffee | 4 ++-- src/packages/collaboration/lib/guest-session.coffee | 2 +- src/packages/collaboration/lib/host-session.coffee | 3 +-- src/packages/collaboration/lib/session-utils.coffee | 4 ++-- vendor/telepath | 2 +- 8 files changed, 10 insertions(+), 11 deletions(-) diff --git a/src/app/edit-session.coffee b/src/app/edit-session.coffee index 0209e30b5..b7bb9e64c 100644 --- a/src/app/edit-session.coffee +++ b/src/app/edit-session.coffee @@ -75,7 +75,7 @@ class EditSession @displayBuffer.on 'grammar-changed', => @handleGrammarChange() - @state.observe ({key, newValue}) => + @state.on 'changed', ({key, newValue}) => switch key when 'scrollTop' @trigger 'scroll-top-changed', newValue diff --git a/src/app/pane-axis.coffee b/src/app/pane-axis.coffee index 427ac157f..46397c69c 100644 --- a/src/app/pane-axis.coffee +++ b/src/app/pane-axis.coffee @@ -18,7 +18,7 @@ class PaneAxis extends View @state = telepath.Document.create(deserializer: @className(), children: []) @addChild(child) for child in args - @state.get('children').observe ({index, inserted, removed, site}) => + @state.get('children').on 'changed', ({index, inserted, removed, site}) => return if site is @state.site.id for childState in removed @removeChild(@children(":eq(#{index})").view(), updateState: false) diff --git a/src/app/pane-container.coffee b/src/app/pane-container.coffee index d8365f41a..2e4826470 100644 --- a/src/app/pane-container.coffee +++ b/src/app/pane-container.coffee @@ -24,7 +24,7 @@ class PaneContainer extends View else @state = telepath.Document.create(deserializer: 'PaneContainer') - @state.observe ({key, newValue, site}) => + @state.on 'changed', ({key, newValue, site}) => return if site is @state.site.id if key is 'root' @setRoot(deserialize(newValue), updateState: false) diff --git a/src/app/pane.coffee b/src/app/pane.coffee index 043c49ccf..c319d1bb5 100644 --- a/src/app/pane.coffee +++ b/src/app/pane.coffee @@ -35,14 +35,14 @@ class Pane extends View deserializer: 'Pane' items: @items.map (item) -> item.getState?() ? item.serialize() - @state.get('items').observe ({index, removed, inserted, site}) => + @state.get('items').on 'changed', ({index, removed, inserted, site}) => return if site is @state.site.id for itemState in removed @removeItemAtIndex(index, updateState: false) for itemState, i in inserted @addItem(deserialize(itemState), index + i, updateState: false) - @state.observe ({key, newValue, site}) => + @state.on 'changed', ({key, newValue, site}) => return if site is @state.site.id @showItemForUri(newValue) if key is 'activeItemUri' diff --git a/src/packages/collaboration/lib/guest-session.coffee b/src/packages/collaboration/lib/guest-session.coffee index 55d9a7af2..5148e9e92 100644 --- a/src/packages/collaboration/lib/guest-session.coffee +++ b/src/packages/collaboration/lib/guest-session.coffee @@ -16,7 +16,7 @@ class GuestSession console.log 'connection opened' connection.once 'data', (data) => console.log 'received document' - doc = telepath.Document.deserialize(telepath.createSite(@getId()), data) + doc = telepath.Document.deserialize(data, site: telepath.createSite(@getId())) atom.windowState = doc.get('windowState') @participants = doc.get('participants') connectDocument(doc, connection) diff --git a/src/packages/collaboration/lib/host-session.coffee b/src/packages/collaboration/lib/host-session.coffee index 24d5d4a47..be8440666 100644 --- a/src/packages/collaboration/lib/host-session.coffee +++ b/src/packages/collaboration/lib/host-session.coffee @@ -22,11 +22,10 @@ class HostSession @participants.push id: @getId() email: git.getConfigValue('user.email') - @participants.observe => + @participants.on 'changed', => @trigger 'participants-changed', @participants.toObject() @peer.on 'connection', (connection) => - console.log connection connection.on 'open', => console.log 'sending document' connection.send(@doc.serialize()) diff --git a/src/packages/collaboration/lib/session-utils.coffee b/src/packages/collaboration/lib/session-utils.coffee index fe5a428be..f93f188ee 100644 --- a/src/packages/collaboration/lib/session-utils.coffee +++ b/src/packages/collaboration/lib/session-utils.coffee @@ -13,7 +13,7 @@ module.exports = event.id = nextOutputEventId++ console.log 'sending event', event.id, event connection.send(event) - doc.outputEvents.on('changed', outputListener) + doc.on('output', outputListener) queuedEvents = [] nextInputEventId = 1 @@ -40,7 +40,7 @@ module.exports = queuedEvents.push(event) connection.on 'close', -> - doc.outputEvents.removeListener('changed', outputListener) + doc.off('changed', outputListener) connection.on 'error', (error) -> console.error 'connection error', error.stack ? error diff --git a/vendor/telepath b/vendor/telepath index 3b465ef7e..a120988f1 160000 --- a/vendor/telepath +++ b/vendor/telepath @@ -1 +1 @@ -Subproject commit 3b465ef7e08c188621e3a30817650fbea38d3656 +Subproject commit a120988f18768145e18d708c8c5b15ad328a6df6 From f3ca26e2c95d6237822043032f39ceb5a210fc4b Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Wed, 10 Jul 2013 09:23:53 -0700 Subject: [PATCH 40/62] Trigger participants-changed in guest session --- src/packages/collaboration/lib/guest-session.coffee | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/packages/collaboration/lib/guest-session.coffee b/src/packages/collaboration/lib/guest-session.coffee index 5148e9e92..c97bcb746 100644 --- a/src/packages/collaboration/lib/guest-session.coffee +++ b/src/packages/collaboration/lib/guest-session.coffee @@ -19,6 +19,8 @@ class GuestSession doc = telepath.Document.deserialize(data, site: telepath.createSite(@getId())) atom.windowState = doc.get('windowState') @participants = doc.get('participants') + @participants.on 'changed', => + @trigger 'participants-changed', @participants.toObject() connectDocument(doc, connection) @trigger 'started' From 98765c7d5cc0ec8bad5cf32f5e872977c03d49a3 Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Wed, 10 Jul 2013 09:24:15 -0700 Subject: [PATCH 41/62] Only display participants that aren't the host --- src/packages/collaboration/lib/host-view.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/packages/collaboration/lib/host-view.coffee b/src/packages/collaboration/lib/host-view.coffee index 5580a808b..b2a59d327 100644 --- a/src/packages/collaboration/lib/host-view.coffee +++ b/src/packages/collaboration/lib/host-view.coffee @@ -31,7 +31,7 @@ class HostView extends View updateParticipants: (participants) -> @participants.empty() - for {email, id} in participants when id is @hostSession.getId() + for {email, id} in participants when id isnt @hostSession.getId() @participants.append $$ -> @div email From 1836257f0be561110a01da26e4beffd9cf73cf24 Mon Sep 17 00:00:00 2001 From: Corey Johnson & Kevin Sawicki Date: Wed, 10 Jul 2013 09:56:57 -0700 Subject: [PATCH 42/62] Show avatars in host and guest views --- src/packages/collaboration/lib/guest-view.coffee | 7 ++++--- src/packages/collaboration/lib/host-view.coffee | 7 ++++--- .../collaboration/lib/participant-view.coffee | 12 ++++++++++++ .../collaboration/stylesheets/collaboration.less | 11 +++++++++++ 4 files changed, 31 insertions(+), 6 deletions(-) create mode 100644 src/packages/collaboration/lib/participant-view.coffee diff --git a/src/packages/collaboration/lib/guest-view.coffee b/src/packages/collaboration/lib/guest-view.coffee index f499ab9da..ab10b3c77 100644 --- a/src/packages/collaboration/lib/guest-view.coffee +++ b/src/packages/collaboration/lib/guest-view.coffee @@ -1,4 +1,5 @@ {$$, View} = require 'space-pen' +ParticipantView = require './participant-view' module.exports = class GuestView extends View @@ -19,9 +20,9 @@ class GuestView extends View updateParticipants: (participants) -> @participants.empty() - for {email, id} in participants when id isnt @guestSession.getId() - @participants.append $$ -> - @div email + guestId = @guestSession.getId() + for participant in participants when participant.id isnt guestId + @participants.append(new ParticipantView(participant)) toggle: -> if @hasParent() diff --git a/src/packages/collaboration/lib/host-view.coffee b/src/packages/collaboration/lib/host-view.coffee index b2a59d327..31dae50a2 100644 --- a/src/packages/collaboration/lib/host-view.coffee +++ b/src/packages/collaboration/lib/host-view.coffee @@ -1,4 +1,5 @@ {$$, View} = require 'space-pen' +ParticipantView = require './participant-view' module.exports = class HostView extends View @@ -31,9 +32,9 @@ class HostView extends View updateParticipants: (participants) -> @participants.empty() - for {email, id} in participants when id isnt @hostSession.getId() - @participants.append $$ -> - @div email + hostId = @hostSession.getId() + for participant in participants when participant.id isnt hostId + @participants.append(new ParticipantView(participant)) toggle: -> if @hasParent() diff --git a/src/packages/collaboration/lib/participant-view.coffee b/src/packages/collaboration/lib/participant-view.coffee new file mode 100644 index 000000000..e5eec1caf --- /dev/null +++ b/src/packages/collaboration/lib/participant-view.coffee @@ -0,0 +1,12 @@ +crypto = require 'crypto' +{View} = require 'space-pen' + +module.exports = +class ParticipantView extends View + @content: -> + @div class: 'participant', => + @img class: 'avatar', outlet: 'avatar' + + initialize: ({id, email}) -> + emailMd5 = crypto.createHash('md5').update(email).digest('hex') + @avatar.attr('src', "http://www.gravatar.com/avatar/#{emailMd5}?s=32") diff --git a/src/packages/collaboration/stylesheets/collaboration.less b/src/packages/collaboration/stylesheets/collaboration.less index 810ef6336..4ed3b3037 100644 --- a/src/packages/collaboration/stylesheets/collaboration.less +++ b/src/packages/collaboration/stylesheets/collaboration.less @@ -10,10 +10,14 @@ .share { .mini-icon(notifications); + margin-bottom: 5px; } .guest { .mini-icon(watchers); + position: relative; + right: -2px; + margin-bottom: 5px; color: #96CBFE; } @@ -24,4 +28,11 @@ color: lighten(@runningColor, 15%); } } + + .avatar { + border-radius: 3px; + height: 16px; + width: 16px; + margin: 2px; + } } From 3ce520d9dee594c6d5ceea129dc86ad0a6aa59f9 Mon Sep 17 00:00:00 2001 From: Corey Johnson & Kevin Sawicki Date: Wed, 10 Jul 2013 10:14:31 -0700 Subject: [PATCH 43/62] Store participants and repository under collaborationState doc --- src/packages/collaboration/lib/guest-session.coffee | 2 +- src/packages/collaboration/lib/host-session.coffee | 9 +++++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/packages/collaboration/lib/guest-session.coffee b/src/packages/collaboration/lib/guest-session.coffee index c97bcb746..a05c6a079 100644 --- a/src/packages/collaboration/lib/guest-session.coffee +++ b/src/packages/collaboration/lib/guest-session.coffee @@ -18,7 +18,7 @@ class GuestSession console.log 'received document' doc = telepath.Document.deserialize(data, site: telepath.createSite(@getId())) atom.windowState = doc.get('windowState') - @participants = doc.get('participants') + @participants = doc.get('collaborationState.participants') @participants.on 'changed', => @trigger 'participants-changed', @participants.toObject() connectDocument(doc, connection) diff --git a/src/packages/collaboration/lib/host-session.coffee b/src/packages/collaboration/lib/host-session.coffee index be8440666..ccec3286d 100644 --- a/src/packages/collaboration/lib/host-session.coffee +++ b/src/packages/collaboration/lib/host-session.coffee @@ -17,8 +17,13 @@ class HostSession @peer = createPeer() @doc = telepath.Document.create({}, site: telepath.createSite(@getId())) @doc.set('windowState', atom.windowState) - @doc.set('participants', []) - @participants = @doc.get('participants') + @doc.set 'collaborationState', + participants: [] + repository: + url: git.getConfigValue('remote.origin.url') + branch: git.getShortHead() + + @participants = @doc.get('collaborationState.participants') @participants.push id: @getId() email: git.getConfigValue('user.email') From af80327995984b240716a119f4073fc7c82f6723 Mon Sep 17 00:00:00 2001 From: Corey Johnson & Kevin Sawicki Date: Wed, 10 Jul 2013 11:29:23 -0700 Subject: [PATCH 44/62] Set guest session project path from repo name --- src/app/project.coffee | 6 ------ src/app/window.coffee | 5 +---- .../collaboration/lib/bootstrap.coffee | 19 +++++++++++++++++++ .../collaboration/lib/guest-session.coffee | 2 ++ 4 files changed, 22 insertions(+), 10 deletions(-) diff --git a/src/app/project.coffee b/src/app/project.coffee index a9bc2ab5f..08fac445e 100644 --- a/src/app/project.coffee +++ b/src/app/project.coffee @@ -15,8 +15,6 @@ BufferedProcess = require 'buffered-process' # of directories and files that you can operate on. module.exports = class Project - registerDeserializer(this) - @deserialize: (state) -> new Project(state.path) @openers: [] @@ -50,10 +48,6 @@ class Project @editSessions = [] @buffers = [] - serialize: -> - deserializer: 'Project' - path: @getPath() - # Retrieves the project path. # # Returns a {String}. diff --git a/src/app/window.coffee b/src/app/window.coffee index e02c1a24c..53318ee25 100644 --- a/src/app/window.coffee +++ b/src/app/window.coffee @@ -120,10 +120,7 @@ window.deserializeEditorWindow = -> atom.packageStates = windowState.getObject('packageStates') ? {} - window.project = deserialize(windowState.get('project')) - unless window.project? - window.project = new Project(atom.getLoadSettings().initialPath) - windowState.set('project', window.project.serialize()) + window.project = new Project(atom.getLoadSettings().initialPath) window.rootView = deserialize(windowState.get('rootView')) unless window.rootView? diff --git a/src/packages/collaboration/lib/bootstrap.coffee b/src/packages/collaboration/lib/bootstrap.coffee index bf13b8fe9..24b095daf 100644 --- a/src/packages/collaboration/lib/bootstrap.coffee +++ b/src/packages/collaboration/lib/bootstrap.coffee @@ -1,3 +1,6 @@ +remote = require 'remote' +path = require 'path' +url = require 'url' require 'atom' require 'window' $ = require 'jquery' @@ -14,7 +17,23 @@ loadingView = $$ -> $(window.rootViewParentSelector).append(loadingView) atom.show() +syncRepositoryState = -> + repoUrl = atom.guestSession.repository.get('url') + [repoName] = url.parse(repoUrl).path.split('/')[-1..] + repoName = repoName.replace(/\.git$/, '') + repoPath = path.join(remote.require('app').getHomeDir(), 'github', repoName) + + # clone if missing + # abort if working directory is unclean + # apply bundle of unpushed changes from host + # prompt for branch name if branch already exists and is cannot be fast-forwarded + # checkout branch + # sync modified and untracked files from host session + + atom.getLoadSettings().initialPath = repoPath + atom.guestSession = new GuestSession(sessionId) atom.guestSession.on 'started', -> + syncRepositoryState() loadingView.remove() window.startEditorWindow() diff --git a/src/packages/collaboration/lib/guest-session.coffee b/src/packages/collaboration/lib/guest-session.coffee index a05c6a079..5f7ecd1a7 100644 --- a/src/packages/collaboration/lib/guest-session.coffee +++ b/src/packages/collaboration/lib/guest-session.coffee @@ -7,6 +7,7 @@ class GuestSession _.extend @prototype, require('event-emitter') participants: null + repository: null peer: null constructor: (sessionId) -> @@ -21,6 +22,7 @@ class GuestSession @participants = doc.get('collaborationState.participants') @participants.on 'changed', => @trigger 'participants-changed', @participants.toObject() + @repository = doc.get('collaborationState.repository') connectDocument(doc, connection) @trigger 'started' From 601efa53e6bca9fb67c736ec350ea2b1d25e0390 Mon Sep 17 00:00:00 2001 From: Corey Johnson & Kevin Sawicki Date: Wed, 10 Jul 2013 15:41:31 -0700 Subject: [PATCH 45/62] Only create single host view instance --- src/packages/collaboration/lib/collaboration.coffee | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/packages/collaboration/lib/collaboration.coffee b/src/packages/collaboration/lib/collaboration.coffee index 0e7c87116..c29115418 100644 --- a/src/packages/collaboration/lib/collaboration.coffee +++ b/src/packages/collaboration/lib/collaboration.coffee @@ -5,6 +5,8 @@ JoinPromptView = require './join-prompt-view' module.exports = activate: -> + hostView = null + if atom.getLoadSettings().sessionId new GuestView(atom.guestSession) else @@ -15,7 +17,7 @@ module.exports = pasteboard.write(sessionId) if sessionId rootView.command 'collaboration:start-session', -> - new HostView(hostSession) + hostView ?= new HostView(hostSession) if sessionId = hostSession.start() pasteboard.write(sessionId) From 72d76e511eb17ae3276546a3bf001d8ab9c5d163 Mon Sep 17 00:00:00 2001 From: Corey Johnson & Kevin Sawicki Date: Wed, 10 Jul 2013 15:42:32 -0700 Subject: [PATCH 46/62] Begin replication of host repo state --- package.json | 2 +- src/app/git.coffee | 6 ++ .../collaboration/lib/bootstrap.coffee | 40 +++++++- .../collaboration/lib/guest-session.coffee | 7 +- .../collaboration/lib/host-session.coffee | 97 +++++++++++++------ 5 files changed, 115 insertions(+), 37 deletions(-) diff --git a/package.json b/package.json index c80db2ffd..c1bcad0d2 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,7 @@ "ctags": "0.5.0", "oniguruma": "0.16.0", "mkdirp": "0.3.5", - "git-utils": "0.19.0", + "git-utils": "0.21.0", "underscore": "1.4.4", "d3": "3.0.8", "coffee-cache": "0.1.0", diff --git a/src/app/git.coffee b/src/app/git.coffee index cc0eb2404..bffae7be7 100644 --- a/src/app/git.coffee +++ b/src/app/git.coffee @@ -215,6 +215,12 @@ class Git getConfigValue: (key) -> @getRepo().getConfigValue(key) + getReferenceTarget: (reference) -> @getRepo().getReferenceTarget(reference) + + getAheadBehindCount: (reference) -> @getRepo().getAheadBehindCount(reference) + + hasBranch: (branch) -> @getReferenceTarget("refs/heads/#{branch}")? + ### Internal ### refreshStatus: -> diff --git a/src/packages/collaboration/lib/bootstrap.coffee b/src/packages/collaboration/lib/bootstrap.coffee index 24b095daf..b9595dc48 100644 --- a/src/packages/collaboration/lib/bootstrap.coffee +++ b/src/packages/collaboration/lib/bootstrap.coffee @@ -1,10 +1,15 @@ +require 'atom' +require 'window' + +{exec} = require 'child_process' +fs = require 'fs' remote = require 'remote' path = require 'path' url = require 'url' -require 'atom' -require 'window' $ = require 'jquery' +temp = require 'temp' {$$} = require 'space-pen' +Git = require 'git' GuestSession = require './guest-session' window.setDimensions(width: 350, height: 100) @@ -19,14 +24,41 @@ atom.show() syncRepositoryState = -> repoUrl = atom.guestSession.repository.get('url') + branch = atom.guestSession.repository.get('branch') [repoName] = url.parse(repoUrl).path.split('/')[-1..] repoName = repoName.replace(/\.git$/, '') repoPath = path.join(remote.require('app').getHomeDir(), 'github', repoName) + git = new Git(repoPath) - # clone if missing + # clone or fetch # abort if working directory is unclean + # apply bundle of unpushed changes from host - # prompt for branch name if branch already exists and is cannot be fast-forwarded + {unpushedChanges, head} =atom.guestSession.repositoryDelta + if unpushedChanges + tempFile = temp.path(suffix: '.bundle') + fs.writeFileSync(tempFile, new Buffer(atom.guestSession.repositoryDelta.unpushedChanges, 'base64')) + command = "git bundle unbundle #{tempFile}" + exec command, {cwd: repoPath}, (error, stdout, stderr) -> + if error? + console.error error + return + + if git.hasBranch(branch) + if git.getAheadBehindCount(branch).ahead is 0 + command = "git checkout #{branch} && git reset --hard #{head}" + exec command, {cwd: repoPath}, (error, stdout, stderr) -> + if error? + console.error error + return + else + # prompt for new branch name + # create branch at head + else + # create branch at head + + # create branch if it doesn't exist + # prompt for branch name if branch already exists and it cannot be fast-forwarded # checkout branch # sync modified and untracked files from host session diff --git a/src/packages/collaboration/lib/guest-session.coffee b/src/packages/collaboration/lib/guest-session.coffee index 5f7ecd1a7..98c118ca0 100644 --- a/src/packages/collaboration/lib/guest-session.coffee +++ b/src/packages/collaboration/lib/guest-session.coffee @@ -16,13 +16,14 @@ class GuestSession connection.on 'open', => console.log 'connection opened' connection.once 'data', (data) => - console.log 'received document' - doc = telepath.Document.deserialize(data, site: telepath.createSite(@getId())) + console.log 'received document', data + @repositoryDelta = data.repositoryDelta + doc = telepath.Document.deserialize(data.doc, site: telepath.createSite(@getId())) atom.windowState = doc.get('windowState') @participants = doc.get('collaborationState.participants') @participants.on 'changed', => @trigger 'participants-changed', @participants.toObject() - @repository = doc.get('collaborationState.repository') + @repository = doc.get('collaborationState.repositoryState') connectDocument(doc, connection) @trigger 'started' diff --git a/src/packages/collaboration/lib/host-session.coffee b/src/packages/collaboration/lib/host-session.coffee index ccec3286d..c947d6bb6 100644 --- a/src/packages/collaboration/lib/host-session.coffee +++ b/src/packages/collaboration/lib/host-session.coffee @@ -1,4 +1,7 @@ +fs = require 'fs' _ = require 'underscore' +async = require 'async' +temp = require 'temp' telepath = require 'telepath' {createPeer, connectDocument} = require './session-utils' @@ -11,46 +14,82 @@ class HostSession peer: null sharing: false + bundleUnpushedChanges: (callback) -> + localBranch = git.getShortHead() + upstreamBranch = git.getRepo().getUpstreamBranch() + + {exec} = require 'child_process' + tempFile = temp.path(suffix: '.bundle') + command = "git bundle create #{tempFile} #{upstreamBranch}..#{localBranch}" + exec command, {cwd: git.getWorkingDirectory()}, (error, stdout, stderr) -> + callback(error, tempFile) + + bundleWorkingDirectoryChanges: -> + + + bundleRepositoryDelta: (callback) -> + repositoryDelta = {} + + operations = [] + if git.upstream.ahead > 0 + operations.push (callback) => + @bundleUnpushedChanges (error, bundleFile) -> + unless error? + repositoryDelta.unpushedChanges = fs.readFileSync(bundleFile, 'base64') + repositoryDelta.head = git.getRepo().getReferenceTarget(git.getRepo().getHead()) + callback(error) + + async.waterfall operations, (error) -> + callback(error, repositoryDelta) + + unless _.isEmpty(git.statuses) + repositoryDelta.workingDirectoryChanges = @bundleWorkingDirectoryChanges() + start: -> return if @peer? @peer = createPeer() @doc = telepath.Document.create({}, site: telepath.createSite(@getId())) @doc.set('windowState', atom.windowState) - @doc.set 'collaborationState', - participants: [] - repository: - url: git.getConfigValue('remote.origin.url') - branch: git.getShortHead() + @bundleRepositoryDelta (error, repositoryDelta) => + if error? + console.error(error) + return - @participants = @doc.get('collaborationState.participants') - @participants.push - id: @getId() - email: git.getConfigValue('user.email') - @participants.on 'changed', => - @trigger 'participants-changed', @participants.toObject() + @doc.set 'collaborationState', + participants: [] + repositoryState: + url: git.getConfigValue('remote.origin.url') + branch: git.getShortHead() - @peer.on 'connection', (connection) => - connection.on 'open', => - console.log 'sending document' - connection.send(@doc.serialize()) - connectDocument(@doc, connection) + @participants = @doc.get('collaborationState.participants') + @participants.push + id: @getId() + email: git.getConfigValue('user.email') + @participants.on 'changed', => + @trigger 'participants-changed', @participants.toObject() - connection.on 'close', => - console.log 'conection closed' - @participants.each (participant, index) => - if connection.peer is participant.get('id') - @participants.remove(index) + @peer.on 'connection', (connection) => + connection.on 'open', => + console.log 'sending document' + connection.send({repositoryDelta, doc: @doc.serialize()}) + connectDocument(@doc, connection) - @peer.on 'open', => - console.log 'sharing session started' - @sharing = true - @trigger 'started' + connection.on 'close', => + console.log 'conection closed' + @participants.each (participant, index) => + if connection.peer is participant.get('id') + @participants.remove(index) - @peer.on 'close', => - console.log 'sharing session stopped' - @sharing = false - @trigger 'stopped' + @peer.on 'open', => + console.log 'sharing session started' + @sharing = true + @trigger 'started' + + @peer.on 'close', => + console.log 'sharing session stopped' + @sharing = false + @trigger 'stopped' @getId() From 56b333e7fb8b3e7d640f662bd32bc011a8c34090 Mon Sep 17 00:00:00 2001 From: Corey Johnson & Kevin Sawicki Date: Wed, 10 Jul 2013 15:44:25 -0700 Subject: [PATCH 47/62] :lipstick: Kevin found this offensive --- src/packages/collaboration/lib/bootstrap.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/packages/collaboration/lib/bootstrap.coffee b/src/packages/collaboration/lib/bootstrap.coffee index b9595dc48..d70da3d36 100644 --- a/src/packages/collaboration/lib/bootstrap.coffee +++ b/src/packages/collaboration/lib/bootstrap.coffee @@ -34,7 +34,7 @@ syncRepositoryState = -> # abort if working directory is unclean # apply bundle of unpushed changes from host - {unpushedChanges, head} =atom.guestSession.repositoryDelta + {unpushedChanges, head} = atom.guestSession.repositoryDelta if unpushedChanges tempFile = temp.path(suffix: '.bundle') fs.writeFileSync(tempFile, new Buffer(atom.guestSession.repositoryDelta.unpushedChanges, 'base64')) From 33f538ebf4b2a1c9cc1e429d31cc00fdc8e73d68 Mon Sep 17 00:00:00 2001 From: Corey Johnson & Kevin Sawicki Date: Wed, 10 Jul 2013 15:52:47 -0700 Subject: [PATCH 48/62] Create new branch if guest has unpushed changes --- src/packages/collaboration/lib/bootstrap.coffee | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/packages/collaboration/lib/bootstrap.coffee b/src/packages/collaboration/lib/bootstrap.coffee index d70da3d36..18149143c 100644 --- a/src/packages/collaboration/lib/bootstrap.coffee +++ b/src/packages/collaboration/lib/bootstrap.coffee @@ -53,7 +53,17 @@ syncRepositoryState = -> return else # prompt for new branch name - # create branch at head + + i = 1 + loop + newBranch = "#{branch}-#{i++}" + break unless git.hasBranch(newBranch) + + command = "git checkout -b #{newBranch} #{head}" + exec command, {cwd: repoPath}, (error, stdout, stderr) -> + if error? + console.error error + return else # create branch at head From 8812b6c31ddfab40caccf96a3ec90a1662d32761 Mon Sep 17 00:00:00 2001 From: Corey Johnson & Kevin Sawicki Date: Wed, 10 Jul 2013 16:44:00 -0700 Subject: [PATCH 49/62] Use patrick to mirror repository state --- package.json | 1 + src/app/git.coffee | 36 +++++++---- .../collaboration/lib/bootstrap.coffee | 64 +------------------ .../collaboration/lib/guest-session.coffee | 23 ++++++- .../collaboration/lib/host-session.coffee | 40 ++---------- 5 files changed, 50 insertions(+), 114 deletions(-) diff --git a/package.json b/package.json index c1bcad0d2..2837eb630 100644 --- a/package.json +++ b/package.json @@ -36,6 +36,7 @@ "guid": "0.0.10", "tantamount": "0.3.0", "coffeestack": "0.4.0", + "patrick": "0.1.0", "c-tmbundle": "1.0.0", "coffee-script-tmbundle": "2.0.0", "css-tmbundle": "1.0.0", diff --git a/src/app/git.coffee b/src/app/git.coffee index bffae7be7..7efc6dfc5 100644 --- a/src/app/git.coffee +++ b/src/app/git.coffee @@ -10,6 +10,28 @@ GitUtils = require 'git-utils' # Ultimately, this is an overlay to the native [git-utils](https://github.com/atom/node-git) module. module.exports = class Git + ### Public ### + # Creates a new `Git` instance. + # + # path - The git repository to open + # options - A hash with one key: + # refreshOnWindowFocus: A {Boolean} that identifies if the windows should refresh + # + # Returns a new {Git} object. + @open: (path, options) -> + return null unless path + try + new Git(path, options) + catch e + null + + @exists: (path) -> + if git = @open(path) + git.destroy() + true + else + false + path: null statuses: null upstream: null @@ -57,20 +79,6 @@ class Git ### Public ### - # Creates a new `Git` instance. - # - # path - The git repository to open - # options - A hash with one key: - # refreshOnWindowFocus: A {Boolean} that identifies if the windows should refresh - # - # Returns a new {Git} object. - @open: (path, options) -> - return null unless path - try - new Git(path, options) - catch e - null - # Retrieves the git repository. # # Returns a new `Repository`. diff --git a/src/packages/collaboration/lib/bootstrap.coffee b/src/packages/collaboration/lib/bootstrap.coffee index 18149143c..e6f69d5db 100644 --- a/src/packages/collaboration/lib/bootstrap.coffee +++ b/src/packages/collaboration/lib/bootstrap.coffee @@ -1,15 +1,8 @@ require 'atom' require 'window' -{exec} = require 'child_process' -fs = require 'fs' -remote = require 'remote' -path = require 'path' -url = require 'url' $ = require 'jquery' -temp = require 'temp' {$$} = require 'space-pen' -Git = require 'git' GuestSession = require './guest-session' window.setDimensions(width: 350, height: 100) @@ -22,60 +15,5 @@ loadingView = $$ -> $(window.rootViewParentSelector).append(loadingView) atom.show() -syncRepositoryState = -> - repoUrl = atom.guestSession.repository.get('url') - branch = atom.guestSession.repository.get('branch') - [repoName] = url.parse(repoUrl).path.split('/')[-1..] - repoName = repoName.replace(/\.git$/, '') - repoPath = path.join(remote.require('app').getHomeDir(), 'github', repoName) - git = new Git(repoPath) - - # clone or fetch - # abort if working directory is unclean - - # apply bundle of unpushed changes from host - {unpushedChanges, head} = atom.guestSession.repositoryDelta - if unpushedChanges - tempFile = temp.path(suffix: '.bundle') - fs.writeFileSync(tempFile, new Buffer(atom.guestSession.repositoryDelta.unpushedChanges, 'base64')) - command = "git bundle unbundle #{tempFile}" - exec command, {cwd: repoPath}, (error, stdout, stderr) -> - if error? - console.error error - return - - if git.hasBranch(branch) - if git.getAheadBehindCount(branch).ahead is 0 - command = "git checkout #{branch} && git reset --hard #{head}" - exec command, {cwd: repoPath}, (error, stdout, stderr) -> - if error? - console.error error - return - else - # prompt for new branch name - - i = 1 - loop - newBranch = "#{branch}-#{i++}" - break unless git.hasBranch(newBranch) - - command = "git checkout -b #{newBranch} #{head}" - exec command, {cwd: repoPath}, (error, stdout, stderr) -> - if error? - console.error error - return - else - # create branch at head - - # create branch if it doesn't exist - # prompt for branch name if branch already exists and it cannot be fast-forwarded - # checkout branch - # sync modified and untracked files from host session - - atom.getLoadSettings().initialPath = repoPath - atom.guestSession = new GuestSession(sessionId) -atom.guestSession.on 'started', -> - syncRepositoryState() - loadingView.remove() - window.startEditorWindow() +atom.guestSession.on 'started', -> loadingView.remove() diff --git a/src/packages/collaboration/lib/guest-session.coffee b/src/packages/collaboration/lib/guest-session.coffee index 98c118ca0..9787bf408 100644 --- a/src/packages/collaboration/lib/guest-session.coffee +++ b/src/packages/collaboration/lib/guest-session.coffee @@ -1,5 +1,11 @@ +path = require 'path' +remote = require 'remote' +url = require 'url' + _ = require 'underscore' +patrick = require 'patrick' telepath = require 'telepath' + {connectDocument, createPeer} = require './session-utils' module.exports = @@ -17,17 +23,30 @@ class GuestSession console.log 'connection opened' connection.once 'data', (data) => console.log 'received document', data - @repositoryDelta = data.repositoryDelta doc = telepath.Document.deserialize(data.doc, site: telepath.createSite(@getId())) atom.windowState = doc.get('windowState') + @repository = doc.get('collaborationState.repositoryState') @participants = doc.get('collaborationState.participants') @participants.on 'changed', => @trigger 'participants-changed', @participants.toObject() - @repository = doc.get('collaborationState.repositoryState') connectDocument(doc, connection) + @mirrorRepository(data.repoSnapshot) + mirrorRepository: (repoSnapshot)-> + repoUrl = @repository.get('url') + [repoName] = url.parse(repoUrl).path.split('/')[-1..] + repoName = repoName.replace(/\.git$/, '') + repoPath = path.join(remote.require('app').getHomeDir(), 'github', repoName) + + patrick.mirror repoPath, repoSnapshot, (error) => + if error? + console.error(error) + else @trigger 'started' + atom.getLoadSettings().initialPath = repoPath + window.startEditorWindow() + @participants.push id: @getId() email: git.getConfigValue('user.email') diff --git a/src/packages/collaboration/lib/host-session.coffee b/src/packages/collaboration/lib/host-session.coffee index c947d6bb6..466ac004b 100644 --- a/src/packages/collaboration/lib/host-session.coffee +++ b/src/packages/collaboration/lib/host-session.coffee @@ -1,8 +1,9 @@ fs = require 'fs' + _ = require 'underscore' -async = require 'async' -temp = require 'temp' +patrick = require 'patrick' telepath = require 'telepath' + {createPeer, connectDocument} = require './session-utils' module.exports = @@ -14,44 +15,13 @@ class HostSession peer: null sharing: false - bundleUnpushedChanges: (callback) -> - localBranch = git.getShortHead() - upstreamBranch = git.getRepo().getUpstreamBranch() - - {exec} = require 'child_process' - tempFile = temp.path(suffix: '.bundle') - command = "git bundle create #{tempFile} #{upstreamBranch}..#{localBranch}" - exec command, {cwd: git.getWorkingDirectory()}, (error, stdout, stderr) -> - callback(error, tempFile) - - bundleWorkingDirectoryChanges: -> - - - bundleRepositoryDelta: (callback) -> - repositoryDelta = {} - - operations = [] - if git.upstream.ahead > 0 - operations.push (callback) => - @bundleUnpushedChanges (error, bundleFile) -> - unless error? - repositoryDelta.unpushedChanges = fs.readFileSync(bundleFile, 'base64') - repositoryDelta.head = git.getRepo().getReferenceTarget(git.getRepo().getHead()) - callback(error) - - async.waterfall operations, (error) -> - callback(error, repositoryDelta) - - unless _.isEmpty(git.statuses) - repositoryDelta.workingDirectoryChanges = @bundleWorkingDirectoryChanges() - start: -> return if @peer? @peer = createPeer() @doc = telepath.Document.create({}, site: telepath.createSite(@getId())) @doc.set('windowState', atom.windowState) - @bundleRepositoryDelta (error, repositoryDelta) => + patrick.snapshot project.getPath(), (error, repoSnapshot) => if error? console.error(error) return @@ -72,7 +42,7 @@ class HostSession @peer.on 'connection', (connection) => connection.on 'open', => console.log 'sending document' - connection.send({repositoryDelta, doc: @doc.serialize()}) + connection.send({repoSnapshot, doc: @doc.serialize()}) connectDocument(@doc, connection) connection.on 'close', => From 89dba4603cb70f9989365d2a663dcf3f41aa2e27 Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Fri, 12 Jul 2013 11:48:38 -0700 Subject: [PATCH 50/62] Add progress bar to loading sesion view --- .../collaboration/lib/bootstrap.coffee | 26 +++++++++++++++---- .../collaboration/lib/guest-session.coffee | 7 +++-- .../stylesheets/collaboration.less | 2 ++ 3 files changed, 28 insertions(+), 7 deletions(-) diff --git a/src/packages/collaboration/lib/bootstrap.coffee b/src/packages/collaboration/lib/bootstrap.coffee index e6f69d5db..51fa1be36 100644 --- a/src/packages/collaboration/lib/bootstrap.coffee +++ b/src/packages/collaboration/lib/bootstrap.coffee @@ -5,15 +5,31 @@ $ = require 'jquery' {$$} = require 'space-pen' GuestSession = require './guest-session' -window.setDimensions(width: 350, height: 100) +window.setDimensions(width: 350, height: 125) window.setUpEnvironment('editor') {sessionId} = atom.getLoadSettings() loadingView = $$ -> - @div style: 'margin: 10px; text-align: center', => - @div "Joining session #{sessionId}" + @div style: 'margin: 10px', => + @h4 style: 'text-align: center', 'Joining Session' + @div class: 'progress progress-striped active', style: 'margin-bottom: 10px', => + @div class: 'progress-bar', style: 'width: 0%' + @div class: 'progress-bar-message', 'Establishing connection\u2026' $(window.rootViewParentSelector).append(loadingView) atom.show() -atom.guestSession = new GuestSession(sessionId) -atom.guestSession.on 'started', -> loadingView.remove() +updateProgressBar = (message, percentDone) -> + loadingView.find('.progress-bar-message').text("#{message}\u2026") + loadingView.find('.progress-bar').css('width', "#{percentDone}%") + +guestSession = new GuestSession(sessionId) +guestSession.on 'started', -> loadingView.remove() +guestSession.on 'connection-opened', -> updateProgressBar('Downloading session data', 25) +guestSession.on 'document-received', -> updateProgressBar('Synchronize repository', 50) +operationsDone = -1 +guestSession.on 'mirror-progress', (message, command, operationCount) -> + operationsDone++ + percentDone = Math.round((operationsDone / operationCount) * 50) + 50 + updateProgressBar(message, percentDone) + +atom.guestSession = guestSession diff --git a/src/packages/collaboration/lib/guest-session.coffee b/src/packages/collaboration/lib/guest-session.coffee index 9787bf408..3ed270125 100644 --- a/src/packages/collaboration/lib/guest-session.coffee +++ b/src/packages/collaboration/lib/guest-session.coffee @@ -21,7 +21,9 @@ class GuestSession connection = @peer.connect(sessionId, {reliable: true, connectionId: @getId()}) connection.on 'open', => console.log 'connection opened' + @trigger 'connection-opened' connection.once 'data', (data) => + @trigger 'connection-document-received' console.log 'received document', data doc = telepath.Document.deserialize(data.doc, site: telepath.createSite(@getId())) atom.windowState = doc.get('windowState') @@ -38,7 +40,9 @@ class GuestSession repoName = repoName.replace(/\.git$/, '') repoPath = path.join(remote.require('app').getHomeDir(), 'github', repoName) - patrick.mirror repoPath, repoSnapshot, (error) => + progressCallback = (args...) => @trigger 'mirror-progress', args... + + patrick.mirror repoPath, repoSnapshot, {progressCallback}, (error) => if error? console.error(error) else @@ -46,7 +50,6 @@ class GuestSession atom.getLoadSettings().initialPath = repoPath window.startEditorWindow() - @participants.push id: @getId() email: git.getConfigValue('user.email') diff --git a/src/packages/collaboration/stylesheets/collaboration.less b/src/packages/collaboration/stylesheets/collaboration.less index 4ed3b3037..5eacafe61 100644 --- a/src/packages/collaboration/stylesheets/collaboration.less +++ b/src/packages/collaboration/stylesheets/collaboration.less @@ -10,6 +10,8 @@ .share { .mini-icon(notifications); + position: relative; + right: -1px; margin-bottom: 5px; } From ae9ffbb52639fe876c10861f0b92b84ca1e726b0 Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Fri, 12 Jul 2013 11:49:23 -0700 Subject: [PATCH 51/62] Rename buddy-list.less to collaboration.less --- themes/atom-dark-ui/{buddy-list.less => collaboration.less} | 0 themes/atom-dark-ui/package.cson | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename themes/atom-dark-ui/{buddy-list.less => collaboration.less} (100%) diff --git a/themes/atom-dark-ui/buddy-list.less b/themes/atom-dark-ui/collaboration.less similarity index 100% rename from themes/atom-dark-ui/buddy-list.less rename to themes/atom-dark-ui/collaboration.less diff --git a/themes/atom-dark-ui/package.cson b/themes/atom-dark-ui/package.cson index a59025250..6a1ef2223 100644 --- a/themes/atom-dark-ui/package.cson +++ b/themes/atom-dark-ui/package.cson @@ -10,5 +10,5 @@ 'blurred' 'image-view' 'archive-view' - 'buddy-list' + 'collaboration' ] From 96b91ef36b9d6cba08154a8fb71a1e094417a90b Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Fri, 12 Jul 2013 12:02:06 -0700 Subject: [PATCH 52/62] Add collaboration stylesheet for light theme --- .../stylesheets/collaboration.less | 12 +----------- themes/atom-dark-ui/collaboration.less | 14 ++++++++++++++ themes/atom-light-ui/collaboration.less | 18 ++++++++++++++++++ themes/atom-light-ui/package.cson | 1 + 4 files changed, 34 insertions(+), 11 deletions(-) create mode 100644 themes/atom-light-ui/collaboration.less diff --git a/src/packages/collaboration/stylesheets/collaboration.less b/src/packages/collaboration/stylesheets/collaboration.less index 5eacafe61..9e2af05e0 100644 --- a/src/packages/collaboration/stylesheets/collaboration.less +++ b/src/packages/collaboration/stylesheets/collaboration.less @@ -1,14 +1,13 @@ @import "bootstrap/less/variables.less"; @import "octicon-mixins.less"; -@runningColor: #99CC99; .collaboration { @item-line-height: @line-height-base * 1.25; padding: 10px; -webkit-user-select: none; - cursor: pointer; .share { + cursor: pointer; .mini-icon(notifications); position: relative; right: -1px; @@ -20,15 +19,6 @@ position: relative; right: -2px; margin-bottom: 5px; - color: #96CBFE; - } - - .running { - color: @runningColor; - - &:hover { - color: lighten(@runningColor, 15%); - } } .avatar { diff --git a/themes/atom-dark-ui/collaboration.less b/themes/atom-dark-ui/collaboration.less index c63508993..bb4d01515 100644 --- a/themes/atom-dark-ui/collaboration.less +++ b/themes/atom-dark-ui/collaboration.less @@ -1,3 +1,5 @@ +@runningColor: #99CC99; + .collaboration { background: #1b1c1e; box-shadow: @@ -5,4 +7,16 @@ inset -1px 0 0 rgba(255, 255, 255, 0.02), 1px 0 3px rgba(0, 0, 0, 0.2); color: #cecece; + + .guest { + color: #96CBFE; + } + + .running { + color: @runningColor; + + &:hover { + color: lighten(@runningColor, 15%); + } + } } diff --git a/themes/atom-light-ui/collaboration.less b/themes/atom-light-ui/collaboration.less new file mode 100644 index 000000000..5079c3763 --- /dev/null +++ b/themes/atom-light-ui/collaboration.less @@ -0,0 +1,18 @@ +@runningColor: #f78a46; + +.collaboration { + background: #dde3e8; + border-left: 1px solid #989898; + + .guest { + color: #5293d8; + } + + .running { + color: @runningColor; + + &:hover { + color: darken(@runningColor, 10%); + } + } +} diff --git a/themes/atom-light-ui/package.cson b/themes/atom-light-ui/package.cson index ace6d0f78..9e3f407e3 100644 --- a/themes/atom-light-ui/package.cson +++ b/themes/atom-light-ui/package.cson @@ -11,4 +11,5 @@ 'blurred' 'image-view' 'archive-view' + 'collaboration' ] From f3bb826e8d7af938bc27aabd6685a70fd8bdb58d Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Fri, 12 Jul 2013 12:02:52 -0700 Subject: [PATCH 53/62] Remove unused color from dark collaboration theme --- themes/atom-dark-ui/collaboration.less | 1 - 1 file changed, 1 deletion(-) diff --git a/themes/atom-dark-ui/collaboration.less b/themes/atom-dark-ui/collaboration.less index bb4d01515..927c66643 100644 --- a/themes/atom-dark-ui/collaboration.less +++ b/themes/atom-dark-ui/collaboration.less @@ -6,7 +6,6 @@ 1px 0 0 #131516, inset -1px 0 0 rgba(255, 255, 255, 0.02), 1px 0 3px rgba(0, 0, 0, 0.2); - color: #cecece; .guest { color: #96CBFE; From a9710e7a63959f355ec1ed5a1810c2d3f4bad369 Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Fri, 12 Jul 2013 12:11:57 -0700 Subject: [PATCH 54/62] Ignore session id if empty --- src/packages/collaboration/lib/collaboration.coffee | 1 + src/packages/collaboration/lib/join-prompt-view.coffee | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/packages/collaboration/lib/collaboration.coffee b/src/packages/collaboration/lib/collaboration.coffee index c29115418..d02addd5f 100644 --- a/src/packages/collaboration/lib/collaboration.coffee +++ b/src/packages/collaboration/lib/collaboration.coffee @@ -23,6 +23,7 @@ module.exports = rootView.command 'collaboration:join-session', -> new JoinPromptView (id) -> + return unless id windowSettings = bootstrapScript: require.resolve('collaboration/lib/bootstrap') resourcePath: window.resourcePath diff --git a/src/packages/collaboration/lib/join-prompt-view.coffee b/src/packages/collaboration/lib/join-prompt-view.coffee index 4ed5a22be..f7a3505f1 100644 --- a/src/packages/collaboration/lib/join-prompt-view.coffee +++ b/src/packages/collaboration/lib/join-prompt-view.coffee @@ -30,7 +30,7 @@ class JoinPromptView extends View @miniEditor.setText('') confirm: -> - @confirmed(@miniEditor.getText()) + @confirmed(@miniEditor.getText().trim()) @remove() attach: -> From 9ccf9365c3c8a4dff334fe1ca9d1a9e8003b4c64 Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Fri, 12 Jul 2013 13:26:47 -0700 Subject: [PATCH 55/62] Make all edit session uri's relative This allows them to be collaborated without having absolute paths in the shared document. --- spec/app/project-spec.coffee | 3 ++- src/app/directory.coffee | 8 ++++++-- src/app/edit-session.coffee | 2 +- src/app/project.coffee | 1 + src/app/root-view.coffee | 2 +- src/app/text-buffer.coffee | 5 ++++- src/packages/archive-view/lib/archive-edit-session.coffee | 7 ++++--- src/packages/image-view/lib/image-edit-session.coffee | 7 ++++--- 8 files changed, 23 insertions(+), 12 deletions(-) diff --git a/spec/app/project-spec.coffee b/spec/app/project-spec.coffee index 7ae1e62cc..e73041c37 100644 --- a/spec/app/project-spec.coffee +++ b/spec/app/project-spec.coffee @@ -82,7 +82,8 @@ describe "Project", -> describe "when passed a path that matches a custom opener", -> it "returns the resource returned by the custom opener", -> - expect(project.open("a.foo", hey: "there")).toEqual { foo: "a.foo", options: {hey: "there"} } + pathToOpen = project.resolve('a.foo') + expect(project.open(pathToOpen, hey: "there")).toEqual { foo: pathToOpen, options: {hey: "there"} } expect(project.open("bar://baz")).toEqual { bar: "bar://baz" } describe ".bufferForPath(path)", -> diff --git a/src/app/directory.coffee b/src/app/directory.coffee index 802c814fd..e4bc243a6 100644 --- a/src/app/directory.coffee +++ b/src/app/directory.coffee @@ -49,7 +49,9 @@ class Directory # pathToCheck - the {String} path to check. # # Returns a {Boolean}. - contains: (pathToCheck='') -> + contains: (pathToCheck) -> + return false unless pathToCheck + if pathToCheck.indexOf(path.join(@getPath(), path.sep)) is 0 true else if pathToCheck.indexOf(path.join(@getRealPath(), path.sep)) is 0 @@ -62,7 +64,9 @@ class Directory # fullPath - The {String} path to convert. # # Returns a {String}. - relativize: (fullPath='') -> + relativize: (fullPath) -> + return fullPath unless fullPath + if fullPath is @getPath() '' else if fullPath.indexOf(path.join(@getPath(), path.sep)) is 0 diff --git a/src/app/edit-session.coffee b/src/app/edit-session.coffee index b7bb9e64c..d9ec4d836 100644 --- a/src/app/edit-session.coffee +++ b/src/app/edit-session.coffee @@ -304,7 +304,7 @@ class EditSession # Retrieves the current buffer's URI. # # Returns a {String}. - getUri: -> @getPath() + getUri: -> @buffer.getUri() # {Delegates to: Buffer.isRowBlank} isBufferRowBlank: (bufferRow) -> @buffer.isRowBlank(bufferRow) diff --git a/src/app/project.coffee b/src/app/project.coffee index 08fac445e..9abfd99ef 100644 --- a/src/app/project.coffee +++ b/src/app/project.coffee @@ -162,6 +162,7 @@ class Project # # Returns either an {EditSession} (for text) or {ImageEditSession} (for images). open: (filePath, options={}) -> + filePath = @resolve(filePath) if filePath? for opener in @constructor.openers return resource if resource = opener(filePath, options) diff --git a/src/app/root-view.coffee b/src/app/root-view.coffee index 0af310717..ee54e48d9 100644 --- a/src/app/root-view.coffee +++ b/src/app/root-view.coffee @@ -114,7 +114,7 @@ class RootView extends View # Returns the `EditSession` for the file URI. open: (path, options = {}) -> changeFocus = options.changeFocus ? true - path = project.resolve(path) if path? + path = project.relativize(path) if activePane = @getActivePane() editSession = activePane.itemForUri(path) ? project.open(path) activePane.showItem(editSession) diff --git a/src/app/text-buffer.coffee b/src/app/text-buffer.coffee index baed87b80..e5bf61052 100644 --- a/src/app/text-buffer.coffee +++ b/src/app/text-buffer.coffee @@ -74,7 +74,7 @@ class TextBuffer serialize: -> deserializer: 'TextBuffer' - path: @getPath() + path: @getUri() text: @getText() if @isModified() @deserialize: ({path, text}) -> @@ -133,6 +133,9 @@ class TextBuffer getPath: -> @file?.getPath() + getUri: -> + project?.relativize(@getPath()) ? @getPath() + # Sets the path for the file. # # path - A {String} representing the new file path diff --git a/src/packages/archive-view/lib/archive-edit-session.coffee b/src/packages/archive-view/lib/archive-edit-session.coffee index 94d6e7be9..c3a2e8e10 100644 --- a/src/packages/archive-view/lib/archive-edit-session.coffee +++ b/src/packages/archive-view/lib/archive-edit-session.coffee @@ -14,10 +14,11 @@ class ArchiveEditSession new ArchiveEditSession(filePath) if archive.isPathSupported(filePath) @deserialize: ({path}={}) -> + path = project.resolve(path) if fsUtils.isFileSync(path) new ArchiveEditSession(path) else - console.warn "Could not build edit session for path '#{path}' because that file no longer exists" + console.warn "Could not build archive edit session for path '#{path}' because that file no longer exists" constructor: (@path) -> @file = new File(@path) @@ -27,7 +28,7 @@ class ArchiveEditSession serialize: -> deserializer: 'ArchiveEditSession' - path: @path + path: @getUri() getViewClass: -> require './archive-view' @@ -38,7 +39,7 @@ class ArchiveEditSession else 'untitled' - getUri: -> @path + getUri: -> project?.relativize(@getPath()) ? @getPath() getPath: -> @path diff --git a/src/packages/image-view/lib/image-edit-session.coffee b/src/packages/image-view/lib/image-edit-session.coffee index ac0410350..cc59431f9 100644 --- a/src/packages/image-view/lib/image-edit-session.coffee +++ b/src/packages/image-view/lib/image-edit-session.coffee @@ -18,7 +18,8 @@ class ImageEditSession new ImageEditSession(filePath) @deserialize: ({path}={}) -> - if fsUtils.exists(path) + path = project.resolve(path) + if fsUtils.isFileSync(path) new ImageEditSession(path) else console.warn "Could not build image edit session for path '#{path}' because that file no longer exists" @@ -27,7 +28,7 @@ class ImageEditSession serialize: -> deserializer: 'ImageEditSession' - path: @path + path: @getUri() getViewClass: -> require './image-view' @@ -48,7 +49,7 @@ class ImageEditSession # Retrieves the URI of the current image. # # Returns a {String}. - getUri: -> @path + getUri: -> project?.relativize(@getPath()) ? @getPath() # Retrieves the path of the current image. # From 3d6fb85152846792e959755b145511786024151e Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Fri, 12 Jul 2013 14:15:22 -0700 Subject: [PATCH 56/62] Upgrade to patrick 2.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 2837eb630..f3c1534d3 100644 --- a/package.json +++ b/package.json @@ -36,7 +36,7 @@ "guid": "0.0.10", "tantamount": "0.3.0", "coffeestack": "0.4.0", - "patrick": "0.1.0", + "patrick": "0.2.0", "c-tmbundle": "1.0.0", "coffee-script-tmbundle": "2.0.0", "css-tmbundle": "1.0.0", From 5bb45d468466c960ac0c7c94fa8c4a696c2a4c52 Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Mon, 15 Jul 2013 15:08:47 -0700 Subject: [PATCH 57/62] Add serialization version to image and archive edit sessions --- src/packages/archive-view/lib/archive-edit-session.coffee | 1 + src/packages/image-view/lib/image-edit-session.coffee | 1 + 2 files changed, 2 insertions(+) diff --git a/src/packages/archive-view/lib/archive-edit-session.coffee b/src/packages/archive-view/lib/archive-edit-session.coffee index c3a2e8e10..1c8e4c789 100644 --- a/src/packages/archive-view/lib/archive-edit-session.coffee +++ b/src/packages/archive-view/lib/archive-edit-session.coffee @@ -7,6 +7,7 @@ File = require 'file' module.exports= class ArchiveEditSession registerDeserializer(this) + @version: 1 @activate: -> Project = require 'project' diff --git a/src/packages/image-view/lib/image-edit-session.coffee b/src/packages/image-view/lib/image-edit-session.coffee index cc59431f9..c4f066b6a 100644 --- a/src/packages/image-view/lib/image-edit-session.coffee +++ b/src/packages/image-view/lib/image-edit-session.coffee @@ -8,6 +8,7 @@ _ = require 'underscore' module.exports= class ImageEditSession registerDeserializer(this) + @version: 1 @activate: -> # Files with these extensions will be opened as images From 522768e6c0ad73472c2a10d8bef54c80f8f661a6 Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Mon, 15 Jul 2013 15:33:11 -0700 Subject: [PATCH 58/62] Handle opening session urls --- src/atom-application.coffee | 11 +++++++++++ src/main.coffee | 7 ------- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/src/atom-application.coffee b/src/atom-application.coffee index 520389efb..bfcb0256f 100644 --- a/src/atom-application.coffee +++ b/src/atom-application.coffee @@ -8,6 +8,7 @@ dialog = require 'dialog' fs = require 'fs' path = require 'path' net = require 'net' +url = require 'url' socketPath = '/tmp/atom.sock' @@ -171,6 +172,16 @@ class AtomApplication event.preventDefault() @openPath({pathToOpen}) + app.on 'open-url', (event, urlToOpen) => + event.preventDefault() + + parsedUrl = url.parse(urlToOpen) + if parsedUrl.host is 'session' + sessionId = parsedUrl.path.split('/')[1] + if sessionId + bootstrapScript = 'collaboration/lib/bootstrap' + new AtomWindow({bootstrapScript, @resourcePath, sessionId}) + autoUpdater.on 'ready-for-update-on-quit', (event, version, quitAndUpdate) => event.preventDefault() @installUpdate = quitAndUpdate diff --git a/src/main.coffee b/src/main.coffee index 69f1c8115..9d69b94b9 100644 --- a/src/main.coffee +++ b/src/main.coffee @@ -22,13 +22,6 @@ delegate.browserMainParts.preMainMessageLoopRun = -> event.preventDefault() args.pathsToOpen.push(filePath) - app.on 'open-url', (event, url) => - event.preventDefault() - dialog.showMessageBox - message: 'Atom opened with URL' - detail: url - buttons: ['OK'] - app.on 'open-file', addPathToOpen app.on 'will-finish-launching', -> From 438b8f6a14cda5d2519168e4b57c77acc8a1a6d4 Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Mon, 15 Jul 2013 15:42:51 -0700 Subject: [PATCH 59/62] Support launching the app directly with a URL In this case there will be no paths to open and so editor windows should be created. This will allow sessions to be joined when Atom isn't currently running but a session link is clicked from within another application. --- src/atom-application.coffee | 20 ++++++++++++-------- src/main.coffee | 15 +++++++++++++-- 2 files changed, 25 insertions(+), 10 deletions(-) diff --git a/src/atom-application.coffee b/src/atom-application.coffee index bfcb0256f..7bb956ab6 100644 --- a/src/atom-application.coffee +++ b/src/atom-application.coffee @@ -39,7 +39,7 @@ class AtomApplication installUpdate: null version: null - constructor: ({@resourcePath, pathsToOpen, @version, test, pidToKillWhenClosed, @dev, newWindow}) -> + constructor: ({@resourcePath, pathsToOpen, urlsToOpen, @version, test, pidToKillWhenClosed, @dev, newWindow}) -> global.atomApplication = this @pidsToOpenWindows = {} @@ -57,6 +57,8 @@ class AtomApplication @runSpecs({exitWhenDone: true, @resourcePath}) else if pathsToOpen.length > 0 @openPaths({pathsToOpen, pidToKillWhenClosed, newWindow}) + else if urlsToOpen.length > 0 + @openUrl(urlToOpen) for urlToOpen in urlsToOpen else # Always open a editor window if this is the first instance of Atom. @openPath({pidToKillWhenClosed, newWindow}) @@ -174,13 +176,7 @@ class AtomApplication app.on 'open-url', (event, urlToOpen) => event.preventDefault() - - parsedUrl = url.parse(urlToOpen) - if parsedUrl.host is 'session' - sessionId = parsedUrl.path.split('/')[1] - if sessionId - bootstrapScript = 'collaboration/lib/bootstrap' - new AtomWindow({bootstrapScript, @resourcePath, sessionId}) + @openUrl(urlToOpen) autoUpdater.on 'ready-for-update-on-quit', (event, version, quitAndUpdate) => event.preventDefault() @@ -251,6 +247,14 @@ class AtomApplication console.log("Killing process #{pid} failed: #{error.code}") delete @pidsToOpenWindows[pid] + openUrl: (urlToOpen) -> + parsedUrl = url.parse(urlToOpen) + if parsedUrl.host is 'session' + sessionId = parsedUrl.path.split('/')[1] + if sessionId + bootstrapScript = 'collaboration/lib/bootstrap' + new AtomWindow({bootstrapScript, @resourcePath, sessionId}) + openConfig: -> if @configWindow @configWindow.focus() diff --git a/src/main.coffee b/src/main.coffee index 9d69b94b9..5eaa3422f 100644 --- a/src/main.coffee +++ b/src/main.coffee @@ -18,11 +18,21 @@ require 'coffee-script' delegate.browserMainParts.preMainMessageLoopRun = -> args = parseCommandLine() - addPathToOpen = (event, filePath) -> + addPathToOpen = (event, pathToOpen) -> event.preventDefault() - args.pathsToOpen.push(filePath) + args.pathsToOpen.push(pathToOpen) + + args.urlsToOpen = [] + addUrlToOpen = (event, urlToOpen) -> + event.preventDefault() + args.urlsToOpen.push(urlToOpen) + + app.on 'open-url', (event, urlToOpen) -> + event.preventDefault() + args.urlsToOpen.push(urlToOpen) app.on 'open-file', addPathToOpen + app.on 'open-url', addUrlToOpen app.on 'will-finish-launching', -> setupCrashReporter() @@ -30,6 +40,7 @@ delegate.browserMainParts.preMainMessageLoopRun = -> app.on 'finish-launching', -> app.removeListener 'open-file', addPathToOpen + app.removeListener 'open-url', addUrlToOpen args.pathsToOpen = args.pathsToOpen.map (pathToOpen) -> path.resolve(args.executedFrom ? process.cwd(), pathToOpen) From c8accea5dc502c8636a4f3c395b5715b250c770a Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Mon, 15 Jul 2013 16:02:59 -0700 Subject: [PATCH 60/62] Synchronize instead of Synchronize --- src/packages/collaboration/lib/bootstrap.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/packages/collaboration/lib/bootstrap.coffee b/src/packages/collaboration/lib/bootstrap.coffee index 51fa1be36..ff2bfa259 100644 --- a/src/packages/collaboration/lib/bootstrap.coffee +++ b/src/packages/collaboration/lib/bootstrap.coffee @@ -25,7 +25,7 @@ updateProgressBar = (message, percentDone) -> guestSession = new GuestSession(sessionId) guestSession.on 'started', -> loadingView.remove() guestSession.on 'connection-opened', -> updateProgressBar('Downloading session data', 25) -guestSession.on 'document-received', -> updateProgressBar('Synchronize repository', 50) +guestSession.on 'document-received', -> updateProgressBar('Synchronizing repository', 50) operationsDone = -1 guestSession.on 'mirror-progress', (message, command, operationCount) -> operationsDone++ From 0a4e3cec94839b147be14d56272c903fbc03cf4f Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Mon, 15 Jul 2013 16:09:08 -0700 Subject: [PATCH 61/62] Add missing connection prefix to event name --- src/packages/collaboration/lib/bootstrap.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/packages/collaboration/lib/bootstrap.coffee b/src/packages/collaboration/lib/bootstrap.coffee index ff2bfa259..02d9f7cf8 100644 --- a/src/packages/collaboration/lib/bootstrap.coffee +++ b/src/packages/collaboration/lib/bootstrap.coffee @@ -25,7 +25,7 @@ updateProgressBar = (message, percentDone) -> guestSession = new GuestSession(sessionId) guestSession.on 'started', -> loadingView.remove() guestSession.on 'connection-opened', -> updateProgressBar('Downloading session data', 25) -guestSession.on 'document-received', -> updateProgressBar('Synchronizing repository', 50) +guestSession.on 'connection-document-received', -> updateProgressBar('Synchronizing repository', 50) operationsDone = -1 guestSession.on 'mirror-progress', (message, command, operationCount) -> operationsDone++ From 6482e2652609e6774ee0066f35e43ded94350d45 Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Mon, 15 Jul 2013 16:10:22 -0700 Subject: [PATCH 62/62] Copy session url to clipboard instead of just id --- .../collaboration/lib/collaboration.coffee | 10 ++++++---- .../collaboration/lib/join-prompt-view.coffee | 7 ++++--- .../collaboration/lib/session-utils.coffee | 19 +++++++++++++++++++ 3 files changed, 29 insertions(+), 7 deletions(-) diff --git a/src/packages/collaboration/lib/collaboration.coffee b/src/packages/collaboration/lib/collaboration.coffee index d02addd5f..dd1a5f7e6 100644 --- a/src/packages/collaboration/lib/collaboration.coffee +++ b/src/packages/collaboration/lib/collaboration.coffee @@ -2,6 +2,7 @@ GuestView = require './guest-view' HostView = require './host-view' HostSession = require './host-session' JoinPromptView = require './join-prompt-view' +{getSessionUrl} = require './session-utils' module.exports = activate: -> @@ -12,14 +13,15 @@ module.exports = else hostSession = new HostSession() - rootView.command 'collaboration:copy-session-id', -> + copySession = -> sessionId = hostSession.getId() - pasteboard.write(sessionId) if sessionId + pasteboard.write(getSessionUrl(sessionId)) if sessionId + + rootView.command 'collaboration:copy-session-id', copySession rootView.command 'collaboration:start-session', -> hostView ?= new HostView(hostSession) - if sessionId = hostSession.start() - pasteboard.write(sessionId) + copySession() if hostSession.start() rootView.command 'collaboration:join-session', -> new JoinPromptView (id) -> diff --git a/src/packages/collaboration/lib/join-prompt-view.coffee b/src/packages/collaboration/lib/join-prompt-view.coffee index f7a3505f1..0621bfe37 100644 --- a/src/packages/collaboration/lib/join-prompt-view.coffee +++ b/src/packages/collaboration/lib/join-prompt-view.coffee @@ -3,7 +3,8 @@ Editor = require 'editor' $ = require 'jquery' Point = require 'point' _ = require 'underscore' -Guid = require 'guid' + +{getSessionId} = require './session-utils' module.exports = class JoinPromptView extends View @@ -20,8 +21,8 @@ class JoinPromptView extends View @on 'core:cancel', => @remove() clipboard = pasteboard.read()[0] - if Guid.isGuid(clipboard) - @miniEditor.setText(clipboard) + if sessionId = getSessionId(clipboard) + @miniEditor.setText(sessionId) @attach() diff --git a/src/packages/collaboration/lib/session-utils.coffee b/src/packages/collaboration/lib/session-utils.coffee index f93f188ee..af5d45e2f 100644 --- a/src/packages/collaboration/lib/session-utils.coffee +++ b/src/packages/collaboration/lib/session-utils.coffee @@ -1,7 +1,26 @@ Peer = require '../vendor/peer.js' Guid = require 'guid' +url = require 'url' + module.exports = + getSessionId: (text) -> + return null unless text + + text = text.trim() + sessionUrl = url.parse(text) + if sessionUrl.host is 'session' + sessionId = sessionUrl.path.split('/')[1] + else + sessionId = text + + if Guid.isGuid(sessionId) + sessionId + else + null + + getSessionUrl: (sessionId) -> "atom://session/#{sessionId}" + createPeer: -> id = Guid.create().toString() new Peer(id, key: '0njqmaln320dlsor')