Merge branch 'develop' into feature/convo.algo
This commit is contained in:
commit
72fd2717b6
104 changed files with 2007 additions and 979 deletions
|
@ -6,7 +6,7 @@ export DB_USERNAME='postgres'
|
|||
export DB_PASSWORD='3112'
|
||||
export DB_HOST='localhost'
|
||||
export DB_PORT='5432'
|
||||
export DB_NAME='metamap002'
|
||||
export DB_NAME='metamaps'
|
||||
|
||||
export REALTIME_SERVER='http://localhost:5000'
|
||||
export MAILER_DEFAULT_URL='localhost:3000'
|
||||
|
|
8
.github/ISSUE_TEMPLATE.md
vendored
8
.github/ISSUE_TEMPLATE.md
vendored
|
@ -1,4 +1,12 @@
|
|||
please link to related trello cards, if they exist, from the following two boards respectively
|
||||
|
||||
https://trello.com/b/8HlCikOX/metamaps-design
|
||||
|
||||
https://trello.com/b/uFOA6a2x/metamaps-feedback-feature-ideas-requests
|
||||
|
||||
[the issue as framed for design]()
|
||||
|
||||
[the issue as framed from the users perspective]()
|
||||
|
||||
|
||||
============
|
||||
|
|
|
@ -19,3 +19,6 @@ Metrics/AbcSize:
|
|||
|
||||
Style/Documentation:
|
||||
Enabled: false
|
||||
|
||||
Style/EmptyMethod:
|
||||
EnforcedStyle: expanded
|
||||
|
|
8
Gemfile
8
Gemfile
|
@ -9,7 +9,6 @@ gem 'aws-sdk'
|
|||
gem 'best_in_place'
|
||||
gem 'delayed_job'
|
||||
gem 'delayed_job_active_record'
|
||||
gem 'sucker_punch'
|
||||
gem 'devise'
|
||||
gem 'doorkeeper'
|
||||
gem 'dotenv-rails'
|
||||
|
@ -20,6 +19,7 @@ gem 'kaminari'
|
|||
gem 'mailboxer'
|
||||
gem 'paperclip'
|
||||
gem 'pg'
|
||||
gem 'puma'
|
||||
gem 'pundit'
|
||||
gem 'pundit_extra'
|
||||
gem 'rack-attack'
|
||||
|
@ -27,7 +27,7 @@ gem 'rack-cors'
|
|||
gem 'redis'
|
||||
gem 'slack-notifier'
|
||||
gem 'snorlax'
|
||||
gem 'puma'
|
||||
gem 'sucker_punch'
|
||||
|
||||
# asset stuff
|
||||
gem 'jquery-rails'
|
||||
|
@ -36,12 +36,12 @@ gem 'sass-rails'
|
|||
gem 'uglifier'
|
||||
|
||||
group :test do
|
||||
gem 'brakeman', require: false
|
||||
gem 'factory_girl_rails'
|
||||
gem 'json-schema'
|
||||
gem 'rspec-rails'
|
||||
gem 'shoulda-matchers'
|
||||
gem 'simplecov', require: false
|
||||
gem 'brakeman', require: false
|
||||
end
|
||||
|
||||
group :development, :test do
|
||||
|
@ -49,6 +49,6 @@ group :development, :test do
|
|||
gem 'binding_of_caller'
|
||||
gem 'pry-byebug'
|
||||
gem 'pry-rails'
|
||||
gem 'tunemygc'
|
||||
gem 'rubocop'
|
||||
gem 'tunemygc'
|
||||
end
|
||||
|
|
201
Gemfile.lock
201
Gemfile.lock
|
@ -1,57 +1,60 @@
|
|||
GEM
|
||||
remote: https://rubygems.org/
|
||||
specs:
|
||||
actioncable (5.0.0.1)
|
||||
actionpack (= 5.0.0.1)
|
||||
actioncable (5.0.1)
|
||||
actionpack (= 5.0.1)
|
||||
nio4r (~> 1.2)
|
||||
websocket-driver (~> 0.6.1)
|
||||
actionmailer (5.0.0.1)
|
||||
actionpack (= 5.0.0.1)
|
||||
actionview (= 5.0.0.1)
|
||||
activejob (= 5.0.0.1)
|
||||
actionmailer (5.0.1)
|
||||
actionpack (= 5.0.1)
|
||||
actionview (= 5.0.1)
|
||||
activejob (= 5.0.1)
|
||||
mail (~> 2.5, >= 2.5.4)
|
||||
rails-dom-testing (~> 2.0)
|
||||
actionpack (5.0.0.1)
|
||||
actionview (= 5.0.0.1)
|
||||
activesupport (= 5.0.0.1)
|
||||
actionpack (5.0.1)
|
||||
actionview (= 5.0.1)
|
||||
activesupport (= 5.0.1)
|
||||
rack (~> 2.0)
|
||||
rack-test (~> 0.6.3)
|
||||
rails-dom-testing (~> 2.0)
|
||||
rails-html-sanitizer (~> 1.0, >= 1.0.2)
|
||||
actionview (5.0.0.1)
|
||||
activesupport (= 5.0.0.1)
|
||||
actionview (5.0.1)
|
||||
activesupport (= 5.0.1)
|
||||
builder (~> 3.1)
|
||||
erubis (~> 2.7.0)
|
||||
rails-dom-testing (~> 2.0)
|
||||
rails-html-sanitizer (~> 1.0, >= 1.0.2)
|
||||
active_model_serializers (0.10.2)
|
||||
active_model_serializers (0.10.4)
|
||||
actionpack (>= 4.1, < 6)
|
||||
activemodel (>= 4.1, < 6)
|
||||
jsonapi (~> 0.1.1.beta2)
|
||||
railties (>= 4.1, < 6)
|
||||
activejob (5.0.0.1)
|
||||
activesupport (= 5.0.0.1)
|
||||
case_transform (>= 0.2)
|
||||
jsonapi (= 0.1.1.beta6)
|
||||
activejob (5.0.1)
|
||||
activesupport (= 5.0.1)
|
||||
globalid (>= 0.3.6)
|
||||
activemodel (5.0.0.1)
|
||||
activesupport (= 5.0.0.1)
|
||||
activerecord (5.0.0.1)
|
||||
activemodel (= 5.0.0.1)
|
||||
activesupport (= 5.0.0.1)
|
||||
activemodel (5.0.1)
|
||||
activesupport (= 5.0.1)
|
||||
activerecord (5.0.1)
|
||||
activemodel (= 5.0.1)
|
||||
activesupport (= 5.0.1)
|
||||
arel (~> 7.0)
|
||||
activesupport (5.0.0.1)
|
||||
activesupport (5.0.1)
|
||||
concurrent-ruby (~> 1.0, >= 1.0.2)
|
||||
i18n (~> 0.7)
|
||||
minitest (~> 5.1)
|
||||
tzinfo (~> 1.1)
|
||||
addressable (2.3.8)
|
||||
arel (7.1.2)
|
||||
addressable (2.5.0)
|
||||
public_suffix (~> 2.0, >= 2.0.2)
|
||||
arel (7.1.4)
|
||||
ast (2.3.0)
|
||||
aws-sdk (2.6.3)
|
||||
aws-sdk-resources (= 2.6.3)
|
||||
aws-sdk-core (2.6.3)
|
||||
aws-sdk (2.7.0)
|
||||
aws-sdk-resources (= 2.7.0)
|
||||
aws-sdk-core (2.7.0)
|
||||
aws-sigv4 (~> 1.0)
|
||||
jmespath (~> 1.0)
|
||||
aws-sdk-resources (2.6.3)
|
||||
aws-sdk-core (= 2.6.3)
|
||||
aws-sdk-resources (2.7.0)
|
||||
aws-sdk-core (= 2.7.0)
|
||||
aws-sigv4 (1.0.0)
|
||||
bcrypt (3.1.11)
|
||||
best_in_place (3.1.0)
|
||||
actionpack (>= 3.2)
|
||||
|
@ -62,21 +65,20 @@ GEM
|
|||
rack (>= 0.9.0)
|
||||
binding_of_caller (0.7.2)
|
||||
debug_inspector (>= 0.0.1)
|
||||
brakeman (3.4.0)
|
||||
builder (3.2.2)
|
||||
byebug (9.0.5)
|
||||
carrierwave (0.11.2)
|
||||
activemodel (>= 3.2.0)
|
||||
activesupport (>= 3.2.0)
|
||||
json (>= 1.7)
|
||||
brakeman (3.4.1)
|
||||
builder (3.2.3)
|
||||
byebug (9.0.6)
|
||||
carrierwave (1.0.0)
|
||||
activemodel (>= 4.0.0)
|
||||
activesupport (>= 4.0.0)
|
||||
mime-types (>= 1.16)
|
||||
mimemagic (>= 0.3.0)
|
||||
climate_control (0.0.3)
|
||||
activesupport (>= 3.0)
|
||||
case_transform (0.2)
|
||||
activesupport
|
||||
climate_control (0.1.0)
|
||||
cocaine (0.5.8)
|
||||
climate_control (>= 0.0.3, < 1.0)
|
||||
coderay (1.1.1)
|
||||
concurrent-ruby (1.0.2)
|
||||
concurrent-ruby (1.0.4)
|
||||
debug_inspector (0.0.2)
|
||||
delayed_job (4.1.2)
|
||||
activesupport (>= 3.0, < 5.1)
|
||||
|
@ -89,23 +91,23 @@ GEM
|
|||
railties (>= 4.1.0, < 5.1)
|
||||
responders
|
||||
warden (~> 1.2.3)
|
||||
diff-lcs (1.2.5)
|
||||
diff-lcs (1.3)
|
||||
docile (1.1.5)
|
||||
doorkeeper (4.2.0)
|
||||
railties (>= 4.2)
|
||||
dotenv (2.1.1)
|
||||
dotenv-rails (2.1.1)
|
||||
dotenv (= 2.1.1)
|
||||
railties (>= 4.0, < 5.1)
|
||||
dotenv (2.1.2)
|
||||
dotenv-rails (2.1.2)
|
||||
dotenv (= 2.1.2)
|
||||
railties (>= 3.2, < 5.1)
|
||||
erubis (2.7.0)
|
||||
exception_notification (4.2.1)
|
||||
actionmailer (>= 4.0, < 6)
|
||||
activesupport (>= 4.0, < 6)
|
||||
execjs (2.7.0)
|
||||
factory_girl (4.7.0)
|
||||
factory_girl (4.8.0)
|
||||
activesupport (>= 3.0.0)
|
||||
factory_girl_rails (4.7.0)
|
||||
factory_girl (~> 4.7.0)
|
||||
factory_girl_rails (4.8.0)
|
||||
factory_girl (~> 4.8.0)
|
||||
railties (>= 3.0.0)
|
||||
globalid (0.3.7)
|
||||
activesupport (>= 4.1.0)
|
||||
|
@ -113,20 +115,32 @@ GEM
|
|||
multi_xml (>= 0.5.2)
|
||||
i18n (0.7.0)
|
||||
jmespath (1.3.1)
|
||||
jquery-rails (4.2.1)
|
||||
jquery-rails (4.2.2)
|
||||
rails-dom-testing (>= 1, < 3)
|
||||
railties (>= 4.2.0)
|
||||
thor (>= 0.14, < 2.0)
|
||||
jquery-ui-rails (5.0.5)
|
||||
jquery-ui-rails (6.0.1)
|
||||
railties (>= 3.2.16)
|
||||
json (1.8.3)
|
||||
json-schema (2.6.2)
|
||||
addressable (~> 2.3.8)
|
||||
jsonapi (0.1.1.beta2)
|
||||
json (~> 1.8)
|
||||
kaminari (0.17.0)
|
||||
actionpack (>= 3.0.0)
|
||||
activesupport (>= 3.0.0)
|
||||
json (2.0.3)
|
||||
json-schema (2.7.0)
|
||||
addressable (>= 2.4)
|
||||
jsonapi (0.1.1.beta6)
|
||||
jsonapi-parser (= 0.1.1.beta3)
|
||||
jsonapi-renderer (= 0.1.1.beta1)
|
||||
jsonapi-parser (0.1.1.beta3)
|
||||
jsonapi-renderer (0.1.1.beta1)
|
||||
kaminari (1.0.1)
|
||||
activesupport (>= 4.1.0)
|
||||
kaminari-actionview (= 1.0.1)
|
||||
kaminari-activerecord (= 1.0.1)
|
||||
kaminari-core (= 1.0.1)
|
||||
kaminari-actionview (1.0.1)
|
||||
actionview
|
||||
kaminari-core (= 1.0.1)
|
||||
kaminari-activerecord (1.0.1)
|
||||
activerecord
|
||||
kaminari-core (= 1.0.1)
|
||||
kaminari-core (1.0.1)
|
||||
loofah (2.0.3)
|
||||
nokogiri (>= 1.5.9)
|
||||
mail (2.6.4)
|
||||
|
@ -140,12 +154,11 @@ GEM
|
|||
mime-types-data (3.2016.0521)
|
||||
mimemagic (0.3.2)
|
||||
mini_portile2 (2.1.0)
|
||||
minitest (5.9.1)
|
||||
multi_xml (0.5.5)
|
||||
minitest (5.10.1)
|
||||
multi_xml (0.6.0)
|
||||
nio4r (1.2.1)
|
||||
nokogiri (1.6.8)
|
||||
nokogiri (1.7.0.1)
|
||||
mini_portile2 (~> 2.1.0)
|
||||
pkg-config (~> 1.1.7)
|
||||
orm_adapter (0.5.0)
|
||||
paperclip (5.1.0)
|
||||
activemodel (>= 4.2.0)
|
||||
|
@ -153,20 +166,20 @@ GEM
|
|||
cocaine (~> 0.5.5)
|
||||
mime-types
|
||||
mimemagic (~> 0.3.0)
|
||||
parser (2.3.1.4)
|
||||
parser (2.3.3.1)
|
||||
ast (~> 2.2)
|
||||
pg (0.19.0)
|
||||
pkg-config (1.1.7)
|
||||
powerpack (0.1.1)
|
||||
pry (0.10.4)
|
||||
coderay (~> 1.1.0)
|
||||
method_source (~> 0.8.1)
|
||||
slop (~> 3.4)
|
||||
pry-byebug (3.4.0)
|
||||
pry-byebug (3.4.2)
|
||||
byebug (~> 9.0)
|
||||
pry (~> 0.10)
|
||||
pry-rails (0.3.4)
|
||||
pry (>= 0.9.10)
|
||||
public_suffix (2.0.5)
|
||||
puma (3.6.2)
|
||||
pundit (1.1.0)
|
||||
activesupport (>= 3.0.0)
|
||||
|
@ -177,35 +190,35 @@ GEM
|
|||
rack-cors (0.4.0)
|
||||
rack-test (0.6.3)
|
||||
rack (>= 1.0)
|
||||
rails (5.0.0.1)
|
||||
actioncable (= 5.0.0.1)
|
||||
actionmailer (= 5.0.0.1)
|
||||
actionpack (= 5.0.0.1)
|
||||
actionview (= 5.0.0.1)
|
||||
activejob (= 5.0.0.1)
|
||||
activemodel (= 5.0.0.1)
|
||||
activerecord (= 5.0.0.1)
|
||||
activesupport (= 5.0.0.1)
|
||||
rails (5.0.1)
|
||||
actioncable (= 5.0.1)
|
||||
actionmailer (= 5.0.1)
|
||||
actionpack (= 5.0.1)
|
||||
actionview (= 5.0.1)
|
||||
activejob (= 5.0.1)
|
||||
activemodel (= 5.0.1)
|
||||
activerecord (= 5.0.1)
|
||||
activesupport (= 5.0.1)
|
||||
bundler (>= 1.3.0, < 2.0)
|
||||
railties (= 5.0.0.1)
|
||||
railties (= 5.0.1)
|
||||
sprockets-rails (>= 2.0.0)
|
||||
rails-dom-testing (2.0.1)
|
||||
rails-dom-testing (2.0.2)
|
||||
activesupport (>= 4.2.0, < 6.0)
|
||||
nokogiri (~> 1.6.0)
|
||||
nokogiri (~> 1.6)
|
||||
rails-html-sanitizer (1.0.3)
|
||||
loofah (~> 2.0)
|
||||
railties (5.0.0.1)
|
||||
actionpack (= 5.0.0.1)
|
||||
activesupport (= 5.0.0.1)
|
||||
railties (5.0.1)
|
||||
actionpack (= 5.0.1)
|
||||
activesupport (= 5.0.1)
|
||||
method_source
|
||||
rake (>= 0.8.7)
|
||||
thor (>= 0.18.1, < 2.0)
|
||||
rainbow (2.1.0)
|
||||
rake (11.3.0)
|
||||
redis (3.3.1)
|
||||
rainbow (2.2.1)
|
||||
rake (12.0.0)
|
||||
redis (3.3.2)
|
||||
responders (2.3.0)
|
||||
railties (>= 4.2.0, < 5.1)
|
||||
rspec-core (3.5.3)
|
||||
rspec-core (3.5.4)
|
||||
rspec-support (~> 3.5.0)
|
||||
rspec-expectations (3.5.0)
|
||||
diff-lcs (>= 1.2.0, < 2.0)
|
||||
|
@ -222,14 +235,14 @@ GEM
|
|||
rspec-mocks (~> 3.5.0)
|
||||
rspec-support (~> 3.5.0)
|
||||
rspec-support (3.5.0)
|
||||
rubocop (0.43.0)
|
||||
parser (>= 2.3.1.1, < 3.0)
|
||||
rubocop (0.47.1)
|
||||
parser (>= 2.3.3.1, < 3.0)
|
||||
powerpack (~> 0.1)
|
||||
rainbow (>= 1.99.1, < 3.0)
|
||||
ruby-progressbar (~> 1.7)
|
||||
unicode-display_width (~> 1.0, >= 1.0.1)
|
||||
ruby-progressbar (1.8.1)
|
||||
sass (3.4.22)
|
||||
sass (3.4.23)
|
||||
sass-rails (5.0.6)
|
||||
railties (>= 4.0.0, < 6)
|
||||
sass (~> 3.1)
|
||||
|
@ -243,11 +256,11 @@ GEM
|
|||
json (>= 1.8, < 3)
|
||||
simplecov-html (~> 0.10.0)
|
||||
simplecov-html (0.10.0)
|
||||
slack-notifier (1.5.1)
|
||||
slack-notifier (2.0.0)
|
||||
slop (3.6.0)
|
||||
snorlax (0.1.6)
|
||||
rails (> 4.1)
|
||||
sprockets (3.7.0)
|
||||
sprockets (3.7.1)
|
||||
concurrent-ruby (~> 1.0)
|
||||
rack (> 1, < 3)
|
||||
sprockets-rails (3.2.0)
|
||||
|
@ -256,15 +269,15 @@ GEM
|
|||
sprockets (>= 3.0.0)
|
||||
sucker_punch (2.0.2)
|
||||
concurrent-ruby (~> 1.0.0)
|
||||
thor (0.19.1)
|
||||
thor (0.19.4)
|
||||
thread_safe (0.3.5)
|
||||
tilt (2.0.5)
|
||||
tunemygc (1.0.68)
|
||||
tunemygc (1.0.69)
|
||||
tzinfo (1.2.2)
|
||||
thread_safe (~> 0.1)
|
||||
uglifier (3.0.2)
|
||||
uglifier (3.0.4)
|
||||
execjs (>= 0.3.0, < 3)
|
||||
unicode-display_width (1.1.1)
|
||||
unicode-display_width (1.1.3)
|
||||
warden (1.2.6)
|
||||
rack (>= 1.0)
|
||||
websocket-driver (0.6.4)
|
||||
|
@ -321,4 +334,4 @@ RUBY VERSION
|
|||
ruby 2.3.0p0
|
||||
|
||||
BUNDLED WITH
|
||||
1.13.6
|
||||
1.13.7
|
||||
|
|
11
README.md
11
README.md
|
@ -2,6 +2,7 @@ Metamaps
|
|||
=======
|
||||
|
||||
[![Build Status](https://travis-ci.org/metamaps/metamaps.svg?branch=develop)](https://travis-ci.org/metamaps/metamaps)
|
||||
[![Code Climate](https://codeclimate.com/github/metamaps/metamaps/badges/gpa.svg)](https://codeclimate.com/github/metamaps/metamaps)
|
||||
|
||||
## What is Metamaps?
|
||||
|
||||
|
@ -16,8 +17,14 @@ Metamaps is developed and maintained by a distributed, nomadic community compris
|
|||
- Contact: [team@metamaps.cc](mailto:team@metamaps.cc) or [@metamapps](https://twitter.com/metamapps) on Twitter
|
||||
- User Documentation: [docs.metamaps.cc](https://docs.metamaps.cc)
|
||||
- User Community: [hylo.com/c/metamaps](https://www.hylo.com/c/metamaps)
|
||||
- Development Roadmap: [github.com/metamaps/metamaps/milestones](https://github.com/metamaps/metamaps/milestones)
|
||||
- To send us a personal message or request an invite to the open beta, get in touch with us via email, Twitter, or Hylo
|
||||
- To see what we're developing, or to weigh in on what you'd like to see developed, see our [Metamaps Feedback and Features](https://trello.com/b/uFOA6a2x/metamaps-feedback-feature-ideas-requests) board on trello
|
||||
- To follow along with, or contribute,to our design process, see our [Metamaps Design](https://trello.com/b/8HlCikOX/metamaps-design) board on trello
|
||||
- To follow along with, or contribute to, our development process, see our [Github Issues and Pull Requests](https://github.com/metamaps/metamaps/issues)
|
||||
- Request an invite to the open beta [here](https://metamaps.cc/request)
|
||||
|
||||
<!-- markdown hack to split two lists -->
|
||||
|
||||
- To send us a personal message get in touch with us via email, Twitter, or Hylo
|
||||
- If you would like to report a bug, please check the [issues][contributing-issues] section in our [contributing instructions][contributing].
|
||||
- If you would like to get set up as a developer, that's great! Read on for help getting your development environment set up.
|
||||
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
// eslint-disable spaced-comment
|
||||
// JS and CSS bundles
|
||||
//= link_directory ../javascripts .js
|
||||
//= link_directory ../stylesheets .css
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
// eslint-disable spaced-comment
|
||||
// This is a manifest file that'll be compiled into application.js, which will include all the files
|
||||
// listed below.
|
||||
//
|
||||
|
@ -10,7 +9,8 @@
|
|||
//
|
||||
// WARNING: THE FIRST BLANK LINE MARKS THE END OF WHAT'S TO BE PROCESSED, ANY BLANK LINE SHOULD
|
||||
// GO AFTER THE REQUIRES BELOW.
|
||||
//
|
||||
//
|
||||
/* eslint-disable spaced-comment */
|
||||
//= require jquery
|
||||
//= require jquery-ui
|
||||
//= require jquery_ujs
|
||||
|
@ -19,3 +19,4 @@
|
|||
//= require ./webpacked/metamaps.bundle
|
||||
//= require ./Metamaps.ServerData
|
||||
//= require homepageVimeoFallback
|
||||
/* eslint-enable spaced-comment */
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
/* global $ */
|
||||
|
||||
$(document).ready(function () {
|
||||
$(document).ready(function() {
|
||||
if (window.location.pathname === '/') {
|
||||
$.ajax({
|
||||
type: 'GET',
|
||||
url: 'https://player.vimeo.com',
|
||||
error: function (e) {
|
||||
error: function(e) {
|
||||
$('.homeVideo').hide()
|
||||
$('.homeVideo').replaceWith($('<video/>', {
|
||||
poster: '/assets/metamaps-intro-poster.webp',
|
||||
|
|
161
app/assets/javascripts/lib/ajaxq.js
Normal file
161
app/assets/javascripts/lib/ajaxq.js
Normal file
|
@ -0,0 +1,161 @@
|
|||
// AjaxQ jQuery Plugin
|
||||
// Copyright (c) 2012 Foliotek Inc.
|
||||
// MIT License
|
||||
// https://github.com/Foliotek/ajaxq
|
||||
// Uses CommonJS, AMD or browser globals to create a jQuery plugin.
|
||||
|
||||
(function (factory) {
|
||||
if (typeof define === 'function' && define.amd) {
|
||||
// AMD. Register as an anonymous module.
|
||||
define(['jquery'], factory);
|
||||
} else if (typeof module === 'object' && module.exports) {
|
||||
// Node/CommonJS
|
||||
module.exports = factory(require('jquery'));
|
||||
} else {
|
||||
// Browser globals
|
||||
factory(jQuery);
|
||||
}
|
||||
}(function ($) {
|
||||
var queues = {};
|
||||
var activeReqs = {};
|
||||
|
||||
// Register an $.ajaxq function, which follows the $.ajax interface, but allows a queue name which will force only one request per queue to fire.
|
||||
// opts can be the regular $.ajax settings plainObject, or a callback returning the settings object, to be evaluated just prior to the actual call to $.ajax.
|
||||
$.ajaxq = function(qname, opts) {
|
||||
|
||||
if (typeof opts === "undefined") {
|
||||
throw ("AjaxQ: queue name is not provided");
|
||||
}
|
||||
|
||||
// Will return a Deferred promise object extended with success/error/callback, so that this function matches the interface of $.ajax
|
||||
var deferred = $.Deferred(),
|
||||
promise = deferred.promise();
|
||||
|
||||
promise.success = promise.done;
|
||||
promise.error = promise.fail;
|
||||
promise.complete = promise.always;
|
||||
|
||||
// Check whether options are to be evaluated at call time or not.
|
||||
var deferredOpts = typeof opts === 'function';
|
||||
// Create a deep copy of the arguments, and enqueue this request.
|
||||
var clonedOptions = !deferredOpts ? $.extend(true, {}, opts) : null;
|
||||
enqueue(function() {
|
||||
// Send off the ajax request now that the item has been removed from the queue
|
||||
var jqXHR = $.ajax.apply(window, [deferredOpts ? opts() : clonedOptions]);
|
||||
|
||||
// Notify the returned deferred object with the correct context when the jqXHR is done or fails
|
||||
// Note that 'always' will automatically be fired once one of these are called: http://api.jquery.com/category/deferred-object/.
|
||||
jqXHR.done(function() {
|
||||
deferred.resolve.apply(this, arguments);
|
||||
});
|
||||
jqXHR.fail(function() {
|
||||
deferred.reject.apply(this, arguments);
|
||||
});
|
||||
|
||||
jqXHR.always(dequeue); // make sure to dequeue the next request AFTER the done and fail callbacks are fired
|
||||
|
||||
return jqXHR;
|
||||
});
|
||||
|
||||
return promise;
|
||||
|
||||
|
||||
// If there is no queue, create an empty one and instantly process this item.
|
||||
// Otherwise, just add this item onto it for later processing.
|
||||
function enqueue(cb) {
|
||||
if (!queues[qname]) {
|
||||
queues[qname] = [];
|
||||
var xhr = cb();
|
||||
activeReqs[qname] = xhr;
|
||||
}
|
||||
else {
|
||||
queues[qname].push(cb);
|
||||
}
|
||||
}
|
||||
|
||||
// Remove the next callback from the queue and fire it off.
|
||||
// If the queue was empty (this was the last item), delete it from memory so the next one can be instantly processed.
|
||||
function dequeue() {
|
||||
if (!queues[qname]) {
|
||||
return;
|
||||
}
|
||||
var nextCallback = queues[qname].shift();
|
||||
if (nextCallback) {
|
||||
var xhr = nextCallback();
|
||||
activeReqs[qname] = xhr;
|
||||
}
|
||||
else {
|
||||
delete queues[qname];
|
||||
delete activeReqs[qname];
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Register a $.postq and $.getq method to provide shortcuts for $.get and $.post
|
||||
// Copied from jQuery source to make sure the functions share the same defaults as $.get and $.post.
|
||||
$.each( [ "getq", "postq" ], function( i, method ) {
|
||||
$[ method ] = function( qname, url, data, callback, type ) {
|
||||
|
||||
if ( $.isFunction( data ) ) {
|
||||
type = type || callback;
|
||||
callback = data;
|
||||
data = undefined;
|
||||
}
|
||||
|
||||
return $.ajaxq(qname, {
|
||||
type: method === "postq" ? "post" : "get",
|
||||
url: url,
|
||||
data: data,
|
||||
success: callback,
|
||||
dataType: type
|
||||
});
|
||||
};
|
||||
});
|
||||
|
||||
var isQueueRunning = function(qname) {
|
||||
return (queues.hasOwnProperty(qname) && queues[qname].length > 0) || activeReqs.hasOwnProperty(qname);
|
||||
};
|
||||
|
||||
var isAnyQueueRunning = function() {
|
||||
for (var i in queues) {
|
||||
if (isQueueRunning(i)) return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
$.ajaxq.isRunning = function(qname) {
|
||||
if (qname) return isQueueRunning(qname);
|
||||
else return isAnyQueueRunning();
|
||||
};
|
||||
|
||||
$.ajaxq.getActiveRequest = function(qname) {
|
||||
if (!qname) throw ("AjaxQ: queue name is required");
|
||||
|
||||
return activeReqs[qname];
|
||||
};
|
||||
|
||||
$.ajaxq.abort = function(qname) {
|
||||
if (!qname) throw ("AjaxQ: queue name is required");
|
||||
|
||||
var current = $.ajaxq.getActiveRequest(qname);
|
||||
delete queues[qname];
|
||||
delete activeReqs[qname];
|
||||
if (current) current.abort();
|
||||
};
|
||||
|
||||
$.ajaxq.clear = function(qname) {
|
||||
if (!qname) {
|
||||
for (var i in queues) {
|
||||
if (queues.hasOwnProperty(i)) {
|
||||
queues[i] = [];
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (queues[qname]) {
|
||||
queues[qname] = [];
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
}));
|
|
@ -1575,6 +1575,7 @@ h3.filterBox {
|
|||
box-sizing: border-box;
|
||||
margin: 0.75em;
|
||||
padding: 0.75em;
|
||||
padding-top: 0.85em;
|
||||
height: 3em;
|
||||
background-color: #AAB0FB;
|
||||
border-radius: 0.3em;
|
||||
|
@ -2029,6 +2030,7 @@ input.collaboratorSearchField {
|
|||
position: relative;
|
||||
cursor: pointer;
|
||||
display: none;
|
||||
padding: 0 2px;
|
||||
}
|
||||
.mapInfoShareIcon {
|
||||
width: 24px;
|
||||
|
@ -2068,6 +2070,43 @@ and it won't be important on password protected instances */
|
|||
.yourMap .mapInfoDelete {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.mapInfoButtonsWrapper .mapInfoThumbnail {
|
||||
display: block;
|
||||
background-image: url(<%= asset_path('screenshot_sprite.png') %>);
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
|
||||
& > span {
|
||||
bottom: -8px;
|
||||
right: 2px;
|
||||
font-size: 12px;
|
||||
color: #e0e0e0;
|
||||
|
||||
&:hover {
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
|
||||
.tooltip {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-position: -32px 0;
|
||||
|
||||
.tooltip {
|
||||
display: block;
|
||||
position: absolute;
|
||||
bottom: 30px;
|
||||
background: black;
|
||||
color: white;
|
||||
border-radius: 2px;
|
||||
padding: 3px 5px 2px 5px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.mapInfoButtonsWrapper span {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
|
|
263
app/assets/stylesheets/emoji-mart-0.3.5.css
Normal file
263
app/assets/stylesheets/emoji-mart-0.3.5.css
Normal file
|
@ -0,0 +1,263 @@
|
|||
.emoji-mart,
|
||||
.emoji-mart * {
|
||||
box-sizing: border-box;
|
||||
line-height: 1.15;
|
||||
}
|
||||
|
||||
.emoji-mart {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Helvetica Neue", sans-serif;
|
||||
font-size: 16px;
|
||||
display: inline-block;
|
||||
color: #222427;
|
||||
border: 1px solid #d9d9d9;
|
||||
border-radius: 5px;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.emoji-mart .emoji-mart-emoji {
|
||||
padding: 6px;
|
||||
}
|
||||
|
||||
.emoji-mart-bar:first-child {
|
||||
border-top-left-radius: 5px;
|
||||
border-top-right-radius: 5px;
|
||||
}
|
||||
.emoji-mart-bar:last-child {
|
||||
border-bottom-left-radius: 5px;
|
||||
border-bottom-right-radius: 5px;
|
||||
}
|
||||
|
||||
.emoji-mart-anchors {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 0 6px;
|
||||
color: #858585;
|
||||
line-height: 0;
|
||||
}
|
||||
|
||||
.emoji-mart-anchor {
|
||||
position: relative;
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
padding: 12px 4px;
|
||||
overflow: hidden;
|
||||
transition: color .1s ease-out;
|
||||
}
|
||||
.emoji-mart-anchor:hover,
|
||||
.emoji-mart-anchor-selected {
|
||||
color: #464646;
|
||||
}
|
||||
|
||||
.emoji-mart-anchor-selected .emoji-mart-anchor-bar {
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
.emoji-mart-anchor-bar {
|
||||
position: absolute;
|
||||
bottom: -3px; left: 0;
|
||||
width: 100%; height: 3px;
|
||||
background-color: #464646;
|
||||
}
|
||||
|
||||
.emoji-mart-anchors i {
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
max-width: 22px;
|
||||
}
|
||||
|
||||
.emoji-mart-anchors svg {
|
||||
fill: currentColor;
|
||||
}
|
||||
|
||||
.emoji-mart-scroll {
|
||||
overflow-y: scroll;
|
||||
height: 270px;
|
||||
padding: 0 6px 6px 6px;
|
||||
border: solid #d9d9d9;
|
||||
border-width: 1px 0;
|
||||
}
|
||||
|
||||
.emoji-mart-search {
|
||||
font-size: 16px;
|
||||
display: block;
|
||||
width: 100%;
|
||||
padding: .2em .6em;
|
||||
margin-top: 6px;
|
||||
border-radius: 25px;
|
||||
border: 1px solid #d9d9d9;
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
.emoji-mart-category .emoji-mart-emoji span {
|
||||
z-index: 1;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.emoji-mart-category .emoji-mart-emoji:hover:before {
|
||||
z-index: 0;
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0; left: 0;
|
||||
width: 100%; height: 100%;
|
||||
background-color: #f4f4f4;
|
||||
border-radius: 100%;
|
||||
}
|
||||
|
||||
.emoji-mart-category-label {
|
||||
z-index: 2;
|
||||
position: relative;
|
||||
position: -webkit-sticky;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.emoji-mart-category-label span {
|
||||
display: block;
|
||||
width: 100%;
|
||||
font-weight: 500;
|
||||
padding: 5px 6px;
|
||||
background-color: #fff;
|
||||
background-color: rgba(255, 255, 255, .95);
|
||||
}
|
||||
|
||||
.emoji-mart-emoji {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
font-size: 0;
|
||||
}
|
||||
|
||||
.emoji-mart-no-results {
|
||||
font-size: 14px;
|
||||
text-align: center;
|
||||
padding-top: 70px;
|
||||
color: #858585;
|
||||
}
|
||||
.emoji-mart-no-results span {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.emoji-mart-preview {
|
||||
position: relative;
|
||||
height: 70px;
|
||||
}
|
||||
|
||||
.emoji-mart-preview-emoji,
|
||||
.emoji-mart-preview-data,
|
||||
.emoji-mart-preview-skins {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
.emoji-mart-preview-emoji {
|
||||
left: 12px;
|
||||
}
|
||||
|
||||
.emoji-mart-preview-data {
|
||||
left: 68px; right: 12px;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.emoji-mart-preview-skins {
|
||||
right: 30px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.emoji-mart-preview-name {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.emoji-mart-preview-shortname {
|
||||
font-size: 12px;
|
||||
color: #888;
|
||||
}
|
||||
.emoji-mart-preview-shortname + .emoji-mart-preview-shortname,
|
||||
.emoji-mart-preview-shortname + .emoji-mart-preview-emoticon,
|
||||
.emoji-mart-preview-emoticon + .emoji-mart-preview-emoticon {
|
||||
margin-left: .5em;
|
||||
}
|
||||
|
||||
.emoji-mart-preview-emoticon {
|
||||
font-size: 11px;
|
||||
color: #bbb;
|
||||
}
|
||||
|
||||
.emoji-mart-title span {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.emoji-mart-title .emoji-mart-emoji {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.emoji-mart-title-label {
|
||||
color: #999A9C;
|
||||
font-size: 26px;
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
.emoji-mart-skin-swatches {
|
||||
font-size: 0;
|
||||
padding: 2px 0;
|
||||
border: 1px solid #d9d9d9;
|
||||
border-radius: 12px;
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
.emoji-mart-skin-swatches-opened .emoji-mart-skin-swatch {
|
||||
width: 16px;
|
||||
padding: 0 2px;
|
||||
}
|
||||
|
||||
.emoji-mart-skin-swatches-opened .emoji-mart-skin-swatch-selected:after {
|
||||
opacity: .75;
|
||||
}
|
||||
|
||||
.emoji-mart-skin-swatch {
|
||||
display: inline-block;
|
||||
width: 0;
|
||||
vertical-align: middle;
|
||||
transition-property: width, padding;
|
||||
transition-duration: .125s;
|
||||
transition-timing-function: ease-out;
|
||||
}
|
||||
|
||||
.emoji-mart-skin-swatch:nth-child(1) { transition-delay: 0 }
|
||||
.emoji-mart-skin-swatch:nth-child(2) { transition-delay: .03s }
|
||||
.emoji-mart-skin-swatch:nth-child(3) { transition-delay: .06s }
|
||||
.emoji-mart-skin-swatch:nth-child(4) { transition-delay: .09s }
|
||||
.emoji-mart-skin-swatch:nth-child(5) { transition-delay: .12s }
|
||||
.emoji-mart-skin-swatch:nth-child(6) { transition-delay: .15s }
|
||||
|
||||
.emoji-mart-skin-swatch-selected {
|
||||
position: relative;
|
||||
width: 16px;
|
||||
padding: 0 2px;
|
||||
}
|
||||
.emoji-mart-skin-swatch-selected:after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 50%; left: 50%;
|
||||
width: 4px; height: 4px;
|
||||
margin: -2px 0 0 -2px;
|
||||
background-color: #fff;
|
||||
border-radius: 100%;
|
||||
pointer-events: none;
|
||||
opacity: 0;
|
||||
transition: opacity .2s ease-out;
|
||||
}
|
||||
|
||||
.emoji-mart-skin {
|
||||
display: inline-block;
|
||||
width: 100%; padding-top: 100%;
|
||||
max-width: 12px;
|
||||
border-radius: 100%;
|
||||
}
|
||||
|
||||
.emoji-mart-skin-tone-1 { background-color: #ffc93a }
|
||||
.emoji-mart-skin-tone-2 { background-color: #fadcbc }
|
||||
.emoji-mart-skin-tone-3 { background-color: #e0bb95 }
|
||||
.emoji-mart-skin-tone-4 { background-color: #bf8f68 }
|
||||
.emoji-mart-skin-tone-5 { background-color: #9b643d }
|
||||
.emoji-mart-skin-tone-6 { background-color: #594539 }
|
|
@ -1,328 +0,0 @@
|
|||
.collaborator-video {
|
||||
z-index: 1;
|
||||
position: absolute;
|
||||
width: 150px;
|
||||
height: 150px;
|
||||
cursor: default;
|
||||
color: #FFF;
|
||||
}
|
||||
.collaborator-video .video-receive {
|
||||
position: absolute;
|
||||
width: 160px;
|
||||
padding: 20px 20px 20px 170px;
|
||||
background: #424242;
|
||||
height: 110px;
|
||||
border-top-left-radius: 75px;
|
||||
border-bottom-left-radius: 75px;
|
||||
border-top-right-radius: 2px;
|
||||
border-bottom-right-radius: 2px;
|
||||
}
|
||||
.collaborator-video .video-receive .video-statement {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.collaborator-video .video-receive .btn-group .btn-yes {
|
||||
margin-right: 10px;
|
||||
}
|
||||
.collaborator-video .video-receive .btn-group .btn-no {
|
||||
background-color: #c04f4f;
|
||||
}
|
||||
.collaborator-video .video-receive .btn-group .btn-no:hover {
|
||||
background-color: #A54242;
|
||||
}
|
||||
.collaborator-video .video-cutoff {
|
||||
width: 150px;
|
||||
height: 150px;
|
||||
overflow: hidden;
|
||||
border-radius: 75px;
|
||||
z-index: 0;
|
||||
position: relative;
|
||||
-webkit-box-shadow: 0px 6px 3px rgba(0, 0, 0, 0.23), 10px 10px 10px rgba(0, 0, 0, 0.19);
|
||||
-moz-box-shadow: 0px 6px 3px rgba(0, 0, 0, 0.23), 10px 10px 10px rgba(0, 0, 0, 0.19);
|
||||
box-shadow: 0px 6px 3px rgba(0, 0, 0, 0.23), 10px 10px 10px rgba(0, 0, 0, 0.19);
|
||||
}
|
||||
.collaborator-video .video-cutoff video {
|
||||
height: 150px;
|
||||
margin-left: -25px;
|
||||
}
|
||||
.collaborator-video .video-cutoff .collaborator-video-avatar {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
-webkit-user-select: none;
|
||||
-khtml-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-o-user-select: none;
|
||||
user-select: none;
|
||||
-webkit-user-drag: none;
|
||||
display: none;
|
||||
}
|
||||
.collaborator-video .video-audio {
|
||||
position: absolute;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
top: 85%;
|
||||
right: 0px;
|
||||
cursor: pointer;
|
||||
background: url(<%= asset_path 'audio_sprite.png' %>) no-repeat;
|
||||
}
|
||||
.collaborator-video .video-audio:hover {
|
||||
background-position-x: -24px;
|
||||
}
|
||||
.collaborator-video .video-audio.active {
|
||||
background-position-y: -24px;
|
||||
}
|
||||
.collaborator-video .video-video {
|
||||
position: absolute;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
top: 85%;
|
||||
left: 0px;
|
||||
cursor: pointer;
|
||||
background: url(<%= asset_path 'camera_sprite.png' %>) no-repeat;
|
||||
}
|
||||
.collaborator-video .video-video:hover {
|
||||
background-position-x: -24px;
|
||||
}
|
||||
.collaborator-video .video-video.active {
|
||||
background-position-y: -24px;
|
||||
}
|
||||
.collaborator-video.my-video {
|
||||
left: 30px;
|
||||
top: 72px;
|
||||
}
|
||||
#chat-box-wrapper {
|
||||
height: 100%;
|
||||
float: right;
|
||||
}
|
||||
.chat-box {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
z-index: 1;
|
||||
width: 300px;
|
||||
height: 100%;
|
||||
background: #424242;
|
||||
box-shadow: -8px 0px 16px 2px rgba(0, 0, 0, 0.23);
|
||||
}
|
||||
.chat-box .chat-button {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: -36px;
|
||||
width: 36px;
|
||||
height: 49px;
|
||||
background: url(<%= asset_path 'junto.png' %>) no-repeat 2px 9px, url(<%= asset_path 'tray_tab.png' %>) no-repeat;
|
||||
cursor: pointer;
|
||||
}
|
||||
.chat-box .chat-button.active {
|
||||
background: url(<%= asset_path 'junto_spinner_dark.gif' %>) no-repeat 2px 8px, url(<%= asset_path 'tray_tab.png' %>) no-repeat !important;
|
||||
}
|
||||
.chat-box .chat-button .chat-unread {
|
||||
background: #DAB539;
|
||||
position: absolute;
|
||||
top: -3px;
|
||||
left: -11px;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 11px;
|
||||
border: 2px solid #424242;
|
||||
color: #424242;
|
||||
text-align: center;
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
line-height: 20px;
|
||||
}
|
||||
.chat-box .junto-header {
|
||||
width: 276px;
|
||||
padding: 16px 8px 16px 16px;
|
||||
font-size: 16px;
|
||||
text-align: left;
|
||||
font-weight: bold;
|
||||
background-color: #000000;
|
||||
color: #f5f5f5;
|
||||
box-shadow: 0px 6px 3px rgba(0, 0, 0, 0.23);
|
||||
}
|
||||
.chat-box .junto-header .cursor-toggle {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
margin-right: 8px;
|
||||
margin-top: -8px;
|
||||
float: right;
|
||||
background: url(<%= asset_path 'cursor_sprite.png' %>) no-repeat;
|
||||
}
|
||||
.chat-box .junto-header .cursor-toggle:hover {
|
||||
background-position-x: -32px;
|
||||
}
|
||||
.chat-box .junto-header .cursor-toggle.active {
|
||||
background-position-y: -32px;
|
||||
}
|
||||
.chat-box .junto-header .video-toggle {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
margin-right: 10px;
|
||||
margin-top: -8px;
|
||||
float: right;
|
||||
background: url(<%= asset_path 'video_sprite.png' %>) no-repeat;
|
||||
}
|
||||
.chat-box .junto-header .video-toggle:hover {
|
||||
background-position-x: -32px;
|
||||
}
|
||||
.chat-box .junto-header .video-toggle.active {
|
||||
background-position-y: -32px;
|
||||
}
|
||||
.chat-box .participants {
|
||||
width: 100%;
|
||||
min-height: 150px;
|
||||
padding: 16px 0px 16px 0px;
|
||||
text-align: left;
|
||||
color: #f5f5f5;
|
||||
overflow-y: auto;
|
||||
}
|
||||
.chat-box .participants .conversation-live {
|
||||
padding: 5px 10px 5px 10px;
|
||||
background: #c04f4f;
|
||||
margin: 5px 10px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
.chat-box .participants .conversation-live .call-action {
|
||||
float: right;
|
||||
cursor: pointer;
|
||||
color: #EBFF00;
|
||||
}
|
||||
.chat-box .participants .participant {
|
||||
width: 89%;
|
||||
padding: 8px 8px 2px 8px;
|
||||
color: #f5f5f5;
|
||||
font-family: arial, sans-serif;
|
||||
font-size: 13px;
|
||||
line-height: 14px;
|
||||
}
|
||||
.chat-box .participants .participant .chat-participant-image {
|
||||
width: 15%;
|
||||
float: left;
|
||||
overflow: hidden;
|
||||
color: #BBB;
|
||||
padding-top: 2px;
|
||||
}
|
||||
.chat-box .participants .participant .chat-participant-image img {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 18px;
|
||||
}
|
||||
.chat-box .participants .participant .chat-participant-name {
|
||||
width: 53%;
|
||||
float: left;
|
||||
font-size: 13px;
|
||||
font-weight: bold;
|
||||
margin-top: 12px;
|
||||
padding: 2px 8px 0;
|
||||
text-align: left;
|
||||
}
|
||||
.chat-box .participants .participant .chat-participant-invite-call,
|
||||
.chat-box .participants .participant .chat-participant-invite-join
|
||||
{
|
||||
float: right;
|
||||
background: #4FC059 url(<%= asset_path 'invitepeer16.png' %>) no-repeat center center;
|
||||
}
|
||||
.chat-box .participants .participant .chat-participant-invite-call.pending,
|
||||
.chat-box .participants .participant .chat-participant-invite-join.pending {
|
||||
background: #dab539 url(<%= asset_path 'ellipsis.gif' %>) no-repeat center center;
|
||||
}
|
||||
.chat-box .participants .participant .chat-participant-participating {
|
||||
float: right;
|
||||
margin-top: 14px;
|
||||
}
|
||||
.chat-box .participants .participant .chat-participant-participating .green-dot {
|
||||
background: #4fc059;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 6px;
|
||||
}
|
||||
.chat-box .chat-header {
|
||||
width: 276px;
|
||||
padding: 16px 8px 16px 16px;
|
||||
font-size: 16px;
|
||||
text-align: left;
|
||||
font-weight: bold;
|
||||
background-color: #000000;
|
||||
color: #f5f5f5;
|
||||
box-shadow: 0px 6px 3px rgba(0, 0, 0, 0.23);
|
||||
}
|
||||
.chat-box .chat-header .sound-toggle {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
margin-right: 10px;
|
||||
margin-top: -2px;
|
||||
float: right;
|
||||
background: url(<%= asset_path 'sound_sprite.png' %>) no-repeat;
|
||||
}
|
||||
.chat-box .chat-header .sound-toggle:hover {
|
||||
background-position-x: -24px;
|
||||
}
|
||||
.chat-box .chat-header .sound-toggle.active {
|
||||
background-position-y: -24px;
|
||||
}
|
||||
.chat-box .chat-input {
|
||||
min-height: 80px;
|
||||
width: 94%;
|
||||
padding: 8px 3% 8px 3%;
|
||||
font-size: 13px;
|
||||
outline: none;
|
||||
resize: none;
|
||||
}
|
||||
.chat-box .chat-messages {
|
||||
width: 100%;
|
||||
padding: 16px 0px;
|
||||
overflow-y: auto;
|
||||
flex-grow: 1;
|
||||
}
|
||||
.chat-box .chat-messages .chat-message {
|
||||
width: 89%;
|
||||
padding: 8px 8px 2px 8px;
|
||||
color: #f5f5f5;
|
||||
font-family: arial, sans-serif;
|
||||
font-size: 13px;
|
||||
line-height: 14px;
|
||||
}
|
||||
.chat-box .chat-messages .chat-message a:link {
|
||||
color: #4fb5c0;
|
||||
text-decoration: underline;
|
||||
}
|
||||
.chat-box .chat-messages .chat-message a:visited {
|
||||
color: #aea9fd;
|
||||
text-decoration: underline;
|
||||
}
|
||||
.chat-box .chat-messages .chat-message a:hover {
|
||||
color: #dab539;
|
||||
text-decoration: underline;
|
||||
}
|
||||
.chat-box .chat-messages .chat-message .chat-message-user {
|
||||
width: 12%;
|
||||
float: left;
|
||||
overflow: hidden;
|
||||
color: #BBB;
|
||||
padding-top: 2px;
|
||||
}
|
||||
.chat-box .chat-messages .chat-message .chat-message-user img {
|
||||
border: 2px solid #424242;
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
border-radius: 14px;
|
||||
}
|
||||
.chat-box .chat-messages .chat-message .chat-message-meta {
|
||||
padding: 0 8px;
|
||||
float: left;
|
||||
}
|
||||
.chat-box .chat-messages .chat-message .chat-message-username {
|
||||
color: #4fc059;
|
||||
}
|
||||
.chat-box .chat-messages .chat-message .chat-message-text {
|
||||
width: 80%;
|
||||
float: left;
|
||||
padding: 2px 8px 0;
|
||||
text-align: left;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
.chat-box .chat-messages .chat-message .chat-message-time {
|
||||
font-size: 10px;
|
||||
color: #757575;
|
||||
}
|
375
app/assets/stylesheets/junto.scss.erb
Normal file
375
app/assets/stylesheets/junto.scss.erb
Normal file
|
@ -0,0 +1,375 @@
|
|||
.collaborator-video {
|
||||
z-index: 1;
|
||||
position: absolute;
|
||||
width: 150px;
|
||||
height: 150px;
|
||||
cursor: default;
|
||||
color: #FFF;
|
||||
|
||||
.video-receive {
|
||||
position: absolute;
|
||||
width: 160px;
|
||||
padding: 20px 20px 20px 170px;
|
||||
background: #424242;
|
||||
height: 110px;
|
||||
border-top-left-radius: 75px;
|
||||
border-bottom-left-radius: 75px;
|
||||
border-top-right-radius: 2px;
|
||||
border-bottom-right-radius: 2px;
|
||||
|
||||
.video-statement {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.btn-group {
|
||||
.btn-yes {
|
||||
margin-right: 10px;
|
||||
}
|
||||
.btn-no {
|
||||
background-color: #c04f4f;
|
||||
&:hover {
|
||||
background-color: #A54242;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.video-cutoff {
|
||||
width: 150px;
|
||||
height: 150px;
|
||||
overflow: hidden;
|
||||
border-radius: 75px;
|
||||
z-index: 0;
|
||||
position: relative;
|
||||
-webkit-box-shadow: 0px 6px 3px rgba(0, 0, 0, 0.23), 10px 10px 10px rgba(0, 0, 0, 0.19);
|
||||
-moz-box-shadow: 0px 6px 3px rgba(0, 0, 0, 0.23), 10px 10px 10px rgba(0, 0, 0, 0.19);
|
||||
box-shadow: 0px 6px 3px rgba(0, 0, 0, 0.23), 10px 10px 10px rgba(0, 0, 0, 0.19);
|
||||
|
||||
video {
|
||||
height: 150px;
|
||||
margin-left: -25px;
|
||||
}
|
||||
.collaborator-video-avatar {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
-webkit-user-select: none;
|
||||
-khtml-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-o-user-select: none;
|
||||
user-select: none;
|
||||
-webkit-user-drag: none;
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
.video-audio {
|
||||
position: absolute;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
top: 85%;
|
||||
right: 0px;
|
||||
cursor: pointer;
|
||||
background: url(<%= asset_path 'audio_sprite.png' %>) no-repeat;
|
||||
}
|
||||
.video-audio:hover {
|
||||
background-position-x: -24px;
|
||||
}
|
||||
.video-audio.active {
|
||||
background-position-y: -24px;
|
||||
}
|
||||
.video-video {
|
||||
position: absolute;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
top: 85%;
|
||||
left: 0px;
|
||||
cursor: pointer;
|
||||
background: url(<%= asset_path 'camera_sprite.png' %>) no-repeat;
|
||||
}
|
||||
.video-video:hover {
|
||||
background-position-x: -24px;
|
||||
}
|
||||
.video-video.active {
|
||||
background-position-y: -24px;
|
||||
}
|
||||
}
|
||||
|
||||
.collaborator-video.my-video {
|
||||
left: 30px;
|
||||
top: 72px;
|
||||
}
|
||||
|
||||
#chat-box-wrapper {
|
||||
height: 100%;
|
||||
float: right;
|
||||
}
|
||||
|
||||
.chat-box {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
z-index: 1;
|
||||
width: 300px;
|
||||
height: 100%;
|
||||
background: #424242;
|
||||
box-shadow: -8px 0px 16px 2px rgba(0, 0, 0, 0.23);
|
||||
|
||||
.chat-button {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: -36px;
|
||||
width: 36px;
|
||||
height: 49px;
|
||||
background: url(<%= asset_path 'junto.png' %>) no-repeat 2px 9px, url(<%= asset_path 'tray_tab.png' %>) no-repeat;
|
||||
cursor: pointer;
|
||||
|
||||
&.active {
|
||||
background: url(<%= asset_path 'junto_spinner_dark.gif' %>) no-repeat 2px 8px, url(<%= asset_path 'tray_tab.png' %>) no-repeat !important;
|
||||
}
|
||||
.chat-unread {
|
||||
background: #DAB539;
|
||||
position: absolute;
|
||||
top: -3px;
|
||||
left: -11px;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 11px;
|
||||
border: 2px solid #424242;
|
||||
color: #424242;
|
||||
text-align: center;
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
line-height: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.junto-header {
|
||||
width: 276px;
|
||||
padding: 16px 8px 16px 16px;
|
||||
font-size: 16px;
|
||||
text-align: left;
|
||||
font-weight: bold;
|
||||
background-color: #000000;
|
||||
color: #f5f5f5;
|
||||
box-shadow: 0px 6px 3px rgba(0, 0, 0, 0.23);
|
||||
|
||||
.cursor-toggle {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
margin-right: 8px;
|
||||
margin-top: -8px;
|
||||
float: right;
|
||||
background: url(<%= asset_path 'cursor_sprite.png' %>) no-repeat;
|
||||
}
|
||||
.cursor-toggle:hover {
|
||||
background-position-x: -32px;
|
||||
}
|
||||
.cursor-toggle.active {
|
||||
background-position-y: -32px;
|
||||
}
|
||||
.video-toggle {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
margin-right: 10px;
|
||||
margin-top: -8px;
|
||||
float: right;
|
||||
background: url(<%= asset_path 'video_sprite.png' %>) no-repeat;
|
||||
}
|
||||
.video-toggle:hover {
|
||||
background-position-x: -32px;
|
||||
}
|
||||
.video-toggle.active {
|
||||
background-position-y: -32px;
|
||||
}
|
||||
}
|
||||
|
||||
.participants {
|
||||
width: 100%;
|
||||
min-height: 150px;
|
||||
padding: 16px 0px 16px 0px;
|
||||
text-align: left;
|
||||
color: #f5f5f5;
|
||||
overflow-y: auto;
|
||||
|
||||
.conversation-live {
|
||||
padding: 5px 10px 5px 10px;
|
||||
background: #c04f4f;
|
||||
margin: 5px 10px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
.conversation-live .call-action {
|
||||
float: right;
|
||||
cursor: pointer;
|
||||
color: #EBFF00;
|
||||
}
|
||||
.participant {
|
||||
width: 89%;
|
||||
padding: 8px 8px 2px 8px;
|
||||
color: #f5f5f5;
|
||||
font-family: arial, sans-serif;
|
||||
font-size: 13px;
|
||||
line-height: 14px;
|
||||
|
||||
.chat-participant-image {
|
||||
width: 15%;
|
||||
float: left;
|
||||
overflow: hidden;
|
||||
color: #BBB;
|
||||
padding-top: 2px;
|
||||
}
|
||||
.chat-participant-image img {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 18px;
|
||||
}
|
||||
.chat-participant-name {
|
||||
width: 53%;
|
||||
float: left;
|
||||
font-size: 13px;
|
||||
font-weight: bold;
|
||||
margin-top: 12px;
|
||||
padding: 2px 8px 0;
|
||||
text-align: left;
|
||||
}
|
||||
.chat-participant-invite-call,
|
||||
.chat-participant-invite-join
|
||||
{
|
||||
float: right;
|
||||
background: #4FC059 url(<%= asset_path 'invitepeer16.png' %>) no-repeat center center;
|
||||
}
|
||||
.chat-participant-invite-call.pending,
|
||||
.chat-participant-invite-join.pending {
|
||||
background: #dab539 url(<%= asset_path 'ellipsis.gif' %>) no-repeat center center;
|
||||
}
|
||||
.chat-participant-participating {
|
||||
float: right;
|
||||
margin-top: 14px;
|
||||
}
|
||||
.chat-participant-participating .green-dot {
|
||||
background: #4fc059;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 6px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.chat-header {
|
||||
width: 276px;
|
||||
padding: 16px 8px 16px 16px;
|
||||
font-size: 16px;
|
||||
text-align: left;
|
||||
font-weight: bold;
|
||||
background-color: #000000;
|
||||
color: #f5f5f5;
|
||||
box-shadow: 0px 6px 3px rgba(0, 0, 0, 0.23);
|
||||
|
||||
.sound-toggle {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
margin-right: 10px;
|
||||
margin-top: -2px;
|
||||
float: right;
|
||||
background: url(<%= asset_path 'sound_sprite.png' %>) no-repeat;
|
||||
}
|
||||
.sound-toggle:hover {
|
||||
background-position-x: -24px;
|
||||
}
|
||||
.sound-toggle.active {
|
||||
background-position-y: -24px;
|
||||
}
|
||||
}
|
||||
|
||||
$chat_font_size: 16px;
|
||||
|
||||
.chat-input {
|
||||
min-height: 80px;
|
||||
width: 88%;
|
||||
padding: 8px 9% 8px 3%;
|
||||
font-size: $chat_font_size;
|
||||
outline: none;
|
||||
resize: none;
|
||||
}
|
||||
|
||||
.chat-messages {
|
||||
width: 100%;
|
||||
padding: 16px 0px;
|
||||
overflow-y: auto;
|
||||
flex-grow: 1;
|
||||
|
||||
.chat-message {
|
||||
width: 89%;
|
||||
padding: 8px 8px 2px 8px;
|
||||
color: #f5f5f5;
|
||||
font-family: arial, sans-serif;
|
||||
font-size: $chat_font_size;
|
||||
line-height: $chat_font_size + 1px;
|
||||
|
||||
a:link {
|
||||
color: #4fb5c0;
|
||||
text-decoration: underline;
|
||||
}
|
||||
a:visited {
|
||||
color: #aea9fd;
|
||||
text-decoration: underline;
|
||||
}
|
||||
a:hover {
|
||||
color: #dab539;
|
||||
text-decoration: underline;
|
||||
}
|
||||
.chat-message-user {
|
||||
width: 12%;
|
||||
float: left;
|
||||
overflow: hidden;
|
||||
color: #BBB;
|
||||
padding-top: 2px;
|
||||
}
|
||||
.chat-message-user img {
|
||||
border: 2px solid #424242;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: 16px;
|
||||
}
|
||||
.chat-message-meta {
|
||||
padding: 0 8px;
|
||||
float: left;
|
||||
}
|
||||
.chat-message-username {
|
||||
color: #4fc059;
|
||||
}
|
||||
.chat-message-text {
|
||||
width: 80%;
|
||||
float: left;
|
||||
padding: 2px 8px 0;
|
||||
text-align: left;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
.chat-message-time {
|
||||
font-size: 12px;
|
||||
color: #757575;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.new-message-area {
|
||||
position: relative;
|
||||
|
||||
.emoji-mart {
|
||||
position: absolute;
|
||||
bottom: 98px;
|
||||
}
|
||||
|
||||
.extra-message-options {
|
||||
height: 20px;
|
||||
position: absolute;
|
||||
right: 2px;
|
||||
bottom: 74px;
|
||||
|
||||
.emoji-picker-button {
|
||||
font-size: 16px;
|
||||
line-height: 20px;
|
||||
cursor: pointer;
|
||||
padding: 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -211,6 +211,16 @@
|
|||
|
||||
span.creatorName {
|
||||
margin-left: 8px;
|
||||
max-width: 162px;
|
||||
display: inline-block;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.creatorAndPerm.cardHasViewOnly span.creatorName {
|
||||
max-width: 95px;
|
||||
}
|
||||
|
||||
.cardViewOnly {
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
# frozen_string_literal: true
|
||||
module ApplicationCable
|
||||
class Channel < ActionCable::Channel::Base
|
||||
end
|
||||
|
|
|
@ -1,8 +1,17 @@
|
|||
# frozen_string_literal: true
|
||||
class MapChannel < ApplicationCable::Channel
|
||||
# Called when the consumer has successfully
|
||||
# become a subscriber of this channel.
|
||||
def subscribed
|
||||
return unless Pundit.policy(current_user, Map.find(params[:id])).show?
|
||||
map = Map.find(params[:id])
|
||||
return unless Pundit.policy(current_user, map).show?
|
||||
stream_from "map_#{params[:id]}"
|
||||
Events::UserPresentOnMap.publish!(map, current_user)
|
||||
end
|
||||
|
||||
def unsubscribed
|
||||
map = Map.find(params[:id])
|
||||
return unless Pundit.policy(current_user, map).show?
|
||||
Events::UserNotPresentOnMap.publish!(map, current_user)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
# frozen_string_literal: true
|
||||
class AccessController < ApplicationController
|
||||
before_action :require_user, only: [:access, :access_request, :approve_access, :approve_access_post,
|
||||
before_action :require_user, only: [:access, :access_request,
|
||||
:approve_access, :approve_access_post,
|
||||
:deny_access, :deny_access_post, :request_access]
|
||||
before_action :set_map, only: [:access, :access_request, :approve_access, :approve_access_post,
|
||||
before_action :set_map, only: [:access, :access_request,
|
||||
:approve_access, :approve_access_post,
|
||||
:deny_access, :deny_access_post, :request_access]
|
||||
after_action :verify_authorized
|
||||
|
||||
|
|
|
@ -5,6 +5,7 @@ module Api
|
|||
include Pundit
|
||||
include PunditExtra
|
||||
|
||||
protect_from_forgery with: :exception
|
||||
snorlax_used_rest!
|
||||
|
||||
before_action :load_resource, only: [:show, :update, :destroy]
|
||||
|
@ -86,7 +87,7 @@ module Api
|
|||
|
||||
def token_user
|
||||
token = params[:access_token]
|
||||
access_token = Token.find_by_token(token)
|
||||
access_token = Token.find_by(token: token)
|
||||
@token_user ||= access_token.user if access_token
|
||||
end
|
||||
|
||||
|
@ -149,19 +150,30 @@ module Api
|
|||
|
||||
# override this method to explicitly set searchable columns
|
||||
def searchable_columns
|
||||
return @searchable_columns unless @searchable_columns.nil?
|
||||
|
||||
columns = resource_class.columns.select do |column|
|
||||
column.type == :text || column.type == :string
|
||||
end
|
||||
columns.map(&:name)
|
||||
@searchable_columns = columns.map(&:name)
|
||||
end
|
||||
|
||||
# e.g. ?q=test&searchfields=name,desc
|
||||
def searchfields
|
||||
return searchable_columns if params[:searchfields].blank?
|
||||
|
||||
searchfields = params[:searchfields].split(',')
|
||||
searchfields.select! { |f| searchable_columns.include?(f.to_sym) }
|
||||
searchfields.empty? ? searchable_columns : searchfields
|
||||
end
|
||||
|
||||
# thanks to http://stackoverflow.com/questions/4430578
|
||||
def search_by_q(collection)
|
||||
table = resource_class.arel_table
|
||||
safe_query = "%#{params[:q].gsub(/[%_]/, '\\\\\0')}%"
|
||||
search_column = -> (column) { table[column].matches(safe_query) }
|
||||
search_column = ->(column) { table[column].matches(safe_query) }
|
||||
|
||||
condition = searchable_columns.reduce(nil) do |prev, column|
|
||||
condition = searchfields.reduce(nil) do |prev, column|
|
||||
next search_column.call(column) if prev.nil?
|
||||
search_column.call(column).or(prev)
|
||||
end
|
||||
|
|
|
@ -5,7 +5,6 @@ class HacksController < ApplicationController
|
|||
include ActionView::Helpers::TextHelper # string truncate method
|
||||
|
||||
# rate limited by rack-attack - currently 5r/s
|
||||
# TODO: what else can we do to make get_with_redirects safer?
|
||||
def load_url_title
|
||||
authorize :Hack
|
||||
url = params[:url]
|
||||
|
|
|
@ -20,6 +20,7 @@ class MapsController < ApplicationController
|
|||
end
|
||||
format.json { render json: @map }
|
||||
format.csv { redirect_to action: :export, format: :csv }
|
||||
format.ttl { redirect_to action: :export, format: :ttl }
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -90,10 +91,12 @@ class MapsController < ApplicationController
|
|||
|
||||
# GET maps/:id/export
|
||||
def export
|
||||
exporter = MapExportService.new(current_user, @map)
|
||||
exporter = MapExportService.new(current_user, @map, base_url: request.base_url)
|
||||
|
||||
respond_to do |format|
|
||||
format.json { render json: exporter.json }
|
||||
format.csv { send_data exporter.csv }
|
||||
format.ttl { render text: exporter.rdf }
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -103,9 +106,6 @@ class MapsController < ApplicationController
|
|||
if params[:event] == 'conversation'
|
||||
Events::ConversationStartedOnMap.publish!(@map, current_user)
|
||||
valid_event = true
|
||||
elsif params[:event] == 'user_presence'
|
||||
Events::UserPresentOnMap.publish!(@map, current_user)
|
||||
valid_event = true
|
||||
end
|
||||
|
||||
respond_to do |format|
|
||||
|
|
|
@ -55,8 +55,13 @@ class MetacodeSetsController < ApplicationController
|
|||
@metacodes.each do |m|
|
||||
InMetacodeSet.create(metacode_id: m, metacode_set_id: @metacode_set.id)
|
||||
end
|
||||
format.html { redirect_to metacode_sets_url, notice: 'Metacode set was successfully created.' }
|
||||
format.json { render json: @metacode_set, status: :created, location: metacode_sets_url }
|
||||
format.html do
|
||||
redirect_to metacode_sets_url,
|
||||
notice: 'Metacode set was successfully created.'
|
||||
end
|
||||
format.json do
|
||||
render json: @metacode_set, status: :created, location: metacode_sets_url
|
||||
end
|
||||
else
|
||||
format.html { render action: 'new' }
|
||||
format.json { render json: @metacode_set.errors, status: :unprocessable_entity }
|
||||
|
@ -73,20 +78,20 @@ class MetacodeSetsController < ApplicationController
|
|||
if @metacode_set.update_attributes(metacode_set_params)
|
||||
|
||||
# build an array of the IDs of the metacodes currently in the set
|
||||
@currentMetacodes = @metacode_set.metacodes.map { |m| m.id.to_s }
|
||||
current_metacodes = @metacode_set.metacodes.map { |m| m.id.to_s }
|
||||
# get the list of desired metacodes for the set from the user input and build an array out of it
|
||||
@newMetacodes = params[:metacodes][:value].split(',')
|
||||
new_metacodes = params[:metacodes][:value].split(',')
|
||||
|
||||
# remove the metacodes that were in it, but now aren't
|
||||
@removedMetacodes = @currentMetacodes - @newMetacodes
|
||||
@removedMetacodes.each do |m|
|
||||
@inmetacodeset = InMetacodeSet.find_by_metacode_id_and_metacode_set_id(m, @metacode_set.id)
|
||||
@inmetacodeset.destroy
|
||||
removed_metacodes = current_metacodes - new_metacodes
|
||||
removed_metacodes.each do |m|
|
||||
inmetacodeset = InMetacodeSet.find_by(metacode_id: m, metacode_set_id: @metacode_set.id)
|
||||
inmetacodeset.destroy
|
||||
end
|
||||
|
||||
# add the new metacodes
|
||||
@addedMetacodes = @newMetacodes - @currentMetacodes
|
||||
@addedMetacodes.each do |m|
|
||||
added_metacodes = new_metacodes - current_metacodes
|
||||
added_metacodes.each do |m|
|
||||
InMetacodeSet.create(metacode_id: m, metacode_set_id: @metacode_set.id)
|
||||
end
|
||||
|
||||
|
|
|
@ -14,7 +14,8 @@ class SearchController < ApplicationController
|
|||
term = params[:term]
|
||||
user = params[:user] ? params[:user] : false
|
||||
|
||||
if term && !term.empty? && term.downcase[0..3] != 'map:' && term.downcase[0..6] != 'mapper:' && !term.casecmp('topic:').zero?
|
||||
if term.present? && term.downcase[0..3] != 'map:' &&
|
||||
term.downcase[0..6] != 'mapper:' && !term.casecmp('topic:').zero?
|
||||
|
||||
# remove "topic:" if appended at beginning
|
||||
term = term[6..-1] if term.downcase[0..5] == 'topic:'
|
||||
|
@ -34,28 +35,28 @@ class SearchController < ApplicationController
|
|||
end
|
||||
|
||||
# check whether there's a filter by metacode as part of the query
|
||||
filterByMetacode = false
|
||||
filter_by_metacode = false
|
||||
Metacode.all.each do |m|
|
||||
lOne = m.name.length + 1
|
||||
lTwo = m.name.length
|
||||
length_one = m.name.length + 1
|
||||
length_two = m.name.length
|
||||
|
||||
if term.downcase[0..lTwo] == m.name.downcase + ':'
|
||||
term = term[lOne..-1]
|
||||
filterByMetacode = m
|
||||
if term.downcase[0..length_two] == m.name.downcase + ':'
|
||||
term = term[length_one..-1]
|
||||
filter_by_metacode = m
|
||||
end
|
||||
end
|
||||
|
||||
search = '%' + term.downcase.strip + '%'
|
||||
builder = policy_scope(Topic)
|
||||
|
||||
if filterByMetacode
|
||||
if filter_by_metacode
|
||||
if term == ''
|
||||
builder = builder.none
|
||||
else
|
||||
builder = builder.where('LOWER("name") like ? OR
|
||||
LOWER("desc") like ? OR
|
||||
LOWER("link") like ?', search, search, search)
|
||||
builder = builder.where(metacode_id: filterByMetacode.id)
|
||||
builder = builder.where(metacode_id: filter_by_metacode.id)
|
||||
end
|
||||
elsif desc
|
||||
builder = builder.where('LOWER("desc") like ?', search)
|
||||
|
@ -82,7 +83,8 @@ class SearchController < ApplicationController
|
|||
term = params[:term]
|
||||
user = params[:user] ? params[:user] : nil
|
||||
|
||||
if term && !term.empty? && term.downcase[0..5] != 'topic:' && term.downcase[0..6] != 'mapper:' && !term.casecmp('map:').zero?
|
||||
if term.present? && term.downcase[0..5] != 'topic:' &&
|
||||
term.downcase[0..6] != 'mapper:' && !term.casecmp('map:').zero?
|
||||
|
||||
# remove "map:" if appended at beginning
|
||||
term = term[4..-1] if term.downcase[0..3] == 'map:'
|
||||
|
@ -115,7 +117,8 @@ class SearchController < ApplicationController
|
|||
# get /search/mappers?term=SOMETERM
|
||||
def mappers
|
||||
term = params[:term]
|
||||
if term && !term.empty? && term.downcase[0..3] != 'map:' && term.downcase[0..5] != 'topic:' && !term.casecmp('mapper:').zero?
|
||||
if term.present? && term.downcase[0..3] != 'map:' &&
|
||||
term.downcase[0..5] != 'topic:' && !term.casecmp('mapper:').zero?
|
||||
|
||||
# remove "mapper:" if appended at beginning
|
||||
term = term[7..-1] if term.downcase[0..6] == 'mapper:'
|
||||
|
@ -138,13 +141,15 @@ class SearchController < ApplicationController
|
|||
topic2id = params[:topic2id]
|
||||
|
||||
if term && !term.empty?
|
||||
@synapses = policy_scope(Synapse).where('LOWER("desc") like ?', '%' + term.downcase.strip + '%').order('"desc"')
|
||||
@synapses = policy_scope(Synapse)
|
||||
.where('LOWER("desc") like ?', '%' + term.downcase.strip + '%')
|
||||
.order('"desc"')
|
||||
|
||||
@synapses = @synapses.uniq(&:desc)
|
||||
elsif topic1id && !topic1id.empty?
|
||||
@one = policy_scope(Synapse).where(topic1_id: topic1id, topic2_id: topic2id)
|
||||
@two = policy_scope(Synapse).where(topic2_id: topic1id, topic1_id: topic2id)
|
||||
@synapses = @one + @two
|
||||
one = policy_scope(Synapse).where(topic1_id: topic1id, topic2_id: topic2id)
|
||||
two = policy_scope(Synapse).where(topic2_id: topic1id, topic1_id: topic2id)
|
||||
@synapses = one + two
|
||||
@synapses.sort! { |s1, s2| s1.desc <=> s2.desc }.to_a
|
||||
else
|
||||
skip_policy_scope
|
||||
|
|
|
@ -71,6 +71,8 @@ class SynapsesController < ApplicationController
|
|||
private
|
||||
|
||||
def synapse_params
|
||||
params.require(:synapse).permit(:id, :desc, :category, :weight, :permission, :topic1_id, :topic2_id, :user_id)
|
||||
params.require(:synapse).permit(
|
||||
:id, :desc, :category, :weight, :permission, :topic1_id, :topic2_id, :user_id
|
||||
)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -11,16 +11,20 @@ class TopicsController < ApplicationController
|
|||
def autocomplete_topic
|
||||
term = params[:term]
|
||||
if term && !term.empty?
|
||||
@topics = policy_scope(Topic).where('LOWER("name") like ?', term.downcase + '%').order('"name"')
|
||||
@mapTopics = @topics.select { |t| t&.metacode&.name == 'Metamap' }
|
||||
topics = policy_scope(Topic)
|
||||
.where('LOWER("name") like ?', term.downcase + '%')
|
||||
.order('"name"')
|
||||
map_topics = topics.select { |t| t&.metacode&.name == 'Metamap' }
|
||||
# prioritize topics which point to maps, over maps
|
||||
@exclude = @mapTopics.length.positive? ? @mapTopics.map(&:name) : ['']
|
||||
@maps = policy_scope(Map).where('LOWER("name") like ? AND name NOT IN (?)', term.downcase + '%', @exclude).order('"name"')
|
||||
exclude = map_topics.length.positive? ? map_topics.map(&:name) : ['']
|
||||
maps = policy_scope(Map)
|
||||
.where('LOWER("name") like ? AND name NOT IN (?)', term.downcase + '%', exclude)
|
||||
.order('"name"')
|
||||
else
|
||||
@topics = []
|
||||
@maps = []
|
||||
topics = []
|
||||
maps = []
|
||||
end
|
||||
@all = @topics.to_a.concat(@maps.to_a).sort_by(&:name)
|
||||
@all = topics.to_a.concat(maps.to_a).sort_by(&:name)
|
||||
|
||||
render json: autocomplete_array_json(@all).to_json
|
||||
end
|
||||
|
@ -70,13 +74,13 @@ class TopicsController < ApplicationController
|
|||
@topic = Topic.find(params[:id])
|
||||
authorize @topic
|
||||
|
||||
topicsAlreadyHas = params[:network] ? params[:network].split(',').map(&:to_i) : []
|
||||
topics_already_has = params[:network] ? params[:network].split(',').map(&:to_i) : []
|
||||
|
||||
alltopics = policy_scope(Topic.relatives(@topic.id, current_user)).to_a
|
||||
alltopics.delete_if { |topic| topic.metacode_id != params[:metacode].to_i } if params[:metacode].present?
|
||||
alltopics.delete_if do |topic|
|
||||
!topicsAlreadyHas.index(topic.id).nil?
|
||||
if params[:metacode].present?
|
||||
alltopics.delete_if { |topic| topic.metacode_id != params[:metacode].to_i }
|
||||
end
|
||||
alltopics.delete_if { |topic| !topics_already_has.index(topic.id).nil? }
|
||||
|
||||
@json = Hash.new(0)
|
||||
alltopics.each do |t|
|
||||
|
@ -93,12 +97,14 @@ class TopicsController < ApplicationController
|
|||
@topic = Topic.find(params[:id])
|
||||
authorize @topic
|
||||
|
||||
topicsAlreadyHas = params[:network] ? params[:network].split(',').map(&:to_i) : []
|
||||
topics_already_has = params[:network] ? params[:network].split(',').map(&:to_i) : []
|
||||
|
||||
alltopics = policy_scope(Topic.relatives(@topic.id, current_user)).to_a
|
||||
alltopics.delete_if { |topic| topic.metacode_id != params[:metacode].to_i } if params[:metacode].present?
|
||||
if params[:metacode].present?
|
||||
alltopics.delete_if { |topic| topic.metacode_id != params[:metacode].to_i }
|
||||
end
|
||||
alltopics.delete_if do |topic|
|
||||
!topicsAlreadyHas.index(topic.id.to_s).nil?
|
||||
!topics_already_has.index(topic.id.to_s).nil?
|
||||
end
|
||||
|
||||
# find synapses between topics in alltopics array
|
||||
|
@ -108,9 +114,9 @@ class TopicsController < ApplicationController
|
|||
!synapse_ids.index(synapse.id).nil?
|
||||
end
|
||||
|
||||
creatorsAlreadyHas = params[:creators] ? params[:creators].split(',').map(&:to_i) : []
|
||||
creators_already_has = params[:creators] ? params[:creators].split(',').map(&:to_i) : []
|
||||
allcreators = (alltopics.map(&:user) + allsynapses.map(&:user)).uniq.delete_if do |user|
|
||||
!creatorsAlreadyHas.index(user.id).nil?
|
||||
!creators_already_has.index(user.id).nil?
|
||||
end
|
||||
|
||||
@json = {}
|
||||
|
|
|
@ -1,12 +1,14 @@
|
|||
# frozen_string_literal: true
|
||||
class Users::PasswordsController < Devise::PasswordsController
|
||||
protected
|
||||
module Users
|
||||
class PasswordsController < Devise::PasswordsController
|
||||
protected
|
||||
|
||||
def after_resetting_password_path_for(resource)
|
||||
signed_in_root_path(resource)
|
||||
end
|
||||
def after_resetting_password_path_for(resource)
|
||||
signed_in_root_path(resource)
|
||||
end
|
||||
|
||||
def after_sending_reset_password_instructions_path_for(_resource_name)
|
||||
sign_in_path if is_navigational_format?
|
||||
def after_sending_reset_password_instructions_path_for(_resource_name)
|
||||
sign_in_path if is_navigational_format?
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,37 +1,39 @@
|
|||
# frozen_string_literal: true
|
||||
class Users::RegistrationsController < Devise::RegistrationsController
|
||||
before_action :configure_sign_up_params, only: [:create]
|
||||
before_action :configure_account_update_params, only: [:update]
|
||||
after_action :store_location, only: [:new]
|
||||
module Users
|
||||
class RegistrationsController < Devise::RegistrationsController
|
||||
before_action :configure_sign_up_params, only: [:create]
|
||||
before_action :configure_account_update_params, only: [:update]
|
||||
after_action :store_location, only: [:new]
|
||||
|
||||
protected
|
||||
protected
|
||||
|
||||
def after_update_path_for(resource)
|
||||
signed_in_root_path(resource)
|
||||
end
|
||||
def after_update_path_for(resource)
|
||||
signed_in_root_path(resource)
|
||||
end
|
||||
|
||||
def after_sign_in_path_for(resource)
|
||||
stored = stored_location_for(User)
|
||||
return stored if stored
|
||||
def after_sign_in_path_for(resource)
|
||||
stored = stored_location_for(User)
|
||||
return stored if stored
|
||||
|
||||
if request.referer&.match(sign_in_url) || request.referer&.match(sign_up_url)
|
||||
super
|
||||
else
|
||||
request.referer || root_path
|
||||
if request.referer&.match(sign_in_url) || request.referer&.match(sign_up_url)
|
||||
super
|
||||
else
|
||||
request.referer || root_path
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def store_location
|
||||
store_location_for(User, params[:redirect_to]) if params[:redirect_to]
|
||||
end
|
||||
|
||||
def configure_sign_up_params
|
||||
devise_parameter_sanitizer.permit(:sign_up, keys: [:name, :joinedwithcode])
|
||||
end
|
||||
|
||||
def configure_account_update_params
|
||||
devise_parameter_sanitizer.permit(:account_update, keys: [:image])
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def store_location
|
||||
store_location_for(User, params[:redirect_to]) if params[:redirect_to]
|
||||
end
|
||||
|
||||
def configure_sign_up_params
|
||||
devise_parameter_sanitizer.permit(:sign_up, keys: [:name, :joinedwithcode])
|
||||
end
|
||||
|
||||
def configure_account_update_params
|
||||
devise_parameter_sanitizer.permit(:account_update, keys: [:image])
|
||||
end
|
||||
end
|
||||
|
|
|
@ -2,12 +2,11 @@
|
|||
module ApplicationHelper
|
||||
def metacodeset
|
||||
metacodes = current_user.settings.metacodes
|
||||
|
||||
return false unless metacodes[0].include?('metacodeset')
|
||||
if metacodes[0].sub('metacodeset-', '') == 'Most'
|
||||
return 'Most'
|
||||
elsif metacodes[0].sub('metacodeset-', '') == 'Recent'
|
||||
return 'Recent'
|
||||
end
|
||||
return 'Most' if metacodes[0].sub('metacodeset-', '') == 'Most'
|
||||
return 'Recent' if metacodes[0].sub('metacodeset-', '') == 'Recent'
|
||||
|
||||
MetacodeSet.find(metacodes[0].sub('metacodeset-', '').to_i)
|
||||
end
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@ module TopicsHelper
|
|||
def autocomplete_array_json(topics)
|
||||
topics.map do |t|
|
||||
is_map = t.is_a?(Map)
|
||||
metamapMetacode = Metacode.find_by_name('Metamap')
|
||||
metamap_metacode = Metacode.find_by(name: 'Metamap')
|
||||
{
|
||||
id: t.id,
|
||||
label: t.name,
|
||||
|
@ -17,8 +17,8 @@ module TopicsHelper
|
|||
rtype: is_map ? 'map' : 'topic',
|
||||
inmaps: is_map ? [] : t.inmaps(current_user),
|
||||
inmapsLinks: is_map ? [] : t.inmapsLinks(current_user),
|
||||
type: is_map ? metamapMetacode.name : t.metacode.name,
|
||||
typeImageURL: is_map ? metamapMetacode.icon : t.metacode.icon,
|
||||
type: is_map ? metamap_metacode.name : t.metacode.name,
|
||||
typeImageURL: is_map ? metamap_metacode.icon : t.metacode.icon,
|
||||
mapCount: is_map ? 0 : t.maps.count,
|
||||
synapseCount: is_map ? 0 : t.synapses.count
|
||||
}
|
||||
|
|
|
@ -12,7 +12,7 @@ class AccessRequest < ApplicationRecord
|
|||
Mailboxer::Receipt.where(notification: notification).update_all(is_read: true)
|
||||
end
|
||||
|
||||
user_map = UserMap.create(user: user, map: map)
|
||||
UserMap.create(user: user, map: map)
|
||||
NotificationService.access_approved(self)
|
||||
end
|
||||
|
||||
|
|
38
app/models/attachment.rb
Normal file
38
app/models/attachment.rb
Normal file
|
@ -0,0 +1,38 @@
|
|||
# frozen_string_literal: true
|
||||
class Attachment < ApplicationRecord
|
||||
belongs_to :attachable, polymorphic: true
|
||||
|
||||
has_attached_file :file,
|
||||
styles: lambda { |a|
|
||||
if a.instance.image?
|
||||
{
|
||||
thumb: 'x128#',
|
||||
medium: 'x320>'
|
||||
}
|
||||
else
|
||||
{}
|
||||
end
|
||||
}
|
||||
|
||||
validates_attachment_content_type :file, content_type: Attachable.allowed_types
|
||||
|
||||
def image?
|
||||
Attachable.image_types.include?(file.instance.file_content_type)
|
||||
end
|
||||
|
||||
def audio?
|
||||
Attachable.audio_types.include?(file.instance.file_content_type)
|
||||
end
|
||||
|
||||
def text?
|
||||
Attachable.text_types.include?(file.instance.file_content_type)
|
||||
end
|
||||
|
||||
def pdf?
|
||||
Attachable.pdf_types.include?(file.instance.file_content_type)
|
||||
end
|
||||
|
||||
def document?
|
||||
text? || pdf?
|
||||
end
|
||||
end
|
50
app/models/concerns/attachable.rb
Normal file
50
app/models/concerns/attachable.rb
Normal file
|
@ -0,0 +1,50 @@
|
|||
# frozen_string_literal: true
|
||||
module Attachable
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
included do
|
||||
has_many :attachments, as: :attachable, dependent: :destroy
|
||||
end
|
||||
|
||||
def images
|
||||
attachments.where(file_content_type: image_types)
|
||||
end
|
||||
|
||||
def audios
|
||||
attachments.where(file_content_type: audio_types)
|
||||
end
|
||||
|
||||
def texts
|
||||
attachments.where(file_content_type: text_types)
|
||||
end
|
||||
|
||||
def pdfs
|
||||
attachments.where(file_content_type: pdf_types)
|
||||
end
|
||||
|
||||
def documents
|
||||
attachments.where(file_content_type: text_types + pdf_types)
|
||||
end
|
||||
|
||||
class << self
|
||||
def image_types
|
||||
['image/png', 'image/gif', 'image/jpeg']
|
||||
end
|
||||
|
||||
def audio_types
|
||||
['audio/ogg', 'audio/mp3']
|
||||
end
|
||||
|
||||
def text_types
|
||||
['text/plain']
|
||||
end
|
||||
|
||||
def pdf_types
|
||||
['application/pdf']
|
||||
end
|
||||
|
||||
def allowed_types
|
||||
image_types + audio_types + text_types + pdf_types
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,9 +1,10 @@
|
|||
# frozen_string_literal: true
|
||||
class Event < ApplicationRecord
|
||||
KINDS = %w(user_present_on_map conversation_started_on_map
|
||||
topic_added_to_map topic_moved_on_map topic_removed_from_map
|
||||
synapse_added_to_map synapse_removed_from_map
|
||||
topic_updated synapse_updated).freeze
|
||||
KINDS = %w(user_present_on_map user_not_present_on_map
|
||||
conversation_started_on_map
|
||||
topic_added_to_map topic_moved_on_map topic_removed_from_map
|
||||
synapse_added_to_map synapse_removed_from_map
|
||||
topic_updated synapse_updated).freeze
|
||||
|
||||
belongs_to :eventable, polymorphic: true
|
||||
belongs_to :map
|
||||
|
|
|
@ -1,11 +1,13 @@
|
|||
# frozen_string_literal: true
|
||||
class Events::ConversationStartedOnMap < Event
|
||||
# after_create :notify_users!
|
||||
module Events
|
||||
class ConversationStartedOnMap < Event
|
||||
# after_create :notify_users!
|
||||
|
||||
def self.publish!(map, user)
|
||||
create!(kind: 'conversation_started_on_map',
|
||||
eventable: map,
|
||||
map: map,
|
||||
user: user)
|
||||
def self.publish!(map, user)
|
||||
create!(kind: 'conversation_started_on_map',
|
||||
eventable: map,
|
||||
map: map,
|
||||
user: user)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,12 +1,14 @@
|
|||
# frozen_string_literal: true
|
||||
class Events::SynapseAddedToMap < Event
|
||||
# after_create :notify_users!
|
||||
module Events
|
||||
class SynapseAddedToMap < Event
|
||||
# after_create :notify_users!
|
||||
|
||||
def self.publish!(synapse, map, user, meta)
|
||||
create!(kind: 'synapse_added_to_map',
|
||||
eventable: synapse,
|
||||
map: map,
|
||||
user: user,
|
||||
meta: meta)
|
||||
def self.publish!(synapse, map, user, meta)
|
||||
create!(kind: 'synapse_added_to_map',
|
||||
eventable: synapse,
|
||||
map: map,
|
||||
user: user,
|
||||
meta: meta)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,12 +1,14 @@
|
|||
# frozen_string_literal: true
|
||||
class Events::SynapseRemovedFromMap < Event
|
||||
# after_create :notify_users!
|
||||
module Events
|
||||
class SynapseRemovedFromMap < Event
|
||||
# after_create :notify_users!
|
||||
|
||||
def self.publish!(synapse, map, user, meta)
|
||||
create!(kind: 'synapse_removed_from_map',
|
||||
eventable: synapse,
|
||||
map: map,
|
||||
user: user,
|
||||
meta: meta)
|
||||
def self.publish!(synapse, map, user, meta)
|
||||
create!(kind: 'synapse_removed_from_map',
|
||||
eventable: synapse,
|
||||
map: map,
|
||||
user: user,
|
||||
meta: meta)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,11 +1,13 @@
|
|||
# frozen_string_literal: true
|
||||
class Events::SynapseUpdated < Event
|
||||
# after_create :notify_users!
|
||||
module Events
|
||||
class SynapseUpdated < Event
|
||||
# after_create :notify_users!
|
||||
|
||||
def self.publish!(synapse, user, meta)
|
||||
create!(kind: 'synapse_updated',
|
||||
eventable: synapse,
|
||||
user: user,
|
||||
meta: meta)
|
||||
def self.publish!(synapse, user, meta)
|
||||
create!(kind: 'synapse_updated',
|
||||
eventable: synapse,
|
||||
user: user,
|
||||
meta: meta)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,12 +1,14 @@
|
|||
# frozen_string_literal: true
|
||||
class Events::TopicAddedToMap < Event
|
||||
# after_create :notify_users!
|
||||
module Events
|
||||
class TopicAddedToMap < Event
|
||||
# after_create :notify_users!
|
||||
|
||||
def self.publish!(topic, map, user, meta)
|
||||
create!(kind: 'topic_added_to_map',
|
||||
eventable: topic,
|
||||
map: map,
|
||||
user: user,
|
||||
meta: meta)
|
||||
def self.publish!(topic, map, user, meta)
|
||||
create!(kind: 'topic_added_to_map',
|
||||
eventable: topic,
|
||||
map: map,
|
||||
user: user,
|
||||
meta: meta)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,12 +1,14 @@
|
|||
# frozen_string_literal: true
|
||||
class Events::TopicMovedOnMap < Event
|
||||
# after_create :notify_users!
|
||||
module Events
|
||||
class TopicMovedOnMap < Event
|
||||
# after_create :notify_users!
|
||||
|
||||
def self.publish!(topic, map, user, meta)
|
||||
create!(kind: 'topic_moved_on_map',
|
||||
eventable: topic,
|
||||
map: map,
|
||||
user: user,
|
||||
meta: meta)
|
||||
def self.publish!(topic, map, user, meta)
|
||||
create!(kind: 'topic_moved_on_map',
|
||||
eventable: topic,
|
||||
map: map,
|
||||
user: user,
|
||||
meta: meta)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,12 +1,14 @@
|
|||
# frozen_string_literal: true
|
||||
class Events::TopicRemovedFromMap < Event
|
||||
# after_create :notify_users!
|
||||
module Events
|
||||
class TopicRemovedFromMap < Event
|
||||
# after_create :notify_users!
|
||||
|
||||
def self.publish!(topic, map, user, meta)
|
||||
create!(kind: 'topic_removed_from_map',
|
||||
eventable: topic,
|
||||
map: map,
|
||||
user: user,
|
||||
meta: meta)
|
||||
def self.publish!(topic, map, user, meta)
|
||||
create!(kind: 'topic_removed_from_map',
|
||||
eventable: topic,
|
||||
map: map,
|
||||
user: user,
|
||||
meta: meta)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,11 +1,13 @@
|
|||
# frozen_string_literal: true
|
||||
class Events::TopicUpdated < Event
|
||||
# after_create :notify_users!
|
||||
module Events
|
||||
class TopicUpdated < Event
|
||||
# after_create :notify_users!
|
||||
|
||||
def self.publish!(topic, user, meta)
|
||||
create!(kind: 'topic_updated',
|
||||
eventable: topic,
|
||||
user: user,
|
||||
meta: meta)
|
||||
def self.publish!(topic, user, meta)
|
||||
create!(kind: 'topic_updated',
|
||||
eventable: topic,
|
||||
user: user,
|
||||
meta: meta)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
13
app/models/events/user_not_present_on_map.rB
Normal file
13
app/models/events/user_not_present_on_map.rB
Normal file
|
@ -0,0 +1,13 @@
|
|||
# frozen_string_literal: true
|
||||
module Events
|
||||
class UserNotPresentOnMap < Event
|
||||
# after_create :notify_users!
|
||||
|
||||
def self.publish!(map, user)
|
||||
create!(kind: 'user_not_present_on_map',
|
||||
eventable: map,
|
||||
map: map,
|
||||
user: user)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,11 +1,13 @@
|
|||
# frozen_string_literal: true
|
||||
class Events::UserPresentOnMap < Event
|
||||
# after_create :notify_users!
|
||||
module Events
|
||||
class UserPresentOnMap < Event
|
||||
# after_create :notify_users!
|
||||
|
||||
def self.publish!(map, user)
|
||||
create!(kind: 'user_present_on_map',
|
||||
eventable: map,
|
||||
map: map,
|
||||
user: user)
|
||||
def self.publish!(map, user)
|
||||
create!(kind: 'user_present_on_map',
|
||||
eventable: map,
|
||||
map: map,
|
||||
user: user)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -3,8 +3,10 @@ class Map < ApplicationRecord
|
|||
belongs_to :user
|
||||
belongs_to :source, class_name: :Map
|
||||
|
||||
has_many :topicmappings, -> { Mapping.topicmapping }, class_name: :Mapping, dependent: :destroy
|
||||
has_many :synapsemappings, -> { Mapping.synapsemapping }, class_name: :Mapping, dependent: :destroy
|
||||
has_many :topicmappings, -> { Mapping.topicmapping },
|
||||
class_name: :Mapping, dependent: :destroy
|
||||
has_many :synapsemappings, -> { Mapping.synapsemapping },
|
||||
class_name: :Mapping, dependent: :destroy
|
||||
has_many :topics, through: :topicmappings, source: :mappable, source_type: 'Topic'
|
||||
has_many :synapses, through: :synapsemappings, source: :mappable, source_type: 'Synapse'
|
||||
has_many :messages, as: :resource, dependent: :destroy
|
||||
|
@ -21,7 +23,6 @@ class Map < ApplicationRecord
|
|||
has_attached_file :screenshot,
|
||||
styles: {
|
||||
thumb: ['220x220#', :png]
|
||||
#:full => ['940x630#', :png]
|
||||
},
|
||||
default_url: 'https://s3.amazonaws.com/metamaps-assets/site/missing-map-square.png'
|
||||
|
||||
|
@ -31,7 +32,7 @@ class Map < ApplicationRecord
|
|||
validates :permission, inclusion: { in: Perm::ISSIONS.map(&:to_s) }
|
||||
|
||||
# Validate the attached image is image/jpg, image/png, etc
|
||||
validates_attachment_content_type :screenshot, content_type: /\Aimage\/.*\Z/
|
||||
validates_attachment_content_type :screenshot, content_type: %r{\Aimage/.*\Z}
|
||||
|
||||
after_update :after_updated
|
||||
after_save :update_deferring_topics_and_synapses, if: :permission_changed?
|
||||
|
@ -80,7 +81,12 @@ class Map < ApplicationRecord
|
|||
end
|
||||
|
||||
def as_json(_options = {})
|
||||
json = super(methods: [:user_name, :user_image, :star_count, :topic_count, :synapse_count, :contributor_count, :collaborator_ids, :screenshot_url], except: [:screenshot_content_type, :screenshot_file_size, :screenshot_file_name, :screenshot_updated_at])
|
||||
json = super(
|
||||
methods: [:user_name, :user_image, :star_count, :topic_count, :synapse_count,
|
||||
:contributor_count, :collaborator_ids, :screenshot_url],
|
||||
except: [:screenshot_content_type, :screenshot_file_size, :screenshot_file_name,
|
||||
:screenshot_updated_at]
|
||||
)
|
||||
json[:created_at_clean] = created_at_str
|
||||
json[:updated_at_clean] = updated_at_str
|
||||
json
|
||||
|
@ -120,17 +126,16 @@ class Map < ApplicationRecord
|
|||
end
|
||||
removed.compact
|
||||
end
|
||||
|
||||
|
||||
def after_updated
|
||||
attrs = ['name', 'desc', 'permission']
|
||||
if attrs.any? {|k| changed_attributes.key?(k)}
|
||||
ActionCable.server.broadcast 'map_' + id.to_s, type: 'mapUpdated'
|
||||
end
|
||||
attrs = %w(name desc permission)
|
||||
return unless attrs.any? { |k| changed_attributes.key?(k) }
|
||||
ActionCable.server.broadcast 'map_' + id.to_s, type: 'mapUpdated'
|
||||
end
|
||||
|
||||
def update_deferring_topics_and_synapses
|
||||
Topic.where(defer_to_map_id: id).update_all(permission: permission)
|
||||
Synapse.where(defer_to_map_id: id).update_all(permission: permission)
|
||||
Topic.where(defer_to_map_id: id).update(permission: permission)
|
||||
Synapse.where(defer_to_map_id: id).update(permission: permission)
|
||||
end
|
||||
|
||||
def invited_text
|
||||
|
|
|
@ -27,7 +27,7 @@ class Mapping < ApplicationRecord
|
|||
|
||||
def after_created
|
||||
if mappable_type == 'Topic'
|
||||
meta = {'mapping_id': id}
|
||||
meta = { 'x': xloc, 'y': yloc, 'mapping_id': id }
|
||||
Events::TopicAddedToMap.publish!(mappable, map, user, meta)
|
||||
ActionCable.server.broadcast 'map_' + map.id.to_s, type: 'topicAdded', topic: mappable.filtered, mapping_id: id
|
||||
elsif mappable_type == 'Synapse'
|
||||
|
@ -38,13 +38,14 @@ class Mapping < ApplicationRecord
|
|||
synapse: mappable.filtered,
|
||||
topic1: mappable.topic1.filtered,
|
||||
topic2: mappable.topic2.filtered,
|
||||
mapping_id: id)
|
||||
mapping_id: id
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
def after_updated
|
||||
if mappable_type == 'Topic' and (xloc_changed? or yloc_changed?)
|
||||
meta = {'x': xloc, 'y': yloc, 'mapping_id': id}
|
||||
if (mappable_type == 'Topic') && (xloc_changed? || yloc_changed?)
|
||||
meta = { 'x': xloc, 'y': yloc, 'mapping_id': id }
|
||||
Events::TopicMovedOnMap.publish!(mappable, map, updated_by, meta)
|
||||
ActionCable.server.broadcast 'map_' + map.id.to_s, type: 'topicMoved', id: mappable.id, mapping_id: id, x: xloc, y: yloc
|
||||
end
|
||||
|
@ -57,7 +58,7 @@ class Mapping < ApplicationRecord
|
|||
mappable.save
|
||||
end
|
||||
|
||||
meta = {'mapping_id': id}
|
||||
meta = { 'mapping_id': id }
|
||||
if mappable_type == 'Topic'
|
||||
Events::TopicRemovedFromMap.publish!(mappable, map, updated_by, meta)
|
||||
ActionCable.server.broadcast 'map_' + map.id.to_s, type: 'topicRemoved', id: mappable.id, mapping_id: id
|
||||
|
|
|
@ -4,7 +4,7 @@ class Message < ApplicationRecord
|
|||
belongs_to :resource, polymorphic: true
|
||||
|
||||
delegate :name, to: :user, prefix: true
|
||||
|
||||
|
||||
after_create :after_created
|
||||
|
||||
def user_image
|
||||
|
@ -15,8 +15,8 @@ class Message < ApplicationRecord
|
|||
json = super(methods: [:user_name, :user_image])
|
||||
json
|
||||
end
|
||||
|
||||
|
||||
def after_created
|
||||
ActionCable.server.broadcast 'map_' + resource.id.to_s, type: 'messageCreated', message: self.as_json
|
||||
ActionCable.server.broadcast 'map_' + resource.id.to_s, type: 'messageCreated', message: as_json
|
||||
end
|
||||
end
|
||||
|
|
|
@ -22,6 +22,7 @@ class Synapse < ApplicationRecord
|
|||
where(topic1_id: topic_id).or(where(topic2_id: topic_id))
|
||||
}
|
||||
|
||||
before_create :set_perm_by_defer
|
||||
after_update :after_updated
|
||||
|
||||
delegate :name, to: :user, prefix: true
|
||||
|
@ -51,17 +52,35 @@ class Synapse < ApplicationRecord
|
|||
super(methods: [:user_name, :user_image, :collaborator_ids])
|
||||
end
|
||||
|
||||
def as_rdf
|
||||
output = ''
|
||||
output += %(d:synapse_#{id} a mm:Synapse ;\n)
|
||||
output += %( mm:topic1 d:topic_#{topic1_id} ;\n)
|
||||
output += %( mm:topic2 d:topic_#{topic2_id} ;\n)
|
||||
output += %( mm:direction "#{category}" ;\n)
|
||||
output += %( rdfs:comment "#{desc}" ;\n) if desc.present?
|
||||
output[-2] = '.'
|
||||
output += %(\n)
|
||||
output
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
def set_perm_by_defer
|
||||
permission = defer_to_map.permission if defer_to_map
|
||||
end
|
||||
|
||||
def after_updated
|
||||
attrs = ['desc', 'category', 'permission', 'defer_to_map_id']
|
||||
if attrs.any? {|k| changed_attributes.key?(k)}
|
||||
new = self.attributes.select {|k| attrs.include?(k) }
|
||||
old = changed_attributes.select {|k| attrs.include?(k) }
|
||||
meta = new.merge(old) # we are prioritizing the old values, keeping them
|
||||
meta['changed'] = changed_attributes.keys.select {|k| attrs.include?(k) }
|
||||
attrs = %w(desc category permission defer_to_map_id)
|
||||
if attrs.any? { |k| changed_attributes.key?(k) }
|
||||
new = attributes.select { |k| attrs.include?(k) }
|
||||
old = changed_attributes.select { |k| attrs.include?(k) }
|
||||
meta = new.merge(old) # we are prioritizing the old values, keeping them
|
||||
meta['changed'] = changed_attributes.keys.select { |k| attrs.include?(k) }
|
||||
Events::SynapseUpdated.publish!(self, user, meta)
|
||||
maps.each {|map|
|
||||
maps.each do |map|
|
||||
ActionCable.server.broadcast 'map_' + map.id.to_s, type: 'synapseUpdated', id: id
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
class Topic < ApplicationRecord
|
||||
include TopicsHelper
|
||||
include Attachable
|
||||
|
||||
belongs_to :user
|
||||
belongs_to :defer_to_map, class_name: 'Map', foreign_key: 'defer_to_map_id'
|
||||
|
@ -15,29 +16,13 @@ class Topic < ApplicationRecord
|
|||
|
||||
belongs_to :metacode
|
||||
|
||||
before_create :set_perm_by_defer
|
||||
before_create :create_metamap?
|
||||
after_update :after_updated
|
||||
|
||||
validates :permission, presence: true
|
||||
validates :permission, inclusion: { in: Perm::ISSIONS.map(&:to_s) }
|
||||
|
||||
# This method associates the attribute ":image" with a file attachment
|
||||
has_attached_file :image
|
||||
|
||||
# , styles: {
|
||||
# thumb: '100x100>',
|
||||
# square: '200x200#',
|
||||
# medium: '300x300>'
|
||||
# }
|
||||
|
||||
# Validate the attached image is image/jpg, image/png, etc
|
||||
validates_attachment_content_type :image, content_type: /\Aimage\/.*\Z/
|
||||
|
||||
# This method associates the attribute ":image" with a file attachment
|
||||
has_attached_file :audio
|
||||
# Validate the attached audio is audio/wav, audio/mp3, etc
|
||||
validates_attachment_content_type :audio, content_type: /\Aaudio\/.*\Z/
|
||||
|
||||
def synapses
|
||||
synapses1.or(synapses2)
|
||||
end
|
||||
|
@ -82,6 +67,19 @@ class Topic < ApplicationRecord
|
|||
map_count: map_count(options[:user]), synapse_count: synapse_count(options[:user]))
|
||||
end
|
||||
|
||||
def as_rdf
|
||||
output = ''
|
||||
output += %(d:topic_#{id} a mm:Topic ;\n)
|
||||
output += %( rdfs:label "#{name}" ;\n)
|
||||
output += %( rdfs:comment "#{desc}" ;\n) if desc.present?
|
||||
output += %( foaf:homepage <#{link}> ;\n) if link.present?
|
||||
output += %( mm:mapper d:mapper_#{user_id} ;\n)
|
||||
output += %( mm:metacode "#{metacode.name}" ;\n)
|
||||
output[-2] = '.' # change last ; to a .
|
||||
output += %(\n)
|
||||
output
|
||||
end
|
||||
|
||||
def collaborator_ids
|
||||
if defer_to_map
|
||||
defer_to_map.editors.select { |mapper| mapper != user }.map(&:id)
|
||||
|
@ -137,6 +135,10 @@ class Topic < ApplicationRecord
|
|||
|
||||
protected
|
||||
|
||||
def set_perm_by_defer
|
||||
permission = defer_to_map.permission if defer_to_map
|
||||
end
|
||||
|
||||
def create_metamap?
|
||||
return unless (link == '') && (metacode.name == 'Metamap')
|
||||
|
||||
|
@ -147,16 +149,16 @@ class Topic < ApplicationRecord
|
|||
end
|
||||
|
||||
def after_updated
|
||||
attrs = ['name', 'desc', 'link', 'metacode_id', 'permission', 'defer_to_map_id']
|
||||
if attrs.any? {|k| changed_attributes.key?(k)}
|
||||
new = self.attributes.select {|k| attrs.include?(k) }
|
||||
old = changed_attributes.select {|k| attrs.include?(k) }
|
||||
meta = new.merge(old) # we are prioritizing the old values, keeping them
|
||||
meta['changed'] = changed_attributes.keys.select {|k| attrs.include?(k) }
|
||||
attrs = %w(name desc link metacode_id permission defer_to_map_id)
|
||||
if attrs.any? { |k| changed_attributes.key?(k) }
|
||||
new = attributes.select { |k| attrs.include?(k) }
|
||||
old = changed_attributes.select { |k| attrs.include?(k) }
|
||||
meta = new.merge(old) # we are prioritizing the old values, keeping them
|
||||
meta['changed'] = changed_attributes.keys.select { |k| attrs.include?(k) }
|
||||
Events::TopicUpdated.publish!(self, user, meta)
|
||||
maps.each {|map|
|
||||
maps.each do |map|
|
||||
ActionCable.server.broadcast 'map_' + map.id.to_s, type: 'topicUpdated', id: id
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -89,6 +89,17 @@ class User < ApplicationRecord
|
|||
}.to_a.sort{ |a, b| b[1] <=> a[1] }.map{|i| i[0]}.slice(0, 5)
|
||||
end
|
||||
|
||||
def as_rdf(opts = {})
|
||||
base_url = opts[:base_url] || 'https://metamaps.cc'
|
||||
output = ''
|
||||
output += %(d:mapper_#{id} a foaf:OnlineAccount ;\n)
|
||||
output += %( foaf:accountName "#{name}" ;\n)
|
||||
output += %( foaf:accountServiceHomepage "#{base_url}/mapper/#{id}" ;\n)
|
||||
output[-2] = '.' # change last ; to a .
|
||||
output += %(\n)
|
||||
output
|
||||
end
|
||||
|
||||
def all_accessible_maps
|
||||
maps + shared_maps
|
||||
end
|
||||
|
@ -115,7 +126,7 @@ class User < ApplicationRecord
|
|||
if code == joinedwithcode
|
||||
update(generation: 0)
|
||||
else
|
||||
update(generation: User.find_by_code(joinedwithcode).generation + 1)
|
||||
update(generation: User.find_by(code: joinedwithcode).generation + 1)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -6,7 +6,7 @@ class UserPreference
|
|||
array = []
|
||||
%w(Action Aim Idea Question Note Wildcard Subject).each do |m|
|
||||
begin
|
||||
metacode = Metacode.find_by_name(m)
|
||||
metacode = Metacode.find_by(name: m)
|
||||
array.push(metacode.id.to_s) if metacode
|
||||
rescue ActiveRecord::StatementInvalid
|
||||
if m == 'Action'
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
class Webhooks::Slack::SynapseRemovedFromMap < Webhooks::Slack::Base
|
||||
def text
|
||||
connector = eventable.desc.empty? ? '->' : eventable.desc
|
||||
# todo express correct directionality of arrows when desc is empty
|
||||
# TODO: express correct directionality of arrows when desc is empty
|
||||
"\"*#{eventable.topic1.name}* #{connector} *#{eventable.topic2.name}*\" was removed by *#{event.user.name}* as a connection from the map *#{view_map_on_metamaps}*"
|
||||
end
|
||||
end
|
||||
|
|
|
@ -35,13 +35,29 @@ module Api
|
|||
Pundit.policy_scope(scope[:current_user], object.send(attr))&.map(&:id) || []
|
||||
end
|
||||
has_many(attr, opts.merge(if: -> { embeds.include?(key) })) do
|
||||
Pundit.policy_scope(scope[:current_user], object.send(attr)) || []
|
||||
list = Pundit.policy_scope(scope[:current_user], object.send(attr)) || []
|
||||
child_serializer = "Api::V2::#{attr.to_s.singularize.camelize}Serializer".constantize
|
||||
resource = ActiveModelSerializers::SerializableResource.new(
|
||||
list,
|
||||
each_serializer: child_serializer,
|
||||
scope: scope.merge(embeds: [])
|
||||
)
|
||||
resource.as_json
|
||||
end
|
||||
else
|
||||
id_opts = opts.merge(key: "#{key}_id")
|
||||
attribute("#{attr}_id".to_sym,
|
||||
id_opts.merge(unless: -> { embeds.include?(key) }))
|
||||
attribute(key, opts.merge(if: -> { embeds.include?(key) }))
|
||||
attribute(key, opts.merge(if: -> { embeds.include?(key) })) do |serializer|
|
||||
object = serializer.object.send(key)
|
||||
child_serializer = "Api::V2::#{object.class.name}Serializer".constantize
|
||||
resource = ActiveModelSerializers::SerializableResource.new(
|
||||
object,
|
||||
serializer: child_serializer,
|
||||
scope: scope.merge(embeds: [])
|
||||
)
|
||||
resource.as_json
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
# frozen_string_literal: true
|
||||
class MapExportService
|
||||
attr_reader :user, :map
|
||||
def initialize(user, map)
|
||||
attr_reader :user, :map, :base_url
|
||||
|
||||
def initialize(user, map, opts = {})
|
||||
@user = user
|
||||
@map = map
|
||||
@base_url = opts[:base_url] || 'https://metamaps.cc'
|
||||
end
|
||||
|
||||
def json
|
||||
|
@ -22,6 +24,25 @@ class MapExportService
|
|||
end
|
||||
end
|
||||
|
||||
def rdf
|
||||
output = ''
|
||||
output += "PREFIX d: <#{base_url}/maps/#{map.id}>\n"
|
||||
output += "PREFIX mm: <#{base_url}/owl/map.owl.ttl>\n"
|
||||
output += "PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>\n"
|
||||
output += "PREFIX foaf: <http://xmlns.com/foaf/0.1/>\n"
|
||||
output += "\n"
|
||||
map.contributors.each do |mapper|
|
||||
output += mapper.as_rdf(base_url: base_url)
|
||||
end
|
||||
map.topics.each do |topic|
|
||||
output += topic.as_rdf
|
||||
end
|
||||
map.synapses.each do |synapse|
|
||||
output += synapse.as_rdf
|
||||
end
|
||||
output
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def topic_headings
|
||||
|
|
|
@ -12,7 +12,7 @@
|
|||
Metamaps.currentPage = "mapper";
|
||||
Metamaps.ServerData.Mapper = {
|
||||
models: <%= @maps.to_json.html_safe %>,
|
||||
id: <%= params[:id] %>
|
||||
mapperId: <%= params[:id] %>
|
||||
};
|
||||
Metamaps.GlobalUI.Search.focus();
|
||||
</script>
|
||||
|
|
|
@ -15,6 +15,19 @@
|
|||
<title><%= yield(:title) %></title>
|
||||
<%= csrf_meta_tags %>
|
||||
<meta name="viewport" content="width=device-width, user-scalable=no">
|
||||
|
||||
<% if controller.class.name == 'MapsController' && @map %>
|
||||
<meta property="og:title" content="<%= @map.name %>" />
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:image" content="<%= @map.screenshot_url %>" />
|
||||
<meta property="og:description" content="<%= @map.desc %>" />
|
||||
<meta property="og:url" content="<%= request.original_url %>" />
|
||||
|
||||
<meta name="twitter:title" content="<%= @map.name %>" />
|
||||
<meta name="twitter:image" content="<%= @map.screenshot_url %>" />
|
||||
<meta name="twitter:description" content="<%= @map.desc %>" />
|
||||
<meta name="twitter:url" content="<%= request.original_url %>" />
|
||||
<% end %>
|
||||
|
||||
<%= stylesheet_link_tag "application", :media => "all" %>
|
||||
<%= javascript_include_tag "application" %>
|
||||
|
|
|
@ -34,6 +34,11 @@
|
|||
<p class="mapCreatedAt"><span>Created by:</span> {{user_name}} on {{created_at}}</p>
|
||||
<p class="mapEditedAt"><span>Last edited:</span> {{updated_at}}</p>
|
||||
<div class="mapInfoButtonsWrapper">
|
||||
<div class="mapInfoThumbnail">
|
||||
<div class="thumbnail"></div>
|
||||
<div class="tooltip">Update Thumbnail</div>
|
||||
<span>Thumb</span>
|
||||
</div>
|
||||
<div class="mapInfoDelete">
|
||||
<div class="deleteMap"></div>
|
||||
<span>Delete</span>
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
|
||||
<% if current_user %>
|
||||
<div class="requestTitle">
|
||||
Click here to name this map!
|
||||
Click here to name this map
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
|
@ -77,6 +77,11 @@
|
|||
<p class="mapCreatedAt"><span>Created by:</span> <%= @map.user == user ? "You" : @map.user.name %> on <%= @map.created_at.strftime("%m/%d/%Y") %></p>
|
||||
<p class="mapEditedAt"><span>Last edited:</span> <%= @map.updated_at.strftime("%m/%d/%Y") %></p>
|
||||
<div class="mapInfoButtonsWrapper">
|
||||
<div class="mapInfoThumbnail">
|
||||
<div class="thumbnail"></div>
|
||||
<div class="tooltip">Update Thumbnail</div>
|
||||
<span>Thumb</span>
|
||||
</div>
|
||||
<div class="mapInfoDelete">
|
||||
<div class="deleteMap"></div>
|
||||
<span>Delete</span>
|
||||
|
|
|
@ -29,4 +29,7 @@ Rails.application.configure do
|
|||
# Expands the lines which load the assets
|
||||
config.assets.debug = false
|
||||
config.assets.quiet = true
|
||||
|
||||
# S3 file storage
|
||||
config.paperclip_defaults = {} # store on local machine for dev
|
||||
end
|
||||
|
|
|
@ -3,3 +3,6 @@
|
|||
|
||||
# Add new mime types for use in respond_to blocks:
|
||||
# Mime::Type.register "text/richtext", :rtf
|
||||
|
||||
# RDF export
|
||||
Mime::Type.register 'text/turtle', :ttl
|
||||
|
|
|
@ -1,61 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
class Rack::Attack
|
||||
Rack::Attack.cache.store = ActiveSupport::Cache::MemoryStore.new
|
||||
|
||||
# Throttle all requests by IP (60rpm)
|
||||
#
|
||||
# Key: "rack::attack:#{Time.now.to_i/:period}:req/ip:#{req.ip}"
|
||||
# throttle('req/ip', :limit => 300, :period => 5.minutes) do |req|
|
||||
# req.ip # unless req.path.start_with?('/assets')
|
||||
# end
|
||||
|
||||
# Throttle POST requests to /login by IP address
|
||||
#
|
||||
# Key: "rack::attack:#{Time.now.to_i/:period}:logins/ip:#{req.ip}"
|
||||
throttle('logins/ip', limit: 5, period: 20.seconds) do |req|
|
||||
req.ip if req.path == '/login' && req.post?
|
||||
end
|
||||
|
||||
# Throttle POST requests to /login by email param
|
||||
#
|
||||
# Key: "rack::attack:#{Time.now.to_i/:period}:logins/email:#{req.email}"
|
||||
#
|
||||
# Note: This creates a problem where a malicious user could intentionally
|
||||
# throttle logins for another user and force their login requests to be
|
||||
# denied, but that's not very common and shouldn't happen to you. (Knock
|
||||
# on wood!)
|
||||
throttle('logins/email', limit: 5, period: 20.seconds) do |req|
|
||||
if req.path == '/login' && req.post?
|
||||
# return the email if present, nil otherwise
|
||||
req.params['email'].presence
|
||||
end
|
||||
end
|
||||
|
||||
throttle('load_url_title/req/5mins/ip', limit: 300, period: 5.minutes) do |req|
|
||||
req.ip if req.path == 'hacks/load_url_title'
|
||||
end
|
||||
throttle('load_url_title/req/1s/ip', limit: 5, period: 1.second) do |req|
|
||||
# If the return value is truthy, the cache key for the return value
|
||||
# is incremented and compared with the limit. In this case:
|
||||
# "rack::attack:#{Time.now.to_i/1.second}:load_url_title/req/ip:#{req.ip}"
|
||||
#
|
||||
# If falsy, the cache key is neither incremented nor checked.
|
||||
|
||||
req.ip if req.path == 'hacks/load_url_title'
|
||||
end
|
||||
|
||||
self.throttled_response = lambda do |env|
|
||||
now = Time.now
|
||||
match_data = env['rack.attack.match_data']
|
||||
period = match_data[:period]
|
||||
limit = match_data[:limit]
|
||||
|
||||
headers = {
|
||||
'X-RateLimit-Limit' => limit.to_s,
|
||||
'X-RateLimit-Remaining' => '0',
|
||||
'X-RateLimit-Reset' => (now + (period - now.to_i % period)).to_s
|
||||
}
|
||||
|
||||
[429, headers, ['']]
|
||||
end
|
||||
end
|
63
config/initializers/rack_attack.rb
Normal file
63
config/initializers/rack_attack.rb
Normal file
|
@ -0,0 +1,63 @@
|
|||
# frozen_string_literal: true
|
||||
module Rack
|
||||
class Attack
|
||||
Rack::Attack.cache.store = ActiveSupport::Cache::MemoryStore.new
|
||||
|
||||
# Throttle all requests by IP (60rpm)
|
||||
#
|
||||
# Key: "rack::attack:#{Time.now.to_i/:period}:req/ip:#{req.ip}"
|
||||
# throttle('req/ip', :limit => 300, :period => 5.minutes) do |req|
|
||||
# req.ip # unless req.path.start_with?('/assets')
|
||||
# end
|
||||
|
||||
# Throttle POST requests to /login by IP address
|
||||
#
|
||||
# Key: "rack::attack:#{Time.now.to_i/:period}:logins/ip:#{req.ip}"
|
||||
throttle('logins/ip', limit: 5, period: 20.seconds) do |req|
|
||||
req.ip if req.path == '/login' && req.post?
|
||||
end
|
||||
|
||||
# Throttle POST requests to /login by email param
|
||||
#
|
||||
# Key: "rack::attack:#{Time.now.to_i/:period}:logins/email:#{req.email}"
|
||||
#
|
||||
# Note: This creates a problem where a malicious user could intentionally
|
||||
# throttle logins for another user and force their login requests to be
|
||||
# denied, but that's not very common and shouldn't happen to you. (Knock
|
||||
# on wood!)
|
||||
throttle('logins/email', limit: 5, period: 20.seconds) do |req|
|
||||
if req.path == '/login' && req.post?
|
||||
# return the email if present, nil otherwise
|
||||
req.params['email'].presence
|
||||
end
|
||||
end
|
||||
|
||||
throttle('load_url_title/req/5mins/ip', limit: 300, period: 5.minutes) do |req|
|
||||
req.ip if req.path == 'hacks/load_url_title'
|
||||
end
|
||||
throttle('load_url_title/req/1s/ip', limit: 5, period: 1.second) do |req|
|
||||
# If the return value is truthy, the cache key for the return value
|
||||
# is incremented and compared with the limit. In this case:
|
||||
# "rack::attack:#{Time.now.to_i/1.second}:load_url_title/req/ip:#{req.ip}"
|
||||
#
|
||||
# If falsy, the cache key is neither incremented nor checked.
|
||||
|
||||
req.ip if req.path == 'hacks/load_url_title'
|
||||
end
|
||||
|
||||
self.throttled_response = lambda do |env|
|
||||
now = Time.zone.now
|
||||
match_data = env['rack.attack.match_data']
|
||||
period = match_data[:period]
|
||||
limit = match_data[:limit]
|
||||
|
||||
headers = {
|
||||
'X-RateLimit-Limit' => limit.to_s,
|
||||
'X-RateLimit-Remaining' => '0',
|
||||
'X-RateLimit-Reset' => (now + (period - now.to_i % period)).to_s
|
||||
}
|
||||
|
||||
[429, headers, ['']]
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,4 +1,4 @@
|
|||
# frozen_string_literal: true
|
||||
METAMAPS_VERSION = '3.0.1'
|
||||
METAMAPS_VERSION = '3.2'
|
||||
METAMAPS_BUILD = `git log -1 --pretty=%H`.chomp[0..11].freeze
|
||||
METAMAPS_LAST_UPDATED = `git log -1 --pretty='%ad'`.split(' ').values_at(1, 2, 4).join(' ').freeze
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
Warden::Manager.after_set_user do |user,auth,opts|
|
||||
# frozen_string_literal: true
|
||||
Warden::Manager.after_set_user do |user, auth, opts|
|
||||
scope = opts[:scope]
|
||||
auth.cookies.signed["#{scope}.id"] = user.id
|
||||
auth.cookies.signed["#{scope}.expires_at"] = 30.minutes.from_now
|
||||
end
|
||||
Warden::Manager.before_logout do |user, auth, opts|
|
||||
Warden::Manager.before_logout do |_user, auth, opts|
|
||||
scope = opts[:scope]
|
||||
auth.cookies.signed["#{scope}.id"] = nil
|
||||
auth.cookies.signed["#{scope}.expires_at"] = nil
|
||||
|
|
|
@ -14,6 +14,7 @@ Metamaps::Application.routes.draw do
|
|||
get 'starred'
|
||||
get 'mapper/:id', action: 'mapper'
|
||||
end
|
||||
get :explore, to: redirect('/')
|
||||
|
||||
resources :maps, except: [:index, :edit] do
|
||||
member do
|
||||
|
|
12
db/migrate/20170122201451_create_attachments.rb
Normal file
12
db/migrate/20170122201451_create_attachments.rb
Normal file
|
@ -0,0 +1,12 @@
|
|||
class CreateAttachments < ActiveRecord::Migration[5.0]
|
||||
def change
|
||||
create_table :attachments do |t|
|
||||
t.references :attachable, polymorphic: true
|
||||
t.attachment :file
|
||||
t.timestamps
|
||||
end
|
||||
|
||||
remove_attachment :topics, :image
|
||||
remove_attachment :topics, :audio
|
||||
end
|
||||
end
|
26
db/schema.rb
26
db/schema.rb
|
@ -10,7 +10,7 @@
|
|||
#
|
||||
# It's strongly recommended that you check this file into your version control system.
|
||||
|
||||
ActiveRecord::Schema.define(version: 20161218183817) do
|
||||
ActiveRecord::Schema.define(version: 20170122201451) do
|
||||
|
||||
# These are extensions that must be enabled in order to support this database
|
||||
enable_extension "plpgsql"
|
||||
|
@ -26,6 +26,18 @@ ActiveRecord::Schema.define(version: 20161218183817) do
|
|||
t.index ["user_id"], name: "index_access_requests_on_user_id", using: :btree
|
||||
end
|
||||
|
||||
create_table "attachments", force: :cascade do |t|
|
||||
t.string "attachable_type"
|
||||
t.integer "attachable_id"
|
||||
t.string "file_file_name"
|
||||
t.string "file_content_type"
|
||||
t.integer "file_file_size"
|
||||
t.datetime "file_updated_at"
|
||||
t.datetime "created_at", null: false
|
||||
t.datetime "updated_at", null: false
|
||||
t.index ["attachable_type", "attachable_id"], name: "index_attachments_on_attachable_type_and_attachable_id", using: :btree
|
||||
end
|
||||
|
||||
create_table "delayed_jobs", force: :cascade do |t|
|
||||
t.integer "priority", default: 0, null: false
|
||||
t.integer "attempts", default: 0, null: false
|
||||
|
@ -269,17 +281,9 @@ ActiveRecord::Schema.define(version: 20161218183817) do
|
|||
t.text "link"
|
||||
t.integer "user_id"
|
||||
t.integer "metacode_id"
|
||||
t.datetime "created_at", null: false
|
||||
t.datetime "updated_at", null: false
|
||||
t.datetime "created_at", null: false
|
||||
t.datetime "updated_at", null: false
|
||||
t.text "permission"
|
||||
t.string "image_file_name", limit: 255
|
||||
t.string "image_content_type", limit: 255
|
||||
t.integer "image_file_size"
|
||||
t.datetime "image_updated_at"
|
||||
t.string "audio_file_name", limit: 255
|
||||
t.string "audio_content_type", limit: 255
|
||||
t.integer "audio_file_size"
|
||||
t.datetime "audio_updated_at"
|
||||
t.integer "defer_to_map_id"
|
||||
t.index ["metacode_id"], name: "index_topics_on_metacode_id", using: :btree
|
||||
t.index ["user_id"], name: "index_topics_on_user_id", using: :btree
|
||||
|
|
10
db/seeds.rb
10
db/seeds.rb
|
@ -38,7 +38,7 @@ Metacode.create(name: 'Process',
|
|||
manual_icon: 'https://s3.amazonaws.com/metamaps-assets/metacodes/blueprint/96px/bp_process.png',
|
||||
color: '#BDB25E')
|
||||
|
||||
Metacode.create(name: 'Future',
|
||||
Metacode.create(name: 'Future Dev',
|
||||
manual_icon: 'https://s3.amazonaws.com/metamaps-assets/metacodes/blueprint/96px/bp_futuredev.png',
|
||||
color: '#25A17F')
|
||||
|
||||
|
@ -70,7 +70,7 @@ Metacode.create(name: 'Need',
|
|||
manual_icon: 'https://s3.amazonaws.com/metamaps-assets/metacodes/blueprint/96px/bp_need.png',
|
||||
color: '#D2A7D4')
|
||||
|
||||
Metacode.create(name: 'Open',
|
||||
Metacode.create(name: 'Open Issue',
|
||||
manual_icon: 'https://s3.amazonaws.com/metamaps-assets/metacodes/blueprint/96px/bp_openissue.png',
|
||||
color: '#9BBF71')
|
||||
|
||||
|
@ -142,7 +142,7 @@ Metacode.create(name: 'Aim',
|
|||
manual_icon: 'https://s3.amazonaws.com/metamaps-assets/metacodes/generics/96px/gen_aim.png',
|
||||
color: '#B0B0B0')
|
||||
|
||||
Metacode.create(name: 'Good',
|
||||
Metacode.create(name: 'Good Practice',
|
||||
manual_icon: 'https://s3.amazonaws.com/metamaps-assets/metacodes/generics/96px/gen_goodpractice.png',
|
||||
color: '#BD9E86')
|
||||
|
||||
|
@ -198,6 +198,10 @@ Metacode.create(name: 'Status',
|
|||
manual_icon: 'https://s3.amazonaws.com/metamaps-assets/metacodes/generics/96px/gen_status.png',
|
||||
color: '#EFA7C0')
|
||||
|
||||
Metacode.create(name: 'Story',
|
||||
manual_icon: 'https://s3.amazonaws.com/metamaps-assets/metacodes/generics/96px/gen_story.png',
|
||||
color: '#A7A2DC')
|
||||
|
||||
Metacode.create(name: 'Tool',
|
||||
manual_icon: 'https://s3.amazonaws.com/metamaps-assets/metacodes/generics/96px/gen_tool.png',
|
||||
color: '#828282')
|
||||
|
|
|
@ -4,3 +4,8 @@ queryParameters:
|
|||
Search text columns for this string. A query of <code>"example"</code> will be passed to SQL as <code>LIKE %example%</code>. The searchable columns are: <pre><< searchFields >></pre>
|
||||
required: false
|
||||
type: string
|
||||
searchfields:
|
||||
description: |
|
||||
A comma-seperated list of columns to search. For instance, to search a topic's name and description (but not link field) for the string "cognition", you could use `?q=cognition&searchfields=name,desc`.
|
||||
required: false
|
||||
type: string
|
||||
|
|
|
@ -14,12 +14,28 @@
|
|||
|
||||
#### Setup Postgres
|
||||
|
||||
sudo apt-get install postgresql-9.4 #specify version!!
|
||||
sudo apt-get install postgresql-9.4
|
||||
# make sure you have development headers for postgres. The package name might be different on your distribution.
|
||||
sudo apt-get install libpq-dev
|
||||
sudo -u postgres psql
|
||||
postgres=# CREATE USER metamaps WITH PASSWORD 'mycoolpassword' CREATEDB;
|
||||
postgres=# CREATE DATABASE metamap002_production OWNER metamaps;
|
||||
postgres=# CREATE DATABASE metamaps_production OWNER metamaps;
|
||||
postgres=# \q
|
||||
|
||||
On some deploys, we have had problems with unicode encoding when trying to run `db:setup`. Running the commands in this Github gist resolved the issue: https://gist.github.com/amolkhanorkar/8706915. Try this link if you have problems
|
||||
|
||||
#### Install Node for javascript building
|
||||
|
||||
# this first line lets us use up-to-date versions of node.js
|
||||
# instead of the old versions in the Ubuntu repositories
|
||||
curl -sL https://deb.nodesource.com/setup_7.x | sudo -E bash -
|
||||
sudo apt-get install nodejs
|
||||
sudo ln -s /usr/bin/nodejs /usr/bin/node
|
||||
|
||||
### Install redis server for action cable
|
||||
|
||||
sudo apt-get install redis-server
|
||||
|
||||
#### Install system-wide rvm:
|
||||
|
||||
sudo gpg --keyserver hkp://keys.gnupg.net \
|
||||
|
@ -38,8 +54,15 @@
|
|||
rvm user gemsets
|
||||
git clone https://github.com/metamaps/metamaps \
|
||||
--branch instance/mycoolinstance
|
||||
rvm install $(cat metamaps/.ruby-version) #ensure ruby is installed
|
||||
cd metamaps
|
||||
cat metamaps/.ruby-version
|
||||
|
||||
The last line tells you what version of ruby you need to install. For example, at the time of writing the version is 2.3.0. As your normal sudo-enabled user, run
|
||||
|
||||
sudo rvm install 2.3.0
|
||||
|
||||
Now switch back to the metamaps user and continue
|
||||
|
||||
cd /home/metamaps/metamaps
|
||||
gem install bundler
|
||||
RAILS_ENV=production bundle install
|
||||
|
||||
|
@ -60,16 +83,11 @@ Run this in the metamaps directory, still as metamaps:
|
|||
# create, load schema, seed
|
||||
bundle exec rails db:setup
|
||||
|
||||
#### Install node & ES6 modules
|
||||
|
||||
sudo aptitude install nodejs npm
|
||||
sudo ln -s /usr/bin/nodejs /usr/bin/node
|
||||
npm install
|
||||
|
||||
#### Precompile assets
|
||||
|
||||
This step depends on running npm install first; assets:precompile calls `npm install` and `bin/build-apidocs.sh`, both of which require node_modules to be installed. We suggest you run the commands separately this time to better catch any errors.
|
||||
Note that `rails assets:precompile` will normally call `npm install` and `bin/build-apidocs.sh` as part of its process. Both of these latter commands require `npm install` to be run first. We suggest you run all five commands separately this time (like below) to better catch any errors. In the future, you won't need to run the second and third commands separately.
|
||||
|
||||
npm install
|
||||
npm run build
|
||||
bin/build-apidocs.sh
|
||||
bundle exec rails assets:precompile
|
||||
|
@ -93,17 +111,18 @@ server to see what problems show up:
|
|||
#### Realtime server:
|
||||
|
||||
sudo npm install -g forever
|
||||
(crontab -u metamaps -l 2>/dev/null; echo "@reboot env NODE_REALTIME_PORT=5000 $(which forever) --append -l /home/metamaps/logs/forever.realtime.log start /home/metamaps/metamaps/realtime/realtime-server.js") | crontab -u metamaps -
|
||||
(sudo crontab -u metamaps -l 2>/dev/null; echo "@reboot NODE_REALTIME_PORT=5000 /usr/bin/forever --minUptime 1000 --spinSleepTime 1000 --append -l /home/metamaps/logs/forever.realtime.log -c /home/metamaps/metamaps/node_modules/.bin/babel-node --workingDir /home/metamaps/metamaps start /home/metamaps/metamaps/realtime/realtime-server.js") | sudo crontab -u metamaps
|
||||
|
||||
mkdir -p /home/metamaps/logs
|
||||
env NODE_REALTIME_PORT=5000 forever --append \
|
||||
/usr/bin/forever --minUptime 1000 --spinSleepTime 1000 \
|
||||
--append -l /home/metamaps/logs/forever.realtime.log \
|
||||
-c /home/metamaps/metamaps/node_modules/.bin/babel-node \
|
||||
-l /home/metamaps/logs/forever.realtime.log \
|
||||
--workingDir /home/metamaps/metamaps \
|
||||
start /home/metamaps/metamaps/realtime/realtime-server.js
|
||||
|
||||
#### Upstart service for delayed_worker:
|
||||
|
||||
Put the following code into `/etc/init/metamaps_delayed_worker.conf`:
|
||||
If your system uses upstart for init scripts, put the following code into `/etc/init/metamaps_delayed_job.conf`:
|
||||
|
||||
description "Delayed Jobs Worker for Metamaps"
|
||||
|
||||
|
@ -128,3 +147,30 @@ Then start the service and check the last ten lines of the log file to make sure
|
|||
|
||||
sudo service metamaps_delayed_job start
|
||||
tail /var/log/upstart/metamaps_delayed_job.log
|
||||
|
||||
#### Systemd service for delayed_worker:
|
||||
|
||||
If your system uses systemd for init scripts, ptu the following code into `/etc/systemd/system/metamaps_delayed_job.service`:
|
||||
|
||||
[Unit]
|
||||
Description=metamaps delayed job service
|
||||
After=network-online.target
|
||||
|
||||
[Service]
|
||||
ExecStart=/usr/local/rvm/gems/ruby-2.3.0@metamaps/bin/bundle exec rails jobs:work
|
||||
WorkingDirectory=/home/metamaps/metamaps
|
||||
Restart=always
|
||||
User=metamaps
|
||||
Group=metamaps
|
||||
Environment=HOME=/home/metamaps
|
||||
Environment=PATH="/usr/local/rvm/gems/ruby-2.3.0@metamaps/bin:/usr/local/rvm/gems/ruby-2.3.0@global/bin:/usr/local/rvm/rubies/ruby-2.3.0/bin:/usr/local/rvm/bin:/usr/local/bin:/usr/bin:/bin"
|
||||
Environment=GEM_PATH="/usr/local/rvm/gems/ruby-2.3.0@metamaps:/usr/local/rvm/gems/ruby-2.3.0@global"
|
||||
Environment=RAILS_ENV="production"
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
|
||||
Then start the service and check the last ten lines of the log file to make sure it's running OK:
|
||||
|
||||
sudo systemctl start metamaps_delayed_job
|
||||
# ??? how the heck do you check systemd logs??
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
/* global $, ActionCable */
|
||||
|
||||
import { indexOf } from 'lodash'
|
||||
|
||||
import Active from './Active'
|
||||
import Control from './Control'
|
||||
import Create from './Create'
|
||||
|
@ -110,7 +112,7 @@ const Cable = {
|
|||
if (edge.getData('mappings').length - 1 === 0) {
|
||||
Control.hideEdge(edge)
|
||||
}
|
||||
|
||||
|
||||
var index = indexOf(edge.getData('synapses'), synapse)
|
||||
edge.getData('mappings').splice(index, 1)
|
||||
edge.getData('synapses').splice(index, 1)
|
||||
|
@ -128,19 +130,20 @@ const Cable = {
|
|||
// containing only the information we need to determine whether the active mapper
|
||||
// can view this topic, then if we determine it can, we make a call for the full model
|
||||
const t = new DataModel.Topic(event.topic)
|
||||
|
||||
// refactor the heck outta this, its adding wicked wait time
|
||||
var topic, mapping, mapper, cancel
|
||||
function waitThenRenderTopic() {
|
||||
if (topic && mapping && mapper) {
|
||||
Topic.renderTopic(mapping, topic, true)
|
||||
Engine.runLayout()
|
||||
} else if (!cancel) {
|
||||
setTimeout(waitThenRenderTopic, 10)
|
||||
}
|
||||
}
|
||||
|
||||
if (t.authorizeToShow(m) && !DataModel.Topics.get(event.topic.id)) {
|
||||
// refactor the heck outta this, its adding wicked wait time
|
||||
var topic, mapping, mapper, cancel
|
||||
|
||||
const waitThenRenderTopic = () => {
|
||||
if (topic && mapping && mapper) {
|
||||
Topic.renderTopic(mapping, topic, true)
|
||||
Engine.runLayout()
|
||||
} else if (!cancel) {
|
||||
setTimeout(waitThenRenderTopic, 10)
|
||||
}
|
||||
}
|
||||
|
||||
mapper = DataModel.Mappers.get(event.topic.user_id)
|
||||
if (mapper === undefined) {
|
||||
Mapper.get(event.topic.user_id, function(m) {
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
/* global $ */
|
||||
|
||||
import _ from 'lodash'
|
||||
import outdent from 'outdent'
|
||||
|
||||
|
@ -8,7 +6,6 @@ import DataModel from './DataModel'
|
|||
import Engine from './Engine'
|
||||
import Filter from './Filter'
|
||||
import GlobalUI from './GlobalUI'
|
||||
import JIT from './JIT'
|
||||
import Mouse from './Mouse'
|
||||
import Selected from './Selected'
|
||||
import Settings from './Settings'
|
||||
|
@ -99,7 +96,6 @@ const Control = {
|
|||
|
||||
var permToDelete = Active.Mapper.id === topic.get('user_id') || Active.Mapper.get('admin')
|
||||
if (permToDelete) {
|
||||
var mappableid = topic.id
|
||||
var mapping = node.getData('mapping')
|
||||
topic.destroy()
|
||||
DataModel.Mappings.remove(mapping)
|
||||
|
@ -149,7 +145,6 @@ const Control = {
|
|||
}
|
||||
|
||||
var topic = node.getData('topic')
|
||||
var mappableid = topic.id
|
||||
var mapping = node.getData('mapping')
|
||||
mapping.destroy()
|
||||
DataModel.Topics.remove(topic)
|
||||
|
@ -268,7 +263,6 @@ const Control = {
|
|||
if (edge.getData('synapses').length - 1 === 0) {
|
||||
Control.hideEdge(edge)
|
||||
}
|
||||
var mappableid = synapse.id
|
||||
synapse.destroy()
|
||||
|
||||
// the server will destroy the mapping, we just need to remove it here
|
||||
|
@ -319,7 +313,6 @@ const Control = {
|
|||
|
||||
var synapse = edge.getData('synapses')[index]
|
||||
var mapping = edge.getData('mappings')[index]
|
||||
var mappableid = synapse.id
|
||||
mapping.destroy()
|
||||
|
||||
DataModel.Synapses.remove(synapse)
|
||||
|
|
|
@ -335,18 +335,18 @@ console.log(codesToSwitchToIds)
|
|||
},
|
||||
source: synapseBloodhound
|
||||
},
|
||||
{
|
||||
name: 'existing_synapses',
|
||||
limit: 50,
|
||||
display: function(s) { return s.label },
|
||||
templates: {
|
||||
suggestion: function(s) {
|
||||
return Hogan.compile($('#synapseAutocompleteTemplate').html()).render(s)
|
||||
},
|
||||
header: '<h3>Existing synapses</h3>'
|
||||
{
|
||||
name: 'existing_synapses',
|
||||
limit: 50,
|
||||
display: function(s) { return s.label },
|
||||
templates: {
|
||||
suggestion: function(s) {
|
||||
return Hogan.compile($('#synapseAutocompleteTemplate').html()).render(s)
|
||||
},
|
||||
source: existingSynapseBloodhound
|
||||
}]
|
||||
header: '<h3>Existing synapses</h3>'
|
||||
},
|
||||
source: existingSynapseBloodhound
|
||||
}]
|
||||
)
|
||||
|
||||
$('#synapse_desc').keyup(function(e) {
|
||||
|
|
|
@ -7,7 +7,6 @@ try { Backbone.$ = window.$ } catch (err) {}
|
|||
import Active from '../Active'
|
||||
import InfoBox from '../Map/InfoBox'
|
||||
import Mapper from '../Mapper'
|
||||
import Realtime from '../Realtime'
|
||||
|
||||
const Map = Backbone.Model.extend({
|
||||
urlRoot: '/maps',
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
/* global $ */
|
||||
|
||||
import _ from 'lodash'
|
||||
import outdent from 'outdent'
|
||||
import Backbone from 'backbone'
|
||||
|
@ -7,8 +5,6 @@ try { Backbone.$ = window.$ } catch (err) {}
|
|||
|
||||
import Active from '../Active'
|
||||
import Filter from '../Filter'
|
||||
import JIT from '../JIT'
|
||||
import Realtime from '../Realtime'
|
||||
import SynapseCard from '../SynapseCard'
|
||||
import Visualize from '../Visualize'
|
||||
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
/* global $ */
|
||||
|
||||
import _ from 'lodash'
|
||||
import Backbone from 'backbone'
|
||||
try { Backbone.$ = window.$ } catch (err) {}
|
||||
|
@ -7,8 +5,6 @@ try { Backbone.$ = window.$ } catch (err) {}
|
|||
import Active from '../Active'
|
||||
import Engine from '../Engine'
|
||||
import Filter from '../Filter'
|
||||
import JIT from '../JIT'
|
||||
import Realtime from '../Realtime'
|
||||
import TopicCard from '../TopicCard'
|
||||
import Visualize from '../Visualize'
|
||||
|
||||
|
|
|
@ -81,11 +81,11 @@ const DataModel = {
|
|||
var myCollection = serverData.Mine ? serverData.Mine : []
|
||||
var sharedCollection = serverData.Shared ? serverData.Shared : []
|
||||
var starredCollection = serverData.Starred ? serverData.Starred : []
|
||||
var mapperCollection = []
|
||||
var mapperCollection = serverData.Mapper ? serverData.Mapper : []
|
||||
var mapperOptionsObj = { id: 'mapper', sortBy: 'updated_at' }
|
||||
if (self.Maps.Mapper.mapperId) {
|
||||
if (serverData.Mapper && serverData.Mapper.mapperId) {
|
||||
mapperCollection = serverData.Mapper.models
|
||||
mapperOptionsObj.mapperId = serverData.Mapper.id
|
||||
mapperOptionsObj.mapperId = serverData.Mapper.mapperId
|
||||
}
|
||||
var featuredCollection = serverData.Featured ? serverData.Featured : []
|
||||
var activeCollection = serverData.Active ? serverData.Active : []
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
const Debug = (arg = window.Metamaps) => {
|
||||
const Debug = function(arg = window.Metamaps) {
|
||||
if (arg === undefined && typeof window !== 'undefined') arg = window.Metamaps
|
||||
console.debug(arg)
|
||||
console.debug(`Metamaps Version: ${arg.VERSION}`)
|
||||
console.debug(`Metamaps Version: ${arg.ServerData.VERSION}`)
|
||||
console.debug(`Build: ${arg.ServerData.BUILD}`)
|
||||
console.debug(`Last Updated: ${arg.ServerData.LAST_UPDATED}`)
|
||||
}
|
||||
|
||||
export default Debug
|
||||
|
|
|
@ -7,6 +7,7 @@ import outdent from 'outdent'
|
|||
import ImportDialogBox from '../../components/ImportDialogBox'
|
||||
|
||||
import PasteInput from '../PasteInput'
|
||||
import Map from '../Map'
|
||||
|
||||
const ImportDialog = {
|
||||
openLightbox: null,
|
||||
|
@ -24,14 +25,19 @@ const ImportDialog = {
|
|||
`))
|
||||
ReactDOM.render(React.createElement(ImportDialogBox, {
|
||||
onFileAdded: PasteInput.handleFile,
|
||||
exampleImageUrl: serverData['import-example.png']
|
||||
exampleImageUrl: serverData['import-example.png'],
|
||||
downloadScreenshot: ImportDialog.downloadScreenshot
|
||||
}), $('.importDialogWrapper').get(0))
|
||||
},
|
||||
show: function() {
|
||||
ImportDialog.openLightbox('import-dialog')
|
||||
},
|
||||
hide: function() {
|
||||
ImportDialog.closeLightbox('import-dialog')
|
||||
ImportDialog.closeLightbox()
|
||||
},
|
||||
downloadScreenshot: function() {
|
||||
ImportDialog.hide()
|
||||
Map.offerScreenshotDownload()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -12,9 +12,11 @@ import NotificationIcon from './NotificationIcon'
|
|||
|
||||
const GlobalUI = {
|
||||
notifyTimeout: null,
|
||||
notifyQueue: [],
|
||||
notifying: false,
|
||||
lightbox: null,
|
||||
init: function(serverData) {
|
||||
var self = GlobalUI
|
||||
const self = GlobalUI
|
||||
|
||||
self.Search.init(serverData)
|
||||
self.CreateMap.init(serverData)
|
||||
|
@ -45,7 +47,7 @@ const GlobalUI = {
|
|||
}, 200, 'easeInCubic', function() { $(this).hide() })
|
||||
},
|
||||
openLightbox: function(which) {
|
||||
var self = GlobalUI
|
||||
const self = GlobalUI
|
||||
|
||||
$('.lightboxContent').hide()
|
||||
$('#' + which).show()
|
||||
|
@ -72,7 +74,7 @@ const GlobalUI = {
|
|||
},
|
||||
|
||||
closeLightbox: function(event) {
|
||||
var self = GlobalUI
|
||||
const self = GlobalUI
|
||||
|
||||
if (event) event.preventDefault()
|
||||
|
||||
|
@ -96,23 +98,45 @@ const GlobalUI = {
|
|||
}
|
||||
self.lightbox = null
|
||||
},
|
||||
notifyUser: function(message, leaveOpen) {
|
||||
var self = GlobalUI
|
||||
notifyUser: function(message, opts = {}) {
|
||||
const self = GlobalUI
|
||||
|
||||
if (self.notifying) {
|
||||
self.notifyQueue.push({ message, opts })
|
||||
return
|
||||
} else {
|
||||
self._notifyUser(message, opts)
|
||||
}
|
||||
},
|
||||
// note: use the wrapper function notifyUser instead of this one
|
||||
_notifyUser: function(message, opts = {}) {
|
||||
const self = GlobalUI
|
||||
|
||||
const { leaveOpen = false, timeOut = 8000 } = opts
|
||||
|
||||
$('#toast').html(message)
|
||||
self.showDiv('#toast')
|
||||
clearTimeout(self.notifyTimeOut)
|
||||
|
||||
if (!leaveOpen) {
|
||||
self.notifyTimeOut = setTimeout(function() {
|
||||
self.hideDiv('#toast')
|
||||
}, 8000)
|
||||
GlobalUI.clearNotify()
|
||||
}, timeOut)
|
||||
}
|
||||
|
||||
self.notifying = true
|
||||
},
|
||||
clearNotify: function() {
|
||||
var self = GlobalUI
|
||||
const self = GlobalUI
|
||||
|
||||
clearTimeout(self.notifyTimeOut)
|
||||
self.hideDiv('#toast')
|
||||
// if there are messages remaining, display them
|
||||
if (self.notifyQueue.length > 0) {
|
||||
const { message, opts } = self.notifyQueue.shift()
|
||||
self._notifyUser(message, opts)
|
||||
} else {
|
||||
self.hideDiv('#toast')
|
||||
self.notifying = false
|
||||
}
|
||||
},
|
||||
shareInvite: function(inviteLink) {
|
||||
clipboard.copy({
|
||||
|
|
|
@ -374,7 +374,7 @@ const Import = {
|
|||
$.get('/hacks/load_url_title', {
|
||||
url
|
||||
}, function success(data, textStatus) {
|
||||
if (data.trim() === '') return
|
||||
if (typeof data === 'string' && data.trim() === '') return
|
||||
var selector = '#showcard #topic_' + topic.get('id') + ' .best_in_place'
|
||||
if ($(selector).find('form').length > 0) {
|
||||
$(selector).find('textarea, input').val(data.title)
|
||||
|
|
|
@ -20,7 +20,7 @@ const Listeners = {
|
|||
if (!(Active.Map || Active.Topic)) return
|
||||
|
||||
const onCanvas = e.target.tagName === 'BODY'
|
||||
|
||||
|
||||
switch (e.which) {
|
||||
case 13: // if enter key is pressed
|
||||
// prevent topic creation if sending a message
|
||||
|
@ -31,6 +31,10 @@ const Listeners = {
|
|||
case 27: // if esc key is pressed
|
||||
JIT.escKeyHandler()
|
||||
break
|
||||
case 46: // if DEL is pressed
|
||||
e.preventDefault()
|
||||
Control.deleteSelected()
|
||||
break
|
||||
case 65: // if a or A is pressed
|
||||
if ((e.ctrlKey || e.metaKey) && onCanvas) {
|
||||
const nodesCount = Object.keys(Visualize.mGraph.graph.nodes).length
|
||||
|
@ -124,7 +128,6 @@ const Listeners = {
|
|||
break
|
||||
}
|
||||
})
|
||||
|
||||
$(window).resize(function() {
|
||||
if (Visualize && Visualize.mGraph) {
|
||||
Util.resizeCanvas(Visualize.mGraph.canvas)
|
||||
|
|
|
@ -35,9 +35,11 @@ const InfoBox = {
|
|||
data-bip-value="{{desc}}"
|
||||
>{{desc}}</span>`,
|
||||
userImageUrl: '',
|
||||
init: function(serverData) {
|
||||
init: function(serverData, updateThumbnail) {
|
||||
var self = InfoBox
|
||||
|
||||
self.updateThumbnail = updateThumbnail
|
||||
|
||||
$('.mapInfoIcon').click(self.toggleBox)
|
||||
$('.mapInfoBox').click(function(event) {
|
||||
event.stopPropagation()
|
||||
|
@ -181,6 +183,7 @@ const InfoBox = {
|
|||
$('.mapInfoBox.yourMap').unbind('.yourMap').bind('click.yourMap', self.hidePermissionSelect)
|
||||
|
||||
$('.yourMap .mapInfoDelete').unbind().click(self.deleteActiveMap)
|
||||
$('.mapInfoThumbnail').unbind().click(self.updateThumbnail)
|
||||
|
||||
$('.mapContributors span, #mapContribs').unbind().click(function(event) {
|
||||
$('.mapContributors .tip').toggle()
|
||||
|
|
|
@ -46,7 +46,10 @@ const Map = {
|
|||
GlobalUI.CreateMap.emptyForkMapForm = $('#fork_map').html()
|
||||
|
||||
self.updateStar()
|
||||
InfoBox.init(serverData)
|
||||
|
||||
InfoBox.init(serverData, function updateThumbnail() {
|
||||
self.uploadMapScreenshot()
|
||||
})
|
||||
CheatSheet.init(serverData)
|
||||
|
||||
$('.viewOnly .requestAccess').click(self.requestAccess)
|
||||
|
@ -253,6 +256,48 @@ const Map = {
|
|||
}
|
||||
},
|
||||
exportImage: function() {
|
||||
Map.uploadMapScreenshot()
|
||||
Map.offerScreenshotDownload()
|
||||
GlobalUI.notifyUser('Note: this button is going away. Check the map card or the import box for setting the map thumbnail or downloading a screenshot.')
|
||||
},
|
||||
offerScreenshotDownload: () => {
|
||||
const canvas = Map.getMapCanvasForScreenshots()
|
||||
const filename = Map.getMapScreenshotFilename(Active.Map)
|
||||
|
||||
var downloadMessage = outdent`
|
||||
Captured map screenshot!
|
||||
<a id="map-screenshot-download-link"
|
||||
href="${canvas.canvas.toDataURL()}"
|
||||
download="${filename}"
|
||||
>
|
||||
DOWNLOAD
|
||||
</a>`
|
||||
GlobalUI.notifyUser(downloadMessage)
|
||||
},
|
||||
uploadMapScreenshot: () => {
|
||||
const canvas = Map.getMapCanvasForScreenshots()
|
||||
const filename = Map.getMapScreenshotFilename(Active.Map)
|
||||
|
||||
canvas.canvas.toBlob(imageBlob => {
|
||||
const formData = new window.FormData()
|
||||
formData.append('map[screenshot]', imageBlob, filename)
|
||||
$.ajax({
|
||||
type: 'PATCH',
|
||||
dataType: 'json',
|
||||
url: `/maps/${Active.Map.id}`,
|
||||
data: formData,
|
||||
processData: false,
|
||||
contentType: false,
|
||||
success: function(data) {
|
||||
GlobalUI.notifyUser('Successfully updated map screenshot.')
|
||||
},
|
||||
error: function() {
|
||||
GlobalUI.notifyUser('Failed to update map screenshot.')
|
||||
}
|
||||
})
|
||||
})
|
||||
},
|
||||
getMapCanvasForScreenshots: () => {
|
||||
var canvas = {}
|
||||
|
||||
canvas.canvas = document.createElement('canvas')
|
||||
|
@ -338,8 +383,9 @@ const Map = {
|
|||
node.visited = !T
|
||||
})
|
||||
|
||||
var map = Active.Map
|
||||
|
||||
return canvas
|
||||
},
|
||||
getMapScreenshotFilename: map => {
|
||||
var today = new Date()
|
||||
var dd = today.getDate()
|
||||
var mm = today.getMonth() + 1 // January is 0!
|
||||
|
@ -354,30 +400,7 @@ const Map = {
|
|||
|
||||
var mapName = map.get('name').split(' ').join(['-'])
|
||||
const filename = `metamap-${map.id}-${mapName}-${today}.png`
|
||||
|
||||
var downloadMessage = outdent`
|
||||
Captured map screenshot!
|
||||
<a href="${canvas.canvas.toDataURL()}" download="${filename}">DOWNLOAD</a>`
|
||||
GlobalUI.notifyUser(downloadMessage)
|
||||
|
||||
canvas.canvas.toBlob(imageBlob => {
|
||||
const formData = new window.FormData()
|
||||
formData.append('map[screenshot]', imageBlob, filename)
|
||||
$.ajax({
|
||||
type: 'PATCH',
|
||||
dataType: 'json',
|
||||
url: `/maps/${map.id}`,
|
||||
data: formData,
|
||||
processData: false,
|
||||
contentType: false,
|
||||
success: function(data) {
|
||||
console.log('successfully uploaded map screenshot')
|
||||
},
|
||||
error: function() {
|
||||
console.log('failed to save map screenshot')
|
||||
}
|
||||
})
|
||||
})
|
||||
return filename
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -6,6 +6,7 @@ import Visualize from './Visualize'
|
|||
|
||||
const PasteInput = {
|
||||
// thanks to https://github.com/kevva/url-regex
|
||||
// eslint-disable-next-line no-useless-escape
|
||||
URL_REGEX: new RegExp('^(?:(?:(?:[a-z]+:)?//)|www\.)(?:\S+(?::\S*)?@)?(?:localhost|(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])(?:\.(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])){3}|(?:(?:[a-z\u00a1-\uffff0-9]-*)*[a-z\u00a1-\uffff0-9]+)(?:\.(?:[a-z\u00a1-\uffff0-9]-*)*[a-z\u00a1-\uffff0-9]+)*(?:\.(?:[a-z\u00a1-\uffff]{2,}))\.?)(?::\d{2,5})?(?:[/?#][^\s"]*)?$'),
|
||||
|
||||
init: function() {
|
||||
|
|
|
@ -14,7 +14,7 @@ module.exports = {
|
|||
SEND_COORDS: 'SEND_COORDS',
|
||||
DRAG_TOPIC: 'DRAG_TOPIC',
|
||||
|
||||
/* EVENTS RECEIVABLE FROM NODE SERVER*/
|
||||
/* EVENTS RECEIVABLE FROM NODE SERVER */
|
||||
JUNTO_UPDATED: 'JUNTO_UPDATED',
|
||||
INVITED_TO_CALL: 'INVITED_TO_CALL',
|
||||
INVITED_TO_JOIN: 'INVITED_TO_JOIN',
|
||||
|
@ -29,5 +29,5 @@ module.exports = {
|
|||
NEW_MAPPER: 'NEW_MAPPER',
|
||||
LOST_MAPPER: 'LOST_MAPPER',
|
||||
TOPIC_DRAGGED: 'TOPIC_DRAGGED',
|
||||
PEER_COORDS_UPDATED: 'PEER_COORDS_UPDATED',
|
||||
PEER_COORDS_UPDATED: 'PEER_COORDS_UPDATED'
|
||||
}
|
||||
|
|
|
@ -9,8 +9,7 @@ import Create from '../Create'
|
|||
import DataModel from '../DataModel'
|
||||
import JIT from '../JIT'
|
||||
import Util from '../Util'
|
||||
import Views from '../Views'
|
||||
import { ChatView } from '../Views'
|
||||
import Views, { ChatView } from '../Views'
|
||||
import Visualize from '../Visualize'
|
||||
|
||||
import {
|
||||
|
@ -153,7 +152,7 @@ let Realtime = {
|
|||
config: { DOUBLE_CLICK_TOLERANCE: 200 }
|
||||
})
|
||||
self.room.videoAdded(self.handleVideoAdded)
|
||||
|
||||
|
||||
self.startActiveMap()
|
||||
} // if Active.Mapper
|
||||
},
|
||||
|
|
|
@ -4,18 +4,12 @@
|
|||
everthing in this file happens as a result of websocket events
|
||||
*/
|
||||
|
||||
import { indexOf } from 'lodash'
|
||||
|
||||
import { JUNTO_UPDATED } from './events'
|
||||
|
||||
import Active from '../Active'
|
||||
import { ChatView } from '../Views'
|
||||
import DataModel from '../DataModel'
|
||||
import GlobalUI from '../GlobalUI'
|
||||
import Control from '../Control'
|
||||
import Mapper from '../Mapper'
|
||||
import Topic from '../Topic'
|
||||
import Synapse from '../Synapse'
|
||||
import Util from '../Util'
|
||||
import Visualize from '../Visualize'
|
||||
|
||||
|
@ -155,7 +149,7 @@ export const invitedToCall = self => inviter => {
|
|||
notifyText += username + ' is inviting you to a conversation. Join live?'
|
||||
notifyText += ' <button type="button" class="toast-button button yes">Yes</button>'
|
||||
notifyText += ' <button type="button" class="toast-button button btn-no no">No</button>'
|
||||
GlobalUI.notifyUser(notifyText, true)
|
||||
GlobalUI.notifyUser(notifyText, { leaveOpen: true })
|
||||
$('#toast button.yes').click(e => self.acceptCall(inviter))
|
||||
$('#toast button.no').click(e => self.denyCall(inviter))
|
||||
}
|
||||
|
@ -168,7 +162,7 @@ export const invitedToJoin = self => inviter => {
|
|||
var notifyText = username + ' is inviting you to the conversation. Join?'
|
||||
notifyText += ' <button type="button" class="toast-button button yes">Yes</button>'
|
||||
notifyText += ' <button type="button" class="toast-button button btn-no no">No</button>'
|
||||
GlobalUI.notifyUser(notifyText, true)
|
||||
GlobalUI.notifyUser(notifyText, { leaveOpen: true })
|
||||
$('#toast button.yes').click(e => self.joinCall())
|
||||
$('#toast button.no').click(e => self.denyInvite(inviter))
|
||||
}
|
||||
|
@ -207,10 +201,10 @@ export const callInProgress = self => () => {
|
|||
var notifyText = "There's a conversation happening, want to join?"
|
||||
notifyText += ' <button type="button" class="toast-button button yes">Yes</button>'
|
||||
notifyText += ' <button type="button" class="toast-button button btn-no no">No</button>'
|
||||
GlobalUI.notifyUser(notifyText, true)
|
||||
GlobalUI.notifyUser(notifyText, { leaveOpen: true })
|
||||
$('#toast button.yes').click(e => self.joinCall())
|
||||
$('#toast button.no').click(e => GlobalUI.clearNotify())
|
||||
ChatView.conversationInProgress()
|
||||
ChatView.conversationInProgress()
|
||||
}
|
||||
|
||||
export const callStarted = self => () => {
|
||||
|
@ -218,7 +212,7 @@ export const callStarted = self => () => {
|
|||
var notifyText = "There's a conversation starting, want to join?"
|
||||
notifyText += ' <button type="button" class="toast-button button">Yes</button>'
|
||||
notifyText += ' <button type="button" class="toast-button button btn-no">No</button>'
|
||||
GlobalUI.notifyUser(notifyText, true)
|
||||
GlobalUI.notifyUser(notifyText, { leaveOpen: true })
|
||||
$('#toast button.yes').click(e => self.joinCall())
|
||||
$('#toast button.no').click(e => GlobalUI.clearNotify())
|
||||
ChatView.conversationInProgress()
|
||||
|
|
|
@ -39,7 +39,7 @@ const _Router = Backbone.Router.extend({
|
|||
|
||||
var navigate = function() {
|
||||
self.timeoutId = setTimeout(function() {
|
||||
self.navigate('')
|
||||
self.navigateAndTrack('')
|
||||
}, 300)
|
||||
}
|
||||
|
||||
|
@ -121,7 +121,7 @@ const _Router = Backbone.Router.extend({
|
|||
path += '/' + DataModel.Maps.Mapper.mapperId
|
||||
}
|
||||
|
||||
self.navigate(path)
|
||||
self.navigateAndTrack(path)
|
||||
}
|
||||
var navigateTimeout = function() {
|
||||
self.timeoutId = setTimeout(navigate, 300)
|
||||
|
@ -202,6 +202,11 @@ const _Router = Backbone.Router.extend({
|
|||
|
||||
const Router = new _Router()
|
||||
|
||||
Router.navigateAndTrack = (fragment, options) => {
|
||||
Router.navigate(fragment, options)
|
||||
window.ga && window.ga('send', 'pageview', location.pathname, {title: document.title})
|
||||
}
|
||||
|
||||
Router.intercept = function(evt) {
|
||||
var segments
|
||||
|
||||
|
|
|
@ -1,6 +1,14 @@
|
|||
/* global $ */
|
||||
|
||||
import { Parser, HtmlRenderer } from 'commonmark'
|
||||
import { emojiIndex } from 'emoji-mart'
|
||||
import { escapeRegExp } from 'lodash'
|
||||
|
||||
const emojiToShortcodes = {}
|
||||
Object.keys(emojiIndex.emojis).forEach(key => {
|
||||
const emoji = emojiIndex.emojis[key]
|
||||
emojiToShortcodes[emoji.native] = emoji.colons
|
||||
})
|
||||
|
||||
const Util = {
|
||||
// helper function to determine how many lines are needed
|
||||
|
@ -150,6 +158,29 @@ const Util = {
|
|||
canvas.scale(oldAttr.scaleX, oldAttr.scaleY)
|
||||
const newAttr = Util.logCanvasAttributes(canvas)
|
||||
canvas.translate(newAttr.centreCoords.x - oldAttr.centreCoords.x, newAttr.centreCoords.y - oldAttr.centreCoords.y)
|
||||
},
|
||||
removeEmoji: function(withEmoji) {
|
||||
let text = withEmoji
|
||||
Object.keys(emojiIndex.emojis).forEach(key => {
|
||||
const emoji = emojiIndex.emojis[key]
|
||||
text = text.replace(new RegExp(escapeRegExp(emoji.native), 'g'), emoji.colons)
|
||||
})
|
||||
return text
|
||||
},
|
||||
addEmoji: function(withoutEmoji, opts = { emoticons: true }) {
|
||||
let text = withoutEmoji
|
||||
Object.keys(emojiIndex.emojis).forEach(key => {
|
||||
const emoji = emojiIndex.emojis[key]
|
||||
text = text.replace(new RegExp(escapeRegExp(emoji.colons), 'g'), emoji.native)
|
||||
})
|
||||
if (opts.emoticons) {
|
||||
Object.keys(emojiIndex.emoticons).forEach(emoticon => {
|
||||
const key = emojiIndex.emoticons[emoticon]
|
||||
const emoji = emojiIndex.emojis[key]
|
||||
text = text.replace(new RegExp(escapeRegExp(emoticon), 'g'), emoji.native)
|
||||
})
|
||||
}
|
||||
return text
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -49,7 +49,7 @@ const ChatView = {
|
|||
$('#' + ChatView.domId).hide()
|
||||
},
|
||||
render: () => {
|
||||
if (!Active.Map) return
|
||||
if (!Active.Map) return
|
||||
const self = ChatView
|
||||
self.mapChat = ReactDOM.render(React.createElement(MapChat, {
|
||||
conversationLive: self.conversationLive,
|
||||
|
@ -111,7 +111,7 @@ const ChatView = {
|
|||
conversationInProgress: participating => {
|
||||
ChatView.conversationLive = true
|
||||
ChatView.isParticipating = participating
|
||||
ChatView.render()
|
||||
ChatView.render()
|
||||
},
|
||||
conversationEnded: () => {
|
||||
ChatView.conversationLive = false
|
||||
|
@ -144,7 +144,7 @@ const ChatView = {
|
|||
},
|
||||
addMessage: (message, isInitial, wasMe) => {
|
||||
const self = ChatView
|
||||
if (!isInitial) self.mapChat.newMessage()
|
||||
if (!isInitial) self.mapChat.newMessage()
|
||||
if (!wasMe && !isInitial && self.alertSound) self.sound.play('receivechat')
|
||||
self.messages.add(message)
|
||||
self.render()
|
||||
|
|
|
@ -1,13 +1,7 @@
|
|||
/* global $ */
|
||||
|
||||
import Backbone from 'backbone'
|
||||
import attachMediaStream from 'attachmediastream'
|
||||
|
||||
// TODO is this line good or bad
|
||||
// Backbone.$ = window.$
|
||||
|
||||
import Active from '../Active'
|
||||
import DataModel from '../DataModel'
|
||||
import Realtime from '../Realtime'
|
||||
import VideoView from './VideoView'
|
||||
|
||||
|
|
|
@ -9,7 +9,7 @@ import { JUNTO_UPDATED } from '../Realtime/events'
|
|||
const Views = {
|
||||
init: (serverData) => {
|
||||
$(document).on(JUNTO_UPDATED, () => ExploreMaps.render())
|
||||
ChatView.init([serverData['sounds/MM_sounds.mp3'],serverData['sounds/MM_sounds.ogg']])
|
||||
ChatView.init([serverData['sounds/MM_sounds.mp3'], serverData['sounds/MM_sounds.ogg']])
|
||||
},
|
||||
ExploreMaps,
|
||||
ChatView,
|
||||
|
|
|
@ -154,8 +154,6 @@ const Visualize = {
|
|||
self.mGraph.graph.empty()
|
||||
}
|
||||
|
||||
if (self.type === 'ForceDirected' && Active.Mapper) $.post('/maps/' + Active.Map.id + '/events/user_presence')
|
||||
|
||||
function runAnimation() {
|
||||
Loading.hide()
|
||||
$('#new_topic').show()
|
||||
|
@ -216,9 +214,9 @@ const Visualize = {
|
|||
var t = Active.Topic
|
||||
|
||||
if (m && window.location.pathname !== '/maps/' + m.id) {
|
||||
Router.navigate('/maps/' + m.id)
|
||||
Router.navigateAndTrack('/maps/' + m.id)
|
||||
} else if (t && window.location.pathname !== '/topics/' + t.id) {
|
||||
Router.navigate('/topics/' + t.id)
|
||||
Router.navigateAndTrack('/topics/' + t.id)
|
||||
}
|
||||
}, 800)
|
||||
},
|
||||
|
|
|
@ -6,7 +6,6 @@ class ImportDialogBox extends Component {
|
|||
super(props)
|
||||
|
||||
this.state = {
|
||||
showImportInstructions: false
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -15,21 +14,9 @@ class ImportDialogBox extends Component {
|
|||
}
|
||||
|
||||
handleFile = (files, e) => {
|
||||
// // for some reason it uploads twice, so we need this debouncer
|
||||
// // eslint-disable-next-line no-return-assign
|
||||
// this.debouncer = this.debouncer || window.setTimeout(() => this.debouncer = null, 10)
|
||||
// if (!this.debouncer) {
|
||||
// this.props.onFileAdded(files[0])
|
||||
// }
|
||||
this.props.onFileAdded(files[0])
|
||||
}
|
||||
|
||||
toggleShowInstructions = e => {
|
||||
this.setState({
|
||||
showImportInstructions: !this.state.showImportInstructions
|
||||
})
|
||||
}
|
||||
|
||||
render = () => {
|
||||
return (
|
||||
<div className="import-dialog">
|
||||
|
@ -40,6 +27,9 @@ class ImportDialogBox extends Component {
|
|||
<div className="import-blue-button" onClick={this.handleExport('json')}>
|
||||
Export as JSON
|
||||
</div>
|
||||
<div className="import-blue-button" onClick={this.props.downloadScreenshot}>
|
||||
Download screenshot
|
||||
</div>
|
||||
<h3>IMPORT</h3>
|
||||
<p>To upload a file, drop it here:</p>
|
||||
<Dropzone onDropAccepted={this.handleFile}
|
||||
|
@ -47,23 +37,7 @@ class ImportDialogBox extends Component {
|
|||
>
|
||||
Drop files here!
|
||||
</Dropzone>
|
||||
<p>
|
||||
<a onClick={this.toggleShowInstructions} style={{ textDecoration: 'underline', cursor: 'pointer' }}>
|
||||
Show/hide import instructions
|
||||
</a>
|
||||
</p>
|
||||
{!this.state.showImportInstructions ? null : (<div>
|
||||
<p>
|
||||
You can import topics and synapses by uploading a spreadsheet here.
|
||||
The file should be in comma-separated format (when you save, change the
|
||||
filetype from .xls to .csv).
|
||||
</p>
|
||||
<img src={this.props.exampleImageUrl} style={{ width: '100%' }} />
|
||||
<p style={{ marginTop: '1em' }}>You can choose which columns to include in your data. Topics must have a name field. Synapses must have Topic 1 and Topic 2.</p>
|
||||
<p> </p>
|
||||
<p> * There are many valid import formats. Try exporting a map to see what columns you can include in your import data. You can also copy-paste from Excel to import, or import JSON.</p>
|
||||
<p> * If you are importing a list of links, you can use a Link column in place of the Name column.</p>
|
||||
</div>)}
|
||||
<p>See <a href="https://docs.metamaps.cc/importing_and_exporting_data.html">docs.metamaps.cc</a> for instructions.</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
@ -71,7 +45,8 @@ class ImportDialogBox extends Component {
|
|||
|
||||
ImportDialogBox.propTypes = {
|
||||
onFileAdded: PropTypes.func,
|
||||
exampleImageUrl: PropTypes.string
|
||||
exampleImageUrl: PropTypes.string,
|
||||
downloadScreenshot: PropTypes.func
|
||||
}
|
||||
|
||||
export default ImportDialogBox
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import React from 'react'
|
||||
import Autolinker from 'autolinker'
|
||||
import Util from '../../Metamaps/Util'
|
||||
|
||||
const linker = new Autolinker({ newWindow: true, truncate: 50, email: false, phone: false })
|
||||
|
||||
|
@ -10,24 +11,25 @@ function addZero(i) {
|
|||
return i
|
||||
}
|
||||
|
||||
function formatDate(created_at) {
|
||||
let date = new Date(created_at)
|
||||
function formatDate(createdAt) {
|
||||
let date = new Date(createdAt)
|
||||
let formatted = (date.getMonth() + 1) + '/' + date.getDate()
|
||||
formatted += ' ' + addZero(date.getHours()) + ':' + addZero(date.getMinutes())
|
||||
return formatted
|
||||
}
|
||||
|
||||
const Message = props => {
|
||||
const { user_image, user_name, message, created_at, heading } = props
|
||||
const messageHtml = {__html: linker.link(message)}
|
||||
const { user_image: userImage, user_name: userName, message, created_at: createdAt, heading } = props
|
||||
const messageHtml = {__html: linker.link(Util.addEmoji(message, { emoticons: false }))}
|
||||
|
||||
return (
|
||||
<div className="chat-message">
|
||||
<div className="chat-message-user">
|
||||
{heading && <img src={user_image} />}
|
||||
{heading && <img src={userImage} />}
|
||||
</div>
|
||||
{heading && <div className="chat-message-meta">
|
||||
<span className='chat-message-username'>{user_name}</span>
|
||||
<span className='chat-message-time'>{formatDate(created_at)}</span>
|
||||
<span className='chat-message-username'>{userName}</span>
|
||||
<span className='chat-message-time'>{formatDate(createdAt)}</span>
|
||||
</div>}
|
||||
<div className="chat-message-text" dangerouslySetInnerHTML={messageHtml}></div>
|
||||
<div className="clearfloat"></div>
|
||||
|
|
65
frontend/src/components/MapChat/NewMessage.js
Normal file
65
frontend/src/components/MapChat/NewMessage.js
Normal file
|
@ -0,0 +1,65 @@
|
|||
import React, { PropTypes, Component } from 'react'
|
||||
import { Emoji, Picker } from 'emoji-mart'
|
||||
|
||||
class NewMessage extends Component {
|
||||
constructor(props) {
|
||||
super(props)
|
||||
|
||||
this.state = {
|
||||
showEmojiPicker: false
|
||||
}
|
||||
}
|
||||
|
||||
toggleEmojiPicker = () => {
|
||||
this.setState({ showEmojiPicker: !this.state.showEmojiPicker })
|
||||
}
|
||||
|
||||
handleClick = (emoji, event) => {
|
||||
const { messageText } = this.props
|
||||
this.props.handleChange({ target: {
|
||||
value: messageText + emoji.colons
|
||||
}})
|
||||
|
||||
this.setState({ showEmojiPicker: false })
|
||||
this.props.focusMessageInput()
|
||||
}
|
||||
|
||||
render = () => {
|
||||
return (
|
||||
<div className="new-message-area">
|
||||
<Picker set="emojione"
|
||||
onClick={this.handleClick}
|
||||
style={{
|
||||
display: this.state.showEmojiPicker ? 'block' : 'none',
|
||||
maxWidth: '100%'
|
||||
}}
|
||||
emoji="upside_down_face"
|
||||
title="Emoji"
|
||||
/>
|
||||
<div className="extra-message-options">
|
||||
<span className="emoji-picker-button" onClick={this.toggleEmojiPicker}><Emoji size={24} emoji="upside_down_face" /></span>
|
||||
</div>
|
||||
<textarea value={this.props.messageText}
|
||||
onChange={this.props.handleChange}
|
||||
{...this.props.textAreaProps}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
NewMessage.propTypes = {
|
||||
messageText: PropTypes.string,
|
||||
handleChange: PropTypes.func,
|
||||
focusMessageInput: PropTypes.func,
|
||||
textAreaProps: PropTypes.shape({
|
||||
className: PropTypes.string,
|
||||
ref: PropTypes.func,
|
||||
placeholder: PropTypes.string,
|
||||
onKeyUp: PropTypes.func,
|
||||
onFocus: PropTypes.func,
|
||||
onBlur: PropTypes.func
|
||||
})
|
||||
}
|
||||
|
||||
export default NewMessage
|
|
@ -2,28 +2,28 @@ import React, { PropTypes, Component } from 'react'
|
|||
|
||||
class Participant extends Component {
|
||||
render() {
|
||||
const { conversationLive, mapperIsLive, isParticipating, isPending, id, self, image, username, selfName, color } = this.props
|
||||
const { conversationLive, mapperIsLive, isParticipating, isPending, id, self, image, username, color } = this.props
|
||||
return (
|
||||
<div className={`participant participant-${id} ${self ? 'is-self' : ''}`}>
|
||||
<div className="chat-participant-image">
|
||||
<img src={image} style={{ border: `2px solid ${color}`}} />
|
||||
</div>
|
||||
<div className="chat-participant-name">
|
||||
{username} {self ? '(me)' : ''}
|
||||
</div>
|
||||
{!self && !conversationLive && <button
|
||||
className={`button chat-participant-invite-call ${isPending ? 'pending' : ''}`}
|
||||
onClick={() => !isPending && this.props.inviteACall(id)} // Realtime.inviteACall(id)
|
||||
/>}
|
||||
{!self && mapperIsLive && !isParticipating && <button
|
||||
className={`button chat-participant-invite-join ${isPending ? 'pending' : ''}`}
|
||||
onClick={() => !isPending && this.props.inviteToJoin(id)} // Realtime.inviteToJoin(id)
|
||||
/>}
|
||||
{isParticipating && <span className="chat-participant-participating">
|
||||
<div className="green-dot"></div>
|
||||
</span>}
|
||||
<div className="clearfloat"></div>
|
||||
</div>
|
||||
<div className={`participant participant-${id} ${self ? 'is-self' : ''}`}>
|
||||
<div className="chat-participant-image">
|
||||
<img src={image} style={{ border: `2px solid ${color}` }} />
|
||||
</div>
|
||||
<div className="chat-participant-name">
|
||||
{username} {self ? '(me)' : ''}
|
||||
</div>
|
||||
{!self && !conversationLive && <button
|
||||
className={`button chat-participant-invite-call ${isPending ? 'pending' : ''}`}
|
||||
onClick={() => !isPending && this.props.inviteACall(id)} // Realtime.inviteACall(id)
|
||||
/>}
|
||||
{!self && mapperIsLive && !isParticipating && <button
|
||||
className={`button chat-participant-invite-join ${isPending ? 'pending' : ''}`}
|
||||
onClick={() => !isPending && this.props.inviteToJoin(id)} // Realtime.inviteToJoin(id)
|
||||
/>}
|
||||
{isParticipating && <span className="chat-participant-participating">
|
||||
<div className="green-dot"></div>
|
||||
</span>}
|
||||
<div className="clearfloat"></div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,6 +2,8 @@ import React, { PropTypes, Component } from 'react'
|
|||
import Unread from './Unread'
|
||||
import Participant from './Participant'
|
||||
import Message from './Message'
|
||||
import NewMessage from './NewMessage'
|
||||
import Util from '../../Metamaps/Util'
|
||||
|
||||
function makeList(messages) {
|
||||
let currentHeader
|
||||
|
@ -29,7 +31,7 @@ class MapChat extends Component {
|
|||
messageText: '',
|
||||
alertSound: true, // whether to play sounds on arrival of new messages or not
|
||||
cursorsShowing: true,
|
||||
videosShowing: true
|
||||
videosShowing: true
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -40,7 +42,7 @@ class MapChat extends Component {
|
|||
messageText: '',
|
||||
alertSound: true, // whether to play sounds on arrival of new messages or not
|
||||
cursorsShowing: true,
|
||||
videosShowing: true
|
||||
videosShowing: true
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -94,12 +96,17 @@ class MapChat extends Component {
|
|||
handleTextareaKeyUp = e => {
|
||||
if (e.which === 13) {
|
||||
e.preventDefault()
|
||||
const text = this.state.messageText
|
||||
const text = Util.removeEmoji(this.state.messageText)
|
||||
this.props.handleInputMessage(text)
|
||||
this.setState({ messageText: '' })
|
||||
}
|
||||
}
|
||||
|
||||
focusMessageInput = () => {
|
||||
if (!this.messageInput) return
|
||||
this.messageInput.focus()
|
||||
}
|
||||
|
||||
render = () => {
|
||||
const rightOffset = this.state.open ? '0' : '-300px'
|
||||
const { conversationLive, isParticipating, participants, messages, inviteACall, inviteToJoin } = this.props
|
||||
|
@ -116,12 +123,12 @@ class MapChat extends Component {
|
|||
<div className="participants">
|
||||
{conversationLive && <div className="conversation-live">
|
||||
LIVE
|
||||
{isParticipating && <span className="call-action leave" onClick={this.props.leaveCall}>
|
||||
LEAVE
|
||||
</span>}
|
||||
{!isParticipating && <span className="call-action join" onClick={this.props.joinCall}>
|
||||
JOIN
|
||||
</span>}
|
||||
{isParticipating && <span className="call-action leave" onClick={this.props.leaveCall}>
|
||||
LEAVE
|
||||
</span>}
|
||||
{!isParticipating && <span className="call-action join" onClick={this.props.joinCall}>
|
||||
JOIN
|
||||
</span>}
|
||||
</div>}
|
||||
{participants.map(participant => <Participant
|
||||
key={participant.id}
|
||||
|
@ -140,17 +147,20 @@ class MapChat extends Component {
|
|||
<div className="tooltips">Chat</div>
|
||||
<Unread count={unreadMessages} />
|
||||
</div>
|
||||
<div className="chat-messages" ref={div => this.messagesDiv = div}>
|
||||
<div className="chat-messages" ref={div => { this.messagesDiv = div }}>
|
||||
{makeList(messages)}
|
||||
</div>
|
||||
<textarea className="chat-input"
|
||||
ref={textarea => this.messageInput = textarea}
|
||||
placeholder="Send a message..."
|
||||
value={this.state.messageText}
|
||||
onChange={this.handleChange('messageText')}
|
||||
onKeyUp={this.handleTextareaKeyUp}
|
||||
onFocus={this.props.inputFocus}
|
||||
onBlur={this.props.inputBlur}
|
||||
<NewMessage messageText={this.state.messageText}
|
||||
focusMessageInput={this.focusMessageInput}
|
||||
handleChange={this.handleChange('messageText')}
|
||||
textAreaProps={{
|
||||
className: 'chat-input',
|
||||
ref: textarea => { this.messageInput = textarea },
|
||||
placeholder: 'Send a message...',
|
||||
onKeyUp: this.handleTextareaKeyUp,
|
||||
onFocus: this.props.inputFocus,
|
||||
onBlur: this.props.inputBlur
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
|
@ -168,7 +178,7 @@ MapChat.propTypes = {
|
|||
inviteToJoin: PropTypes.func,
|
||||
videoToggleClick: PropTypes.func,
|
||||
cursorToggleClick: PropTypes.func,
|
||||
soundToggleClick: PropTypes.func,
|
||||
soundToggleClick: PropTypes.func,
|
||||
participants: PropTypes.arrayOf(PropTypes.shape({
|
||||
color: PropTypes.string, // css color
|
||||
id: PropTypes.number,
|
||||
|
|
|
@ -41,7 +41,7 @@ class Header extends Component {
|
|||
linkClass={activeClass('active')}
|
||||
data-router="true"
|
||||
text="All Maps"
|
||||
/>
|
||||
/>
|
||||
<MapLink show={signedIn && explore}
|
||||
href="/explore/mine"
|
||||
linkClass={activeClass('my')}
|
||||
|
|
|
@ -112,7 +112,7 @@ class MapCard extends Component {
|
|||
{ mobile && hasConversation && <div className='mobileHasConversation'><MapperList mappers={ mapperList } /></div> }
|
||||
{ mobile && d && <div className="desc">{ d }</div> }
|
||||
{ mobile && <div className='mobileMetadata'><Metadata map={ map } /></div> }
|
||||
<div className='creatorAndPerm'>
|
||||
<div className={`creatorAndPerm ${map.authorizeToEdit(currentUser) ? '' : 'cardHasViewOnly'}`}>
|
||||
<img className='creatorImage' src={ map.get('user_image') } />
|
||||
<span className='creatorName'>{ map.get('user_name') }</span>
|
||||
{ !map.authorizeToEdit(currentUser) && <div className='cardViewOnly'>View Only</div> }
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue