├── .gitignore ├── .kick ├── CONTRIBUTE_README.md ├── Gemfile ├── Gemfile.lock ├── LICENSE ├── Procfile ├── README.md ├── Rakefile ├── config.ru ├── config └── asset_sync.rb ├── lib ├── assets │ ├── fonts │ │ └── .gitkeep │ ├── images │ │ ├── appicon114.png │ │ ├── appicon57.png │ │ ├── appicon72.png │ │ ├── default_avatar.png │ │ └── favicon.png │ ├── javascripts │ │ ├── .gitkeep │ │ ├── application.js.coffee │ │ ├── collection.js.coffee │ │ ├── collection_pool.js.coffee │ │ ├── collections │ │ │ ├── .gitkeep │ │ │ ├── followers.js.coffee │ │ │ ├── followings.js.coffee │ │ │ ├── posts.js.coffee │ │ │ ├── reposts.js.coffee │ │ │ ├── search_results.js.coffee │ │ │ └── status_replies.js.coffee │ │ ├── config.js.coffee │ │ ├── core_ext │ │ │ ├── regexp.js.coffee │ │ │ ├── setImmediate.js.coffee │ │ │ └── string_score.js.coffee │ │ ├── fetch_interval.js.coffee │ │ ├── helpers │ │ │ ├── .gitkeep │ │ │ ├── formatting.js.coffee │ │ │ ├── general.js.coffee │ │ │ ├── input_selection.js.coffee │ │ │ ├── routes.js.coffee │ │ │ └── url.js.coffee │ │ ├── http │ │ │ └── tent_client │ │ │ │ ├── middleware.js.coffee │ │ │ │ └── middleware │ │ │ │ └── mac_auth.js.coffee │ │ ├── models │ │ │ ├── .gitkeep │ │ │ ├── meta_profile.js.coffee │ │ │ ├── pagination_link_header.js.coffee │ │ │ ├── post.js.coffee │ │ │ ├── post │ │ │ │ ├── cursor.js.coffee │ │ │ │ ├── follower.js.coffee │ │ │ │ ├── following.js.coffee │ │ │ │ ├── status.js.coffee │ │ │ │ ├── status │ │ │ │ │ └── reply.js.coffee │ │ │ │ └── subscription.js.coffee │ │ │ └── search_result.js.coffee │ │ ├── router.js.coffee │ │ ├── routers │ │ │ ├── .gitkeep │ │ │ ├── auth.js.coffee │ │ │ ├── follows.js.coffee │ │ │ ├── posts.js.coffee │ │ │ ├── profile.js.coffee │ │ │ └── search.js.coffee │ │ ├── services │ │ │ └── entity_search.js.coffee │ │ ├── static_config.js.erb │ │ ├── templates │ │ │ ├── .gitkeep │ │ │ ├── _404.js.lodash_template │ │ │ ├── _conversation_children.js.lodash_template │ │ │ ├── _conversation_parents.js.lodash_template │ │ │ ├── _new_following_form.js.lodash_template │ │ │ ├── _new_post_form.js.lodash_template │ │ │ ├── _post.js.lodash_template │ │ │ ├── _post_inner.js.lodash_template │ │ │ ├── _post_inner_actions.js.lodash_template │ │ │ ├── _post_reply_form.js.lodash_template │ │ │ ├── _profile_avatar.js.lodash_template │ │ │ ├── _profile_name.js.lodash_template │ │ │ ├── _repost.js.lodash_template │ │ │ ├── conversation.js.lodash_template │ │ │ ├── conversation_child_posts.js.lodash_template │ │ │ ├── conversation_parent_posts.js.lodash_template │ │ │ ├── edit_post.js.lodash_template │ │ │ ├── feed.js.lodash_template │ │ │ ├── fetch_posts_pool.js.lodash_template │ │ │ ├── followers.js.lodash_template │ │ │ ├── mentions.js.lodash_template │ │ │ ├── mentions_autocomplete_textarea_container.js.lodash_template │ │ │ ├── mentions_unread_count.js.lodash_template │ │ │ ├── mini_profile.js.lodash_template │ │ │ ├── not_found.js.lodash_template │ │ │ ├── permissions_fields.js.lodash_template │ │ │ ├── permissions_fields_options.js.lodash_template │ │ │ ├── permissions_fields_picker.js.lodash_template │ │ │ ├── permissions_fields_toggle.js.lodash_template │ │ │ ├── posts_feed.js.lodash_template │ │ │ ├── profile.js.lodash_template │ │ │ ├── profile │ │ │ │ └── resource_count.js.lodash_template │ │ │ ├── relationship.js.lodash_template │ │ │ ├── relationships_feed.js.lodash_template │ │ │ ├── repost_visibility.js.lodash_template │ │ │ ├── reposts.js.lodash_template │ │ │ ├── search.js.lodash_template │ │ │ ├── search_form.js.lodash_template │ │ │ ├── search_form_advanced_options.js.lodash_template │ │ │ ├── search_form_advanced_options_toggle.js.lodash_template │ │ │ ├── search_hits.js.lodash_template │ │ │ ├── signin.js.lodash_template │ │ │ ├── single_post.js.lodash_template │ │ │ ├── site_feed.js.lodash_template │ │ │ ├── subscribers.js.lodash_template │ │ │ ├── subscription.js.lodash_template │ │ │ ├── subscription_toggle.js.lodash_template │ │ │ ├── subscriptions.js.lodash_template │ │ │ └── subscriptions_feed.js.lodash_template │ │ ├── tent_status.js.coffee │ │ ├── unified_collection.js.coffee │ │ ├── unified_collection_pool.js.coffee │ │ └── views │ │ │ ├── .gitkeep │ │ │ ├── 001_permissions_fields.js.coffee │ │ │ ├── 002_permissions_fields_picker.js.coffee │ │ │ ├── 003_mentions_auto_complete_textarea_container.js.coffee │ │ │ ├── 003_permissions_fields_options.js.coffee │ │ │ ├── 004_mentions_auto_complete_textarea.js.coffee │ │ │ ├── app_navigation.js.coffee │ │ │ ├── app_navigation_item.js.coffee │ │ │ ├── auth_button.js.coffee │ │ │ ├── container.js.coffee │ │ │ ├── external_link.js.coffee │ │ │ ├── feed.js.coffee │ │ │ ├── fetch_posts_pool.js.coffee │ │ │ ├── follower.js.coffee │ │ │ ├── full_width.js.coffee │ │ │ ├── global_navigation.js.coffee │ │ │ ├── loading_indicator.js.coffee │ │ │ ├── mentions.js.coffee │ │ │ ├── mentions_auto_complete_textarea │ │ │ └── inline_mentions_manager.js.coffee │ │ │ ├── mentions_unread_count.js.coffee │ │ │ ├── mini_profile.js.coffee │ │ │ ├── navigation_active.js.coffee │ │ │ ├── new_following_form.js.coffee │ │ │ ├── not_found.js.coffee │ │ │ ├── permissions_fields_toggle.js.coffee │ │ │ ├── post.js.coffee │ │ │ ├── post │ │ │ ├── 001_post_action.js.coffee │ │ │ ├── 002_new_post_form.js.coffee │ │ │ ├── conversation.js.coffee │ │ │ ├── conversation │ │ │ │ ├── 001_component.js.coffee │ │ │ │ ├── children.js.coffee │ │ │ │ ├── parents.js.coffee │ │ │ │ └── reference.js.coffee │ │ │ ├── edit_post.js.coffee │ │ │ ├── post_action_conversation.js.coffee │ │ │ ├── post_action_delete.js.coffee │ │ │ ├── post_action_edit.js.coffee │ │ │ ├── post_action_reply.js.coffee │ │ │ ├── post_action_repost.js.coffee │ │ │ ├── post_reply_form.js.coffee │ │ │ └── repost.js.coffee │ │ │ ├── posts_feed.js.coffee │ │ │ ├── posts_feed │ │ │ ├── mentions.js.coffee │ │ │ ├── profile.js.coffee │ │ │ ├── reposts.js.coffee │ │ │ └── site.js.coffee │ │ │ ├── profile.js.coffee │ │ │ ├── profile │ │ │ ├── resource_count.js.coffee │ │ │ └── resource_count │ │ │ │ ├── followers_count.js.coffee │ │ │ │ ├── posts_count.js.coffee │ │ │ │ └── subscription_count.js.coffee │ │ │ ├── profile_component.js.coffee │ │ │ ├── profile_component │ │ │ ├── avatar.js.coffee │ │ │ └── name.js.coffee │ │ │ ├── relationship.js.coffee │ │ │ ├── relative_timestamp.js.coffee │ │ │ ├── repost_visibility.js.coffee │ │ │ ├── reposts.js.coffee │ │ │ ├── search.js.coffee │ │ │ ├── search_fetch_pool.js.coffee │ │ │ ├── search_form.js.coffee │ │ │ ├── search_form_advanced_options.js.coffee │ │ │ ├── search_form_advanced_options_toggle.js.coffee │ │ │ ├── search_hits.js.coffee │ │ │ ├── search_results.js.coffee │ │ │ ├── signin.js.coffee │ │ │ ├── signin_form.js.coffee │ │ │ ├── single_post.js.coffee │ │ │ ├── site_feed.js.coffee │ │ │ ├── subscribers.js.coffee │ │ │ ├── subscribers_feed.js.coffee │ │ │ ├── subscription.js.coffee │ │ │ ├── subscription_toggle.js.coffee │ │ │ ├── subscriptions.js.coffee │ │ │ ├── subscriptions_feed.js.coffee │ │ │ ├── unread_count.js.coffee │ │ │ └── unread_count │ │ │ ├── mentions_unread_count.js.coffee │ │ │ └── reposts_unread_count.js.coffee │ └── stylesheets │ │ ├── .gitkeep │ │ ├── application.css.scss │ │ └── permissions.css.scss ├── tent-status.rb ├── tent-status │ ├── app.rb │ ├── app │ │ ├── asset_server.rb │ │ ├── authentication.rb │ │ ├── middleware.rb │ │ ├── render_view.rb │ │ └── serialize_response.rb │ ├── compiler.rb │ ├── model.rb │ ├── model │ │ └── user.rb │ ├── tasks │ │ ├── assets.rb │ │ └── layout.rb │ ├── utils.rb │ └── version.rb └── views │ ├── application.erb │ ├── config.json │ ├── global_nav.erb │ ├── nav.erb │ ├── oauth_confirm.erb │ ├── search_nav_links.erb │ └── status_nav_links.erb ├── tent-status.gemspec └── vendor └── assets └── javascripts ├── lodash.js ├── moment.js ├── sjcl.js ├── store.js ├── tent-markdown.js └── textarea_cursor_position.js /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | *.rbc 3 | .bundle 4 | .config 5 | coverage 6 | InstalledFiles 7 | lib/bundler/man 8 | pkg 9 | rdoc 10 | spec/reports 11 | test/tmp 12 | test/version_tmp 13 | tmp 14 | 15 | .env 16 | 17 | # YARD artifacts 18 | .yardoc 19 | _yardoc 20 | doc/ 21 | public 22 | 23 | # Added by Tommi Kaikkonen 24 | 25 | database.yml 26 | .sass-cache/ 27 | -------------------------------------------------------------------------------- /.kick: -------------------------------------------------------------------------------- 1 | process do |files| 2 | test_files = files.take_and_map do |file| 3 | if file =~ %r{^(spec|assets)/javascripts/(.+?)(_spec)?\.(js|coffee|js\.coffee)$} 4 | "spec/javascripts/#{$2}_spec.coffee" 5 | end 6 | end 7 | execute "bundle exec evergreen run" unless test_files.empty? 8 | end 9 | -------------------------------------------------------------------------------- /CONTRIBUTE_README.md: -------------------------------------------------------------------------------- 1 | ## Design 2 | 3 | ### Views 4 | 5 | There are two places to find views. The main layout, navigation, and authentication views are found in `lib/tent-status/views` (html/erb). Everything else is found in `assets/javascripts/templates` (html/lo-dash). 6 | 7 | Here are a few things you need to know: 8 | 9 | - Elements with `data-view='SomeViewName'` will cause `Marbles.Views.SomeViewName` view class to be initialized using that element. `ack SomeViewName assets/javascripts/views` is a good way to find the relevant CoffeeScript file. 10 | - Routers live in `assets/javascripts/routers` with easy to understand route maps at the top of each file. Look in these files to find the relevant view, and look in the view file to find the template name(s). 11 | - Views (the CoffeeScript classes, not tempaltes) live in `assets/javascripts/views` and reference their coresponding template name and any templates rendered inside of that template (partials). 12 | 13 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gemspec 4 | 5 | gem 'puma' 6 | 7 | gem 'rack-putty', :git => 'git://github.com/tent/rack-putty.git', :branch => 'master' 8 | gem 'tent-client', :git => 'git://github.com/tent/tent-client-ruby.git', :branch => 'master' 9 | gem 'hawk-auth', :git => 'git://github.com/tent/hawk-ruby.git', :branch => 'master' 10 | gem 'omniauth-tent', :git => 'git://github.com/tent/omniauth-tent.git', :branch => 'master' 11 | gem 'marbles-js', :git => 'git://github.com/jvatic/marbles-js.git', :branch => 'master' 12 | gem 'marbles-tent-client-js', :git => 'git://github.com/tent/marbles-tent-client-js.git', :branch => 'master' 13 | gem 'lodash-assets', :git => 'git://github.com/jvatic/lodash-assets.git', :branch => 'master' 14 | gem 'icing', :git => 'git://github.com/tent/icing.git', :branch => 'master' 15 | gem 'sequel-json', :git => 'git://github.com/tent/sequel-json.git', :branch => 'master' 16 | gem 'sprockets', :git => 'git://github.com/jvaill/sprockets.git', :branch => 'master' 17 | 18 | group :development do 19 | gem 'asset_sync', :git => 'git://github.com/titanous/asset_sync.git', :branch => 'fix-mime' 20 | end 21 | 22 | group :assets do 23 | gem 'uglifier' 24 | gem 'sprockets-rainpress' 25 | end 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013 Tent.is, LLC. All rights reserved. 2 | 3 | Redistribution and use in source and binary forms, with or without 4 | modification, are permitted provided that the following conditions are 5 | met: 6 | 7 | * Redistributions of source code must retain the above copyright 8 | notice, this list of conditions and the following disclaimer. 9 | * Redistributions in binary form must reproduce the above 10 | copyright notice, this list of conditions and the following disclaimer 11 | in the documentation and/or other materials provided with the 12 | distribution. 13 | * Neither the name of Tent.is, LLC nor the names of its 14 | contributors may be used to endorse or promote products derived from 15 | this software without specific prior written permission. 16 | 17 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 18 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 19 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 20 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 21 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 22 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 23 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 24 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 25 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: bundle exec puma -p $PORT 2 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'bundler/setup' 2 | require 'bundler/gem_tasks' 3 | 4 | require 'tent-status/tasks/assets' 5 | require 'tent-status/tasks/layout' 6 | 7 | task :compile => ["assets:precompile", "layout:compile"] do 8 | end 9 | -------------------------------------------------------------------------------- /config.ru: -------------------------------------------------------------------------------- 1 | lib = File.expand_path('../lib', __FILE__) 2 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 3 | 4 | require 'bundler' 5 | Bundler.require 6 | 7 | $stdout.sync = true 8 | 9 | require 'tent-status' 10 | require 'securerandom' 11 | 12 | map '/' do 13 | use Rack::Session::Cookie, :key => 'tent-status.session', 14 | :expire_after => 2592000, # 1 month 15 | :secret => ENV['SESSION_SECRET'] || SecureRandom.hex 16 | run TentStatus.new 17 | end 18 | -------------------------------------------------------------------------------- /config/asset_sync.rb: -------------------------------------------------------------------------------- 1 | require 'asset_sync' 2 | 3 | AssetSync.configure do |config| 4 | config.fog_provider = 'AWS' 5 | config.fog_directory = ENV['S3_BUCKET'] 6 | config.aws_access_key_id = ENV['AWS_ACCESS_KEY_ID'] 7 | config.aws_secret_access_key = ENV['AWS_SECRET_ACCESS_KEY'] 8 | config.prefix = "assets" 9 | config.public_path = Pathname("./public") 10 | config.gzip_compression = true 11 | config.always_upload = %w( manifest.json ) 12 | end 13 | -------------------------------------------------------------------------------- /lib/assets/fonts/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tent/tent-status/4b8e79350a99baf65ad34e116398a46f59f8acbf/lib/assets/fonts/.gitkeep -------------------------------------------------------------------------------- /lib/assets/images/appicon114.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tent/tent-status/4b8e79350a99baf65ad34e116398a46f59f8acbf/lib/assets/images/appicon114.png -------------------------------------------------------------------------------- /lib/assets/images/appicon57.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tent/tent-status/4b8e79350a99baf65ad34e116398a46f59f8acbf/lib/assets/images/appicon57.png -------------------------------------------------------------------------------- /lib/assets/images/appicon72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tent/tent-status/4b8e79350a99baf65ad34e116398a46f59f8acbf/lib/assets/images/appicon72.png -------------------------------------------------------------------------------- /lib/assets/images/default_avatar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tent/tent-status/4b8e79350a99baf65ad34e116398a46f59f8acbf/lib/assets/images/default_avatar.png -------------------------------------------------------------------------------- /lib/assets/images/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tent/tent-status/4b8e79350a99baf65ad34e116398a46f59f8acbf/lib/assets/images/favicon.png -------------------------------------------------------------------------------- /lib/assets/javascripts/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tent/tent-status/4b8e79350a99baf65ad34e116398a46f59f8acbf/lib/assets/javascripts/.gitkeep -------------------------------------------------------------------------------- /lib/assets/javascripts/application.js.coffee: -------------------------------------------------------------------------------- 1 | #= require ./tent_status 2 | 3 | TentStatus.once 'config:ready', -> TentStatus.run() 4 | -------------------------------------------------------------------------------- /lib/assets/javascripts/collection_pool.js.coffee: -------------------------------------------------------------------------------- 1 | TentStatus.CollectionPool = class CollectionPool 2 | MAX_OVERFLOW_SIZE: 100 3 | 4 | constructor: (collection) -> 5 | @collection_cid = collection.cid 6 | 7 | shadow_collection = @initShadowCollection(collection) 8 | 9 | @interval = new TentStatus.FetchInterval fetch_callback: @fetch 10 | 11 | collection.on 'reset', => @reset() 12 | collection.on 'prepend', => @updatePagination(collection.first()) 13 | 14 | @reset() 15 | 16 | initShadowCollection: (collection) => 17 | shadow_collection = new collection.constructor 18 | @shadow_collection_cid = shadow_collection.cid 19 | shadow_collection 20 | 21 | collection: => TentStatus.Collection.find(cid: @collection_cid) 22 | shadowCollection: => TentStatus.Collection.find(cid: @shadow_collection_cid) 23 | 24 | reset: => 25 | collection = @collection() 26 | shadow_collection = @shadowCollection() 27 | 28 | shadow_collection.empty() 29 | 30 | shadow_collection.params = collection.params 31 | shadow_collection.pagination = {} 32 | if collection.pagination.prev 33 | shadow_collection.pagination.prev = collection.pagination.prev 34 | else 35 | @updatePagination(collection.first()) 36 | 37 | @interval.reset() 38 | 39 | updatePagination: (latest_post) => 40 | shadow_collection = @shadowCollection() 41 | 42 | if latest_post 43 | shadow_collection.pagination.prev = { 44 | since: (latest_post.get('received_at') || latest_post.get('published_at')) + " " + latest_post.get('version.id') 45 | } 46 | else 47 | shadow_collection.pagination.prev = { 48 | since: (new Date) * 1 49 | } 50 | 51 | fetch: => 52 | @shadowCollection().fetchPrev success: @fetchSuccess, failure: @fetchFailure 53 | 54 | fetchSuccess: (models, res, xhr, params, options) => 55 | if models.length 56 | shadow_collection = @shadowCollection() 57 | 58 | size = shadow_collection.model_ids.length 59 | if size > @MAX_OVERFLOW_SIZE 60 | shadow_collection.empty() 61 | @trigger("pool:overflow", size) 62 | else 63 | @trigger("pool:expand", size) 64 | 65 | @updatePagination(shadow_collection.first()) 66 | @interval.reset() 67 | else 68 | @interval.increaseDelay() 69 | 70 | fetchFailure: (res, xhr, params, options) => 71 | @interval.increaseDelay() 72 | 73 | _.extend CollectionPool::, Marbles.Accessors, Marbles.Events 74 | -------------------------------------------------------------------------------- /lib/assets/javascripts/collections/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tent/tent-status/4b8e79350a99baf65ad34e116398a46f59f8acbf/lib/assets/javascripts/collections/.gitkeep -------------------------------------------------------------------------------- /lib/assets/javascripts/collections/followers.js.coffee: -------------------------------------------------------------------------------- 1 | TentStatus.Collections.Followers = class FollowersCollection extends TentStatus.Collection 2 | @model: TentStatus.Models.Follower 3 | @params: { 4 | limit: TentStatus.config.PER_PAGE 5 | } 6 | -------------------------------------------------------------------------------- /lib/assets/javascripts/collections/followings.js.coffee: -------------------------------------------------------------------------------- 1 | TentStatus.Collections.Followings = class FollowingsCollection extends TentStatus.Collection 2 | @model: TentStatus.Models.Following 3 | @params: { 4 | limit: TentStatus.config.PER_PAGE 5 | } 6 | -------------------------------------------------------------------------------- /lib/assets/javascripts/collections/posts.js.coffee: -------------------------------------------------------------------------------- 1 | TentStatus.Collections.Posts = class PostsCollection extends TentStatus.Collection 2 | @model: TentStatus.Models.Post 3 | @id_mapping_scope: ['entity', 'context'] 4 | @collection_name: 'posts_collection' 5 | 6 | @generateContext: (name, params) -> 7 | name + '+' + sjcl.codec.base64.fromBits(sjcl.codec.utf8String.toBits(JSON.stringify(params))) 8 | 9 | constructor: -> 10 | super 11 | 12 | # id mapping 13 | @set('entity', @options.entity || TentStatus.config.meta.content.entity) 14 | @set('context', @options.context || 'default') 15 | 16 | -------------------------------------------------------------------------------- /lib/assets/javascripts/collections/reposts.js.coffee: -------------------------------------------------------------------------------- 1 | TentStatus.Collections.Reposts = class RepostsCollection extends TentStatus.Collection 2 | @model: TentStatus.Models.Post 3 | @id_mapping_scope: ['entity', 'post_id', 'context'] 4 | @collection_name: 'reposts_collection' 5 | 6 | constructor: -> 7 | super 8 | 9 | # id mapping 10 | @set('entity', @options.entity || TentStatus.config.meta.content.entity) 11 | @set('post_id', @options.post_id) 12 | @set('context', @options.context || 'default') 13 | 14 | -------------------------------------------------------------------------------- /lib/assets/javascripts/collections/search_results.js.coffee: -------------------------------------------------------------------------------- 1 | TentStatus.Collections.SearchResults = class SearchResultsCollection extends TentStatus.Collection 2 | @id_mapping_scope: ['entity', 'context'] 3 | @collection_name: 'posts_collection' 4 | 5 | constructor: (options = {}) -> 6 | @api_root = options.api_root 7 | throw new Error("#{@constructor.name} requires options.api_root!") unless @api_root 8 | 9 | super 10 | 11 | # id mapping 12 | @set('entity', @options.entity || TentStatus.config.meta.content.entity) 13 | @set('context', @options.context || 'default') 14 | 15 | fetch: (params = {}, options = {}) => 16 | client = new Marbles.HTTP.Client middleware: [Marbles.HTTP.Middleware.SerializeJSON] 17 | client.get( 18 | url: @api_root 19 | params: @searchParams(params) 20 | callback: (res, xhr) => @fetchComplete(params, options, res, xhr) 21 | ) 22 | 23 | searchParams: (params = {}) => 24 | params = _.clone(params) 25 | [q, entity, types] = [params.q || '', params.entity, params.types || TentStatus.config.feed_types] 26 | delete params.entity 27 | delete params.types 28 | 29 | params.api_key = TentStatus.config.services.search_api_key 30 | params.entity = entity if entity 31 | params.types = types 32 | 33 | params 34 | 35 | -------------------------------------------------------------------------------- /lib/assets/javascripts/collections/status_replies.js.coffee: -------------------------------------------------------------------------------- 1 | TentStatus.Collections.StatusReplies = class StatusRepliesCollection extends TentStatus.Collection 2 | @model: TentStatus.Models.StatusReplyPost 3 | @id_mapping_scope: ['entity', 'post_id'] 4 | @collection_name: 'replies_collection' 5 | 6 | constructor: -> 7 | super 8 | 9 | # id mapping 10 | @set('entity', @options.entity || TentStatus.config.meta.content.entity) 11 | @set('post_id', @options.post_id) 12 | -------------------------------------------------------------------------------- /lib/assets/javascripts/config.js.coffee: -------------------------------------------------------------------------------- 1 | #= require ./static_config 2 | #= require_self 3 | 4 | window.TentStatus ?= {} 5 | 6 | unless TentStatus.config.JSON_CONFIG_URL 7 | throw "json_config_url is required!" 8 | 9 | new Marbles.HTTP( 10 | method: 'GET' 11 | url: TentStatus.config.JSON_CONFIG_URL 12 | middleware: [Marbles.HTTP.Middleware.WithCredentials] 13 | callback: (res, xhr) -> 14 | if xhr.status != 200 15 | # Redirect to signin 16 | 17 | setImmediate -> 18 | TentStatus.run(history: { silent: true }) 19 | 20 | fragment = Marbles.history.getFragment() 21 | if fragment.match /^signin/ 22 | Marbles.history.navigate(fragment, trigger: true, replace: true) 23 | else 24 | if fragment == "" 25 | Marbles.history.navigate("/signin", trigger: true) 26 | else 27 | Marbles.history.navigate("/signin?redirect=#{encodeURIComponent(Marbles.history.getFragment())}", trigger: true) 28 | 29 | return 30 | 31 | TentStatus.config ?= {} 32 | for key, val of JSON.parse(res) 33 | TentStatus.config[key] = val 34 | 35 | TentStatus.config.authenticated = !!TentStatus.config.credentials 36 | 37 | TentStatus.tent_client = new TentClient( 38 | TentStatus.config.meta.content.entity, 39 | credentials: TentStatus.config.credentials 40 | server_meta_post: TentStatus.config.meta 41 | ) 42 | 43 | TentStatus.config_ready = true 44 | TentStatus.trigger?('config:ready') 45 | ) 46 | 47 | -------------------------------------------------------------------------------- /lib/assets/javascripts/core_ext/regexp.js.coffee: -------------------------------------------------------------------------------- 1 | RegExp.escape ?= (text) -> 2 | text.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&") 3 | -------------------------------------------------------------------------------- /lib/assets/javascripts/core_ext/setImmediate.js.coffee: -------------------------------------------------------------------------------- 1 | # TODO: use window.postMessage 2 | window.setImmediate ?= (-> 3 | window.clearImmediate = window.clearTimeout 4 | 5 | (fn, args...) -> 6 | setTimeout(fn, 0, args...) 7 | )() 8 | -------------------------------------------------------------------------------- /lib/assets/javascripts/core_ext/string_score.js.coffee: -------------------------------------------------------------------------------- 1 | String::score = (abbreviation) -> 2 | string = @toLowerCase() 3 | abbreviation = abbreviation.toLowerCase() 4 | 5 | return 1 if string == abbreviation 6 | 7 | index = string.indexOf(abbreviation) 8 | 9 | # only allow substrings to match 10 | return 0 if index == -1 11 | 12 | return 1 if index == 0 13 | 14 | abbreviation.length / string.length 15 | 16 | -------------------------------------------------------------------------------- /lib/assets/javascripts/fetch_interval.js.coffee: -------------------------------------------------------------------------------- 1 | class TentStatus.FetchInterval 2 | constructor: (options = {}) -> 3 | @options = _.extend { 4 | max_delay: TentStatus.config.MAX_FETCH_LATENCY 5 | delay_increment: TentStatus.config.FETCH_INTERVAL 6 | }, options 7 | 8 | start: => @reset() 9 | stop: => @clear() 10 | resume: => @resetInterval() 11 | 12 | resetInterval: => 13 | @clear() 14 | @_delay_interval = setInterval @options.fetch_callback, @delay_offset 15 | 16 | increaseDelay: => 17 | @delay_offset = Math.min(@delay_offset + @options.delay_increment, @options.max_delay - @options.delay_increment) 18 | @resetInterval() 19 | 20 | resetDelay: => 21 | @delay_offset = @options.delay_increment 22 | 23 | reset: => 24 | @resetDelay() 25 | @resetInterval() 26 | 27 | clear: => 28 | clearInterval @_delay_interval 29 | 30 | -------------------------------------------------------------------------------- /lib/assets/javascripts/helpers/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tent/tent-status/4b8e79350a99baf65ad34e116398a46f59f8acbf/lib/assets/javascripts/helpers/.gitkeep -------------------------------------------------------------------------------- /lib/assets/javascripts/helpers/formatting.js.coffee: -------------------------------------------------------------------------------- 1 | _.extend TentStatus.Helpers, 2 | formatRelativeTime: (timestamp_int) -> 3 | now = moment() 4 | time = moment(timestamp_int) 5 | 6 | formatted_time = time.fromNow() 7 | 8 | "#{formatted_time}" 9 | 10 | rawTime: (timestamp_int) -> 11 | moment(timestamp_int).format() 12 | 13 | formatCount: (count, options = {}) -> 14 | return count unless options.max && count > options.max 15 | "#{options.max}+" 16 | 17 | minimalEntity: (entity) -> 18 | @formatUrlWithPath(entity) 19 | 20 | formatUrlWithPath: (url = '') -> 21 | url.replace(/^\w+:\/\/(.*)$/, '$1') 22 | 23 | capitalize: (string) -> 24 | string.substr(0, 1).toUpperCase() + string.substr(1, string.length) 25 | 26 | pluralize: (word, count, plural) -> 27 | if count is 1 || count is -1 28 | word 29 | else 30 | plural 31 | 32 | # HTML escaping 33 | HTML_ENTITIES: { 34 | '&': '&', 35 | '>': '>', 36 | '<': '<', 37 | '"': '"', 38 | "'": ''' 39 | } 40 | htmlEscapeText: (text) -> 41 | return unless text 42 | text.replace /[&"'><]/g, (character) -> TentStatus.Helpers.HTML_ENTITIES[character] 43 | 44 | extractTrailingHtmlEntitiesFromText: (text) -> 45 | trailing_text = "" 46 | for char, entities of TentStatus.Helpers.HTML_ENTITIES 47 | regex = new RegExp("(#{TentStatus.Helpers.escapeRegExChars(entities)}?)$") 48 | if regex.test(text) 49 | trailing_text = text.match(regex)[1] + trailing_text 50 | text = text.replace(regex, "") 51 | [text, trailing_text] 52 | 53 | truncate: (text, length, elipses='...', options = {}) -> 54 | return text unless text 55 | if text.length > length 56 | _truncated = text.substr(0, length-elipses.length) 57 | _truncated += elipses 58 | else 59 | _truncated = text 60 | _truncated 61 | 62 | formatTentMarkdown: (text = '', mentions = []) -> 63 | inline_mention_urls = _.map mentions, (m) => TentStatus.Helpers.entityProfileUrl(m.entity) 64 | 65 | preprocessors = [] 66 | 67 | parsePara = (para, callback) -> 68 | new_para = for item in para 69 | if _.isArray(item) && item[0] in ['para', 'strong', 'em', 'del'] 70 | parsePara(item, callback) 71 | else if typeof item is 'string' 72 | callback(item) 73 | else 74 | item 75 | new_para 76 | 77 | externalLinkPreprocessor = (jsonml) -> 78 | return jsonml unless jsonml[0] is 'link' 79 | return jsonml unless TentStatus.Helpers.isURLExternal(jsonml[1]?.href) 80 | jsonml[1].href = TentStatus.Helpers.ensureUrlHasScheme(jsonml[1].href) 81 | jsonml[1]['data-view'] = 'ExternalLink' 82 | jsonml 83 | 84 | preprocessors.push(externalLinkPreprocessor) 85 | 86 | # Disable hashtag autolinking when search isn't enabled 87 | unless TentStatus.config.services.search_api_root 88 | disableHashtagAutolinking = (jsonml) -> 89 | return jsonml unless jsonml[0] is 'link' 90 | return jsonml unless jsonml[1]?.rel is 'hashtag' 91 | 92 | ['span', jsonml[2]] 93 | 94 | preprocessors.push(disableHashtagAutolinking) 95 | 96 | markdown.toHTML(text, 'Tent', { 97 | footnotes: inline_mention_urls 98 | hashtagURITemplate: @fullPath('/search') + '?q=%23{hashtag}' 99 | preprocessors: preprocessors 100 | }) 101 | 102 | expandTentMarkdown: (text, mentions = []) -> 103 | # Replace mention indices with entity URI 104 | text.replace(/(\^\[[^\]]+\])\((\d+)\)/g, (match, m1, m2) -> 105 | m1 + "(" + (mentions[m2]?.entity || '') + ")" 106 | ) 107 | 108 | -------------------------------------------------------------------------------- /lib/assets/javascripts/helpers/general.js.coffee: -------------------------------------------------------------------------------- 1 | _.extend TentStatus.Helpers, 2 | # Taken from http://mths.be/punycode 3 | decodeUCS: (string) -> 4 | chars = [] 5 | counter = 0 6 | length = string.length 7 | 8 | while counter < length 9 | value = string.charCodeAt(counter++) 10 | if value >= 0xD800 && value <= 0xDBFF && counter < length 11 | # high surrogate, and there is a next character 12 | extra = string.charCodeAt(counter++) 13 | if (extra & 0xFC00) == 0xDC00 # low surrogate 14 | chars.push(((value & 0x3FF) << 10) + (extra & 0x3FF) + 0x10000) 15 | else 16 | # unmatched surrogate; only append this code unit, in case the next 17 | # code unit is the high surrogate of a surrogate pair 18 | chars.push(value) 19 | counter-- 20 | else 21 | chars.push(value) 22 | 23 | chars 24 | 25 | numChars: (string) -> 26 | return 0 unless string 27 | @decodeUCS(string).length 28 | 29 | replaceIndexRange: (start_index, end_index, string, replacement) -> 30 | string.substr(0, start_index) + replacement + string.substr(end_index, string.length-1) 31 | 32 | substringIndices: (string, substring, invalid_after) -> 33 | return [] unless string and substring 34 | 35 | _indices = [] 36 | _length = substring.length 37 | _offset = 0 38 | while string.length 39 | i = string.substr(_offset, string.length).indexOf(substring) 40 | break if i == -1 41 | _start_index = i + _offset 42 | _end_index = _start_index + _length 43 | break if string.substr(_end_index, 1).match(invalid_after) if invalid_after 44 | _offset += i + _length 45 | _indices.push _start_index, _end_index 46 | 47 | _indices 48 | 49 | escapeRegExChars: (string) -> 50 | string ?= "" 51 | string.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&") 52 | -------------------------------------------------------------------------------- /lib/assets/javascripts/helpers/input_selection.js.coffee: -------------------------------------------------------------------------------- 1 | TentStatus.Helpers.InputSelection = Marbles.DOM.InputSelection 2 | -------------------------------------------------------------------------------- /lib/assets/javascripts/helpers/routes.js.coffee: -------------------------------------------------------------------------------- 1 | _.extend TentStatus.Helpers, 2 | entityProfileUrl: (entity) -> 3 | return unless entity 4 | @route('profile', entity: entity) 5 | 6 | route: (route_name, params = {}) -> 7 | switch route_name 8 | when 'root' 9 | @fullPath('/') 10 | when 'subscribers' 11 | if params.entity == TentStatus.config.meta.content.entity 12 | @fullPath('/subscribers') 13 | else 14 | @fullPath('/' + encodeURIComponent(params.entity) + '/subscribers') 15 | when 'subscriptions' 16 | if params.entity == TentStatus.config.meta.content.entity 17 | @fullPath('/subscriptions') 18 | else 19 | @fullPath('/' + encodeURIComponent(params.entity) + '/subscriptions') 20 | when 'profile' 21 | if @isAppDomain() 22 | if params.entity == TentStatus.config.meta.content.entity 23 | @fullPath("/profile") 24 | else 25 | @fullPath("/#{encodeURIComponent params.entity}/profile") 26 | else 27 | params.entity 28 | when 'post' 29 | "/posts/#{encodeURIComponent params.entity}/#{encodeURIComponent params.post_id}" 30 | 31 | fullPath: (path) -> 32 | (TentStatus.config.PATH_PREFIX || '').replace(/\/$/, '') + path 33 | -------------------------------------------------------------------------------- /lib/assets/javascripts/helpers/url.js.coffee: -------------------------------------------------------------------------------- 1 | _.extend TentStatus.Helpers, 2 | assertUrlHostsMatch: (url, other_url) -> 3 | uri = new Marbles.HTTP.URI(url) 4 | other_uri = new Marbles.HTTP.URI(other_url) 5 | 6 | (other_uri.hostname == uri.hostname) && 7 | (other_uri.port == uri.port) && 8 | (other_uri.scheme == uri.scheme) 9 | 10 | ensureUrlHasScheme: (url) -> 11 | return unless url 12 | return url if url.match /^[a-z]+:\/\//i 13 | return url if url.match /^\// # relative 14 | 'http://' + url 15 | 16 | isAppDomain: -> 17 | @assertUrlHostsMatch(window.location.href, TentStatus.config.APP_URL) 18 | 19 | isURLExternal: (url) -> 20 | return false unless url 21 | return false if url.match(/^\//) 22 | 23 | !@assertUrlHostsMatch(window.location.href, url) 24 | 25 | isCurrentUserEntity: (entity) -> 26 | return false unless TentStatus.config.meta 27 | uri = new Marbles.HTTP.URI(TentStatus.config.meta.content.entity) 28 | uri.assertEqual( new Marbles.HTTP.URI entity ) 29 | 30 | -------------------------------------------------------------------------------- /lib/assets/javascripts/http/tent_client/middleware.js.coffee: -------------------------------------------------------------------------------- 1 | #= require_tree ./middleware 2 | -------------------------------------------------------------------------------- /lib/assets/javascripts/http/tent_client/middleware/mac_auth.js.coffee: -------------------------------------------------------------------------------- 1 | #= require sjcl 2 | 3 | Marbles.HTTP.Middleware ||= {} 4 | Marbles.HTTP.Middleware.MacAuth = class MacAuthMiddleware 5 | constructor: (@options) -> 6 | @options = _.extend { 7 | time: parseInt((new Date * 1) / 1000) 8 | nonce: Math.random().toString(16).substring(3) 9 | }, @options 10 | 11 | process: (request, body) => 12 | @signRequest(request, body) 13 | 14 | signRequest: (request, body, options = @options) => 15 | request_string = @buildRequestString(request, body) 16 | hmac = new sjcl.misc.hmac(sjcl.codec.utf8String.toBits(options.mac_key)) 17 | signature = sjcl.codec.base64.fromBits(hmac.mac(request_string)) 18 | request.setHeader('Authorization', @buildAuthHeader(signature)) 19 | 20 | buildRequestString: (request, body, options = @options) => 21 | [options.time, options.nonce, request.method.toUpperCase(), request.path, request.host, request.port, null, null].join("\n") 22 | 23 | buildAuthHeader: (signature, options = @options) => 24 | """ 25 | MAC id="#{options.mac_key_id}", ts="#{options.time}", nonce="#{options.nonce}", mac="#{signature}" 26 | """ 27 | 28 | -------------------------------------------------------------------------------- /lib/assets/javascripts/models/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tent/tent-status/4b8e79350a99baf65ad34e116398a46f59f8acbf/lib/assets/javascripts/models/.gitkeep -------------------------------------------------------------------------------- /lib/assets/javascripts/models/meta_profile.js.coffee: -------------------------------------------------------------------------------- 1 | AVATAR_EXP_TIMESTAMP = 1376669460 + 3154000000 # 100 years from 2013-08-16 -0400 2 | 3 | TentStatus.Models.MetaProfile = class MetaProfileModel extends Marbles.Model 4 | @model_name: 'meta_profile' 5 | @id_mapping_scope: ['entity'] 6 | 7 | @post_type: new TentClient.PostType(TentStatus.config.POST_TYPES.META) 8 | 9 | @fetch: (entity, options = {}) -> 10 | if entity.hasOwnProperty?('entity') 11 | params = entity 12 | else 13 | params = {entity:entity} 14 | 15 | failureFn = (res, xhr) => 16 | @trigger("fetch:failure", params, res, xhr) 17 | @trigger("#{entity}:fetch:failure", params, res, xhr) 18 | options.failure?(res, xhr) 19 | options.complete?(res, xhr) 20 | 21 | if !TentStatus.tent_client.credentials 22 | failureFn() 23 | return 24 | 25 | completeFn = (res, xhr) => 26 | if xhr.status != 200 27 | failureFn(res, xhr) 28 | return 29 | 30 | constructorFn = @ 31 | server_meta_post = res.post 32 | 33 | attrs = _.extend({ 34 | id: server_meta_post.id 35 | entity: server_meta_post.content.entity 36 | avatar_digest: server_meta_post.attachments?[0]?.digest 37 | }, server_meta_post.content.profile || {}) 38 | 39 | model = constructorFn.find(entity, fetch: false) 40 | 41 | if model 42 | model.parseAttributes(attrs) 43 | else 44 | model = new constructorFn(attrs) 45 | 46 | @trigger("fetch:success", model, xhr) 47 | @trigger("#{entity}:fetch:success", model, xhr) 48 | options.success?(model, xhr) 49 | options.complete?(res, xhr) 50 | 51 | TentStatus.tent_client.discover( 52 | params: params 53 | callback: completeFn 54 | ) 55 | 56 | constructor: -> 57 | @on 'change:avatar_digest', (digest) => 58 | if digest 59 | @set('avatar_url', TentStatus.tent_client.getSignedUrl('attachment', entity: @get('entity'), digest: digest, exp: AVATAR_EXP_TIMESTAMP)) 60 | else 61 | @set('avatar_url', TentStatus.config.defaultAvatarURL(@get('entity'))) 62 | 63 | super 64 | 65 | parseAttributes: => 66 | super 67 | 68 | unless @get('avatar_url') 69 | @set('avatar_url', TentStatus.config.defaultAvatarURL(@get('entity'))) 70 | 71 | fetch: (options = {}) => 72 | @constructor.fetch(@get('entity'), options) 73 | 74 | TentStatus.once 'config:ready', -> 75 | meta = TentStatus.config.meta 76 | TentStatus.meta_profile = new MetaProfileModel(_.extend( 77 | { 78 | entity: meta.content.entity, 79 | avatar_digest: meta.attachments?[0]?.digest 80 | }, 81 | meta.content.profile || {} 82 | )) 83 | -------------------------------------------------------------------------------- /lib/assets/javascripts/models/pagination_link_header.js.coffee: -------------------------------------------------------------------------------- 1 | TentStatus.PaginationLinkHeader = class PaginationLinkHeader extends Marbles.Object 2 | constructor: (link_header='') -> 3 | @pagination_params = {} 4 | parts = link_header.split(/,\s*/) 5 | for part in parts 6 | continue unless part.match(/<([^>]+)>;\s*rel=['"]([^'"]+)['"]/) 7 | continue unless RegExp.$2 in ['next', 'prev'] 8 | path = RegExp.$1 9 | params = Marbles.History::deserializeParams(path.split('?')[1]) 10 | @pagination_params[RegExp.$2] = params 11 | -------------------------------------------------------------------------------- /lib/assets/javascripts/models/post/cursor.js.coffee: -------------------------------------------------------------------------------- 1 | TentStatus.Models.CursorPost = class CursorPostModel extends TentStatus.Models.Post 2 | @model_name: 'cursor_post' 3 | @id_mapping_scope: ['type', 'entity'] 4 | 5 | @fetch: (params, options) -> 6 | callbackFn = (res, xhr) => 7 | if xhr.status == 200 && res.posts.length 8 | if post = @find(params, fetch: false) 9 | post.parseAttributes(res.posts[0]) 10 | else 11 | if params.cid 12 | post = new @(res.posts[0], cid: params.cid) 13 | else 14 | post = new @(res.posts[0]) 15 | 16 | if res.refs && res.refs.length 17 | post.ref_post = res.refs[0] 18 | 19 | options.success?(post, xhr) 20 | else 21 | options.failure?(res, xhr) 22 | 23 | options.complete?(res, xhr) 24 | 25 | TentStatus.tent_client.post.list( 26 | params: { 27 | types: params.type 28 | entities: params.entity 29 | max_refs: 1 30 | limit: 1 31 | }, 32 | callback: callbackFn 33 | ) 34 | 35 | fetch: (options = {}) => 36 | @constructor.fetch({ 37 | cid: @cid 38 | type: @get('type') 39 | entity: @get('entity') 40 | }, options) 41 | 42 | -------------------------------------------------------------------------------- /lib/assets/javascripts/models/post/follower.js.coffee: -------------------------------------------------------------------------------- 1 | TentStatus.Models.Follower = class FollowerModel extends TentStatus.Models.Post 2 | @model_name: 'follower' 3 | 4 | @fetchCount: (params, options = {}) -> 5 | params = _.extend(params, { 6 | types: TentStatus.config.subscriber_feed_types 7 | }) 8 | 9 | super(params, options) 10 | 11 | -------------------------------------------------------------------------------- /lib/assets/javascripts/models/post/following.js.coffee: -------------------------------------------------------------------------------- 1 | TentStatus.Models.Following = class FollowingModel extends TentStatus.Models.Post 2 | @model_name: 'following' 3 | 4 | @validate: (entity) -> 5 | null 6 | 7 | @discover: (entity, options) -> 8 | TentStatus.Models.MetaProfile.find({entity: entity}, options) 9 | 10 | @create: (entity, options) -> 11 | # TODO: 12 | # - show "pending" placeholder in list 13 | # - poll until relationship# post exists 14 | # - if delivery failure post exists for relationship, show warning/error 15 | 16 | @discover(entity, 17 | success: (meta_profile, xhr) => 18 | @createSubscriptions(entity, options) 19 | 20 | failure: (res, xhr) => 21 | options.failure?({error: "Discovery Failed"}, xhr) 22 | ) 23 | 24 | @fetchCount: (params, options = {}) -> 25 | params = _.extend(params, { 26 | types: TentStatus.config.subscription_count_types, 27 | }) 28 | 29 | super(params, options) 30 | 31 | @createSubscriptions: (entity, options) -> 32 | num_pending = 0 33 | errors = [] 34 | subscriptions = [] 35 | completeFn = (subscription, res, xhr) => 36 | num_pending -= 1 37 | 38 | if xhr.status == 200 39 | subscriptions.push(subscription) 40 | else 41 | errors.push(error: res?.error || "#{xhr.status}: #{JSON.stringify(res)}") 42 | 43 | if num_pending <= 0 44 | if errors.length 45 | options.failure?(errors) 46 | else 47 | options.success?(subscriptions) 48 | 49 | for type in TentStatus.config.subscription_types 50 | do (type) => 51 | type = new TentClient.PostType(type) 52 | subscription_type = new TentClient.PostType(TentStatus.config.POST_TYPES.SUBSCRIPTION) 53 | subscription_type.setFragment(type.toStringWithoutFragment()) 54 | 55 | num_pending += 1 56 | TentStatus.Models.Subscription.create({ 57 | type: subscription_type.toString() 58 | content: 59 | type: type.toString() 60 | mentions: [{ entity: entity }] 61 | permissions: 62 | public: true 63 | entities: [entity] 64 | }, { 65 | complete: (subscription, res, xhr) => 66 | completeFn(subscription, res, xhr) 67 | }) 68 | 69 | -------------------------------------------------------------------------------- /lib/assets/javascripts/models/post/status.js.coffee: -------------------------------------------------------------------------------- 1 | TentStatus.Models.StatusPost = class StatusPostModel extends TentStatus.Models.Post 2 | @model_name: 'post' 3 | @post_type: new TentClient.PostType(TentStatus.config.POST_TYPES.STATUS) 4 | 5 | @validate: (attrs, options = {}) -> 6 | errors = [] 7 | 8 | if (attrs.content?.text and attrs.content.text.match /^[\s\r\t]*$/) || (options.validate_empty and attrs.content?.text == "") 9 | errors.push { text: 'Status must not be empty' } 10 | 11 | if attrs.content?.text and TentStatus.Helpers.numChars(attrs.content.text) > TentStatus.config.MAX_STATUS_LENGTH 12 | errors.push { text: "Status must be no more than #{TentStatus.config.MAX_STATUS_LENGTH} characters" } 13 | 14 | return errors if errors.length 15 | null 16 | 17 | @fetchCount: (params, options = {}) -> 18 | params.types ?= [ 19 | @post_type.toStringWithoutFragment() 20 | ] 21 | 22 | super(params, options) 23 | 24 | fetchReplies: (options = {}) => 25 | num_pending_posts = 0 26 | models = {} 27 | mentions = [] 28 | 29 | keyForMention = (mention) => 30 | mention.entity + ' ' + mention.post 31 | 32 | fetchPostComplete = (mention, res, xhr) => 33 | num_pending_posts -= 1 34 | 35 | if xhr.status == 200 36 | postConstructor = TentStatus.Models.Post.constructorForType(res.post.type) 37 | models[keyForMention(mention)] = new postConstructor(res.post) 38 | 39 | if num_pending_posts <= 0 40 | _models = [] 41 | for mention in mentions 42 | _model = models[keyForMention(mention)] 43 | continue unless _model 44 | _models.push(_model) 45 | 46 | options.success?(_models) 47 | 48 | fetchPostFromMention = (mention) => 49 | TentStatus.tent_client.post.get( 50 | params: { 51 | entity: mention.entity || @get('entity') 52 | post: mention.post 53 | } 54 | 55 | headers: { 56 | 'Cache-Control': 'proxy-if-miss' 57 | } 58 | 59 | callback: (res, xhr) => 60 | fetchPostComplete(mention, res, xhr) 61 | ) 62 | 63 | mentionsCompleteFn = (res, xhr) => 64 | if xhr.status == 200 65 | num_pending_posts = res.mentions.length 66 | for mention in res.mentions 67 | continue unless mention.type == TentStatus.config.POST_TYPES.STATUS_REPLY 68 | mentions.push(mention) 69 | fetchPostFromMention(mention) 70 | else 71 | options.failure?(res, xhr) 72 | 73 | TentStatus.tent_client.post.mentions( 74 | params: { 75 | entity: @get('entity') 76 | post: @get('id') 77 | limit: TentStatus.config.CONVERSATION_PER_PAGE 78 | } 79 | 80 | headers: { 81 | 'Cache-Control': 'proxy' 82 | } 83 | 84 | callback: mentionsCompleteFn 85 | ) 86 | 87 | null 88 | 89 | -------------------------------------------------------------------------------- /lib/assets/javascripts/models/post/status/reply.js.coffee: -------------------------------------------------------------------------------- 1 | TentStatus.Models.StatusReplyPost = class StatusReplyPostModel extends TentStatus.Models.StatusPost 2 | @model_name: 'post' 3 | @post_type: new TentClient.PostType(TentStatus.config.POST_TYPES.STATUS_REPLY) 4 | 5 | -------------------------------------------------------------------------------- /lib/assets/javascripts/models/post/subscription.js.coffee: -------------------------------------------------------------------------------- 1 | TentStatus.Models.Subscription = class SubscriptionModel extends TentStatus.Models.Post 2 | @model_name: 'subscription' 3 | @id_mapping_scope: ['entity', 'target_entity', 'content.type'] 4 | 5 | parseAttributes: (attrs) => 6 | super 7 | 8 | @set('target_entity', @get('mentions')[0].entity) 9 | 10 | -------------------------------------------------------------------------------- /lib/assets/javascripts/models/search_result.js.coffee: -------------------------------------------------------------------------------- 1 | TentStatus.Models.SearchResult = class SearchResultModel extends Marbles.Model 2 | @model_name: 'search_result' 3 | @id_mapping_scope: ['id'] 4 | 5 | parseAttributes: (attributes) => 6 | _attrs = {} 7 | 8 | if profile = TentStatus.Models.Profile.find(entity: attributes.source.entity, fetch: false, include_partial_data: true) 9 | _attrs.profile_cid = profile.cid 10 | else 11 | _profile_attrs = {} 12 | _profile_attrs[TentStatus.config.PROFILE_TYPES.CORE] = { 13 | entity: attributes.source.entity 14 | } 15 | _profile_attrs[TentStatus.config.PROFILE_TYPES.BASIC] = { 16 | name: attributes.source.name 17 | avatar_url: attributes.source.avatar_url 18 | } 19 | profile = new TentStatus.Models.Profile(_profile_attrs, partial_data: true) 20 | 21 | _attrs.profile_cid = profile.cid 22 | 23 | if post = TentStatus.Models.Post.find(id: attributes.source.public_id, entity: attributes.source.entity, fetch: false, include_partial_data: true) 24 | _attrs.post_cid = post.cid 25 | else 26 | post = new TentStatus.Models.Post({ 27 | type: attributes.source.post_type 28 | content: { 29 | text: attributes.source.content 30 | } 31 | entity: attributes.source.entity 32 | id: attributes.source.public_id 33 | published_at: attributes.source.published_at 34 | version: attributes.source.post_version 35 | permissions: 36 | public: true 37 | }, partial_data: true) 38 | _attrs.post_cid = post.cid 39 | 40 | post.set('profile_cid', _attrs.profile_cid) 41 | super(_.extend(_attrs, highlight: attributes.highlight, id: attributes.id, published_at: attributes.source.published_at)) 42 | 43 | post: => 44 | Marbles.Model.instances.all[@post_cid] 45 | 46 | -------------------------------------------------------------------------------- /lib/assets/javascripts/router.js.coffee: -------------------------------------------------------------------------------- 1 | #= require_self 2 | #= require_tree ./routers 3 | 4 | TentStatus.Routers.default = new class DefaultRotuer extends Marbles.Router 5 | routes: { 6 | "*" : "notFound" 7 | } 8 | 9 | actions_titles: { 10 | notFound : "Not Found" 11 | } 12 | 13 | notFound: => 14 | TentStatus.setPageTitle @actions_titles.notFound 15 | new Marbles.Views.NotFound container: Marbles.Views.container 16 | 17 | -------------------------------------------------------------------------------- /lib/assets/javascripts/routers/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tent/tent-status/4b8e79350a99baf65ad34e116398a46f59f8acbf/lib/assets/javascripts/routers/.gitkeep -------------------------------------------------------------------------------- /lib/assets/javascripts/routers/auth.js.coffee: -------------------------------------------------------------------------------- 1 | TentStatus.Routers.posts = new class PostsRouter extends Marbles.Router 2 | routes: { 3 | "signin" : "signin" 4 | } 5 | 6 | actions_titles: { 7 | "signin" : "Sign in" 8 | } 9 | 10 | signin: (params) => 11 | if params.redirect && params.redirect.indexOf('//') != -1 && params.redirect.indexOf('//') == params.redirect.indexOf('/') 12 | params.redirect = null 13 | params.redirect ?= TentStatus.config.PATH_PREFIX || '/' 14 | 15 | if TentStatus.config.authenticated 16 | return Marbles.history.navigate(params.redirect, trigger: true) 17 | 18 | unless TentStatus.config.SIGNIN_URL 19 | return window.location.href = TentStatus.config.SIGNOUT_REDIRECT_URL 20 | 21 | Marbles.Views.AppNavigationItem.disableAll() 22 | 23 | TentStatus.setPageTitle page: @actions_titles.signin 24 | 25 | new Marbles.Views.Signin container: Marbles.Views.container, redirect_url: params.redirect 26 | 27 | -------------------------------------------------------------------------------- /lib/assets/javascripts/routers/follows.js.coffee: -------------------------------------------------------------------------------- 1 | TentStatus.Routers.follows = new class FollowsRouter extends Marbles.Router 2 | routes: { 3 | "subscriptions" : "subscriptions" 4 | ":entity/subscriptions" : "subscriptions" 5 | "subscribers" : "subscribers" 6 | ":entity/subscribers" : "subscribers" 7 | } 8 | 9 | actions_titles: { 10 | 'subscriptions' : 'Subscriptions' 11 | 'subscribers' : 'Subscribers' 12 | } 13 | 14 | subscriptions: (params) => 15 | new Marbles.Views.Subscriptions entity: (params.entity || TentStatus.config.meta.content.entity) 16 | 17 | title = @actions_titles.subscriptions 18 | title = "#{TentStatus.Helpers.formatUrlWithPath(params.entity)} - #{title}" if params.entity 19 | TentStatus.setPageTitle page: title 20 | 21 | subscribers: (params) => 22 | new Marbles.Views.Subscribers entity: (params.entity || TentStatus.config.meta.content.entity) 23 | 24 | title = @actions_titles.subscribers 25 | title = "#{TentStatus.Helpers.formatUrlWithPath(params.entity)} - #{title}" if params.entity 26 | TentStatus.setPageTitle page: title 27 | 28 | -------------------------------------------------------------------------------- /lib/assets/javascripts/routers/posts.js.coffee: -------------------------------------------------------------------------------- 1 | TentStatus.Routers.posts = new class PostsRouter extends Marbles.Router 2 | routes: { 3 | "" : "root" 4 | "posts" : "index" 5 | "site-feed" : "siteFeed" 6 | "mentions" : "mentions" 7 | "reposts" : "reposts" 8 | "posts/:id" : "post" 9 | "posts/:entity/:id" : "post" 10 | } 11 | 12 | actions_titles: { 13 | 'feed' : 'My Feed' 14 | 'siteFeed' : 'Site Feed' 15 | 'post' : 'Conversation' 16 | 'mentions' : 'Mentions' 17 | 'reposts' : 'Reposts' 18 | } 19 | 20 | _initMiniProfileView: (options = {}) => 21 | new Marbles.Views.MiniProfile _.extend options, 22 | el: document.getElementById('author-info') 23 | 24 | index: (params) => 25 | if TentStatus.config.guest 26 | return @navigate('/profile', {trigger:true, replace: true}) 27 | 28 | @feed(arguments...) 29 | 30 | root: => 31 | @index(arguments...) 32 | 33 | feed: (params) => 34 | new Marbles.Views.Feed 35 | @_initMiniProfileView(entity: TentStatus.config.meta.content.entity) 36 | TentStatus.setPageTitle page: @actions_titles.feed 37 | 38 | siteFeed: (params) => 39 | unless TentStatus.config.services.site_feed_api_root 40 | @navigate(TentStatus.Helpers.route('root'), trigger: true, replace: true) 41 | 42 | new Marbles.Views.SiteFeed 43 | @_initMiniProfileView() 44 | TentStatus.setPageTitle page: @actions_titles.siteFeed 45 | 46 | post: (params) => 47 | entity = params.entity || TentStatus.config.meta.content.entity 48 | new Marbles.Views.SinglePost entity: entity, id: params.id 49 | @_initMiniProfileView(entity: entity) 50 | TentStatus.setPageTitle page: @actions_titles.post 51 | 52 | mentions: (params) => 53 | params.entity ?= TentStatus.config.meta.content.entity 54 | new Marbles.Views.Mentions(entity: params.entity) 55 | @_initMiniProfileView(entity: params.entity) 56 | TentStatus.setPageTitle page: @actions_titles.mentions 57 | 58 | reposts: (params) => 59 | params.entity ?= TentStatus.config.meta.content.entity 60 | new Marbles.Views.Reposts(entity: params.entity) 61 | @_initMiniProfileView(entity: params.entity) 62 | TentStatus.setPageTitle page: @actions_titles.reposts 63 | 64 | -------------------------------------------------------------------------------- /lib/assets/javascripts/routers/profile.js.coffee: -------------------------------------------------------------------------------- 1 | TentStatus.Routers.profile = new class ProfileRouter extends Marbles.Router 2 | routes: { 3 | "profile" : "currentProfile" 4 | ":entity/profile" : "profile" 5 | } 6 | 7 | actions_titles: { 8 | "currentProfile" : "Profile" 9 | "profile" : "Profile" 10 | } 11 | 12 | _initMiniProfileView: (options = {}) => 13 | new Marbles.Views.MiniProfile _.extend options, 14 | el: document.getElementById('author-info') 15 | 16 | currentProfile: (params) => 17 | new Marbles.Views.Profile entity: TentStatus.config.meta.content.entity 18 | @_initMiniProfileView() 19 | TentStatus.setPageTitle page: @actions_titles.currentProfile 20 | 21 | profile: (params) => 22 | new Marbles.Views.Profile entity: params.entity 23 | 24 | title = @actions_titles.profile 25 | title = "#{TentStatus.Helpers.formatUrlWithPath(params.entity)} - #{title}" if params.entity 26 | TentStatus.setPageTitle page: title 27 | -------------------------------------------------------------------------------- /lib/assets/javascripts/routers/search.js.coffee: -------------------------------------------------------------------------------- 1 | TentStatus.Routers.search = new class SearchRouter extends Marbles.Router 2 | routes: { 3 | "search" : "search" 4 | } 5 | 6 | actions_titles: { 7 | 'search' : 'Skate Search' 8 | } 9 | 10 | _initMiniProfileView: (options = {}) => 11 | new Marbles.Views.MiniProfile _.extend options, 12 | el: document.getElementById('author-info') 13 | 14 | search: (params) => 15 | if !TentStatus.config.services.search_api_root 16 | return @navigate('/', {trigger: true, replace: true}) 17 | 18 | new Marbles.Views.Search(params: params, container: Marbles.Views.container) 19 | @_initMiniProfileView() 20 | 21 | TentStatus.setPageTitle page: @actions_titles.search 22 | -------------------------------------------------------------------------------- /lib/assets/javascripts/services/entity_search.js.coffee: -------------------------------------------------------------------------------- 1 | class EntitySearchService 2 | constructor: (@options = {}) -> 3 | @client = new Marbles.HTTP.Client(middleware: [Marbles.HTTP.Middleware.SerializeJSON]) 4 | 5 | # callback can either be a function or an object: 6 | # - success: fn 7 | # - error: fn 8 | # - complete: fn 9 | search: (query, callback) => 10 | @client.get(url: @options.api_root, params: { q: query }, callback: callback) 11 | 12 | _.extend EntitySearchService::, Marbles.Events 13 | _.extend EntitySearchService::, Marbles.Accessors 14 | 15 | if (api_root = TentStatus.config.entity_search_api_root) 16 | TentStatus.services ?= {} 17 | TentStatus.services.entity_search = new EntitySearchService(api_root: api_root) 18 | 19 | -------------------------------------------------------------------------------- /lib/assets/javascripts/templates/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tent/tent-status/4b8e79350a99baf65ad34e116398a46f59f8acbf/lib/assets/javascripts/templates/.gitkeep -------------------------------------------------------------------------------- /lib/assets/javascripts/templates/_404.js.lodash_template: -------------------------------------------------------------------------------- 1 |
2 |
 
3 |
4 | <% if (text) { %> 5 |

<%- text %> 404

6 | <% } %> 7 | 8 | <% if (subtext) { %> 9 |

<%- subtext %>

10 | <% } %> 11 | 12 | <% if (text == null) { %> 13 |

The page you are looking for does not exist 404

14 | <% } %> 15 |
16 |
17 | -------------------------------------------------------------------------------- /lib/assets/javascripts/templates/_conversation_children.js.lodash_template: -------------------------------------------------------------------------------- 1 | <% if (posts) { %> 2 | <%= partials['_post'].render(context) %> 3 | <% } %> 4 | -------------------------------------------------------------------------------- /lib/assets/javascripts/templates/_conversation_parents.js.lodash_template: -------------------------------------------------------------------------------- 1 | <% if (posts) { %><%= partials['_post'].render(context) %> <% } %> 2 | -------------------------------------------------------------------------------- /lib/assets/javascripts/templates/_new_following_form.js.lodash_template: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 | 5 |
6 |
7 |
8 | -------------------------------------------------------------------------------- /lib/assets/javascripts/templates/_new_post_form.js.lodash_template: -------------------------------------------------------------------------------- 1 | <% if (TentStatus.config.authenticated) { %> 2 | 32 | <% } %> 33 | 34 | -------------------------------------------------------------------------------- /lib/assets/javascripts/templates/_post.js.lodash_template: -------------------------------------------------------------------------------- 1 |
  • 2 | <%= partials['_post_inner'].render(context) %> 3 |
  • 4 | -------------------------------------------------------------------------------- /lib/assets/javascripts/templates/_post_inner.js.lodash_template: -------------------------------------------------------------------------------- 1 | <% if (!post.get('is_repost')) { %> 2 |
    3 |
    4 |
    '>
    5 |
    6 | 7 |
    8 |
    9 | 10 | 11 | '> 12 | 13 | <% if (!post.get('permissions.public')) { %> <% if (only_me) { %>Private<% } else { %>Limited<% } %><% } %> 14 |
    15 | 16 |
    17 |

    <%= formatted.content %>

    18 | 19 | <% if (context.has_parent) { %> 20 |
    21 | <% } %> 22 |
    23 | 24 | <%= partials['_post_inner_actions'].render(context) %> 25 | 26 | <% if (TentStatus.config.authenticated) { %> 27 |
    28 | <% } %> 29 |
    30 |
    31 | <% } else { %> 32 |
    33 | <% } %> 34 | -------------------------------------------------------------------------------- /lib/assets/javascripts/templates/_post_inner_actions.js.lodash_template: -------------------------------------------------------------------------------- 1 |
    2 | <% if (TentStatus.config.authenticated) { %> 3 | Reply 4 | 5 | <% if (!current_user_owns_post) { %> 6 | Repost 7 | <% } %> 8 | 9 | <% if (!context.is_conversation_view) { %> 10 | 11 | 12 | <% if (!context.in_reply_to) { %> 13 | Conversation 14 | <% } else { %> 15 | 16 | in reply to <%- context.in_reply_to.name %> 17 | 18 | <% } %> 19 | 20 | <% } %> 21 | 22 | <% if (context.is_conversation_view_parent) { %> 23 | <% if (in_reply_to) { %> 24 | 25 | 26 | in reply to <%- context.in_reply_to.name %> 27 | 28 | 29 | <% } %> 30 | <% } %> 31 | 32 | <% if (current_user_owns_post) { %> 33 | Edit 34 | Delete 35 | <% } %> 36 | 37 | <% } else { %> 38 |   39 | <% } %> 40 |
    41 | -------------------------------------------------------------------------------- /lib/assets/javascripts/templates/_post_reply_form.js.lodash_template: -------------------------------------------------------------------------------- 1 | <% if (TentStatus.config.authenticated) { %> 2 |
    3 | 4 |
    5 |
     
    6 |
    7 | 8 | <%- max_chars %> 9 | 10 |
    11 | 12 |
    13 |
    14 |
    15 | <% } %> 16 | -------------------------------------------------------------------------------- /lib/assets/javascripts/templates/_profile_avatar.js.lodash_template: -------------------------------------------------------------------------------- 1 | title="<%- context.title %>"<% } %>> 2 | <% if (profile) { %> 3 | 4 | <% } else { %> 5 | 6 | <% } %> 7 | 8 | -------------------------------------------------------------------------------- /lib/assets/javascripts/templates/_profile_name.js.lodash_template: -------------------------------------------------------------------------------- 1 | <% if (has_name) { %> 2 | <% if (!no_link) { %> 3 | 4 | <% } %> 5 | 6 | <%- profile.get('name') %> 7 | 8 | <% if (!no_link) { %> 9 | 10 | <% } %> 11 | <% } %> 12 | 13 | <% if (!has_name) { %> 14 | <% if (!no_link) { %> 15 | 16 | <% } %> 17 | 18 | <%- formatted.entity %> 19 | 20 | <% if (!no_link) { %> 21 | 22 | <% } %> 23 | <% } %> 24 | -------------------------------------------------------------------------------- /lib/assets/javascripts/templates/_repost.js.lodash_template: -------------------------------------------------------------------------------- 1 | <%= partials['_post_inner'].render(context) %> 2 | -------------------------------------------------------------------------------- /lib/assets/javascripts/templates/conversation.js.lodash_template: -------------------------------------------------------------------------------- 1 |
  • 2 | 4 | 5 | 7 | 8 | 10 |
  • 11 | -------------------------------------------------------------------------------- /lib/assets/javascripts/templates/conversation_child_posts.js.lodash_template: -------------------------------------------------------------------------------- 1 |
    2 |
     
    3 |
    4 | 7 |
    8 |
    9 | -------------------------------------------------------------------------------- /lib/assets/javascripts/templates/conversation_parent_posts.js.lodash_template: -------------------------------------------------------------------------------- 1 |
     
    2 |
    3 |
     
    4 |
    5 | 6 |
    7 |
    8 | -------------------------------------------------------------------------------- /lib/assets/javascripts/templates/edit_post.js.lodash_template: -------------------------------------------------------------------------------- 1 |
  • 2 |
    3 |
    '> 4 | 5 |
    6 | 7 |
    8 |
    9 | 10 | 11 | ' class='post-profile-name'> 12 | <%- formatted.entity %> 13 | 14 |
    15 | 16 |
    17 |
     
    18 |
    19 | 20 | <%- max_chars %> 21 | Cancel 22 | 23 |
    24 | 25 |
    26 |
    27 |
    28 |
    29 |
     
    30 |
  • 31 | -------------------------------------------------------------------------------- /lib/assets/javascripts/templates/feed.js.lodash_template: -------------------------------------------------------------------------------- 1 | <% if (TentStatus.config.authenticated) { %> 2 | <% if (!TentStatus.config.guest_authenticated) { %> 3 |
    4 |
    5 |
    6 | <% } %> 7 | <% } %> 8 | 9 |
    10 |
    11 |
    12 |
    13 | 14 |
    15 | -------------------------------------------------------------------------------- /lib/assets/javascripts/templates/fetch_posts_pool.js.lodash_template: -------------------------------------------------------------------------------- 1 | <% if (posts_count) { %> 2 | 3 |
    4 | <%- posts_count %>  New Posts 5 |
    6 |
    7 | <% } %> 8 | -------------------------------------------------------------------------------- /lib/assets/javascripts/templates/followers.js.lodash_template: -------------------------------------------------------------------------------- 1 |
    2 |

    Subscribers

    3 | 4 | 5 | <% if (followers) { %><%= partials['_follower'].render(context) %><% } %> 6 | 7 |
    8 |
    9 | -------------------------------------------------------------------------------- /lib/assets/javascripts/templates/mentions.js.lodash_template: -------------------------------------------------------------------------------- 1 |
    2 |
    3 |
    4 | 5 |
    6 | 7 |
    8 | -------------------------------------------------------------------------------- /lib/assets/javascripts/templates/mentions_autocomplete_textarea_container.js.lodash_template: -------------------------------------------------------------------------------- 1 | 2 |
    3 |
    4 |
    Edit | Preview
    5 |
    6 |
     
    7 | -------------------------------------------------------------------------------- /lib/assets/javascripts/templates/mentions_unread_count.js.lodash_template: -------------------------------------------------------------------------------- 1 | <% if (unread_count) { %><%- unread_count %><% } %> 2 | -------------------------------------------------------------------------------- /lib/assets/javascripts/templates/mini_profile.js.lodash_template: -------------------------------------------------------------------------------- 1 | <% if (profile) { %> 2 | 3 |
    4 | <% entity = profile.get('entity') %> 5 | 6 | 12 |
    13 | 14 |

    <%- formatted.bio %>

    15 | 16 | '><%- formatted.website %> 17 | 18 |
    19 | <% } %> 20 | -------------------------------------------------------------------------------- /lib/assets/javascripts/templates/not_found.js.lodash_template: -------------------------------------------------------------------------------- 1 |
    2 |

    Not Found 404

    3 |

    You requested <%- window.location.href %>

    4 |
    5 | -------------------------------------------------------------------------------- /lib/assets/javascripts/templates/permissions_fields.js.lodash_template: -------------------------------------------------------------------------------- 1 |
    2 | 3 | 4 |
    5 | 6 |
    7 |
    8 | -------------------------------------------------------------------------------- /lib/assets/javascripts/templates/permissions_fields_options.js.lodash_template: -------------------------------------------------------------------------------- 1 | <% context.options.forEach(function(option) { %> 2 |
  • title='<%- option.value %>'<% } %>> 3 | <%- option.text %>× 4 |
  • 5 | <% }) %> 6 |
  • 7 | 8 |
     
    9 |
  • 10 | -------------------------------------------------------------------------------- /lib/assets/javascripts/templates/permissions_fields_picker.js.lodash_template: -------------------------------------------------------------------------------- 1 | <% if (context.options) { %> 2 | <% context.options.forEach(function(option) { %> 3 |
  • <% if (option.name) { %><%- option.name %> <%- option.entity %><% } else { %><%- option.entity %><% } %>
  • 4 | <% }) %> 5 | <% } %> 6 | -------------------------------------------------------------------------------- /lib/assets/javascripts/templates/permissions_fields_toggle.js.lodash_template: -------------------------------------------------------------------------------- 1 | <% if (permissions.public) { %> 2 | Everyone 3 | <% } %> 4 | 5 | <% if (!permissions.public) { %> 6 | Limited 7 | <% } %> 8 | -------------------------------------------------------------------------------- /lib/assets/javascripts/templates/posts_feed.js.lodash_template: -------------------------------------------------------------------------------- 1 | <% posts.forEach(function(post_context) { %> 2 | <%= partials['_post'].render(post_context, partials) %> 3 | <% }) %> 4 | -------------------------------------------------------------------------------- /lib/assets/javascripts/templates/profile.js.lodash_template: -------------------------------------------------------------------------------- 1 |
    2 | 43 |
    44 | 45 | <% if (TentStatus.config.authenticated === true) { %> 46 |
    47 |
    48 |
    49 | <% } %> 50 | 51 |
    52 |
    53 |
    54 | 55 |
    56 | 57 |
    58 | -------------------------------------------------------------------------------- /lib/assets/javascripts/templates/profile/resource_count.js.lodash_template: -------------------------------------------------------------------------------- 1 | <% if (url) { %><% } %> 2 | <% if (count != null) { %><%- count %> <% } else { %> <% } %><%- pluralized_resource_name %> 3 | <% if (url) { %><% } %> 4 | -------------------------------------------------------------------------------- /lib/assets/javascripts/templates/relationship.js.lodash_template: -------------------------------------------------------------------------------- 1 | <% entity = ((relationship.get('mentions') || [])[0] || {}).entity %> 2 | <% if (entity) { %> 3 |
  • 4 |
    5 |
    6 |
    7 |
    8 | 9 |
    10 |
    11 | 12 | ... 13 |
    14 |
    15 |
    16 |
  • 17 | <% } %> 18 | -------------------------------------------------------------------------------- /lib/assets/javascripts/templates/relationships_feed.js.lodash_template: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /lib/assets/javascripts/templates/repost_visibility.js.lodash_template: -------------------------------------------------------------------------------- 1 | <% if (entity) { %> 2 | 3 | Reposted by 4 | <% if (count) { %> 5 | and <%- count %> <%- pluralized_other %> 6 | <% } %> 7 | 8 | <% } %> 9 | 10 |
    11 | <% entities.forEach(function(entity) { %> 12 |
    13 | <% }) %> 14 |
    15 | -------------------------------------------------------------------------------- /lib/assets/javascripts/templates/reposts.js.lodash_template: -------------------------------------------------------------------------------- 1 |
    2 |
    3 |
    4 | 5 |
    6 | 7 |
    8 | -------------------------------------------------------------------------------- /lib/assets/javascripts/templates/search.js.lodash_template: -------------------------------------------------------------------------------- 1 |
    2 |
    3 |
    4 | 5 |
    6 |
    7 |
    8 | 9 |
    10 | -------------------------------------------------------------------------------- /lib/assets/javascripts/templates/search_form.js.lodash_template: -------------------------------------------------------------------------------- 1 |
    2 |
    3 | 4 |
    5 | 6 | 7 |
    8 | -------------------------------------------------------------------------------- /lib/assets/javascripts/templates/search_form_advanced_options.js.lodash_template: -------------------------------------------------------------------------------- 1 | <% if (visible) { %> 2 |
     
    3 |
    4 | 5 | 6 |
    7 | <% } %> 8 | -------------------------------------------------------------------------------- /lib/assets/javascripts/templates/search_form_advanced_options_toggle.js.lodash_template: -------------------------------------------------------------------------------- 1 | <% if (visible == null) { %>More Options<% } %><% if (visible) { %>Less Options<% } %> 2 | -------------------------------------------------------------------------------- /lib/assets/javascripts/templates/search_hits.js.lodash_template: -------------------------------------------------------------------------------- 1 | <% if (total_hits) { %> 2 | 3 | <% } %> 4 | 5 | <% if (no_results) { %> 6 |

    No Results

    7 | <% } %> 8 | -------------------------------------------------------------------------------- /lib/assets/javascripts/templates/signin.js.lodash_template: -------------------------------------------------------------------------------- 1 | 2 | 3 |
    4 | 5 | 6 |
    7 | 8 |
    9 | 10 | 11 |
    12 |
    13 | 14 |
    15 | 16 |
    17 | 18 | 19 |
    20 |
    21 | 22 | 23 |
    24 | -------------------------------------------------------------------------------- /lib/assets/javascripts/templates/single_post.js.lodash_template: -------------------------------------------------------------------------------- 1 |
    2 | 5 |
    6 | -------------------------------------------------------------------------------- /lib/assets/javascripts/templates/site_feed.js.lodash_template: -------------------------------------------------------------------------------- 1 | <% if (TentStatus.config.authenticated) { %> 2 |
    3 |
    4 |
    5 | <% } %> 6 | 7 |
    8 |
    9 |
    10 |
    11 | 12 |
    13 | -------------------------------------------------------------------------------- /lib/assets/javascripts/templates/subscribers.js.lodash_template: -------------------------------------------------------------------------------- 1 |
    2 | <% if (TentStatus.config.authenticated && TentStatus.config.meta.content.entity == entity) { %> 3 |

    Subscribers

    4 | <% } else { %> 5 |

    Subscribers <%- entity %>

    6 | <% } %> 7 | 8 |
     
    9 | 10 |
    11 |
    12 | -------------------------------------------------------------------------------- /lib/assets/javascripts/templates/subscription.js.lodash_template: -------------------------------------------------------------------------------- 1 |
  • 2 |
    3 |
    4 |
    5 |
    6 | 7 |
    8 |
    9 | 10 | ... 11 |
    12 |
    13 |
    14 |
  • 15 | -------------------------------------------------------------------------------- /lib/assets/javascripts/templates/subscription_toggle.js.lodash_template: -------------------------------------------------------------------------------- 1 | 2 | <% if (me) { %>You<% } else { %> 3 | <% if (subscribed) { %>Unsubscribe<% } else { %>Subscribe<% } %> 4 | <% } %> 5 | 6 | -------------------------------------------------------------------------------- /lib/assets/javascripts/templates/subscriptions.js.lodash_template: -------------------------------------------------------------------------------- 1 |
    2 | <% if (TentStatus.config.authenticated && TentStatus.config.meta.content.entity == entity) { %> 3 |

    Subscriptions

    4 |
    5 | <% } else { %> 6 |

    Subscriptions <%- entity %>

    7 | <% } %> 8 | 9 |
     
    10 | 11 |
    12 |
    13 | -------------------------------------------------------------------------------- /lib/assets/javascripts/templates/subscriptions_feed.js.lodash_template: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /lib/assets/javascripts/tent_status.js.coffee: -------------------------------------------------------------------------------- 1 | #= require_tree ./core_ext 2 | #= require moment 3 | #= require tent-client 4 | #= require ./config 5 | #= require sjcl 6 | #= require tent-markdown 7 | #= require textarea_cursor_position 8 | #= require_tree ./templates 9 | #= require_self 10 | #= require ./fetch_interval 11 | #= require_tree ./services 12 | #= require_tree ./models 13 | #= require ./collection 14 | #= require ./unified_collection 15 | #= require ./collection_pool 16 | #= require ./unified_collection_pool 17 | #= require_tree ./collections 18 | #= require_tree ./helpers 19 | #= require_tree ./views 20 | #= require ./router 21 | 22 | Marbles.View.templates = LoDashTemplates 23 | 24 | _.extend TentStatus, Marbles.Events, { 25 | Models: {} 26 | Collections: {} 27 | Routers: {} 28 | Helpers: {} 29 | 30 | setPageTitle: (options={}) -> 31 | @current_title ?= {} 32 | options.page += " -" if options.page 33 | [prefix, page] = [options.prefix, options.page || @current_title.page] 34 | 35 | if @current_title.page && !options.prefix 36 | prefix = null if page != @current_title.page 37 | 38 | @current_title.prefix = prefix 39 | @current_title.page = page 40 | 41 | title = [] 42 | for part in [prefix, page, @config.BASE_TITLE] 43 | continue unless part 44 | title.push part 45 | title = title.join(" ") 46 | document.title = title 47 | 48 | run: (options = {}) -> 49 | return if Marbles.history.started 50 | 51 | @showLoadingIndicator() 52 | @once 'ready', @hideLoadingIndicator 53 | 54 | @on 'loading:start', @showLoadingIndicator 55 | @on 'loading:stop', @hideLoadingIndicator 56 | 57 | Marbles.DOM.on window, 'scroll', (e) => @trigger 'window:scroll', e 58 | Marbles.DOM.on window, 'resize', (e) => @trigger 'window:resize', e 59 | 60 | # load top level data-view bindings 61 | _body_view = new Marbles.View el: document.body 62 | _body_view.trigger('ready') 63 | 64 | Marbles.history.start(_.extend({ root: (TentStatus.config.PATH_PREFIX || '') + '/' }, options.history || {})) 65 | 66 | if !TentStatus.config.authenticated && !options.history?.silent 67 | Marbles.Views.AppNavigationItem.disableAllExcept('profile') 68 | Marbles.history.navigate('profile', { trigger: true, replace: true }) 69 | 70 | @ready = true 71 | @trigger 'ready' 72 | 73 | showLoadingIndicator: -> 74 | @_num_running_requests ?= 0 75 | @_num_running_requests += 1 76 | Marbles.Views.loading_indicator.show() if @_num_running_requests == 1 77 | 78 | hideLoadingIndicator: -> 79 | @_num_running_requests ?= 1 80 | @_num_running_requests -= 1 81 | Marbles.Views.loading_indicator.hide() if @_num_running_requests == 0 82 | } 83 | 84 | TentStatus.trigger('config:ready') if TentStatus.config_ready 85 | 86 | -------------------------------------------------------------------------------- /lib/assets/javascripts/unified_collection.js.coffee: -------------------------------------------------------------------------------- 1 | TentStatus.UnifiedCollection = class UnifiedCollection extends Marbles.UnifiedCollection 2 | pagination: {} 3 | ignore_model_cids: {} 4 | 5 | sortModelsBy: (model) => 6 | (model.get('received_at') || model.get('published_at')) * -1 7 | 8 | fetchPrev: (options = {}) => 9 | prev_params = null 10 | for cid, _pagination of @pagination 11 | continue unless _pagination.prev 12 | prev_params ?= {} 13 | prev_params[cid] = Marbles.History::parseQueryParams(_pagination.prev) 14 | return false unless prev_params 15 | @fetch(prev_params, _.extend({ prepend: true }, options)) 16 | 17 | fetchNext: (options = {}) => 18 | next_params = null 19 | for cid, _pagination of @pagination 20 | continue unless _pagination.next 21 | next_params ?= {} 22 | next_params[cid] = Marbles.History::parseQueryParams(_pagination.next) 23 | return false unless next_params 24 | @fetch(next_params, _.extend({ append: true }, options)) 25 | 26 | ignoreModelId: (cid) => 27 | @ignore_model_cids[cid] = true 28 | 29 | postTypes: => 30 | types = [] 31 | for collection in @collections() 32 | types.push(collection.postTypes()...) 33 | _.uniq(types) 34 | 35 | fetch: (params = {}, options = {}) => 36 | for cid in @collection_ids 37 | do (cid) => 38 | _completeFn = options[cid]?.complete 39 | options[cid] ?= {} 40 | options[cid].complete = (models, res, xhr, _params) => 41 | _completeFn?.apply?(null, arguments) 42 | 43 | return unless xhr.status == 200 44 | 45 | _pagination = _.extend({ 46 | first: @pagination[cid]?.first 47 | last: @pagination[cid]?.last 48 | }, _.clone(res.pages)) 49 | 50 | if _pagination.next && models.length != res.posts.length 51 | if models.length 52 | # sometimes not all the results are used 53 | # in this case we need to create our own `next` query 54 | _last_model = _.last(models) 55 | _before = "before=#{_last_model.get('received_at') || _last_model.get('published_at')} #{_last_model.get('version.id')}" 56 | _pagination.next = _pagination.next.replace(/before=[^&]+/, _before) 57 | else 58 | # in the case that no models are returned, 59 | # just use the same query as last time 60 | _pagination.next = @pagination[cid]?.next || Marbles.history.serializeParams(_params) 61 | else if models.length != res.posts.length 62 | # in the case that no models are returned 63 | # and there is only a single, non-empty page 64 | _pagination.next = Marbles.history.serializeParams(_params) 65 | 66 | if options.prepend # fetchPrev 67 | _pagination.next = @pagination[cid]?.next 68 | 69 | @pagination[cid] = _pagination 70 | 71 | unless @pagination[cid].prev 72 | model = @constructor.collection.find(cid: cid)?.first() 73 | since = model?.get('received_at') || model?.get('published_at') || (new Date * 1) 74 | if version_id = model?.get('version.id') 75 | since = "#{since} #{version_id}" 76 | @pagination[cid].prev = "?since=#{since}" 77 | 78 | super(params, options) 79 | 80 | fetchCount: (params = {}, options = {}) => 81 | num_pending = @collection_ids.length 82 | count = 0 83 | is_success = false 84 | xhrs = [] 85 | completeFn = (_count, xhr) => 86 | num_pending -= 1 87 | xhrs.push(xhr) 88 | 89 | if xhr.status == 200 90 | is_success = true 91 | count += _count 92 | 93 | return unless num_pending <= 0 94 | 95 | if is_success 96 | options.success?(count, xhrs) 97 | options.complete?(count, xhrs) 98 | else 99 | options.failure?(count, xhrs) 100 | options.complete?(count, xhrs) 101 | 102 | for cid in @collection_ids 103 | collection = @constructor.collection.find(cid: cid) 104 | unless collection 105 | num_pending -= 1 106 | continue 107 | 108 | collection.fetchCount(params, complete: completeFn) 109 | 110 | fetchSuccess: (new_models, options) => 111 | new_models = _.uniq(new_models, is_sorted = true) 112 | _new_models = [] 113 | for model in new_models 114 | continue if @ignore_model_cids[model.cid] 115 | _new_models.push(model) 116 | super(_new_models, options) 117 | 118 | -------------------------------------------------------------------------------- /lib/assets/javascripts/unified_collection_pool.js.coffee: -------------------------------------------------------------------------------- 1 | TentStatus.UnifiedCollectionPool = class UnifiedCollectionPool extends TentStatus.CollectionPool 2 | 3 | constructor: (unified_collection) -> 4 | super 5 | 6 | TentStatus.Models.StatusPost.on 'create:success', (post, xhr) => 7 | @shadow_collection.ignoreModelId(post.cid) 8 | 9 | collection: => @unified_collection 10 | shadowCollection: => @shadow_collection 11 | 12 | initShadowCollection: (unified_collection) => 13 | @unified_collection = unified_collection 14 | @shadow_collection = new TentStatus.UnifiedCollection unified_collection.collections() 15 | 16 | reset: => 17 | @shadow_collection.empty() 18 | 19 | @interval.reset() 20 | 21 | updatePagination: => # ignore 22 | 23 | -------------------------------------------------------------------------------- /lib/assets/javascripts/views/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tent/tent-status/4b8e79350a99baf65ad34e116398a46f59f8acbf/lib/assets/javascripts/views/.gitkeep -------------------------------------------------------------------------------- /lib/assets/javascripts/views/001_permissions_fields.js.coffee: -------------------------------------------------------------------------------- 1 | Marbles.Views.PermissionsFields = class PermissionsFieldsView extends Marbles.View 2 | @template_name: 'permissions_fields' 3 | @view_name: 'permissions_fields' 4 | 5 | constructor: (options = {}) -> 6 | super 7 | 8 | @on 'init:PermissionsFieldsPicker', @initPicker 9 | @on 'init:PermissionsFieldsOptions', @initOptions 10 | @render() 11 | 12 | setImmediate @subscribeToMentions 13 | 14 | mentionsView: => 15 | unless @mentions_view_cid && mentions_view = Marbles.View.find(cid: @mentions_view_cid) 16 | return unless mentions_container_view = @findSiblingViews('MentionsAutoCompleteTextareaContainer')?[0] 17 | return unless mentions_view = mentions_container_view.childViews('MentionsAutoCompleteTextarea')?[0] 18 | @mentions_view_cid = mentions_view.cid 19 | mentions_view 20 | 21 | subscribeToMentions: => 22 | return if @_subscribed_to_mentions 23 | return unless mentions_view = @mentionsView() 24 | return unless mentions_manager = mentions_view.inline_mentions_manager 25 | 26 | mentions_manager.on 'change:inline_mentions', @inlineMentionsChanged 27 | @_subscribed_to_mentions = true 28 | 29 | inlineMentionsChanged: (inline_mentions) => 30 | mentions_view = @mentionsView() 31 | 32 | for entity, items of inline_mentions 33 | continue unless items.length 34 | @options_view.addOption( 35 | value: entity 36 | text: items[0].display_text 37 | group: false 38 | ) 39 | 40 | optionsInclude: (option) => 41 | @options_view.optionsInclude(option) 42 | 43 | initPicker: (@picker_view) => 44 | @initInput() 45 | 46 | initInput: => 47 | value = @picker_view.input?.getValue() || '' 48 | @picker_view.initInput Marbles.DOM.querySelector('.picker-input', @el) 49 | @picker_view.input.clear() 50 | @picker_view.input.focusAtEnd() unless @mentionsView().hasFocus() 51 | 52 | initOptions: (@options_view) => 53 | @options_view.on 'ready', (=> @initInput()), @ 54 | @options_view.on 'change:options', => @trigger('change:options', arguments...) 55 | 56 | @bindEvents() 57 | @hide() 58 | 59 | bindEvents: => 60 | @elements = { 61 | input_toggle: Marbles.DOM.querySelector('.permissions-options-container', @el) 62 | } 63 | 64 | Marbles.DOM.on(@elements.input_toggle, 'click', @focusInput) 65 | 66 | Marbles.DOM.on @el, 'click', (e) => 67 | return unless _.any(Marbles.DOM.parentNodes(e.target), (el) => el == @el) 68 | @focusInput() 69 | 70 | hide: => 71 | @visible = false 72 | Marbles.DOM.hide(@options_view.el) 73 | @picker_view?.hide() 74 | 75 | show: (should_focus = true) => 76 | @visible = true 77 | Marbles.DOM.show(@options_view.el) 78 | @focusInput() if should_focus 79 | 80 | addOption: (option) => 81 | @options_view.addOption(option) 82 | 83 | removeOption: (option) => 84 | @options_view.removeOption(option) 85 | 86 | focusInput: => 87 | @picker_view.input.focus() 88 | 89 | buildPermissions: => 90 | data = { 91 | public: false 92 | entities: [] 93 | } 94 | for option in @options_view.options 95 | return { public: true } if option.value == 'all' 96 | data.entities.push(option.value) 97 | data 98 | 99 | -------------------------------------------------------------------------------- /lib/assets/javascripts/views/003_mentions_auto_complete_textarea_container.js.coffee: -------------------------------------------------------------------------------- 1 | Marbles.Views.MentionsAutoCompleteTextareaContainer = class MentionsAutoCompleteTextareaContainerView extends Marbles.View 2 | @template_name: 'mentions_autocomplete_textarea_container' 3 | @view_name: 'mentions_autocomplete_textarea_container' 4 | 5 | constructor: (options = {}) -> 6 | super 7 | 8 | @on 'init:MentionsAutoCompleteTextarea', (view) => 9 | @mentions_autocomplete_textarea_view_cid = view.cid 10 | 11 | @on 'ready', @initMarkdownPreview 12 | 13 | @render() 14 | 15 | initMarkdownPreview: => 16 | @elements ?= {} 17 | 18 | @_mode = 'edit' 19 | 20 | @elements.toggles = { 21 | edit: Marbles.DOM.querySelector('[data-action=edit-markdown]', @el) 22 | preview: Marbles.DOM.querySelector('[data-action=preview-markdown]', @el) 23 | } 24 | 25 | @elements.preview = Marbles.DOM.querySelector('.markdown-preview', @el) 26 | @elements.textarea = Marbles.DOM.querySelector('textarea', @el) 27 | 28 | Marbles.DOM.on @elements.toggles.edit, 'click', (e) => 29 | return if @_mode is 'edit' 30 | @_mode = 'edit' 31 | Marbles.DOM.hide(@elements.preview) 32 | Marbles.DOM.show(@elements.textarea) 33 | @textareaView().focus() 34 | 35 | Marbles.DOM.on @elements.toggles.preview, 'click', (e) => 36 | return if @_mode is 'preview' 37 | @_mode = 'preview' 38 | Marbles.DOM.removeChildren(@elements.preview) 39 | mentions = _.map @textareaView().inline_mentions_manager.entities, (e) => { entity: e } 40 | html = TentStatus.Helpers.formatTentMarkdown(@textareaView().inline_mentions_manager.processedMarkdown(), mentions) 41 | Marbles.DOM.appendHTML(@elements.preview, html) 42 | Marbles.DOM.hide(@elements.textarea) 43 | Marbles.DOM.show(@elements.preview) 44 | 45 | textareaView: => 46 | Marbles.View.find(@mentions_autocomplete_textarea_view_cid) 47 | 48 | optionsInclude: (option) => 49 | @textareaView()?.optionsInclude(option) 50 | 51 | addOption: (option) => 52 | @textareaView()?.addOption(option) 53 | 54 | -------------------------------------------------------------------------------- /lib/assets/javascripts/views/003_permissions_fields_options.js.coffee: -------------------------------------------------------------------------------- 1 | Marbles.Views.PermissionsFieldsOptions = class PermissionsFieldsOptionsView extends Marbles.View 2 | @template_name: 'permissions_fields_options' 3 | @view_name: 'permissions_fields_options' 4 | 5 | constructor: (options = {}) -> 6 | super 7 | 8 | @on 'ready', @initOptions 9 | 10 | post = @parentView().parentView().parentView().post?() 11 | if !post || post.get('permissions.public') 12 | @set 'options', [ 13 | { 14 | text: 'Everyone' 15 | value: 'all' 16 | group: true 17 | } 18 | ] 19 | else 20 | options = [] 21 | if post 22 | for m in post.get('mentioned_posts') 23 | continue unless m.entity 24 | options.push { 25 | text: TentStatus.Helpers.minimalEntity(m.entity) 26 | value: m.entity 27 | group: false 28 | } 29 | @set 'options', options 30 | 31 | @on 'change:options', @render 32 | @render() 33 | 34 | initOptions: => 35 | return unless @options 36 | option_els = Marbles.DOM.querySelectorAll('.option', @el) 37 | @option_views = for option, index in @options 38 | new OptionView parent_view: @, option: option, el: option_els[index] 39 | 40 | optionsInclude: (option) => 41 | for item in @options 42 | return true if item.value == option.value 43 | false 44 | 45 | addOption: (option) => 46 | should_push = true 47 | for item in @options 48 | if item.value == option.value 49 | item.text = option.text 50 | should_push = false 51 | break 52 | 53 | if item.text == option.text 54 | item.value = option.value 55 | should_push = false 56 | break 57 | 58 | @options.push(option) if should_push 59 | @trigger 'change:options' 60 | 61 | removeOption: (option) => 62 | options = [] 63 | for item in @options 64 | continue if item.value == option.value 65 | options.push item 66 | @options = options 67 | @trigger 'change:options' 68 | 69 | context: => 70 | options: @options 71 | 72 | class OptionView 73 | constructor: (params = {}) -> 74 | for k,v of params 75 | @[k] = v 76 | @_parent_view_cid = params.parent_view.cid 77 | 78 | @elements = { 79 | remove: Marbles.DOM.querySelector('.remove', @el) 80 | } 81 | 82 | Marbles.DOM.on @elements.remove, 'click', @remove 83 | 84 | parentView: => 85 | Marbles.View.find(@_parent_view_cid) 86 | 87 | unmarkDelete: => 88 | @marked_delete = false 89 | Marbles.DOM.removeClass(@elements.remove, 'active') 90 | 91 | markDelete: => 92 | @marked_delete = true 93 | Marbles.DOM.removeClass(@elements.remove, 'active') 94 | 95 | remove: (e) => 96 | e?.stopPropagation() 97 | @parentView().removeOption(@option) 98 | 99 | -------------------------------------------------------------------------------- /lib/assets/javascripts/views/004_mentions_auto_complete_textarea.js.coffee: -------------------------------------------------------------------------------- 1 | Marbles.Views.MentionsAutoCompleteTextarea = class MentionsAutoCompleteTextareaView extends Marbles.View 2 | @view_name: 'mentions_autocomplete_textarea' 3 | 4 | constructor: (options = {}) -> 5 | super 6 | 7 | @initInlineMentionsManager() 8 | 9 | initInlineMentionsManager: => 10 | @inline_mentions_manager = new TentStatus.InlineMentionsManager(el: @el) 11 | 12 | hasFocus: => 13 | Marbles.DOM.match(@el, ":focus") 14 | 15 | focus: => 16 | return if @hasFocus() 17 | 18 | selection = new Marbles.DOM.InputSelection(@el) 19 | end = @el.value.length 20 | selection.setSelectionRange(end, end) 21 | 22 | -------------------------------------------------------------------------------- /lib/assets/javascripts/views/app_navigation.js.coffee: -------------------------------------------------------------------------------- 1 | Marbles.Views.AppNavigation = class AppNavigationView extends Marbles.View 2 | @view_name: 'app_navigation' 3 | 4 | elements: {} 5 | 6 | constructor: (options = {}) -> 7 | @setupMenuToggle() 8 | 9 | setupMenuToggle: => 10 | @elements.menu_toggle = Marbles.DOM.querySelector('.js-menu-switch') 11 | @elements.app_nav_list = Marbles.DOM.querySelector('.app-nav-list') 12 | 13 | @menu_visible = Marbles.DOM.match(@elements.app_nav_list, '.show') 14 | 15 | Marbles.DOM.on @elements.menu_toggle, 'click', @toggleMenu 16 | 17 | toggleMenu: (e) => 18 | e?.preventDefault() 19 | 20 | if @menu_visible 21 | @hideMenu() 22 | else 23 | @showMenu() 24 | 25 | showMenu: => 26 | Marbles.DOM.addClass @elements.app_nav_list, 'show' 27 | @menu_visible = true 28 | 29 | hideMenu: => 30 | Marbles.DOM.removeClass @elements.app_nav_list, 'show' 31 | @menu_visible = false 32 | 33 | -------------------------------------------------------------------------------- /lib/assets/javascripts/views/app_navigation_item.js.coffee: -------------------------------------------------------------------------------- 1 | Marbles.Views.AppNavigationItem = class AppNavigationItemView extends Marbles.View 2 | @view_name: 'app_navigation_item' 3 | 4 | @find: (fragment) -> 5 | for item in @allItems() 6 | return item if item.fragment == fragment 7 | null 8 | 9 | @allItems: -> 10 | for cid in Marbles.View.instances.app_navigation_item || [] 11 | Marbles.View.instances.all[cid] 12 | 13 | @disableAllExcept: (whitelist...) -> 14 | for item in @allItems() 15 | continue if whitelist.indexOf(item.fragment) != -1 16 | item.disable() 17 | 18 | @disableAll: @disableAllExcept 19 | 20 | initialize: => 21 | @fragment = Marbles.DOM.attr(@el, 'data-fragment') 22 | Marbles.DOM.on(@el, 'click', @navigate) 23 | 24 | navigate: (e) => 25 | return unless @disabled 26 | e?.preventDefault() 27 | 28 | disable: => 29 | @disabled = true 30 | Marbles.DOM.addClass(@el, 'disabled') 31 | 32 | enable: => 33 | @disabled = false 34 | Marbles.DOM.removeClass(@el, 'disabled') 35 | 36 | -------------------------------------------------------------------------------- /lib/assets/javascripts/views/auth_button.js.coffee: -------------------------------------------------------------------------------- 1 | Marbles.Views.AuthButton = class AuthButtonView extends Marbles.View 2 | @view_name: 'auth_button' 3 | 4 | initialize: => 5 | Marbles.DOM.on @el, 'click', @performAction 6 | 7 | if TentStatus.config.authenticated 8 | @actionFn = @performSignout 9 | Marbles.DOM.setAttr(@el, 'title', Marbles.DOM.attr(@el, 'data-signout-title')) 10 | else 11 | @actionFn = @redirectToSignin 12 | Marbles.DOM.setAttr(@el, 'title', Marbles.DOM.attr(@el, 'data-signin-title')) 13 | 14 | performAction: => @actionFn() 15 | 16 | performSignout: (e) => 17 | e?.preventDefault() 18 | 19 | new Marbles.HTTP { 20 | method: 'POST' 21 | url: TentStatus.config.SIGNOUT_URL 22 | middleware: [Marbles.HTTP.Middleware.WithCredentials] 23 | callback: (res, xhr) => 24 | @signoutRedirect() 25 | } 26 | 27 | redirectToSignin: => 28 | Marbles.history.navigate('/signin', trigger: true) 29 | 30 | signoutRedirect: => 31 | window.location.href = TentStatus.config.SIGNOUT_REDIRECT_URL 32 | 33 | -------------------------------------------------------------------------------- /lib/assets/javascripts/views/container.js.coffee: -------------------------------------------------------------------------------- 1 | class ContainerView extends Marbles.View 2 | @view_name: 'container' 3 | 4 | Marbles.Views.container = new ContainerView el: document.getElementById('main') 5 | -------------------------------------------------------------------------------- /lib/assets/javascripts/views/external_link.js.coffee: -------------------------------------------------------------------------------- 1 | Marbles.Views.ExternalLink = class ExternalLinkView extends Marbles.View 2 | @view_name: 'external_link' 3 | 4 | constructor: (options = {}) -> 5 | super 6 | 7 | Marbles.DOM.on @el, 'click', (e) => 8 | middle_click = event.which == 2 9 | return true if middle_click || e.ctrlKey || e.metaKey || e.shiftKey 10 | 11 | e.preventDefault() 12 | url = Marbles.DOM.attr(@el, 'href') 13 | window.open(url) if url 14 | 15 | -------------------------------------------------------------------------------- /lib/assets/javascripts/views/feed.js.coffee: -------------------------------------------------------------------------------- 1 | Marbles.Views.Feed = class FeedView extends Marbles.View 2 | @template_name: 'feed' 3 | @view_name: 'feed' 4 | 5 | constructor: (options = {}) -> 6 | @container = Marbles.Views.container 7 | super 8 | 9 | @render() 10 | -------------------------------------------------------------------------------- /lib/assets/javascripts/views/fetch_posts_pool.js.coffee: -------------------------------------------------------------------------------- 1 | Marbles.Views.FetchPostsPool = class FetchPostsPoolView extends Marbles.View 2 | @template_name: 'fetch_posts_pool' 3 | @view_name: 'fetch_posts_pool' 4 | 5 | constructor: (options = {}) -> 6 | super 7 | 8 | @on 'ready', @bindLink 9 | 10 | @parentView().on('init-view', @parentViewInit) 11 | 12 | parentViewInit: (view_class_name, view) => 13 | return unless view_class_name.match /PostsFeed$/ 14 | 15 | @parentView().off('init-view', @parentViewInit) 16 | 17 | @posts_feed_view_cid = view.cid 18 | 19 | posts_feed_collection = view.postsCollection() # UnifiedCollection 20 | @pool = new TentStatus.UnifiedCollectionPool posts_feed_collection 21 | 22 | @pool.on 'pool:expand', @poolExpanded 23 | @pool.on 'pool:overflow', @poolExpanded 24 | 25 | poolExpanded: (size) => 26 | @size = size 27 | @render() 28 | 29 | emptyPool: => 30 | posts_feed_view = Marbles.View.instances.all[@posts_feed_view_cid] 31 | return unless posts_feed_view 32 | 33 | collection = @pool.shadowCollection() 34 | 35 | posts_feed_view.prependRender(collection.models()) 36 | posts_feed_view.postsCollection().prependIds?(collection.model_ids...) 37 | 38 | @pool.reset() 39 | @size = 0 40 | 41 | @render() 42 | 43 | context: => 44 | if !@size 45 | posts_count: null 46 | else if @size <= @pool.MAX_OVERFLOW_SIZE 47 | posts_count: @size 48 | else 49 | posts_count: "#{@pool.MAX_OVERFLOW_SIZE}+" 50 | 51 | bindLink: => 52 | link_element = Marbles.DOM.querySelector('.fetch-posts-pool', @el) 53 | Marbles.DOM.on link_element, 'click', (e) => 54 | e.preventDefault() 55 | @emptyPool() 56 | 57 | render: (context = @context()) => 58 | super(context) 59 | 60 | Marbles.Views.FetchPostsPool.trigger('render') 61 | 62 | if context.posts_count 63 | TentStatus.setPageTitle prefix: "(#{context.posts_count})" 64 | else 65 | TentStatus.setPageTitle prefix: null 66 | 67 | -------------------------------------------------------------------------------- /lib/assets/javascripts/views/follower.js.coffee: -------------------------------------------------------------------------------- 1 | Marbles.Views.Follower = class FollowerView extends Marbles.View 2 | @template_name: '_follower' 3 | @view_name: 'follower' 4 | 5 | constructor: (options = {}) -> 6 | super 7 | 8 | @follower_cid = Marbles.DOM.attr(@el, 'data-cid') 9 | @entity = @follower().get('entity') 10 | 11 | context: (follower) => 12 | _.extend super, 13 | cid: follower.cid 14 | 15 | follower: => 16 | TentStatus.Models.Follower.find(cid: @follower_cid) 17 | 18 | profile: => 19 | new Marbles.Object entity: @entity 20 | -------------------------------------------------------------------------------- /lib/assets/javascripts/views/full_width.js.coffee: -------------------------------------------------------------------------------- 1 | Marbles.Views.FullWidth = class FullWidthView extends Marbles.View 2 | @view_name: 'full_width' 3 | 4 | constructor: -> 5 | super 6 | 7 | @calibrate() 8 | TentStatus.on 'window:resize', @calibrate 9 | 10 | calibrate: => 11 | width = parseInt(Marbles.DOM.getStyle(@el.parentNode, 'width')) 12 | padding = parseInt(Marbles.DOM.getStyle(@el, 'padding-left')) + parseInt(Marbles.DOM.getStyle(@el, 'padding-right')) 13 | border = parseInt(Marbles.DOM.getStyle(@el, 'border-left-width')) + parseInt(Marbles.DOM.getStyle(@el, 'border-right-width')) 14 | Marbles.DOM.setStyle(@el, 'width', "#{width - padding - border}px") 15 | 16 | -------------------------------------------------------------------------------- /lib/assets/javascripts/views/global_navigation.js.coffee: -------------------------------------------------------------------------------- 1 | Marbles.Views.GlobalNavigation = class GlobalNavigationView extends Marbles.View 2 | @view_name: 'global_navigation' 3 | 4 | -------------------------------------------------------------------------------- /lib/assets/javascripts/views/loading_indicator.js.coffee: -------------------------------------------------------------------------------- 1 | Marbles.Views.LoadingIndicator = class LoadingIndicatorView extends Marbles.View 2 | @view_name: 'loading_indicator' 3 | 4 | show: => 5 | clearTimeout @_showTimeout 6 | @_showTimeout = setTimeout (=> 7 | Marbles.DOM.addClass(@el, 'pulse') 8 | 9 | clearTimeout @_pulseTimeout 10 | @_pulseTimeout = setTimeout @pulse, 1400 11 | ), 0 12 | 13 | pulse: => 14 | @hide() 15 | @_pulseTimeout = setTimeout @show, 600 16 | 17 | hide: => 18 | clearTimeout @_showTimeout 19 | clearTimeout @_pulseTimeout 20 | Marbles.DOM.removeClass(@el, 'pulse') 21 | 22 | Marbles.Views.loading_indicator = new LoadingIndicatorView el: document.getElementById('loading-indicator') 23 | -------------------------------------------------------------------------------- /lib/assets/javascripts/views/mentions.js.coffee: -------------------------------------------------------------------------------- 1 | Marbles.Views.Mentions = class MentionsView extends Marbles.View 2 | @template_name: 'mentions' 3 | @view_name: 'mentions' 4 | 5 | constructor: (options = {}) -> 6 | @container = Marbles.Views.container 7 | @entity = options.entity 8 | super 9 | 10 | @render() 11 | -------------------------------------------------------------------------------- /lib/assets/javascripts/views/mentions_auto_complete_textarea/inline_mentions_manager.js.coffee: -------------------------------------------------------------------------------- 1 | class TentStatus.InlineMentionsManager extends Marbles.Object 2 | constructor: (options = {}) -> 3 | @elements = { 4 | textarea: options.el 5 | } 6 | 7 | @entities = [] 8 | @inline_mentions = {} 9 | @excess_char_count = 0 10 | 11 | @bindInputEvents() 12 | 13 | bindInputEvents: => 14 | Marbles.DOM.on @elements.textarea, 'keydown', @processKeyDown 15 | 16 | processedMarkdown: => 17 | @updateMentions() 18 | 19 | text = @elements.textarea.value 20 | 21 | offset = 0 22 | 23 | for entity, inline_mentions of @inline_mentions 24 | for inline_mention in inline_mentions 25 | mention_markdown = inline_mention.toMarkdownString() 26 | text = text.slice(0, inline_mention.start_index + offset) + mention_markdown + text.slice(inline_mention.end_index + offset, text.length) 27 | 28 | offset -= inline_mention.input_text.length - mention_markdown.length 29 | 30 | text 31 | 32 | processKeyDown: (e) => 33 | clearTimeout @_update_mentions_timeout 34 | @_update_mentions_timeout = setTimeout @updateMentions, 10 35 | 36 | updateMentions: => 37 | value = @elements.textarea.value 38 | length = value.length 39 | offset = 0 40 | 41 | regex = /(\^\[([^\]]*)\]\(([^\)]*)\))/ 42 | 43 | entities = [] 44 | inline_mentions = {} 45 | excess_char_count = 0 46 | 47 | while (_val = value.slice(offset, length)) && (index = _val.search(regex)) != -1 48 | m = _val.match(regex) 49 | input_text = m[1] 50 | display_text = m[2] 51 | entity = m[3] 52 | 53 | start_index = offset + index 54 | end_index = start_index + input_text.length 55 | 56 | offset += index + input_text.length 57 | 58 | entities.push(entity) if entities.indexOf(entity) == -1 59 | 60 | inline_mention = new @constructor.InlineMention( 61 | start_index: start_index 62 | end_index: end_index 63 | input_text: input_text 64 | display_text: display_text 65 | entity: entity 66 | entity_index: entities.indexOf(entity) 67 | ) 68 | 69 | # the entity URI will be replaces with an index, 70 | # keep track of the number of chars exceeding the length of all the indices 71 | excess_char_count += TentStatus.Helpers.numChars(entity) - inline_mention.entity_index.toString().length 72 | 73 | inline_mentions[entity] ?= [] 74 | inline_mentions[entity].push(inline_mention) 75 | 76 | @set 'entities', entities 77 | @set 'inline_mentions', inline_mentions 78 | @set 'excess_char_count', excess_char_count 79 | 80 | @InlineMention = class InlineMention extends Marbles.Object 81 | constructor: (properties) -> 82 | (@[k] = v) for k,v of properties 83 | 84 | toMarkdownString: => 85 | "^[#{@display_text}](#{@entity_index})" 86 | 87 | toExpandedMarkdownString: => 88 | "^[#{@display_text}](#{@entity})" 89 | 90 | -------------------------------------------------------------------------------- /lib/assets/javascripts/views/mentions_unread_count.js.coffee: -------------------------------------------------------------------------------- 1 | Marbles.Views.MentionsUnreadCount = class MentionsUnreadCountView extends Marbles.View 2 | @view_name: 'mentions_unread_count' 3 | @template_name: 'mentions_unread_count' 4 | 5 | constructor: (options = {}) -> 6 | super 7 | 8 | @init() 9 | 10 | init: => 11 | unless TentStatus.background_mentions_unread_count 12 | return TentStatus.on 'init:background_mentions_unread_count', @init 13 | 14 | @render() 15 | TentStatus.background_mentions_unread_count.on 'change:unread_count', => @render() 16 | 17 | context: => 18 | unread_count: TentStatus.Helpers.formatCount(TentStatus.background_mentions_unread_count.get('unread_count'), max: 99) 19 | 20 | -------------------------------------------------------------------------------- /lib/assets/javascripts/views/mini_profile.js.coffee: -------------------------------------------------------------------------------- 1 | Marbles.Views.MiniProfile = class MiniProfileView extends Marbles.View 2 | @template_name: 'mini_profile' 3 | @partial_names: [] 4 | @view_name: 'mini_profile' 5 | 6 | constructor: (options = {}) -> 7 | super 8 | 9 | # Don't show the mini profile when not authenticated 10 | return unless TentStatus.config.authenticated 11 | 12 | @fetchProfile(options.entity) if options.entity 13 | 14 | @current_post_view = null 15 | 16 | Marbles.Views.Post.on 'focus', (view, e) => 17 | return @render() unless view 18 | 19 | @setCurrentPostView(view) 20 | 21 | Marbles.Views.PostsFeed.on 'prepend', => setImmediate(@adjustPosition) 22 | 23 | Marbles.Views.FetchPostsPool.on 'render', => setImmediate(@adjustPosition) 24 | 25 | setCurrentPostView: (view) => 26 | @current_post_view = view 27 | 28 | @adjustPosition() 29 | 30 | post = view.post() 31 | 32 | if post.get('is_repost') 33 | @fetchProfile(post.get('content.entity')) 34 | else 35 | @fetchProfile(post.get('entity')) 36 | 37 | adjustPosition: => 38 | return unless @current_post_view 39 | 40 | Marbles.DOM.setStyle(@el, 'top', "#{Marbles.DOM.offsetTop(@current_post_view.el)}px") 41 | 42 | fetchProfile: (entity) => 43 | return unless entity 44 | return if entity == @profile()?.get('entity') 45 | TentStatus.Models.MetaProfile.find({entity: entity}, 46 | success: (profile) => 47 | @current_profile_cid = profile.cid 48 | @render(@context(profile)) 49 | 50 | failure: => 51 | @render() 52 | ) 53 | 54 | profile: => 55 | TentStatus.Models.MetaProfile.find(cid: @current_profile_cid) 56 | 57 | context: (profile = @profile()) => 58 | return { profile: null } unless profile 59 | 60 | profile: profile 61 | profile_url: TentStatus.Helpers.entityProfileUrl(profile.get('entity')) 62 | formatted: 63 | name: TentStatus.Helpers.truncate(profile.get('name') || TentStatus.Helpers.formatUrlWithPath(profile.get('entity')), 15) 64 | bio: TentStatus.Helpers.truncate(profile.get('bio'), 256) 65 | website: TentStatus.Helpers.formatUrlWithPath(profile.get('website')) 66 | 67 | -------------------------------------------------------------------------------- /lib/assets/javascripts/views/navigation_active.js.coffee: -------------------------------------------------------------------------------- 1 | Marbles.Views.NavigationActive = class NavigationActiveView extends Marbles.View 2 | @view_name: 'navigation_active' 3 | 4 | @buildMappingRegexp: (mapping) -> 5 | new RegExp("^#{mapping.replace("*", ".*?")}$") 6 | 7 | initialize: -> 8 | @active_class = Marbles.DOM.attr(@el, 'data-active-class') 9 | @active_selector = Marbles.DOM.attr(@el, 'data-active-selector') 10 | 11 | @buildActiveMapping() 12 | @markActiveItem() 13 | 14 | Marbles.history.on 'route', (router, name, args) => 15 | @markActiveItem() 16 | 17 | buildActiveMapping: => 18 | @active_mapping = [] 19 | for el in Marbles.DOM.querySelectorAll(@active_selector, @el) 20 | continue unless mapping = Marbles.DOM.attr(el, 'data-match-url') 21 | reg = @constructor.buildMappingRegexp(mapping) 22 | @active_mapping.push([reg, el]) 23 | @active_mapping = _.sortBy(@active_mapping, ( (item) => item[0].source.length * -1 )) 24 | 25 | markActiveItem: => 26 | path = window.location.pathname.replace(new RegExp("^#{TentStatus.config.PATH_PREFIX || ''}"), '') 27 | path = path.replace(/^\/?/, '/') # ensure path begins with a / 28 | matched = false 29 | for item in @active_mapping 30 | [reg, el] = item 31 | if !matched && reg.test(path) 32 | matched = true 33 | Marbles.DOM.addClass(el, @active_class) 34 | else 35 | Marbles.DOM.removeClass(el, @active_class) 36 | 37 | 38 | -------------------------------------------------------------------------------- /lib/assets/javascripts/views/new_following_form.js.coffee: -------------------------------------------------------------------------------- 1 | Marbles.Views.NewFollowingForm = class NewFollowingFormView extends Marbles.View 2 | @template_name: '_new_following_form' 3 | @view_name: 'new_following_form' 4 | 5 | constructor: (options = {}) -> 6 | super 7 | 8 | @elements = {} 9 | 10 | @on 'ready', @init 11 | 12 | @render() 13 | 14 | init: => 15 | @elements.form = Marbles.DOM.querySelector('form', @el) 16 | @elements.input = Marbles.DOM.querySelector('input[name=entity]', @el) 17 | @elements.submit = Marbles.DOM.querySelector('input[type=submit]', @el) 18 | @elements.errors = Marbles.DOM.querySelector('.alert-error', @el) 19 | 20 | Marbles.DOM.on(@elements.form, 'submit', @submit) 21 | Marbles.DOM.on(@elements.submit, 'click', @submit) 22 | 23 | submit: (e) => 24 | e?.preventDefault() 25 | return if @frozen 26 | 27 | entity = @buildEntity(@elements.input.value) 28 | 29 | @clearErrors() 30 | return unless @validate(entity) 31 | @disable() 32 | 33 | TentStatus.Models.Following.create entity, 34 | failure: (res, xhr) => 35 | @enable() 36 | @showErrors([{ entity: "Error: #{res?.error}" }]) 37 | success: (following) => 38 | @reset() 39 | 40 | reset: => 41 | @clearErrors() 42 | @enable() 43 | @elements.input.value = "" 44 | 45 | disable: => 46 | @frozen = true 47 | @elements.submit.disabled = true 48 | @elements.form.disabled = true 49 | 50 | enable: => 51 | @frozen = false 52 | @elements.submit.disabled = false 53 | @elements.form.disabled = false 54 | 55 | validate: (data, options = {}) => 56 | return if @frozen 57 | errors = TentStatus.Models.Following.validate(data, options) 58 | @clearErrors() 59 | @showErrors(errors) if errors 60 | 61 | !errors 62 | 63 | clearErrors: => 64 | for el in Marbles.DOM.querySelectorAll('.error', @el) 65 | Marbles.DOM.removeClass(el, 'error') 66 | Marbles.DOM.hide(@elements.errors) 67 | 68 | showErrors: (errors) => 69 | error_messages = [] 70 | for error in errors 71 | for name, msg of error 72 | input = Marbles.DOM.querySelector("[name=#{name}]", @el) 73 | Marbles.DOM.addClass(input, 'error') 74 | error_messages.push(msg) 75 | console.log(error_messages.join("\n")) 76 | @elements.errors.innerHTML = error_messages.join("
    ") 77 | Marbles.DOM.show(@elements.errors) 78 | 79 | buildEntity: (entity) => 80 | return unless (m = entity.match(/^(https?:\/\/)?([^\/]+)(.*?)$/)) 81 | parts = { 82 | scheme: m[1] 83 | domain: m[2] 84 | rest: m[3] || "" 85 | } 86 | 87 | parts.scheme ?= 'http://' 88 | entity = parts.scheme + parts.domain + parts.rest 89 | 90 | entity 91 | 92 | -------------------------------------------------------------------------------- /lib/assets/javascripts/views/not_found.js.coffee: -------------------------------------------------------------------------------- 1 | Marbles.Views.NotFound = class NotFoundView extends Marbles.View 2 | @template_name: 'not_found' 3 | @view_name: 'not_found' 4 | 5 | initialize: => 6 | @render() 7 | 8 | -------------------------------------------------------------------------------- /lib/assets/javascripts/views/permissions_fields_toggle.js.coffee: -------------------------------------------------------------------------------- 1 | Marbles.Views.PermissionsFieldsToggle = class PermissionsFieldsToggleView extends Marbles.View 2 | @template_name: 'permissions_fields_toggle' 3 | @view_name: 'permissions_fields_toggle' 4 | 5 | constructor: -> 6 | super 7 | 8 | @once 'ready', => 9 | setImmediate @bindEvents 10 | 11 | @render() 12 | 13 | context: (permissions) => 14 | permissions ?= @parentView()?.post()?.get('permissions') 15 | permissions ?= { public: true } 16 | _.extend super, 17 | permissions: permissions 18 | 19 | permissionsFieldsView: => 20 | _.last(@parentView()?.childViews('PermissionsFields') || []) 21 | 22 | bindEvents: => 23 | permissions_fields_view = @permissionsFieldsView() 24 | return unless permissions_fields_view 25 | 26 | permissions_fields_view.on 'change:options', => 27 | permissions = permissions_fields_view.buildPermissions() 28 | @render(@context(permissions)) 29 | 30 | @text ?= {} 31 | @text.visibility_toggle = { 32 | show: Marbles.DOM.attr(@el, 'data-show-text') 33 | hide: Marbles.DOM.attr(@el, 'data-hide-text') 34 | } 35 | 36 | Marbles.DOM.on @el, 'click', (e) => 37 | e.stopPropagation() 38 | @toggleVisibility() 39 | 40 | toggleVisibility: => 41 | if @visible 42 | @hide() 43 | else 44 | @show() 45 | 46 | hide: => 47 | @visible = false 48 | Marbles.DOM.removeClass(@el, 'visible') 49 | @permissionsFieldsView()?.hide() 50 | 51 | show: (should_focus = true) => 52 | @visible = true 53 | Marbles.DOM.addClass(@el, 'visible') 54 | @permissionsFieldsView()?.show() 55 | 56 | -------------------------------------------------------------------------------- /lib/assets/javascripts/views/post.js.coffee: -------------------------------------------------------------------------------- 1 | Marbles.Views.Post = class PostView extends Marbles.View 2 | @template_name: '_post' 3 | @partial_names: ['_post_inner', '_post_inner_actions'] 4 | @view_name: 'post' 5 | 6 | constructor: (options = {}) -> 7 | super(_.extend(options, {render_method: 'replace'})) 8 | 9 | @post_cid = Marbles.DOM.attr(@el, 'data-post_cid') 10 | 11 | @bindEl() 12 | @on 'ready', @bindEl 13 | 14 | bindEl: => 15 | Marbles.DOM.on @el, 'click', @focus 16 | 17 | focus: (e) => 18 | @constructor.trigger('focus', @, e) 19 | 20 | post: => 21 | Marbles.Model.instances.all[@post_cid] 22 | 23 | hide: => 24 | Marbles.DOM.hide(@el) 25 | 26 | detach: => 27 | Marbles.DOM.removeNode(@el) 28 | super 29 | 30 | inReplyToJSON: (mention) => 31 | return unless mention && mention.entity && mention.post 32 | { 33 | entity: mention.entity 34 | name: TentStatus.Helpers.formatUrlWithPath(mention.entity) 35 | url: TentStatus.Helpers.route('post', entity: mention.entity, post_id: mention.post) 36 | } 37 | 38 | getPermissibleEntities: (post, should_trim=true) => 39 | if should_trim 40 | _.map post.get('permissions.entities') || [], (entity) => 41 | TentStatus.Helpers.minimalEntity(entity) 42 | else 43 | post.get('permissions.entities') || [] 44 | 45 | context: (post = @post()) => 46 | permissible_entities = @getPermissibleEntities(post) 47 | 48 | post: post 49 | in_reply_to: @inReplyToJSON(post.get('mentioned_posts')[0]) 50 | url: TentStatus.Helpers.route('post', entity: post.get('entity'), post_id: post.get('id')) 51 | only_me: !post.get('permissions.public') && !permissible_entities.length && TentStatus.Helpers.isCurrentUserEntity(post.get('entity')) 52 | current_user_owns_post: TentStatus.Helpers.isCurrentUserEntity(post.get('entity')) 53 | formatted: 54 | permissible_entities: permissible_entities.join(', ') 55 | content: TentStatus.Helpers.formatTentMarkdown( 56 | TentStatus.Helpers.truncate(post.get('content.text'), TentStatus.config.MAX_STATUS_LENGTH, ''), 57 | post.get('mentions') 58 | ) 59 | 60 | -------------------------------------------------------------------------------- /lib/assets/javascripts/views/post/001_post_action.js.coffee: -------------------------------------------------------------------------------- 1 | Marbles.Views.PostAction = class PostActionView extends Marbles.View 2 | @view_name: 'post_action' 3 | 4 | constructor: -> 5 | super 6 | 7 | @text = { 8 | confirm: Marbles.DOM.attr(@el, 'data-confirm') 9 | } 10 | 11 | Marbles.DOM.on(@el, 'click', @confirmAction) 12 | 13 | confirmAction: => 14 | return if @disabled 15 | return @performAction() unless @text.confirm 16 | @performAction() if confirm(@text.confirm) 17 | 18 | performAction: => 19 | console.warn "#{@constructor.name}::performAction needs to be defined" 20 | console.log @el 21 | 22 | postView: => @findParentView('post') 23 | 24 | -------------------------------------------------------------------------------- /lib/assets/javascripts/views/post/conversation.js.coffee: -------------------------------------------------------------------------------- 1 | Marbles.Views.Conversation = class ConversationView extends Marbles.View 2 | @template_name: 'conversation' 3 | @view_name: 'conversation' 4 | 5 | constructor: (options = {}) -> 6 | super(_.extend({render_method:'replace'}, options)) 7 | 8 | @el = document.createElement('div') 9 | Marbles.DOM.insertBefore(@el, @parentView().el) 10 | 11 | @render() 12 | 13 | destroy: => 14 | @detachChildViews() 15 | Marbles.DOM.removeNode(@el) 16 | @detach() 17 | 18 | -------------------------------------------------------------------------------- /lib/assets/javascripts/views/post/conversation/001_component.js.coffee: -------------------------------------------------------------------------------- 1 | Marbles.Views.ConversationComponent = class ConversationComponentView extends Marbles.View 2 | postView: => 3 | @findParentView('post') 4 | 5 | post: => 6 | return unless post_view = @postView() 7 | repost_view = _.last(post_view.childViews('Repost') || []) 8 | (repost_view || post_view).post() 9 | 10 | postContext: => 11 | _.extend Marbles.Views.Post::context(arguments...), 12 | is_conversation_view: true 13 | 14 | renderPostHTML: => 15 | Marbles.Views.PostsFeed::renderPostHTML.apply(@, arguments) 16 | 17 | renderHTML: (posts) => 18 | html = "" 19 | for post in posts 20 | html += @renderPostHTML(post) 21 | html 22 | 23 | prependRender: => 24 | Marbles.Views.PostsFeed::prependRender.apply(@, arguments) 25 | 26 | appendRender: => 27 | Marbles.Views.PostsFeed::appendRender.apply(@, arguments) 28 | 29 | fetchPost: (params, callback) => 30 | TentStatus.Models.Post.find(params, { success: callback }) 31 | -------------------------------------------------------------------------------- /lib/assets/javascripts/views/post/conversation/children.js.coffee: -------------------------------------------------------------------------------- 1 | Marbles.Views.ConversationChildren = class ConversationChildrenView extends Marbles.Views.ConversationComponent 2 | @template_name: '_conversation_children' 3 | @partial_names: ['_post'].concat(Marbles.Views.Post.partial_names) 4 | @view_name: 'conversation_children' 5 | 6 | constructor: (options = {}) -> 7 | super 8 | 9 | # Replying to a post with the conversation view open prepends it to the replies feed 10 | TentStatus.Models.StatusPost.on 'create:success', (post, xhr) => 11 | return unless post.get('type') is TentStatus.config.POST_TYPES.STATUS_REPLY 12 | conversation_post = @post() 13 | return unless _.any post.get('mentions') || [], (m) => 14 | conversation_post.get('entity') == m.entity && conversation_post.get('id') == m.post && (!m.version || conversation_post.get('version.id') == m.version) 15 | @prependRender([post]) 16 | 17 | setImmediate @fetchPosts 18 | 19 | fetchPosts: (options = {}) => 20 | reference_post = @post() 21 | reference_post.fetchReplies?(_.extend( 22 | success: (posts) => 23 | @render(posts) 24 | , options)) 25 | 26 | -------------------------------------------------------------------------------- /lib/assets/javascripts/views/post/conversation/parents.js.coffee: -------------------------------------------------------------------------------- 1 | Marbles.Views.ConversationParents = class ConversationParentsView extends Marbles.Views.ConversationComponent 2 | @template_name: '_conversation_parents' 3 | @partial_names: ['_post'].concat(Marbles.Views.Post.partial_names) 4 | @view_name: 'conversation_parents' 5 | 6 | constructor: (options = {}) -> 7 | super 8 | 9 | setImmediate @fetchPosts 10 | 11 | postContext: => 12 | _.extend super, 13 | is_conversation_view_parent: true 14 | 15 | fetchPosts: (reference_post) => 16 | reference_post ?= @post() 17 | mentions = reference_post.get('mentioned_posts') 18 | 19 | for m in mentions 20 | do (m) => 21 | @fetchPost {entity: m.entity, id: m.post}, (post) => 22 | @prependRender([post]) 23 | @trigger('ready') 24 | 25 | -------------------------------------------------------------------------------- /lib/assets/javascripts/views/post/conversation/reference.js.coffee: -------------------------------------------------------------------------------- 1 | Marbles.Views.ConversationReference = class ConversationReferenceView extends Marbles.Views.ConversationComponent 2 | @view_name: 'conversation_reference' 3 | 4 | constructor: (options = {}) -> 5 | super 6 | 7 | @el.appendChild(@postView().el) 8 | 9 | unless @postView().post()?.get('is_repost') 10 | post_container_el = Marbles.DOM.querySelector('.post-container', @el) 11 | @repost_visibility_el = @postView().el.repost_visibility_el ?= document.createElement('div') 12 | Marbles.DOM.setAttr(@repost_visibility_el, 'data-view', 'RepostVisibility') 13 | post_container_el.appendChild(@repost_visibility_el) 14 | @bindViews() 15 | 16 | detach: => 17 | Marbles.DOM.insertBefore(@postView().el, @parentView().el) 18 | Marbles.DOM.removeNode(@repost_visibility_el) if @repost_visibility_el 19 | super 20 | 21 | -------------------------------------------------------------------------------- /lib/assets/javascripts/views/post/edit_post.js.coffee: -------------------------------------------------------------------------------- 1 | bindFn = (fn, me) -> 2 | return -> fn.apply(me, arguments) 3 | 4 | Marbles.Views.EditPost = class EditPostView extends Marbles.View 5 | @template_name: 'edit_post' 6 | @view_name: 'edit_post' 7 | 8 | render_method: 'replace' 9 | 10 | constructor: -> 11 | # inherit most NewPostForm methods 12 | _blacklist = ['constructor', 'initialRender', 'post', 'submit', 'buildPostAttributes'] 13 | for k, fn of Marbles.Views.NewPostForm:: 14 | continue unless Marbles.Views.NewPostForm::.hasOwnProperty(k) 15 | continue if _blacklist.indexOf(k) != -1 16 | @[k] = bindFn(fn, @) 17 | 18 | super 19 | 20 | initialize: (options = {}) -> 21 | @elements ?= {} 22 | @text = {} 23 | 24 | @mentions = [] 25 | 26 | post = options.parent_view.post() 27 | @post_cid = post.cid 28 | 29 | @entity = post.get('entity') 30 | 31 | profile = TentStatus.Models.MetaProfile.find(entity: @entity, fetch: false) 32 | unless profile 33 | profile = new TentStatus.Models.MetaProfile(entity: @entity) 34 | profile.fetch(null, success: @profileFetchSuccess) 35 | @profile_cid = profile.cid 36 | 37 | @on 'ready', @bindCancel 38 | @on 'ready', @initPermissions 39 | @on 'ready', @initPostMarkdown 40 | @on 'ready', @focusTextarea 41 | @on 'ready', @init 42 | @on 'ready', => 43 | @permissionsFieldsView().subscribeToMentions() 44 | @textareaMentionsView().inline_mentions_manager.updateMentions() 45 | 46 | bindCancel: => 47 | @elements.cancel_el = @el.querySelector('[data-action=cancel]') 48 | 49 | Marbles.DOM.on @elements.cancel_el, 'click', @renderPost 50 | 51 | permissionsFieldsView: => 52 | @childViews('PermissionsFields')[0] 53 | 54 | initPermissions: => 55 | entities = @post().get('permissions.entities') || [] 56 | permissions_view = @permissionsFieldsView() 57 | 58 | for entity in entities 59 | permissions_view.addOption( 60 | text: TentStatus.Helpers.minimalEntity(entity) 61 | value: entity 62 | ) 63 | 64 | initPostMarkdown: => 65 | markdown = TentStatus.Helpers.expandTentMarkdown(@post().get('content.text'), @post().get('mentions')) 66 | textarea_view = @textareaMentionsView() 67 | textarea_view.el.value = markdown 68 | 69 | renderPost: => 70 | @parentView().render() 71 | 72 | buildPostAttributes: => 73 | attrs = Marbles.DOM.serializeForm(@elements.form) 74 | post = @post() 75 | 76 | in_reply_to_mention = post.get('mentioned_posts')[0] 77 | if in_reply_to_mention 78 | attrs.mentions_post_entity = in_reply_to_mention.entity || post.get('entity') 79 | attrs.mentions_post_id = in_reply_to_mention.post 80 | 81 | @buildPostMentionsAttributes(attrs) 82 | @buildPostPermissionsAttributes(attrs) 83 | attrs = _.extend attrs, { 84 | type: post.get('type').toString() 85 | } 86 | attrs.content = { text: @textareaMentionsView().inline_mentions_manager.processedMarkdown() } 87 | delete attrs.text 88 | attrs 89 | 90 | submit: (data) => 91 | @disableWith(@text.disable_with) 92 | data ?= @buildPostAttributes() 93 | 94 | @post().update(data, 95 | failure: (res, xhr) => 96 | @enable() 97 | @showErrors([{ text: "Error: #{JSON.parse(xhr.responseText)?.error}" }]) 98 | 99 | success: @renderPost 100 | ) 101 | 102 | post: => 103 | Marbles.Model.find(cid: @post_cid) 104 | 105 | render: => 106 | super 107 | 108 | # the element is replaced on render 109 | @parentView().el = @el 110 | 111 | -------------------------------------------------------------------------------- /lib/assets/javascripts/views/post/post_action_conversation.js.coffee: -------------------------------------------------------------------------------- 1 | Marbles.Views.PostActionConversation = class PostActionConversationView extends Marbles.Views.PostAction 2 | @view_name: 'post_action_conversation' 3 | 4 | performAction: => 5 | if view = @findParentView('conversation_parents') 6 | post_view = @findParentView('post') 7 | reference_post = post_view.post() 8 | 9 | el = post_view.el 10 | offset_top = el.offsetTop - window.scrollY 11 | 12 | unless @visible 13 | @visible = true 14 | 15 | view.once 'ready', => 16 | delta = (el.offsetTop - window.scrollY) - offset_top 17 | window.scrollTo(window.scrollX, window.scrollY + delta) 18 | post_view.focus() 19 | 20 | view.fetchPosts(reference_post) if reference_post 21 | return 22 | 23 | if (post_view = @postView()) && (reference_post = post_view.post()) && reference_post.options.partial_data 24 | return reference_post.fetch 25 | success: (post) => 26 | post_view.render(post_view.context(post)) 27 | _.last(post_view.childViews('PostActionConversation'))?.performAction() 28 | 29 | if @visible 30 | @hide() 31 | else 32 | @show() 33 | 34 | conversationView: => 35 | return view if @conversation_view_cid && (view = Marbles.Views.Conversation.find(@conversation_view_cid)) 36 | post_view = @postView() 37 | view = new Marbles.Views.Conversation parent_view: post_view 38 | @conversation_view_cid = view.cid 39 | view 40 | 41 | hide: => 42 | view = @conversationView() 43 | 44 | el = view.parentView().el 45 | offsetTop = el.offsetTop - window.scrollY 46 | 47 | view.destroy() 48 | delete @conversation_view_cid 49 | @visible = false 50 | 51 | delta = (el.offsetTop - window.scrollY) - offsetTop 52 | window.scrollTo(window.scrollX, window.scrollY + delta) 53 | 54 | show: => 55 | @visible = true 56 | view = @conversationView() 57 | post_view = view.parentView() 58 | 59 | el = post_view.el 60 | offsetTop = el.offsetTop - window.scrollY 61 | 62 | view.on 'init:ConversationReference', (reference_view) => 63 | delta = (el.offsetTop - window.scrollY) - offsetTop 64 | window.scrollTo(window.scrollX, window.scrollY + delta) 65 | post_view.focus() 66 | 67 | view.on 'init:ConversationParents', (parents_view) => 68 | parents_view.once 'ready', => 69 | delta = (el.offsetTop - window.scrollY) - offsetTop 70 | window.scrollTo(window.scrollX, window.scrollY + delta) 71 | post_view.focus() 72 | 73 | -------------------------------------------------------------------------------- /lib/assets/javascripts/views/post/post_action_delete.js.coffee: -------------------------------------------------------------------------------- 1 | Marbles.Views.PostActionDelete = class PostActionDeleteView extends Marbles.Views.PostAction 2 | @view_name: 'post_action_delete' 3 | 4 | postView: => @parentView() 5 | 6 | post: => 7 | @postView()?.parentPost?() || @postView()?.post() 8 | 9 | showErrors: (error) => 10 | alert(_.map(error, (e) -> e.text).join("\n")) 11 | 12 | performAction: => 13 | @delete() 14 | 15 | delete: => 16 | post = @post() 17 | post.delete( 18 | error: (res, xhr) => 19 | @enable() 20 | @showErrors([{ text: "Error: #{JSON.parse(xhr.responseText)?.error}" }]) 21 | 22 | success: (post, xhr) => 23 | @detachPost() 24 | ) 25 | 26 | detachPost: => 27 | post_view = @postView() 28 | Marbles.DOM.removeNode(post_view.el) 29 | post_view.detach() 30 | 31 | -------------------------------------------------------------------------------- /lib/assets/javascripts/views/post/post_action_edit.js.coffee: -------------------------------------------------------------------------------- 1 | Marbles.Views.PostActionEdit = class PostActionEditView extends Marbles.Views.PostAction 2 | @view_name: 'post_action_edit' 3 | 4 | performAction: => 5 | post_view = @postView() 6 | edit_view = @editPostView() 7 | 8 | edit_view.render() 9 | 10 | editPostView: => 11 | view = Marbles.Views.EditPost.find(@edit_view_cid) if @edit_view_cid 12 | return view if view 13 | 14 | post_view = @postView() 15 | view = new Marbles.Views.EditPost(el: post_view.el, parent_view: @postView()) 16 | @edit_view_cid = view.cid 17 | 18 | view 19 | 20 | -------------------------------------------------------------------------------- /lib/assets/javascripts/views/post/post_action_reply.js.coffee: -------------------------------------------------------------------------------- 1 | Marbles.Views.PostActionReply = class PostActionReplyView extends Marbles.Views.PostAction 2 | @view_name: 'post_action_reply' 3 | 4 | performAction: => 5 | post_reply_view_cid = @parentView()._child_views.PostReplyForm[0] 6 | post_reply_view = Marbles.View.instances.all[post_reply_view_cid] 7 | post_reply_view.toggle() 8 | 9 | -------------------------------------------------------------------------------- /lib/assets/javascripts/views/post/post_action_repost.js.coffee: -------------------------------------------------------------------------------- 1 | Marbles.Views.PostActionRepost = class PostActionRepostView extends Marbles.Views.PostAction 2 | @view_name: 'post_action_repost' 3 | 4 | performAction: => 5 | post = TentStatus.Models.Post.find(cid: @parentView().post_cid) 6 | data = { 7 | permissions: 8 | public: true 9 | type: "https://tent.io/types/repost/v0##{(new TentClient.PostType post.get('type')).toStringWithoutFragment()}" 10 | mentions: [{ entity: post.get('entity'), post: post.get('id'), type: post.get('type') }] 11 | refs: [{ entity: post.get('entity'), post: post.get('id'), type: post.get('type') }] 12 | } 13 | TentStatus.Models.Post.create(data, 14 | error: (res, xhr) => 15 | @enable() 16 | alert("Error: #{JSON.parse(xhr.responseText)?.error}") # TODO: use a more unobtrusive notification 17 | 18 | success: (post, xhr) => 19 | @disable() 20 | ) 21 | 22 | enable: => 23 | @disabled = false 24 | 25 | disable: => 26 | @disabled = true 27 | 28 | -------------------------------------------------------------------------------- /lib/assets/javascripts/views/post/post_reply_form.js.coffee: -------------------------------------------------------------------------------- 1 | Marbles.Views.PostReplyForm = class PostReplyFormView extends Marbles.Views.NewPostForm 2 | @template_name: '_post_reply_form' 3 | @view_name: 'post_reply_form' 4 | @model: TentStatus.Models.StatusReplyPost 5 | 6 | is_reply_form: true 7 | 8 | constructor: -> 9 | super 10 | 11 | @on 'ready', @initInlineMentions 12 | 13 | fetchProfile: (entity, callback) => 14 | profile = TentStatus.Models.MetaProfile.find(entity: entity) 15 | profile ?= new TentStatus.Models.MetaProfile(entity: entity) 16 | 17 | if profile.get('id') 18 | callback(profile) 19 | else 20 | profile.fetch( 21 | complete: => 22 | callback(profile) 23 | ) 24 | 25 | initInlineMentions: => 26 | textarea_view = @textareaMentionsView() 27 | return unless textarea_view 28 | 29 | text = "" 30 | 31 | entities = @post().conversation_entities 32 | entities_display_text = {} 33 | num_pending_profiles = entities.length 34 | 35 | return unless entities.length 36 | 37 | Marbles.DOM.setAttr(textarea_view.el, 'disabled', 'disabled') 38 | 39 | entityCompleteFn = (entity, profile) => 40 | inline_mention = new TentStatus.InlineMentionsManager.InlineMention( 41 | entity: entity 42 | display_text: profile?.get('name') || TentStatus.Helpers.minimalEntity(entity) 43 | ) 44 | 45 | entities_display_text[entity] = inline_mention.toExpandedMarkdownString() 46 | 47 | num_pending_profiles -= 1 48 | if num_pending_profiles <= 0 49 | for entity in entities 50 | text += entities_display_text[entity] + " " 51 | 52 | textarea_view.el.value = text 53 | Marbles.DOM.removeAttr(textarea_view.el, 'disabled') 54 | 55 | textarea_view.inline_mentions_manager.updateMentions() 56 | 57 | for entity in entities 58 | do (entity) => 59 | @fetchProfile entity, (profile) => 60 | entityCompleteFn(entity, profile) 61 | 62 | # no initial render 63 | initialRender: => 64 | 65 | profileFetchSuccess: => 66 | @render() if @visible 67 | 68 | toggle: => 69 | if @visible 70 | @hide() 71 | else 72 | @show() 73 | 74 | hide: => 75 | @visible = false 76 | Marbles.DOM.hide(@el) 77 | 78 | show: => 79 | @visible = true 80 | 81 | setImmediate @focusTextarea 82 | 83 | if @ready 84 | Marbles.DOM.show(@el) 85 | else 86 | @render() 87 | 88 | post: => 89 | TentStatus.Models.Post.instances.all[@parentView().post_cid] 90 | 91 | -------------------------------------------------------------------------------- /lib/assets/javascripts/views/post/repost.js.coffee: -------------------------------------------------------------------------------- 1 | Marbles.Views.Repost = class RepostView extends Marbles.Views.Post 2 | @template_name: '_repost' 3 | @partial_names: ['_post_inner', '_post_inner_actions'] 4 | @view_name: 'repost' 5 | 6 | constructor: -> 7 | super 8 | 9 | @parent_post_cid = Marbles.DOM.attr(@el, 'data-parent_post_cid') 10 | 11 | @fetchPost() 12 | 13 | parentPost: => 14 | TentStatus.Models.Post.instances.all[@parent_post_cid] 15 | 16 | conversationView: => @findParentView('conversation') 17 | 18 | fetchPost: (parent_post = @parentPost()) => 19 | TentStatus.Models.Post.find { id: parent_post.get('refs.0.post'), entity: (parent_post.get('refs.0.entity') || parent_post.get('entity')) }, { 20 | success: @fetchSuccess 21 | failure: @fetchFailure 22 | } 23 | 24 | fetchSuccess: (post) => 25 | return (setImmediate => @fetchPost(post)) if post.get('is_repost') 26 | @post_cid = post.cid 27 | @render(@context(post)) 28 | 29 | fetchFailure: => 30 | @parentView().detach() 31 | 32 | post: => 33 | TentStatus.Models.Post.find(cid: @post_cid) 34 | 35 | context: (post) => 36 | parent_post = @parentPost() 37 | _.extend super, { 38 | has_parent: true 39 | is_conversation_view: !!@conversationView() 40 | parent: 41 | cid: parent_post.cid 42 | formatted: 43 | entity: TentStatus.Helpers.minimalEntity(parent_post.get('entity')) 44 | } 45 | -------------------------------------------------------------------------------- /lib/assets/javascripts/views/posts_feed/mentions.js.coffee: -------------------------------------------------------------------------------- 1 | Marbles.Views.MentionsPostsFeed = class MentionsPostsFeedView extends Marbles.Views.PostsFeed 2 | @view_name: 'mentions_posts_feed' 3 | @last_post_selector: "ul[data-view=MentionsPostsFeed]>li.post:last-of-type" 4 | 5 | initialize: (options = {}) => 6 | options.entity = options.parent_view.entity 7 | options.types = [TentStatus.config.POST_TYPES.STATUS_REPLY, TentStatus.config.POST_TYPES.STATUS] 8 | options.feed_queries = [{ 9 | mentions: options.entity 10 | entities: false 11 | profiles: 'entity' 12 | }] 13 | super(options) 14 | 15 | collection = @postsCollection() 16 | collection.on 'reset', @clearRepliesUnreadCount 17 | collection.on 'prepend', @clearRepliesUnreadCount 18 | 19 | shouldAddPostToFeed: (post) => 20 | super && post.isEntityMentioned(@entity) 21 | 22 | clearRepliesUnreadCount: => 23 | ref = @postsCollection().first() 24 | for cid in Marbles.View.instances.mentions_unread_count 25 | continue unless v = Marbles.View.instances.all[cid] 26 | v.clearCount(ref) 27 | 28 | -------------------------------------------------------------------------------- /lib/assets/javascripts/views/posts_feed/profile.js.coffee: -------------------------------------------------------------------------------- 1 | Marbles.Views.ProfilePostsFeed = class ProfilePostsFeedView extends Marbles.Views.PostsFeed 2 | @view_name: 'profile_posts_feed' 3 | @last_post_selector: "ul[data-view=ProfilePostsFeed]>li.post:last-of-type" 4 | 5 | initialize: (options = {}) => 6 | options.entity = @findParentView('profile').profile().get('entity') 7 | options.headers = { 8 | 'Cache-Control': 'proxy' 9 | } 10 | super(options) 11 | 12 | shouldAddPostToFeed: (post) => 13 | return false unless post.get('entity') == @entity 14 | true 15 | 16 | -------------------------------------------------------------------------------- /lib/assets/javascripts/views/posts_feed/reposts.js.coffee: -------------------------------------------------------------------------------- 1 | Marbles.Views.RepostsPostsFeed = class RepostsPostsFeedView extends Marbles.Views.PostsFeed 2 | @view_name: 'reposts_posts_feed' 3 | @last_post_selector: "ul[data-view=RepostsPostsFeed]>li.post:last-of-type" 4 | 5 | initialize: (options = {}) => 6 | options.entity = options.parent_view.entity 7 | options.types = TentStatus.config.repost_types 8 | options.feed_queries = [{ 9 | mentions: options.entity 10 | entities: false 11 | profiles: 'entity' 12 | }] 13 | super(options) 14 | 15 | collection = @postsCollection() 16 | collection.on 'reset', @clearRepostsUnreadCount 17 | collection.on 'prepend', @clearRepostsUnreadCount 18 | 19 | shouldAddPostToFeed: (post) => 20 | super && post.isEntityMentioned(@entity) 21 | 22 | clearRepostsUnreadCount: => 23 | ref = @postsCollection().first() 24 | for cid in Marbles.View.instances.reposts_unread_count 25 | continue unless v = Marbles.View.instances.all[cid] 26 | v.clearCount(ref) 27 | 28 | -------------------------------------------------------------------------------- /lib/assets/javascripts/views/posts_feed/site.js.coffee: -------------------------------------------------------------------------------- 1 | Marbles.Views.SitePostsFeed = class SitePostsFeedView extends Marbles.Views.PostsFeed 2 | @view_name: 'site_posts_feed' 3 | @last_post_selector: "ul[data-view=SitePostsFeed]>li.post:last-of-type" 4 | 5 | initialize: (options = {}) => 6 | site_feed_meta_post = { 7 | content: { 8 | servers: [{ 9 | urls: { 10 | "posts_feed": TentStatus.config.services.site_feed_api_root 11 | } 12 | }] 13 | } 14 | } 15 | 16 | options.entity = TentStatus.config.meta.content.entity 17 | options.types = [TentStatus.config.POST_TYPES.STATUS] 18 | options.feed_queries = [{ entities: false, profiles: 'entity' }] 19 | options.context = 'site-feed' 20 | 21 | @tent_client = new TentClient(TentStatus.config.meta.content.entity, 22 | server_meta_post: site_feed_meta_post 23 | ) 24 | 25 | # WithCredentials replaces Hawk middleware 26 | @tent_client.middleware = [Marbles.HTTP.Middleware.WithCredentials] 27 | 28 | super(options) 29 | 30 | shouldAddPostToFeed: (post) => 31 | post.get('permissions.public') == true && !post.is_repost 32 | -------------------------------------------------------------------------------- /lib/assets/javascripts/views/profile.js.coffee: -------------------------------------------------------------------------------- 1 | Marbles.Views.Profile = class ProfileView extends Marbles.View 2 | @template_name: 'profile' 3 | @view_name: 'profile' 4 | 5 | constructor: (options = {}) -> 6 | @container = Marbles.Views.container 7 | super 8 | 9 | @fetchMetaProfile(options.entity) 10 | 11 | fetchMetaProfile: (entity) => 12 | model = TentStatus.Models.MetaProfile.find(entity: entity, fetch: false) 13 | 14 | if model 15 | @profile_cid = model.cid 16 | @render(@context(model)) 17 | else 18 | TentStatus.Models.MetaProfile.fetch(entity, 19 | success: (model) => 20 | @profile_cid = model.cid 21 | @render(@context(model)) 22 | 23 | failure: (res, xhr) => 24 | console.warn("No profile found for #{JSON.stringify(entity)}! #{xhr.status} #{res}") 25 | ) 26 | 27 | profile: => 28 | TentStatus.Models.MetaProfile.find(cid: @profile_cid, fetch: false) 29 | 30 | context: (profile = @profile()) => 31 | profile: profile 32 | has_name: !!profile.get('name') 33 | formatted: 34 | bio: profile.get('bio') 35 | entity: TentStatus.Helpers.formatUrlWithPath(profile.get('entity')) 36 | website: TentStatus.Helpers.formatUrlWithPath(profile.get('website')) 37 | 38 | -------------------------------------------------------------------------------- /lib/assets/javascripts/views/profile/resource_count.js.coffee: -------------------------------------------------------------------------------- 1 | Marbles.Views.ProfileResourceCount = class FollowersCountView extends Marbles.View 2 | @template_name: 'profile/resource_count' 3 | 4 | constructor: (options = {}) -> 5 | super 6 | 7 | @render() 8 | 9 | return unless profile = @profile() 10 | @constructor.model.fetchCount {entities: profile.get('entity')}, 11 | failure: (res, xhr) => 12 | 13 | success: (count) => 14 | @render(@context(count)) 15 | 16 | profile: => @parentView().profile() 17 | 18 | context: (count) => 19 | profile = @profile() 20 | 21 | url: TentStatus.Helpers.route(@constructor.route, {entity: profile.get('entity')}) 22 | count: count 23 | pluralized_resource_name: TentStatus.Helpers.capitalize TentStatus.Helpers.pluralize(@constructor.resource_name.singular, count, @constructor.resource_name.plural) 24 | 25 | -------------------------------------------------------------------------------- /lib/assets/javascripts/views/profile/resource_count/followers_count.js.coffee: -------------------------------------------------------------------------------- 1 | Marbles.Views.ProfileSubscriberCount = class SubscriberCountView extends Marbles.Views.ProfileResourceCount 2 | @view_name: 'profile/subscriber_count' 3 | @model: TentStatus.Models.Follower 4 | @resource_name: {singular: 'subscriber', plural: 'subscribers'} 5 | @route: 'subscribers' 6 | -------------------------------------------------------------------------------- /lib/assets/javascripts/views/profile/resource_count/posts_count.js.coffee: -------------------------------------------------------------------------------- 1 | Marbles.Views.ProfilePostCount = class PostCountView extends Marbles.Views.ProfileResourceCount 2 | @view_name: 'profile/post_count' 3 | @model: TentStatus.Models.StatusPost 4 | @resource_name: {singular: 'post', plural: 'posts'} 5 | @route: 'profile' 6 | 7 | context: => 8 | _.extend super, 9 | url: TentStatus.Helpers.entityProfileUrl(@profile().get('entity')) 10 | -------------------------------------------------------------------------------- /lib/assets/javascripts/views/profile/resource_count/subscription_count.js.coffee: -------------------------------------------------------------------------------- 1 | Marbles.Views.ProfileSubscriptionCount = class SubscriptionCountView extends Marbles.Views.ProfileResourceCount 2 | @view_name: 'profile/subscription_count' 3 | @model: TentStatus.Models.Following 4 | @resource_name: {singular: 'subscription', plural: 'subscriptions'} 5 | @route: 'subscriptions' 6 | -------------------------------------------------------------------------------- /lib/assets/javascripts/views/profile_component.js.coffee: -------------------------------------------------------------------------------- 1 | Marbles.Views.ProfileComponent = class ProfileComponentView extends Marbles.View 2 | constructor: -> 3 | super 4 | 5 | @entity = Marbles.DOM.attr(@el, 'data-entity') 6 | @add_title = Marbles.DOM.hasAttr(@el, 'data-title') 7 | @no_link = Marbles.DOM.hasAttr(@el, 'data-no_link') 8 | @css_class = Marbles.DOM.attr(@el, 'data-class') 9 | 10 | TentStatus.Models.MetaProfile.on("#{@entity}:fetch:success", @render, null, args: false) 11 | TentStatus.Models.MetaProfile.on("#{@entity}:fetch:failure", @render, null, args: false) 12 | 13 | model = @profileModel() 14 | model.on 'change:avatar_url change:name', @render, null, args: false 15 | 16 | if model.get('id') 17 | @render() 18 | else 19 | model.fetch() 20 | 21 | createProfileModel: => 22 | new TentStatus.Models.MetaProfile(entity: @entity) 23 | 24 | profileModel: => 25 | TentStatus.Models.MetaProfile.find(entity: @entity, fetch: false) || @createProfileModel() 26 | 27 | context: (profile = @profileModel()) => 28 | profile: profile 29 | has_name: !!(profile?.get('name')) 30 | entity: @entity 31 | profile_url: TentStatus.Helpers.entityProfileUrl(@entity) 32 | css_class: @css_class 33 | title: if @add_title then profile?.get('name') || TentStatus.Helpers.formatUrlWithPath(@entity) else null 34 | no_link: @no_link 35 | formatted: 36 | entity: TentStatus.Helpers.formatUrlWithPath(@entity) 37 | 38 | -------------------------------------------------------------------------------- /lib/assets/javascripts/views/profile_component/avatar.js.coffee: -------------------------------------------------------------------------------- 1 | Marbles.Views.ProfileAvatar = class ProfileAvatarView extends Marbles.Views.ProfileComponent 2 | @template_name: '_profile_avatar' 3 | @view_name: 'profile_avatar' 4 | 5 | constructor: -> 6 | super 7 | 8 | @on 'ready', @checkImageMortality 9 | 10 | checkImageMortality: => 11 | img = Marbles.DOM.querySelector('img', @el) 12 | return unless img 13 | unless img.complete 14 | return setTimeout @checkImageMortality, 10 15 | 16 | # Fallback to default avatar if image fails to load 17 | if img.naturalHeight == 0 && (url = TentStatus.config.defaultAvatarURL(@get('entity'))) 18 | img.src = url 19 | 20 | -------------------------------------------------------------------------------- /lib/assets/javascripts/views/profile_component/name.js.coffee: -------------------------------------------------------------------------------- 1 | Marbles.Views.ProfileName = class ProfileNameView extends Marbles.Views.ProfileComponent 2 | @template_name: '_profile_name' 3 | @view_name: 'profile_name' 4 | 5 | constructor: -> 6 | super 7 | 8 | @render() 9 | 10 | -------------------------------------------------------------------------------- /lib/assets/javascripts/views/relationship.js.coffee: -------------------------------------------------------------------------------- 1 | Marbles.Views.Relationship = class RelationshipView extends Marbles.View 2 | @view_name: 'relationship' 3 | @template_name: 'relationship' 4 | 5 | getEntity: => 6 | @parentView()?.getEntity?() 7 | -------------------------------------------------------------------------------- /lib/assets/javascripts/views/relative_timestamp.js.coffee: -------------------------------------------------------------------------------- 1 | Marbles.Views.RelativeTimestamp = class RelativeTimestampView extends Marbles.View 2 | @view_name: 'relative_timestamp' 3 | 4 | constructor: -> 5 | super 6 | 7 | @time = parseInt Marbles.DOM.attr(@el, 'data-datetime') 8 | 9 | @on 'ready', @setUpdateDelay 10 | @setUpdateDelay() 11 | 12 | setUpdateDelay: => 13 | delta = (new Date * 1) - (@time * 1000) 14 | 15 | if delta < 60000 # less than 1 minute ago 16 | setTimeout (=> @render()), 2000 # update in 2 seconds 17 | else if delta < 3600000 # less than 1 hour ago 18 | setTimeout (=> @render()), 30000 # update in 30 seconds 19 | else if delta < 86400000 # less than 1 day ago 20 | setTimeout (=> @render()), 1800000 # update in 30 minutes 21 | else if delta < 2678400000 # 31 days ago 22 | setTimeout (=> @render()), 43200000 # update in 12 hours 23 | else 24 | setTimeout (=> @render()), 2419200 # update in 28 days 25 | 26 | context: => 27 | formatted: 28 | time: TentStatus.Helpers.formatRelativeTime @time 29 | 30 | renderHTML: (context = @context()) => 31 | context.formatted.time.toString() 32 | 33 | -------------------------------------------------------------------------------- /lib/assets/javascripts/views/repost_visibility.js.coffee: -------------------------------------------------------------------------------- 1 | Marbles.Views.RepostVisibility = class RepostVisibilityView extends Marbles.View 2 | @template_name: 'repost_visibility' 3 | @view_name: 'repost_visibility' 4 | 5 | @types: TentStatus.config.repost_types 6 | 7 | constructor: -> 8 | super 9 | 10 | _post = @findParentView('post')?.post() 11 | @entity = if _post?.get('is_repost') then _post.get('entity') else null 12 | 13 | @reposter_profile_cids = {} 14 | setImmediate => @initialFetchReposters() 15 | 16 | @render() 17 | 18 | post: => 19 | @parentView()?.post() 20 | 21 | initialFetchReposters: => 22 | return unless post = @post() 23 | 24 | params = { 25 | entity: post.get('entity') 26 | post: post.get('id') 27 | profiles: 'entity' 28 | limit: 60 29 | } 30 | 31 | @fetchReposters(params) 32 | 33 | isRepostType: (type) => 34 | for t in @constructor.types 35 | return true if type == t 36 | false 37 | 38 | fetchReposters: (params) => 39 | return unless params.entity && params.post 40 | 41 | TentStatus.tent_client.post.mentions( 42 | params: params 43 | callback: (res, xhr) => 44 | return unless xhr.status == 200 45 | 46 | for mention in res.mentions 47 | continue unless @isRepostType(mention.type) 48 | profile = TentStatus.Models.MetaProfile.find(entity: mention.entity, fetch: false) 49 | if !profile && _profile_attrs = res.profiles[mention.entity] 50 | profile = new TentStatus.Models.MetaProfile(_profile_attrs) 51 | 52 | @reposter_profile_cids[mention.entity || params.entity] = profile?.cid 53 | 54 | @render() if Object.keys(@reposter_profile_cids).length 55 | 56 | if res.pages?.next 57 | _.extend(params, Marbles.history.deserializeParams(res.pages.next)) 58 | @fetchReposters(params) 59 | ) 60 | 61 | context: => 62 | entity: @entity 63 | count: @count 64 | pluralized_other: if @count then TentStatus.Helpers.pluralize('other', @count, 'others') else null 65 | entities: Object.keys(@reposter_profile_cids) 66 | 67 | -------------------------------------------------------------------------------- /lib/assets/javascripts/views/reposts.js.coffee: -------------------------------------------------------------------------------- 1 | Marbles.Views.Reposts = class RepostsView extends Marbles.View 2 | @template_name: 'reposts' 3 | @view_name: 'reposts' 4 | 5 | constructor: (options = {}) -> 6 | @container = Marbles.Views.container 7 | @entity = options.entity 8 | super 9 | 10 | @render() 11 | -------------------------------------------------------------------------------- /lib/assets/javascripts/views/search.js.coffee: -------------------------------------------------------------------------------- 1 | Marbles.Views.Search = class SearchView extends Marbles.View 2 | @view_name: 'search' 3 | @template_name: 'search' 4 | 5 | constructor: (options = {}) -> 6 | super 7 | @params = options.params 8 | 9 | setImmediate @render 10 | 11 | -------------------------------------------------------------------------------- /lib/assets/javascripts/views/search_fetch_pool.js.coffee: -------------------------------------------------------------------------------- 1 | Marbles.Views.SearchFetchPool = class SearchFetchPoolView extends Marbles.View 2 | @view_name: 'search_fetch_pool' 3 | @template_name: 'fetch_posts_pool' 4 | 5 | constructor: (options = {}) -> 6 | super 7 | 8 | @on 'ready', @bindLink 9 | 10 | @fetch_interval = new TentStatus.FetchInterval fetch_callback: @fetchResults 11 | 12 | options.parent_view.on 'init-view', (view_class_name, view) => 13 | switch view_class_name 14 | when 'SearchResults' 15 | @initSearchResultsView(view) 16 | when 'SearchHits' 17 | @initSearchHitsView(view) 18 | 19 | @initSearchHitsView(_.last(options.parent_view.childViews('SearchHits') || [])) 20 | 21 | initSearchHitsView: (hits_view) => 22 | @hits_view_cid = hits_view.cid 23 | 24 | initSearchResultsView: (results_feed_view) => 25 | @results_feed_view_cid = results_feed_view.cid 26 | results_collection = results_feed_view.postsCollection() 27 | results_collection.on 'reset', => 28 | @results_collection = new TentStatus.Collections.SearchResults api_root: results_collection.api_root 29 | @latest_published_at = (results_collection.first()?.get('published_at') || (new Date * 1)) 30 | @params = results_feed_view.params 31 | @fetch_interval.start() 32 | 33 | updateHits: (res) => 34 | return unless hits_view = Marbles.View.find(@hits_view_cid) 35 | return unless results_feed_view = Marbles.View.find(@results_feed_view_cid) 36 | hits_view.render(hits_view.context(total_hits: results_feed_view.total_hits + res.total_hits)) 37 | 38 | fetchResults: => 39 | return if @frozen 40 | return unless @params.q 41 | 42 | params = _.extend {}, @params, { 43 | until: @latest_published_at 44 | } 45 | delete params.max_time 46 | 47 | options = { success: @fetchSuccess, error: @fetchError, prepend: true } 48 | @results_collection.fetch(params, options) 49 | 50 | fetchSuccess: (results, res) => 51 | if results.length 52 | @fetch_interval.reset() 53 | @latest_published_at = Math.max(_.map(results, (r) -> r.get('published_at'))...) 54 | @updateHits(res) 55 | @render() 56 | else 57 | @fetch_interval.increaseDelay() 58 | 59 | @frozen = false 60 | 61 | fetchError: => 62 | @fetch_interval.increaseDelay() 63 | @frozen = false 64 | 65 | emptyPool: => 66 | results_feed_view = Marbles.View.find(@results_feed_view_cid) 67 | return unless results_feed_view 68 | 69 | last_result_cid = _.last(@results_collection.model_ids) 70 | 71 | results_feed_view.prependRender(@results_collection.models()) 72 | results_feed_view.postsCollection().prependIds(@results_collection.model_ids) 73 | @results_collection.empty() 74 | 75 | @render() 76 | 77 | context: => 78 | posts_count: @results_collection.model_ids.length 79 | 80 | bindLink: => 81 | link_element = Marbles.DOM.querySelector('.fetch-posts-pool', @el) 82 | Marbles.DOM.on link_element, 'click', (e) => 83 | e.preventDefault() 84 | @emptyPool() 85 | 86 | render: => 87 | context = @context() 88 | super(context) 89 | 90 | if context.posts_count 91 | TentStatus.setPageTitle prefix: "(#{context.posts_count})" 92 | else 93 | TentStatus.setPageTitle prefix: null 94 | 95 | -------------------------------------------------------------------------------- /lib/assets/javascripts/views/search_form.js.coffee: -------------------------------------------------------------------------------- 1 | Marbles.Views.SearchForm = class SearchFormView extends Marbles.View 2 | @view_name: 'search_form' 3 | @template_name: 'search_form' 4 | 5 | constructor: (options = {}) -> 6 | super 7 | @params = options.parent_view.params 8 | 9 | @show_advanced_options = !!@params.entity 10 | 11 | @on 'ready', @loadFormParams 12 | @on 'ready', @focus 13 | 14 | Marbles.DOM.on @el, 'submit', (e) => 15 | e.preventDefault() 16 | @submit() 17 | return false 18 | 19 | @render() 20 | 21 | submit: => 22 | params = Marbles.DOM.serializeForm(@el) 23 | query_string = Marbles.history.serializeParams(params) 24 | 25 | return unless query_string 26 | 27 | TentStatus.Routers.search.navigate("/search#{query_string}", {trigger: true}) 28 | 29 | focus: => 30 | Marbles.DOM.querySelector('[name=q]', @el)?.focus() 31 | 32 | loadFormParams: => 33 | Marbles.DOM.loadFormParams(@el, @params) 34 | 35 | context: => 36 | params: @params 37 | 38 | -------------------------------------------------------------------------------- /lib/assets/javascripts/views/search_form_advanced_options.js.coffee: -------------------------------------------------------------------------------- 1 | Marbles.Views.SearchFormAdvancedOptions = class SearchFormAdvancedOptionsView extends Marbles.View 2 | @view_name: 'search_form_advanced_options' 3 | @template_name: 'search_form_advanced_options' 4 | 5 | constructor: (options = {}) -> 6 | super 7 | 8 | @on 'ready', @loadFormParams 9 | @on 'ready', => 10 | return unless @get('auto_focus') 11 | Marbles.DOM.querySelector('input', @el)?.focus() 12 | 13 | loadFormParams: => 14 | return unless @visible 15 | Marbles.DOM.loadFormParams(@el, @parentView().params) 16 | 17 | context: => 18 | visible: @visible 19 | -------------------------------------------------------------------------------- /lib/assets/javascripts/views/search_form_advanced_options_toggle.js.coffee: -------------------------------------------------------------------------------- 1 | Marbles.Views.SearchFormAdvancedOptionsToggle = class SearchFormAdvancedOptionsToggleView extends Marbles.View 2 | @view_name: 'search_form_advanced_options_toggle' 3 | @template_name: 'search_form_advanced_options_toggle' 4 | 5 | constructor: (options = {}) -> 6 | super 7 | 8 | Marbles.DOM.on @el, 'click', => @toggle() 9 | 10 | @render() 11 | 12 | setImmediate => 13 | if options.parent_view.show_advanced_options 14 | @toggle(auto_focus: false) 15 | options.parent_view.focus() 16 | 17 | advancedOptionsView: => 18 | _.last(@parentView()?.childViews('SearchFormAdvancedOptions')) 19 | 20 | toggle: (options = { auto_focus: true }) => 21 | view = @advancedOptionsView() 22 | return unless view 23 | @visible = !@visible 24 | 25 | if @visible 26 | Marbles.DOM.addClass(@el, 'visible') 27 | else 28 | Marbles.DOM.removeClass(@el, 'visible') 29 | 30 | view.set('visible', @visible) 31 | view.set('auto_focus', options.auto_focus) 32 | view.render() 33 | @parentView()?.focus() if not(@visible) 34 | @render() 35 | 36 | context: => 37 | visible: @visible 38 | -------------------------------------------------------------------------------- /lib/assets/javascripts/views/search_hits.js.coffee: -------------------------------------------------------------------------------- 1 | Marbles.Views.SearchHits = class SearchHitsView extends Marbles.View 2 | @view_name: 'search_hits' 3 | @template_name: 'search_hits' 4 | 5 | constructor: (options = {}) -> 6 | super 7 | 8 | options.parent_view.on 'init:SearchResults', (search_results_view) => 9 | results_collection = search_results_view.postsCollection() 10 | results_collection.once 'fetch:success', @fetchSuccess 11 | results_collection.once 'fetch:error', @fetchError 12 | 13 | fetchSuccess: (collection, res, xhr) => 14 | @render(@context(res)) 15 | 16 | fetchError: (collection, res, xhr) => 17 | @render() 18 | 19 | context: (res = {}) => 20 | total_hits: res.search?.hits 21 | no_results: res.search?.hits == 0 22 | -------------------------------------------------------------------------------- /lib/assets/javascripts/views/search_results.js.coffee: -------------------------------------------------------------------------------- 1 | Marbles.Views.SearchResults = class SearchResultsView extends Marbles.Views.PostsFeed 2 | @view_name: 'search_results' 3 | @last_post_selector: "ul[data-view=SearchResults]>li.post:last-of-type" 4 | 5 | initialize: (options = {}) => 6 | @types = options.types || TentStatus.config.feed_types 7 | 8 | # fire focus event for first post view in feed (caught by author info view) 9 | # TODO: find a better way to do this! 10 | @once 'ready', => 11 | first_post_view = @childViews('Post')?[0] 12 | if first_post_view 13 | first_post_view.constructor.trigger('focus', first_post_view) 14 | 15 | @on 'ready', @initAutoPaginate 16 | 17 | @params = options.parent_view.params 18 | 19 | return unless @params.q 20 | 21 | setImmediate => @fetch(@params) 22 | 23 | postsCollection: => 24 | @_posts_collection ?= new TentStatus.Collections.SearchResults api_root: TentStatus.config.services.search_api_root 25 | 26 | -------------------------------------------------------------------------------- /lib/assets/javascripts/views/signin.js.coffee: -------------------------------------------------------------------------------- 1 | Marbles.Views.Signin = class SigninView extends Marbles.View 2 | @view_name: 'signin' 3 | @template_name: 'signin' 4 | 5 | initialize: (options = {}) => 6 | @redirect_url = options.redirect_url 7 | 8 | @on 'init:SigninForm', @signinFormInit 9 | 10 | @render() 11 | 12 | signinFormInit: (signin_form) => 13 | signin_form.on 'signin:success', @performRedirect 14 | 15 | performRedirect: => 16 | window.location.href = @redirect_url 17 | 18 | -------------------------------------------------------------------------------- /lib/assets/javascripts/views/signin_form.js.coffee: -------------------------------------------------------------------------------- 1 | CSS_VALID_CLASS = "has-success" 2 | CSS_INVALID_CLASS = "has-error" 3 | CSS_HIDDEN_CLASS = "hidden" 4 | CSS_ALERT_ERROR_CLASS = "alert-error" 5 | CSS_ALERT_INFO_CLASS = "alert-info" 6 | 7 | Marbles.Views.SigninForm = class SigninFormView extends Marbles.View 8 | @view_name: 'signin_form' 9 | 10 | initialize: => 11 | @fields = { 12 | username: new Field(Marbles.DOM.querySelector('[name=username]', @el)) 13 | passphrase: new Field(Marbles.DOM.querySelector('[name=passphrase]', @el)) 14 | } 15 | 16 | @alert_el = Marbles.DOM.querySelector('.alert', @el) 17 | 18 | Marbles.DOM.on @el, 'submit', @handleSubmit 19 | 20 | handleSubmit: (e) => 21 | e?.preventDefault() 22 | 23 | for name, field of @fields 24 | field.clearInvalid() 25 | 26 | @showInfo 'Please wait...' 27 | 28 | new Marbles.HTTP( 29 | method: 'POST' 30 | url: TentStatus.config.SIGNIN_URL 31 | body: { 32 | username: @fields.username.getValue() 33 | passphrase: @fields.passphrase.getValue() 34 | } 35 | headers: { 36 | 'Content-Type': 'application/x-www-form-urlencoded' 37 | } 38 | middleware: [ 39 | Marbles.HTTP.Middleware.WithCredentials, 40 | Marbles.HTTP.Middleware.FormEncoded, 41 | Marbles.HTTP.Middleware.SerializeJSON 42 | ] 43 | callback: @submitComplete 44 | ) 45 | 46 | submitComplete: (res, xhr) => 47 | if xhr.status == 200 48 | @handleSuccess() 49 | else 50 | @handleFailure(res, xhr) 51 | 52 | handleSuccess: => 53 | @hideAlert() 54 | 55 | for name, field of @fields 56 | field.markValid() 57 | 58 | @trigger 'signin:success' 59 | 60 | handleFailure: (res) => 61 | @showError(res.error || 'Something went wrong') 62 | 63 | for name in (res.fields || Object.keys(@fields)) 64 | @fields[name]?.markInvalid() 65 | 66 | showError: (msg) => 67 | Marbles.DOM.setInnerText(@alert_el, msg) 68 | 69 | Marbles.DOM.removeClass(@alert_el, CSS_ALERT_INFO_CLASS) 70 | Marbles.DOM.addClass(@alert_el, CSS_ALERT_ERROR_CLASS) 71 | Marbles.DOM.removeClass(@alert_el, CSS_HIDDEN_CLASS) 72 | 73 | showInfo: (msg) => 74 | Marbles.DOM.setInnerText(@alert_el, msg) 75 | 76 | Marbles.DOM.removeClass(@alert_el, CSS_ALERT_ERROR_CLASS) 77 | Marbles.DOM.addClass(@alert_el, CSS_ALERT_INFO_CLASS) 78 | Marbles.DOM.removeClass(@alert_el, CSS_HIDDEN_CLASS) 79 | 80 | hideAlert: => 81 | Marbles.DOM.addClass(@alert_el, CSS_HIDDEN_CLASS) 82 | 83 | class Field 84 | constructor: (@el) -> 85 | @container_el = Marbles.DOM.parentQuerySelector(@el, '.control-group') 86 | @error_msg_el = Marbles.DOM.querySelector('.error-msg', @container_el) 87 | 88 | getValue: => 89 | @el.value 90 | 91 | clearInvalid: => 92 | Marbles.DOM.removeClass(@container_el, CSS_INVALID_CLASS) 93 | 94 | markValid: => 95 | Marbles.DOM.removeClass(@container_el, CSS_INVALID_CLASS) 96 | Marbles.DOM.addClass(@container_el, CSS_VALID_CLASS) 97 | 98 | markInvalid: => 99 | Marbles.DOM.removeClass(@container_el, CSS_VALID_CLASS) 100 | Marbles.DOM.addClass(@container_el, CSS_INVALID_CLASS) 101 | 102 | -------------------------------------------------------------------------------- /lib/assets/javascripts/views/single_post.js.coffee: -------------------------------------------------------------------------------- 1 | Marbles.Views.SinglePost = class SinglePostView extends Marbles.View 2 | @template_name: 'single_post' 3 | @partial_names: ['_post'].concat(Marbles.Views.Post.partial_names) 4 | @view_name: 'single_post' 5 | 6 | constructor: (options = {}) -> 7 | @container = Marbles.Views.container 8 | super 9 | 10 | TentStatus.Models.StatusPost.fetch {entity: options.entity, id: options.id}, 11 | error: => 12 | success: (post) => 13 | @post_cid = post 14 | @render(@context(post)) 15 | 16 | post: => 17 | TentStatus.Models.StatusPost.find(cid: @post_cid) 18 | 19 | context: (post = @post()) => 20 | Marbles.Views.Post::context(post) 21 | -------------------------------------------------------------------------------- /lib/assets/javascripts/views/site_feed.js.coffee: -------------------------------------------------------------------------------- 1 | Marbles.Views.SiteFeed = class SiteFeedView extends Marbles.View 2 | @template_name: 'site_feed' 3 | @view_name: 'site_feed' 4 | 5 | constructor: (options = {}) -> 6 | @container = Marbles.Views.container 7 | super 8 | 9 | @render() 10 | -------------------------------------------------------------------------------- /lib/assets/javascripts/views/subscribers.js.coffee: -------------------------------------------------------------------------------- 1 | Marbles.Views.Subscribers = class SubscribersView extends Marbles.View 2 | @view_name: 'subscribers' 3 | @template_name: 'subscribers' 4 | 5 | constructor: (options = {}) -> 6 | @container = Marbles.Views.container 7 | @entity = options.entity 8 | super 9 | 10 | @render() 11 | 12 | context: => 13 | entity: @entity 14 | 15 | -------------------------------------------------------------------------------- /lib/assets/javascripts/views/subscribers_feed.js.coffee: -------------------------------------------------------------------------------- 1 | Marbles.Views.SubscribersFeed = class SubscribersFeedView extends Marbles.Views.PostsFeed 2 | @template_name: 'relationships_feed' 3 | @partial_names: ['relationship'] 4 | @view_name: 'subscribers_feed' 5 | @last_post_selector: "[data-view=SubscribersFeed] li.post:last-of-type" 6 | 7 | initialize: (options = {}) => 8 | options.types = TentStatus.config.subscriber_feed_types 9 | options.entity = options.parent_view.entity 10 | options.headers = { 11 | 'Cache-Control': 'proxy' 12 | } 13 | options.feed_queries = [ 14 | { types: options.types, profiles: 'mentions', entities: options.entity } 15 | ] 16 | 17 | super(options) 18 | 19 | @on 'ready', => 20 | @ul_el = Marbles.DOM.querySelector('ul', @el) 21 | 22 | getEntity: => 23 | @parentView()?.entity 24 | 25 | shouldAddPostToFeed: (post) => 26 | true 27 | 28 | appendRender: (posts) => 29 | fragment = document.createDocumentFragment() 30 | for post in posts 31 | Marbles.DOM.appendHTML(fragment, @renderSubscriberHTML(post)) 32 | 33 | @bindViews(fragment) 34 | @ul_el.appendChild(fragment) 35 | 36 | prependRender: (posts) => 37 | fragment = document.createDocumentFragment() 38 | for post in posts 39 | Marbles.DOM.appendHTML(fragment, @renderSubscriberHTML(post)) 40 | 41 | @bindViews(fragment) 42 | Marbles.DOM.prependChild(@ul_el, fragment) 43 | 44 | context: (relationships = @postsCollection().models()) => 45 | relationships: relationships 46 | 47 | renderSubscriberHTML: (post) => 48 | @constructor.partials['relationship'].render({ relationship: post }, @constructor.partials) 49 | 50 | -------------------------------------------------------------------------------- /lib/assets/javascripts/views/subscription.js.coffee: -------------------------------------------------------------------------------- 1 | Marbles.Views.Subscription = class SubscriptionView extends Marbles.View 2 | @view_name: 'subscription' 3 | @template_name: 'subscription' 4 | 5 | getEntity: => 6 | @parentView()?.getEntity?() 7 | 8 | -------------------------------------------------------------------------------- /lib/assets/javascripts/views/subscription_toggle.js.coffee: -------------------------------------------------------------------------------- 1 | Marbles.Views.SubscriptionToggle = class SubscriptionToggleView extends Marbles.View 2 | @template_name: 'subscription_toggle' 3 | @view_name: 'subscription_toggle' 4 | 5 | constructor: (options = {}) -> 6 | options.render_method = 'replace' 7 | super(options) 8 | 9 | initialize: (options = {}) -> 10 | @entity = Marbles.DOM.attr(@el, 'data-entity') 11 | @reset() 12 | 13 | reset: => 14 | if @entity == TentStatus.config.meta.content.entity 15 | @subscribed = true 16 | @me = true 17 | @finalize() 18 | return 19 | 20 | @subscription_cids = _.inject(TentStatus.config.subscription_types, ((memo, type) => 21 | _model = TentStatus.Models.Subscription.find( 22 | entity: TentStatus.config.meta.content.entity, 23 | target_entity: @entity, 24 | 'content.type': type 25 | fetch: false 26 | ) 27 | memo.push(_model.cid) if _model 28 | memo 29 | ), []) 30 | 31 | if @subscription_cids.length 32 | @subscribed = true 33 | else 34 | @subscribed = false 35 | 36 | @my_feed = @parentView().getEntity?() == TentStatus.config.meta.content.entity && @parentView().constructor.view_name == 'subscriptions_feed' 37 | 38 | unless @my_feed 39 | params = { 40 | types: TentStatus.config.subscription_feed_types, 41 | mentions: @entity 42 | } 43 | _collection_context = TentStatus.Collections.Posts.generateContext('subscription', params) 44 | collection = TentStatus.Collections.Posts.find(entity: TentStatus.config.meta.content.entity, context: _collection_context) 45 | collection = new TentStatus.Collections.Posts(entity: TentStatus.config.meta.content.entity, context: _collection_context) 46 | collection.options.params = params 47 | 48 | collection.fetch({}, 49 | success: (posts, xhr) => 50 | if posts.length 51 | @subscription_cids = _.map(posts, (post) => post.cid) 52 | @subscribed = true 53 | else 54 | @subscribed = false 55 | 56 | @finalize() 57 | 58 | failure: (res, xhr) => 59 | @subscribed = false 60 | 61 | @finalize() 62 | ) 63 | else 64 | @finalize() 65 | 66 | 67 | finalize: => 68 | @render() 69 | 70 | return if @me 71 | 72 | @on 'ready', @bindEl 73 | @bindEl() 74 | 75 | bindEl: => 76 | Marbles.DOM.removeClass(@el, 'disabled') 77 | Marbles.DOM.on @el, 'click', @toggle 78 | 79 | toggle: => 80 | if @subscribed 81 | return false unless confirm("Unsubscribe from #{@entity}?") 82 | @deleteSubscriptions() 83 | else 84 | @createSubscriptions() 85 | 86 | createSubscriptions: => 87 | Marbles.DOM.addClass(@el, 'disabled') 88 | Marbles.DOM.setInnerText(@el, '...') 89 | 90 | TentStatus.Models.Following.create @entity, 91 | failure: (res, xhr) => 92 | @render() 93 | 94 | success: (following) => 95 | @reset() 96 | 97 | deleteSubscriptions: => 98 | el = @parentView().el 99 | Marbles.DOM.hide(el) if @my_feed 100 | 101 | for cid in @subscription_cids 102 | model = TentStatus.Models.Subscription.find(cid: cid) 103 | continue unless model 104 | model.delete( 105 | success: => 106 | if @my_feed 107 | Marbles.DOM.removeNode(el) 108 | else 109 | @reset() 110 | 111 | failure: => 112 | Marbles.DOM.show(el) 113 | ) 114 | 115 | context: => 116 | subscribed: @subscribed 117 | me: !!@me 118 | 119 | -------------------------------------------------------------------------------- /lib/assets/javascripts/views/subscriptions.js.coffee: -------------------------------------------------------------------------------- 1 | Marbles.Views.Subscriptions = class SubscriptionsView extends Marbles.View 2 | @template_name: 'subscriptions' 3 | @view_name: 'subscriptions' 4 | 5 | constructor: (options = {}) -> 6 | @container = Marbles.Views.container 7 | @entity = options.entity 8 | super 9 | 10 | @render() 11 | 12 | context: => 13 | entity: @entity 14 | 15 | -------------------------------------------------------------------------------- /lib/assets/javascripts/views/subscriptions_feed.js.coffee: -------------------------------------------------------------------------------- 1 | Marbles.Views.SubscriptionsFeed = class SubscriptionsFeedView extends Marbles.Views.PostsFeed 2 | @template_name: 'subscriptions_feed' 3 | @partial_names: ['subscription'] 4 | @view_name: 'subscriptions_feed' 5 | @last_post_selector: "[data-view=SubscriptionsFeed] li.post:last-of-type" 6 | 7 | initialize: (options = {}) => 8 | options.types = TentStatus.config.subscription_feed_types 9 | options.entity = options.parent_view.entity 10 | options.headers = { 11 | 'Cache-Control': 'proxy' 12 | } 13 | options.feed_queries = [ 14 | { types: options.types, profiles: 'mentions' } 15 | ] 16 | 17 | super(options) 18 | 19 | if TentStatus.config.meta.content.entity == options.parent_view.entity 20 | TentStatus.Models.Subscription.on 'create:success', (post, xhr) => 21 | return unless @shouldAddPostToFeed(post) 22 | collection = @postsCollection() 23 | return unless @shouldAddPostTypeToFeed(post.get('type'), collection.postTypes()) 24 | collection.unshift(post) 25 | @render() 26 | 27 | @on 'ready', => 28 | @ul_el = Marbles.DOM.querySelector('ul', @el) 29 | 30 | getEntity: => 31 | @parentView()?.entity 32 | 33 | shouldAddPostToFeed: (post) => 34 | true 35 | 36 | appendRender: (posts) => 37 | fragment = document.createDocumentFragment() 38 | for entity, subscriptions of @groupSubscriptions(posts) 39 | Marbles.DOM.appendHTML(fragment, @renderSubscriptionHTML(entity: entity, subscriptions: subscriptions)) 40 | 41 | @bindViews(fragment) 42 | @ul_el.appendChild(fragment) 43 | 44 | prependRender: (posts) => 45 | fragment = document.createDocumentFragment() 46 | for entity, subscriptions of @groupSubscriptions(posts) 47 | Marbles.DOM.appendHTML(fragment, @renderSubscriptionHTML(entity: entity, subscriptions: subscriptions)) 48 | 49 | @bindViews(fragment) 50 | Marbles.DOM.prependChild(@ul_el, fragment) 51 | 52 | groupSubscriptions: (subscriptions) => 53 | _.inject subscriptions, ((memo, subscription) => 54 | memo[subscription.get('target_entity')] ?= [] 55 | memo[subscription.get('target_entity')].push(subscription) 56 | memo 57 | ), {} 58 | 59 | context: (subscriptions = @postsCollection().models()) => 60 | subscriptions: @groupSubscriptions(subscriptions) 61 | 62 | renderSubscriptionHTML: (context) => 63 | @constructor.partials['subscription'].render(context, @constructor.partials) 64 | 65 | -------------------------------------------------------------------------------- /lib/assets/javascripts/views/unread_count.js.coffee: -------------------------------------------------------------------------------- 1 | Marbles.Views.UnreadCount = class UnreadCountView extends Marbles.View 2 | @view_name: 'unread_count' 3 | 4 | initialize: -> 5 | return unless TentStatus.config.authenticated 6 | 7 | @interval = new TentStatus.FetchInterval fetch_callback: @fetchCount 8 | @cursor_interval = new TentStatus.FetchInterval fetch_callback: @fetchCursor, delay_increment: 30000 # fetch cursor post every 30 seconds 9 | 10 | # find or fetch existing cursor post 11 | TentStatus.Models.CursorPost.find( 12 | { 13 | type: @constructor.cursor_post_type 14 | entity: TentStatus.config.meta.content.entity 15 | }, 16 | 17 | success: @fetchSuccess 18 | failure: @fetchFailure 19 | complete: => 20 | @trigger('fetch:complete') 21 | ) 22 | 23 | fetchCursor: => 24 | return unless cursor_post = TentStatus.Models.CursorPost.find(cid: @post_cid) 25 | 26 | _version_id = cursor_post.get('version.id') 27 | 28 | cursor_post.fetch( 29 | params: { 30 | since: cursor_post.get('received_at') + ' ' + cursor_post.get('version.id') 31 | } 32 | 33 | success: (post) => 34 | @reset() if _version_id != post.get('version.id') 35 | ) 36 | 37 | hide: => 38 | Marbles.DOM.hide(@el, visibility: true) 39 | 40 | show: => 41 | Marbles.DOM.removeClass(@el, 'hidden') 42 | Marbles.DOM.show(@el, visibility: true) 43 | 44 | clearCount: (ref) => 45 | @count = 0 46 | @render() 47 | @frozen = true 48 | 49 | unless @post_cid 50 | return @once 'fetch:complete', => @clearCount(ref) 51 | 52 | post = @getPost() 53 | post.ref_post = ref 54 | post.set('refs', [{ 55 | entity: ref.get('entity') 56 | type: ref.get('type') 57 | post: ref.get('id') 58 | }]) 59 | post.set('permissions', { public: false }) 60 | post.saveVersion( 61 | complete: => 62 | if @fetch_count_pending 63 | @once 'fetch-count:complete', => @frozen = false 64 | else 65 | @frozen = false 66 | ) 67 | 68 | getPost: => 69 | TentStatus.Models.CursorPost.find(cid: @post_cid) 70 | 71 | fetchSuccess: (post) => 72 | @post_cid = post.cid 73 | 74 | @cursor_interval.start() 75 | 76 | @reset() 77 | 78 | fetchFailure: => 79 | post = new TentStatus.Models.CursorPost( 80 | type: @constructor.cursor_post_type 81 | entity: TentStatus.config.meta.content.entity 82 | ) 83 | @post_cid = post.cid 84 | 85 | @reset() 86 | 87 | reset: => 88 | @interval.reset() 89 | 90 | fetchParams: => 91 | params = { 92 | types: @constructor.post_types 93 | } 94 | 95 | post = @getPost() 96 | if _ref = post.get('ref_post') 97 | params.since = "#{_ref.received_at || _ref.published_at} #{_ref.version.id || ''}" 98 | 99 | params 100 | 101 | fetchCount: => 102 | return if @frozen 103 | @fetch_count_pending = true 104 | 105 | callbackFn = (res, xhr) => 106 | if xhr.status == 200 107 | @fetchCountSuccess(res, xhr) 108 | else 109 | @fetchCountFailure(res, xhr) 110 | @fetch_count_pending = false 111 | @trigger('fetch-count:complete') 112 | 113 | params = @fetchParams() 114 | TentStatus.tent_client.post.list( 115 | method: 'HEAD' 116 | params: params 117 | callback: callbackFn 118 | ) 119 | 120 | fetchCountSuccess: (res, xhr) => 121 | return if @frozen 122 | 123 | count = parseInt(xhr.getResponseHeader('Count')) 124 | return @fetchCountFailure(res, xhr) if _.isNaN(count) 125 | return @interval.increaseDelay() if count == @count 126 | 127 | @count = count 128 | @render() 129 | 130 | @interval.reset() 131 | 132 | fetchCountFailure: (res, xhr) => 133 | @interval.increaseDelay() 134 | console.log('fetchCountFailure', res, xhr) 135 | 136 | render: => 137 | if @count > 0 138 | if @count.toString().length > 2 139 | Marbles.DOM.setInnerText(@el, "99+") 140 | else 141 | Marbles.DOM.setInnerText(@el, @count) 142 | @show() 143 | else 144 | @hide() 145 | 146 | -------------------------------------------------------------------------------- /lib/assets/javascripts/views/unread_count/mentions_unread_count.js.coffee: -------------------------------------------------------------------------------- 1 | Marbles.Views.MentionsUnreadCount = class MentionsUnreadCountView extends Marbles.Views.UnreadCount 2 | @view_name: 'mentions_unread_count' 3 | @cursor_post_type: TentStatus.config.POST_TYPES.MENTIONS_CURSOR 4 | @post_types: [TentStatus.config.POST_TYPES.STATUS_REPLY, TentStatus.config.POST_TYPES.STATUS] 5 | 6 | fetchParams: => 7 | params = super 8 | params.mentions = TentStatus.config.meta.content.entity 9 | params 10 | 11 | -------------------------------------------------------------------------------- /lib/assets/javascripts/views/unread_count/reposts_unread_count.js.coffee: -------------------------------------------------------------------------------- 1 | Marbles.Views.RepostsUnreadCount = class RepostsUnreadCountView extends Marbles.Views.UnreadCount 2 | @view_name: 'reposts_unread_count' 3 | @cursor_post_type: TentStatus.config.POST_TYPES.REPOSTS_CURSOR 4 | @post_types: TentStatus.config.repost_types 5 | 6 | fetchParams: => 7 | params = super 8 | params.mentions = TentStatus.config.meta.content.entity 9 | params 10 | 11 | -------------------------------------------------------------------------------- /lib/assets/stylesheets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tent/tent-status/4b8e79350a99baf65ad34e116398a46f59f8acbf/lib/assets/stylesheets/.gitkeep -------------------------------------------------------------------------------- /lib/assets/stylesheets/application.css.scss: -------------------------------------------------------------------------------- 1 | //= require ./permissions 2 | 3 | blockquote { 4 | font-family: "Helvetica Neue", Helvetica, Arial; 5 | font-size: 1.8em; 6 | font-weight: 100; 7 | line-height: 1.1em; 8 | } 9 | 10 | // disabled app nav items 11 | @import 'icing/settings'; 12 | .app-nav-list { 13 | a.disabled { 14 | color: lighten($grayTextColor, 20%); 15 | cursor: default; 16 | 17 | &:hover { 18 | color: lighten($grayTextColor, 20%); 19 | } 20 | } 21 | } 22 | 23 | .mentions-autocomplete { 24 | .markdown-preview { 25 | width: 100%; 26 | min-height: 70px; 27 | display: none; 28 | background: #fff; 29 | } 30 | 31 | .markdown-preview-toggles { 32 | position: absolute; 33 | bottom: -22px; 34 | 35 | .btn-link { 36 | padding: 0px 4px; 37 | } 38 | } 39 | } 40 | 41 | // subscription.js.lodash_template 42 | .subscription { 43 | margin-bottom: 4px; 44 | 45 | .post-profile-name { 46 | line-height: 42px; 47 | } 48 | 49 | .btn { 50 | margin-top: 8.5px; 51 | } 52 | } 53 | 54 | // _404.js.lodash_template 55 | // fetch_posts_pool.js.lodash_template 56 | .text-centered { 57 | text-align: center; 58 | } 59 | 60 | // _new_following_form.js.lodash_template 61 | .new-following-form { 62 | .input { 63 | width: 82%; 64 | } 65 | 66 | .btn-primary { 67 | width: 12%; 68 | } 69 | } 70 | 71 | // follow_button.js.lodash_template 72 | form.follow-button { 73 | display: inline; 74 | } 75 | 76 | // nav.erb 77 | .count-badge-container { 78 | position: relative; 79 | display: inline-block; 80 | 81 | .count-badge { 82 | position: absolute; 83 | top: -12px; 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /lib/tent-status/app.rb: -------------------------------------------------------------------------------- 1 | require 'rack-putty' 2 | require 'omniauth-tent' 3 | 4 | module TentStatus 5 | class App 6 | 7 | require 'tent-status/app/middleware' 8 | require 'tent-status/app/serialize_response' 9 | require 'tent-status/app/asset_server' 10 | require 'tent-status/app/render_view' 11 | require 'tent-status/app/authentication' 12 | 13 | AssetServer.asset_roots = [ 14 | File.expand_path('../../assets', __FILE__), # lib/assets 15 | File.expand_path('../../../vendor/assets', __FILE__) # vendor/assets 16 | ] 17 | 18 | RenderView.view_roots = [ 19 | File.expand_path(File.join(File.dirname(__FILE__), '..', 'views')) # lib/views 20 | ] 21 | 22 | include Rack::Putty::Router 23 | 24 | stack_base SerializeResponse 25 | 26 | class Favicon < Middleware 27 | def action(env) 28 | env['REQUEST_PATH'].sub!(%r{/favicon}, "/assets/favicon") 29 | env['params'][:splat] = 'favicon.ico' 30 | env 31 | end 32 | end 33 | 34 | class CacheControl < Middleware 35 | def action(env) 36 | env['response.headers'] ||= {} 37 | env['response.headers'].merge!( 38 | 'Cache-Control' => @options[:value].to_s, 39 | 'Vary' => 'Cookie' 40 | ) 41 | env 42 | end 43 | end 44 | 45 | class AccessControl < Middleware 46 | def action(env) 47 | env['response.headers'] ||= {} 48 | if @options[:allow_credentials] 49 | env['response.headers']['Access-Control-Allow-Credentials'] = 'true' 50 | end 51 | env['response.headers'].merge!( 52 | 'Access-Control-Allow-Origin' => 'self', 53 | 'Access-Control-Allow-Methods' => 'DELETE, GET, HEAD, PATCH, POST, PUT', 54 | 'Access-Control-Allow-Headers' => 'Cache-Control, Pragma', 55 | 'Access-Control-Max-Age' => '10000' 56 | ) 57 | env 58 | end 59 | end 60 | 61 | class ContentSecurityPolicy < Middleware 62 | def action(env) 63 | env['response.headers'] ||= {} 64 | env['response.headers']["Content-Security-Policy"] = content_security_policy 65 | env 66 | end 67 | 68 | def content_security_policy 69 | [ 70 | "default-src 'self'", 71 | "object-src 'none'", 72 | "img-src *", 73 | "connect-src *" 74 | ].join('; ') 75 | end 76 | end 77 | 78 | get '/assets/*' do |b| 79 | b.use AssetServer 80 | end 81 | 82 | get '/favicon.ico' do |b| 83 | b.use Favicon 84 | b.use AssetServer 85 | end 86 | 87 | unless TentStatus.settings[:skip_authentication] 88 | match %r{\A/auth/tent(/callback)?} do |b| 89 | b.use OmniAuth::Builder do 90 | provider :tent, { 91 | :get_app => AppLookup, 92 | :on_app_created => AppCreate, 93 | :app => { 94 | :name => TentStatus.settings[:name], 95 | :description => TentStatus.settings[:description], 96 | :icon => TentStatus.settings[:icon], 97 | :url => TentStatus.settings[:url], 98 | :redirect_uri => TentStatus.settings[:redirect_uri], 99 | :read_types => TentStatus.settings[:read_types], 100 | :write_types => TentStatus.settings[:write_types], 101 | :scopes => TentStatus.settings[:scopes] 102 | } 103 | } 104 | end 105 | b.use OmniAuthCallback 106 | end 107 | 108 | post '/signout' do |b| 109 | b.use Signout 110 | end 111 | end 112 | 113 | get '/config.json' do |b| 114 | b.use AccessControl, :allow_credentials => true 115 | b.use CacheControl, :value => 'no-cache' 116 | b.use Authentication, :redirect => false 117 | b.use CacheControl, :value => 'private, max-age=600' 118 | b.use RenderView, :view => :'config.json', :content_type => "application/json" 119 | end 120 | 121 | get '*' do |b| 122 | b.use ContentSecurityPolicy 123 | b.use Authentication 124 | b.use RenderView, :view => :application 125 | end 126 | 127 | end 128 | end 129 | -------------------------------------------------------------------------------- /lib/tent-status/app/asset_server.rb: -------------------------------------------------------------------------------- 1 | require 'yaml' 2 | require 'mimetype_fu' 3 | require 'sprockets' 4 | require 'coffee_script' 5 | require 'sass' 6 | require 'lodash-assets' 7 | require 'marbles-js' 8 | require 'marbles-tent-client-js' 9 | require 'icing' 10 | 11 | module TentStatus 12 | class App 13 | class AssetServer < Middleware 14 | 15 | module SprocketsHelpers 16 | AssetNotFoundError = Class.new(StandardError) 17 | def asset_path(source, options = {}) 18 | asset = environment.find_asset(source) 19 | raise AssetNotFoundError.new("#{source.inspect} does not exist within #{environment.paths.inspect}!") unless asset 20 | "#{TentStatus.settings[:asset_root]}/#{asset.digest_path}" 21 | end 22 | end 23 | 24 | DEFAULT_MIME = 'application/octet-stream'.freeze 25 | 26 | class << self 27 | attr_accessor :asset_roots, :logfile 28 | end 29 | 30 | def self.sprockets_environment 31 | @environment ||= begin 32 | environment = Sprockets::Environment.new do |env| 33 | env.logger = Logger.new(@logfile || STDOUT) 34 | env.context_class.class_eval do 35 | include SprocketsHelpers 36 | end 37 | 38 | env.cache = Sprockets::Cache::FileStore.new(TentStatus.settings[:asset_cache_dir]) if TentStatus.settings[:asset_cache_dir] 39 | end 40 | 41 | paths = %w[ javascripts stylesheets images fonts ] 42 | @asset_roots.each do |asset_root| 43 | paths.each do |path| 44 | full_path = File.join(asset_root, path) 45 | next unless File.exists?(full_path) 46 | environment.append_path(full_path) 47 | end 48 | end 49 | 50 | MarblesJS::Sprockets.setup(environment) 51 | MarblesTentClientJS::Sprockets.setup(environment) 52 | Icing::Sprockets.setup(environment) 53 | 54 | Sprockets::Sass.options[:load_paths] = environment.paths 55 | 56 | environment 57 | end 58 | end 59 | 60 | def initialize(app, options = {}) 61 | super 62 | 63 | @public_dir = @options[:public_dir] || TentStatus.settings[:public_dir] 64 | end 65 | 66 | def action(env) 67 | asset_name = env['params'][:splat] 68 | compiled_path = File.join(@public_dir, asset_name) 69 | 70 | if File.exists?(compiled_path) 71 | [200, { 'Content-Type' => asset_mime_type(asset_name) }, [File.read(compiled_path)]] 72 | else 73 | new_env = env.clone 74 | new_env["PATH_INFO"] = env["REQUEST_PATH"].sub(%r{\A/assets}, '') 75 | sprockets_environment.call(new_env) 76 | end 77 | end 78 | 79 | private 80 | 81 | def sprockets_environment 82 | @sprockets_environment ||= self.class.sprockets_environment 83 | end 84 | 85 | def asset_mime_type(asset_name) 86 | mime = File.mime_type?(asset_name) 87 | mime == 'unknown/unknown' ? DEFAULT_MIME : mime 88 | end 89 | 90 | end 91 | end 92 | end 93 | -------------------------------------------------------------------------------- /lib/tent-status/app/authentication.rb: -------------------------------------------------------------------------------- 1 | module TentStatus 2 | class App 3 | class Authentication < Middleware 4 | def action(env) 5 | return env if TentStatus.settings[:skip_authentication] 6 | 7 | if current_user(env) && current_user(env).app_exists? 8 | env 9 | else 10 | if @options[:redirect] == false 11 | [404, env['response.headers'] || {}, []] 12 | else 13 | redirect('/auth/tent', env) 14 | end 15 | end 16 | end 17 | end 18 | 19 | class Signout < Middleware 20 | def action(env) 21 | env['rack.session'].delete('current_user_id') 22 | env.delete('current_user') 23 | 24 | [200, {}, []] 25 | end 26 | end 27 | 28 | module AppLookup 29 | extend self 30 | 31 | def call(entity) 32 | user = Model::User.lookup(entity) 33 | user.app if user 34 | end 35 | end 36 | 37 | module AppCreate 38 | extend self 39 | 40 | def call(app, entity) 41 | Model::User.create(entity, app.to_hash) 42 | end 43 | end 44 | 45 | class OmniAuthCallback < Middleware 46 | def action(env) 47 | return env unless callback_phase?(env) 48 | 49 | if user = Model::User.lookup(env['omniauth.auth']['uid']) 50 | env['rack.session']['current_user_id'] = user.id 51 | env['current_user'] = user 52 | 53 | user.update_authorization(env['omniauth.auth'].extra.credentials) 54 | 55 | redirect('/', env) 56 | else 57 | # something went wrong 58 | redirect('/auth/tent', env) 59 | end 60 | end 61 | 62 | private 63 | 64 | def callback_phase?(env) 65 | env['params'][:captures].include?("/callback") 66 | end 67 | end 68 | end 69 | end 70 | -------------------------------------------------------------------------------- /lib/tent-status/app/middleware.rb: -------------------------------------------------------------------------------- 1 | module TentStatus 2 | class App 3 | class Middleware < Rack::Putty::Middleware 4 | 5 | class Halt < StandardError 6 | attr_accessor :code, :message, :headers, :body 7 | def initialize(code, message=nil, headers = {}) 8 | super(message) 9 | @code, @message = code, message 10 | @headers = { 'Content-Type' => 'text/plain' }.merge(headers) 11 | @body = message.to_s 12 | end 13 | 14 | def to_response 15 | [code, headers, [body]] 16 | end 17 | end 18 | 19 | def call(env) 20 | super 21 | rescue Halt => e 22 | e.to_response 23 | end 24 | 25 | def current_user(env) 26 | return unless id = env['rack.session']['current_user_id'] 27 | env['current_user'] ||= Model::User.first(:id => id) 28 | end 29 | 30 | def halt!(code, message) 31 | raise Halt.new(code, message) 32 | end 33 | 34 | def redirect(location, env = {}) 35 | [302, { 'Location' => location }.merge(env['response.headers'] || {}), []] 36 | end 37 | 38 | def redirect!(location, env = {}) 39 | halt = Halt.new(302, nil, { 40 | 'Location' => location.to_s 41 | }.merge(env['response.headers'] || {})) 42 | raise halt 43 | end 44 | 45 | end 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /lib/tent-status/app/render_view.rb: -------------------------------------------------------------------------------- 1 | require 'erb' 2 | 3 | module TentStatus 4 | class App 5 | class RenderView < Middleware 6 | 7 | class TemplateContext 8 | AssetNotFoundError = AssetServer::SprocketsHelpers::AssetNotFoundError 9 | 10 | attr_reader :env 11 | def initialize(env, renderer, &block) 12 | @env, @renderer, @block = env, renderer, block 13 | end 14 | 15 | def erb(view_name) 16 | @renderer.erb(view_name, binding) 17 | end 18 | 19 | def block_given? 20 | !@block.nil? && @block.respond_to?(:call) 21 | end 22 | 23 | def yield 24 | @block.call(self) 25 | end 26 | 27 | def current_user 28 | return unless (env['rack.session'] || {})['current_user_id'] 29 | env['current_user'] ||= Model::User.first(:id => env['rack.session']['current_user_id']) 30 | end 31 | 32 | def sprockets_environment 33 | AssetServer.sprockets_environment 34 | end 35 | 36 | def asset_manifest_path(asset_name) 37 | manifests = TentStatus.settings[:asset_manifests].to_a.select { |m| Hash === m && Hash === m['files'] } 38 | return if manifests.empty? 39 | 40 | compiled_name = manifests.inject(nil) do |memo, manifest| 41 | memo = manifest['files'].find { |k,v| 42 | v['logical_path'] == asset_name 43 | }.to_a[0] 44 | 45 | break memo if memo 46 | end 47 | 48 | return unless compiled_name 49 | 50 | full_asset_path(compiled_name) 51 | end 52 | 53 | def asset_path(name) 54 | path = asset_manifest_path(name) 55 | return path if path 56 | 57 | asset = sprockets_environment.find_asset(name) 58 | raise AssetNotFoundError.new("#{name.inspect} does not exist within #{sprockets_environment.paths.inspect}!") unless asset 59 | full_asset_path(asset.digest_path) 60 | end 61 | 62 | def path_prefix 63 | TentStatus.settings[:path_prefix].to_s 64 | end 65 | 66 | def asset_root 67 | TentStatus.settings[:asset_root].to_s 68 | end 69 | 70 | def full_path(path) 71 | "#{path_prefix}/#{path}".gsub(%r{/+}, '/') 72 | end 73 | 74 | def full_asset_path(path) 75 | "#{asset_root}" + "/#{path}".gsub(%r{/+}, '/') 76 | end 77 | end 78 | 79 | class << self 80 | attr_accessor :view_roots 81 | end 82 | 83 | def action(env) 84 | env['response.view'] ||= @options[:view].to_s if @options[:view] 85 | return env unless env['response.view'] 86 | 87 | status = env['response.status'] || 200 88 | headers = { 'Content-Type' => (@options[:content_type] || 'text/html') }.merge(env['response.headers'] || Hash.new) 89 | body = render(env) 90 | 91 | unless body 92 | status = 404 93 | body = "View not found: #{env['response.view'].inspect}" 94 | end 95 | 96 | [status, headers, [body]] 97 | end 98 | 99 | def erb(view_name, binding, &block) 100 | view_paths = Array(self.class.view_roots).map { |view_root| File.join(view_root, "#{view_name}.erb") } 101 | view_paths.concat Array(self.class.view_roots).map { |view_root| File.join(view_root, "#{view_name}") } 102 | return unless view_path = view_paths.find { |path| File.exists?(path) } 103 | 104 | template = ERB.new(File.read(view_path)) 105 | template.result(binding) 106 | end 107 | 108 | private 109 | 110 | def render(env) 111 | if env['response.layout'] 112 | layout = env['response.layout'] 113 | view = env['response.view'] 114 | block = proc { |binding| erb(view, template_binding(env)) } 115 | erb(layout, template_binding(env, &block)) 116 | else 117 | erb(env['response.view'], template_binding(env)) 118 | end 119 | end 120 | 121 | def template_binding(env, &block) 122 | TemplateContext.new(env, self, &block).instance_eval { binding } 123 | end 124 | 125 | end 126 | end 127 | end 128 | -------------------------------------------------------------------------------- /lib/tent-status/app/serialize_response.rb: -------------------------------------------------------------------------------- 1 | module TentStatus 2 | class App 3 | module SerializeResponse 4 | extend self 5 | 6 | def call(env) 7 | [404, { 'Content-Type' => 'text/plain' }, ['Not Found']] 8 | end 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /lib/tent-status/model.rb: -------------------------------------------------------------------------------- 1 | require 'sequel' 2 | 3 | module TentStatus 4 | module Model 5 | class << self 6 | attr_accessor :db 7 | end 8 | 9 | def self.new(options = {}) 10 | self.db ||= Sequel.connect( 11 | options[:database_url] || TentStatus.settings[:database_url], 12 | :logger => Logger.new(options[:database_logfile] || TentStatus.settings[:database_logfile]) 13 | ) 14 | 15 | require 'tent-status/model/user' 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /lib/tent-status/model/user.rb: -------------------------------------------------------------------------------- 1 | require 'sequel-json' 2 | require 'tent-client' 3 | 4 | module TentStatus 5 | module Model 6 | 7 | unless Model.db.table_exists?(:users) 8 | Model.db.create_table(:users) do 9 | primary_key :id 10 | column :entity, 'text', :null => false 11 | column :app, 'text', :null => false 12 | column :auth, 'text' 13 | end 14 | end 15 | 16 | class User < Sequel::Model(Model.db[:users]) 17 | plugin :serialization 18 | serialize_attributes :json, :app, :auth 19 | 20 | def self.lookup(entity) 21 | first(:entity => entity) 22 | end 23 | 24 | def self.create(entity, app) 25 | if user = first(:entity => entity) 26 | user.update(:app => app) 27 | else 28 | user = super(:entity => entity, :app => app) 29 | end 30 | user 31 | end 32 | 33 | def update_authorization(credentials) 34 | self.update(:auth => { 35 | :id => credentials[:id], 36 | :hawk_key => credentials[:hawk_key], 37 | :hawk_algorithm => credentials[:hawk_algorithm] 38 | }) 39 | self.auth 40 | end 41 | 42 | def app_client 43 | @app_client ||= ::TentClient.new(entity, :credentials => Utils::Hash.symbolize_keys(app['credentials'].merge(:id => app['credentials']['hawk_id']))) 44 | end 45 | 46 | def client 47 | @client ||= ::TentClient.new(entity, :credentials => Utils::Hash.symbolize_keys(auth)) 48 | end 49 | 50 | def app_exists? 51 | res = app_client.post.get(app['entity'], app['id']) 52 | res.success? 53 | end 54 | 55 | def server_meta_post 56 | @server_meta_post ||= begin 57 | post = client.server_meta_post 58 | if post && post['content']['entity'] != entity 59 | self.update(:entity => post['content']['entity']) 60 | end 61 | post 62 | end 63 | end 64 | 65 | def json_config 66 | { 67 | :credentials => auth, 68 | :meta => server_meta_post 69 | } 70 | end 71 | end 72 | 73 | end 74 | end 75 | -------------------------------------------------------------------------------- /lib/tent-status/tasks/assets.rb: -------------------------------------------------------------------------------- 1 | require 'tent-status/compiler' 2 | 3 | def configure_tent_status 4 | return if @tent_status_configured 5 | @tent_status_configured = true 6 | TentStatus.configure 7 | end 8 | 9 | namespace :icing do 10 | task :configure do 11 | configure_tent_status 12 | TentStatus::Compiler.compile_icing = true 13 | end 14 | end 15 | 16 | namespace :marbles do 17 | task :configure do 18 | configure_tent_status 19 | TentStatus::Compiler.compile_marbles = true 20 | end 21 | end 22 | 23 | namespace :assets do 24 | task :configure do 25 | configure_tent_status 26 | end 27 | 28 | task :compile => :configure do 29 | TentStatus::Compiler.compile_assets 30 | end 31 | 32 | task :gzip => :configure do 33 | TentStatus::Compiler.gzip_assets 34 | end 35 | 36 | task :deploy => :gzip do 37 | if ENV['S3_ASSETS'] == 'true' && ENV['S3_BUCKET'] && ENV['AWS_ACCESS_KEY_ID'] && ENV['AWS_SECRET_ACCESS_KEY'] 38 | require File.expand_path(File.join(File.dirname(__FILE__), '..', '..', '..', 'config', 'asset_sync')) 39 | AssetSync.sync 40 | end 41 | end 42 | 43 | # deploy assets when deploying to heroku 44 | task :precompile => ['icing:configure', 'marbles:configure', :deploy] 45 | end 46 | -------------------------------------------------------------------------------- /lib/tent-status/tasks/layout.rb: -------------------------------------------------------------------------------- 1 | require 'tent-status/compiler' 2 | 3 | namespace :layout do 4 | task :compile do 5 | TentStatus::Compiler.compile_layout 6 | end 7 | 8 | task :gzip do 9 | TentStatus::Compiler.gzip_layout 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /lib/tent-status/utils.rb: -------------------------------------------------------------------------------- 1 | module TentStatus 2 | module Utils 3 | extend self 4 | 5 | module Hash 6 | extend self 7 | 8 | def dup(hash) 9 | transform_keys(hash, nil) { |k| k }.first 10 | end 11 | 12 | def slice(hash, *keys) 13 | keys.each_with_object(hash.class.new) { |k, new_hash| 14 | new_hash[k] = hash[k] if hash.has_key?(k) 15 | } 16 | end 17 | 18 | def slice!(hash, *keys) 19 | hash.replace(slice(hash, *keys)) 20 | end 21 | 22 | def stringify_keys(hash) 23 | transform_keys(hash, :to_s).first 24 | end 25 | 26 | def stringify_keys!(hash) 27 | hash.replace(stringify_keys(hash)) 28 | end 29 | 30 | def symbolize_keys(hash) 31 | transform_keys(hash, :to_sym).first 32 | end 33 | 34 | def symbolize_keys!(hash) 35 | hash.replace(symbolize_keys(hash)) 36 | end 37 | 38 | def transform_keys(*items, method, &block) 39 | items.map do |item| 40 | case item 41 | when ::Hash 42 | item.inject(::Hash.new) do |new_hash, (k,v)| 43 | new_key = method ? k.send(method) : block.call(k) 44 | new_hash[new_key] = transform_keys(v, method, &block).first 45 | new_hash 46 | end 47 | when ::Array 48 | item.map { |i| transform_keys(i, method, &block).first } 49 | else 50 | item 51 | end 52 | end 53 | end 54 | end 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /lib/tent-status/version.rb: -------------------------------------------------------------------------------- 1 | module TentStatus 2 | VERSION = '0.2.0' 3 | end 4 | -------------------------------------------------------------------------------- /lib/views/application.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | " /> 11 | " /> 12 | " /> 13 | " /> 14 | 15 | " /> 16 | " /> 17 | 18 | <%= TentStatus.settings[:name] %> 19 | 20 | 21 | 22 |
    23 | <%= erb :global_nav %> 24 |
    25 | 26 | <% if TentStatus.settings[:render_app_nav] %> 27 | 31 | <% end %> 32 | 33 |
    34 | <% unless block_given? %> 35 |
     
    36 |
    37 |
    38 | <% end %> 39 | 40 |
    41 | <% if block_given? %> 42 | <%= yield %> 43 | <% end %> 44 |
    45 |
    46 | 47 | <% unless block_given? %> 48 | 49 | 50 | 51 | <% end %> 52 | 53 | 54 | -------------------------------------------------------------------------------- /lib/views/config.json: -------------------------------------------------------------------------------- 1 | <%= Yajl::Encoder.encode(current_user ? current_user.json_config : {}) %> 2 | -------------------------------------------------------------------------------- /lib/views/global_nav.erb: -------------------------------------------------------------------------------- 1 | 14 | -------------------------------------------------------------------------------- /lib/views/nav.erb: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /lib/views/oauth_confirm.erb: -------------------------------------------------------------------------------- 1 |
    2 |

    Authorize App: <%= env['oauth.app'][:content][:name] %>

    3 | 4 |

    Requested Post Type Access

    5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | <% read_types = env['oauth.app'][:content][:types][:read].sort %> 15 | <% write_types = env['oauth.app'][:content][:types][:write].sort %> 16 | <% [read_types.size, write_types.size].max.times do |index| %> 17 | 18 | 25 | 26 | 33 | 34 | <% end %> 35 | 36 |
    ReadWrite
    19 | <% unless index >= read_types.size %> 20 | 23 | <% end %> 24 | 27 | <% unless index >= write_types.size %> 28 | 31 | <% end %> 32 |
    37 | 38 | <% if env['oauth.app'][:content][:scopes].to_a.any? %> 39 |

    Requested Scopes

    40 | 41 | 42 | 43 | <% env['oauth.app'][:content][:scopes].each do |scope| %> 44 | 45 | 50 | 51 | <% end %> 52 | 53 |
    46 | 49 |
    54 | <% end %> 55 | 56 |
    57 | 58 | 59 |
    60 |
    61 | -------------------------------------------------------------------------------- /lib/views/search_nav_links.erb: -------------------------------------------------------------------------------- 1 | 2 |
  • 3 | Global 4 |
  • 5 |
    6 | -------------------------------------------------------------------------------- /lib/views/status_nav_links.erb: -------------------------------------------------------------------------------- 1 | 2 |
  • 3 | Timeline 4 |
  • 5 |
    6 | 7 | 8 |
  • 9 | Mentions 10 |
  • 11 |
    12 | 13 | 14 |
  • 15 | Reposts 16 |
  • 17 |
    18 | 19 | 20 |
  • 21 | Profile 22 |
  • 23 |
    24 | 25 | <% if TentStatus.settings[:site_feed_api_root] %> 26 | 27 |
  • 28 | Site Feed 29 |
  • 30 |
    31 | <% end %> 32 | -------------------------------------------------------------------------------- /tent-status.gemspec: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | 3 | lib = File.expand_path('../lib', __FILE__) 4 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 5 | require 'tent-status/version' 6 | 7 | Gem::Specification.new do |gem| 8 | gem.name = "tent-status" 9 | gem.version = TentStatus::VERSION 10 | gem.authors = ["Jesse Stuart"] 11 | gem.email = ["jesse@jessestuart.ca"] 12 | gem.description = %(Tent app for 512 character posts. See README for details.) 13 | gem.summary = %(Tent app for 512 character posts) 14 | gem.homepage = "" 15 | 16 | gem.files = `git ls-files`.split($/) 17 | gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) } 18 | gem.test_files = gem.files.grep(%r{^(test|spec|features)/}) 19 | gem.require_paths = ["lib"] 20 | 21 | 22 | gem.add_runtime_dependency 'rack-putty' 23 | gem.add_runtime_dependency 'tent-client' 24 | gem.add_runtime_dependency 'omniauth-tent' 25 | 26 | gem.add_runtime_dependency 'mimetype-fu' 27 | gem.add_runtime_dependency 'sprockets' , '~> 2.0' 28 | gem.add_runtime_dependency 'sprockets-sass' , '~> 0.5' 29 | gem.add_runtime_dependency 'coffee-script' 30 | gem.add_runtime_dependency 'marbles-js' 31 | gem.add_runtime_dependency 'marbles-tent-client-js' 32 | gem.add_runtime_dependency 'lodash-assets' 33 | gem.add_runtime_dependency 'icing' 34 | 35 | gem.add_runtime_dependency 'pg' 36 | gem.add_runtime_dependency 'sequel', '3.46' 37 | gem.add_runtime_dependency 'sequel-json' 38 | 39 | gem.add_development_dependency 'rake' 40 | gem.add_development_dependency 'asset_sync' 41 | gem.add_development_dependency 'mime-types' 42 | end 43 | --------------------------------------------------------------------------------