Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

Initial Commit

Signed-off-by: Chris Aniszczyk <zx@twitter.com>
  • Loading branch information...
commit 73aca58f1e6679b6c53e74caa48a0578c7b60c2f 0 parents
@couch couch authored caniszczyk committed
Showing with 7,880 additions and 0 deletions.
  1. +2 −0  .gitignore
  2. +177 −0 LICENSE
  3. +111 −0 README.md
  4. +103 −0 app/config/application.js
  5. +4 −0 app/config/environments/development.js
  6. +4 −0 app/config/environments/production.js
  7. +27 −0 app/config/routes.js
  8. +33 −0 app/controllers/ApplicationController.js
  9. +53 −0 app/controllers/HomeController.js
  10. +63 −0 app/controllers/api/ApiController.js
  11. +22 −0 app/models/ApplicationModel.js
  12. +131 −0 app/models/FavoritesModel.js
  13. +9 −0 app/services/BaseService.js
  14. +264 −0 app/services/StreamService.js
  15. +29 −0 app/views/display.html
  16. +33 −0 app/views/index.html
  17. +56 −0 app/views/layout.html
  18. +2 −0  app/views/partials/event.html
  19. +7 −0 app/views/partials/participants.html
  20. +7 −0 app/views/partials/tweets_current.html
  21. +8 −0 app/views/partials/tweets_feed.html
  22. +11 −0 app/views/partials/tweets_top.html
  23. +32 −0 package.json
  24. +116 −0 public/css/application-responsive.css
  25. +704 −0 public/css/application.css
  26. +5 −0 public/css/filters.svg
  27. BIN  public/img/apple-touch-icon-114x114.png
  28. BIN  public/img/apple-touch-icon-72x72.png
  29. BIN  public/img/apple-touch-icon.png
  30. BIN  public/img/coffman.gif
  31. BIN  public/img/couch.gif
  32. BIN  public/img/favicon.ico
  33. BIN  public/img/hamid.gif
  34. BIN  public/img/hechanova.gif
  35. BIN  public/img/leibrock.gif
  36. BIN  public/img/linen_dark.png
  37. BIN  public/img/linen_light.png
  38. +460 −0 public/js/application.js
  39. +581 −0 public/lib/bootstrap/bootstrap-responsive.css
  40. +270 −0 public/lib/bootstrap/bootstrap-tooltip.js
  41. +51 −0 public/lib/bootstrap/bootstrap-transition.js
  42. +3,496 −0 public/lib/bootstrap/bootstrap.css
  43. BIN  public/lib/bootstrap/glyphicons-halflings-white.png
  44. BIN  public/lib/bootstrap/glyphicons-halflings.png
  45. +5 −0 public/lib/hogan/hogan-1.0.5.min.js
  46. +4 −0 public/lib/jquery/jquery-1.7.1.min.js
  47. +2 −0  public/lib/moment/moment.min.js
  48. +862 −0 public/lib/twitter-text/twitter-text-1.4.9.js
  49. +31 −0 public/lib/underscore/underscore-min.js
  50. +105 −0 server.js
2  .gitignore
@@ -0,0 +1,2 @@
+/data
+/node_modules
177 LICENSE
@@ -0,0 +1,177 @@
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
111 README.md
@@ -0,0 +1,111 @@
+## Ospriet — An example audience moderation app built on Twitter
+
+[Ospriet](http://twitter.github.com/ospriet) is a moderation tool that allows for audiences to post and vote on questions/comments for talks, presentations, and events. The application is written in [node.js](http://nodejs.org/), uses [MongoDB](http://www.mongodb.org/) for database storage, and is designed for deployment on [nodejitsu](http://nodejitsu.com/).
+
+## Motivations
+
+Why open source it?
+
+* Several people asked for the source after SXSW 2012 to implement the application for their own use
+* The app serves as a good example of building upon the Twitter platform
+* Everyone can now use it and help improve the code base
+
+You can view an example of the site powered by the application used at a 2012 SXSW panel at [http://designfromthegut.com](http://designfromthegut.com).
+
+For more information on the origins of the application, read [this post](http://couch.tumblr.com/post/18854314402).
+
+## Overview
+
+Ospriet allows anyone with a Twitter account to submit a question or comment, by posting an @-reply to a Twitter account dedicated for an audience-oriented event. The submission will be reposted to the event's account, with attribution. Audience members can vote up the best submissions by favoriting the submissions on the event account. Ospriet will then keep track of all of the favorites and provide the top submissions. Ospriet provides one single, simple interface for all of this that audience members can use on most devices.
+
+As an example, let's take the event account of <a href="https://twitter.com/dftg">@dftg</a> and submit a question.
+
+ @dftg: What are your thoughts on Apple’s approach to design?
+
+This tweet will be reposted by the application to <a href="https://twitter.com/dftg">@dftg</a> as an @-reply to your submission, and look like this:
+
+ From @couch: What are your thoughts on Apple's approach to design?
+
+Anyone can then favorite that reposted tweet, and see the top favorited submissions on the site.
+
+## Setup
+
+Please refer to these wiki pages to download, customize, and deploy your own instance of Ospriet.
+
+* [Creating your development environment](https://github.com/twitter/ospriet/wiki/Creating-your-development-environment)
+* [Creating a Twitter app](https://github.com/twitter/ospriet/wiki/Creating-a-Twitter-app)
+* [Customizing Ospriet](https://github.com/twitter/ospriet/wiki/Customizing-Ospriet)
+* [Testing Opsriet](https://github.com/twitter/ospriet/wiki/Testing-Ospriet)
+* [Working with nodejitsu](https://github.com/twitter/ospriet/wiki/Working-with-nodejitsu)
+
+## Libraries
+
+**Server-side**
+
+- [Matador](http://obvious.github.com/matador) _for MVC app structure_
+- [ntwitter](http://github.com/avianflu/ntwitter) _node.js wrapper for Twitter API_
+- [mongoose](http://mongoosejs.com/) _node.js wrapper for MongoDB_
+- [socket.io](http://socket.io) _for real-time updating of client-side UI_
+
+**Client-side**
+
+- [Bootstrap](http://twitter.github.com/bootstrap) _for skeletal layout and micro-jQuery plugins_
+- [underscore.js](http://documentcloud.github.com/underscore) _for client-side data manipulation_
+- [hogan.js](http://twitter.github.com/hogan.js) _for template rendering_
+- [Twitter Web Intents](https://dev.twitter.com/docs/intents) _for tweet actions without authentication_
+
+## Screenshots
+
+**Main index view**
+<br/>
+<img src="http://twitter.github.com/ospriet/images/index.jpg" title="Ospriet primary view" alt="Ospriet primary view" width="500" />
+<br/>
+
+**Presentation view for projector/screens**
+<br/>
+<img src="http://twitter.github.com/ospriet/images/display.jpg" title="Ospriet presentation view" alt="Ospriet presentation view" width="500" />
+<br/>
+
+**Responsive layout for mobile**
+<br/>
+<img src="http://twitter.github.com/ospriet/images/iphone.jpg" title="Ospriet mobile view" alt="Ospriet mobile view" width="300" />
+<br/>
+
+## Issues and Contributions
+
+Have a bug or contribution? Create an issue here on GitHub!
+
+[https://github.com/twitter/ospriet/issues](https://github.com/twitter/ospriet/issues)
+
+## Versioning
+
+For transparency and insight, releases will be numbered with the follow format:
+
+ <major>.<minor>.<patch>
+
+And constructed with the following guidelines:
+
+* Breaking backwards compatibility bumps the major
+* New additions without breaking backwards compatibility bumps the minor
+* Bug fixes and misc changes bump the patch
+
+For more information on semantic versioning, please visit http://semver.org/.
+
+
+## Authors
+
+**Bill Couch**
+
++ [https://github.com/couch](https://github.com/couch)
++ [https://twitter.com/couch](https://twitter.com/couch)
+
+**Dustin Senos**
+
++ [https://github.com/dustinsenos](https://github.com/dustinsenos)
++ [https://twitter.com/dustin](https://twitter.com/dustin)
+
+## License
+
+Copyright 2012 Twitter, Inc.
+
+Licensed under the Apache License, Version 2.0: [http://www.apache.org/licenses/LICENSE-2.0](http://www.apache.org/licenses/LICENSE-2.0)
103 app/config/application.js
@@ -0,0 +1,103 @@
+/*
+ * Copyright 2012 Twitter, Inc.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+module.exports = {
+
+ /*
+ TODO: Change all of the values here with your app's own credentials
+ Refer to https://github.com/twitter/ospriet/wiki/Customizing-Ospriet for help
+ */
+
+ /* Application configuration */
+ twitter_app: {
+ consumer_key: ''
+ , consumer_secret: ''
+ , access_token_key: ''
+ , access_token_secret: ''
+ }
+
+ , twitter_account: {
+ id: '495336570'
+ , screen_name: 'dftg'
+ }
+
+ , moderators: [
+ {
+ id_str: '18644328'
+ , screen_name: 'philcoffman'
+ }
+ ]
+
+ , blacklist: [
+ {
+ id_str: ''
+ , screen_name: ''
+ }
+ ]
+
+ /* Site/UI configuration */
+ , site: {
+ raw_url: 'http://designfromthegut.com'
+ , display_url: 'designfromthegut.com'
+ , google_analytics: 'UA-8533626-2'
+ }
+
+ , event: {
+ title: 'Design from the Gut'
+ , description: 'Debating whether research or intuition is a better approach to design should be a communal discussion.'
+ , instructions: '<strong>Submit a question or comment by posting a tweet to <a href="https://twitter.com/intent/user?screen_name=dftg" title="Design from the Gut on Twitter">@dftg</a> via the button below. Review the submissions and favorite the ones you\'d like to see answered</strong>. The moderator will choose from the top picks.'
+ , time: 'Friday, 3/9, 3:30p'
+ , location: 'Ballroom BC &bull; ACC'
+ , details_url: 'http://schedule.sxsw.com/2012/events/event_IAP11592'
+ }
+
+ , participants: [
+ {
+ name: 'Phil Coffman'
+ , title: 'Principal, Element'
+ , twitter: 'philcoffman'
+ , img_url: 'coffman.gif'
+ , role: 'Moderator'
+ }
+ , {
+ name: 'Bill Couch'
+ , title: 'Software engineer, Twitter'
+ , twitter: 'couch'
+ , img_url: 'couch.gif'
+ , role: 'Panelist'
+ }
+ , {
+ name: 'Naz Hamid'
+ , title: 'Principal, Weightshift'
+ , twitter: 'weightshift'
+ , img_url: 'hamid.gif'
+ , role: 'Panelist'
+ }
+ , {
+ name: 'Laurel Hechanova'
+ , title: 'Designer, Illustrator, Apocalypse OK'
+ , twitter: 'hechanova'
+ , img_url: 'hechanova.gif'
+ , role: 'Panelist'
+ }
+ , {
+ name: 'Jane Leibrock'
+ , title: 'User Experience Researcher, Facebook'
+ , twitter: 'fencebreak'
+ , img_url: 'leibrock.gif'
+ , role: 'Panelist'
+ }
+ ]
+}
4 app/config/environments/development.js
@@ -0,0 +1,4 @@
+module.exports = {
+ database: 'mongodb://'
+ , server_port: '3000'
+}
4 app/config/environments/production.js
@@ -0,0 +1,4 @@
+module.exports = {
+ database: 'mongodb://'
+ , server_port: '80'
+}
27 app/config/routes.js
@@ -0,0 +1,27 @@
+/*
+ * Copyright 2012 Twitter, Inc.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+module.exports = {
+ root: [
+ ['get', '/', 'Home', 'index'],
+ ['get', '/display', 'Home', 'display']
+ ],
+ 'api': [
+ ['get', '/feed', 'Api', 'feed'],
+ ['get', '/feed/refresh', 'Api', 'refresh'],
+ ['get', '/top', 'Api', 'top'],
+ ['get', '/question', 'Api', 'question']
+ ]
+}
33 app/controllers/ApplicationController.js
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2012 Twitter, Inc.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+module.exports = require('matador').BaseController.extend(function () {
+ this.viewFolder = ''
+ this.layout = 'layout'
+ this.config = app.getConfig()
+})
+ .methods({
+
+
+ /**
+ * Returns a service by its name
+ * @param {String} name
+ * @return Service instance
+ */
+ getService: function (name) {
+ return app.getService(name)
+ }
+
+ })
53 app/controllers/HomeController.js
@@ -0,0 +1,53 @@
+/*
+ * Copyright 2012 Twitter, Inc.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+module.exports = require('./ApplicationController').extend(function () {
+ this.StreamService = this.getService('Stream')
+})
+ .methods({
+
+ /**
+ * Main page for the site. Renders a list of
+ * all tweets, favorites, participants and questions.
+ */
+ index: function () {
+ this.render('index', {
+ view: 'index'
+ , event: this.config.event
+ , participants: this.config.participants
+ , screen_name: this.config.twitter_account.screen_name
+ , site: this.config.site
+ , css: [
+ {url: 'lib/bootstrap/bootstrap-responsive.css'}
+ , {url: 'css/application-responsive.css'}
+ ]
+ });
+ }
+
+ /**
+ * The screen specifically designed for high-contrast
+ * reading. Used for participants / hosts to be able to
+ * quickly grok the current question etc.
+ */
+ , display: function () {
+ this.render('display', {
+ view: 'presentation'
+ , event: this.config.event
+ , screen_name: this.config.twitter_account.screen_name
+ , site: this.config.site
+ });
+ }
+
+ });
63 app/controllers/api/ApiController.js
@@ -0,0 +1,63 @@
+/*
+ * Copyright 2012 Twitter, Inc.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+module.exports = require('../ApplicationController').extend(function () {
+ this.StreamService = this.getService('Stream')
+})
+ .methods({
+
+
+ /**
+ * @return Returns JSON list of all tweets
+ */
+ feed: function () {
+ this.StreamService.getFeed(function (feed) {
+ this.response.json(feed)
+ }.bind(this))
+ }
+
+
+ /**
+ * @return Returns JSON list of all favorites
+ */
+ , top: function () {
+ this.StreamService.getFavorites(function (favorites) {
+ favorites = (favorites.length > 0) ? favorites : [] // Todo: this should be handled at the service level
+ this.response.json(favorites)
+ }.bind(this))
+ }
+
+
+ /**
+ * @return Returns JSON list of all tweets which have been marked
+ */
+ , question: function () {
+ this.StreamService.getQuestions(function(questions) {
+ this.response.json(questions)
+ }.bind(this))
+ }
+
+
+ /**
+ * Forces a refresh on the service
+ * @return Returns JSON indicating if the refresh was successful
+ */
+ , refresh: function () {
+ this.StreamService.refreshFeed(function(resp) {
+ this.response.json(resp)
+ }.bind(this))
+ }
+
+ })
22 app/models/ApplicationModel.js
@@ -0,0 +1,22 @@
+/*
+ * Copyright 2012 Twitter, Inc.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+module.exports = Class(function () {
+ this.mongo = require('mongodb')
+ this.mongoose = require('mongoose')
+ this.Schema = this.mongoose.Schema
+ this.config = app.getConfig()
+ this.mongoose.connect(this.config.database)
+})
131 app/models/FavoritesModel.js
@@ -0,0 +1,131 @@
+/*
+ * Copyright 2012 Twitter, Inc.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+module.exports = require(app.set('models') + '/ApplicationModel').extend(function () {
+ this.UserSchema = new this.Schema({
+ id_str : { type: String, required: true }
+ , profile_icon : { type: String, required: true }
+ , screen_name : { type: String, required: true }
+ , full_name : { type: String, required: true }
+ })
+
+ this.FavoritesSchema = new this.Schema({
+ status_id : String
+ , status : this.Schema.Types.Mixed
+ , users : [this.UserSchema]
+ , marked : Boolean
+ })
+
+ this.DBModel = this.mongoose.model('Favorite', this.FavoritesSchema)
+})
+ .methods({
+
+
+ /**
+ * Gets a favorite by twitter status id
+ * @param {string} id
+ * @param {function} callback A favorite object if found, or null
+ */
+ getFavorite: function (id, callback) {
+ this.DBModel
+ .findOne()
+ .where('status_id', id)
+ .run(callback)
+ }
+
+
+ /**
+ * Gets a list of all favorites
+ * @param {function} callback An array containing favorites
+ */
+ , getFavorites: function (callback) {
+ this.DBModel
+ .find({})
+ .run(callback)
+ }
+
+
+ /**
+ * Gets a list favorites which have been marked by a moderator to be answered
+ * @param {function} callback An array containing favorites
+ */
+ , getQuestions: function (callback) {
+ this.DBModel
+ .find()
+ .where('marked', true)
+ .run(callback)
+ }
+
+
+ /**
+ * Creates a new 'tweet' in the system
+ * @param {object} user Creators twitter object
+ * @param {object} status The twitter status object
+ * @param {boolen} marked If a moderator has marked it to be answered
+ * @param {function} callback The object which was created
+ */
+ , create: function (user, status, marked, callback) {
+ var favorite = new this.DBModel()
+ favorite.status_id = status.id_str
+ favorite.status = status
+ favorite.marked = marked
+ favorite.users.push({
+ id_str : user.id_str
+ , profile_icon : user.profile_image_url
+ , screen_name : user.screen_name
+ , full_name : user.name
+ })
+ favorite.save(callback)
+ }
+
+
+ /**
+ * Adds a new favorite to a 'tweet'. This is
+ * used when people favorite the items in our system
+ * to increment the amount of users who've liked the item
+ * @param {object} user The user who has liked the tweet
+ * @param {object} favorite The item which they are liking
+ * @param {boolen} marked If a moderator has marked it to be answered
+ * @param {function} callback The object which was created
+ */
+ , add: function (user, favorite, marked, callback) {
+ favorite.marked = marked
+ favorite.users.push({
+ id_str : user.id_str
+ , profile_icon : user.profile_image_url
+ , screen_name : user.screen_name
+ , full_name : user.name
+ })
+ favorite.save(callback)
+ }
+
+
+ /**
+ * Removes a favorite from an item. Called if a user
+ * has liked an item, and decides to unlink it.
+ * @param {object} user The user who has unliked the tweet
+ * @param {object} favorite The item which they are unliking
+ * @param {boolen} marked If a moderator has marked it to be answered
+ * @param {function} callback The object which was removed
+ */
+ , remove: function (user, favorite, marked, callback) {
+ // TODO: remove status if there are no favorites
+ this.DBModel.update(
+ { "status_id" : favorite.status_id }
+ , { $set : { "marked" : marked }
+ , $pull : { "users" : { 'id_str': user.id_str } } }
+ , callback)
+ }
+
+ });
9 app/services/BaseService.js
@@ -0,0 +1,9 @@
+module.exports = Class(function(){
+ this.config = app.getConfig()
+})
+ .methods ({
+ getModel: function (name) {
+ if (app.set('modelCache')[name]) return app.set('modelCache')[name]
+ return (app.set('modelCache')[name] = new (require(app.set('models') + '/' + name + 'Model')))
+ }
+ })
264 app/services/StreamService.js
@@ -0,0 +1,264 @@
+/*
+ * Copyright 2012 Twitter, Inc.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+var twitter = require('ntwitter')
+var io = require('socket.io').listen(app)
+ io.set('log level', 1)
+
+module.exports = require(app.set('services') + '/BaseService').extend(function () {
+ this.twttr = new twitter({
+ consumer_key: this.config.twitter_app.consumer_key
+ , consumer_secret: this.config.twitter_app.consumer_secret
+ , access_token_key: this.config.twitter_app.access_token_key
+ , access_token_secret: this.config.twitter_app.access_token_secret
+ })
+
+ this.stream_active = false
+ this.account_details = {
+ id_str: this.config.twitter_account.id
+ , screen_name: this.config.twitter_account.screen_name
+ }
+
+ this.moderators = [
+ {
+ id_str: this.config.twitter_account.id
+ , screen_name: this.config.twitter_account.screen_name
+ }
+ ]
+ v.each(this.config.moderators, function(moderator) {
+ this.moderators.push(moderator)
+ }.bind(this))
+
+ this.blacklist = this.config.blacklist
+
+ this.account_statuses = []
+ this.model = this.getModel('Favorites')
+ this.socket = {}
+
+})
+ .methods({
+
+ init: function () {
+ this.start();
+ }
+ , start: function () {
+ this.initTimeline()
+ this.initStream()
+ this.initSockets()
+ }
+
+ , initTimeline: function () {
+ this.twttr.getUserTimeline({user_id:this.account_details.id_str, count:200, include_entities: true}, function(err, data){
+ this.account_statuses = data;
+ console.log(err)
+ }.bind(this));
+ }
+
+ , initStream: function () {
+
+ this.stream_active = true;
+
+ this.twttr
+ .stream('user', {track:this.account_details.screen_name, replies:'all', include_entities: true}, function(stream) {
+
+ stream.on('data', function (data){
+ console.dir(data)
+ this.handleData(data);
+ }.bind(this));
+
+ stream.on('delete', function(data){
+ console.dir(data)
+ this.initTimeline()
+ this.pushDelete(data)
+ }.bind(this))
+
+ stream.on('end', function (resp) {
+ console.log('disconnected from twitter: ' + resp.statusCode);
+ this.start();
+ this.stream_active = false;
+ }.bind(this));
+
+ stream.on('destroy', function (resp) {
+ console.log('disconnected by twitter: ' + resp.statusCode);
+ this.start();
+ this.stream_active = false;
+ }.bind(this));
+
+ stream.on('error', function (obj) {
+ console.log('error: ', obj);
+ });
+
+ }.bind(this));
+
+ }
+
+ , initSockets: function () {
+ io.sockets
+ .on('connection', function (socket){
+ console.log('THE SOCKET CONNECTED!' + socket);
+ this.socket = socket;
+ }.bind(this))
+ }
+
+ , handleData: function (data) {
+ switch (data.event){
+ case 'favorite':
+ console.log('event is favorite')
+ this.addFavorite(data)
+ break
+ case 'unfavorite':
+ console.log('event is unfavorite')
+ this.removeFavorite(data)
+ break
+ default:
+ this.filter(data)
+ }
+ }
+
+ , getFeed: function (callback) {
+ // query REST API on load for all previous tweets, push to array
+ callback(this.account_statuses)
+ }
+
+ , refreshFeed: function (callback) {
+ this.initTimeline()
+ callback({status: 'refresh requested'})
+ }
+
+ , getFavorites: function (callback) {
+ this.model.getFavorites(function(err, doc) {
+ callback(doc)
+ })
+ }
+ , getFavorite: function (id, callback) {
+ this.model.getFavorite(id, function (err, obj) {
+ callback(err, obj)
+ });
+ }
+ , getQuestions: function (callback) {
+ this.model.getQuestions(function(err, doc) {
+ callback(doc)
+ })
+ }
+ , addFavorite: function(data) {
+ // if a favorite and exists in the database, add user to its favorites array
+ // else push as an item to the database, setting user in its favorites array
+ this.getFavorite(data.target_object.id_str, function (err, fav) {
+
+ // check if any moderator favorites, if so, pass marked as true
+ // var marked = (data.source.id_str === this.moderator.id_str) ? true : null
+ var marked = null;
+ v.each(this.moderators, function (moderator) {
+ if(data.source.id_str === moderator.id_str){
+ marked = true
+ }
+ })
+
+ if(fav){
+ console.log('fav to add: ' + fav)
+ this.model.add(data.source, fav, marked, function(err, doc) {
+ console.log('added to model ', doc)
+ this.pushFavorite(err, doc)
+ }.bind(this))
+ } else {
+ console.log('fav to create: '+fav)
+ this.model.create(data.source, data.target_object, marked, function(err, doc) {
+ console.log('created model ', doc)
+ this.pushFavorite(err, doc)
+ }.bind(this))
+ }
+ }.bind(this))
+ }
+
+ , removeFavorite: function (data) {
+ // if unfavorited and exists in the database, remove user from favorites array
+ this.getFavorite(data.target_object.id_str, function (err, fav) {
+ if(fav){
+ console.log('fav to remove: ' + fav)
+ // var marked = (data.source.id_str === this.moderator.id_str) ? false : null
+ var marked = null;
+ v.each(this.moderators, function (moderator) {
+ if(data.source.id_str === moderator.id_str){
+ marked = false
+ }
+ })
+
+ this.model.remove(data.source, fav, marked, function(err, doc) {
+ this.pushFavorite(err, doc)
+ }.bind(this))
+ }
+ }.bind(this))
+ }
+
+ // FILTER FOR REPLIES
+ , filter: function (data) {
+
+ var blocked = false
+ v.each(this.blacklist, function (blacklisted) {
+ if(data.user.id_str === blacklisted.id_str){
+ blocked = true
+ }
+ })
+
+ // don't post if the user has been blacklisted
+ if(!blocked){
+ // if a reply, take text str, strip username, repost as new tweet with author username
+ if (data.in_reply_to_user_id_str &&
+ data.in_reply_to_user_id_str === this.account_details.id_str) {
+
+ var prefix = new RegExp('@'+this.account_details.screen_name+'[:\\s]?[\\-–—]?\\s*')
+ , text_str = data.text.replace(prefix, '')
+ , new_text_str = 'From @'+data.user.screen_name+': '+text_str;
+
+ if(new_text_str.length > 140){
+ new_text_str = new_text_str.slice(0,139)
+ new_text_str += ''
+ }
+
+ var status_id = data.id_str;
+
+ this.twttr.updateStatus(new_text_str, {in_reply_to_status_id: status_id}, function (err, data){
+ if(err) {
+ return console.log('error on posting: '+err);
+ }
+ // prepend tweet to array to maintain reverse chronological order
+ this.account_statuses.splice(0,0,data);
+ this.pushTweet(data);
+
+ }.bind(this));
+ }
+ }
+ }
+
+ , pushTweet: function (tweet) {
+ this.socket.emit('tweet', tweet)
+ }
+ , pushDelete: function (tweet) {
+ console.log('emit deletion')
+ setTimeout(function(){
+ this.socket.emit('deletion', tweet)
+ }.bind(this), 3000)
+ }
+ , pushFavorite: function (err, fav) {
+ if (fav.marked === true){
+ this.socket.emit('selected', fav)
+ }
+ if (fav === 1) {
+ this.socket.emit('deselected')
+ }
+ this.socket.emit('favorite', fav)
+ }
+
+ })
29 app/views/display.html
@@ -0,0 +1,29 @@
+<div class="container display-content">
+
+ <div class="row top-question patterned">
+ <div class="span12">
+ {{> tweets_current}}
+ </div>
+ </div>
+
+ <div class="row additional-content patterned">
+ <div class="span5 sidebar">
+ <div class="desc">
+ {{#site}}
+ <h1 class="inset"><a href="{{{raw_url}}}">{{{display_url}}}</a></h1>
+ {{/site}}
+ <ol class="inset">
+ <li>Question or comment?</li>
+ <li>Post as a reply to <a href="https://twitter.com/{{screen_name}}">@{{screen_name}}</a></li>
+ <li>See submissions on <a href="https://twitter.com/{{screen_name}}">@{{screen_name}}</a>'s timeline</li>
+ <li>Favorite the ones you like</li>
+ <li>Top picks will be discussed</li>
+ </ol>
+ </div>
+ </div>
+
+ <div class="span7 offset5 content">
+ {{> tweets_top}}
+ </div>
+ </div>
+</div>
33 app/views/index.html
@@ -0,0 +1,33 @@
+<div class="container">
+
+ <div class="row">
+ <div class="span6 desc">
+ {{#event}}
+ {{>event}}
+ {{/event}}
+ </div>
+ <div class="span6">
+ <ul class="participants">
+ {{#participants}}
+ {{>participants}}
+ {{/participants}}
+ </ul>
+ </div>
+ </div>
+
+ <div class="row contents">
+ <div class="span6 left">
+ {{> tweets_current}}
+ {{> tweets_top}}
+ </div>
+ <div class="span6 right">
+ {{> tweets_feed}}
+ </div>
+ </div>
+
+ <div class="row footer">
+ <div class="span8 offset2">
+ <p>Powered by <a href="http://github.com/twitter/ospriet">Ospriet from Twitter</a> on <a href="http://nodejitsu.com/">nodejitsu</a>.</p>
+ </div>
+ </div>
+</div>
56 app/views/layout.html
@@ -0,0 +1,56 @@
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <meta charset="utf-8">
+ {{#event}}
+ <title>{{{title}}}</title>
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <meta name="description" content="{{description}}">
+ {{/event}}
+
+ <link href="lib/bootstrap/bootstrap.css" rel="stylesheet">
+ <link href="css/application.css" rel="stylesheet">
+
+ {{#css}}
+ <link href="{{url}}" rel="stylesheet">
+ {{/css}}
+
+ <script src="lib/jquery/jquery-1.7.1.min.js"></script>
+ <script src="lib/bootstrap/bootstrap-tooltip.js"></script>
+ <script src="lib/bootstrap/bootstrap-transition.js"></script>
+ <script src="lib/underscore/underscore-min.js"></script>
+ <script src="lib/twitter-text/twitter-text-1.4.9.js"></script>
+ <script src="lib/hogan/hogan-1.0.5.min.js"></script>
+ <script src="/socket.io/socket.io.js"></script>
+ <script src="lib/moment/moment.min.js"></script>
+
+ <script src="//platform.twitter.com/widgets.js"></script>
+ <script src="js/application.js"></script>
+
+ <link rel="shortcut icon" href="img/favicon.ico">
+ <meta name="apple-mobile-web-app-capable" content="yes">
+ <meta name="apple-mobile-web-app-status-bar-style" content="default">
+
+ <link rel="apple-touch-icon-precomposed" href="img/apple-touch-icon.png">
+ <link rel="apple-touch-icon-precomposed" sizes="72x72" href="img/apple-touch-icon-72x72.png">
+ <link rel="apple-touch-icon-precomposed" sizes="114x114" href="img/apple-touch-icon-114x114.png">
+
+ {{#site}}
+ <script type="text/javascript">
+ var _gaq = _gaq || [];
+ _gaq.push(['_setAccount', '{{{google_analytics}}}']);
+ _gaq.push(['_trackPageview']);
+
+ (function() {
+ var ga = document.createElement('script'); ga.type = 'text/javascript'; ga.async = true;
+ ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js';
+ var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s);
+ })();
+ </script>
+ {{/site}}
+ </head>
+
+ <body class="patterned {{view}}">
+ {{{body}}}
+ </body>
+</html>
2  app/views/partials/event.html
@@ -0,0 +1,2 @@
+<h1>{{{title}}} <a href="{{{details_url}}}" class="help" target="_blank">{{{time}}} &bull; {{{location}}}</a></h1>
+<p><span class="intro">{{{description}}}</span> {{{instructions}}}</p>
7 app/views/partials/participants.html
@@ -0,0 +1,7 @@
+<li rel="tooltip" data-placement="bottom" data-original-title="{{title}}">
+ <a href="https://twitter.com/intent/user?screen_name={{twitter}}">
+ <img src="img/{{img_url}}" />
+ <h3>{{name}}</h3>
+ <p class="caption">{{role}}</p>
+ </a>
+</li>
7 app/views/partials/tweets_current.html
@@ -0,0 +1,7 @@
+<div class="tweets-section tweets-current">
+ <div class="header">
+ <h3><span class="subheader-icon finger">&#9758;</span> Current pick</h3>
+ </div>
+ <ul class="tweets">
+ </ul>
+</div>
8 app/views/partials/tweets_feed.html
@@ -0,0 +1,8 @@
+<div class="tweets-section tweets-feed">
+ <div class="header">
+ <h3>Submissions</h3>
+ <a class="btn-custom pull-right" href="https://twitter.com/intent/tweet?text=%40{{screen_name}}%20"><i class="icon-edit icon-white"></i> Submit</a>
+ </div>
+ <ul class="tweets">
+ </ul>
+</div>
11 app/views/partials/tweets_top.html
@@ -0,0 +1,11 @@
+<div class="tweets-section tweets-top">
+ <div class="header">
+ <h3><span class="subheader-icon star">&#9733;</span> Top picks</h3>
+ <div class="timer">
+ <canvas width="30" height="30"></canvas>
+ <p>Next refresh</p>
+ </div>
+ </div>
+ <ul class="tweets">
+ </ul>
+</div>
32 package.json
@@ -0,0 +1,32 @@
+{
+ "name": "ospriet",
+ "engines": {
+ "node": "0.6.x"
+ },
+ "version": "1.0.0",
+ "private": true,
+ "dependencies": {
+ "mongodb": "0.9.6-23",
+ "mongoose": "2.5.6",
+ "ntwitter": "0.2.10",
+ "socket.io": "0.8.7",
+ "express": ">=2.5.0 <3.0.0",
+ "valentine": "*",
+ "klass": "*",
+ "colors": "*",
+ "hogan.js": "*",
+ "matador": "<=0.0.14"
+ },
+ "domains": [
+ "designfromthegut.com"
+ ],
+ "subdomain": "fromthegut",
+ "scripts": {
+ "start": "server.js"
+ },
+ "licenses": [
+ { "type": "Apache-2.0"
+ , "url": "http://www.apache.org/licenses/LICENSE-2.0"
+ }
+ ]
+}
116 public/css/application-responsive.css
@@ -0,0 +1,116 @@
+@media (min-width: 768px) and (max-width: 979px) {
+
+ ul.participants li {
+ display: inline-block;
+ text-align: left;
+ width: 175px;
+ margin-bottom: 10px;
+ }
+ ul.participants li img {
+ float: left;
+ height: 30px;
+ width: 30px;
+ margin-right: 10px;
+
+ -webkit-border-radius: 5px;
+ -moz-border-radius: 5px;
+ border-radius: 5px;
+
+ filter: none;
+ -webkit-filter: grayscale(0);
+ }
+}
+
+@media (max-width: 1200px) {
+
+
+ body {
+ padding-top: 20px;
+ padding-top: 20px;
+ }
+
+ h1 a {
+ display: block;
+ padding-top: 6px;
+ padding-left: 0;
+ line-height: 16px;
+ }
+
+ h1 a.help {
+ padding-left: 0px;
+ }
+
+ ul.participants li {
+ display: inline-block;
+ text-align: left;
+ width: 175px;
+ margin-bottom: 10px;
+ }
+ ul.participants li img {
+ float: left;
+ height: 30px;
+ width: 30px;
+ margin-right: 10px;
+
+ -webkit-border-radius: 5px;
+ -moz-border-radius: 5px;
+ border-radius: 5px;
+
+ filter: none;
+ -webkit-filter: grayscale(0);
+ }
+}
+
+
+@media (max-width: 767px) {
+
+ body {
+ padding-top: 10px;
+ padding-bottom: 10px;
+ }
+
+ h1 a {
+ display: block;
+ padding-top: 6px;
+ padding-left: 0;
+ line-height: 16px;
+ }
+
+ .desc p {
+ line-height: 18px;
+ font-size: 13px;
+ }
+
+ .desc span.intro {
+ display: none;
+ }
+
+ ul.participants {
+ display: none;
+ }
+
+ .row > .span6.left {
+ float: none;
+ }
+
+ .row > .span6.right {
+ float: none;
+ }
+
+ .tweets-section {
+ background-color: #fff;
+ }
+
+ .tweets-section .tweets > li {
+ border-bottom: 1px solid #EEE;
+ }
+
+ .tweets-section .tweets li .tweet-metadata .tweet-favorites ul.users {
+ display: none;
+ }
+
+ .footer {
+ margin-top: 0px;
+ }
+
+}
704 public/css/application.css
@@ -0,0 +1,704 @@
+/*
+ * Copyright 2012 Twitter, Inc.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+body {
+ padding-top: 40px;
+ padding-bottom: 40px;
+ font-family: "Helvetica", sans-serif;
+}
+
+body.index {
+ background-color: rgb(250, 250, 250);
+ color: rgb(71, 71, 71);
+}
+
+.patterned {
+ background-attachment: fixed;
+ background-repeat: repeat;
+}
+
+.index.patterned,
+.index .patterned {
+ background-image: url('../img/linen_light.png');
+}
+
+.index h1 {
+ text-shadow: 0px 1px 0px rgba(255,255,255,1);
+}
+
+.index h1 a {
+ color: #666;
+}
+
+.index p {
+ color: rgb(150,150,150);
+ text-shadow: 0px 1px 0px rgba(255,255,255,1);
+}
+
+
+h1 {
+ margin: 0 0 10px 0;
+ font-weight: 100;
+}
+
+h1 a {
+ font-size: 30px;
+ color: #666;
+}
+
+h1 a.help {
+ padding-left: 20px;
+ font-size: 12px;
+ text-transform: uppercase;
+ cursor: help;
+}
+
+p {
+ font-size: 14px;
+ line-height: 22px;
+}
+
+a:hover {
+ text-decoration: none;
+}
+
+p a {
+ color: #3a87ad;
+ font-weight: bold;
+}
+
+p a span.deemphasize {
+ font-weight: 300;
+}
+
+p a:hover {
+ color: #307191;
+ text-decoration: none;
+}
+
+
+/* bootstrap overrride */
+
+.btn-custom {
+ display: inline-block;
+ padding: 4px 10px 4px;
+ margin-bottom: 0;
+ font-size: 13px;
+ line-height: 18px;
+ color: #333333;
+ text-align: center;
+ text-shadow: 0 1px 1px rgba(0, 0, 0, 1);
+ vertical-align: middle;
+
+ background-color: #3a87ad;
+ color: #fff;
+ text-shadow: none;
+
+ font-weight: 100;
+
+ -webkit-border-radius: 4px;
+ -moz-border-radius: 4px;
+ border-radius: 4px;
+
+ -webkit-box-shadow: inset 0px -1px 0px rgba(255,255,255,.75);
+ -moz-box-shadow: inset 0px -1px 0px rgba(255,255,255,.75);
+ box-shadow: inset 0px -1px 0px rgba(255,255,255,.75);
+
+ cursor: pointer;
+ filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
+ *margin-left: .3em;
+
+ -webkit-transition: .3s;
+ -moz-transition: .3s;
+ transition: .3s;
+}
+.btn-custom:hover {
+ text-decoration: none;
+ color: #FFF;
+ background-color: #307191;
+}
+
+/* end override */
+
+
+ul.participants {
+ margin: 0;
+ padding: 10px 10px 10px 20px;
+ background-color: rgba(0,0,0,.05);
+
+ -webkit-border-radius: 5px;
+ -moz-border-radius: 5px;
+ border-radius: 5px;
+
+ -webkit-box-shadow: inset 0px 1px 1px rgba(0,0,0,.15);
+ -moz-box-shadow: inset 0px 1px 1px rgba(0,0,0,.15);
+ box-shadow: inset 0px 1px 1px rgba(0,0,0,.15);
+
+}
+
+ul.participants li {
+ display: inline-block;
+ margin: 0px 10px;
+ list-style: none;
+ text-align: center;
+}
+
+ul.participants li img {
+ width: 75px;
+ height: 75px;
+
+ -webkit-border-radius: 50px;
+ -moz-border-radius: 50px;
+ border-radius: 50px;
+
+ -webkit-box-shadow: 0px 1px 1px rgba(0,0,0,.5);
+ -moz-box-shadow: 0px 1px 1px rgba(0,0,0,.5);
+ box-shadow: 0px 1px 1px rgba(0,0,0,.5);
+
+ /* filter: url(filters.svg#grayscale); *//* Firefox 3.5+ */
+ filter: gray; /* IE5+ */
+ -webkit-filter: grayscale(.65); /* Webkit Nightlies & Chrome Canary */
+
+ -webkit-transition: .5s ease-out;
+ -moz-transition: .5s ease-out;
+ transition: .5s ease-out;
+}
+
+ul.participants li:hover img {
+
+ -webkit-border-radius: 5px;
+ -moz-border-radius: 5px;
+ border-radius: 5px;
+
+ filter: none;
+ -webkit-filter: grayscale(0);
+}
+
+ul.participants li a,
+ul.participants li a:hover,
+ul.participants li a:visited {
+ color: rgb(150,150,150);
+ text-decoration: none;
+}
+
+ul.participants li h3 {
+ line-height: 14px;
+ font-size: 12px;
+}
+
+ul.participants li p {
+ font-family: "Helvetica", sans-serif;
+ font-size: 10px;
+ font-weight: 100;
+ line-height: 20px;
+ margin-bottom: 0;
+ text-transform: uppercase;
+}
+
+.contents {
+ margin-top: 20px;
+}
+
+.tweets-section {
+ padding: 20px;
+ margin-bottom: 20px;
+
+ -webkit-border-radius: 5px;
+ -moz-border-radius: 5px;
+ border-radius: 5px;
+
+ border: 1px solid rgba(0,0,0,.1);
+
+ background-color: rgba(255,255,255,.25);
+
+ -webkit-transition: height 1s;
+ -moz-transition: height 1s;
+ transition: height 1s;
+}
+
+.tweets-section.tweets-top {
+ border: 1px solid rgba(248, 148, 6, .25);
+}
+
+.tweets-section.tweets-current {
+ display: none;
+}
+
+.tweets-section .header {
+ margin-bottom: 20px;
+}
+
+.tweets-section .header h3,
+.tweets-section .header a {
+ display: inline-block;
+}
+
+.tweets-section .header h3 {
+ font-size: 16px;
+ font-weight: 100;
+ text-transform: uppercase;
+}
+
+.tweets-section .header h3 span.subheader-icon {
+ padding: 0px 15px 0px 5px;
+}
+
+.tweets-section .header h3 span.finger {
+ color: rgba(150, 25, 6, 1);
+}
+
+.tweets-section .header h3 span.star {
+ color: rgba(248, 148, 6, 1);
+}
+
+.tweets-section .header .timer {
+ float: right;
+ opacity: 0;
+}
+
+.tweets-section .header .timer canvas {
+ clear: none;
+ float: right;
+}
+
+.tweets-section .header .timer p {
+ display: inline;
+ text-align: right;
+ font-size: 10px;
+ text-transform: uppercase;
+ float: right;
+ line-height: 30px;
+ padding-right: 10px;
+ color: #307191;
+}
+
+.tweets-section p.empty {
+ padding: 40px;
+ text-align: center;
+}
+
+.tweets-section .tweets {
+ width: 100%;
+ margin-left: 0px;
+ overflow: hidden;
+}
+
+.tweets-section .tweets > li {
+ margin: 0;
+ padding: 15px 0px 15px;
+ list-style: none;
+ border-bottom: 1px solid #EEE;
+
+ overflow: none;
+ min-height: 50px;
+ opacity: 1;
+ height: auto;
+
+ background-color: rgba(255,255,255,0);
+ -webkit-transition: background-color .5s;
+ -moz-transition: background-color .5s;
+ transition: background-color .5s;
+}
+.tweets-section .tweets > li:hover {
+ background-color: rgba(255,255,255,.25);
+}
+
+.tweets-section .tweets > li p {
+ margin-bottom: 0;
+ -webkit-transition: color .5s;
+ -moz-transition: color .5s;
+ transition: color .5s;
+}
+.tweets-section .tweets > li:hover p {
+ color: #666;
+}
+
+
+.tweets-section .tweets li.offset {
+ -webkit-transition: margin-top 1s;
+ -moz-transition: margin-top 1s;
+ transition: margin-top 1s;
+}
+
+
+
+.tweets-section .tweets li:last-child {
+ border: none;
+}
+
+.tweets-section .tweets li .tweet-block {
+}
+
+.tweets-section .tweets li .tweet-stats {
+ float: left;
+ display: inline-block;
+ padding-right: 23px;
+ width: 20px;
+ height: 30px;
+}
+
+.tweets-section .tweets li .tweet-stats span {
+ float: left;
+ padding: 3px 2px 2px 3px;
+ width: 20px;
+ height: 20px;
+
+ color: #999;
+ font-size: 14px;
+ text-align: center;
+ vertical-align: middle;
+ cursor: pointer;
+
+ border-radius: 50px;
+ border: 1px solid #CCC;
+
+ border: 1px solid rgba(248, 148, 6, 1);
+ color: rgba(248, 148, 6, 1);
+
+ -webkit-transition: .3s;
+ -moz-transition: .3s;
+ transition: .3s;
+}
+
+.tweets-section .tweets li:hover .tweet-stats span {
+}
+
+
+.tweets-section .tweets li .tweet-content {
+ padding-top: 0px;
+ margin-bottom: 12px;
+}
+
+.tweets-section .tweets li .tweet-content p {
+ display: block;
+}
+
+.tweets-section .tweets li .tweet-content a {
+ text-decoration: none;
+}
+
+.tweets-section .tweets li .tweet-content a.tweet-url.hashtag {
+ font-weight: normal;
+ opacity: .75;
+}
+
+.tweets-section .tweets li .tweet-metadata {
+ margin-left: 40px;
+ overflow: hidden;
+}
+
+.tweets-section .tweets li .tweet-metadata .tweet-favorites {
+ display: inline-block;
+ margin-left: 0px;
+}
+.tweets-section .tweets li .tweet-metadata .tweet-favorites ul.users {
+ margin-left: 0;
+ padding-left: 2px;
+}
+.tweets-section .tweets li .tweet-metadata .tweet-favorites ul.users li {
+ display: inline-block;
+ margin: 0px 3px 3px 0px;
+}
+.tweets-section .tweets li .tweet-metadata .tweet-favorites ul.users li a img {
+ height: 20px;
+ width: 20px;
+
+ -webkit-border-radius: 3px;
+ -moz-border-radius: 3px;
+ border-radius: 3px;
+
+ /*filter: url(filters.svg#grayscale);*/ /* Firefox 3.5+ */
+ filter: gray; /* IE5+ */
+ -webkit-filter: grayscale(1); /* Webkit Nightlies & Chrome Canary */
+
+ -webkit-transition: .5s ease-out;
+ -moz-transition: .5s ease-out;
+ transition: .5s ease-out;
+}
+.tweets-section .tweets li:hover .tweet-metadata .tweet-favorites ul.users li a img {
+ filter: none;
+ -webkit-filter: grayscale(0);
+}
+
+
+.tweets-section .tweets li .tweet-metadata {}
+.tweets-section .tweets li .tweet-metadata .tweet-actions .actions {
+ list-style: none;
+}
+.tweets-section .tweets li .tweet-metadata .tweet-actions .actions .action-link {
+ display: inline-block;
+ color: #CCC;
+}
+
+.tweets-section .tweets li .tweet-metadata .tweet-actions .actions .action-link a {
+ font-size: 10px;
+ text-transform: uppercase;
+ -webkit-transition: .3s;
+ -moz-transition: .3s;
+ transition: .3s;
+}
+
+.tweets-section .tweets li .tweet-metadata .tweet-actions .actions .action-link a.action-permalink {
+ text-transform: none;
+ color: #CCC;
+}
+.tweets-section .tweets li .tweet-metadata .tweet-actions .actions .action-link a.action-permalink:hover {
+ color: #666;
+}
+
+.tweets-section .tweets li:hover .tweet-metadata .tweet-actions .actions .action-link a.action-favorite {
+ color: rgba(248, 148, 6, 1);
+}
+
+.footer {
+ margin-top: 20px;
+}
+
+.footer p {
+ font-size: 12px;
+ line-height: 14px;
+ text-align: center;
+}
+
+
+ .row > .span6.left {
+ float: right;
+ }
+
+ .row > .span6.right {
+ float: left;
+ }
+
+/* presenter's display styles */
+
+body.presentation {
+ background-color: rgb(0, 0, 0);
+ color: rgb(100, 100, 100);
+}
+
+.presentation.patterned,
+.presentation .patterned {
+ background-image: url('../img/linen_dark.png');
+}
+
+.presentation .inset,
+.presentation .tweets-section {
+
+ text-shadow: 0px 2px 1px rgba(0,0,0,1);
+
+ -webkit-border-radius: 5px;
+ -moz-border-radius: 5px;
+ border-radius: 5px;
+
+ -webkit-box-shadow: 0px -1px 1px rgba(0,0,0,1);
+ -moz-box-shadow: 0px -1px 1px rgba(0,0,0,1);
+ box-shadow: 0px -1px 1px rgba(0,0,0,1);
+}
+
+.presentation .inset {
+ background-color: rgba(0,0,0,.25);
+
+}
+
+.presentation .tweets-section {
+ background-color: #111;
+}
+
+
+.display-content .top-question,
+.display-content .sidebar { position: fixed;}
+
+.display-content .top-question {
+ margin-top: -40px;
+ padding-top: 40px;
+
+ z-index: 10;
+
+ background-color: rgb(250, 250, 250);
+}
+
+.display-content ol {
+ padding: 15px 0px 20px 20px;
+ margin-left: 0px;
+}
+
+.display-content ol li {
+ list-style-position: inside;
+}
+
+.display-content p {
+ font-size: 20px;
+ line-height: 30px;
+}
+
+.display-content a {
+ color: #5E92A8;
+}
+
+.tweets-section .header h3 span.finger {
+ color: rgba(200, 25, 0, 1);
+}
+
+.display-content .desc h1 {
+ margin-bottom: 20px;
+ font-weight: bold;
+ font-size: 36px;
+ line-height: 48px;
+ margin-top: 5px;
+ width: 100%;
+
+ text-align: center;
+ text-shadow: none;
+}
+
+.display-content .desc h1 a {
+ width: 100%;
+ padding: 10px 20px;
+ color: #999;
+}
+
+.display-content h1 a span {
+ padding: 0px 5px;
+}
+
+.display-content .desc p {margin-top: 20px; color: #333;}
+
+.display-content .desc p,
+.display-content .desc p a,
+.display-content ol li {font-size: 20px; line-height: 35px;}
+
+
+.display-content .content {
+ -webkit-border-radius: 5px 5px 0px 0px;
+ -moz-border-radius: 5px 5px 0px 0px;
+ -border-radius: 5px 5px 0px 0px;
+}
+
+.display-content .tweets-section.tweets-current {
+ margin-bottom: 35px;
+
+ -webkit-border-radius: 5px 5px 0px 0px;
+ -moz-border-radius: 5px 5px 0px 0px;
+ -border-radius: 5px 5px 0px 0px;
+}
+
+.display-content .tweets-section.tweets-top {
+ border: none;
+}
+
+
+.display-content .tweets-section.tweets-top .header {
+ background-color: #111;
+
+ background: -moz-linear-gradient(top, rgba(17,17,17,1) 0%, rgba(17,17,17,1) 75%, rgba(17,17,17,0) 100%);
+ background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,rgba(17,17,17,1)), color-stop(75%,rgba(17,17,17,1)), color-stop(100%,rgba(17,17,17,0)));
+ background: -webkit-linear-gradient(top, rgba(17,17,17,1) 0%,rgba(17,17,17,1) 75%,rgba(17,17,17,0) 100%);
+ background: -o-linear-gradient(top, rgba(17,17,17,1) 0%,rgba(17,17,17,1) 75%,rgba(17,17,17,0) 100%);
+ background: -ms-linear-gradient(top, rgba(17,17,17,1) 0%,rgba(17,17,17,1) 75%,rgba(17,17,17,0) 100%);
+ background: linear-gradient(top, rgba(17,17,17,1) 0%,rgba(17,17,17,1) 75%,rgba(17,17,17,0) 100%);
+ filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#111111', endColorstr='#00111111',GradientType=0 );
+
+
+ position: fixed;
+ z-index: 10;
+ width: 512px;
+
+ padding: 20px 22px 15px;
+ margin: -21px -15px;
+ margin-left: -20px;
+ padding-right: 6px;
+
+/* border-top: 1px solid #CCC;
+ border-left: 1px solid #CCC;
+ border-right: 1px solid #CCC;
+*/
+/* -webkit-border-radius: 5px 5px 0px 0px;
+ -moz-border-radius: 5px 5px 0px 0px;
+ -border-radius: 5px 5px 0px 0px;
+*/
+
+}
+
+
+
+.display-content .tweets-section.tweets-top .tweets {
+ margin-top: 35px;
+}
+
+
+.display-content .tweets-section h3 {
+ color: #999;
+ font-size: 20px;
+ font-weight: bold;
+}
+
+.display-content .tweets-section .tweets > li {
+ min-height: 60px;
+ padding-top: 20px;
+ padding-bottom: 20px;
+
+ border-bottom: 1px solid #222;
+}
+
+.display-content .tweets-section .tweets > li:last-child {
+ border-bottom: none;
+}
+
+.display-content .tweets-section .tweets > li:hover {
+ background-color: transparent;
+}
+
+.display-content .tweets-section .tweets > li p {
+ color: #FFF;
+}
+
+.display-content .tweets-section.tweets-current .tweets li p {
+ font-size: 30px;
+ line-height: 40px;
+}
+
+.display-content .tweets-section .tweets li .tweet-stats {
+ margin-bottom: 60px;
+}
+
+.display-content .tweets-current .tweets li .tweet-stats {
+ margin-top: 5px;
+}
+
+.display-content .tweets-section .tweets li .tweet-stats span {
+ font-size: 20px;
+ padding: 5px 6px 5px 5px;
+}
+
+.display-content .tweets-section .tweets li .tweet-metadata .tweet-favorites ul.users li a img {
+ height: 25px;
+ width: 25px;
+ margin-right: 5px;
+}
+
+.display-content .tweets-section .tweets li .tweet-metadata .tweet-actions .actions .action-link a {
+ font-size: 20px;
+ color: #999;
+}
+
+
+/* comment out to show favorites per section on the display page */
+.display-content .tweets-section.tweets-current .tweets li .tweet-metadata .tweet-favorites,
+.display-content .tweets-section.tweets-top .tweets li .tweet-metadata .tweet-favorites,
+
+.display-content .tweets-section .tweets li .tweet-metadata .tweet-actions .actions .action-link .bullet,
+.display-content .tweets-section .tweets li .tweet-metadata .tweet-actions .actions .action-favorite {
+ display: none;
+}
5 public/css/filters.svg
@@ -0,0 +1,5 @@
+<svg xmlns="http://www.w3.org/2000/svg">
+ <filter id=" ">
+ <feColorMatrix type="matrix" values="0.3333 0.3333 0.3333 0 0 0.3333 0.3333 0.3333 0 0 0.3333 0.3333 0.3333 0 0 0 0 0 1 0"/>
+ </filter>
+</svg>
BIN  public/img/apple-touch-icon-114x114.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
BIN  public/img/apple-touch-icon-72x72.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
BIN  public/img/apple-touch-icon.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
BIN  public/img/coffman.gif
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
BIN  public/img/couch.gif
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
BIN  public/img/favicon.ico
Binary file not shown
BIN  public/img/hamid.gif
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
BIN  public/img/hechanova.gif
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
BIN  public/img/leibrock.gif
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
BIN  public/img/linen_dark.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
BIN  public/img/linen_light.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
460 public/js/application.js
@@ -0,0 +1,460 @@
+/*
+ * Copyright 2012 Twitter, Inc.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+var Ospriet = Ospriet || {};
+
+!function(exports) {
+
+ var Timeline = {}
+ , TopTweets = {}
+ , CurrentQuestion = {}
+ , mixins = {}
+ , refresh_interval
+ , updated = new Date();
+
+ exports.socket = io.connect('/', {
+ 'connect timeout': 5000,
+ 'reconnect': true,
+ 'reconnection delay': 500,
+ 'reopen delay': 500,
+ 'max reconnection attempts': 10
+ });
+
+ exports.init = function () {
+
+ moment.relativeTime = {
+ future: "in %s",
+ past: "%s",
+ s: "%ds",
+ m: "1m",
+ mm: "%dm",
+ h: "1h",
+ hh: "%dh",
+ d: "1 day ago",
+ dd: "%d days ago",
+ M: "1 month ago",
+ MM: "%d months ago",
+ y: "1 year ago",
+ yy: "%d years ago"
+ };
+
+ Timeline = Ospriet.timeline();
+ TopTweets = Ospriet.toptweets();
+ CurrentQuestion = Ospriet.currentquestion();
+
+ Timeline.init();
+ TopTweets.init();
+ CurrentQuestion.init();
+
+ $('ul.participants li').tooltip({delay: 0});
+
+ // change interval time to be much shorter if on display page
+ refresh_interval = ($('.display-content').length > 0) ? 5 : 30
+
+ // update timestamps every 5s
+ var timestamps = setInterval(Timeline.updateTimestamps, 5000)
+ // update content manually every 30s just in case websockets fail
+ , refresh = setInterval(Ospriet.updateAll, (refresh_interval * 1000))
+
+ // update content if tab had fallen out of focus
+ $(document).bind("webkitvisibilitychange", function(){
+ if(!document.webkitHidden) Ospriet.updateAll();
+ });
+ }
+
+ exports.updateAll = function () {
+
+ // reload the content if it's been longer than 30s since the last manual refresh
+
+ var now = new Date()
+ , refresh = updated.getTime() + (refresh_interval * 1000)
+
+ if (refresh < now.getTime()) {
+ Timeline.query();
+ TopTweets.query();
+ CurrentQuestion.query();
+ updated = now;
+ }
+ }
+
+ exports.extend = function (obj) {
+ var args = Array.prototype.slice.call(arguments, 1)
+ , l = args.length
+ , i = 0
+ , prop
+ , source
+
+ for (; i < l; i++) {
+ source = args[i]
+ for (prop in source) {
+ if (source[prop] !== void 0) {
+ obj[prop] = source[prop]
+ }
+ }
+ }
+
+ return obj
+ }
+
+ !function (exports) {
+ exports.base = {
+ init: function () {
+ this.query();
+ }
+ , query: function () {
+ $.ajax({
+ url: 'api/'+this.data_url
+ , dataType: 'json'
+ , success: $.proxy(function (data) {
+ this.update(data);
+ }, this)
+ })
+ }
+
+ , renderTweetHTML: function (txt) {
+ return twttr.txt.autoLink(txt)
+ }
+
+ , setTweetData: function (tweet) {
+ if(this.trim_question) tweet.status.text = tweet.status.text.slice(4)
+
+ return {
+ tweet_id: tweet.status_id
+ , tweet_html: this.renderTweetHTML(tweet.status.text)
+ , tweet_user: tweet.status.user.screen_name
+ , tweet_time: tweet.status.created_at
+ , tweet_time_clean: moment(tweet.status.created_at).format('hh:mma dddd MMM D YYYY')
+ , tweet_time_rel: moment(tweet.status.created_at).fromNow(true)
+ }
+ }
+ }
+ }(mixins);
+
+ !function (exports) {
+
+ var Timeline = function () {
+ this.initialized = false
+ this.trim_question = true
+ this.data_url = 'feed'
+ this.dom_container = '.tweets-feed'
+ this.templates = {
+ empty: Hogan.compile('<p class="empty">No questions have been {{action}} yet.</p>')
+ , tweet: Hogan.compile('<li><div class="tweet-content tweet-block"> <p>{{{tweet_html}}}</p></div><div class="tweet-metadata tweet-block"><div class="tweet-actions pull-right"> <ul class="actions"><li class="action-link"><a class="action-permalink" href="https://twitter.com/{{tweet_user}}/status/{{tweet_id}}" data-time-raw="{{tweet_time}}" title="{{tweet_time_clean}}" target="_blank">{{tweet_time_rel}}</a><span class="bullet">&nbsp;&nbsp;&bull;&nbsp;&nbsp;</span></li> <li class="action-link"><a class="action-favorite" href="https://twitter.com/intent/favorite?tweet_id={{tweet_id}}">&#9733;&nbsp;Favorite</a></li> </ul> </div></div></li>')
+ }
+
+ Ospriet.socket
+ .on('tweet', $.proxy(function(data){
+ this.insert(data)
+ }, this))
+
+ .on('deletion', $.proxy(function(data){
+ this.query()
+ }, this));
+
+ }
+
+ Timeline.prototype = Ospriet.extend({}, mixins.base);
+
+ Timeline.prototype.update = function (tweets) {
+
+ var html = []
+ , tweet_data
+ , tweets_container = $(this.dom_container+' .tweets');
+
+ tweets = this.sortTweets(tweets);
+
+ if(tweets.length > 0){
+
+ _.each(tweets, $.proxy(function(tweet) {
+ tweet_data = this.setTweetData(tweet)
+ html.push(this.templates.tweet.render(tweet_data))
+ }, this))
+
+ if(this.initialized) {
+ $(this.dom_container+' p.empty').remove()
+ }
+
+ tweets_container
+ .html(html.join(