├── .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 |
8 |
--------------------------------------------------------------------------------
/lib/assets/javascripts/templates/_new_post_form.js.lodash_template:
--------------------------------------------------------------------------------
1 | <% if (TentStatus.config.authenticated) { %>
2 |
3 |
4 |
5 |
'>
6 |
7 |
8 |
9 |
10 |
11 | now
12 |
13 | ' class='post-profile-name'>
14 | <%- formatted.entity %>
15 |
16 |
17 |
18 |
28 |
29 |
30 |
31 |
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 |
6 |
7 |
8 |
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 |
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 |
5 | <% if (posts) { %><%= partials['_post'].render(context) %><% } %>
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/lib/assets/javascripts/templates/conversation_parent_posts.js.lodash_template:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
<% if (parent_posts) { %><%= partials['_post'].render(context) %><% } %>
6 |
7 |
8 |
--------------------------------------------------------------------------------
/lib/assets/javascripts/templates/edit_post.js.lodash_template:
--------------------------------------------------------------------------------
1 |
2 |
3 |
'>
4 |
5 |
6 |
7 |
8 |
9 | now
10 |
11 | ' class='post-profile-name'>
12 | <%- formatted.entity %>
13 |
14 |
15 |
16 |
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 |
6 | <% } %>
7 | <% } %>
8 |
9 |
12 |
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 |
4 |
5 |
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 |
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 |
49 | <% } %>
50 |
51 |
54 |
55 |
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 |
8 |
9 |
10 |
11 |
12 | ...
13 |
14 |
15 |
16 |
17 | <% } %>
18 |
--------------------------------------------------------------------------------
/lib/assets/javascripts/templates/relationships_feed.js.lodash_template:
--------------------------------------------------------------------------------
1 |
2 | <% _.forEach(relationships, function(relationship) { %>
3 | <%= partials['relationship'].render({ relationship: relationship }) %>
4 | <% }) %>
5 |
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 |
4 |
5 |
8 |
--------------------------------------------------------------------------------
/lib/assets/javascripts/templates/search.js.lodash_template:
--------------------------------------------------------------------------------
1 |
4 |
5 |
10 |
--------------------------------------------------------------------------------
/lib/assets/javascripts/templates/search_form.js.lodash_template:
--------------------------------------------------------------------------------
1 |
8 |
--------------------------------------------------------------------------------
/lib/assets/javascripts/templates/search_form_advanced_options.js.lodash_template:
--------------------------------------------------------------------------------
1 | <% if (visible) { %>
2 |
3 |
4 | Author
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 |
24 |
--------------------------------------------------------------------------------
/lib/assets/javascripts/templates/single_post.js.lodash_template:
--------------------------------------------------------------------------------
1 |
2 |
3 | <%= partials['_post'].render(context) %>
4 |
5 |
6 |
--------------------------------------------------------------------------------
/lib/assets/javascripts/templates/site_feed.js.lodash_template:
--------------------------------------------------------------------------------
1 | <% if (TentStatus.config.authenticated) { %>
2 |
5 | <% } %>
6 |
7 |
10 |
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 |
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 |
2 | <% _.forEach(subscriptions, function(subscriptions, entity) { %>
3 | <%= partials['subscription'].render({ entity: entity, subscriptions: subscriptions }) %>
4 | <% }) %>
5 |
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 |
28 |
29 | <%= erb :nav %>
30 |
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 |
2 | <% TentStatus.settings[:global_nav_config]['items'].to_a.each do |item| %>
3 | class='nav-selected'<% end %> <% if item['url'] === TentStatus.settings[:url] %>data-match-url='/*'<% end %> <% if item['url'].match(/\/search\Z/) %>data-match-url='/search'<% end %>>
4 | ' title='<%= item['name'] %>'>'>
5 |
6 | <% end %>
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/lib/views/nav.erb:
--------------------------------------------------------------------------------
1 |
2 | <%= erb :"#{TentStatus.settings[:app_nav_key]}_nav_links" %>
3 |
4 |
--------------------------------------------------------------------------------
/lib/views/oauth_confirm.erb:
--------------------------------------------------------------------------------
1 |
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 |
--------------------------------------------------------------------------------