diff --git a/Gemfile b/Gemfile index 13d32d1d..010bd089 100644 --- a/Gemfile +++ b/Gemfile @@ -44,6 +44,7 @@ gem "newrelic_rpm", "~> 3.9.6" gem "draper", "~> 0.11.1" gem "open_uri_redirections", "~> 0.1.4" gem "rack-timeout", "~> 0.1.0" +gem "font-awesome-rails" # background job queue gem "delayed_job", :git => "git://github.com/collectiveidea/delayed_job.git", :tag => "v2.1.4" diff --git a/Gemfile.lock b/Gemfile.lock index 8a46b313..5a008817 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -110,6 +110,8 @@ GEM faraday (0.9.0) multipart-post (>= 1.2, < 3) ffi (1.9.3) + font-awesome-rails (4.3.0.0) + railties (>= 3.2, < 5.0) fssm (0.2.10) haml (3.1.8) haml-rails (0.3.5) @@ -302,6 +304,7 @@ DEPENDENCIES draper (~> 0.11.1) exceptional (~> 2.0.32) fabrication (~> 1.2.0) + font-awesome-rails haml (~> 3.1.4) haml-rails (~> 0.3.4) i18n (~> 0.6.0) diff --git a/app/assets/javascripts/updates.js.coffee b/app/assets/javascripts/updates.js.coffee new file mode 100644 index 00000000..35106ca0 --- /dev/null +++ b/app/assets/javascripts/updates.js.coffee @@ -0,0 +1,25 @@ +jQuery -> + if $('.updates-scroll').length + $(window).scroll -> + next_button = $('#next_button') + prev_button = $('#prev_button') + url = next_button.attr('href') + + if (next_button.length || prev_button.length) && $(window).scrollTop() > $(document).height() - $(window).height() - 150 + next_button.addClass('hidden') + prev_button.addClass('hidden') + $('.loading-info').removeClass('hidden') + if url && $(window).scrollTop() > $(document).height() - $(window).height() - 2 + $('.pagination').html("
") + $.getScript(url) + $(window).scroll() + + popover = $('.scroll-help-popover') + $('.scroll-help').on 'click', (e) -> + e.stopPropagation() + if popover.hasClass('hidden') + popover.removeClass('hidden') + $(document).on 'click', -> + popover.addClass('hidden') + else + popover.addClass('hidden') diff --git a/app/assets/javascripts/user_edit.js.coffee b/app/assets/javascripts/user_edit.js.coffee new file mode 100644 index 00000000..adf46af8 --- /dev/null +++ b/app/assets/javascripts/user_edit.js.coffee @@ -0,0 +1,16 @@ +jQuery -> + if $('#profile').length + container = $('.infinite_scroll_toggle') + # if javascript is not supported, infinite scroll option is not displayed + container.removeClass('hidden') + checkbox = $('#user_infinite_scroll') + + container.on 'click', '.fa-toggle-on', (e) => + toggleIcon = $(e.target) + toggleIcon.removeClass('fa-toggle-on').addClass('fa-toggle-off') + checkbox.attr('checked', false) + + container.on 'click', '.fa-toggle-off', (e) => + toggleIcon = $(e.target) + toggleIcon.removeClass('fa-toggle-off').addClass('fa-toggle-on') + checkbox.attr('checked', true) diff --git a/app/assets/stylesheets/application.css.scss b/app/assets/stylesheets/application.css.scss index 08ff3256..98a4108e 100644 --- a/app/assets/stylesheets/application.css.scss +++ b/app/assets/stylesheets/application.css.scss @@ -15,3 +15,5 @@ @import "lib/base"; @import "partials/flash"; + +@import "font-awesome"; diff --git a/app/assets/stylesheets/content.scss.erb b/app/assets/stylesheets/content.scss.erb index 29ce2cbc..c8ae3327 100644 --- a/app/assets/stylesheets/content.scss.erb +++ b/app/assets/stylesheets/content.scss.erb @@ -10,6 +10,10 @@ } } +.hidden { + display: none; +} + /* ****************************** */ /* Updates Navigation */ /* ****************************** */ @@ -407,6 +411,13 @@ border-color: #522; } +.infinite_scroll_toggle { + span.fa { + margin-left: 9px; + cursor: pointer; + } +} + /* ****************************** */ /* Sidebar */ /* ****************************** */ diff --git a/app/assets/stylesheets/layout.scss.erb b/app/assets/stylesheets/layout.scss.erb index 40bd4b87..e5d2876f 100644 --- a/app/assets/stylesheets/layout.scss.erb +++ b/app/assets/stylesheets/layout.scss.erb @@ -93,6 +93,20 @@ h2.title { margin-top: 1em; } + .loading-info { + text-align: center; + padding-top: 7px; + p { + margin: 0; + span { + padding-top: 1px; + float: right; + margin-left: -20px; + cursor: pointer; + } + } + } + #next_button { float: right; margin-right: 0.5em; @@ -102,6 +116,53 @@ h2.title { margin-left: 0.5em; } } + + .popover { + position: absolute; + z-index: 1000; + width: 230px; + padding: 5px; + border: 1px solid rgba(0,0,0,.2); + border-radius: 6px; + box-shadow: 0 5px 10px rgba(0,0,0,.2); + background-color: #fff; + + .arrow { + + &, + &:after { + position: absolute; + display: block; + width: 0; + height: 0; + border-color: transparent; + border-style: solid; + } + + border-width: 11px; + left: 50%; + margin-left: -11px; + border-bottom-width: 0; + border-top-color: #999; + border-top-color: rgba(0,0,0,.25); + bottom: -11px; + + &:after { + border-width: 10px; + content: ""; + content: " "; + bottom: 1px; + margin-left: -10px; + border-bottom-width: 0; + border-top-color: #fff; + } + } + } + + .scroll-help-popover { + right: -102px; + bottom: 60px; + } } /* *******************************/ diff --git a/app/controllers/updates_controller.rb b/app/controllers/updates_controller.rb index 7e4b0571..e254d313 100644 --- a/app/controllers/updates_controller.rb +++ b/app/controllers/updates_controller.rb @@ -5,27 +5,35 @@ class UpdatesController < ApplicationController def index @title = "updates" @list_class = "all" + prepare_updates(Update) respond_to do |format| - format.html { render_index(Update) } + format.html { render :index, :layout => show_layout? } format.json { - @updates = Update - set_pagination render :json => @updates.map{ |u| UpdateJsonDecorator.decorate(u) } } + format.js end end def timeline if current_user @list_class = "friends" - render_index(current_user.timeline) + prepare_updates(current_user.timeline) + respond_to do |format| + format.html { render :index, :layout => show_layout? } + format.js { render :index } + end end end def replies @list_class = "mentions" - render_index(current_user.at_replies) + prepare_updates(current_user.at_replies) + respond_to do |format| + format.html { render :index, :layout => show_layout? } + format.js { render :index } + end end def export @@ -93,20 +101,13 @@ def destroy protected - # Manage page state - def set_pagination + # Prepare correct updates depending on request type + def prepare_updates(updates) set_params_page - @updates = @updates.paginate(:page => params[:page], :per_page => params[:per_page], :order => :created_at.desc) + @updates = updates.paginate(:page => params[:page], :per_page => params[:per_page], :order => :created_at.desc) set_pagination_buttons(@updates) end - # Render correct haml depending on request type - def render_index(updates) - @updates = updates - set_pagination - render :index, :layout => show_layout? - end - def process_params @update_id, @update_text = "", "" diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index 1aa6908b..72a2fd9f 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -44,6 +44,7 @@ def show format.json { render :json => @updates.map{ |u| UpdateJsonDecorator.decorate(u) } } + format.js { render 'updates/index', locals: { no_highlight: true } } end end diff --git a/app/models/user.rb b/app/models/user.rb index 4704062f..c9385672 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -42,6 +42,8 @@ def admin? # Global preference set via user's profile controlling the state of the Post to Twitter checkbox key :always_send_to_twitter, Integer, :default => 1 + key :infinite_scroll, Boolean, default: true + validate :email_already_confirmed validates_uniqueness_of :username, :allow_nil => :true, @@ -355,6 +357,8 @@ def update_profile!(params) self.always_send_to_twitter = params[:user] && params[:user][:always_send_to_twitter].to_i + self.infinite_scroll = params[:user][:infinite_scroll] if params[:user] && params[:user][:infinite_scroll] + # I can't figure out how to use a real rails validator to confirm that # password matches password_confirm, since these two attributes are # virtual and we only want to check this in this particular case of diff --git a/app/views/shared/_pagination.haml b/app/views/shared/_pagination.haml index 19e67312..e0c6b037 100644 --- a/app/views/shared/_pagination.haml +++ b/app/views/shared/_pagination.haml @@ -1,4 +1,19 @@ %nav.pagination + + .loading-info.hidden + .popover.scroll-help-popover.hidden + %b TIP: + You can disable infinite scroll in your + - if logged_in? + != "#{link_to 'profile settings', edit_user_path(current_user)}." + - else + profile settings. + .arrow + %p + Keep scrolling to load more data + %span.fa.fa-question-circle.fa-lg.scroll-help + %i.fa.fa-hand-o-down.fa-lg + - unless @previous_page.nil? %a.button{:href => @previous_page, :id => "prev_button", :rel => "previous"} « Previous diff --git a/app/views/updates/_list.html.haml b/app/views/updates/_list.html.haml index 3dd5cbd3..870b7419 100644 --- a/app/views/updates/_list.html.haml +++ b/app/views/updates/_list.html.haml @@ -1,51 +1,13 @@ - unless defined?(id) - id = (request.path_info.gsub("/", "_")[1..-1]) +- if logged_in? + - infinite_scroll_toggle = current_user.infinite_scroll ? "updates-scroll" : "" +- else + - infinite_scroll_toggle = controller_name == "static" ? "" : "updates-scroll" #messages - %ul.updates{:class => "#{list_class}#{(controller.controller_name == "updates" ? " has-update-form" : "")}", :id => id} + %ul.updates{:class => "#{list_class}#{(controller.controller_name == "updates" ? " has-update-form #{infinite_scroll_toggle}" : " #{infinite_scroll_toggle}")}", :id => id} - updates.each do |update| - - unless defined?(no_highlight) and no_highlight - - mine = current_user.nil? ? false : (update.author.user == current_user) - - mentioned = current_user.nil? ? false : update.mentioned?(current_user.username) - %li.update.hentry.message{:class => (mentioned ? "mention " : "") + (mine ? "mine " : ""), :id => "update-#{update.id}", "data-id" => update.id, "data-name" => update.author ? update.author.fully_qualified_name : ""} - - if update.author - .author.vcard - %a.url{:href => update.author.url, :rel => "user"} - = AuthorDecorator.decorate(update.author).avatar - %span.fn - %a.url{:href => update.author.url} - = update.author.display_name - (#{update.author.username}) - .entry-content - %span.message-text - != update.to_html - - .info - = TimeDecorator.decorate(update).permalink - - - if !update.referral.nil? - %span.in-reply - %a{:href => "/updates/#{update.referral.id}"} - in reply to - - if update.referral.author - %span.name= update.referral.author.username - - else - an update - - elsif !update.referral_url.nil? - %span.in-reply - %a{:href => update.referral_url} - in reply to - - .actions - -# when @timeline is true, this is a list on the user's page - -unless current_user.nil? or (current_user.author.id == update.author.id) - %a.share{:href => (controller.controller_name == "updates" ? "" : "/") + "?share=#{update.id}"} share - | - %a.reply{:href => (controller.controller_name == "updates" ? "" : "/") + "?reply=#{update.id}"} reply - - - if current_user and update.author.id == current_user.author.id - = form_tag "/updates/#{update.id}" do - %input{:type => "hidden", :name => "_method", :value => "delete"} - = submit_tag "I Regret This", :class => "remove-update", :confirm => t(:remove_update, :scope => :confirms) + = render update, no_highlight: defined?(no_highlight) != render :partial => "shared/pagination" diff --git a/app/views/updates/_update.html.haml b/app/views/updates/_update.html.haml new file mode 100644 index 00000000..464119b7 --- /dev/null +++ b/app/views/updates/_update.html.haml @@ -0,0 +1,43 @@ +- unless defined?(no_highlight) and no_highlight + - mine = current_user.nil? ? false : (update.author.user == current_user) + - mentioned = current_user.nil? ? false : update.mentioned?(current_user.username) +%li.update.hentry.message{:class => (mentioned ? "mention " : "") + (mine ? "mine " : ""), :id => "update-#{update.id}", "data-id" => update.id, "data-name" => update.author ? update.author.fully_qualified_name : ""} + - if update.author + .author.vcard + %a.url{:href => update.author.url, :rel => "user"} + = AuthorDecorator.decorate(update.author).avatar + %span.fn + %a.url{:href => update.author.url} + = update.author.display_name + (#{update.author.username}) + .entry-content + %span.message-text + != update.to_html + + .info + = TimeDecorator.decorate(update).permalink + + - if !update.referral.nil? + %span.in-reply + %a{:href => "/updates/#{update.referral.id}"} + in reply to + - if update.referral.author + %span.name= update.referral.author.username + - else + an update + - elsif !update.referral_url.nil? + %span.in-reply + %a{:href => update.referral_url} + in reply to + + .actions + -# when @timeline is true, this is a list on the user's page + -unless current_user.nil? or (current_user.author.id == update.author.id) + %a.share{:href => (controller.controller_name == "updates" ? "" : "/") + "?share=#{update.id}"} share + | + %a.reply{:href => (controller.controller_name == "updates" ? "" : "/") + "?reply=#{update.id}"} reply + + - if current_user and update.author.id == current_user.author.id + = form_tag "/updates/#{update.id}" do + %input{:type => "hidden", :name => "_method", :value => "delete"} + = submit_tag "I Regret This", :class => "remove-update", :confirm => t(:remove_update, :scope => :confirms) diff --git a/app/views/updates/index.js.erb b/app/views/updates/index.js.erb new file mode 100644 index 00000000..b661adda --- /dev/null +++ b/app/views/updates/index.js.erb @@ -0,0 +1,21 @@ +$('.updates-scroll').append('<%= j render @updates, no_highlight: defined?(no_highlight) %>'); +<% if @updates.next_page %> + $('.pagination').replaceWith('<%= j raw render "shared/pagination" %>'); +<% else %> + $('.pagination').remove(); +<% end %> + +var popover; +popover = $('.scroll-help-popover'); + +$('.scroll-help').on('click', function(e) { + e.stopPropagation(); + if (popover.hasClass('hidden')) { + popover.removeClass('hidden'); + return $(document).on('click', function() { + return popover.addClass('hidden'); + }); + } else { + return popover.addClass('hidden'); + } +}); diff --git a/app/views/users/edit.haml b/app/views/users/edit.haml index 4a6ffefa..41e806af 100644 --- a/app/views/users/edit.haml +++ b/app/views/users/edit.haml @@ -20,6 +20,14 @@ = render :partial => "shared/field", :locals => {:obj => @user.author, :attr => :website, :params => params} = render :partial => "shared/field", :locals => {:obj => @user.author, :attr => :bio, :as => :text, :params => params} + .form-section.infinite_scroll_toggle.hidden + = f.check_box :infinite_scroll, class: 'hidden' + = label_tag '', "Updates' feed infinite scroll" + - if current_user.infinite_scroll + %span.fa.fa-toggle-on.fa-lg.infinite-scroll-on + - else + %span.fa.fa-toggle-off.fa-lg.inifinite-scroll-off + - if @user.twitter? .form-section.preferences .section-label