diff --git a/Gemfile b/Gemfile
index eecfddd925508b86b4f44682696ebc96b0a881c1..6efc0df6cebd1bd689e72fa6079301173bfdd284 100644
--- a/Gemfile
+++ b/Gemfile
@@ -17,6 +17,7 @@ gem 'rack-cors', '~> 0.2.4', :require => 'rack/cors'
 gem 'devise', '1.5.3'
 gem 'jwt'
 gem 'oauth2-provider', '0.0.19'
+gem 'remotipart', '~> 1.0'
 
 gem 'omniauth', '1.0.1'
 gem 'omniauth-facebook'
diff --git a/Gemfile.lock b/Gemfile.lock
index e97cd5e605a9468a0c574d36f5b0793b89753d49..d3e313444b73d44691b7a60b0feca57eae00569c 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -162,7 +162,7 @@ GEM
     fixture_builder (0.3.1)
       activerecord (>= 2)
       activesupport (>= 2)
-    fog (1.2.0)
+    fog (1.3.0)
       builder
       excon (~> 0.12.0)
       formatador (~> 0.2.0)
@@ -341,6 +341,7 @@ GEM
     redis (2.2.2)
     redis-namespace (1.0.3)
       redis (< 3.0.0)
+    remotipart (1.0.2)
     resque (1.20.0)
       multi_json (~> 1.0)
       redis-namespace (~> 1.0.2)
@@ -513,6 +514,7 @@ DEPENDENCIES
   rails-i18n
   rails_autolink
   redcarpet (= 2.0.1)
+  remotipart (~> 1.0)
   resque (= 1.20.0)
   resque-timeout (= 1.0.0)
   rest-client (= 1.6.7)
diff --git a/app/controllers/photos_controller.rb b/app/controllers/photos_controller.rb
index c2d1978da9acb6a8b906a6a1156f7dbe5c77d51c..a419bccb0bc0b93e8a4948c1b87849c94a32b584 100644
--- a/app/controllers/photos_controller.rb
+++ b/app/controllers/photos_controller.rb
@@ -41,55 +41,19 @@ class PhotosController < ApplicationController
   end
 
   def create
-    begin
-      raise unless params[:photo][:aspect_ids]
-
-      if params[:photo][:aspect_ids] == "all"
-        params[:photo][:aspect_ids] = current_user.aspects.collect{|x| x.id}
-      elsif params[:photo][:aspect_ids].is_a?(Hash)
-        params[:photo][:aspect_ids] = params[:photo][:aspect_ids].values
-      end
-
-      params[:photo][:user_file] = file_handler(params)
-
-      @photo = current_user.build_post(:photo, params[:photo])
-
-      if @photo.save
-
-        aspects = current_user.aspects_from_ids(params[:photo][:aspect_ids])
-
-        unless @photo.pending
-          current_user.add_to_streams(@photo, aspects)
-          current_user.dispatch_post(@photo, :to => params[:photo][:aspect_ids])
-        end
-
-        if params[:photo][:set_profile_photo]
-          profile_params = {:image_url => @photo.url(:thumb_large),
-                           :image_url_medium => @photo.url(:thumb_medium),
-                           :image_url_small => @photo.url(:thumb_small)}
-          current_user.update_profile(profile_params)
-        end
-
-        respond_to do |format|
-          format.json{ render(:layout => false , :json => {"success" => true, "data" => @photo}.to_json )}
-          format.html{ render(:layout => false , :json => {"success" => true, "data" => @photo}.to_json )}
+    rescuing_photo_errors do
+      if remotipart_submitted?
+        @photo = current_user.build_post(:photo, params[:photo])
+        if @photo.save
+          respond_to do |format|
+            format.json { render :json => {"success" => true, "data" => @photo.as_api_response(:backbone)} }
+          end
+        else
+          respond_with @photo, :location => photos_path, :error => message
         end
       else
-        respond_with @photo, :location => photos_path, :error => message
+        legacy_create
       end
-
-    rescue TypeError
-      message = I18n.t 'photos.create.type_error'
-      respond_with @photo, :location => photos_path, :error => message
-
-    rescue CarrierWave::IntegrityError
-      message = I18n.t 'photos.create.integrity_error'
-      respond_with @photo, :location => photos_path, :error => message
-
-    rescue RuntimeError => e
-      message = I18n.t 'photos.create.runtime_error'
-      respond_with @photo, :location => photos_path, :error => message
-      raise e
     end
   end
 
@@ -200,4 +164,57 @@ class PhotosController < ApplicationController
       file
     end
   end
+
+  def legacy_create
+    if params[:photo][:aspect_ids] == "all"
+      params[:photo][:aspect_ids] = current_user.aspects.collect { |x| x.id }
+    elsif params[:photo][:aspect_ids].is_a?(Hash)
+      params[:photo][:aspect_ids] = params[:photo][:aspect_ids].values
+    end
+
+    params[:photo][:user_file] = file_handler(params)
+
+    @photo = current_user.build_post(:photo, params[:photo])
+
+    if @photo.save
+      aspects = current_user.aspects_from_ids(params[:photo][:aspect_ids])
+
+      unless @photo.pending
+        current_user.add_to_streams(@photo, aspects)
+        current_user.dispatch_post(@photo, :to => params[:photo][:aspect_ids])
+      end
+
+      if params[:photo][:set_profile_photo]
+        profile_params = {:image_url => @photo.url(:thumb_large),
+                          :image_url_medium => @photo.url(:thumb_medium),
+                          :image_url_small => @photo.url(:thumb_small)}
+        current_user.update_profile(profile_params)
+      end
+
+      respond_to do |format|
+        format.json{ render(:layout => false , :json => {"success" => true, "data" => @photo}.to_json )}
+        format.html{ render(:layout => false , :json => {"success" => true, "data" => @photo}.to_json )}
+      end
+    else
+      respond_with @photo, :location => photos_path, :error => message
+    end
+  end
+
+  def rescuing_photo_errors
+    begin
+      yield
+    rescue TypeError
+      message = I18n.t 'photos.create.type_error'
+      respond_with @photo, :location => photos_path, :error => message
+
+    rescue CarrierWave::IntegrityError
+      message = I18n.t 'photos.create.integrity_error'
+      respond_with @photo, :location => photos_path, :error => message
+
+    rescue RuntimeError => e
+      message = I18n.t 'photos.create.runtime_error'
+      respond_with @photo, :location => photos_path, :error => message
+      raise e
+    end
+  end
 end
diff --git a/app/controllers/posts_controller.rb b/app/controllers/posts_controller.rb
index 667eb51a49125d20f789e59612ce22f90b13955b..128c7cc4f277878b9ecf41e5f38191391abb0444 100644
--- a/app/controllers/posts_controller.rb
+++ b/app/controllers/posts_controller.rb
@@ -8,11 +8,18 @@ class PostsController < ApplicationController
   before_filter :authenticate_user!, :except => :show
   before_filter :set_format_if_malformed_from_status_net, :only => :show
 
+  layout 'post'
+
   respond_to :html,
              :mobile,
              :json,
              :xml
 
+  def new
+    redirect_to "/stream" and return unless FeatureFlags.new_publisher
+    render :text => "", :layout => true
+  end
+
   def show
     key = params[:id].to_s.length <= 8 ? :id : :guid
 
@@ -34,7 +41,7 @@ class PostsController < ApplicationController
         format.xml{ render :xml => @post.to_diaspora_xml }
         format.mobile{render 'posts/show.mobile.haml'}
         format.json{ render :json => PostPresenter.new(@post, current_user).to_json }
-        format.any{render 'posts/show.html.haml', :layout => 'layouts/post'}
+        format.any{render 'posts/show.html.haml'}
       end
 
     else
diff --git a/app/controllers/status_messages_controller.rb b/app/controllers/status_messages_controller.rb
index 5953993a925886268f86e03d565d8ec4bdac76da..b6764b890a61c456607587c7b1635797e44c5878 100644
--- a/app/controllers/status_messages_controller.rb
+++ b/app/controllers/status_messages_controller.rb
@@ -53,7 +53,8 @@ class StatusMessagesController < ApplicationController
       receiving_services = Service.titles(services)
 
       current_user.dispatch_post(@status_message, :url => short_post_url(@status_message.guid), :service_types => receiving_services)
-      
+
+      #this is done implicitly, somewhere else, apparently, says max. :'(
       # @status_message.photos.each do |photo|
       #   current_user.dispatch_post(photo)
       # end
diff --git a/app/models/feature_flags.rb b/app/models/feature_flags.rb
new file mode 100644
index 0000000000000000000000000000000000000000..ef0ae3fc5c80149fae90acccbd7c5d607241e023
--- /dev/null
+++ b/app/models/feature_flags.rb
@@ -0,0 +1,5 @@
+module FeatureFlags
+  def self.new_publisher
+    !(Rails.env.production? || Rails.env.staging?)
+  end
+end
\ No newline at end of file
diff --git a/app/models/photo.rb b/app/models/photo.rb
index fa76787c51b4f4363a603804a5c3a77afe4a7710..0f3f4aa929991a5a88981b7f2ceeb41b93f53fab 100644
--- a/app/models/photo.rb
+++ b/app/models/photo.rb
@@ -9,8 +9,6 @@ class Photo < ActiveRecord::Base
   include Diaspora::Commentable
   include Diaspora::Shareable
 
-
-
   # NOTE API V1 to be extracted
   acts_as_api
   api_accessible :backbone do |t|
diff --git a/app/models/service.rb b/app/models/service.rb
index 9eab8442a7f90da5a432a12797079a80c71bc72d..8f0ed9d1c37f9bf5e1b67fa80a55cf23afe0b692 100644
--- a/app/models/service.rb
+++ b/app/models/service.rb
@@ -14,6 +14,7 @@ class Service < ActiveRecord::Base
   end
 
   def public_message(post, length, url = "")
+    Rails.logger.info("Posting out to #{self.class}")
     url = "" if post.respond_to?(:photos) && post.photos.count == 0
     space_for_url = url.blank? ? 0 : (url.length + 1)
     truncated = truncate(post.text(:plain_text => true), :length => (length - space_for_url))
diff --git a/app/models/status_message.rb b/app/models/status_message.rb
index 289150a87458bb6cc0de419853fd9e93de8d8b60..aad453ac1f1fd71bba7ef5dc27da11f2033a7777 100644
--- a/app/models/status_message.rb
+++ b/app/models/status_message.rb
@@ -21,7 +21,7 @@ class StatusMessage < Post
   # therefore, we put the validation in a before_destory callback instead of a validation
   before_destroy :presence_of_content
 
-  attr_accessible :text, :provider_display_name
+  attr_accessible :text, :provider_display_name, :frame_name
   attr_accessor :oembed_url
 
   after_create :create_mentions
diff --git a/app/presenters/aspect_presenter.rb b/app/presenters/aspect_presenter.rb
new file mode 100644
index 0000000000000000000000000000000000000000..881be4b3a100f50c342a6f5392f89522562488d9
--- /dev/null
+++ b/app/presenters/aspect_presenter.rb
@@ -0,0 +1,15 @@
+class AspectPresenter < BasePresenter
+  def initialize(aspect)
+    @aspect = aspect
+  end
+
+  def as_json
+    { :id => @aspect.id,
+      :name => @aspect.name,
+    }
+  end
+
+  def to_json(options = {})
+    as_json.to_json(options)
+  end
+end
\ No newline at end of file
diff --git a/app/presenters/base_presenter.rb b/app/presenters/base_presenter.rb
new file mode 100644
index 0000000000000000000000000000000000000000..8f83962e9a185cb9cf10889167da204b2b4c8ac2
--- /dev/null
+++ b/app/presenters/base_presenter.rb
@@ -0,0 +1,5 @@
+class BasePresenter
+  def self.as_collection(collection)
+    collection.map{|object| self.new(object).as_json}
+  end
+end
diff --git a/app/presenters/post_presenter.rb b/app/presenters/post_presenter.rb
index f8e1febf54a2e6c4bdb6e7afbf02191be6d13218..0f798494744f2936201bf105a255cc9e5dc335b5 100644
--- a/app/presenters/post_presenter.rb
+++ b/app/presenters/post_presenter.rb
@@ -23,7 +23,7 @@ class PostPresenter
         :reshares => self.reshares,
         :comments => self.comments,
         :participations => self.participations,
-        :templateName => template_name,
+        :frame_name => self.post.frame_name || template_name,
         :title => title
       })
   end
diff --git a/app/presenters/service_presenter.rb b/app/presenters/service_presenter.rb
new file mode 100644
index 0000000000000000000000000000000000000000..f44ad8eb7903c5f605532bca16f91055b64fc54d
--- /dev/null
+++ b/app/presenters/service_presenter.rb
@@ -0,0 +1,15 @@
+class ServicePresenter < BasePresenter
+  def initialize(service)
+    @service = service
+  end
+
+  def as_json
+    {
+      :provider => @service.provider
+    }
+  end
+
+  def to_json(options = {})
+    as_json.to_json(options)
+  end
+end
\ No newline at end of file
diff --git a/app/presenters/user_presenter.rb b/app/presenters/user_presenter.rb
index 38436efd5a22403f2d6cb020fdea4f13f3c26ace..8fd5af0a191862645f9862506c5f7c401d17c4cb 100644
--- a/app/presenters/user_presenter.rb
+++ b/app/presenters/user_presenter.rb
@@ -1,6 +1,6 @@
 class UserPresenter
   attr_accessor :user
-  
+
   def initialize(user)
     self.user = user
   end
@@ -9,15 +9,23 @@ class UserPresenter
     self.user.person.as_api_response(:backbone).update(
       { :notifications_count => notifications_count,
         :unread_messages_count => unread_messages_count,
-        :admin => admin
+        :admin => admin,
+        :aspects => aspects,
+        :services => services
       }
     ).to_json(options)
   end
 
-  protected
+  def services
+    ServicePresenter.as_collection(user.services)
+  end
+
+  def aspects
+    AspectPresenter.as_collection(user.aspects)
+  end
 
   def notifications_count
-    @notification_count ||= user.unread_notifications.count 
+    @notification_count ||= user.unread_notifications.count
   end
 
   def unread_messages_count
@@ -27,4 +35,4 @@ class UserPresenter
   def admin
     user.admin?
   end
-end
\ No newline at end of file
+end
diff --git a/app/views/layouts/post.html.haml b/app/views/layouts/post.html.haml
index bc10e192a3b9cdcf2a425fb59708ec961c87d768..b2a2e4d0a043c769cbd1aad4df53784fe43de078 100644
--- a/app/views/layouts/post.html.haml
+++ b/app/views/layouts/post.html.haml
@@ -50,5 +50,6 @@
 
   %body
     = flash_messages
+    #container
 
     = yield
diff --git a/app/views/photos/_new_photo.haml b/app/views/photos/_new_photo.haml
index 4da494f18861e0c3a665b7c713bf02794a0c5df7..95fa60e27b5fa8a9308ee2ab80df1d41ccf6c6b7 100644
--- a/app/views/photos/_new_photo.haml
+++ b/app/views/photos/_new_photo.haml
@@ -83,4 +83,4 @@
    });
  }
 
- createUploader();
+ createUploader();
\ No newline at end of file
diff --git a/app/views/posts/show.html.haml b/app/views/posts/show.html.haml
index ad90ddc2ee1a49522bf9caa4dacd7c899506b433..5720b3364cf69a6165f2e541f0698630f39c4a2f 100644
--- a/app/views/posts/show.html.haml
+++ b/app/views/posts/show.html.haml
@@ -4,5 +4,3 @@
 
 - content_for :page_title do
   = post_page_title @post
-
-#container
diff --git a/config/assets.yml b/config/assets.yml
index e2a445ec6a71440a32aaff5ac398643702e98bca..e7086b92b16730b5c9a8c798752f43fed64f82cc 100644
--- a/config/assets.yml
+++ b/config/assets.yml
@@ -25,12 +25,15 @@ javascripts:
     - public/javascripts/vendor/timeago.js
     - public/javascripts/vendor/facebox.js
     - public/javascripts/vendor/underscore.js
+    - public/javascripts/vendor/jquery.iframe-transport.js
+    - public/javascripts/vendor/jquery.remotipart.js
     - public/javascripts/vendor/jquery.events.input.js
     - public/javascripts/vendor/jquery.elastic.js
     - public/javascripts/vendor/jquery.mentionsInput.js
     - public/javascripts/vendor/jquery.idle-timer.js
     - public/javascripts/jquery.infinitescroll-custom.js
     - public/javascripts/jquery.autocomplete-custom.js
+    - public/javascripts/vendor/jquery.textchange.min.js
     - public/javascripts/keycodes.js
     - public/javascripts/fileuploader-custom.js
 
@@ -43,6 +46,7 @@ javascripts:
     - public/javascripts/app/helpers/*
     - public/javascripts/app/router.js
     - public/javascripts/app/views.js
+    - public/javascripts/app/forms.js
     - public/javascripts/app/models/post.js
     - public/javascripts/app/models/*
     - public/javascripts/app/pages/*
@@ -51,6 +55,7 @@ javascripts:
     - public/javascripts/app/views/content_view.js
     - public/javascripts/app/views/*.js
     - public/javascripts/app/views/**/*.js
+    - public/javascripts/app/forms/*.js
 
     - public/javascripts/diaspora.js
     - public/javascripts/helpers/*.js
@@ -71,6 +76,7 @@ javascripts:
 
     - public/javascripts/vendor/bootstrap/bootstrap-twipsy.js
     - public/javascripts/vendor/bootstrap/bootstrap-popover.js
+    - public/javascripts/vendor/bootstrap/bootstrap-dropdown.js
 
   login:
     - public/javascripts/login.js
@@ -90,7 +96,6 @@ javascripts:
     - public/javascripts/friend-finder.js
   home:
     - public/javascripts/publisher.js
-    - public/javascripts/vendor/jquery.textchange.min.js
     - public/javascripts/aspect-edit-pane.js
     - public/javascripts/fileuploader-custom.js
   people:
diff --git a/config/routes.rb b/config/routes.rb
index 59fe1a02733216baa503055287eba984509852de..d6063f3d6660fc3f526c0c700fb48e60d717617b 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -8,11 +8,14 @@ Diaspora::Application.routes.draw do
 
   resources :status_messages, :only => [:new, :create]
 
-  resources :posts, :only => [:show, :destroy] do
+  resources :posts, :only => [:show, :new, :destroy] do
     resources :likes, :only => [:create, :destroy, :index]
     resources :participations, :only => [:create, :destroy, :index]
     resources :comments, :only => [:new, :create, :destroy, :index]
   end
+
+  match "/framer" => redirect("/posts/new")
+
   get 'p/:id' => 'posts#show', :as => 'short_post'
   # roll up likes into a nested resource above
   resources :comments, :only => [:create, :destroy] do
@@ -32,7 +35,6 @@ Diaspora::Application.routes.draw do
   get "commented" => "streams#commented", :as => "commented_stream"
   get "aspects" => "streams#aspects", :as => "aspects_stream"
   
-
   resources :aspects do
     put :toggle_contact_visibility
   end
diff --git a/db/migrate/20120322223517_add_template_name_to_posts.rb b/db/migrate/20120322223517_add_template_name_to_posts.rb
new file mode 100644
index 0000000000000000000000000000000000000000..cedd5951c00ed98bd4bacca148908850466f62ee
--- /dev/null
+++ b/db/migrate/20120322223517_add_template_name_to_posts.rb
@@ -0,0 +1,5 @@
+class AddTemplateNameToPosts < ActiveRecord::Migration
+  def change # thanks josh susser
+    add_column :posts, :frame_name, :string
+  end
+end
diff --git a/db/schema.rb b/db/schema.rb
index f662bf314c9807b39e486cb9b5cf6b496fe0ea7a..1f2bfd100e2ca3c7f01d4e1dd980d916bdb1c70b 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -11,7 +11,7 @@
 #
 # It's strongly recommended to check this file into your version control system.
 
-ActiveRecord::Schema.define(:version => 20120301143226) do
+ActiveRecord::Schema.define(:version => 20120322223517) do
 
   create_table "account_deletions", :force => true do |t|
     t.string  "diaspora_handle"
@@ -334,6 +334,7 @@ ActiveRecord::Schema.define(:version => 20120301143226) do
     t.integer  "o_embed_cache_id"
     t.integer  "reshares_count",                      :default => 0
     t.datetime "interacted_at"
+    t.string   "frame_name"
   end
 
   add_index "posts", ["author_id", "root_guid"], :name => "index_posts_on_author_id_and_root_guid", :unique => true
diff --git a/features/notifications.feature b/features/notifications.feature
index 78871ea5712ba4786f366b02ea9390cd06ec25b5..fe91d5817970dad1b6504bd2aba3a424ce953669 100644
--- a/features/notifications.feature
+++ b/features/notifications.feature
@@ -64,13 +64,10 @@ Feature: Notifications
     And I focus the comment field
     And I fill in "text" with "great post!"
     And I press "Comment"
-    And I wait for the ajax to finish
     And I go to the destroy user session page
     When I sign in as "alice@alice.alice"
     And I follow "Notifications" in the header
-    And I wait for the ajax to finish
     Then the notification dropdown should be visible
-    And I wait for the ajax to finish
     Then I should see "commented on your post"
     And I should have 1 email delivery
 
diff --git a/features/step_definitions/stream_steps.rb b/features/step_definitions/stream_steps.rb
index d508ebc58ccd9d773a6c5fec4100a95ed5a4259e..ec173268077e04636cf7d53dda8be14ddbc51264 100644
--- a/features/step_definitions/stream_steps.rb
+++ b/features/step_definitions/stream_steps.rb
@@ -16,4 +16,10 @@ end
 
 Then /^I should have (\d+) nsfw posts$/ do |num_posts|
   all(".nsfw-shield").size.should == num_posts.to_i
+end
+
+When /^I click the show page link for "([^"]*)"$/ do |post_text|
+  within(find_post_by_text(post_text)) do
+    find("time").click
+  end
 end
\ No newline at end of file
diff --git a/features/step_definitions/trumpeter_steps.rb b/features/step_definitions/trumpeter_steps.rb
new file mode 100644
index 0000000000000000000000000000000000000000..56d58bc598681b2d28444d538e4895536188cbcc
--- /dev/null
+++ b/features/step_definitions/trumpeter_steps.rb
@@ -0,0 +1,120 @@
+def fill_in_autocomplete(selector, value)
+  pending #make me work if yr board, investigate send_keys
+  page.execute_script %Q{$('#{selector}').val('#{value}').keyup()}
+end
+
+def aspects_dropdown
+  find(".dropdown-toggle")
+end
+
+def select_from_dropdown(option_text, dropdown)
+  dropdown.click
+  within ".dropdown-menu" do
+    link = find("a:contains('#{option_text}')")
+    link.should be_visible
+    link.click
+  end
+  #assert dropdown text is link
+end
+
+def go_to_framer
+  click_button "Next"
+end
+
+def finalize_frame
+  click_button "done"
+end
+
+def assert_post_renders_with(template_name)
+  find(".post")["data-template"].should == template_name.downcase
+end
+
+def find_image_by_filename(filename)
+  find("img[src='#{@image_sources[filename]}']")
+end
+
+def store_image_filename(file_name)
+  @image_sources ||= {}
+  @image_sources[file_name] = all(".photos img").last["src"]
+  @image_sources[file_name].should be_present
+end
+
+def upload_photo(file_name)
+  orig_photo_count = all(".photos img").size
+
+  within ".new_photo" do
+    attach_file "photo[user_file]", Rails.root.join("spec", "fixtures", file_name)
+    wait_until { all(".photos img").size == orig_photo_count + 1 }
+  end
+
+  store_image_filename(file_name)
+end
+
+When /^I trumpet$/ do
+  visit new_post_path
+end
+
+When /^I write "([^"]*)"$/ do |text|
+  fill_in 'text', :with => text
+end
+
+Then /I mention "([^"]*)"$/ do |text|
+  fill_in_autocomplete('textarea.text', '@a')
+  sleep(5)
+  find("li.active").click
+end
+
+When /^I select "([^"]*)" in my aspects dropdown$/ do |title|
+  within ".aspect_selector" do
+    select_from_dropdown(title, aspects_dropdown)
+  end
+end
+
+Then /^"([^"]*)" should be a (limited|public) post in my stream$/ do |post_text, scope|
+  find_post_by_text(post_text).find(".post_scope").text.should =~ /#{scope}/i
+end
+
+When /^I upload a fixture picture with filename "([^"]*)"$/ do |file_name|
+  upload_photo(file_name)
+end
+
+Then /^"([^"]*)" should have the "([^"]*)" picture$/ do |post_text, file_name|
+  within find_post_by_text(post_text) do
+    find_image_by_filename(file_name).should be_present
+  end
+end
+
+When /^I go through the default composer$/ do
+  go_to_framer
+  finalize_frame
+end
+
+When /^I start the framing process$/ do
+  go_to_framer
+end
+
+When /^I finalize my frame$/ do
+  finalize_frame
+end
+
+Then /^"([^"]*)" should have (\d+) pictures$/ do |post_text, number_of_pictures|
+  find_post_by_text(post_text).all(".photo_attachments img").size.should == number_of_pictures.to_i
+end
+
+Then /^I should see "([^"]*)" in the framer preview$/ do |post_text|
+  within(find(".post")) { page.should have_content(post_text) }
+end
+
+When /^I select the mood "([^"]*)"$/ do |template_name|
+  select template_name, :from => 'template'
+end
+
+Then /^the post's mood should (?:still |)be "([^"]*)"$/ do |template_name|
+  assert_post_renders_with(template_name)
+end
+
+When /^"([^"]*)" should be in the post's picture viewer$/ do |file_name|
+  within(".photo_viewer") do
+    find_image_by_filename(file_name).should be_present
+  end
+end
\ No newline at end of file
diff --git a/features/trumpeter.feature b/features/trumpeter.feature
new file mode 100644
index 0000000000000000000000000000000000000000..cbeb8a45cf2965cce54cd7cda0d0e0328d72e948
--- /dev/null
+++ b/features/trumpeter.feature
@@ -0,0 +1,59 @@
+@javascript
+Feature: Creating a new post
+  Background:
+    Given a user with username "bob"
+    And I sign in as "bob@bob.bob"
+    And I trumpet
+
+  Scenario: Posting a public message with a photo
+    And I write "I love RMS"
+    When I select "Public" in my aspects dropdown
+    And I upload a fixture picture with filename "button.gif"
+    When I go through the default composer
+    When I go to "/stream"
+    Then I should see "I love RMS" as the first post in my stream
+    And "I love RMS" should be a public post in my stream
+    Then "I love RMS" should have the "button.gif" picture
+
+  Scenario: Posting to Aspects
+    And I write "This is super skrunkle"
+    When I select "All Aspects" in my aspects dropdown
+    And I go through the default composer
+    When I go to "/stream"
+    Then I should see "This is super skrunkle" as the first post in my stream
+    Then "This is super skrunkle" should be a limited post in my stream
+
+  Scenario: Mention a contact
+   Given a user named "Alice Smith" with email "alice@alice.alice"
+   And a user with email "bob@bob.bob" is connected with "alice@alice.alice"
+   And I mention "alice@alice.alice"
+   And I go through the default composer
+   And I go to "/stream"
+   Then I follow "Alice Smith"
+
+  Scenario: Uploading multiple photos
+    When I write "check out these pictures"
+    And I upload a fixture picture with filename "button.gif"
+    And I upload a fixture picture with filename "button.gif"
+    And I go through the default composer
+    And I go to "/stream"
+    Then "check out these pictures" should have 2 pictures
+
+  Scenario: Framing your frame
+    When I write "This is hella customized"
+    And I upload a fixture picture with filename "button.gif"
+
+    And I start the framing process
+    Then I should see "This is hella customized" in the framer preview
+  # Then the default mood for the post should be "Wallpaper"
+  # And I should see the image "button.gif" background
+    When I select the mood "Day"
+    Then the post's mood should be "Day"
+    And "button.gif" should be in the post's picture viewer
+    And I should see "This is hella customized" in the framer preview
+
+    When I finalize my frame
+    And I go to "/stream"
+    Then "This is hella customized" should be post 1
+    And I click the show page link for "This is hella customized"
+    And the post's mood should still be "Day"
diff --git a/public/javascripts/app/app.js b/public/javascripts/app/app.js
index 3f019e0f1a3f7e5ebaf04d7d75fef3b8305900c1..962c7a181bb5dce3e0b2265002f6e880b7a61bbc 100644
--- a/public/javascripts/app/app.js
+++ b/public/javascripts/app/app.js
@@ -4,6 +4,7 @@ var app = {
   helpers: {},
   views: {},
   pages: {},
+  forms: {},
 
   user: function(userAttrs) {
     if(userAttrs) { return this._user = new app.models.User(userAttrs) }
diff --git a/public/javascripts/app/forms.js b/public/javascripts/app/forms.js
new file mode 100644
index 0000000000000000000000000000000000000000..a9f3fe10546e626abe9571b758829b1274740b06
--- /dev/null
+++ b/public/javascripts/app/forms.js
@@ -0,0 +1,14 @@
+app.forms.Base = app.views.Base.extend({
+  formSelector : "form",
+
+  initialize : function() {
+    this.setupFormEvents()
+  },
+
+  setupFormEvents : function(){
+    this.events = {}
+    this.events['submit ' + this.formSelector] =  'setModelAttributes';
+    this.delegateEvents();
+  },
+
+})
diff --git a/public/javascripts/app/forms/picture_form.js b/public/javascripts/app/forms/picture_form.js
new file mode 100644
index 0000000000000000000000000000000000000000..f8aaf64fd6725eea5dc9171d63ddb5a0a35abf6d
--- /dev/null
+++ b/public/javascripts/app/forms/picture_form.js
@@ -0,0 +1,40 @@
+app.forms.Picture = app.forms.Base.extend({
+  templateName : "picture-form",
+
+  events : {
+    'ajax:complete .new_photo' : "photoUploaded",
+    "change input[name='photo[user_file]']" : "submitForm"
+  },
+
+  initialize : function() {
+    this.photos = new Backbone.Collection()
+    this.photos.bind("add", this.render, this)
+  },
+
+  postRenderTemplate : function(){
+    this.$("input[name=authenticity_token]").val($("meta[name=csrf-token]").attr("content"))
+    this.$("input[name=photo_ids]").val(this.photos.pluck("id"))
+    this.renderPhotos();
+  },
+
+  submitForm : function (){
+    this.$("form").submit();
+  },
+
+  photoUploaded : function(evt, xhr) {
+    resp = JSON.parse(xhr.responseText)
+    if(resp.success) {
+      this.photos.add(new Backbone.Model(resp.data))
+    } else {
+      alert("Upload failed!  Please try again. " + resp.error);
+    }
+  },
+
+  renderPhotos : function(){
+    var photoContainer = this.$(".photos")
+    this.photos.each(function(photo){
+      var photoView = new app.views.Photo({model : photo}).render().el
+      photoContainer.append(photoView)
+    })
+  }
+});
\ No newline at end of file
diff --git a/public/javascripts/app/forms/post_form.js b/public/javascripts/app/forms/post_form.js
new file mode 100644
index 0000000000000000000000000000000000000000..9acc56ab4d241015a954a31cb37cb6ea07aca3a0
--- /dev/null
+++ b/public/javascripts/app/forms/post_form.js
@@ -0,0 +1,63 @@
+app.forms.Post = app.forms.Base.extend({
+  templateName : "post-form",
+  formSelector : ".new-post",
+
+  subviews : {
+    ".aspect_selector" : "aspectsDropdown",
+    ".service_selector" : "servicesSelector",
+    ".new_picture" : "pictureForm"
+   },
+
+  formAttrs : {
+    "textarea#text_with_markup" : "text",
+    "input.aspect_ids" : "aspect_ids",
+    "input.service:checked" : "services"
+  },
+
+  initialize : function() {
+    this.aspectsDropdown = new app.views.AspectsDropdown();
+    this.servicesSelector = new app.views.ServicesSelector();
+    this.pictureForm = new app.forms.Picture();
+
+    this.setupFormEvents();
+  },
+
+  setModelAttributes : function(evt){
+    if(evt){ evt.preventDefault(); }
+    var form = this.$(this.formSelector);
+
+    this.model.set(_.inject(this.formAttrs, setValueFromField, {}))
+    //pass collections across
+    this.model.photos = this.pictureForm.photos
+    this.model.set({"photos": this.model.photos.toJSON() })
+
+    function setValueFromField(memo, attribute, selector){
+      var selectors = form.find(selector);
+      if(selectors.length > 1) {
+        memo[attribute] = _.map(selectors, function(selector){
+          return $(selector).val()
+        })
+      } else {
+        memo[attribute] = selectors.val();
+      }
+      return memo
+    }
+  },
+
+  postRenderTemplate : function() {
+    this.prepAndBindMentions()
+  },
+
+  prepAndBindMentions : function(){
+    Mentions.initialize(this.$("textarea.text"));
+    Mentions.fetchContacts();
+
+    this.$("textarea.text").bind("textchange", $.proxy(this.updateTextWithMarkup, this))
+  },
+
+  updateTextWithMarkup : function() {
+    this.$("form textarea.text").mentionsInput('val', function(markup){
+      $('#text_with_markup').val(markup);
+    });
+  }
+});
\ No newline at end of file
diff --git a/public/javascripts/app/models/post.js b/public/javascripts/app/models/post.js
index a5beb5ecd358d55eee4e3b82173ae9120146aacf..efe9c9a8c6e74c7803914b0a7b583e169c46ef69 100644
--- a/public/javascripts/app/models/post.js
+++ b/public/javascripts/app/models/post.js
@@ -90,4 +90,19 @@ app.models.Post = Backbone.Model.extend({
       self.trigger('interacted', this)
     }});
   }
+}, {
+
+  frameMoods : [
+    "Day"
+  ],
+
+  legacyTemplateNames : [
+    "status-with-photo-backdrop",
+    "note",
+    "rich-media",
+    "multi-photo",
+    "photo-backdrop",
+    "activity-streams-photo",
+    "status"
+  ]
 });
diff --git a/public/javascripts/app/models/status_message.js b/public/javascripts/app/models/status_message.js
index 7a2c8120aff6913dc05643fa55f65dbd45f23555..d1af3f03eb034d0b077672cf200865943a284d7f 100644
--- a/public/javascripts/app/models/status_message.js
+++ b/public/javascripts/app/models/status_message.js
@@ -1 +1,24 @@
-app.models.StatusMessage = app.models.Post.extend({ });
+app.models.StatusMessage = app.models.Post.extend({
+  url : function(){
+    return this.isNew() ? '/status_messages' : '/posts/' + this.get("id");
+  },
+
+  defaults : {
+    'post_type' : 'StatusMessage',
+    'author' : app.currentUser ? app.currentUser.attributes : {}
+  },
+
+  toJSON : function(){
+    return {
+      status_message : _.clone(this.attributes),
+      aspect_ids : this.get("aspect_ids") && this.get("aspect_ids").split(","),
+      photos : this.photos && this.photos.pluck("id"),
+      services : mungeServices(this.get("services"))
+    }
+
+    function mungeServices (values) {
+      if(!values) { return; }
+      return values.length > 1 ?  values :  [values]
+    }
+  }
+});
diff --git a/public/javascripts/app/pages/framer.js b/public/javascripts/app/pages/framer.js
new file mode 100644
index 0000000000000000000000000000000000000000..e75f3b1ac1c22e9026b952cef51be530031f0806
--- /dev/null
+++ b/public/javascripts/app/pages/framer.js
@@ -0,0 +1,30 @@
+app.pages.Framer = app.views.Base.extend({
+  templateName : "framer",
+
+  id : "post-content",
+
+  events : {
+    "click button.done" : "saveFrame"
+  },
+
+  subviews : {
+    ".post-view" : "postView",
+    ".template-picker" : "templatePicker"
+  },
+
+  initialize : function(){
+    this.model = app.frame
+    this.model.authorIsCurrentUser = function(){ return true }
+
+    this.model.bind("change", this.render, this)
+    this.templatePicker = new app.views.TemplatePicker({ model: this.model })
+  },
+
+  postView : function(){
+    return app.views.Post.showFactory(this.model)
+  },
+
+  saveFrame : function(){
+    this.model.save()
+  }
+})
diff --git a/public/javascripts/app/pages/post-new.js b/public/javascripts/app/pages/post-new.js
new file mode 100644
index 0000000000000000000000000000000000000000..ab8ed72a12eeed5cbeedb0c07575b348f3954fba
--- /dev/null
+++ b/public/javascripts/app/pages/post-new.js
@@ -0,0 +1,20 @@
+app.pages.PostNew = app.views.Base.extend({
+  templateName : "post-new",
+
+  subviews : { "#new-post" : "postForm"},
+
+  events : {
+    "click button.next" : "navigateNext"
+  },
+
+  initialize : function(){
+    this.model = new app.models.StatusMessage()
+    this.postForm = new app.forms.Post({model : this.model})
+  },
+
+  navigateNext : function(){
+    this.postForm.setModelAttributes()
+    app.frame = this.model;
+    app.router.navigate("framer", true)
+  }
+});
diff --git a/public/javascripts/app/pages/post-viewer.js b/public/javascripts/app/pages/post-viewer.js
index f43bf957fc6df10e7a1a83327518d19d11f94486..803dbe384f9ece5bb3e02a004aed18e7372026cf 100644
--- a/public/javascripts/app/pages/post-viewer.js
+++ b/public/javascripts/app/pages/post-viewer.js
@@ -25,12 +25,7 @@ app.pages.PostViewer = app.views.Base.extend({
     this.authorView = new app.views.PostViewerAuthor({ model : this.model });
     this.interactionsView = new app.views.PostViewerInteractions({ model : this.model });
     this.navView = new app.views.PostViewerNav({ model : this.model });
-    this.postView = new app.views.Post({
-      model : this.model,
-      className : this.model.get("templateName") + " post loaded",
-      templateName : "post-viewer/content/" + this.model.get("templateName"),
-      attributes : {"data-template" : this.model.get("templateName")}
-    });
+    this.postView = app.views.Post.showFactory(this.model)
 
     this.render();
   },
diff --git a/public/javascripts/app/router.js b/public/javascripts/app/router.js
index 30d12cc7db8d181a5c6c2eb61b6a14674f9d19df..f45d158e41ce73c84f11ba1538841e0dd66f7d47 100644
--- a/public/javascripts/app/router.js
+++ b/public/javascripts/app/router.js
@@ -16,26 +16,37 @@ app.Router = Backbone.Router.extend({
     "followed_tags": "stream",
     "tags/:name": "stream",
 
+    "posts/new" : "newPost",
     "posts/:id": "singlePost",
-    "p/:id": "singlePost"
+    "p/:id": "singlePost",
+    "framer": "framer"
   },
 
   stream : function() {
     app.stream = new app.models.Stream();
-    app.page = new app.views.Stream({model : app.stream}).render();
+    app.page = new app.views.Stream({model : app.stream});
     app.publisher = app.publisher || new app.views.Publisher({collection : app.stream.posts});
 
-    var streamFacesView = new app.views.StreamFaces({collection : app.stream.posts}).render();
+    var streamFacesView = new app.views.StreamFaces({collection : app.stream.posts});
 
-    $("#main_stream").html(app.page.el);
-    $('#selected_aspect_contacts .content').html(streamFacesView.el);
+    $("#main_stream").html(app.page.render().el);
+    $('#selected_aspect_contacts .content').html(streamFacesView.render().el);
   },
 
   photos : function() {
     app.photos = new app.models.Photos();
-    app.page = new app.views.Photos({model : app.photos}).render();
+    app.page = new app.views.Photos({model : app.photos});
+    $("#main_stream").html(app.page.render().el);
+  },
+
+  newPost : function(){
+    var page = new app.pages.PostNew();
+    $("#container").html(page.render().el)
+  },
 
-    $("#main_stream").html(app.page.el);
+  framer : function(){
+    var page = new app.pages.Framer();
+    $("#container").html(page.render().el)
   },
 
   singlePost : function(id) {
diff --git a/public/javascripts/app/templates/aspects-dropdown.handlebars b/public/javascripts/app/templates/aspects-dropdown.handlebars
new file mode 100644
index 0000000000000000000000000000000000000000..aca5b30d726fa5079c1e83919f8eb432e6c936eb
--- /dev/null
+++ b/public/javascripts/app/templates/aspects-dropdown.handlebars
@@ -0,0 +1,18 @@
+<div class="btn-group aspects_dropdown check-group">
+    <a class="btn btn-info dropdown-toggle" data-toggle="dropdown" href="#">
+        <span class="text"></span> <span class="caret"></span>
+    </a>
+    <ul class="dropdown-menu">
+        <li><i class='icon-ok'/><a href="#" class="public" data-aspect-id="public" data-visibility="public">Public</a></li>
+        <li><i class='icon-ok'/><a href="#" class="all-aspects" data-aspect-id="all_aspects" data-visibility="all-aspects">All Aspects</a></li>
+
+        <li class="divider"></li>
+        {{#each current_user.aspects}}
+            <li><i class='icon-ok'/><a href="#" data-aspect-id="{{id}}"  data-visibility="custom">{{name}}</a></li>
+        {{/each}}
+
+    </ul>
+</div>
+
+<input type="hidden" class="aspect_ids"/>
+
diff --git a/public/javascripts/app/templates/day.handlebars b/public/javascripts/app/templates/day.handlebars
new file mode 100644
index 0000000000000000000000000000000000000000..7a9acc3fc7401fd7d4ab29b52bd3f2582439083d
--- /dev/null
+++ b/public/javascripts/app/templates/day.handlebars
@@ -0,0 +1,2 @@
+<section class="text">{{{text}}}</section>
+<section class="photo_viewer"></section>
diff --git a/public/javascripts/app/templates/framer.handlebars b/public/javascripts/app/templates/framer.handlebars
new file mode 100644
index 0000000000000000000000000000000000000000..ca3e884d346f737f1f9312851db95b6b0df5fa65
--- /dev/null
+++ b/public/javascripts/app/templates/framer.handlebars
@@ -0,0 +1,5 @@
+<div class="controls">
+    <div class='template-picker'></div>
+    <button class="done btn-primary">done</button>
+</div>
+<div class="post-view"></div>
\ No newline at end of file
diff --git a/public/javascripts/app/templates/photo-viewer.handlebars b/public/javascripts/app/templates/photo-viewer.handlebars
new file mode 100644
index 0000000000000000000000000000000000000000..1e83ff5d328ea48a9b874bc060e7f9d90f3c734e
--- /dev/null
+++ b/public/javascripts/app/templates/photo-viewer.handlebars
@@ -0,0 +1,3 @@
+{{#each photos}}
+   <img src="{{sizes.large}}"/>
+{{/each}}
\ No newline at end of file
diff --git a/public/javascripts/app/templates/picture-form.handlebars b/public/javascripts/app/templates/picture-form.handlebars
new file mode 100644
index 0000000000000000000000000000000000000000..665aed54fc47e2d03f5757e56bbd13a0fb7737fc
--- /dev/null
+++ b/public/javascripts/app/templates/picture-form.handlebars
@@ -0,0 +1,8 @@
+<form accept-charset="UTF-8" action="/photos" class="new_photo" data-remote="true" enctype="multipart/form-data" method="post">
+    <input name="authenticity_token" type="hidden"/>
+    <div style="margin:0;padding:0;display:inline">
+        <input name="utf8" type="hidden" value="✓"/>
+    </div>
+    <input name="photo[user_file]" type="file"/>
+    <div class="photos"></div>
+</form>
diff --git a/public/javascripts/app/templates/post-form.handlebars b/public/javascripts/app/templates/post-form.handlebars
new file mode 100644
index 0000000000000000000000000000000000000000..e9f0ee4b608d895d57976bdf86b3ab50c1a31767
--- /dev/null
+++ b/public/javascripts/app/templates/post-form.handlebars
@@ -0,0 +1,24 @@
+<div class='row'>
+    <div class='span8 offset2 new-post-section'>
+
+        <div class="new_picture"/>
+
+        <form class="new-post">
+            <fieldset>
+              <legend>
+                New Post
+              </legend>
+              <label>
+                text
+                <textarea class="text span8"/>
+              </label>
+              <textarea id="text_with_markup" style="display:none;"/>
+
+              <div class="aspect_selector"></div>
+              <div class="service_selector"></div>
+
+              <input type="submit" class="btn-primary" value="Share" />
+            </fieldset>
+        </form>
+    </div>
+</div>
diff --git a/public/javascripts/app/templates/post-new.handlebars b/public/javascripts/app/templates/post-new.handlebars
new file mode 100644
index 0000000000000000000000000000000000000000..ab2b6264ae430eff37f7ce88db9af14ded5adfda
--- /dev/null
+++ b/public/javascripts/app/templates/post-new.handlebars
@@ -0,0 +1,4 @@
+<div class="container">
+  <div id="new-post"></div>
+  <button class="btn-primary next">Next</button>
+</div>
diff --git a/public/javascripts/app/templates/services-selector.handlebars b/public/javascripts/app/templates/services-selector.handlebars
new file mode 100644
index 0000000000000000000000000000000000000000..a8fd3c827c6c73186f04e885a83c7a858978d124
--- /dev/null
+++ b/public/javascripts/app/templates/services-selector.handlebars
@@ -0,0 +1,7 @@
+Broadcast:
+{{#each current_user.services}}
+    <input type="checkbox" name="services" class="service" value="{{provider}}" />
+    <img src="/images/social_media_logos/{{provider}}-16x16.png" />
+    <br />
+{{/each}}
+<br />
diff --git a/public/javascripts/app/templates/stream-element.handlebars b/public/javascripts/app/templates/stream-element.handlebars
index 2e0fe9a838745db28abceddf2bbcf39860afaa9c..024c51a0b6fe8fb1b74d1047a0df04dafb636057 100644
--- a/public/javascripts/app/templates/stream-element.handlebars
+++ b/public/javascripts/app/templates/stream-element.handlebars
@@ -2,7 +2,7 @@
 
   {{#if current_user}}
     <div class="controls">
-      {{#if authorIsNotCurrentUser}}
+      {{#unless authorIsCurrentUser}}
         <a href="#" rel=nofollow>
           <img src="{{imageUrl "/images/icons/ignoreuser.png"}}"" alt="Ignoreuser" class="block_user control_icon" title="{{t "ignore"}}" />
         </a>
@@ -13,7 +13,7 @@
         <a href="#" rel=nofollow>
           <img src="{{imageUrl "/images/deletelabel.png"}}" class="delete control_icon remove_post" title="{{t "delete"}}" />
         </a>
-      {{/if}}
+      {{/unless}}
     </div>
   {{/if}}
 
diff --git a/public/javascripts/app/templates/template-picker.handlebars b/public/javascripts/app/templates/template-picker.handlebars
new file mode 100644
index 0000000000000000000000000000000000000000..9617e86f212f914f0c3c87c7b6d86988f5a6ff44
--- /dev/null
+++ b/public/javascripts/app/templates/template-picker.handlebars
@@ -0,0 +1,5 @@
+<select name="template">
+    {{#each templates}}
+        <option value="{{.}}">{{.}}</option>
+    {{/each}}
+</select>
\ No newline at end of file
diff --git a/public/javascripts/app/views.js b/public/javascripts/app/views.js
index 6ebd412e0b3c71476ff9038f1d2306702819a04b..faadd32ce5ac946185f14b94e0f129e1943c58dd 100644
--- a/public/javascripts/app/views.js
+++ b/public/javascripts/app/views.js
@@ -9,7 +9,10 @@ app.views.Base = Backbone.View.extend({
   },
 
   setupRenderEvents : function(){
-    this.model.bind('remove', this.remove, this);
+    if(this.model) {
+      //this should be in streamobjects view
+      this.model.bind('remove', this.remove, this);
+    }
 
     // this line is too generic.  we usually only want to re-render on
     // feedback changes as the post content, author, and time do not change.
@@ -18,7 +21,7 @@ app.views.Base = Backbone.View.extend({
   },
 
   defaultPresenter : function(){
-    var modelJson = this.model ? this.model.toJSON() : {}
+    var modelJson = this.model ? _.clone(this.model.attributes) : {}
     return _.extend(modelJson, {
       current_user : app.currentUser.attributes,
       loggedIn : app.currentUser.authenticated()
@@ -37,7 +40,9 @@ app.views.Base = Backbone.View.extend({
   renderTemplate : function(){
     var presenter = _.isFunction(this.presenter) ? this.presenter() : this.presenter
     this.template = JST[this.templateName]
-    $(this.el).html(this.template(presenter));
+    $(this.el)
+      .html(this.template(presenter))
+      .attr("data-template", _.last(this.templateName.split("/")));
     this.postRenderTemplate();
   },
 
diff --git a/public/javascripts/app/views/aspects_dropdown_view.js b/public/javascripts/app/views/aspects_dropdown_view.js
new file mode 100644
index 0000000000000000000000000000000000000000..a6085263f2bf68714eee56e92612143f06bf0d39
--- /dev/null
+++ b/public/javascripts/app/views/aspects_dropdown_view.js
@@ -0,0 +1,68 @@
+app.views.AspectsDropdown = app.views.Base.extend({
+  templateName : "aspects-dropdown",
+  events : {
+    "click .dropdown-menu a" : "setVisibility"
+  },
+
+  postRenderTemplate : function(){
+    this.setVisibility({target : this.$("a[data-visibility='all-aspects']").first()})
+  },
+
+  setVisibility : function(evt){
+    var self = this
+      , link = $(evt.target).closest("a")
+      , visibilityCallbacks = {
+          'public' : setPublic,
+          'all-aspects' : setPrivate,
+          'custom' : setCustom
+        }
+
+    visibilityCallbacks[link.data("visibility")]()
+
+    this.setAspectIds()
+
+    function setPublic (){
+      deselectAll()
+      selectAspect()
+      self.setDropdownText(link.text())
+    }
+
+    function setPrivate (){
+      deselectAll()
+      selectAspect()
+      self.setDropdownText(link.text())
+    }
+
+    function setCustom (){
+      deselectOverrides()
+      link.parents("li").toggleClass("selected")
+      self.setDropdownText(link.text())
+      evt.stopImmediatePropagation();
+    }
+
+    function selectAspect() {
+      link.parents("li").addClass("selected")
+    }
+
+    function deselectOverrides() {
+      self.$("a.public, a.all-aspects").parent().removeClass("selected")
+    }
+
+    function deselectAll() {
+      self.$("li.selected").removeClass("selected")
+    }
+  },
+
+  setDropdownText : function(text){
+    $.trim(this.$(".dropdown-toggle .text").text(text))
+  },
+
+  setAspectIds : function(){
+    var selectedAspects = this.$("li.selected a")
+    var aspectIds = _.map(selectedAspects, function(aspect){
+      return $(aspect).data("aspect-id")}
+    )
+
+    this.$("input.aspect_ids").val(aspectIds)
+  }
+})
diff --git a/public/javascripts/app/views/photo_viewer.js b/public/javascripts/app/views/photo_viewer.js
new file mode 100644
index 0000000000000000000000000000000000000000..3f30cfbd469dbaaaa6c78178dbfce8827b9f88a3
--- /dev/null
+++ b/public/javascripts/app/views/photo_viewer.js
@@ -0,0 +1,7 @@
+app.views.PhotoViewer = app.views.Base.extend({
+  templateName : "photo-viewer",
+
+  presenter : function(){
+    return { photos : this.model.get("photos") } //json array of attributes, not backbone models, yet.
+  }
+});
\ No newline at end of file
diff --git a/public/javascripts/app/views/post/day_view.js b/public/javascripts/app/views/post/day_view.js
new file mode 100644
index 0000000000000000000000000000000000000000..f01bb41a9f81f6a6c9ef28fffa280fdc856c4afa
--- /dev/null
+++ b/public/javascripts/app/views/post/day_view.js
@@ -0,0 +1,16 @@
+app.views.Post.Day = app.views.Post.extend({
+  templateName : "day",
+  className : "day post loaded",
+
+  subviews : { "section.photo_viewer" : "photoViewer" },
+
+  photoViewer : function(){
+    return new app.views.PhotoViewer({ model : this.model })
+  },
+
+  postRenderTemplate : function(){
+    if(this.model.get("text").length < 140){
+      this.$('section.text').addClass('headline');
+    }
+  }
+});
\ No newline at end of file
diff --git a/public/javascripts/app/views/post_view.js b/public/javascripts/app/views/post_view.js
index 5900bf75c39c2669e258d2d2d371ef9f6c65da41..4f167fd3ae030b400d28748f08716d3e61756259 100644
--- a/public/javascripts/app/views/post_view.js
+++ b/public/javascripts/app/views/post_view.js
@@ -1,133 +1,44 @@
 app.views.Post = app.views.StreamObject.extend({
-
-  templateName: "stream-element",
-
-  className : "stream_element loaded",
-
-  events: {
-    "click .focus_comment_textarea": "focusCommentTextarea",
-    "click .show_nsfw_post": "removeNsfwShield",
-    "click .toggle_nsfw_state": "toggleNsfwState",
-
-    "click .remove_post": "destroyModel",
-    "click .hide_post": "hidePost",
-    "click .block_user": "blockUser"
-  },
-
-  subviews : {
-    ".feedback" : "feedbackView",
-    ".likes" : "likesInfoView",
-    ".comments" : "commentStreamView",
-    ".post-content" : "postContentView"
-  },
-
-  tooltipSelector : ".delete, .block_user, .post_scope",
-
-  initialize : function(options) {
-    // allow for a custom template name to be passed in via the options hash
-    this.templateName = options.templateName || this.templateName
-
-    this.model.bind('remove', this.remove, this);
-    this.model.bind('destroy', this.destroy, this);
-
-    //subviews
-    this.commentStreamView = new app.views.CommentStream({ model : this.model});
-
-    return this;
-  },
-
-  likesInfoView : function(){
-    return new app.views.LikesInfo({ model : this.model});
-  },
-
-  feedbackView : function(){
-    if(!app.currentUser.authenticated()) { return null }
-    return new app.views.Feedback({model : this.model});
-  },
-
-  postContentView: function(){
-    var normalizedClass = this.model.get("post_type").replace(/::/, "__");
-    var postClass = app.views[normalizedClass] || app.views.StatusMessage;
-    return new postClass({ model : this.model });
-  },
-
   presenter : function() {
     return _.extend(this.defaultPresenter(), {
-      authorIsNotCurrentUser : this.authorIsNotCurrentUser(),
+      authorIsCurrentUser : this.authorIsCurrentUser(),
       showPost : this.showPost(),
       text : app.helpers.textFormatter(this.model)
     })
   },
 
-  showPost : function() {
-    return (app.currentUser.get("showNsfw")) || !this.model.get("nsfw")
-  },
-
-  removeNsfwShield: function(evt){
-    if(evt){ evt.preventDefault(); }
-    this.model.set({nsfw : false})
-    this.render();
+  authorIsCurrentUser : function() {
+    return app.currentUser.authenticated() && this.model.get("author").id == app.user().id
   },
 
-  toggleNsfwState: function(evt){
-    if(evt){ evt.preventDefault(); }
-    app.currentUser.toggleNsfwState();
-  },
-
-  blockUser: function(evt){
-    if(evt) { evt.preventDefault(); }
-    if(!confirm("Ignore this user?")) { return }
-
-    var personId = this.model.get("author").id;
-    var block = new app.models.Block();
-
-    block.save({block : {person_id : personId}}, {
-      success : function(){
-        if(!app.stream) { return }
-
-        _.each(app.stream.posts.models, function(model){
-          if(model.get("author").id == personId) {
-            app.stream.posts.remove(model);
-          }
-        })
-      }
-    })
-  },
-
-  hidePost : function(evt) {
-    if(evt) { evt.preventDefault(); }
-    if(!confirm(Diaspora.I18n.t('confirm_dialog'))) { return }
-
-    $.ajax({
-      url : "/share_visibilities/42",
-      type : "PUT",
-      data : {
-        post_id : this.model.id
-      }
-    })
-
-    this.slideAndRemove();
-  },
-
-  focusCommentTextarea: function(evt){
-    evt.preventDefault();
-    this.$(".new_comment_form_wrapper").removeClass("hidden");
-    this.$(".comment_box").focus();
-
-    return this;
-  },
+  showPost : function() {
+    return (app.currentUser.get("showNsfw")) || !this.model.get("nsfw")
+  }
+}, { //static methods below
 
-  authorIsNotCurrentUser : function() {
-    return this.model.get("author").id != app.user().id
-  },
+  showFactory : function(model) {
+    var frameName = model.get("frame_name");
 
-  isOnShowPage : function() {
-    return (!this.model.collection) && (this.model.url() == document.location.pathname);
-  },
+    if(_.include(app.models.Post.legacyTemplateNames, frameName)){
+      return legacyShow(model)
+    } else {
+      return new app.views.Post[frameName]({
+        model : model
+      })
+    }
 
-  destroy : function() {
-    if (this.isOnShowPage()) {
-      document.location.replace(Backbone.history.options.root);
+    function legacyShow(model) {
+      return new app.views.Post.Legacy({
+        model : model,
+        className :   frameName + " post loaded",
+        templateName : "post-viewer/content/" +  frameName
+      });
     }
   }
 });
+
+app.views.Post.Legacy = app.views.Post.extend({
+  initialize : function(options) {
+    this.templateName = options.templateName || this.templateName
+  }
+})
\ No newline at end of file
diff --git a/public/javascripts/app/views/publisher_view.js b/public/javascripts/app/views/publisher_view.js
index 963949338eb50f000c7abf8a01d07cbb873d1a89..2274aeb746ed588ea5b6facff2c192210b2970bf 100644
--- a/public/javascripts/app/views/publisher_view.js
+++ b/public/javascripts/app/views/publisher_view.js
@@ -22,8 +22,9 @@ app.views.Publisher = Backbone.View.extend({
 
     var serializedForm = $(evt.target).closest("form").serializeObject();
 
-    // save status message
-    var statusMessage = new app.models.StatusMessage();
+    // lulz this code should be killed.
+    var statusMessage = new app.models.Post();
+
     statusMessage.save({
       "status_message" : {
         "text" : serializedForm["status_message[text]"]
diff --git a/public/javascripts/app/views/services_selector_view.js b/public/javascripts/app/views/services_selector_view.js
new file mode 100644
index 0000000000000000000000000000000000000000..7ed3e3c99cad834ab34061c6f952b6ff2c771b85
--- /dev/null
+++ b/public/javascripts/app/views/services_selector_view.js
@@ -0,0 +1,5 @@
+app.views.ServicesSelector = app.views.Base.extend({
+
+  templateName : "services-selector"
+
+});
\ No newline at end of file
diff --git a/public/javascripts/app/views/stream_object_view.js b/public/javascripts/app/views/stream_object_view.js
index 1f46e732b50641b891d7fb3e5a4e2f0154085ee2..42d7bc6d4195dd420307386c9f59e350aae041c4 100644
--- a/public/javascripts/app/views/stream_object_view.js
+++ b/public/javascripts/app/views/stream_object_view.js
@@ -1,6 +1,5 @@
 app.views.StreamObject = app.views.Base.extend({
-
-  destroyModel: function(evt) {
+   destroyModel: function(evt) {
     if (evt) {
       evt.preventDefault();
     }
diff --git a/public/javascripts/app/views/stream_post_views.js b/public/javascripts/app/views/stream_post_views.js
new file mode 100644
index 0000000000000000000000000000000000000000..5d955c54de447c7b39dc4db5b1bf3c96188a0cf6
--- /dev/null
+++ b/public/javascripts/app/views/stream_post_views.js
@@ -0,0 +1,102 @@
+app.views.StreamPost = app.views.Post.extend({
+  templateName: "stream-element",
+  className : "stream_element loaded",
+
+  subviews : {
+    ".feedback" : "feedbackView",
+    ".likes" : "likesInfoView",
+    ".comments" : "commentStreamView",
+    ".post-content" : "postContentView"
+  },
+
+  events: {
+    "click .focus_comment_textarea": "focusCommentTextarea",
+    "click .show_nsfw_post": "removeNsfwShield",
+    "click .toggle_nsfw_state": "toggleNsfwState",
+
+    "click .remove_post": "destroyModel",
+    "click .hide_post": "hidePost",
+    "click .block_user": "blockUser"
+  },
+
+  tooltipSelector : ".delete, .block_user, .post_scope",
+
+  initialize : function(){
+    this.model.bind('remove', this.remove, this);
+
+    //subviews
+    this.commentStreamView = new app.views.CommentStream({ model : this.model});
+  },
+
+
+  likesInfoView : function(){
+    return new app.views.LikesInfo({ model : this.model});
+  },
+
+  feedbackView : function(){
+    if(!app.currentUser.authenticated()) { return null }
+    return new app.views.Feedback({model : this.model});
+  },
+
+  postContentView: function(){
+    var normalizedClass = this.model.get("post_type").replace(/::/, "__");
+    var postClass = app.views[normalizedClass] || app.views.StatusMessage;
+    return new postClass({ model : this.model });
+  },
+
+  removeNsfwShield: function(evt){
+    if(evt){ evt.preventDefault(); }
+    this.model.set({nsfw : false})
+    this.render();
+  },
+
+  toggleNsfwState: function(evt){
+    if(evt){ evt.preventDefault(); }
+    app.currentUser.toggleNsfwState();
+  },
+
+
+  blockUser: function(evt){
+    if(evt) { evt.preventDefault(); }
+    if(!confirm("Ignore this user?")) { return }
+
+    var personId = this.model.get("author").id;
+    var block = new app.models.Block();
+
+    block.save({block : {person_id : personId}}, {
+      success : function(){
+        if(!app.stream) { return }
+
+        _.each(app.stream.posts.models, function(model){
+          if(model.get("author").id == personId) {
+            app.stream.posts.remove(model);
+          }
+        })
+      }
+    })
+  },
+
+  hidePost : function(evt) {
+    if(evt) { evt.preventDefault(); }
+    if(!confirm(Diaspora.I18n.t('confirm_dialog'))) { return }
+
+    $.ajax({
+      url : "/share_visibilities/42",
+      type : "PUT",
+      data : {
+        post_id : this.model.id
+      }
+    })
+
+    this.slideAndRemove();
+  },
+
+  focusCommentTextarea: function(evt){
+    evt.preventDefault();
+    this.$(".new_comment_form_wrapper").removeClass("hidden");
+    this.$(".comment_box").focus();
+
+    return this;
+  }
+
+})
\ No newline at end of file
diff --git a/public/javascripts/app/views/stream_view.js b/public/javascripts/app/views/stream_view.js
index a39892dd96961407fc543edf39b5c25e64f76ea5..b17ae4c60a6efa083d849a2f328c01593b50b828 100644
--- a/public/javascripts/app/views/stream_view.js
+++ b/public/javascripts/app/views/stream_view.js
@@ -27,7 +27,7 @@ app.views.Stream = Backbone.View.extend({
   },
 
   addPost : function(post) {
-    var postView = new app.views.Post({ model: post });
+    var postView = new app.views.StreamPost({ model: post });
 
     $(this.el)[
       (this.collection.at(0).id == post.id)
diff --git a/public/javascripts/app/views/template_picker_view.js b/public/javascripts/app/views/template_picker_view.js
new file mode 100644
index 0000000000000000000000000000000000000000..834bd72e776b73c72c7d5c3a3160dbd0db66f907
--- /dev/null
+++ b/public/javascripts/app/views/template_picker_view.js
@@ -0,0 +1,25 @@
+app.views.TemplatePicker = app.views.Base.extend({
+  templateName : "template-picker",
+
+  initialize : function(){
+    this.model.set({frame_name : 'status'})
+  },
+
+  events : {
+    "change select" : "setModelTemplate"
+  },
+
+  postRenderTemplate : function(){
+    this.$("select[name=template]").val(this.model.get("frame_name"))
+  },
+
+  setModelTemplate : function(evt){
+    this.model.set({"frame_name": this.$("select[name=template]").val()})
+  },
+
+  presenter : function() {
+    return _.extend(this.defaultPresenter(), {
+      templates : _.union(app.models.Post.frameMoods, app.models.Post.legacyTemplateNames)
+    })
+  }
+})
\ No newline at end of file
diff --git a/public/javascripts/mentions.js b/public/javascripts/mentions.js
index 449a57f079df035a290dd64feb923d0e8a6c734c..b6e23043734f76ee04ab7f19a50ac7f35c6ef578 100644
--- a/public/javascripts/mentions.js
+++ b/public/javascripts/mentions.js
@@ -1,10 +1,10 @@
 var Mentions = {
   initialize: function(mentionsInput) {
-    mentionsInput.mentionsInput(Mentions.options);
+    return mentionsInput.mentionsInput(Mentions.options);
   },
 
   fetchContacts : function(){
-    Mentions.contacts || $.getJSON($(".selected_contacts_link").attr("href"), function(data) {
+    Mentions.contacts || $.getJSON("/contacts", function(data) {
       Mentions.contacts = data;
     });
   },
diff --git a/public/javascripts/rails.js b/public/javascripts/rails.js
index 2fbb9b83c06cc9339d4a82d28c02cb23a1fd41b3..06b4e0b53a07a1b3654ba00d39c271b0c57f412b 100644
--- a/public/javascripts/rails.js
+++ b/public/javascripts/rails.js
@@ -1,198 +1,373 @@
-/* Clear form plugin - called using $("elem").clearForm(); */
-$.fn.clearForm = function() {
-  return this.each(function() {
-    if ($(this).is('form')) {
-      return $(':input', this).clearForm();
-    }
-    if ($(this).hasClass('clear_on_submit') || $(this).is(':text') || $(this).is(':password') || $(this).is('textarea')) {
-      $(this).val('');
-    } else if ($(this).is(':checkbox') || $(this).is(':radio')) {
-      $(this).attr('checked', false);
-    } else if ($(this).is('select')) {
-      this.selectedIndex = -1;
-    } else if ($(this).attr('name') == 'photos[]') {
-      $(this).val('');
-    }
-    $(this).blur();
-  });
-};
+(function($, undefined) {
 
 /**
  * Unobtrusive scripting adapter for jQuery
  *
- * Requires jQuery 1.4.3 or later.
+ * Requires jQuery 1.6.0 or later.
  * https://github.com/rails/jquery-ujs
+
+ * Uploading file using rails.js
+ * =============================
+ *
+ * By default, browsers do not allow files to be uploaded via AJAX. As a result, if there are any non-blank file fields
+ * in the remote form, this adapter aborts the AJAX submission and allows the form to submit through standard means.
+ *
+ * The `ajax:aborted:file` event allows you to bind your own handler to process the form submission however you wish.
+ *
+ * Ex:
+ *     $('form').live('ajax:aborted:file', function(event, elements){
+ *       // Implement own remote file-transfer handler here for non-blank file inputs passed in `elements`.
+ *       // Returning false in this handler tells rails.js to disallow standard form submission
+ *       return false;
+ *     });
+ *
+ * The `ajax:aborted:file` event is fired when a file-type input is detected with a non-blank value.
+ *
+ * Third-party tools can use this hook to detect when an AJAX file upload is attempted, and then use
+ * techniques like the iframe method to upload the file instead.
+ *
+ * Required fields in rails.js
+ * ===========================
+ *
+ * If any blank required inputs (required="required") are detected in the remote form, the whole form submission
+ * is canceled. Note that this is unlike file inputs, which still allow standard (non-AJAX) form submission.
+ *
+ * The `ajax:aborted:required` event allows you to bind your own handler to inform the user of blank required inputs.
+ *
+ * !! Note that Opera does not fire the form's submit event if there are blank required inputs, so this event may never
+ *    get fired in Opera. This event is what causes other browsers to exhibit the same submit-aborting behavior.
+ *
+ * Ex:
+ *     $('form').live('ajax:aborted:required', function(event, elements){
+ *       // Returning false in this handler tells rails.js to submit the form anyway.
+ *       // The blank required inputs are passed to this function in `elements`.
+ *       return ! confirm("Would you like to submit the form with missing info?");
+ *     });
  */
 
-(function($) {
-  // Make sure that every Ajax request sends the CSRF token
-  function CSRFProtection(fn) {
-    var token = $('meta[name="csrf-token"]').attr('content');
-    if (token) fn(function(xhr) { xhr.setRequestHeader('X-CSRF-Token', token) });
-  }
-  if ($().jquery == '1.5') { // gruesome hack
-    var factory = $.ajaxSettings.xhr;
-    $.ajaxSettings.xhr = function() {
-      var xhr = factory();
-      CSRFProtection(function(setHeader) {
-        var open = xhr.open;
-        xhr.open = function() { open.apply(this, arguments); setHeader(this) };
+  // Shorthand to make it a little easier to call public rails functions from within rails.js
+  var rails;
+
+  $.rails = rails = {
+    // Link elements bound by jquery-ujs
+    linkClickSelector: 'a[data-confirm], a[data-method], a[data-remote], a[data-disable-with]',
+
+    // Select elements bound by jquery-ujs
+    inputChangeSelector: 'select[data-remote], input[data-remote], textarea[data-remote]',
+
+    // Form elements bound by jquery-ujs
+    formSubmitSelector: 'form',
+
+    // Form input elements bound by jquery-ujs
+    formInputClickSelector: 'form input[type=submit], form input[type=image], form button[type=submit], form button:not(button[type])',
+
+    // Form input elements disabled during form submission
+    disableSelector: 'input[data-disable-with], button[data-disable-with], textarea[data-disable-with]',
+
+    // Form input elements re-enabled after form submission
+    enableSelector: 'input[data-disable-with]:disabled, button[data-disable-with]:disabled, textarea[data-disable-with]:disabled',
+
+    // Form required input elements
+    requiredInputSelector: 'input[name][required]:not([disabled]),textarea[name][required]:not([disabled])',
+
+    // Form file input elements
+    fileInputSelector: 'input:file',
+
+    // Link onClick disable selector with possible reenable after remote submission
+    linkDisableSelector: 'a[data-disable-with]',
+
+    // Make sure that every Ajax request sends the CSRF token
+    CSRFProtection: function(xhr) {
+      var token = $('meta[name="csrf-token"]').attr('content');
+      if (token) xhr.setRequestHeader('X-CSRF-Token', token);
+    },
+
+    // Triggers an event on an element and returns false if the event result is false
+    fire: function(obj, name, data) {
+      var event = $.Event(name);
+      obj.trigger(event, data);
+      return event.result !== false;
+    },
+
+    // Default confirm dialog, may be overridden with custom confirm dialog in $.rails.confirm
+    confirm: function(message) {
+      return confirm(message);
+    },
+
+    // Default ajax function, may be overridden with custom function in $.rails.ajax
+    ajax: function(options) {
+      return $.ajax(options);
+    },
+
+    // Submits "remote" forms and links with ajax
+    handleRemote: function(element) {
+      var method, url, data,
+        crossDomain = element.data('cross-domain') || null,
+        dataType = element.data('type') || ($.ajaxSettings && $.ajaxSettings.dataType),
+        options;
+
+      if (rails.fire(element, 'ajax:before')) {
+
+        if (element.is('form')) {
+          method = element.attr('method');
+          url = element.attr('action');
+          data = element.serializeArray();
+          // memoized value from clicked submit button
+          var button = element.data('ujs:submit-button');
+          if (button) {
+            data.push(button);
+            element.data('ujs:submit-button', null);
+          }
+        } else if (element.is(rails.inputChangeSelector)) {
+          method = element.data('method');
+          url = element.data('url');
+          data = element.serialize();
+          if (element.data('params')) data = data + "&" + element.data('params');
+        } else {
+          method = element.data('method');
+          url = element.attr('href');
+          data = element.data('params') || null;
+        }
+
+        options = {
+          type: method || 'GET', data: data, dataType: dataType, crossDomain: crossDomain,
+          // stopping the "ajax:beforeSend" event will cancel the ajax request
+          beforeSend: function(xhr, settings) {
+            if (settings.dataType === undefined) {
+              xhr.setRequestHeader('accept', '*/*;q=0.5, ' + settings.accepts.script);
+            }
+            return rails.fire(element, 'ajax:beforeSend', [xhr, settings]);
+          },
+          success: function(data, status, xhr) {
+            element.trigger('ajax:success', [data, status, xhr]);
+          },
+          complete: function(xhr, status) {
+            element.trigger('ajax:complete', [xhr, status]);
+          },
+          error: function(xhr, status, error) {
+            element.trigger('ajax:error', [xhr, status, error]);
+          }
+        };
+        // Only pass url to `ajax` options if not blank
+        if (url) { options.url = url; }
+
+        return rails.ajax(options);
+      } else {
+        return false;
+      }
+    },
+
+    // Handles "data-method" on links such as:
+    // <a href="/users/5" data-method="delete" rel="nofollow" data-confirm="Are you sure?">Delete</a>
+    handleMethod: function(link) {
+      var href = link.attr('href'),
+        method = link.data('method'),
+        target = link.attr('target'),
+        csrf_token = $('meta[name=csrf-token]').attr('content'),
+        csrf_param = $('meta[name=csrf-param]').attr('content'),
+        form = $('<form method="post" action="' + href + '"></form>'),
+        metadata_input = '<input name="_method" value="' + method + '" type="hidden" />';
+
+      if (csrf_param !== undefined && csrf_token !== undefined) {
+        metadata_input += '<input name="' + csrf_param + '" value="' + csrf_token + '" type="hidden" />';
+      }
+
+      if (target) { form.attr('target', target); }
+
+      form.hide().append(metadata_input).appendTo('body');
+      form.submit();
+    },
+
+    /* Disables form elements:
+      - Caches element value in 'ujs:enable-with' data store
+      - Replaces element text with value of 'data-disable-with' attribute
+      - Sets disabled property to true
+    */
+    disableFormElements: function(form) {
+      form.find(rails.disableSelector).each(function() {
+        var element = $(this), method = element.is('button') ? 'html' : 'val';
+        element.data('ujs:enable-with', element[method]());
+        element[method](element.data('disable-with'));
+        element.prop('disabled', true);
       });
-      return xhr;
-    };
-  }
-  else $(document).ajaxSend(function(e, xhr) {
-    CSRFProtection(function(setHeader) { setHeader(xhr) });
-  });
+    },
+
+    /* Re-enables disabled form elements:
+      - Replaces element text with cached value from 'ujs:enable-with' data store (created in `disableFormElements`)
+      - Sets disabled property to false
+    */
+    enableFormElements: function(form) {
+      form.find(rails.enableSelector).each(function() {
+        var element = $(this), method = element.is('button') ? 'html' : 'val';
+        if (element.data('ujs:enable-with')) element[method](element.data('ujs:enable-with'));
+        element.prop('disabled', false);
+      });
+    },
+
+   /* For 'data-confirm' attribute:
+      - Fires `confirm` event
+      - Shows the confirmation dialog
+      - Fires the `confirm:complete` event
 
-  // Triggers an event on an element and returns the event result
-  function fire(obj, name, data) {
-    var event = new $.Event(name);
-    obj.trigger(event, data);
-    return event.result !== false;
-  }
-
-  // Submits "remote" forms and links with ajax
-  function handleRemote(element) {
-    var method, url, data,
-      dataType = element.attr('data-type') || ($.ajaxSettings && $.ajaxSettings.dataType);
-
-    if (element.is('form')) {
-      method = element.attr('method');
-      url = element.attr('action');
-      data = element.serializeArray();
-      // memoized value from clicked submit button
-      var button = element.data('ujs:submit-button');
-      if (button) {
-        data.push(button);
-        element.data('ujs:submit-button', null);
+      Returns `true` if no function stops the chain and user chose yes; `false` otherwise.
+      Attaching a handler to the element's `confirm` event that returns a `falsy` value cancels the confirmation dialog.
+      Attaching a handler to the element's `confirm:complete` event that returns a `falsy` value makes this function
+      return false. The `confirm:complete` event is fired whether or not the user answered true or false to the dialog.
+   */
+    allowAction: function(element) {
+      var message = element.data('confirm'),
+          answer = false, callback;
+      if (!message) { return true; }
+
+      if (rails.fire(element, 'confirm')) {
+        answer = rails.confirm(message);
+        callback = rails.fire(element, 'confirm:complete', [answer]);
       }
-    } else {
-      method = element.attr('data-method');
-      url = element.attr('href');
-      data = null;
-    }
+      return answer && callback;
+    },
 
-    $.ajax({
-      url: url, type: method || 'GET', data: data, dataType: dataType,
-      // stopping the "ajax:beforeSend" event will cancel the ajax request
-      beforeSend: function(xhr, settings) {
-        if (settings.dataType === undefined) {
-          xhr.setRequestHeader('accept', '*/*;q=0.5, ' + settings.accepts.script);
+    // Helper function which checks for blank inputs in a form that match the specified CSS selector
+    blankInputs: function(form, specifiedSelector, nonBlank) {
+      var inputs = $(), input,
+        selector = specifiedSelector || 'input,textarea';
+      form.find(selector).each(function() {
+        input = $(this);
+        // Collect non-blank inputs if nonBlank option is true, otherwise, collect blank inputs
+        if (nonBlank ? input.val() : !input.val()) {
+          inputs = inputs.add(input);
         }
-        return fire(element, 'ajax:beforeSend', [xhr, settings]);
-      },
-      success: function(data, status, xhr) {
-        element.trigger('ajax:success', [data, status, xhr]);
-      },
-      complete: function(xhr, status) {
-        element.trigger('ajax:complete', [xhr, status]);
-      },
-      error: function(xhr, status, error) {
-        element.trigger('ajax:error', [xhr, status, error]);
+      });
+      return inputs.length ? inputs : false;
+    },
+
+    // Helper function which checks for non-blank inputs in a form that match the specified CSS selector
+    nonBlankInputs: function(form, specifiedSelector) {
+      return rails.blankInputs(form, specifiedSelector, true); // true specifies nonBlank
+    },
+
+    // Helper function, needed to provide consistent behavior in IE
+    stopEverything: function(e) {
+      $(e.target).trigger('ujs:everythingStopped');
+      e.stopImmediatePropagation();
+      return false;
+    },
+
+    // find all the submit events directly bound to the form and
+    // manually invoke them. If anyone returns false then stop the loop
+    callFormSubmitBindings: function(form, event) {
+      var events = form.data('events'), continuePropagation = true;
+      if (events !== undefined && events['submit'] !== undefined) {
+        $.each(events['submit'], function(i, obj){
+          if (typeof obj.handler === 'function') return continuePropagation = obj.handler(event);
+        });
       }
-    });
-  }
-
-  // Handles "data-method" on links such as:
-  // <a href="/users/5" data-method="delete" rel="nofollow" data-confirm="Are you sure?">Delete</a>
-  function handleMethod(link) {
-    var href = link.attr('href'),
-      method = link.attr('data-method'),
-      csrf_token = $('meta[name=csrf-token]').attr('content'),
-      csrf_param = $('meta[name=csrf-param]').attr('content'),
-      form = $('<form method="post" action="' + href + '"></form>'),
-      metadata_input = '<input name="_method" value="' + method + '" type="hidden" />',
-      form_params = link.data('form-params');
-
-    if (csrf_param !== undefined && csrf_token !== undefined) {
-      metadata_input += '<input name="' + csrf_param + '" value="' + csrf_token + '" type="hidden" />';
-    }
+      return continuePropagation;
+    },
+
+    //  replace element's html with the 'data-disable-with' after storing original html
+    //  and prevent clicking on it
+    disableElement: function(element) {
+      element.data('ujs:enable-with', element.html()); // store enabled state
+      element.html(element.data('disable-with')); // set to disabled state
+      element.bind('click.railsDisable', function(e) { // prevent further clicking
+        return rails.stopEverything(e)
+      });
+    },
 
-    // support non-nested JSON encoded params for links
-    if (form_params != undefined) {
-      var params = $.parseJSON(form_params);
-      for (key in params) {
-        form.append($("<input>").attr({"type": "hidden", "name": key, "value": params[key]}));
+    // restore element to its original state which was disabled by 'disableElement' above
+    enableElement: function(element) {
+      if (element.data('ujs:enable-with') !== undefined) {
+        element.html(element.data('ujs:enable-with')); // set to old enabled state
+        // this should be element.removeData('ujs:enable-with')
+        // but, there is currently a bug in jquery which makes hyphenated data attributes not get removed
+        element.data('ujs:enable-with', false); // clean up cache
       }
+      element.unbind('click.railsDisable'); // enable element
     }
 
-    form.hide().append(metadata_input).appendTo('body');
-    form.submit();
-  }
-
-  function disableFormElements(form) {
-    form.find('input[data-disable-with]').each(function() {
-      var input = $(this);
-      input.data('ujs:enable-with', input.val())
-        .val(input.attr('data-disable-with'))
-        .attr('disabled', 'disabled');
-    });
-  }
-
-  function enableFormElements(form) {
-    form.find('input[data-disable-with]').each(function() {
-      var input = $(this);
-      input.val(input.data('ujs:enable-with')).removeAttr('disabled');
-    });
-  }
-
-  function allowAction(element) {
-    var message = element.attr('data-confirm');
-    return !message || (fire(element, 'confirm') && confirm(message));
-  }
-
-  function requiredValuesMissing(form) {
-    var missing = false;
-    form.find('input[name][required]').each(function() {
-      if (!$(this).val()) missing = true;
-    });
-    return missing;
-  }
-
-  $('a[data-confirm], a[data-method], a[data-remote]').live('click.rails', function(e) {
-    var link = $(this);
-    if (!allowAction(link)) return false;
+  };
+
+  $.ajaxPrefilter(function(options, originalOptions, xhr){ if ( !options.crossDomain ) { rails.CSRFProtection(xhr); }});
+
+  $(document).delegate(rails.linkDisableSelector, 'ajax:complete', function() {
+      rails.enableElement($(this));
+  });
 
-    if (link.attr('data-remote') != undefined) {
-      handleRemote(link);
+  $(document).delegate(rails.linkClickSelector, 'click.rails', function(e) {
+    var link = $(this), method = link.data('method'), data = link.data('params');
+    if (!rails.allowAction(link)) return rails.stopEverything(e);
+
+    if (link.is(rails.linkDisableSelector)) rails.disableElement(link);
+
+    if (link.data('remote') !== undefined) {
+      if ( (e.metaKey || e.ctrlKey) && (!method || method === 'GET') && !data ) { return true; }
+
+      if (rails.handleRemote(link) === false) { rails.enableElement(link); }
       return false;
-    } else if (link.attr('data-method')) {
-      handleMethod(link);
+
+    } else if (link.data('method')) {
+      rails.handleMethod(link);
       return false;
     }
   });
 
-  $('form').live('submit.rails', function(e) {
-    var form = $(this), remote = form.attr('data-remote') != undefined;
-    if (!allowAction(form)) return false;
+  $(document).delegate(rails.inputChangeSelector, 'change.rails', function(e) {
+    var link = $(this);
+    if (!rails.allowAction(link)) return rails.stopEverything(e);
+
+    rails.handleRemote(link);
+    return false;
+  });
+
+  $(document).delegate(rails.formSubmitSelector, 'submit.rails', function(e) {
+    var form = $(this),
+      remote = form.data('remote') !== undefined,
+      blankRequiredInputs = rails.blankInputs(form, rails.requiredInputSelector),
+      nonBlankFileInputs = rails.nonBlankInputs(form, rails.fileInputSelector);
 
-    // skip other logic when required values are missing
-    if (requiredValuesMissing(form)) return !remote;
+    if (!rails.allowAction(form)) return rails.stopEverything(e);
+
+    // skip other logic when required values are missing or file upload is present
+    if (blankRequiredInputs && form.attr("novalidate") == undefined && rails.fire(form, 'ajax:aborted:required', [blankRequiredInputs])) {
+      return rails.stopEverything(e);
+    }
 
     if (remote) {
-      handleRemote(form);
+      if (nonBlankFileInputs) {
+        return rails.fire(form, 'ajax:aborted:file', [nonBlankFileInputs]);
+      }
+
+      // If browser does not support submit bubbling, then this live-binding will be called before direct
+      // bindings. Therefore, we should directly call any direct bindings before remotely submitting form.
+      if (!$.support.submitBubbles && $().jquery < '1.7' && rails.callFormSubmitBindings(form, e) === false) return rails.stopEverything(e);
+
+      rails.handleRemote(form);
       return false;
+
     } else {
       // slight timeout so that the submit button gets properly serialized
-      setTimeout(function(){ disableFormElements(form) }, 13);
+      setTimeout(function(){ rails.disableFormElements(form); }, 13);
     }
   });
 
-  $('form input[type=submit], form button[type=submit], form button:not([type])').live('click.rails', function() {
+  $(document).delegate(rails.formInputClickSelector, 'click.rails', function(event) {
     var button = $(this);
-    if (!allowAction(button)) return false;
+
+    if (!rails.allowAction(button)) return rails.stopEverything(event);
+
     // register the pressed submit button
-    var name = button.attr('name'), data = name ? {name:name, value:button.val()} : null;
+    var name = button.attr('name'),
+      data = name ? {name:name, value:button.val()} : null;
+
     button.closest('form').data('ujs:submit-button', data);
   });
 
-  $('form').live('ajax:beforeSend.rails', function(event) {
-    if (this == event.target) disableFormElements($(this));
+  $(document).delegate(rails.formSubmitSelector, 'ajax:beforeSend.rails', function(event) {
+    if (this == event.target) rails.disableFormElements($(this));
   });
 
-  $('form').live('ajax:complete.rails', function(event) {
-    if (this == event.target) enableFormElements($(this));
+  $(document).delegate(rails.formSubmitSelector, 'ajax:complete.rails', function(event) {
+    if (this == event.target) rails.enableFormElements($(this));
   });
-})( jQuery );
 
+})( jQuery );
diff --git a/public/javascripts/vendor/bootstrap/bootstrap-dropdown.js b/public/javascripts/vendor/bootstrap/bootstrap-dropdown.js
new file mode 100644
index 0000000000000000000000000000000000000000..f80995ca2f9a936199a35c3cf5ef669c76f6a6fa
--- /dev/null
+++ b/public/javascripts/vendor/bootstrap/bootstrap-dropdown.js
@@ -0,0 +1,92 @@
+/* ============================================================
+ * bootstrap-dropdown.js v2.0.1
+ * http://twitter.github.com/bootstrap/javascript.html#dropdowns
+ * ============================================================
+ * 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.
+ * ============================================================ */
+
+
+!function( $ ){
+
+  "use strict"
+
+  /* DROPDOWN CLASS DEFINITION
+   * ========================= */
+
+  var toggle = '[data-toggle="dropdown"]'
+    , Dropdown = function ( element ) {
+      var $el = $(element).on('click.dropdown.data-api', this.toggle)
+      $('html').on('click.dropdown.data-api', function () {
+        $el.parent().removeClass('open')
+      })
+    }
+
+  Dropdown.prototype = {
+
+    constructor: Dropdown
+
+    , toggle: function ( e ) {
+      var $this = $(this)
+        , selector = $this.attr('data-target')
+        , $parent
+        , isActive
+
+      if (!selector) {
+        selector = $this.attr('href')
+        selector = selector && selector.replace(/.*(?=#[^\s]*$)/, '') //strip for ie7
+      }
+
+      $parent = $(selector)
+      $parent.length || ($parent = $this.parent())
+
+      isActive = $parent.hasClass('open')
+
+      clearMenus()
+      !isActive && $parent.toggleClass('open')
+
+      return false
+    }
+
+  }
+
+  function clearMenus() {
+    $(toggle).parent().removeClass('open')
+  }
+
+
+  /* DROPDOWN PLUGIN DEFINITION
+   * ========================== */
+
+  $.fn.dropdown = function ( option ) {
+    return this.each(function () {
+      var $this = $(this)
+        , data = $this.data('dropdown')
+      if (!data) $this.data('dropdown', (data = new Dropdown(this)))
+      if (typeof option == 'string') data[option].call($this)
+    })
+  }
+
+  $.fn.dropdown.Constructor = Dropdown
+
+
+  /* APPLY TO STANDARD DROPDOWN ELEMENTS
+   * =================================== */
+
+  $(function () {
+    $('html').on('click.dropdown.data-api', clearMenus)
+    $('body').on('click.dropdown.data-api', toggle, Dropdown.prototype.toggle)
+  })
+
+}( window.jQuery );
\ No newline at end of file
diff --git a/public/javascripts/vendor/jquery.iframe-transport.js b/public/javascripts/vendor/jquery.iframe-transport.js
new file mode 100644
index 0000000000000000000000000000000000000000..3407da0a539ea94b1995c889e094ee79cc0d9528
--- /dev/null
+++ b/public/javascripts/vendor/jquery.iframe-transport.js
@@ -0,0 +1,233 @@
+// This [jQuery](http://jquery.com/) plugin implements an `<iframe>`
+// [transport](http://api.jquery.com/extending-ajax/#Transports) so that
+// `$.ajax()` calls support the uploading of files using standard HTML file
+// input fields. This is done by switching the exchange from `XMLHttpRequest` to
+// a hidden `iframe` element containing a form that is submitted.
+
+// The [source for the plugin](http://github.com/cmlenz/jquery-iframe-transport)
+// is available on [Github](http://github.com/) and dual licensed under the MIT
+// or GPL Version 2 licenses.
+
+// ## Usage
+
+// To use this plugin, you simply add a `iframe` option with the value `true`
+// to the Ajax settings an `$.ajax()` call, and specify the file fields to
+// include in the submssion using the `files` option, which can be a selector,
+// jQuery object, or a list of DOM elements containing one or more
+// `<input type="file">` elements:
+
+//     $("#myform").submit(function() {
+//         $.ajax(this.action, {
+//             files: $(":file", this),
+//             iframe: true
+//         }).complete(function(data) {
+//             console.log(data);
+//         });
+//     });
+
+// The plugin will construct a hidden `<iframe>` element containing a copy of
+// the form the file field belongs to, will disable any form fields not
+// explicitly included, submit that form, and process the response.
+
+// If you want to include other form fields in the form submission, include them
+// in the `data` option, and set the `processData` option to `false`:
+
+//     $("#myform").submit(function() {
+//         $.ajax(this.action, {
+//             data: $(":text", this).serializeArray(),
+//             files: $(":file", this),
+//             iframe: true,
+//             processData: false
+//         }).complete(function(data) {
+//             console.log(data);
+//         });
+//     });
+
+// ### The Server Side
+
+// If the response is not HTML or XML, you (unfortunately) need to apply some
+// trickery on the server side. To send back a JSON payload, send back an HTML
+// `<textarea>` element with a `data-type` attribute that contains the MIME
+// type, and put the actual payload in the textarea:
+
+//     <textarea data-type="application/json">
+//       {"ok": true, "message": "Thanks so much"}
+//     </textarea>
+
+// The iframe transport plugin will detect this and attempt to apply the same
+// conversions that jQuery applies to regular responses. That means for the
+// example above you should get a Javascript object as the `data` parameter of
+// the `complete` callback, with the properties `ok: true` and
+// `message: "Thanks so much"`.
+
+// ### Compatibility
+
+// This plugin has primarily been tested on Safari 5, Firefox 4, and Internet
+// Explorer all the way back to version 6. While I haven't found any issues with
+// it so far, I'm fairly sure it still doesn't work around all the quirks in all
+// different browsers. But the code is still pretty simple overall, so you
+// should be able to fix it and contribute a patch :)
+
+// ## Annotated Source
+
+(function($, undefined) {
+
+  // Register a prefilter that checks whether the `iframe` option is set, and
+  // switches to the iframe transport if it is `true`.
+  $.ajaxPrefilter(function(options, origOptions, jqXHR) {
+    if (options.iframe) {
+      return "iframe";
+    }
+  });
+
+  // Register an iframe transport, independent of requested data type. It will
+  // only activate when the "files" option has been set to a non-empty list of
+  // enabled file inputs.
+  $.ajaxTransport("iframe", function(options, origOptions, jqXHR) {
+    var form = null,
+        iframe = null,
+        origAction = null,
+        origTarget = null,
+        origEnctype = null,
+        addedFields = [],
+        disabledFields = [],
+        files = $(options.files).filter(":file:enabled");
+
+    // This function gets called after a successful submission or an abortion
+    // and should revert all changes made to the page to enable the
+    // submission via this transport.
+    function cleanUp() {
+      $(addedFields).each(function() {
+        this.remove();
+      });
+      $(disabledFields).each(function() {
+        this.disabled = false;
+      });
+      form.attr("action", origAction || "")
+          .attr("target", origTarget || "")
+          .attr("enctype", origEnctype || "");
+      iframe.attr("src", "javascript:false;").remove();
+    }
+
+    // Remove "iframe" from the data types list so that further processing is
+    // based on the content type returned by the server, without attempting an
+    // (unsupported) conversion from "iframe" to the actual type.
+    options.dataTypes.shift();
+
+    if (files.length) {
+      // Determine the form the file fields belong to, and make sure they all
+      // actually belong to the same form.
+      files.each(function() {
+        if (form !== null && this.form !== form) {
+          jQuery.error("All file fields must belong to the same form");
+        }
+        form = this.form;
+      });
+      form = $(form);
+
+      // Store the original form attributes that we'll be replacing temporarily.
+      origAction = form.attr("action");
+      origTarget = form.attr("target");
+      origEnctype = form.attr("enctype");
+
+      // We need to disable all other inputs in the form so that they don't get
+      // included in the submitted data unexpectedly.
+      form.find(":input:not(:submit)").each(function() {
+        if (!this.disabled && (this.type != "file" || files.index(this) < 0)) {
+          this.disabled = true;
+          disabledFields.push(this);
+        }
+      });
+
+      // If there is any additional data specified via the `data` option,
+      // we add it as hidden fields to the form. This (currently) requires
+      // the `processData` option to be set to false so that the data doesn't
+      // get serialized to a string.
+      if (typeof(options.data) === "string" && options.data.length > 0) {
+        jQuery.error("data must not be serialized");
+      }
+      $.each(options.data || {}, function(name, value) {
+        if ($.isPlainObject(value)) {
+          name = value.name;
+          value = value.value;
+        }
+        addedFields.push($("<input type='hidden'>").attr("name", name)
+          .attr("value", value).appendTo(form));
+      });
+
+      // Add a hidden `X-Requested-With` field with the value `IFrame` to the
+      // field, to help server-side code to determine that the upload happened
+      // through this transport.
+      addedFields.push($("<input type='hidden' name='X-Requested-With'>")
+        .attr("value", "IFrame").appendTo(form));
+
+      // Borrowed straight from the JQuery source
+      // Provides a way of specifying the accepted data type similar to HTTP_ACCEPTS
+      accepts = options.dataTypes[ 0 ] && options.accepts[ options.dataTypes[0] ] ?
+        options.accepts[ options.dataTypes[0] ] + ( options.dataTypes[ 0 ] !== "*" ? ", */*; q=0.01" : "" ) :
+        options.accepts[ "*" ]
+
+      addedFields.push($("<input type='hidden' name='X-Http-Accept'>")
+        .attr("value", accepts).appendTo(form));
+
+      return {
+
+        // The `send` function is called by jQuery when the request should be
+        // sent.
+        send: function(headers, completeCallback) {
+          iframe = $("<iframe src='javascript:false;' name='iframe-" + $.now()
+            + "' style='display:none'></iframe>");
+
+          // The first load event gets fired after the iframe has been injected
+          // into the DOM, and is used to prepare the actual submission.
+          iframe.bind("load", function() {
+
+            // The second load event gets fired when the response to the form
+            // submission is received. The implementation detects whether the
+            // actual payload is embedded in a `<textarea>` element, and
+            // prepares the required conversions to be made in that case.
+            iframe.unbind("load").bind("load", function() {
+
+              var doc = this.contentWindow ? this.contentWindow.document :
+                (this.contentDocument ? this.contentDocument : this.document),
+                root = doc.documentElement ? doc.documentElement : doc.body,
+                textarea = root.getElementsByTagName("textarea")[0],
+                type = textarea ? textarea.getAttribute("data-type") : null;
+
+              var status = textarea ? parseInt(textarea.getAttribute("response-code")) : 200,
+                statusText = "OK",
+                responses = { text: type ? textarea.value : root ? root.innerHTML : null },
+                headers = "Content-Type: " + (type || "text/html")
+
+              completeCallback(status, statusText, responses, headers);
+
+              setTimeout(cleanUp, 50);
+            });
+
+            // Now that the load handler has been set up, reconfigure and
+            // submit the form.
+            form.attr("action", options.url)
+              .attr("target", iframe.attr("name"))
+              .attr("enctype", "multipart/form-data")
+              .get(0).submit();
+          });
+
+          // After everything has been set up correctly, the iframe gets
+          // injected into the DOM so that the submission can be initiated.
+          iframe.insertAfter(form);
+        },
+
+        // The `abort` function is called by jQuery when the request should be
+        // aborted.
+        abort: function() {
+          if (iframe !== null) {
+            iframe.unbind("load").attr("src", "javascript:false;");
+            cleanUp();
+          }
+        }
+
+      };
+    }
+  });
+
+})(jQuery);
diff --git a/public/javascripts/vendor/jquery.remotipart.js b/public/javascripts/vendor/jquery.remotipart.js
new file mode 100644
index 0000000000000000000000000000000000000000..9e00854e00937bf108c2ad7e730e83bf90e70da5
--- /dev/null
+++ b/public/javascripts/vendor/jquery.remotipart.js
@@ -0,0 +1,69 @@
+//= require jquery.iframe-transport.js
+//= require_self
+
+(function($) {
+
+  var remotipart;
+
+  $.remotipart = remotipart = {
+
+    setup: function(form) {
+      form
+        // Allow setup part of $.rails.handleRemote to setup remote settings before canceling default remote handler
+        // This is required in order to change the remote settings using the form details
+        .one('ajax:beforeSend.remotipart', function(e, xhr, settings){
+          // Delete the beforeSend bindings, since we're about to re-submit via ajaxSubmit with the beforeSubmit
+          // hook that was just setup and triggered via the default `$.rails.handleRemote`
+          // delete settings.beforeSend;
+          delete settings.beforeSend;
+
+          settings.iframe      = true;
+          settings.files       = $($.rails.fileInputSelector, form);
+          settings.data        = form.serializeArray();
+          settings.processData = false;
+
+          // Modify some settings to integrate JS request with rails helpers and middleware
+          if (settings.dataType === undefined) { settings.dataType = 'script *'; }
+          settings.data.push({name: 'remotipart_submitted', value: true});
+
+          // Allow remotipartSubmit to be cancelled if needed
+          if ($.rails.fire(form, 'ajax:remotipartSubmit', [xhr, settings])) {
+            // Second verse, same as the first
+            $.rails.ajax(settings);
+          }
+
+          //Run cleanup
+          remotipart.teardown(form);
+
+          // Cancel the jQuery UJS request
+          return false;
+        })
+
+        // Keep track that we just set this particular form with Remotipart bindings
+        // Note: The `true` value will get over-written with the `settings.dataType` from the `ajax:beforeSend` handler
+        .data('remotipartSubmitted', true);
+    },
+
+    teardown: function(form) {
+      form
+        .unbind('ajax:beforeSend.remotipart')
+        .removeData('remotipartSubmitted')
+    }
+  };
+
+  $('form').live('ajax:aborted:file', function(){
+    var form = $(this);
+
+    remotipart.setup(form);
+
+    // If browser does not support submit bubbling, then this live-binding will be called before direct
+    // bindings. Therefore, we should directly call any direct bindings before remotely submitting form.
+    if (!$.support.submitBubbles && $().jquery < '1.7' && $.rails.callFormSubmitBindings(form) === false) return $.rails.stopEverything(e);
+
+    // Manually call jquery-ujs remote call so that it can setup form and settings as usual,
+    // and trigger the `ajax:beforeSend` callback to which remotipart binds functionality.
+    $.rails.handleRemote(form);
+    return false;
+  });
+
+})(jQuery);
diff --git a/public/javascripts/view.js b/public/javascripts/view.js
index 39543a2cb5aa15c8f02057f328fe51e764b63312..977b7cded8005bdf7b8e7b03190bad6cc9a45786 100644
--- a/public/javascripts/view.js
+++ b/public/javascripts/view.js
@@ -25,7 +25,25 @@ var View = {
     /* Avatars */
     $(this.avatars.selector).error(this.avatars.fallback);
 
-    /* Clear forms after successful submit */
+    /* Clear forms after successful submit, this is some legacy dan hanson stuff, do we still want it? */
+    $.fn.clearForm = function() {
+      return this.each(function() {
+        if ($(this).is('form')) {
+          return $(':input', this).clearForm();
+        }
+        if ($(this).hasClass('clear_on_submit') || $(this).is(':text') || $(this).is(':password') || $(this).is('textarea')) {
+          $(this).val('');
+        } else if ($(this).is(':checkbox') || $(this).is(':radio')) {
+          $(this).attr('checked', false);
+        } else if ($(this).is('select')) {
+          this.selectedIndex = -1;
+        } else if ($(this).attr('name') == 'photos[]') {
+          $(this).val('');
+        }
+        $(this).blur();
+      });
+    };
+
     $('form[data-remote]').live('ajax:success', function (e) {
       $(this).clearForm();
       $(this).focusout();
diff --git a/public/stylesheets/sass/new-templates.scss b/public/stylesheets/sass/new-templates.scss
index 094627e99f6300e3d6419335f9403a5564e748a8..c8ce38355676c758d7ba4d14fba067c3c27e8367 100644
--- a/public/stylesheets/sass/new-templates.scss
+++ b/public/stylesheets/sass/new-templates.scss
@@ -226,7 +226,7 @@ $pane-width: 420px;
 
 .photo-fill {
   @include background-cover();
-
+  z-index : -5000; //so the framer controls don't get lost
   position: absolute;
   top: 0;
   left: 0;
@@ -250,6 +250,7 @@ $pane-width: 420px;
 }
 
 .rich-media {
+  z-index : -5000; //so the framer controls don't get lost
   position: absolute;
   height: 100%;
   width: 100%;
@@ -764,3 +765,47 @@ text-rendering: optimizelegibility;
   right: 8px;
   top: 8px;
 }
+
+.aspects_dropdown {
+  i {
+    display: none;
+  }
+
+  .selected i {
+    display: inline-block;
+  }
+
+  a {
+    display : inline-block;
+  }
+}
+
+.new_photo .photo{
+  display: inline;
+  max-width: 200px;
+  max-height: 200px;
+}
+
+.new-post-section {
+  margin-top: 100px;
+}
+
+.aspect_selector {
+  float: right;
+}
+
+.post-view {
+  display: table;
+  height: 100%;
+  width: 100%;
+}
+
+#post-content {
+  button {
+    position: absolute;
+  }
+}
+
+.headline p{
+  @include media-text();
+}
\ No newline at end of file
diff --git a/spec/controllers/status_messages_controller_spec.rb b/spec/controllers/status_messages_controller_spec.rb
index b6dbd587de06e753d4b406094c95b02f56cb356c..fa823b3fd590f447f02cd365b73cbc85b222cdcf 100644
--- a/spec/controllers/status_messages_controller_spec.rb
+++ b/spec/controllers/status_messages_controller_spec.rb
@@ -75,7 +75,7 @@ describe StatusMessagesController do
       { :status_message => {
         :public  => "true",
         :text => "facebook, is that you?",
-        },
+      },
       :aspect_ids => [@aspect1.id.to_s] }
     }
 
diff --git a/spec/javascripts/app/app_spec.js b/spec/javascripts/app/app_spec.js
index 6989301f41a04ad33d77ca76050329db50fa1443..9323020d94dd2835b9c4f3e624ae013b4d2af37d 100644
--- a/spec/javascripts/app/app_spec.js
+++ b/spec/javascripts/app/app_spec.js
@@ -3,20 +3,16 @@ describe("app", function() {
     it("sets the user if given one and returns the current user", function() {
       expect(app.user()).toBeFalsy()
     });
-    
+
     it("returns false if the current_user isn't set", function() {
       app._user = undefined;
-
       expect(app.user()).toEqual(false);
     });
-  });
 
-  describe('currentUser', function(){
     it("sets the user if given one and returns the current user", function() {
-      expect(app.currentUser.authenticated()).toBeFalsy()
+      expect(app.user()).toBeFalsy()
       app.user({name: "alice"});
-
       expect(app.user().get("name")).toEqual("alice");
     });
   });
-});
+})
diff --git a/spec/javascripts/app/forms/picture_form_spec.js b/spec/javascripts/app/forms/picture_form_spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..42d53cce1e5f87e86a5a99d713d54711068a3c67
--- /dev/null
+++ b/spec/javascripts/app/forms/picture_form_spec.js
@@ -0,0 +1,57 @@
+describe("app.forms.Picture", function(){
+  beforeEach(function(){
+    $("<meta/>", {
+      "name" : "csrf-token",
+      "content" : "supersecrettokenlol"
+    }).prependTo("head")
+
+    this.form = new app.forms.Picture().render()
+  });
+
+  it("sets the authenticity token from the meta tag", function(){
+    expect(this.form.$("input[name='authenticity_token']").val()).toBe("supersecrettokenlol")
+  });
+
+  describe("selecting a photo", function(){
+    it("submits the form", function(){
+      var submitSpy = jasmine.createSpy();
+
+      this.form.$("form").submit(function(event){
+        event.preventDefault();
+        submitSpy();
+      });
+
+      this.form.$("input[name='photo[user_file]']").change()
+      expect(submitSpy).toHaveBeenCalled();
+    })
+  });
+
+  describe("when a photo is suceessfully submitted", function(){
+    beforeEach(function(){
+      this.photoAttrs = { name : "Obama rides a bicycle" }
+      this.respond = function() {
+        this.form.$(".new_photo").trigger("ajax:complete", {
+          responseText : JSON.stringify({success : true, data : this.photoAttrs})
+        })
+      }
+    })
+
+    it("adds a new model to the photos", function(){
+      expect(this.form.$(".photos div").length).toBe(0);
+      this.respond()
+      expect(this.form.$(".photos div").length).toBeGreaterThan(0);
+    })
+  })
+
+  describe("when a photo is unsuccessfully submitted", function(){
+    beforeEach(function(){
+      this.response = {responseText : JSON.stringify({success : false, message : "I like to eat basketballs"}) }
+    })
+
+    it("adds a new model to the photos", function(){
+      spyOn(window, "alert")
+      this.form.$(".new_photo").trigger("ajax:complete", this.response)
+      expect(window.alert).toHaveBeenCalled();
+    })
+  })
+});
\ No newline at end of file
diff --git a/spec/javascripts/app/forms/post_form_spec.js b/spec/javascripts/app/forms/post_form_spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..8514e1db2471597d7b80d665083aa478bd446061
--- /dev/null
+++ b/spec/javascripts/app/forms/post_form_spec.js
@@ -0,0 +1,42 @@
+describe("app.forms.Post", function(){
+  beforeEach(function(){
+    this.post = new app.models.Post();
+    this.view = new app.forms.Post({model : this.post})
+  })
+
+  describe("rendering", function(){
+    beforeEach(function(){
+      this.view.render()
+    })
+
+
+    describe("submitting a valid form", function(){
+      beforeEach(function(){
+        this.view.$("form #text_with_markup").val("Oh My")
+        this.view.$("form .aspect_ids").val("public")
+
+        /* appending checkboxes */
+        this.view.$(".new-post").append($("<input/>", {
+          value : "fakeBook",
+          checked : "checked",
+          "class" : "service",
+          "type" : "checkbox"
+        }))
+
+        this.view.$(".new-post").append($("<input/>", {
+          value : "twitter",
+          checked : "checked",
+          "class" : "service",
+          "type" : "checkbox"
+        }))
+      })
+
+      it("instantiates a post on form submit", function(){
+        this.view.$(".new-post").submit()
+        expect(this.view.model.get("text")).toBe("Oh My")
+        expect(this.view.model.get("aspect_ids")).toBe("public")
+        expect(this.view.model.get("services").length).toBe(2)
+      })
+    })
+  })
+})
diff --git a/spec/javascripts/app/models/status_message_spec.js b/spec/javascripts/app/models/status_message_spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..ad8c93a31ec1bbad84d1fd9171c73bd8dda1a98f
--- /dev/null
+++ b/spec/javascripts/app/models/status_message_spec.js
@@ -0,0 +1,12 @@
+describe("app.models.StatusMessage", function(){
+  describe("#url", function(){
+    it("is /status_messages when its new", function(){
+      var post = new app.models.StatusMessage()
+      expect(post.url()).toBe("/status_messages")
+    })
+
+    it("is /posts/id when it has an id", function(){
+      expect(new app.models.StatusMessage({id : 5}).url()).toBe("/posts/5")
+    })
+  })
+})
\ No newline at end of file
diff --git a/spec/javascripts/app/pages/framer_spec.js b/spec/javascripts/app/pages/framer_spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..db9deb0de45d058eb59a1eaf8f463be53b394083
--- /dev/null
+++ b/spec/javascripts/app/pages/framer_spec.js
@@ -0,0 +1,27 @@
+describe("app.pages.Framer", function(){
+  beforeEach(function(){
+    loginAs(factory.user())
+    app.frame = new factory.statusMessage();
+    this.page = new app.pages.Framer();
+  });
+
+  it("passes the model down to the template picker", function(){
+    expect(this.page.templatePicker.model).toBe(app.frame)
+  });
+
+  it("passes the model down to the post view", function(){
+    expect(this.page.postView().model).toBe(app.frame)
+  });
+
+  describe("rendering", function(){
+    beforeEach(function(){
+      this.page.render();
+    });
+
+    it("saves the model when you click done",function(){
+      spyOn(app.frame, "save");
+      this.page.$("button.done").click();
+      expect(app.frame.save).toHaveBeenCalled();
+    });
+  });
+});
diff --git a/spec/javascripts/app/pages/post_new_spec.js b/spec/javascripts/app/pages/post_new_spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..0c701e51fb8ea570f6bf868fb7c7aa8a9c9fb847
--- /dev/null
+++ b/spec/javascripts/app/pages/post_new_spec.js
@@ -0,0 +1,32 @@
+describe("app.pages.PostNew", function(){
+  beforeEach(function(){
+    this.page = new app.pages.PostNew()
+  })
+
+  describe("rendering", function(){
+    beforeEach(function(){
+      this.page.render();
+    })
+
+    describe("clicking next", function(){
+      beforeEach(function(){
+        spyOn(app.router, "navigate")
+        spyOn(this.page.postForm, "setModelAttributes")
+        this.page.$("button.next").click()
+      })
+
+      it("calls tells the form to set the models attributes", function(){
+        expect(this.page.postForm.setModelAttributes).toHaveBeenCalled();
+      });
+
+      it("stores a reference to the form as app.composer" , function(){
+        expect(this.page.model).toBeDefined()
+        expect(app.frame).toBe(this.page.model)
+      });
+
+      it("navigates to the framer", function(){
+        expect(app.router.navigate).toHaveBeenCalledWith("framer", true)
+      });
+    })
+  })
+});
\ No newline at end of file
diff --git a/spec/javascripts/app/views/aspects_dropdown_view_spec.js b/spec/javascripts/app/views/aspects_dropdown_view_spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..b5a47604d642849ec231c1452d7a9bb1ba5d2378
--- /dev/null
+++ b/spec/javascripts/app/views/aspects_dropdown_view_spec.js
@@ -0,0 +1,114 @@
+describe("app.views.AspectsDropdown", function(){
+  beforeEach(function(){
+    loginAs(factory.user({
+      aspects : [
+        { id : 3, name : "sauce" },
+        { id : 5, name : "conf" },
+        { id : 7, name : "lovers" }
+      ]
+    }))
+
+    this.view = new app.views.AspectsDropdown
+  })
+
+  describe("rendering", function(){
+    beforeEach(function(){
+      this.view.render()
+    })
+
+    it("defaults to All Aspects Visibility", function(){
+      expect(this.view.$("input.aspect_ids").val()).toBe("all_aspects")
+      expect($.trim(this.view.$(".dropdown-toggle .text").text())).toBe("All Aspects")
+    })
+
+    describe("selecting Public", function(){
+      beforeEach(function(){
+       this.link = this.view.$("a[data-visibility='public']")
+       this.link.click()
+      })
+
+      it("calls set aspect_ids to 'public'", function(){
+        expect(this.view.$("input.aspect_ids").val()).toBe("public")
+      })
+
+      it("sets the dropdown title to 'public'", function(){
+        expect(this.view.$(".dropdown-toggle .text").text()).toBe("Public")
+      })
+
+      it("adds the selected class to the link", function(){
+        expect(this.link.parent().hasClass("selected")).toBeTruthy();
+      })
+    })
+
+    describe("selecting All Aspects", function(){
+      beforeEach(function(){
+        this.link = this.view.$("a[data-visibility='all-aspects']")
+        this.link.click()
+      })
+
+      it("calls set aspect_ids to 'all'", function(){
+        expect(this.view.$("input.aspect_ids").val()).toBe("all_aspects")
+      })
+
+      it("sets the dropdown title to 'public'", function(){
+        expect($.trim(this.view.$(".dropdown-toggle .text").text())).toBe("All Aspects")
+      })
+
+      it("adds the selected class to the link", function(){
+        expect(this.link.parent().hasClass("selected")).toBeTruthy();
+      })
+    })
+
+
+    describe("selecting An Aspect", function(){
+      beforeEach(function(){
+        this.link = this.view.$("a:contains('lovers')")
+        this.link.click()
+      })
+
+      it("sets the dropdown title to the aspect title", function(){
+        expect($.trim(this.view.$(".dropdown-toggle .text").text())).toBe("lovers")
+      })
+
+      it("adds the selected class to the link", function(){
+        expect(this.link.parent().hasClass("selected")).toBeTruthy();
+      })
+
+      it("sets aspect_ids to to the aspect id", function(){
+        expect(this.view.$("input.aspect_ids").val()).toBe("7")
+      })
+
+      describe("selecting another aspect", function(){
+        beforeEach(function(){
+          this.view.$("a:contains('sauce')").click()
+        })
+
+        it("sets aspect_ids to the selected aspects", function(){
+          expect(this.view.$("input.aspect_ids").val()).toBe("3,7")
+        })
+
+        describe("deselecting another aspect", function(){
+          it("removes the clicked aspect", function(){
+            expect(this.view.$("input.aspect_ids").val()).toBe("3,7")
+            this.view.$("a:contains('lovers')").click()
+            expect(this.view.$("input.aspect_ids").val()).toBe("3")
+          })
+        })
+
+        describe("selecting all_aspects", function(){
+          it("sets aspect_ids to all_aspects", function(){
+            this.view.$("a[data-visibility='all-aspects']").click()
+            expect(this.view.$("input.aspect_ids").val()).toBe("all_aspects")
+          })
+        })
+
+        describe("selecting public", function(){
+          it("sets aspect_ids to public", function(){
+            this.view.$("a[data-visibility='public']").click()
+            expect(this.view.$("input.aspect_ids").val()).toBe("public")
+          })
+        })
+      })
+    })
+  })
+})
\ No newline at end of file
diff --git a/spec/javascripts/app/views/photo_viewer_spec.js b/spec/javascripts/app/views/photo_viewer_spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..6f25d580b6b646912a491f81a12a1ad26ad7d202
--- /dev/null
+++ b/spec/javascripts/app/views/photo_viewer_spec.js
@@ -0,0 +1,19 @@
+describe("app.views.PhotoViewer", function(){
+  beforeEach(function(){
+    this.model = factory.post({
+      photos : [
+        factory.photoAttrs({sizes : {large : "http://tieguy.org/me.jpg"}}),
+        factory.photoAttrs({sizes : {large : "http://whatthefuckiselizabethstarkupto.com/none_knows.gif"}}) //SIC
+      ]
+    })
+    this.view = new app.views.PhotoViewer({model : this.model})
+  })
+
+  describe("rendering", function(){
+    it("should have an image for each photoAttr on the model", function(){
+      this.view.render()
+      expect(this.view.$("img").length).toBe(2)
+      expect(this.view.$("img[src='http://tieguy.org/me.jpg']")).toExist()
+    })
+  })
+})
\ No newline at end of file
diff --git a/spec/javascripts/app/views/post/day_view_spec.js b/spec/javascripts/app/views/post/day_view_spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..397ac46151b9857a7a90a6b2c86f0d7d2206ae94
--- /dev/null
+++ b/spec/javascripts/app/views/post/day_view_spec.js
@@ -0,0 +1,34 @@
+describe("app.views.Post.Day", function(){
+  beforeEach(function(){
+    this.post = factory.post()
+    this.view = new app.views.Post.Day({model : this.post})
+  })
+
+  describe("rendering", function(){
+    it("is happy", function(){
+      this.view.render()
+    })
+
+    describe("when the text is under 140 characters", function(){
+      it("has class headline", function(){
+        this.post.set({text : "Lol this is a short headline"})
+        this.view.render()
+        expect(this.view.$("section.text")).toHaveClass("headline")
+      })
+    })
+
+    describe("when the text is over 140 characters", function(){
+      it("has doesn't have headline", function(){
+        this.post.set({text :"Vegan bushwick tempor labore. Nulla seitan anim, aesthetic ex gluten-free viral" +
+          "thundercats street art. Occaecat carles deserunt lomo messenger bag wes anderson. Narwhal cray selvage " +
+          "dolor. Mixtape wes anderson american apparel, mustache readymade cred nulla squid veniam small batch id " +
+          "cupidatat. Pork belly high life consequat, raw denim sint terry richardson seitan single-origin coffee " +
+          "butcher. Sint yr fugiat cillum."
+        })
+
+        this.view.render()
+        expect(this.view.$("section.text")).not.toHaveClass("headline")
+      })
+    })
+  })
+})
\ No newline at end of file
diff --git a/spec/javascripts/app/views/post_view_spec.js b/spec/javascripts/app/views/post_view_spec.js
index d1ef0e2b35da511db74c884b5ca36ccecfb7ad4f..58225d1afbfffc37e6f19188bd3ac9dcd64c533b 100644
--- a/spec/javascripts/app/views/post_view_spec.js
+++ b/spec/javascripts/app/views/post_view_spec.js
@@ -1,188 +1,3 @@
 describe("app.views.Post", function(){
-
-  describe("#render", function(){
-    beforeEach(function(){
-      loginAs({name: "alice", avatar : {small : "http://avatar.com/photo.jpg"}});
-
-      Diaspora.I18n.loadLocale({stream : {
-        reshares : {
-          one : "<%= count %> reshare",
-          other : "<%= count %> reshares"
-        },
-        likes : {
-          zero : "<%= count %> Likes",
-          one : "<%= count %> Like",
-          other : "<%= count %> Likes"
-        }
-      }})
-
-      var posts = $.parseJSON(spec.readFixture("stream_json"))["posts"];
-
-      this.collection = new app.collections.Posts(posts);
-      this.statusMessage = this.collection.models[0];
-      this.reshare = this.collection.models[1];
-    })
-
-    context("reshare", function(){
-        it("displays a reshare count", function(){
-          this.statusMessage.set({reshares_count : 2})
-          var view = new app.views.Post({model : this.statusMessage}).render();
-
-          expect($(view.el).html()).toContain(Diaspora.I18n.t('stream.reshares', {count: 2}))
-        })
-
-        it("does not display a reshare count for 'zero'", function(){
-          this.statusMessage.set({reshares_count : 0})
-          var view = new app.views.Post({model : this.statusMessage}).render();
-
-          expect($(view.el).html()).not.toContain("0 Reshares")
-        })
-    })
-
-    context("likes", function(){
-        it("displays a like count", function(){
-          this.statusMessage.set({likes_count : 1})
-          var view = new app.views.Post({model : this.statusMessage}).render();
-
-          expect($(view.el).html()).toContain(Diaspora.I18n.t('stream.likes', {count: 1}))
-        })
-        it("does not display a like count for 'zero'", function(){
-          this.statusMessage.set({likes_count : 0})
-          var view = new app.views.Post({model : this.statusMessage}).render();
-
-          expect($(view.el).html()).not.toContain("0 Likes")
-        })
-    })
-
-    context("embed_html", function(){
-      it("provides oembed html from the model response", function(){
-        this.statusMessage.set({"o_embed_cache" : {
-          "data" : {
-            "html" : "some html"
-          }
-        }})
-
-        var view = new app.views.Content({model : this.statusMessage});
-        expect(view.presenter().o_embed_html).toContain("some html")
-      })
-
-      it("does not provide oembed html from the model response if none is present", function(){
-        this.statusMessage.set({"o_embed_cache" : null})
-
-        var view = new app.views.Content({model : this.statusMessage});
-        expect(view.presenter().o_embed_html).toBe("");
-      })
-    })
-
-    context("user not signed in", function(){
-      it("does not provide a Feedback view", function(){
-        logout()
-        var view = new app.views.Post({model : this.statusMessage}).render();
-        expect(view.feedbackView()).toBeFalsy();
-      })
-    })
-
-    context("NSFW", function(){
-      beforeEach(function(){
-        this.statusMessage.set({nsfw: true});
-        this.view = new app.views.Post({model : this.statusMessage}).render();
-
-        this.hiddenPosts = function(){
-           return this.view.$(".nsfw-shield")
-         }
-      });
-
-      it("contains a shield element", function(){
-        expect(this.hiddenPosts().length).toBe(1)
-      });
-
-      it("does not contain a shield element when nsfw is false", function(){
-        this.statusMessage.set({nsfw: false});
-        this.view.render();
-        expect(this.hiddenPosts()).not.toExist();
-      })
-
-      context("showing a single post", function(){
-        it("removes the shields when the post is clicked", function(){
-          expect(this.hiddenPosts()).toExist();
-          this.view.$(".nsfw-shield .show_nsfw_post").click();
-          expect(this.hiddenPosts()).not.toExist();
-        });
-      });
-
-      context("clicking the toggle nsfw link toggles it on the user", function(){
-        it("calls toggleNsfw on the user", function(){
-          spyOn(app.user(), "toggleNsfwState")
-          this.view.$(".toggle_nsfw_state").first().click();
-          expect(app.user().toggleNsfwState).toHaveBeenCalled();
-        });
-      })
-    })
-
-    context("user views their own post", function(){
-      beforeEach(function(){
-        this.statusMessage.set({ author: {
-          id : app.user().id
-        }});
-        this.view = new app.views.Post({model : this.statusMessage}).render();
-      })
-
-      it("contains remove post", function(){
-        expect(this.view.$(".remove_post")).toExist();
-      })
-
-      it("destroys the view when they delete a their post from the show page", function(){
-        spyOn(window, "confirm").andReturn(true);
-
-        this.view.$(".remove_post").click();
-
-        expect(window.confirm).toHaveBeenCalled();
-        expect(this.view).not.toExist();
-      })
-    })
-
-    context("markdown rendering", function() {
-      beforeEach(function() {
-        // example from issue #2665
-        this.evilUrl  = "http://www.bürgerentscheid-krankenhäuser.de";
-        this.asciiUrl = "http://www.xn--brgerentscheid-krankenhuser-xkc78d.de";
-      });
-
-      it("correctly handles non-ascii characters in urls", function() {
-        this.statusMessage.set({text: "<"+this.evilUrl+">"});
-        var view = new app.views.Post({model : this.statusMessage}).render();
-
-        expect($(view.el).html()).toContain(this.asciiUrl);
-        expect($(view.el).html()).toContain(this.evilUrl);
-      });
-
-      it("doesn't break link texts for non-ascii urls", function() {
-        var linkText = "check out this awesome link!";
-        this.statusMessage.set({text: "["+linkText+"]("+this.evilUrl+")"});
-        var view = new app.views.Post({model: this.statusMessage}).render();
-
-        expect($(view.el).html()).toContain(this.asciiUrl);
-        expect($(view.el).html()).toContain(linkText);
-      });
-
-      it("doesn't break reference style links for non-ascii urls", function() {
-        var postContent = "blabla blab [my special link][1] bla blabla\n\n[1]: "+this.evilUrl+" and an optional title)";
-        this.statusMessage.set({text: postContent});
-        var view = new app.views.Post({model: this.statusMessage}).render();
-
-        expect($(view.el).html()).not.toContain(this.evilUrl);
-        expect($(view.el).html()).toContain(this.asciiUrl);
-      });
-
-      it("correctly handles images with non-ascii urls", function() {
-        var postContent = "![logo](http://bündnis-für-krankenhäuser.de/wp-content/uploads/2011/11/cropped-logohp.jpg)";
-        var niceImg = '"http://xn--bndnis-fr-krankenhuser-i5b27cha.de/wp-content/uploads/2011/11/cropped-logohp.jpg"';
-        this.statusMessage.set({text: postContent});
-        var view = new app.views.Post({model: this.statusMessage}).render();
-
-        expect($(view.el).html()).toContain(niceImg);
-      });
-
-    });
-  })
-});
+  //check out StreamPost
+})
diff --git a/spec/javascripts/app/views/services_selector_view_spec.js b/spec/javascripts/app/views/services_selector_view_spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..afa19486a578339921c68b8f4c6b25f3c40d508f
--- /dev/null
+++ b/spec/javascripts/app/views/services_selector_view_spec.js
@@ -0,0 +1,23 @@
+describe("app.views.ServicesSelector", function(){
+  beforeEach(function(){
+    loginAs(factory.user({
+      services : [
+        { provider : "fakeBook" }
+      ]
+    }));
+
+    this.view = new app.views.ServicesSelector();
+  });
+
+  describe("rendering", function(){
+    beforeEach(function(){
+      this.view.render();
+    });
+
+    it("displays all services", function(){
+      var checkboxes = $(this.view.el).find('input[type="checkbox"]');
+
+      expect(checkboxes.val()).toBe("fakeBook");
+    });
+  });
+});
\ No newline at end of file
diff --git a/spec/javascripts/app/views/stream_post_spec.js b/spec/javascripts/app/views/stream_post_spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..7b0f545f67e86393292ef71127ea2684ca99f6fe
--- /dev/null
+++ b/spec/javascripts/app/views/stream_post_spec.js
@@ -0,0 +1,148 @@
+describe("app.views.StreamPost", function(){
+  beforeEach(function(){
+    this.PostViewClass = app.views.StreamPost
+  })
+
+  describe("#render", function(){
+    beforeEach(function(){
+      loginAs({name: "alice", avatar : {small : "http://avatar.com/photo.jpg"}});
+
+      Diaspora.I18n.loadLocale({stream : {
+        reshares : {
+          one : "<%= count %> reshare",
+          other : "<%= count %> reshares"
+        },
+        likes : {
+          zero : "<%= count %> Likes",
+          one : "<%= count %> Like",
+          other : "<%= count %> Likes"
+        }
+      }})
+
+      var posts = $.parseJSON(spec.readFixture("stream_json"))["posts"];
+
+      this.collection = new app.collections.Posts(posts);
+      this.statusMessage = this.collection.models[0];
+      this.reshare = this.collection.models[1];
+    })
+
+    context("reshare", function(){
+      it("displays a reshare count", function(){
+        this.statusMessage.set({reshares_count : 2})
+        var view = new this.PostViewClass({model : this.statusMessage}).render();
+
+        expect($(view.el).html()).toContain(Diaspora.I18n.t('stream.reshares', {count: 2}))
+      })
+
+      it("does not display a reshare count for 'zero'", function(){
+        this.statusMessage.set({reshares_count : 0})
+        var view = new this.PostViewClass({model : this.statusMessage}).render();
+
+        expect($(view.el).html()).not.toContain("0 Reshares")
+      })
+    })
+
+    context("likes", function(){
+      it("displays a like count", function(){
+        this.statusMessage.set({likes_count : 1})
+        var view = new this.PostViewClass({model : this.statusMessage}).render();
+
+        expect($(view.el).html()).toContain(Diaspora.I18n.t('stream.likes', {count: 1}))
+      })
+      it("does not display a like count for 'zero'", function(){
+        this.statusMessage.set({likes_count : 0})
+        var view = new this.PostViewClass({model : this.statusMessage}).render();
+
+        expect($(view.el).html()).not.toContain("0 Likes")
+      })
+    })
+
+    context("embed_html", function(){
+      it("provides oembed html from the model response", function(){
+        this.statusMessage.set({"o_embed_cache" : {
+          "data" : {
+            "html" : "some html"
+          }
+        }})
+
+        var view = new app.views.Content({model : this.statusMessage});
+        expect(view.presenter().o_embed_html).toContain("some html")
+      })
+
+      it("does not provide oembed html from the model response if none is present", function(){
+        this.statusMessage.set({"o_embed_cache" : null})
+
+        var view = new app.views.Content({model : this.statusMessage});
+        expect(view.presenter().o_embed_html).toBe("");
+      })
+    })
+
+    context("user not signed in", function(){
+      it("does not provide a Feedback view", function(){
+        logout()
+        var view = new this.PostViewClass({model : this.statusMessage}).render();
+        expect(view.feedbackView()).toBeFalsy();
+      })
+    })
+
+    context("NSFW", function(){
+      beforeEach(function(){
+        this.statusMessage.set({nsfw: true});
+        this.view = new this.PostViewClass({model : this.statusMessage}).render();
+
+        this.hiddenPosts = function(){
+          return this.view.$(".nsfw-shield")
+        }
+      });
+
+      it("contains a shield element", function(){
+        expect(this.hiddenPosts().length).toBe(1)
+      });
+
+      it("does not contain a shield element when nsfw is false", function(){
+        this.statusMessage.set({nsfw: false});
+        this.view.render();
+        expect(this.hiddenPosts()).not.toExist();
+      })
+
+      context("showing a single post", function(){
+        it("removes the shields when the post is clicked", function(){
+          expect(this.hiddenPosts()).toExist();
+          this.view.$(".nsfw-shield .show_nsfw_post").click();
+          expect(this.hiddenPosts()).not.toExist();
+        });
+      });
+
+      context("clicking the toggle nsfw link toggles it on the user", function(){
+        it("calls toggleNsfw on the user", function(){
+          spyOn(app.user(), "toggleNsfwState")
+          this.view.$(".toggle_nsfw_state").first().click();
+          expect(app.user().toggleNsfwState).toHaveBeenCalled();
+        });
+      })
+    })
+
+    context("user views their own post", function(){
+      beforeEach(function(){
+        this.statusMessage.set({ author: {
+          id : app.user().id
+        }});
+        this.view = new this.PostViewClass({model : this.statusMessage}).render();
+      })
+
+      it("contains remove post", function(){
+        expect(this.view.$(".remove_post")).toExist();
+      })
+
+      it("destroys the view when they delete a their post from the show page", function(){
+        spyOn(window, "confirm").andReturn(true);
+
+        this.view.$(".remove_post").click();
+
+        expect(window.confirm).toHaveBeenCalled();
+        expect(this.view).not.toExist();
+      })
+    })
+
+  })
+});
diff --git a/spec/javascripts/app/views/template_picker_view_spec.js b/spec/javascripts/app/views/template_picker_view_spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..7b1e8ef19b92c0c93a1aea16c35c0987db1e624b
--- /dev/null
+++ b/spec/javascripts/app/views/template_picker_view_spec.js
@@ -0,0 +1,28 @@
+describe("app.views.TemplatePicker", function(){
+  beforeEach(function(){
+    this.model = factory.statusMessage({frame_name: undefined})
+    this.view = new app.views.TemplatePicker({model : this.model })
+  })
+
+  describe("initialization", function(){
+    it("sets the frame_name of the model to 'status' by default", function(){
+      expect(this.view.model.get("frame_name")).toBe("status")
+    })
+  })
+
+  describe("rendering", function(){
+    beforeEach(function(){
+      this.view.render()
+    })
+
+    it("selects the model's frame_name from the dropdown", function(){
+      expect(this.view.$("select[name=template]").val()).toBe("status")
+    })
+
+    it("changes the frame_name on the model when is is selected", function(){
+      this.view.$("select[name=template]").val("note")
+      this.view.$("select[name=template]").trigger("change")
+      expect(this.model.get("frame_name")).toBe('note')
+    })
+  })
+})
\ No newline at end of file
diff --git a/spec/javascripts/helpers/factory.js b/spec/javascripts/helpers/factory.js
index 03be9097c5a76de23db0b7badbe71a0caa57c3eb..b1ff242729f6eff4c9fc5074106b68026e36ee2a 100644
--- a/spec/javascripts/helpers/factory.js
+++ b/spec/javascripts/helpers/factory.js
@@ -33,6 +33,10 @@ factory = {
     return new app.models.Comment(_.extend(defaultAttrs, overrides))
   },
 
+  user : function(overrides) {
+    return new app.models.User(factory.userAttrs(overrides))
+  },
+
   userAttrs : function(overrides){
     var id = this.id.next()
     var defaultAttrs = {
@@ -48,8 +52,8 @@ factory = {
     return _.extend(defaultAttrs, overrides)
   },
 
-  post :  function(overrides) {
-    var defaultAttrs = {
+  postAttrs : function(){
+    return  {
       "provider_display_name" : null,
       "created_at" : "2012-01-03T19:53:13Z",
       "interacted_at" : '2012-01-03T19:53:13Z',
@@ -57,7 +61,6 @@ factory = {
       "public" : false,
       "guid" : this.guid(),
       "image_url" : null,
-      "author" : this.author(),
       "o_embed_cache" : null,
       "photos" : [],
       "text" : "jasmine is bomb",
@@ -69,10 +72,32 @@ factory = {
       "likes_count" : 0,
       "comments_count" : 0
     }
+  },
 
+  photoAttrs : function(overrides){
+    return _.extend({
+      author: factory.userAttrs(),
+      created_at: "2012-03-27T20:11:52Z",
+      guid: "8b0db16a4c4307b2",
+      id: 117,
+      sizes: {
+          large: "http://localhost:3000/uploads/images/scaled_full_d85410bd19db1016894c.jpg",
+          medium: "http://localhost:3000/uploads/images/thumb_medium_d85410bd19db1016894c.jpg",
+          small: "http://localhost:3000/uploads/images/thumb_small_d85410bd19db1016894c.jpg"
+        }
+    }, overrides)
+  },
+
+  post :  function(overrides) {
+    defaultAttrs = _.extend(factory.postAttrs(),  {"author" : this.author()})
     return new app.models.Post(_.extend(defaultAttrs, overrides))
   },
 
+  statusMessage : function(overrides){
+    //intentionally doesn't have an author to mirror creation process, maybe we should change the creation process
+    return new app.models.StatusMessage(_.extend(factory.postAttrs(), overrides))
+  },
+
   comment: function(overrides) {
     var defaultAttrs = {
       "text" : "This is an awesome comment!",
diff --git a/spec/javascripts/support/jasmine.yml b/spec/javascripts/support/jasmine.yml
index 83fbd18746b400064e1c23f26198310d2948de28..81f68950c3f4adeac06ce98bc0ac76b00ad02fec 100644
--- a/spec/javascripts/support/jasmine.yml
+++ b/spec/javascripts/support/jasmine.yml
@@ -18,8 +18,8 @@ src_files:
   - public/javascripts/vendor/underscore.js
   - public/javascripts/vendor/jquery-1.7.1.min.js
   - public/javascripts/vendor/jquery-ui-1.8.9.custom.min.js
-  - public/javascripts/vendor/bootstrap/bootstrap-popover.js
   - public/javascripts/vendor/bootstrap/bootstrap-twipsy.js
+  - public/javascripts/vendor/bootstrap/bootstrap-popover.js
   - public/javascripts/vendor/jquery.tipsy.js
   - public/javascripts/vendor/jquery.infinitescroll.min.js
   - public/javascripts/vendor/jquery.autoresize.js
@@ -46,6 +46,7 @@ src_files:
   - public/javascripts/app/helpers/*
   - public/javascripts/app/router.js
   - public/javascripts/app/views.js
+  - public/javascripts/app/forms.js
   - public/javascripts/app/models/post.js
   - public/javascripts/app/models/*
   - public/javascripts/app/collections/*
@@ -53,6 +54,8 @@ src_files:
   - public/javascripts/app/views/content_view.js
   - public/javascripts/app/views/*.js
   - public/javascripts/app/views/**/*.js
+  - public/javascripts/app/pages/**/*.js
+  - public/javascripts/app/forms/**/*.js
 
   - public/javascripts/mobile.js
   - public/javascripts/contact-list.js
diff --git a/spec/presenters/aspect_presenter_spec.rb b/spec/presenters/aspect_presenter_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..d38b97f426f30166f8cb0642fc0f0cadba0e6188
--- /dev/null
+++ b/spec/presenters/aspect_presenter_spec.rb
@@ -0,0 +1,13 @@
+require 'spec_helper'
+
+describe AspectPresenter do
+  before do
+    @presenter = AspectPresenter.new(bob.aspects.first)
+  end
+
+  describe '#to_json' do
+    it 'works' do
+      @presenter.to_json.should be_present
+    end
+  end
+end
\ No newline at end of file
diff --git a/spec/presenters/service_presenter_spec.rb b/spec/presenters/service_presenter_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..fe3c8a418f95b77fdeb2569530f6b62741060830
--- /dev/null
+++ b/spec/presenters/service_presenter_spec.rb
@@ -0,0 +1,10 @@
+require 'spec_helper'
+
+describe ServicePresenter do
+  describe '#as_json' do
+    it 'includes the provider name of the json' do
+      presenter = ServicePresenter.new(stub(:provider => "fakebook"))
+      presenter.as_json[:provider].should == 'fakebook'
+    end
+  end
+end
\ No newline at end of file
diff --git a/spec/presenters/user_presenter_spec.rb b/spec/presenters/user_presenter_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..9406741c95b31907a91b242b41ffd8880ff66522
--- /dev/null
+++ b/spec/presenters/user_presenter_spec.rb
@@ -0,0 +1,29 @@
+require 'spec_helper'
+
+describe UserPresenter do
+  before do
+    @presenter = UserPresenter.new(bob)
+  end
+
+  describe '#to_json' do
+    it 'works' do
+      @presenter.to_json.should be_present
+    end
+  end
+
+  describe '#aspects' do
+    it 'provides an array of the jsonified aspects' do
+      aspect = bob.aspects.first
+      @presenter.aspects.first[:id].should == aspect.id
+      @presenter.aspects.first[:name].should == aspect.name
+    end
+  end
+
+  describe '#services' do
+    it 'provides an array of jsonifed services' do
+      fakebook = stub(:provider => 'fakebook')
+      bob.stub(:services).and_return([fakebook])
+      @presenter.services.should include(:provider => 'fakebook')
+    end
+  end
+end
\ No newline at end of file