├── Procfile ├── public ├── css │ ├── baobab-light.less │ ├── baobab-dark.less │ ├── dropzone.css │ ├── mixins.less │ ├── angular-animation.css │ └── baobab.less ├── img │ ├── favicon.png │ ├── tab_drafts@2x.png │ ├── tab_inbox@2x.png │ ├── tab_logout@2x.png │ ├── tab_sent@2x.png │ ├── tab_theme@2x.png │ └── tab_archive@2x.png ├── partials │ ├── tag.html │ ├── compose-zen.html │ ├── message.html │ ├── compose.html │ ├── thread.html │ └── thread_list.html ├── sound │ ├── scrollUp.mp3 │ ├── backspace.mp3 │ ├── key-new-01.mp3 │ ├── key-new-02.mp3 │ ├── key-new-03.mp3 │ ├── key-new-04.mp3 │ ├── key-new-05.mp3 │ ├── return-new.mp3 │ ├── scrollDown.mp3 │ └── space-new.mp3 ├── fonts │ ├── glyphicons-halflings-regular.eot │ ├── glyphicons-halflings-regular.ttf │ └── glyphicons-halflings-regular.woff ├── js │ ├── baobab.service.tags.coffee │ ├── baobab.service.contacts.coffee │ ├── error.coffee │ ├── baobab.directive.inParticipantBubble.coffee │ ├── baobab.directive.scribe.coffee │ ├── baobab.service.namespace.coffee │ ├── baobab.directive.inBindIframeContents.coffee │ ├── baobab.directive.inParticipants.coffee │ ├── minievents.js │ ├── baobab.service.scrollstate.coffee │ ├── baobab.directive.autofocus.coffee │ ├── baobab.directive.dropzone.coffee │ ├── baobab.controller.app.coffee │ ├── baobab.service.auth.coffee │ ├── baobab.directive.typewriter.coffee │ ├── main.coffee │ ├── infinite-scroll.js │ ├── baobab.directive.hotkeys.coffee │ ├── baobab.service.threads.coffee │ ├── app.js │ ├── baobab.filter.coffee │ ├── baobab.controller.compose.coffee │ ├── baobab.controller.thread.coffee │ ├── baobab.controller.threadList.coffee │ ├── baobab.directive.autocomplete.coffee │ ├── FileSaver.js │ └── bootstrap-tokenfield.js ├── set-app-id.html ├── scratch │ └── attachments.html └── index.html ├── arclib ├── __phutil_library_init__.php └── __phutil_library_map__.php ├── .gitmodules ├── screenshots ├── screenshot_reply.png ├── screenshot_thread.png ├── screenshot_compose.png ├── screenshot_threads.png └── screenshot_dark_theme.png ├── .arcconfig ├── .gitignore ├── .jshintrc ├── test ├── filter │ ├── tag_expand-spec.coffee │ ├── pretty_size-spec.coffee │ ├── shorten-spec.coffee │ ├── not_me-spec.coffee │ └── participants-spec.coffee ├── test-main.coffee └── controller │ ├── thread-spec.coffee │ └── compose-spec.coffee ├── .jscsrc ├── server.js ├── bower.json ├── LICENSE.md ├── package.json ├── karma.conf.js ├── coffeelint.json ├── gulpfile.js ├── CONTRIBUTING.md └── README.md /Procfile: -------------------------------------------------------------------------------- 1 | web: node server.js -------------------------------------------------------------------------------- /public/css/baobab-light.less: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /arclib/__phutil_library_init__.php: -------------------------------------------------------------------------------- 1 | {{tag.email}}{{tag.name}} -------------------------------------------------------------------------------- /public/sound/scrollUp.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nylas/inbox-scaffold-html5/HEAD/public/sound/scrollUp.mp3 -------------------------------------------------------------------------------- /public/img/tab_drafts@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nylas/inbox-scaffold-html5/HEAD/public/img/tab_drafts@2x.png -------------------------------------------------------------------------------- /public/img/tab_inbox@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nylas/inbox-scaffold-html5/HEAD/public/img/tab_inbox@2x.png -------------------------------------------------------------------------------- /public/img/tab_logout@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nylas/inbox-scaffold-html5/HEAD/public/img/tab_logout@2x.png -------------------------------------------------------------------------------- /public/img/tab_sent@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nylas/inbox-scaffold-html5/HEAD/public/img/tab_sent@2x.png -------------------------------------------------------------------------------- /public/img/tab_theme@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nylas/inbox-scaffold-html5/HEAD/public/img/tab_theme@2x.png -------------------------------------------------------------------------------- /public/sound/backspace.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nylas/inbox-scaffold-html5/HEAD/public/sound/backspace.mp3 -------------------------------------------------------------------------------- /public/sound/key-new-01.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nylas/inbox-scaffold-html5/HEAD/public/sound/key-new-01.mp3 -------------------------------------------------------------------------------- /public/sound/key-new-02.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nylas/inbox-scaffold-html5/HEAD/public/sound/key-new-02.mp3 -------------------------------------------------------------------------------- /public/sound/key-new-03.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nylas/inbox-scaffold-html5/HEAD/public/sound/key-new-03.mp3 -------------------------------------------------------------------------------- /public/sound/key-new-04.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nylas/inbox-scaffold-html5/HEAD/public/sound/key-new-04.mp3 -------------------------------------------------------------------------------- /public/sound/key-new-05.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nylas/inbox-scaffold-html5/HEAD/public/sound/key-new-05.mp3 -------------------------------------------------------------------------------- /public/sound/return-new.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nylas/inbox-scaffold-html5/HEAD/public/sound/return-new.mp3 -------------------------------------------------------------------------------- /public/sound/scrollDown.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nylas/inbox-scaffold-html5/HEAD/public/sound/scrollDown.mp3 -------------------------------------------------------------------------------- /public/sound/space-new.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nylas/inbox-scaffold-html5/HEAD/public/sound/space-new.mp3 -------------------------------------------------------------------------------- /public/img/tab_archive@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nylas/inbox-scaffold-html5/HEAD/public/img/tab_archive@2x.png -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "vendor/inbox.js"] 2 | path = vendor/inbox.js 3 | url = https://github.com/inboxapp/inbox.js.git 4 | -------------------------------------------------------------------------------- /public/partials/compose-zen.html: -------------------------------------------------------------------------------- 1 | 3 | -------------------------------------------------------------------------------- /screenshots/screenshot_reply.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nylas/inbox-scaffold-html5/HEAD/screenshots/screenshot_reply.png -------------------------------------------------------------------------------- /screenshots/screenshot_thread.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nylas/inbox-scaffold-html5/HEAD/screenshots/screenshot_thread.png -------------------------------------------------------------------------------- /screenshots/screenshot_compose.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nylas/inbox-scaffold-html5/HEAD/screenshots/screenshot_compose.png -------------------------------------------------------------------------------- /screenshots/screenshot_threads.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nylas/inbox-scaffold-html5/HEAD/screenshots/screenshot_threads.png -------------------------------------------------------------------------------- /screenshots/screenshot_dark_theme.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nylas/inbox-scaffold-html5/HEAD/screenshots/screenshot_dark_theme.png -------------------------------------------------------------------------------- /.arcconfig: -------------------------------------------------------------------------------- 1 | { 2 | "project_id" : "inbox", 3 | "conduit_uri" : "https://review.inboxapp.com/", 4 | "load" : [ 5 | "arclib" 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /public/fonts/glyphicons-halflings-regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nylas/inbox-scaffold-html5/HEAD/public/fonts/glyphicons-halflings-regular.eot -------------------------------------------------------------------------------- /public/fonts/glyphicons-halflings-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nylas/inbox-scaffold-html5/HEAD/public/fonts/glyphicons-halflings-regular.ttf -------------------------------------------------------------------------------- /public/fonts/glyphicons-halflings-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nylas/inbox-scaffold-html5/HEAD/public/fonts/glyphicons-halflings-regular.woff -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | *.DS_Store 3 | *.swp 4 | *.swo 5 | /node_modules/ 6 | public/js/angular-inbox.js 7 | npm-debug.log 8 | bower.json 9 | bower_components 10 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "indent": 2, 3 | "node": true, 4 | "undef": true, 5 | "expr": true, 6 | "browser": true, 7 | "eqnull": true, 8 | "strict": false, 9 | "sub": true, 10 | "globals": { 11 | }, 12 | "predef": ["define", "$", "alert"] 13 | } 14 | -------------------------------------------------------------------------------- /public/js/baobab.service.tags.coffee: -------------------------------------------------------------------------------- 1 | 2 | define ["angular", "error"], (angular, error) -> 3 | angular.module('baobab.service.tags', []) 4 | .service('$tags', ['$namespaces', ($namespaces) -> 5 | 6 | @_list = [] 7 | @list = () => @_list 8 | 9 | $namespaces.current().tags().then (tags) => 10 | @_list = tags 11 | , error._handleAPIError 12 | 13 | @ 14 | ]) 15 | -------------------------------------------------------------------------------- /public/js/baobab.service.contacts.coffee: -------------------------------------------------------------------------------- 1 | 2 | define ["angular", "error"], (angular, error) -> 3 | angular.module('baobab.service.contacts', []) 4 | .service('$contacts', ['$namespaces', ($namespaces) -> 5 | 6 | @_list = [] 7 | @list = () => @_list 8 | 9 | $namespaces.current().contacts().then (contacts) => 10 | @_list = contacts 11 | , error._handleAPIError 12 | 13 | @ 14 | ]) 15 | -------------------------------------------------------------------------------- /public/js/error.coffee: -------------------------------------------------------------------------------- 1 | define [], () -> 2 | _displayErrors = true 3 | window.onbeforeunload = () -> 4 | _displayErrors = false 5 | return 6 | 7 | @_handleAPIError = (error) -> 8 | if (!_displayErrors) 9 | return 10 | msg = "An unexpected error occurred. (HTTP code " + error['status'] + "). Please try again." 11 | if error['message'] 12 | msg = error['message'] 13 | alert(msg) 14 | -------------------------------------------------------------------------------- /test/filter/tag_expand-spec.coffee: -------------------------------------------------------------------------------- 1 | define ['angular', 'angularMocks', 'baobab.filter'], (angular) -> 2 | describe 'tag_expand filter', -> 3 | tag_expand = null 4 | 5 | beforeEach -> 6 | angular.mock.module("baobab.filter") 7 | angular.mock.inject ($filter) -> 8 | tag_expand = $filter("tag_expand") 9 | 10 | it 'should expand tags', -> 11 | tags = [ 12 | { name: "first" }, 13 | { name: "second" }, 14 | ] 15 | 16 | expect(tag_expand(tags)).toEqual("first second ") 17 | -------------------------------------------------------------------------------- /test/filter/pretty_size-spec.coffee: -------------------------------------------------------------------------------- 1 | define ['angular', 'angularMocks', 'baobab.filter'], (angular) -> 2 | describe 'pretty_size filter', -> 3 | pretty_size = null 4 | 5 | beforeEach -> 6 | angular.mock.module("baobab.filter") 7 | angular.mock.inject ($filter) -> pretty_size = $filter("pretty_size") 8 | 9 | it 'should format appropriately', -> 10 | expect(pretty_size(999)).toEqual("999 B") 11 | expect(pretty_size(1000)).toEqual("1 KB") 12 | expect(pretty_size(1000000)).toEqual("1 MB") 13 | expect(pretty_size(1001)).toEqual("1 KB") 14 | -------------------------------------------------------------------------------- /.jscsrc: -------------------------------------------------------------------------------- 1 | { 2 | "preset": "google", 3 | "maximumLineLength": { 4 | "value": 100, 5 | "allowComments": true, 6 | "allowRegex": true 7 | }, 8 | "disallowSpacesInsideObjectBrackets": null, 9 | "disallowSpacesInsideArrayBrackets": null, 10 | "disallowMultipleLineBreaks": null, 11 | "requireCurlyBraces": [ 12 | "else", 13 | "for", 14 | "while", 15 | "do", 16 | "try", 17 | "catch" 18 | ], 19 | "requireSpaceAfterKeywords": [ 20 | "if", 21 | "else", 22 | "for", 23 | "while", 24 | "do", 25 | "switch", 26 | "return", 27 | "try" 28 | ], 29 | "validateJSDoc": null 30 | } 31 | -------------------------------------------------------------------------------- /test/filter/shorten-spec.coffee: -------------------------------------------------------------------------------- 1 | define ['angular', 'angularMocks', 'baobab.filter'], (angular) -> 2 | describe 'Shorten filter', -> 3 | shorten = null 4 | maxlen = 64 5 | 6 | beforeEach -> 7 | angular.mock.module('baobab.filter') 8 | angular.mock.inject ($filter) -> 9 | shorten = $filter("shorten") 10 | 11 | it 'should not touch short strings', -> 12 | str = ([1..maxlen].map () -> "a").join("") 13 | expect(str.length).toBe(maxlen) 14 | expect(shorten(str)).toBe(str) 15 | 16 | it 'should truncate long strings', -> 17 | str = ([1..(maxlen+1)].map () -> "a").join("") 18 | expect(str.length).toBe(maxlen + 1) 19 | expect(shorten(str).length).toBe(maxlen) 20 | -------------------------------------------------------------------------------- /arclib/__phutil_library_map__.php: -------------------------------------------------------------------------------- 1 | 2, 11 | 'class' => 12 | array( 13 | 'InboxServerLintEngine' => 'src/InboxServerLintEngine.php', 14 | 'MGArcanistPyLintLinter' => 'src/MGArcanistPyLintLinter.php', 15 | 'PytestTestEngine' => 'src/PytestTestEngine.php', 16 | ), 17 | 'function' => 18 | array( 19 | ), 20 | 'xmap' => 21 | array( 22 | 'InboxServerLintEngine' => 'ArcanistLintEngine', 23 | 'MGArcanistPyLintLinter' => 'ArcanistLinter', 24 | 'PytestTestEngine' => 'ArcanistBaseUnitTestEngine', 25 | ), 26 | )); 27 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | var coffeescript = require('coffee-script'); 2 | var coffeeMiddleware = require('coffee-middleware'); 3 | var express = require("express"); 4 | var expressLess = require('express-less'); 5 | var fs = require('fs'); 6 | var logfmt = require("logfmt"); 7 | var request = require('request'); 8 | 9 | var app = express(); 10 | 11 | app.use(logfmt.requestLogger()); 12 | app.use('/', express.static(__dirname + '/public')); 13 | app.use('/components', express.static(__dirname + '/bower_components')); 14 | app.use('/css', expressLess(__dirname + '/public/css')); 15 | app.use(coffeeMiddleware({ 16 | src: __dirname + '/public' 17 | })); 18 | 19 | var port = Number(process.env.PORT || 6001); 20 | app.listen(port, function() { 21 | console.log("Listening on " + port); 22 | }); 23 | -------------------------------------------------------------------------------- /test/filter/not_me-spec.coffee: -------------------------------------------------------------------------------- 1 | define ['angular', 'angularMocks', 'baobab.filter'], (angular) -> 2 | not_me = null 3 | me = 4 | email: "me@example.com" 5 | 6 | describe 'not_me filter', -> 7 | beforeEach -> 8 | angular.mock.module 'baobab.filter', ($provide) -> 9 | $provide.value '$namespaces', 10 | current: -> 11 | emailAddress: me.email 12 | return 13 | angular.mock.inject ($filter) -> 14 | not_me = $filter("not_me") 15 | expect(not_me).not.toBeNull() 16 | 17 | it "should remove elements from a list if me.email matches", -> 18 | result = not_me([me]) 19 | expect(result.length).toBe(0) 20 | 21 | it "should not break on other items", -> 22 | expect(not_me(["beep"]).length).toBe(1) 23 | -------------------------------------------------------------------------------- /public/js/baobab.directive.inParticipantBubble.coffee: -------------------------------------------------------------------------------- 1 | 2 | define ["angular", "blueimp-md5", "jQuery"], (angular, md5) -> 3 | angular.module('baobab.directive.inParticipantBubble', []) 4 | 5 | .directive 'inParticipantBubble', () -> 6 | restrict: "E" 7 | template: '
' 8 | link: (scope, element, attrs, ctrl) -> 9 | email = attrs['email'].toLowerCase().trim() 10 | url = "http://www.gravatar.com/avatar/" + md5(email)+ "?d=blank" 11 | hue = 0 12 | for ii in [0..email.length-1] by 1 13 | hue += email.charCodeAt(ii) 14 | 15 | el = $(element).find('.participant-bubble') 16 | el.css('background-color', 'hsl('+hue+',70%,60%)') 17 | el.css('background-image', 'url('+url+')') 18 | el.css('background-size','cover') 19 | -------------------------------------------------------------------------------- /public/js/baobab.directive.scribe.coffee: -------------------------------------------------------------------------------- 1 | define ['angular', 'scribe'], (angular, Scribe) -> 2 | angular.module 'baobab.directive.scribe', [] 3 | .directive 'scribe', () -> 4 | require: 'ngModel' 5 | link: (scope, elem, attr, model) -> 6 | window.Scribe = Scribe 7 | scribe = new Scribe(elem[0]) 8 | 9 | safeApply = (fn) -> 10 | if scope.$$phase || scope.$root.$$phase 11 | fn() 12 | else 13 | scope.$apply fn 14 | 15 | model.$render = () -> 16 | scribe.setContent (model.$viewValue || "") 17 | 18 | model.$isEmpty = (value) -> 19 | !value || scribe.allowsBlockElements() && value == '


' 20 | 21 | scribe.on "content-changed", () -> 22 | value = scribe.getContent() 23 | safeApply () -> model.$setViewValue(value) -------------------------------------------------------------------------------- /public/js/baobab.service.namespace.coffee: -------------------------------------------------------------------------------- 1 | 2 | define ["angular"], (angular, events) -> 3 | angular.module('baobab.service.namespace', []) 4 | .service('$namespaces', ['$inbox', '$auth', ($inbox, $auth) -> 5 | @_namespaces = [] 6 | 7 | @current = () => @_namespaces[0] 8 | 9 | if $auth.token || !$auth.needToken() 10 | @promise = $inbox.namespaces().then (namespaces) => 11 | @_namespaces = namespaces 12 | , 13 | (err) -> 14 | if (window.confirm("/n/ returned no namespaces. Click OK to be\ 15 | logged out, or Cancel if you think this is a temporary issue.")) 16 | $auth.clearToken() 17 | else 18 | @promise = Promise.reject("No auth token") 19 | 20 | @ 21 | ]) 22 | 23 | .factory('$namespaces-promise', ['$namespaces', ($n) -> $n.promise ]) 24 | 25 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "baobab", 3 | "version": "0.0.1", 4 | "description": "Inbox Internal Web Client", 5 | "main": "public/js/app.js", 6 | "private": true, 7 | "ignore": [ 8 | "**/.*", 9 | "node_modules", 10 | "bower_components", 11 | "test", 12 | "tests" 13 | ], 14 | "dependencies": { 15 | "angular": "~1.4.0", 16 | "angular-cookies": "~1.4.0", 17 | "angular-sanitize": "~1.4.0", 18 | "angular-route": "~1.4.0", 19 | "bootstrap": "~3.2.0", 20 | "jquery": "<2.0.0", 21 | "angular-strap": "~2.0.5", 22 | "font-awesome": "~4.2.0", 23 | "underscore": "~1.7.0", 24 | "moment": "~2.8.2", 25 | "requirejs": "~2.1.15", 26 | "dropzone": "~3.10.2", 27 | "blueimp-md5": "1.1.0" 28 | }, 29 | "devDependencies": { 30 | "angular-mocks": "~1.4.0", 31 | "scribe": "~0.1.17" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /public/js/baobab.directive.inBindIframeContents.coffee: -------------------------------------------------------------------------------- 1 | 2 | define ["angular", "jQuery"], (angular) -> 3 | angular.module('baobab.directive.inBindIframeContents', []) 4 | 5 | .directive 'inBindIframeContents', () -> 6 | require: 'ngModel' 7 | link: (scope, element, attrs, ngModel) -> 8 | # Specify how UI should be updated 9 | ngModel.$render = () -> 10 | doc = element[0].contentWindow.document 11 | element[0].onload = () -> 12 | height = doc.body.scrollHeight + 'px' 13 | doc.body.className += ' ' + 'heightDetermined' 14 | $(element).height(height) 15 | scope.$emit('inIframeLoaded') 16 | 17 | style = $('#iframe-css').html() 18 | doc.open() 19 | doc.write(style) 20 | doc.write("") 21 | doc.write(ngModel.$viewValue) 22 | doc.close() 23 | -------------------------------------------------------------------------------- /public/js/baobab.directive.inParticipants.coffee: -------------------------------------------------------------------------------- 1 | 2 | define ["angular"], (angular) -> 3 | angular.module('baobab.directive.inParticipants', []) 4 | 5 | .directive 'inParticipants', () -> 6 | format = (value) -> 7 | if (value && Object.prototype.toString.call(value) == '[object Array]') 8 | str = '' 9 | for i in [0..value.length-1] by 1 10 | p = value[i] 11 | if p && typeof p == 'object' && p.email 12 | str += ', ' if str.length 13 | str += p.email 14 | str 15 | 16 | 17 | parse = (value) -> 18 | if typeof value == 'string' 19 | values = value.split(/\s*,\s*/) 20 | out = [] 21 | for value in values 22 | out.push 23 | name: '' 24 | email: value[i] 25 | out 26 | 27 | 28 | return { 29 | require: 'ngModel' 30 | link: (scope, element, attrs, ngModel) -> 31 | ngModel.$formatters.push(format) 32 | ngModel.$parsers.push(parse) 33 | } -------------------------------------------------------------------------------- /public/js/minievents.js: -------------------------------------------------------------------------------- 1 | define("Events", [], function () { 2 | function Events(target){ 3 | var events = {}; 4 | target = target || this 5 | /** 6 | * On: listen to events 7 | */ 8 | target.on = function(type, func, ctx){ 9 | events[type] || (events[type] = []) 10 | events[type].push({f:func, c:ctx}) 11 | } 12 | /** 13 | * Off: stop listening to event / specific callback 14 | */ 15 | target.off = function(type, func){ 16 | var list = events[type] || [], 17 | i = list.length = func ? list.length : 0 18 | while(i-->0) func == list[i].f && list.splice(i,1) 19 | } 20 | /** 21 | * Emit: send event, callbacks will be triggered 22 | */ 23 | target.emit = function(){ 24 | var args = Array.apply([], arguments), 25 | list = events[args.shift()] || [], 26 | i = list.length, j 27 | for(j=0;j 3 | angular.module('baobab.service.scrollstate', []) 4 | .service('$scrollState', ['$rootScope', ($rootScope) -> 5 | 6 | @_scrollID = null 7 | 8 | @scrollTo = (id) => 9 | @_scrollID = id 10 | @runScroll() 11 | 12 | @runScroll = () => 13 | return unless @_scrollID 14 | offset = $('#'+@_scrollID).offset() 15 | return if offset == undefined 16 | 17 | $('body').scrollTop(offset.top) 18 | 19 | # update our scroll offset when components of the view load: angular views, 20 | # angular partials, and our own (async) iFrames for messages. 21 | $rootScope.$on('$viewContentLoaded', @runScroll) 22 | $rootScope.$on('$includeContentLoaded', @runScroll) 23 | $rootScope.$on('inIframeLoaded', @runScroll) 24 | 25 | # reset the scroll offset when we visit a new route 26 | $rootScope.$on '$routeChangeStart', () => 27 | @_scrollID = null 28 | $('body').scrollTop(0) 29 | 30 | @ 31 | ]) 32 | -------------------------------------------------------------------------------- /public/js/baobab.directive.autofocus.coffee: -------------------------------------------------------------------------------- 1 | define(["angular"], (angular) -> 2 | 3 | angular.module('baobab.directive.autofocus', []) 4 | 5 | .directive('autofocus', ['$timeout', ($timeout) -> 6 | (scope, elem, attr) -> 7 | findTargetWithin = (elem) -> 8 | focusable = $(elem).find("input, textarea") 9 | targets = focusable.filter (i, input) -> 10 | model = angular.element(input).attr("ng-model") 11 | return _.isEmpty(scope.$eval(model)) 12 | 13 | if _.isEmpty(targets) 14 | focusable.last() 15 | else 16 | targets.first() 17 | 18 | performFocus = (target) -> 19 | animatingParent = target.closest('.ng-animate') 20 | if animatingParent.length 21 | animatingParent.on('transitionend webkitTransitionEnd', () -> 22 | $timeout(_.bind(target.focus, target),1) 23 | ); 24 | else 25 | $timeout(_.bind(target.focus, target),1) 26 | 27 | scope.$on(attr.autofocus, (e) -> 28 | $timeout(() -> 29 | target = findTargetWithin(elem) 30 | performFocus(target) 31 | ) 32 | ) 33 | ]) 34 | ) 35 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | ---- 3 | 4 | Copyright (c) 2014 InboxApp, Inc. and Contributors 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in 14 | all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /public/partials/message.html: -------------------------------------------------------------------------------- 1 |
2 | {{msg.date | timestamp_ago}} 3 | DRAFT 4 | {{msg.from | participants:'long'}} 5 | to 6 | {{msg.to | participants:'long'}} 7 | cc 8 | {{msg.cc | participants:'long'}} 9 | bcc 10 | {{msg.bcc | participants:'long'}} 11 |
12 | 13 | 14 | 15 |
16 | {{aID}} 17 |
{{data.filename | extension}}
18 |
{{data.filename}}
19 |
20 |
21 |
22 | -------------------------------------------------------------------------------- /public/js/baobab.directive.dropzone.coffee: -------------------------------------------------------------------------------- 1 | define ['angular', 'dropzone', 'underscore'], (angular, Dropzone, _) -> 2 | Dropzone.autoDiscover = false 3 | 4 | DropzoneException = (message) -> @message = message 5 | DropzoneException.prototype = Error 6 | ensureDefined = (value, msg) -> 7 | throw new DropzoneException msg if value == undefined 8 | 9 | Dropzone.prototype.putFile = (file) -> 10 | file.upload = 11 | progress: 100 12 | total: file.size 13 | bytesSent: file.size 14 | file.status = Dropzone.SUCCESS 15 | @files.push file 16 | @emit "addedfile", file 17 | 18 | angular.module 'baobab.directive.dropzone', [] 19 | .directive 'dropzone', () -> 20 | link: (scope, elem, attr) -> 21 | ensureDefined attr.dropzone, 'No Dropzone config' 22 | 23 | scope.$watch attr.dropzone, (config) -> 24 | return if !config 25 | 26 | ensureDefined config.options, 'No dropzone options' 27 | ensureDefined config.eventHandlers, 'No dropzone handlers' 28 | 29 | elem.addClass('dropzone') 30 | dropzone = new Dropzone(elem[0], config.options) 31 | 32 | angular.forEach config.eventHandlers, (handler, event) -> 33 | dropzone.on event, handler 34 | 35 | if config.dropzoneReady 36 | config.dropzoneReady(dropzone) 37 | -------------------------------------------------------------------------------- /public/js/baobab.controller.app.coffee: -------------------------------------------------------------------------------- 1 | define ['angular'], (angular) -> 2 | angular.module("baobab.controller.app", []) 3 | .controller('AppCtrl', [ 4 | '$scope', 5 | '$namespaces', 6 | '$inbox', 7 | '$auth', 8 | '$location', 9 | '$cookieStore', 10 | '$sce', 11 | ($scope, $namespaces, $inbox, $auth, $location, $cookieStore, $sce) -> 12 | window.AppCtrl = @ 13 | 14 | @inboxAuthURL = $sce.trustAsResourceUrl('https://api.nylas.com/oauth/authorize') 15 | @inboxClientID = $inbox.appId() 16 | @inboxRedirectURL = window.location.href.split('/#')[0].replace('index.html', '') 17 | @loginHint = '' 18 | 19 | @clearToken = $auth.clearToken 20 | @token = () => $auth.token 21 | @needToken = () => $auth.needToken() 22 | 23 | @namespace = () => $namespaces.current() 24 | 25 | @theme = $cookieStore.get('baobab_theme') || 'light' 26 | @setTheme = (theme) => 27 | @theme = theme 28 | $cookieStore.put('baobab_theme', theme) 29 | 30 | 31 | @toggleTheme = () => 32 | @setTheme({light: 'dark', dark: 'light'}[@theme]) 33 | 34 | 35 | @cssForTab = (path) => 36 | if $location.path().indexOf(path) != -1 37 | 'active' 38 | else 39 | '' 40 | 41 | @ 42 | ]) 43 | -------------------------------------------------------------------------------- /public/js/baobab.service.auth.coffee: -------------------------------------------------------------------------------- 1 | 2 | require ["angular"], (angular) -> 3 | angular.module('baobab.service.auth', []) 4 | .service('$auth', ['$cookieStore', '$location', '$inbox', ($cookieStore, $location, $inbox) -> 5 | 6 | @clearToken = () => 7 | $cookieStore.remove('inbox_auth_token') 8 | window.location = '/' 9 | 10 | @needToken = () => 11 | $inbox.appId() != "localhost" 12 | 13 | @readTokenFromCookie = () => 14 | try 15 | @token = $cookieStore.get('inbox_auth_token') 16 | catch e 17 | @clearToken() 18 | !!@token 19 | 20 | 21 | @readTokenFromURL = () => 22 | search = window.location.search 23 | tokenStart = search.indexOf('access_token=') 24 | return if tokenStart == -1 25 | 26 | tokenStart += ('access_token=').length 27 | 28 | tokenEnd = search.indexOf('&', tokenStart) 29 | tokenEnd = search.length - tokenStart if tokenEnd == -1 30 | 31 | token = search.substr(tokenStart, tokenEnd-tokenStart) 32 | $cookieStore.put('inbox_auth_token', token) 33 | window.location.href = window.location.href.split('?')[0] 34 | 35 | 36 | @readTokenFromURL() unless @readTokenFromCookie() 37 | 38 | if @token 39 | $inbox.withCredentials(true) 40 | $inbox.setRequestHeader('Authorization', 'Basic '+btoa(@token+':')) 41 | 42 | @ 43 | ]) 44 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "inbox-client-scaffold-web", 3 | "version": "0.0.1", 4 | "description": "Inbox Internal Web Client", 5 | "main": "server.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "postinstall": "echo postinstall time; ./node_modules/bower/bin/bower install --config.interactive=false; cd ./vendor/inbox.js; npm install; ./node_modules/gulp/bin/gulp.js build; mv ./build/angular-inbox.js ./../../public/js/angular-inbox.js; cd ./../../;" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "https://github.com/inboxapp/inbox-client-scaffold-web.git" 13 | }, 14 | "author": "InboxApp, Inc.", 15 | "license": "MIT", 16 | "bugs": { 17 | "url": "https://github.com/inboxapp/inbox-client-scaffold-web/issues" 18 | }, 19 | "engines": { 20 | "node": "0.10.x" 21 | }, 22 | "dependencies": { 23 | "bower": "^1.3.9", 24 | "coffee-middleware": "0.3.0", 25 | "coffee-script": "^1.8.0", 26 | "express": "^4.6.1", 27 | "express-less": "0.0.7", 28 | "logfmt": "^1.1.2", 29 | "request": "^2.40.0" 30 | }, 31 | "devDependencies": { 32 | "gulp": "^3.7.0", 33 | "gulp-coffee": "^2.2.0", 34 | "gulp-less": "^3.0.3", 35 | "gulp-concat": "^2.2.0", 36 | "gulp-jscs": "^1.1.0", 37 | "gulp-jshint": "^1.6.2", 38 | "gulp-connect": "^2.0.5", 39 | "gulp-coffeelint": "0.5.0", 40 | "karma": "~0.13.3", 41 | "karma-jasmine": "^0.3.6", 42 | "jshint-stylish": "^2.0.1", 43 | "jasmine": "~2.3.1", 44 | "karma-chrome-launcher": "~0.2.0", 45 | "karma-coffee-preprocessor": "~0.3.0", 46 | "mock-promises": "0.4.1", 47 | "requirejs": "~2.1.15", 48 | "karma-requirejs": "~0.2.2" 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /public/set-app-id.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Inbox Client 6 | 7 | 8 | 9 | 10 | 11 | 12 | 25 |
26 | 36 |
37 | 38 | 39 | -------------------------------------------------------------------------------- /public/js/baobab.directive.typewriter.coffee: -------------------------------------------------------------------------------- 1 | 2 | define ["angular", "jQuery"], (angular, jquery) -> 3 | angular.module('baobab.directive.typewriter', []) 4 | .directive('typewriter', ['$timeout', '$namespaces', ($timeout, $namespaces) -> 5 | (scope, elem, attr) -> 6 | ii = 0 7 | if $('#sounds').length == 0 8 | $('body').append($('
\ 9 | \ 10 | \ 11 | \ 12 | \ 13 | \ 14 | \ 15 | \ 16 |
')) 17 | 18 | $('body').on 'keydown', (e) -> 19 | sound = null 20 | if e.keyCode == 32 21 | sound = document.getElementById('space-new') 22 | else if e.keyCode == 13 23 | sound = document.getElementById('return-new') 24 | else if e.keyCode > 32 25 | sound = document.getElementById('key-new-0'+ii) 26 | ii = (ii + 1) % 5 27 | 28 | if sound 29 | sound.currentTime = 0 30 | sound.play() 31 | ]) 32 | -------------------------------------------------------------------------------- /public/partials/compose.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | cc/bcc 5 |
6 |
7 | 8 |
9 |
10 | 15 |
16 |
Loading...
17 | 18 |
{{message}}
19 |
20 | -------------------------------------------------------------------------------- /public/js/main.coffee: -------------------------------------------------------------------------------- 1 | require.config 2 | paths: 3 | "scribe": "../components/scribe/scribe" 4 | "angular": "../components/angular/angular" 5 | "angularRoute": "../components/angular-route/angular-route" 6 | "angularSanitize": "../components/angular-sanitize/angular-sanitize" 7 | "angularCookies": "../components/angular-cookies/angular-cookies" 8 | "angularAnimate": "../components/angular-animate/angular-animate" 9 | "angularStrap": [ 10 | "../components/angular-strap/dist/angular-strap", 11 | "../components/angular-strap/dist/angular-strap.tpl" 12 | ] 13 | "angularMocks": "../components/angular-mocks/angular-mocks" 14 | "underscore": "../components/underscore/underscore" 15 | "angularInfiniteScroll": "infinite-scroll" 16 | "jQuery": "../components/jquery/dist/jquery" 17 | "Events": "minievents" 18 | "moment": "../components/moment/min/moment.min" 19 | "bootstrap": "../components/bootstrap/dist/js/bootstrap.min" 20 | "scribe": "../components/scribe/scribe" 21 | "dropzone": "../components/dropzone/downloads/dropzone-amd-module.min" 22 | "blueimp-md5": "../components/blueimp-md5/js/md5.min" 23 | 24 | shim: 25 | "angular": 26 | exports: "angular" 27 | deps: ["jQuery"] 28 | "angularRoute": ["angular"] 29 | "angularSanitize": ["angular"] 30 | "angularCookies": ["angular"] 31 | "angularAnimate": ["angular"] 32 | "angularStrap": ["angular", "bootstrap"] 33 | "angularInfiniteScroll": ["angular", "angularStrap"] 34 | "angularMocks": ["angular"] 35 | "bootstrap": ["jQuery"] 36 | 37 | # https://code.angularjs.org/1.2.1/docs/guide/bootstrap#overview_deferred-bootstrap 38 | window.name = "NG_DEFER_BOOTSTRAP!" 39 | 40 | require ['angular', 'app'], (angular, app) -> 41 | angular.element(document.querySelector('html')[0]).ready () -> 42 | angular.resumeBootstrap([app.name]) 43 | -------------------------------------------------------------------------------- /test/test-main.coffee: -------------------------------------------------------------------------------- 1 | allTestFiles = [] 2 | TEST_REGEXP = /spec\.js$/ 3 | 4 | pathToModule = (path) -> 5 | path.replace(/^\/base\//, '../../').replace(/\.js$/, '') 6 | 7 | Object.keys(window.__karma__.files).forEach (file) -> 8 | if TEST_REGEXP.test file 9 | #Normalize paths to RequireJS module names. 10 | allTestFiles.push pathToModule file 11 | 12 | require.config 13 | baseUrl: '/base/public/js' 14 | 15 | paths: 16 | "scribe": "/base/bower_components/scribe/scribe" 17 | "angular": "/base/bower_components/angular/angular" 18 | "angularRoute": "/base/bower_components/angular-route/angular-route" 19 | "angularSanitize": "/base/bower_components/angular-sanitize/angular-sanitize" 20 | "angularCookies": "/base/bower_components/angular-cookies/angular-cookies" 21 | "angularAnimate": "/base/bower_components/angular-animate/angular-animate" 22 | "angularStrap": [ 23 | "/base/bower_components/angular-strap/dist/angular-strap", 24 | "/base/bower_components/angular-strap/dist/angular-strap.tpl" 25 | ] 26 | "angularMocks": "/base/bower_components/angular-mocks/angular-mocks" 27 | "underscore": "/base/bower_components/underscore/underscore" 28 | "angularInfiniteScroll": "infinite-scroll" 29 | "jQuery": "/base/bower_components/jquery/dist/jquery" 30 | "Events": "minievents" 31 | "moment": "/base/bower_components/moment/min/moment.min" 32 | "bootstrap": "/base/bower_components/bootstrap/dist/js/bootstrap.min" 33 | 34 | shim: 35 | "angular": 36 | exports: "angular" 37 | deps: ["jQuery"] 38 | "angularRoute": ["angular"] 39 | "angularSanitize": ["angular"] 40 | "angularCookies": ["angular"] 41 | "angularAnimate": ["angular"] 42 | "angularStrap": ["angular", "bootstrap"] 43 | "angularInfiniteScroll": ["angular", "angularStrap"] 44 | "angularMocks": ["angular"] 45 | "bootstrap": ["jQuery"] 46 | 47 | deps: allTestFiles 48 | 49 | callback: window.__karma__.start 50 | -------------------------------------------------------------------------------- /public/js/infinite-scroll.js: -------------------------------------------------------------------------------- 1 | /* ng-infinite-scroll - v1.0.0 - 2013-02-23 */ 2 | var mod; 3 | 4 | mod = angular.module('infinite-scroll', []); 5 | 6 | mod.directive('infiniteScroll', [ 7 | '$rootScope', '$window', '$timeout', function($rootScope, $window, $timeout) { 8 | return { 9 | link: function(scope, elem, attrs) { 10 | var checkWhenEnabled, handler, scrollDistance, scrollEnabled; 11 | $window = angular.element($window); 12 | scrollDistance = 0; 13 | if (attrs.infiniteScrollDistance != null) { 14 | scope.$watch(attrs.infiniteScrollDistance, function(value) { 15 | return scrollDistance = parseInt(value, 10); 16 | }); 17 | } 18 | scrollEnabled = true; 19 | checkWhenEnabled = false; 20 | if (attrs.infiniteScrollDisabled != null) { 21 | scope.$watch(attrs.infiniteScrollDisabled, function(value) { 22 | scrollEnabled = !value; 23 | if (scrollEnabled && checkWhenEnabled) { 24 | checkWhenEnabled = false; 25 | return handler(); 26 | } 27 | }); 28 | } 29 | handler = function() { 30 | var elementBottom, remaining, shouldScroll, windowBottom; 31 | windowBottom = $window.height() + $window.scrollTop(); 32 | elementBottom = elem.offset().top + elem.height(); 33 | remaining = elementBottom - windowBottom; 34 | shouldScroll = remaining <= $window.height() * scrollDistance; 35 | if (shouldScroll && scrollEnabled) { 36 | if ($rootScope.$$phase) { 37 | return scope.$eval(attrs.infiniteScroll); 38 | } else { 39 | return scope.$apply(attrs.infiniteScroll); 40 | } 41 | } else if (shouldScroll) { 42 | return checkWhenEnabled = true; 43 | } 44 | }; 45 | $window.on('scroll', handler); 46 | scope.$on('$destroy', function() { 47 | return $window.off('scroll', handler); 48 | }); 49 | return $timeout((function() { 50 | if (attrs.infiniteScrollImmediateCheck) { 51 | if (scope.$eval(attrs.infiniteScrollImmediateCheck)) { 52 | return handler(); 53 | } 54 | } else { 55 | return handler(); 56 | } 57 | }), 0); 58 | } 59 | }; 60 | } 61 | ]); 62 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration 2 | // Generated on Mon Sep 08 2014 15:09:32 GMT-0700 (PDT) 3 | 4 | module.exports = function(config) { 5 | config.set({ 6 | 7 | // base path that will be used to resolve all patterns (eg. files, exclude) 8 | basePath: '', 9 | 10 | 11 | // frameworks to use 12 | // available frameworks: https://npmjs.org/browse/keyword/karma-adapter 13 | frameworks: ['jasmine', 'requirejs'], 14 | 15 | 16 | // list of files / patterns to load in the browser 17 | files: [ 18 | 'node_modules/mock-promises/lib/mock-promises.js', 19 | { pattern: 'bower_components/**/*.js', included: false }, 20 | { pattern: 'public/js/*.js', included: false }, 21 | { pattern: 'public/js/*.coffee', included: false }, 22 | { pattern: 'test/**/*spec.coffee', included: false }, 23 | 'test/test-main.coffee', 24 | ], 25 | 26 | 27 | // list of files to exclude 28 | exclude: [ 29 | 'public/js/main.js', 30 | ], 31 | 32 | 33 | // preprocess matching files before serving them to the browser 34 | // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor 35 | preprocessors: { 36 | 'public/js/*.coffee': ['coffee'], 37 | 'test/**/*.coffee': ['coffee'], 38 | }, 39 | 40 | coffeePreprocessor: { 41 | // options passed to the coffee compiler 42 | options: { 43 | bare: true, 44 | sourceMap: false 45 | }, 46 | // transforming the filenames 47 | transformPath: function(path) { 48 | return path.replace(/\.coffee$/, '.js'); 49 | } 50 | }, 51 | 52 | // test results reporter to use 53 | // possible values: 'dots', 'progress' 54 | // available reporters: https://npmjs.org/browse/keyword/karma-reporter 55 | reporters: ['progress'], 56 | 57 | 58 | // web server port 59 | port: 9876, 60 | 61 | 62 | // enable / disable colors in the output (reporters and logs) 63 | colors: true, 64 | 65 | 66 | // level of logging 67 | // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG 68 | logLevel: config.LOG_INFO, 69 | 70 | 71 | // enable / disable watching file and executing tests whenever any file changes 72 | autoWatch: true, 73 | 74 | 75 | // start these browsers 76 | // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher 77 | browsers: ['Chrome'], 78 | 79 | 80 | // Continuous Integration mode 81 | // if true, Karma captures browsers, runs the tests and exits 82 | singleRun: false 83 | }); 84 | }; 85 | -------------------------------------------------------------------------------- /public/scratch/attachments.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Attachment CSS check 5 | 6 | 7 | 8 | 9 | 10 |
11 | 22 |
23 |
24 |
25 |
26 |
27 |
28 | Bw-RDCZCEAARAZM.jpg 29 |
30 |
31 | 240 kb 32 |
33 | 34 |
35 |
36 | 37 |
38 |
39 |
40 |
41 | Remove file 42 |
43 |
44 |
45 |
46 | Bw-RDCZCEAARAZM.jpg 47 |
48 |
49 | 240 kb 50 |
51 | 52 |
53 |
54 | 55 |
56 |
57 |
58 |
59 | Remove file 60 |
61 |
62 | 63 | 64 | -------------------------------------------------------------------------------- /test/filter/participants-spec.coffee: -------------------------------------------------------------------------------- 1 | define ['angular', 'angularMocks', 'baobab.filter'], (angular) -> 2 | describe 'participants filter', -> 3 | participants = null 4 | 5 | mockParticipant1 = 6 | name: "Slartibartfast" 7 | email: "me@example.com" 8 | 9 | mockParticipant2 = 10 | name: "Ford Prefect" 11 | email: "ford@example.com" 12 | 13 | mockParticipant3 = 14 | email: "zaphod@example.com" 15 | 16 | mockParticipant4 = 17 | email: "arthur@earth.com" 18 | 19 | mockParticipant5 = 20 | email: "marvin@example.com" 21 | name: "Marvin (The Paranoid Android)" 22 | 23 | mockParticipant6 = 24 | name: "" 25 | email: "arthur@earth.com" 26 | 27 | beforeEach -> 28 | angular.module("baobab.test", ["baobab.filter"]) 29 | .service "$namespaces", () -> 30 | current: () -> 31 | emailAddress: "me@example.com" 32 | 33 | angular.mock.module("baobab.test") 34 | angular.mock.inject ($filter) -> 35 | participants = $filter("participants") 36 | 37 | it 'should show user\'s own name as \'Me\'', -> 38 | expect(participants [ mockParticipant1 ]).toEqual("Me") 39 | 40 | it 'should show only the other user in 2-person email', -> 41 | expect(participants [ mockParticipant1, mockParticipant2 ]).toEqual(mockParticipant2.name) 42 | expect(participants [ mockParticipant2, mockParticipant1 ]).toEqual(mockParticipant2.name) 43 | expect(participants [ mockParticipant2, mockParticipant2 ]).toEqual( 44 | mockParticipant2.name + ', ' + mockParticipant2.name 45 | ) 46 | 47 | it 'should fall back to the email address if there is no name', -> 48 | expect(participants [ mockParticipant4 ]).toEqual mockParticipant4.email 49 | expect(participants [ mockParticipant6 ]).toEqual mockParticipant6.email 50 | 51 | it 'should fall back to email and truncate if in the same domain as user', -> 52 | expect(participants [ mockParticipant3 ]).toEqual (mockParticipant3.email.split("@")[0]) 53 | 54 | it 'should show the domain for auto-responders', -> 55 | expect(participants [ { email: 'support@fake.com' }]).toEqual "fake.com" 56 | expect(participants [ { email: 'no-reply@fake.com'}]).toEqual "fake.com" 57 | expect(participants [ { email: 'info@fake.com' }]).toEqual "fake.com" 58 | 59 | it 'should trim parens', -> 60 | expect(participants [ mockParticipant5 ]).toEqual "The Paranoid Android" 61 | 62 | it 'should allow the "short" preset to be disabled', -> 63 | expect(participants [ mockParticipant5 ], "no").toEqual mockParticipant5.name 64 | expect(participants [ mockParticipant3 ], "no").toEqual mockParticipant3.email 65 | -------------------------------------------------------------------------------- /coffeelint.json: -------------------------------------------------------------------------------- 1 | { 2 | "arrow_spacing": { 3 | "level": "ignore" 4 | }, 5 | "camel_case_classes": { 6 | "level": "error" 7 | }, 8 | "coffeescript_error": { 9 | "level": "error" 10 | }, 11 | "colon_assignment_spacing": { 12 | "level": "ignore", 13 | "spacing": { 14 | "left": 0, 15 | "right": 0 16 | } 17 | }, 18 | "cyclomatic_complexity": { 19 | "value": 10, 20 | "level": "ignore" 21 | }, 22 | "duplicate_key": { 23 | "level": "error" 24 | }, 25 | "empty_constructor_needs_parens": { 26 | "level": "ignore" 27 | }, 28 | "indentation": { 29 | "value": 2, 30 | "level": "error" 31 | }, 32 | "line_endings": { 33 | "level": "ignore", 34 | "value": "unix" 35 | }, 36 | "max_line_length": { 37 | "value": 120, 38 | "level": "error", 39 | "limitComments": true 40 | }, 41 | "missing_fat_arrows": { 42 | "level": "ignore" 43 | }, 44 | "newlines_after_classes": { 45 | "value": 3, 46 | "level": "ignore" 47 | }, 48 | "no_backticks": { 49 | "level": "error" 50 | }, 51 | "no_debugger": { 52 | "level": "warn" 53 | }, 54 | "no_empty_functions": { 55 | "level": "ignore" 56 | }, 57 | "no_empty_param_list": { 58 | "level": "ignore" 59 | }, 60 | "no_implicit_braces": { 61 | "level": "ignore", 62 | "strict": true 63 | }, 64 | "no_implicit_parens": { 65 | "strict": true, 66 | "level": "ignore" 67 | }, 68 | "no_interpolation_in_single_quotes": { 69 | "level": "ignore" 70 | }, 71 | "no_plusplus": { 72 | "level": "ignore" 73 | }, 74 | "no_stand_alone_at": { 75 | "level": "ignore" 76 | }, 77 | "no_tabs": { 78 | "level": "error" 79 | }, 80 | "no_throwing_strings": { 81 | "level": "error" 82 | }, 83 | "no_trailing_semicolons": { 84 | "level": "error" 85 | }, 86 | "no_trailing_whitespace": { 87 | "level": "error", 88 | "allowed_in_comments": false, 89 | "allowed_in_empty_lines": true 90 | }, 91 | "no_unnecessary_double_quotes": { 92 | "level": "ignore" 93 | }, 94 | "no_unnecessary_fat_arrows": { 95 | "level": "ignore" 96 | }, 97 | "non_empty_constructor_needs_parens": { 98 | "level": "ignore" 99 | }, 100 | "prefer_english_operator": { 101 | "level": "ignore", 102 | "doubleNotLevel": "ignore" 103 | }, 104 | "space_operators": { 105 | "level": "ignore" 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /public/js/baobab.directive.hotkeys.coffee: -------------------------------------------------------------------------------- 1 | define ["angular"], (angular) -> 2 | 3 | angular.module('baobab.directive.hotkeys', []) 4 | 5 | .directive('bindKeys', ["$rootScope", ($rootScope) -> 6 | restrict: 'A', 7 | link: (scope, element, attrs) -> 8 | meta = false 9 | 10 | isMeta = (event) -> event.ctrlKey 11 | 12 | element.bind('keyup', (event) -> 13 | return true if (isMeta(event) || !meta) 14 | meta = false 15 | $rootScope.$broadcast("meta-up") 16 | true 17 | ) 18 | 19 | element.bind('keydown', (event) -> 20 | return true if !isMeta(event) || meta 21 | meta = true 22 | $rootScope.$broadcast "meta-down" 23 | true 24 | ) 25 | 26 | element.bind('keypress', (event) -> 27 | return true if !isMeta(event) 28 | action = $rootScope.keybindings[event.which] 29 | if action 30 | event.preventDefault() 31 | $rootScope.$broadcast("meta-used") 32 | action() 33 | return false 34 | true 35 | ) 36 | ]) 37 | 38 | .directive('hotkey', ["$rootScope", "$parse", "$location", 39 | ($rootScope, $parse, $location) -> 40 | restrict: 'A', 41 | link: (scope, element, attrs) -> 42 | $rootScope.keybindings = $rootScope.keybindings || {} 43 | action = undefined 44 | if attrs['ngClick'] 45 | action = _.bind($parse(attrs['ngClick']), this, scope, {}) 46 | else if element.is("a") && !!attrs["href"] 47 | action = -> 48 | $location.path(attrs["href"].replace(/^#/, "")) 49 | scope.$apply() 50 | console.log(attrs["href"]) 51 | else 52 | return 53 | 54 | mapping = 55 | "enter": 13 56 | key = mapping[attrs['hotkey']] || attrs['hotkey'].charCodeAt(0) 57 | 58 | $rootScope.keybindings[key & ~(64 | 32)] = action 59 | 60 | timeout = undefined 61 | hint = $("
") 62 | 63 | showHint = -> 64 | el = element 65 | hint 66 | .css("top", el.position().top) 67 | .css("left", el.position().left) 68 | .css("position", "absolute") 69 | .width(el.outerWidth()) 70 | .height(el.outerHeight()) 71 | .css("line-height", el.outerHeight()+"px") 72 | .text(attrs['hotkey']) 73 | hint.insertAfter(element) 74 | element.css('opacity', 0.2) 75 | 76 | hideHint = -> 77 | element.css('opacity', '') 78 | hint.remove() 79 | 80 | clearHint = -> 81 | if timeout 82 | window.clearTimeout(timeout) 83 | hideHint() 84 | 85 | scope.$on "meta-down", -> 86 | timeout = window.setTimeout(showHint, 500) 87 | 88 | scope.$on("meta-up", clearHint) 89 | scope.$on("meta-used", clearHint) 90 | 91 | scope.$on "$destroy", -> 92 | if ($rootScope.keybindings[key & ~(64 | 32)] == action) 93 | $rootScope.keybindings[key & ~(64 | 32)] = undefined 94 | ]) 95 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | var gulp = require('gulp'); 2 | var concat = require('gulp-concat'); 3 | var jscs = require('gulp-jscs'); 4 | var jshint = require('gulp-jshint'); 5 | var coffeelint = require('gulp-coffeelint'); 6 | var coffee = require('gulp-coffee'); 7 | var less = require('gulp-less'); 8 | 9 | var paths = { 10 | javascript: [ 11 | 'public/js/**/*.js', 12 | 13 | '!public/js/angular-inbox.js', 14 | '!public/js/FileSaver.js', 15 | '!public/js/infinite-scroll.js', 16 | '!public/js/bootstrap-tokenfield.js', 17 | '!public/js/minievents.js', 18 | ], 19 | coffeescript: [ 20 | 'public/js/**/*.coffee' 21 | ], 22 | tests: [ 23 | 'tests/**/*.js' 24 | ], 25 | less: [ 26 | 'public/css/*.less' 27 | ], 28 | assets: [ 29 | 'public/*.html', 30 | 'public/partials/*.html', 31 | 'public/sound/*', 32 | 'public/img/*', 33 | 'public/fonts/*', 34 | 'public/css/*.css', 35 | 'public/js/angular-inbox.js', 36 | 'public/js/FileSaver.js', 37 | 'public/js/infinite-scroll.js', 38 | 'public/js/bootstrap-tokenfield.js', 39 | 'public/js/minievents.js' 40 | ], 41 | components: [ 42 | 'bower_components/**' 43 | ] 44 | } 45 | 46 | gulp.task('default', ['style', 'lint']); 47 | gulp.task('style', ['jscs']); 48 | gulp.task('lint', ['lint-js', 'lint-coffee']); 49 | 50 | gulp.task('test', function (done) { 51 | var karma = require('karma').server; 52 | karma.start({ 53 | configFile: __dirname + '/karma.conf.js', 54 | singleRun: true 55 | }, done); 56 | }); 57 | 58 | gulp.task('jscs', function() { 59 | var src = gulp.src(paths.javascript). 60 | pipe(jscs()); 61 | var test = gulp.src(paths.tests). 62 | pipe(jscs()); 63 | return merge(src, test); 64 | }); 65 | 66 | gulp.task('lint-js', function() { 67 | return gulp.src(paths.javascript). 68 | pipe(jshint()). 69 | pipe(jshint.reporter('jshint-stylish')). 70 | pipe(jshint.reporter('fail')); 71 | }); 72 | 73 | gulp.task('lint-coffee', function() { 74 | return gulp.src(paths.coffeescript). 75 | pipe(coffeelint()). 76 | pipe(coffeelint.reporter()); 77 | }); 78 | 79 | // For compiling a minimal version of the app that can be deployed 80 | // without any server whatsoever to gh-pages or S3: 81 | 82 | gulp.task('compile-less', function() { 83 | return gulp.src(paths.less) 84 | .pipe(less({ 85 | paths: ['public/css'] 86 | })) 87 | .pipe(gulp.dest('gh-pages/css')); 88 | }); 89 | 90 | gulp.task('compile-coffee', function() { 91 | return gulp.src(paths.coffeescript) 92 | .pipe(coffee()) 93 | .pipe(gulp.dest('gh-pages/js')); 94 | }); 95 | 96 | gulp.task('compile-js', function() { 97 | return gulp.src(paths.javascript) 98 | .pipe(gulp.dest('gh-pages/js')); 99 | }); 100 | 101 | gulp.task('copy-assets', function() { 102 | gulp.src(paths.assets, {base: "public"}) 103 | .pipe(gulp.dest('gh-pages')); 104 | gulp.src(paths.components, {base: "bower_components"}) 105 | .pipe(gulp.dest('gh-pages/components')); 106 | }); 107 | 108 | gulp.task('gh-pages', ['compile-less', 'compile-coffee', 'compile-js', 'copy-assets']); 109 | 110 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Inbox Client 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 | 21 | 22 | 23 | 37 | 38 |
39 |
40 | 46 | 47 | 53 |
54 | 55 |
56 | 57 |
Token: {{App.token()}}
Namespace: {{App.namespace().id}}
58 |
59 |
60 | 61 | 62 | -------------------------------------------------------------------------------- /public/partials/thread.html: -------------------------------------------------------------------------------- 1 | 2 |
3 | Reply 4 | Reply All 5 | Archive 6 |
7 | 8 | 40 | 41 |
42 | 43 |
{{ThreadCtrl.thread ? (ThreadCtrl.thread.subject || "No Subject") : " "}}
44 | 45 |
46 | 47 |
48 | 49 | 50 |
51 | 52 |
53 | 54 | 55 |
56 |

57 | This thread does not contain any messages. 58 |

59 |
60 | 61 | 62 |
63 |
64 |
65 | 66 | 67 | 78 | 79 | 80 |
81 |
82 |
83 |
84 | 85 |
86 | -------------------------------------------------------------------------------- /public/partials/thread_list.html: -------------------------------------------------------------------------------- 1 | 30 | 31 | 61 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | #Contributing to Inbox.js 2 | 3 | - [Get the code](#get-the-code) 4 | - [Install dependencies](#install-dependencies) 5 | - [Working with the code](#working-with-the-code) 6 | - [Commit Message Guidelines](#commit-message-guidelines) 7 | 8 | ## Get the code 9 | 10 | ```bash 11 | git clone https://github.com/inboxapp/inbox.js.git 12 | 13 | cd inbox.js 14 | ``` 15 | 16 | ## Install dependencies 17 | 18 | ```bash 19 | # Install devDependencies from NPM 20 | npm install 21 | 22 | # For testing, install an appropriate [karma](http://karma-runner.github.io/) 23 | # browser launcher 24 | npm install karma-chrome-launcher 25 | ``` 26 | 27 | ## Working with the code 28 | 29 | ```bash 30 | # Perform style checks to ensure that the code meets style guidelines 31 | gulp lint 32 | 33 | # Build the frameworks 34 | gulp build 35 | ``` 36 | 37 | ## Running unit tests 38 | 39 | ```bash 40 | # Run unit tests 41 | gulp test 42 | 43 | # Run unit tests with custom browsers 44 | gulp test --browsers 45 | ``` 46 | 47 | ##Commit message guidelines 48 | 49 | Inbox.js is using a commit message scheme based on that used by [angular.js](https://github.com/angular/angular.js) and [conventional-changelog](https://www.npmjs.org/package/conventional-changelog), in order to simplify generation of meaningful changelogs, simplify bisecting to find and fix regressions, and to clarify what a given change has done. 50 | 51 | All contributions should fit this format, and all contributions should be squashed as appropriate. 52 | 53 | ### Commit Message Format 54 | Each commit message consists of a **header**, a **body** and a **footer**. The header has a special 55 | format that includes a **type**, a **scope** and a **subject**: 56 | 57 | ``` 58 | (): 59 | 60 | 61 | 62 |