Merge branch 'develop' into feature/convo.algo

This commit is contained in:
Connor Turland 2017-02-05 05:26:58 +00:00
commit 72fd2717b6
104 changed files with 2007 additions and 979 deletions

View file

@ -6,7 +6,7 @@ export DB_USERNAME='postgres'
export DB_PASSWORD='3112' export DB_PASSWORD='3112'
export DB_HOST='localhost' export DB_HOST='localhost'
export DB_PORT='5432' export DB_PORT='5432'
export DB_NAME='metamap002' export DB_NAME='metamaps'
export REALTIME_SERVER='http://localhost:5000' export REALTIME_SERVER='http://localhost:5000'
export MAILER_DEFAULT_URL='localhost:3000' export MAILER_DEFAULT_URL='localhost:3000'

View file

@ -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]()
============ ============

View file

@ -19,3 +19,6 @@ Metrics/AbcSize:
Style/Documentation: Style/Documentation:
Enabled: false Enabled: false
Style/EmptyMethod:
EnforcedStyle: expanded

View file

@ -9,7 +9,6 @@ gem 'aws-sdk'
gem 'best_in_place' gem 'best_in_place'
gem 'delayed_job' gem 'delayed_job'
gem 'delayed_job_active_record' gem 'delayed_job_active_record'
gem 'sucker_punch'
gem 'devise' gem 'devise'
gem 'doorkeeper' gem 'doorkeeper'
gem 'dotenv-rails' gem 'dotenv-rails'
@ -20,6 +19,7 @@ gem 'kaminari'
gem 'mailboxer' gem 'mailboxer'
gem 'paperclip' gem 'paperclip'
gem 'pg' gem 'pg'
gem 'puma'
gem 'pundit' gem 'pundit'
gem 'pundit_extra' gem 'pundit_extra'
gem 'rack-attack' gem 'rack-attack'
@ -27,7 +27,7 @@ gem 'rack-cors'
gem 'redis' gem 'redis'
gem 'slack-notifier' gem 'slack-notifier'
gem 'snorlax' gem 'snorlax'
gem 'puma' gem 'sucker_punch'
# asset stuff # asset stuff
gem 'jquery-rails' gem 'jquery-rails'
@ -36,12 +36,12 @@ gem 'sass-rails'
gem 'uglifier' gem 'uglifier'
group :test do group :test do
gem 'brakeman', require: false
gem 'factory_girl_rails' gem 'factory_girl_rails'
gem 'json-schema' gem 'json-schema'
gem 'rspec-rails' gem 'rspec-rails'
gem 'shoulda-matchers' gem 'shoulda-matchers'
gem 'simplecov', require: false gem 'simplecov', require: false
gem 'brakeman', require: false
end end
group :development, :test do group :development, :test do
@ -49,6 +49,6 @@ group :development, :test do
gem 'binding_of_caller' gem 'binding_of_caller'
gem 'pry-byebug' gem 'pry-byebug'
gem 'pry-rails' gem 'pry-rails'
gem 'tunemygc'
gem 'rubocop' gem 'rubocop'
gem 'tunemygc'
end end

View file

@ -1,57 +1,60 @@
GEM GEM
remote: https://rubygems.org/ remote: https://rubygems.org/
specs: specs:
actioncable (5.0.0.1) actioncable (5.0.1)
actionpack (= 5.0.0.1) actionpack (= 5.0.1)
nio4r (~> 1.2) nio4r (~> 1.2)
websocket-driver (~> 0.6.1) websocket-driver (~> 0.6.1)
actionmailer (5.0.0.1) actionmailer (5.0.1)
actionpack (= 5.0.0.1) actionpack (= 5.0.1)
actionview (= 5.0.0.1) actionview (= 5.0.1)
activejob (= 5.0.0.1) activejob (= 5.0.1)
mail (~> 2.5, >= 2.5.4) mail (~> 2.5, >= 2.5.4)
rails-dom-testing (~> 2.0) rails-dom-testing (~> 2.0)
actionpack (5.0.0.1) actionpack (5.0.1)
actionview (= 5.0.0.1) actionview (= 5.0.1)
activesupport (= 5.0.0.1) activesupport (= 5.0.1)
rack (~> 2.0) rack (~> 2.0)
rack-test (~> 0.6.3) rack-test (~> 0.6.3)
rails-dom-testing (~> 2.0) rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.0, >= 1.0.2) rails-html-sanitizer (~> 1.0, >= 1.0.2)
actionview (5.0.0.1) actionview (5.0.1)
activesupport (= 5.0.0.1) activesupport (= 5.0.1)
builder (~> 3.1) builder (~> 3.1)
erubis (~> 2.7.0) erubis (~> 2.7.0)
rails-dom-testing (~> 2.0) rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.0, >= 1.0.2) rails-html-sanitizer (~> 1.0, >= 1.0.2)
active_model_serializers (0.10.2) active_model_serializers (0.10.4)
actionpack (>= 4.1, < 6) actionpack (>= 4.1, < 6)
activemodel (>= 4.1, < 6) activemodel (>= 4.1, < 6)
jsonapi (~> 0.1.1.beta2) case_transform (>= 0.2)
railties (>= 4.1, < 6) jsonapi (= 0.1.1.beta6)
activejob (5.0.0.1) activejob (5.0.1)
activesupport (= 5.0.0.1) activesupport (= 5.0.1)
globalid (>= 0.3.6) globalid (>= 0.3.6)
activemodel (5.0.0.1) activemodel (5.0.1)
activesupport (= 5.0.0.1) activesupport (= 5.0.1)
activerecord (5.0.0.1) activerecord (5.0.1)
activemodel (= 5.0.0.1) activemodel (= 5.0.1)
activesupport (= 5.0.0.1) activesupport (= 5.0.1)
arel (~> 7.0) arel (~> 7.0)
activesupport (5.0.0.1) activesupport (5.0.1)
concurrent-ruby (~> 1.0, >= 1.0.2) concurrent-ruby (~> 1.0, >= 1.0.2)
i18n (~> 0.7) i18n (~> 0.7)
minitest (~> 5.1) minitest (~> 5.1)
tzinfo (~> 1.1) tzinfo (~> 1.1)
addressable (2.3.8) addressable (2.5.0)
arel (7.1.2) public_suffix (~> 2.0, >= 2.0.2)
arel (7.1.4)
ast (2.3.0) ast (2.3.0)
aws-sdk (2.6.3) aws-sdk (2.7.0)
aws-sdk-resources (= 2.6.3) aws-sdk-resources (= 2.7.0)
aws-sdk-core (2.6.3) aws-sdk-core (2.7.0)
aws-sigv4 (~> 1.0)
jmespath (~> 1.0) jmespath (~> 1.0)
aws-sdk-resources (2.6.3) aws-sdk-resources (2.7.0)
aws-sdk-core (= 2.6.3) aws-sdk-core (= 2.7.0)
aws-sigv4 (1.0.0)
bcrypt (3.1.11) bcrypt (3.1.11)
best_in_place (3.1.0) best_in_place (3.1.0)
actionpack (>= 3.2) actionpack (>= 3.2)
@ -62,21 +65,20 @@ GEM
rack (>= 0.9.0) rack (>= 0.9.0)
binding_of_caller (0.7.2) binding_of_caller (0.7.2)
debug_inspector (>= 0.0.1) debug_inspector (>= 0.0.1)
brakeman (3.4.0) brakeman (3.4.1)
builder (3.2.2) builder (3.2.3)
byebug (9.0.5) byebug (9.0.6)
carrierwave (0.11.2) carrierwave (1.0.0)
activemodel (>= 3.2.0) activemodel (>= 4.0.0)
activesupport (>= 3.2.0) activesupport (>= 4.0.0)
json (>= 1.7)
mime-types (>= 1.16) mime-types (>= 1.16)
mimemagic (>= 0.3.0) case_transform (0.2)
climate_control (0.0.3) activesupport
activesupport (>= 3.0) climate_control (0.1.0)
cocaine (0.5.8) cocaine (0.5.8)
climate_control (>= 0.0.3, < 1.0) climate_control (>= 0.0.3, < 1.0)
coderay (1.1.1) coderay (1.1.1)
concurrent-ruby (1.0.2) concurrent-ruby (1.0.4)
debug_inspector (0.0.2) debug_inspector (0.0.2)
delayed_job (4.1.2) delayed_job (4.1.2)
activesupport (>= 3.0, < 5.1) activesupport (>= 3.0, < 5.1)
@ -89,23 +91,23 @@ GEM
railties (>= 4.1.0, < 5.1) railties (>= 4.1.0, < 5.1)
responders responders
warden (~> 1.2.3) warden (~> 1.2.3)
diff-lcs (1.2.5) diff-lcs (1.3)
docile (1.1.5) docile (1.1.5)
doorkeeper (4.2.0) doorkeeper (4.2.0)
railties (>= 4.2) railties (>= 4.2)
dotenv (2.1.1) dotenv (2.1.2)
dotenv-rails (2.1.1) dotenv-rails (2.1.2)
dotenv (= 2.1.1) dotenv (= 2.1.2)
railties (>= 4.0, < 5.1) railties (>= 3.2, < 5.1)
erubis (2.7.0) erubis (2.7.0)
exception_notification (4.2.1) exception_notification (4.2.1)
actionmailer (>= 4.0, < 6) actionmailer (>= 4.0, < 6)
activesupport (>= 4.0, < 6) activesupport (>= 4.0, < 6)
execjs (2.7.0) execjs (2.7.0)
factory_girl (4.7.0) factory_girl (4.8.0)
activesupport (>= 3.0.0) activesupport (>= 3.0.0)
factory_girl_rails (4.7.0) factory_girl_rails (4.8.0)
factory_girl (~> 4.7.0) factory_girl (~> 4.8.0)
railties (>= 3.0.0) railties (>= 3.0.0)
globalid (0.3.7) globalid (0.3.7)
activesupport (>= 4.1.0) activesupport (>= 4.1.0)
@ -113,20 +115,32 @@ GEM
multi_xml (>= 0.5.2) multi_xml (>= 0.5.2)
i18n (0.7.0) i18n (0.7.0)
jmespath (1.3.1) jmespath (1.3.1)
jquery-rails (4.2.1) jquery-rails (4.2.2)
rails-dom-testing (>= 1, < 3) rails-dom-testing (>= 1, < 3)
railties (>= 4.2.0) railties (>= 4.2.0)
thor (>= 0.14, < 2.0) thor (>= 0.14, < 2.0)
jquery-ui-rails (5.0.5) jquery-ui-rails (6.0.1)
railties (>= 3.2.16) railties (>= 3.2.16)
json (1.8.3) json (2.0.3)
json-schema (2.6.2) json-schema (2.7.0)
addressable (~> 2.3.8) addressable (>= 2.4)
jsonapi (0.1.1.beta2) jsonapi (0.1.1.beta6)
json (~> 1.8) jsonapi-parser (= 0.1.1.beta3)
kaminari (0.17.0) jsonapi-renderer (= 0.1.1.beta1)
actionpack (>= 3.0.0) jsonapi-parser (0.1.1.beta3)
activesupport (>= 3.0.0) 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) loofah (2.0.3)
nokogiri (>= 1.5.9) nokogiri (>= 1.5.9)
mail (2.6.4) mail (2.6.4)
@ -140,12 +154,11 @@ GEM
mime-types-data (3.2016.0521) mime-types-data (3.2016.0521)
mimemagic (0.3.2) mimemagic (0.3.2)
mini_portile2 (2.1.0) mini_portile2 (2.1.0)
minitest (5.9.1) minitest (5.10.1)
multi_xml (0.5.5) multi_xml (0.6.0)
nio4r (1.2.1) nio4r (1.2.1)
nokogiri (1.6.8) nokogiri (1.7.0.1)
mini_portile2 (~> 2.1.0) mini_portile2 (~> 2.1.0)
pkg-config (~> 1.1.7)
orm_adapter (0.5.0) orm_adapter (0.5.0)
paperclip (5.1.0) paperclip (5.1.0)
activemodel (>= 4.2.0) activemodel (>= 4.2.0)
@ -153,20 +166,20 @@ GEM
cocaine (~> 0.5.5) cocaine (~> 0.5.5)
mime-types mime-types
mimemagic (~> 0.3.0) mimemagic (~> 0.3.0)
parser (2.3.1.4) parser (2.3.3.1)
ast (~> 2.2) ast (~> 2.2)
pg (0.19.0) pg (0.19.0)
pkg-config (1.1.7)
powerpack (0.1.1) powerpack (0.1.1)
pry (0.10.4) pry (0.10.4)
coderay (~> 1.1.0) coderay (~> 1.1.0)
method_source (~> 0.8.1) method_source (~> 0.8.1)
slop (~> 3.4) slop (~> 3.4)
pry-byebug (3.4.0) pry-byebug (3.4.2)
byebug (~> 9.0) byebug (~> 9.0)
pry (~> 0.10) pry (~> 0.10)
pry-rails (0.3.4) pry-rails (0.3.4)
pry (>= 0.9.10) pry (>= 0.9.10)
public_suffix (2.0.5)
puma (3.6.2) puma (3.6.2)
pundit (1.1.0) pundit (1.1.0)
activesupport (>= 3.0.0) activesupport (>= 3.0.0)
@ -177,35 +190,35 @@ GEM
rack-cors (0.4.0) rack-cors (0.4.0)
rack-test (0.6.3) rack-test (0.6.3)
rack (>= 1.0) rack (>= 1.0)
rails (5.0.0.1) rails (5.0.1)
actioncable (= 5.0.0.1) actioncable (= 5.0.1)
actionmailer (= 5.0.0.1) actionmailer (= 5.0.1)
actionpack (= 5.0.0.1) actionpack (= 5.0.1)
actionview (= 5.0.0.1) actionview (= 5.0.1)
activejob (= 5.0.0.1) activejob (= 5.0.1)
activemodel (= 5.0.0.1) activemodel (= 5.0.1)
activerecord (= 5.0.0.1) activerecord (= 5.0.1)
activesupport (= 5.0.0.1) activesupport (= 5.0.1)
bundler (>= 1.3.0, < 2.0) bundler (>= 1.3.0, < 2.0)
railties (= 5.0.0.1) railties (= 5.0.1)
sprockets-rails (>= 2.0.0) sprockets-rails (>= 2.0.0)
rails-dom-testing (2.0.1) rails-dom-testing (2.0.2)
activesupport (>= 4.2.0, < 6.0) activesupport (>= 4.2.0, < 6.0)
nokogiri (~> 1.6.0) nokogiri (~> 1.6)
rails-html-sanitizer (1.0.3) rails-html-sanitizer (1.0.3)
loofah (~> 2.0) loofah (~> 2.0)
railties (5.0.0.1) railties (5.0.1)
actionpack (= 5.0.0.1) actionpack (= 5.0.1)
activesupport (= 5.0.0.1) activesupport (= 5.0.1)
method_source method_source
rake (>= 0.8.7) rake (>= 0.8.7)
thor (>= 0.18.1, < 2.0) thor (>= 0.18.1, < 2.0)
rainbow (2.1.0) rainbow (2.2.1)
rake (11.3.0) rake (12.0.0)
redis (3.3.1) redis (3.3.2)
responders (2.3.0) responders (2.3.0)
railties (>= 4.2.0, < 5.1) railties (>= 4.2.0, < 5.1)
rspec-core (3.5.3) rspec-core (3.5.4)
rspec-support (~> 3.5.0) rspec-support (~> 3.5.0)
rspec-expectations (3.5.0) rspec-expectations (3.5.0)
diff-lcs (>= 1.2.0, < 2.0) diff-lcs (>= 1.2.0, < 2.0)
@ -222,14 +235,14 @@ GEM
rspec-mocks (~> 3.5.0) rspec-mocks (~> 3.5.0)
rspec-support (~> 3.5.0) rspec-support (~> 3.5.0)
rspec-support (3.5.0) rspec-support (3.5.0)
rubocop (0.43.0) rubocop (0.47.1)
parser (>= 2.3.1.1, < 3.0) parser (>= 2.3.3.1, < 3.0)
powerpack (~> 0.1) powerpack (~> 0.1)
rainbow (>= 1.99.1, < 3.0) rainbow (>= 1.99.1, < 3.0)
ruby-progressbar (~> 1.7) ruby-progressbar (~> 1.7)
unicode-display_width (~> 1.0, >= 1.0.1) unicode-display_width (~> 1.0, >= 1.0.1)
ruby-progressbar (1.8.1) ruby-progressbar (1.8.1)
sass (3.4.22) sass (3.4.23)
sass-rails (5.0.6) sass-rails (5.0.6)
railties (>= 4.0.0, < 6) railties (>= 4.0.0, < 6)
sass (~> 3.1) sass (~> 3.1)
@ -243,11 +256,11 @@ GEM
json (>= 1.8, < 3) json (>= 1.8, < 3)
simplecov-html (~> 0.10.0) simplecov-html (~> 0.10.0)
simplecov-html (0.10.0) simplecov-html (0.10.0)
slack-notifier (1.5.1) slack-notifier (2.0.0)
slop (3.6.0) slop (3.6.0)
snorlax (0.1.6) snorlax (0.1.6)
rails (> 4.1) rails (> 4.1)
sprockets (3.7.0) sprockets (3.7.1)
concurrent-ruby (~> 1.0) concurrent-ruby (~> 1.0)
rack (> 1, < 3) rack (> 1, < 3)
sprockets-rails (3.2.0) sprockets-rails (3.2.0)
@ -256,15 +269,15 @@ GEM
sprockets (>= 3.0.0) sprockets (>= 3.0.0)
sucker_punch (2.0.2) sucker_punch (2.0.2)
concurrent-ruby (~> 1.0.0) concurrent-ruby (~> 1.0.0)
thor (0.19.1) thor (0.19.4)
thread_safe (0.3.5) thread_safe (0.3.5)
tilt (2.0.5) tilt (2.0.5)
tunemygc (1.0.68) tunemygc (1.0.69)
tzinfo (1.2.2) tzinfo (1.2.2)
thread_safe (~> 0.1) thread_safe (~> 0.1)
uglifier (3.0.2) uglifier (3.0.4)
execjs (>= 0.3.0, < 3) execjs (>= 0.3.0, < 3)
unicode-display_width (1.1.1) unicode-display_width (1.1.3)
warden (1.2.6) warden (1.2.6)
rack (>= 1.0) rack (>= 1.0)
websocket-driver (0.6.4) websocket-driver (0.6.4)
@ -321,4 +334,4 @@ RUBY VERSION
ruby 2.3.0p0 ruby 2.3.0p0
BUNDLED WITH BUNDLED WITH
1.13.6 1.13.7

View file

@ -2,6 +2,7 @@ Metamaps
======= =======
[![Build Status](https://travis-ci.org/metamaps/metamaps.svg?branch=develop)](https://travis-ci.org/metamaps/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? ## 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 - 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 Documentation: [docs.metamaps.cc](https://docs.metamaps.cc)
- User Community: [hylo.com/c/metamaps](https://www.hylo.com/c/metamaps) - 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 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 send us a personal message or request an invite to the open beta, get in touch with us via email, Twitter, or Hylo - 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 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. - If you would like to get set up as a developer, that's great! Read on for help getting your development environment set up.

View file

@ -1,3 +1,4 @@
// eslint-disable spaced-comment
// JS and CSS bundles // JS and CSS bundles
//= link_directory ../javascripts .js //= link_directory ../javascripts .js
//= link_directory ../stylesheets .css //= link_directory ../stylesheets .css

View file

@ -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 // This is a manifest file that'll be compiled into application.js, which will include all the files
// listed below. // listed below.
// //
@ -10,7 +9,8 @@
// //
// WARNING: THE FIRST BLANK LINE MARKS THE END OF WHAT'S TO BE PROCESSED, ANY BLANK LINE SHOULD // WARNING: THE FIRST BLANK LINE MARKS THE END OF WHAT'S TO BE PROCESSED, ANY BLANK LINE SHOULD
// GO AFTER THE REQUIRES BELOW. // GO AFTER THE REQUIRES BELOW.
// //
/* eslint-disable spaced-comment */
//= require jquery //= require jquery
//= require jquery-ui //= require jquery-ui
//= require jquery_ujs //= require jquery_ujs
@ -19,3 +19,4 @@
//= require ./webpacked/metamaps.bundle //= require ./webpacked/metamaps.bundle
//= require ./Metamaps.ServerData //= require ./Metamaps.ServerData
//= require homepageVimeoFallback //= require homepageVimeoFallback
/* eslint-enable spaced-comment */

View file

@ -1,11 +1,11 @@
/* global $ */ /* global $ */
$(document).ready(function () { $(document).ready(function() {
if (window.location.pathname === '/') { if (window.location.pathname === '/') {
$.ajax({ $.ajax({
type: 'GET', type: 'GET',
url: 'https://player.vimeo.com', url: 'https://player.vimeo.com',
error: function (e) { error: function(e) {
$('.homeVideo').hide() $('.homeVideo').hide()
$('.homeVideo').replaceWith($('<video/>', { $('.homeVideo').replaceWith($('<video/>', {
poster: '/assets/metamaps-intro-poster.webp', poster: '/assets/metamaps-intro-poster.webp',

View 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] = [];
}
}
};
}));

View file

@ -1575,6 +1575,7 @@ h3.filterBox {
box-sizing: border-box; box-sizing: border-box;
margin: 0.75em; margin: 0.75em;
padding: 0.75em; padding: 0.75em;
padding-top: 0.85em;
height: 3em; height: 3em;
background-color: #AAB0FB; background-color: #AAB0FB;
border-radius: 0.3em; border-radius: 0.3em;
@ -2029,6 +2030,7 @@ input.collaboratorSearchField {
position: relative; position: relative;
cursor: pointer; cursor: pointer;
display: none; display: none;
padding: 0 2px;
} }
.mapInfoShareIcon { .mapInfoShareIcon {
width: 24px; width: 24px;
@ -2068,6 +2070,43 @@ and it won't be important on password protected instances */
.yourMap .mapInfoDelete { .yourMap .mapInfoDelete {
display: block; 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 { .mapInfoButtonsWrapper span {
position: absolute; position: absolute;
width: 100%; width: 100%;

View 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 }

View file

@ -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;
}

View 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;
}
}
}
}

View file

@ -211,6 +211,16 @@
span.creatorName { span.creatorName {
margin-left: 8px; 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 { .cardViewOnly {

View file

@ -1,3 +1,4 @@
# frozen_string_literal: true
module ApplicationCable module ApplicationCable
class Channel < ActionCable::Channel::Base class Channel < ActionCable::Channel::Base
end end

View file

@ -1,8 +1,17 @@
# frozen_string_literal: true
class MapChannel < ApplicationCable::Channel class MapChannel < ApplicationCable::Channel
# Called when the consumer has successfully # Called when the consumer has successfully
# become a subscriber of this channel. # become a subscriber of this channel.
def subscribed 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]}" 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
end end

View file

@ -1,8 +1,10 @@
# frozen_string_literal: true # frozen_string_literal: true
class AccessController < ApplicationController 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] :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] :deny_access, :deny_access_post, :request_access]
after_action :verify_authorized after_action :verify_authorized

View file

@ -5,6 +5,7 @@ module Api
include Pundit include Pundit
include PunditExtra include PunditExtra
protect_from_forgery with: :exception
snorlax_used_rest! snorlax_used_rest!
before_action :load_resource, only: [:show, :update, :destroy] before_action :load_resource, only: [:show, :update, :destroy]
@ -86,7 +87,7 @@ module Api
def token_user def token_user
token = params[:access_token] 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 @token_user ||= access_token.user if access_token
end end
@ -149,19 +150,30 @@ module Api
# override this method to explicitly set searchable columns # override this method to explicitly set searchable columns
def searchable_columns def searchable_columns
return @searchable_columns unless @searchable_columns.nil?
columns = resource_class.columns.select do |column| columns = resource_class.columns.select do |column|
column.type == :text || column.type == :string column.type == :text || column.type == :string
end 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 end
# thanks to http://stackoverflow.com/questions/4430578 # thanks to http://stackoverflow.com/questions/4430578
def search_by_q(collection) def search_by_q(collection)
table = resource_class.arel_table table = resource_class.arel_table
safe_query = "%#{params[:q].gsub(/[%_]/, '\\\\\0')}%" 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? next search_column.call(column) if prev.nil?
search_column.call(column).or(prev) search_column.call(column).or(prev)
end end

View file

@ -5,7 +5,6 @@ class HacksController < ApplicationController
include ActionView::Helpers::TextHelper # string truncate method include ActionView::Helpers::TextHelper # string truncate method
# rate limited by rack-attack - currently 5r/s # rate limited by rack-attack - currently 5r/s
# TODO: what else can we do to make get_with_redirects safer?
def load_url_title def load_url_title
authorize :Hack authorize :Hack
url = params[:url] url = params[:url]

View file

@ -20,6 +20,7 @@ class MapsController < ApplicationController
end end
format.json { render json: @map } format.json { render json: @map }
format.csv { redirect_to action: :export, format: :csv } format.csv { redirect_to action: :export, format: :csv }
format.ttl { redirect_to action: :export, format: :ttl }
end end
end end
@ -90,10 +91,12 @@ class MapsController < ApplicationController
# GET maps/:id/export # GET maps/:id/export
def export def export
exporter = MapExportService.new(current_user, @map) exporter = MapExportService.new(current_user, @map, base_url: request.base_url)
respond_to do |format| respond_to do |format|
format.json { render json: exporter.json } format.json { render json: exporter.json }
format.csv { send_data exporter.csv } format.csv { send_data exporter.csv }
format.ttl { render text: exporter.rdf }
end end
end end
@ -103,9 +106,6 @@ class MapsController < ApplicationController
if params[:event] == 'conversation' if params[:event] == 'conversation'
Events::ConversationStartedOnMap.publish!(@map, current_user) Events::ConversationStartedOnMap.publish!(@map, current_user)
valid_event = true valid_event = true
elsif params[:event] == 'user_presence'
Events::UserPresentOnMap.publish!(@map, current_user)
valid_event = true
end end
respond_to do |format| respond_to do |format|

View file

@ -55,8 +55,13 @@ class MetacodeSetsController < ApplicationController
@metacodes.each do |m| @metacodes.each do |m|
InMetacodeSet.create(metacode_id: m, metacode_set_id: @metacode_set.id) InMetacodeSet.create(metacode_id: m, metacode_set_id: @metacode_set.id)
end end
format.html { redirect_to metacode_sets_url, notice: 'Metacode set was successfully created.' } format.html do
format.json { render json: @metacode_set, status: :created, location: metacode_sets_url } 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 else
format.html { render action: 'new' } format.html { render action: 'new' }
format.json { render json: @metacode_set.errors, status: :unprocessable_entity } 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) if @metacode_set.update_attributes(metacode_set_params)
# build an array of the IDs of the metacodes currently in the set # 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 # 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 # remove the metacodes that were in it, but now aren't
@removedMetacodes = @currentMetacodes - @newMetacodes removed_metacodes = current_metacodes - new_metacodes
@removedMetacodes.each do |m| removed_metacodes.each do |m|
@inmetacodeset = InMetacodeSet.find_by_metacode_id_and_metacode_set_id(m, @metacode_set.id) inmetacodeset = InMetacodeSet.find_by(metacode_id: m, metacode_set_id: @metacode_set.id)
@inmetacodeset.destroy inmetacodeset.destroy
end end
# add the new metacodes # add the new metacodes
@addedMetacodes = @newMetacodes - @currentMetacodes added_metacodes = new_metacodes - current_metacodes
@addedMetacodes.each do |m| added_metacodes.each do |m|
InMetacodeSet.create(metacode_id: m, metacode_set_id: @metacode_set.id) InMetacodeSet.create(metacode_id: m, metacode_set_id: @metacode_set.id)
end end

View file

@ -14,7 +14,8 @@ class SearchController < ApplicationController
term = params[:term] term = params[:term]
user = params[:user] ? params[:user] : false 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 # remove "topic:" if appended at beginning
term = term[6..-1] if term.downcase[0..5] == 'topic:' term = term[6..-1] if term.downcase[0..5] == 'topic:'
@ -34,28 +35,28 @@ class SearchController < ApplicationController
end end
# check whether there's a filter by metacode as part of the query # check whether there's a filter by metacode as part of the query
filterByMetacode = false filter_by_metacode = false
Metacode.all.each do |m| Metacode.all.each do |m|
lOne = m.name.length + 1 length_one = m.name.length + 1
lTwo = m.name.length length_two = m.name.length
if term.downcase[0..lTwo] == m.name.downcase + ':' if term.downcase[0..length_two] == m.name.downcase + ':'
term = term[lOne..-1] term = term[length_one..-1]
filterByMetacode = m filter_by_metacode = m
end end
end end
search = '%' + term.downcase.strip + '%' search = '%' + term.downcase.strip + '%'
builder = policy_scope(Topic) builder = policy_scope(Topic)
if filterByMetacode if filter_by_metacode
if term == '' if term == ''
builder = builder.none builder = builder.none
else else
builder = builder.where('LOWER("name") like ? OR builder = builder.where('LOWER("name") like ? OR
LOWER("desc") like ? OR LOWER("desc") like ? OR
LOWER("link") like ?', search, search, search) LOWER("link") like ?', search, search, search)
builder = builder.where(metacode_id: filterByMetacode.id) builder = builder.where(metacode_id: filter_by_metacode.id)
end end
elsif desc elsif desc
builder = builder.where('LOWER("desc") like ?', search) builder = builder.where('LOWER("desc") like ?', search)
@ -82,7 +83,8 @@ class SearchController < ApplicationController
term = params[:term] term = params[:term]
user = params[:user] ? params[:user] : nil 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 # remove "map:" if appended at beginning
term = term[4..-1] if term.downcase[0..3] == 'map:' term = term[4..-1] if term.downcase[0..3] == 'map:'
@ -115,7 +117,8 @@ class SearchController < ApplicationController
# get /search/mappers?term=SOMETERM # get /search/mappers?term=SOMETERM
def mappers def mappers
term = params[:term] 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 # remove "mapper:" if appended at beginning
term = term[7..-1] if term.downcase[0..6] == 'mapper:' term = term[7..-1] if term.downcase[0..6] == 'mapper:'
@ -138,13 +141,15 @@ class SearchController < ApplicationController
topic2id = params[:topic2id] topic2id = params[:topic2id]
if term && !term.empty? 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) @synapses = @synapses.uniq(&:desc)
elsif topic1id && !topic1id.empty? elsif topic1id && !topic1id.empty?
@one = policy_scope(Synapse).where(topic1_id: topic1id, topic2_id: topic2id) one = policy_scope(Synapse).where(topic1_id: topic1id, topic2_id: topic2id)
@two = policy_scope(Synapse).where(topic2_id: topic1id, topic1_id: topic2id) two = policy_scope(Synapse).where(topic2_id: topic1id, topic1_id: topic2id)
@synapses = @one + @two @synapses = one + two
@synapses.sort! { |s1, s2| s1.desc <=> s2.desc }.to_a @synapses.sort! { |s1, s2| s1.desc <=> s2.desc }.to_a
else else
skip_policy_scope skip_policy_scope

View file

@ -71,6 +71,8 @@ class SynapsesController < ApplicationController
private private
def synapse_params 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
end end

View file

@ -11,16 +11,20 @@ class TopicsController < ApplicationController
def autocomplete_topic def autocomplete_topic
term = params[:term] term = params[:term]
if term && !term.empty? if term && !term.empty?
@topics = policy_scope(Topic).where('LOWER("name") like ?', term.downcase + '%').order('"name"') topics = policy_scope(Topic)
@mapTopics = @topics.select { |t| t&.metacode&.name == 'Metamap' } .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 # prioritize topics which point to maps, over maps
@exclude = @mapTopics.length.positive? ? @mapTopics.map(&: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"') maps = policy_scope(Map)
.where('LOWER("name") like ? AND name NOT IN (?)', term.downcase + '%', exclude)
.order('"name"')
else else
@topics = [] topics = []
@maps = [] maps = []
end 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 render json: autocomplete_array_json(@all).to_json
end end
@ -70,13 +74,13 @@ class TopicsController < ApplicationController
@topic = Topic.find(params[:id]) @topic = Topic.find(params[:id])
authorize @topic 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 = 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 do |topic| alltopics.delete_if { |topic| topic.metacode_id != params[:metacode].to_i }
!topicsAlreadyHas.index(topic.id).nil?
end end
alltopics.delete_if { |topic| !topics_already_has.index(topic.id).nil? }
@json = Hash.new(0) @json = Hash.new(0)
alltopics.each do |t| alltopics.each do |t|
@ -93,12 +97,14 @@ class TopicsController < ApplicationController
@topic = Topic.find(params[:id]) @topic = Topic.find(params[:id])
authorize @topic 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 = 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| alltopics.delete_if do |topic|
!topicsAlreadyHas.index(topic.id.to_s).nil? !topics_already_has.index(topic.id.to_s).nil?
end end
# find synapses between topics in alltopics array # find synapses between topics in alltopics array
@ -108,9 +114,9 @@ class TopicsController < ApplicationController
!synapse_ids.index(synapse.id).nil? !synapse_ids.index(synapse.id).nil?
end 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| 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 end
@json = {} @json = {}

View file

@ -1,12 +1,14 @@
# frozen_string_literal: true # frozen_string_literal: true
class Users::PasswordsController < Devise::PasswordsController module Users
protected class PasswordsController < Devise::PasswordsController
protected
def after_resetting_password_path_for(resource) def after_resetting_password_path_for(resource)
signed_in_root_path(resource) signed_in_root_path(resource)
end end
def after_sending_reset_password_instructions_path_for(_resource_name) def after_sending_reset_password_instructions_path_for(_resource_name)
sign_in_path if is_navigational_format? sign_in_path if is_navigational_format?
end
end end
end end

View file

@ -1,37 +1,39 @@
# frozen_string_literal: true # frozen_string_literal: true
class Users::RegistrationsController < Devise::RegistrationsController module Users
before_action :configure_sign_up_params, only: [:create] class RegistrationsController < Devise::RegistrationsController
before_action :configure_account_update_params, only: [:update] before_action :configure_sign_up_params, only: [:create]
after_action :store_location, only: [:new] before_action :configure_account_update_params, only: [:update]
after_action :store_location, only: [:new]
protected protected
def after_update_path_for(resource) def after_update_path_for(resource)
signed_in_root_path(resource) signed_in_root_path(resource)
end end
def after_sign_in_path_for(resource) def after_sign_in_path_for(resource)
stored = stored_location_for(User) stored = stored_location_for(User)
return stored if stored return stored if stored
if request.referer&.match(sign_in_url) || request.referer&.match(sign_up_url) if request.referer&.match(sign_in_url) || request.referer&.match(sign_up_url)
super super
else else
request.referer || root_path 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
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 end

View file

@ -2,12 +2,11 @@
module ApplicationHelper module ApplicationHelper
def metacodeset def metacodeset
metacodes = current_user.settings.metacodes metacodes = current_user.settings.metacodes
return false unless metacodes[0].include?('metacodeset') return false unless metacodes[0].include?('metacodeset')
if metacodes[0].sub('metacodeset-', '') == 'Most' return 'Most' if metacodes[0].sub('metacodeset-', '') == 'Most'
return 'Most' return 'Recent' if metacodes[0].sub('metacodeset-', '') == 'Recent'
elsif metacodes[0].sub('metacodeset-', '') == 'Recent'
return 'Recent'
end
MetacodeSet.find(metacodes[0].sub('metacodeset-', '').to_i) MetacodeSet.find(metacodes[0].sub('metacodeset-', '').to_i)
end end

View file

@ -4,7 +4,7 @@ module TopicsHelper
def autocomplete_array_json(topics) def autocomplete_array_json(topics)
topics.map do |t| topics.map do |t|
is_map = t.is_a?(Map) is_map = t.is_a?(Map)
metamapMetacode = Metacode.find_by_name('Metamap') metamap_metacode = Metacode.find_by(name: 'Metamap')
{ {
id: t.id, id: t.id,
label: t.name, label: t.name,
@ -17,8 +17,8 @@ module TopicsHelper
rtype: is_map ? 'map' : 'topic', rtype: is_map ? 'map' : 'topic',
inmaps: is_map ? [] : t.inmaps(current_user), inmaps: is_map ? [] : t.inmaps(current_user),
inmapsLinks: is_map ? [] : t.inmapsLinks(current_user), inmapsLinks: is_map ? [] : t.inmapsLinks(current_user),
type: is_map ? metamapMetacode.name : t.metacode.name, type: is_map ? metamap_metacode.name : t.metacode.name,
typeImageURL: is_map ? metamapMetacode.icon : t.metacode.icon, typeImageURL: is_map ? metamap_metacode.icon : t.metacode.icon,
mapCount: is_map ? 0 : t.maps.count, mapCount: is_map ? 0 : t.maps.count,
synapseCount: is_map ? 0 : t.synapses.count synapseCount: is_map ? 0 : t.synapses.count
} }

View file

@ -12,7 +12,7 @@ class AccessRequest < ApplicationRecord
Mailboxer::Receipt.where(notification: notification).update_all(is_read: true) Mailboxer::Receipt.where(notification: notification).update_all(is_read: true)
end end
user_map = UserMap.create(user: user, map: map) UserMap.create(user: user, map: map)
NotificationService.access_approved(self) NotificationService.access_approved(self)
end end

38
app/models/attachment.rb Normal file
View 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

View 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

View file

@ -1,9 +1,10 @@
# frozen_string_literal: true # frozen_string_literal: true
class Event < ApplicationRecord class Event < ApplicationRecord
KINDS = %w(user_present_on_map conversation_started_on_map KINDS = %w(user_present_on_map user_not_present_on_map
topic_added_to_map topic_moved_on_map topic_removed_from_map conversation_started_on_map
synapse_added_to_map synapse_removed_from_map topic_added_to_map topic_moved_on_map topic_removed_from_map
topic_updated synapse_updated).freeze synapse_added_to_map synapse_removed_from_map
topic_updated synapse_updated).freeze
belongs_to :eventable, polymorphic: true belongs_to :eventable, polymorphic: true
belongs_to :map belongs_to :map

View file

@ -1,11 +1,13 @@
# frozen_string_literal: true # frozen_string_literal: true
class Events::ConversationStartedOnMap < Event module Events
# after_create :notify_users! class ConversationStartedOnMap < Event
# after_create :notify_users!
def self.publish!(map, user) def self.publish!(map, user)
create!(kind: 'conversation_started_on_map', create!(kind: 'conversation_started_on_map',
eventable: map, eventable: map,
map: map, map: map,
user: user) user: user)
end
end end
end end

View file

@ -1,12 +1,14 @@
# frozen_string_literal: true # frozen_string_literal: true
class Events::SynapseAddedToMap < Event module Events
# after_create :notify_users! class SynapseAddedToMap < Event
# after_create :notify_users!
def self.publish!(synapse, map, user, meta) def self.publish!(synapse, map, user, meta)
create!(kind: 'synapse_added_to_map', create!(kind: 'synapse_added_to_map',
eventable: synapse, eventable: synapse,
map: map, map: map,
user: user, user: user,
meta: meta) meta: meta)
end
end end
end end

View file

@ -1,12 +1,14 @@
# frozen_string_literal: true # frozen_string_literal: true
class Events::SynapseRemovedFromMap < Event module Events
# after_create :notify_users! class SynapseRemovedFromMap < Event
# after_create :notify_users!
def self.publish!(synapse, map, user, meta) def self.publish!(synapse, map, user, meta)
create!(kind: 'synapse_removed_from_map', create!(kind: 'synapse_removed_from_map',
eventable: synapse, eventable: synapse,
map: map, map: map,
user: user, user: user,
meta: meta) meta: meta)
end
end end
end end

View file

@ -1,11 +1,13 @@
# frozen_string_literal: true # frozen_string_literal: true
class Events::SynapseUpdated < Event module Events
# after_create :notify_users! class SynapseUpdated < Event
# after_create :notify_users!
def self.publish!(synapse, user, meta) def self.publish!(synapse, user, meta)
create!(kind: 'synapse_updated', create!(kind: 'synapse_updated',
eventable: synapse, eventable: synapse,
user: user, user: user,
meta: meta) meta: meta)
end
end end
end end

View file

@ -1,12 +1,14 @@
# frozen_string_literal: true # frozen_string_literal: true
class Events::TopicAddedToMap < Event module Events
# after_create :notify_users! class TopicAddedToMap < Event
# after_create :notify_users!
def self.publish!(topic, map, user, meta) def self.publish!(topic, map, user, meta)
create!(kind: 'topic_added_to_map', create!(kind: 'topic_added_to_map',
eventable: topic, eventable: topic,
map: map, map: map,
user: user, user: user,
meta: meta) meta: meta)
end
end end
end end

View file

@ -1,12 +1,14 @@
# frozen_string_literal: true # frozen_string_literal: true
class Events::TopicMovedOnMap < Event module Events
# after_create :notify_users! class TopicMovedOnMap < Event
# after_create :notify_users!
def self.publish!(topic, map, user, meta) def self.publish!(topic, map, user, meta)
create!(kind: 'topic_moved_on_map', create!(kind: 'topic_moved_on_map',
eventable: topic, eventable: topic,
map: map, map: map,
user: user, user: user,
meta: meta) meta: meta)
end
end end
end end

View file

@ -1,12 +1,14 @@
# frozen_string_literal: true # frozen_string_literal: true
class Events::TopicRemovedFromMap < Event module Events
# after_create :notify_users! class TopicRemovedFromMap < Event
# after_create :notify_users!
def self.publish!(topic, map, user, meta) def self.publish!(topic, map, user, meta)
create!(kind: 'topic_removed_from_map', create!(kind: 'topic_removed_from_map',
eventable: topic, eventable: topic,
map: map, map: map,
user: user, user: user,
meta: meta) meta: meta)
end
end end
end end

View file

@ -1,11 +1,13 @@
# frozen_string_literal: true # frozen_string_literal: true
class Events::TopicUpdated < Event module Events
# after_create :notify_users! class TopicUpdated < Event
# after_create :notify_users!
def self.publish!(topic, user, meta) def self.publish!(topic, user, meta)
create!(kind: 'topic_updated', create!(kind: 'topic_updated',
eventable: topic, eventable: topic,
user: user, user: user,
meta: meta) meta: meta)
end
end end
end end

View 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

View file

@ -1,11 +1,13 @@
# frozen_string_literal: true # frozen_string_literal: true
class Events::UserPresentOnMap < Event module Events
# after_create :notify_users! class UserPresentOnMap < Event
# after_create :notify_users!
def self.publish!(map, user) def self.publish!(map, user)
create!(kind: 'user_present_on_map', create!(kind: 'user_present_on_map',
eventable: map, eventable: map,
map: map, map: map,
user: user) user: user)
end
end end
end end

View file

@ -3,8 +3,10 @@ class Map < ApplicationRecord
belongs_to :user belongs_to :user
belongs_to :source, class_name: :Map belongs_to :source, class_name: :Map
has_many :topicmappings, -> { Mapping.topicmapping }, class_name: :Mapping, dependent: :destroy has_many :topicmappings, -> { Mapping.topicmapping },
has_many :synapsemappings, -> { Mapping.synapsemapping }, class_name: :Mapping, dependent: :destroy 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 :topics, through: :topicmappings, source: :mappable, source_type: 'Topic'
has_many :synapses, through: :synapsemappings, source: :mappable, source_type: 'Synapse' has_many :synapses, through: :synapsemappings, source: :mappable, source_type: 'Synapse'
has_many :messages, as: :resource, dependent: :destroy has_many :messages, as: :resource, dependent: :destroy
@ -21,7 +23,6 @@ class Map < ApplicationRecord
has_attached_file :screenshot, has_attached_file :screenshot,
styles: { styles: {
thumb: ['220x220#', :png] thumb: ['220x220#', :png]
#:full => ['940x630#', :png]
}, },
default_url: 'https://s3.amazonaws.com/metamaps-assets/site/missing-map-square.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) } validates :permission, inclusion: { in: Perm::ISSIONS.map(&:to_s) }
# Validate the attached image is image/jpg, image/png, etc # 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_update :after_updated
after_save :update_deferring_topics_and_synapses, if: :permission_changed? after_save :update_deferring_topics_and_synapses, if: :permission_changed?
@ -80,7 +81,12 @@ class Map < ApplicationRecord
end end
def as_json(_options = {}) 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[:created_at_clean] = created_at_str
json[:updated_at_clean] = updated_at_str json[:updated_at_clean] = updated_at_str
json json
@ -120,17 +126,16 @@ class Map < ApplicationRecord
end end
removed.compact removed.compact
end end
def after_updated def after_updated
attrs = ['name', 'desc', 'permission'] attrs = %w(name desc permission)
if attrs.any? {|k| changed_attributes.key?(k)} return unless attrs.any? { |k| changed_attributes.key?(k) }
ActionCable.server.broadcast 'map_' + id.to_s, type: 'mapUpdated' ActionCable.server.broadcast 'map_' + id.to_s, type: 'mapUpdated'
end
end end
def update_deferring_topics_and_synapses def update_deferring_topics_and_synapses
Topic.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_all(permission: permission) Synapse.where(defer_to_map_id: id).update(permission: permission)
end end
def invited_text def invited_text

View file

@ -27,7 +27,7 @@ class Mapping < ApplicationRecord
def after_created def after_created
if mappable_type == 'Topic' if mappable_type == 'Topic'
meta = {'mapping_id': id} meta = { 'x': xloc, 'y': yloc, 'mapping_id': id }
Events::TopicAddedToMap.publish!(mappable, map, user, meta) Events::TopicAddedToMap.publish!(mappable, map, user, meta)
ActionCable.server.broadcast 'map_' + map.id.to_s, type: 'topicAdded', topic: mappable.filtered, mapping_id: id ActionCable.server.broadcast 'map_' + map.id.to_s, type: 'topicAdded', topic: mappable.filtered, mapping_id: id
elsif mappable_type == 'Synapse' elsif mappable_type == 'Synapse'
@ -38,13 +38,14 @@ class Mapping < ApplicationRecord
synapse: mappable.filtered, synapse: mappable.filtered,
topic1: mappable.topic1.filtered, topic1: mappable.topic1.filtered,
topic2: mappable.topic2.filtered, topic2: mappable.topic2.filtered,
mapping_id: id) mapping_id: id
)
end end
end end
def after_updated def after_updated
if mappable_type == 'Topic' and (xloc_changed? or yloc_changed?) if (mappable_type == 'Topic') && (xloc_changed? || yloc_changed?)
meta = {'x': xloc, 'y': yloc, 'mapping_id': id} meta = { 'x': xloc, 'y': yloc, 'mapping_id': id }
Events::TopicMovedOnMap.publish!(mappable, map, updated_by, meta) 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 ActionCable.server.broadcast 'map_' + map.id.to_s, type: 'topicMoved', id: mappable.id, mapping_id: id, x: xloc, y: yloc
end end
@ -57,7 +58,7 @@ class Mapping < ApplicationRecord
mappable.save mappable.save
end end
meta = {'mapping_id': id} meta = { 'mapping_id': id }
if mappable_type == 'Topic' if mappable_type == 'Topic'
Events::TopicRemovedFromMap.publish!(mappable, map, updated_by, meta) Events::TopicRemovedFromMap.publish!(mappable, map, updated_by, meta)
ActionCable.server.broadcast 'map_' + map.id.to_s, type: 'topicRemoved', id: mappable.id, mapping_id: id ActionCable.server.broadcast 'map_' + map.id.to_s, type: 'topicRemoved', id: mappable.id, mapping_id: id

View file

@ -4,7 +4,7 @@ class Message < ApplicationRecord
belongs_to :resource, polymorphic: true belongs_to :resource, polymorphic: true
delegate :name, to: :user, prefix: true delegate :name, to: :user, prefix: true
after_create :after_created after_create :after_created
def user_image def user_image
@ -15,8 +15,8 @@ class Message < ApplicationRecord
json = super(methods: [:user_name, :user_image]) json = super(methods: [:user_name, :user_image])
json json
end end
def after_created 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
end end

View file

@ -22,6 +22,7 @@ class Synapse < ApplicationRecord
where(topic1_id: topic_id).or(where(topic2_id: topic_id)) where(topic1_id: topic_id).or(where(topic2_id: topic_id))
} }
before_create :set_perm_by_defer
after_update :after_updated after_update :after_updated
delegate :name, to: :user, prefix: true delegate :name, to: :user, prefix: true
@ -51,17 +52,35 @@ class Synapse < ApplicationRecord
super(methods: [:user_name, :user_image, :collaborator_ids]) super(methods: [:user_name, :user_image, :collaborator_ids])
end 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 def after_updated
attrs = ['desc', 'category', 'permission', 'defer_to_map_id'] attrs = %w(desc category permission defer_to_map_id)
if attrs.any? {|k| changed_attributes.key?(k)} if attrs.any? { |k| changed_attributes.key?(k) }
new = self.attributes.select {|k| attrs.include?(k) } new = attributes.select { |k| attrs.include?(k) }
old = changed_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 = new.merge(old) # we are prioritizing the old values, keeping them
meta['changed'] = changed_attributes.keys.select {|k| attrs.include?(k) } meta['changed'] = changed_attributes.keys.select { |k| attrs.include?(k) }
Events::SynapseUpdated.publish!(self, user, meta) 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 ActionCable.server.broadcast 'map_' + map.id.to_s, type: 'synapseUpdated', id: id
} end
end end
end end
end end

View file

@ -1,6 +1,7 @@
# frozen_string_literal: true # frozen_string_literal: true
class Topic < ApplicationRecord class Topic < ApplicationRecord
include TopicsHelper include TopicsHelper
include Attachable
belongs_to :user belongs_to :user
belongs_to :defer_to_map, class_name: 'Map', foreign_key: 'defer_to_map_id' belongs_to :defer_to_map, class_name: 'Map', foreign_key: 'defer_to_map_id'
@ -15,29 +16,13 @@ class Topic < ApplicationRecord
belongs_to :metacode belongs_to :metacode
before_create :set_perm_by_defer
before_create :create_metamap? before_create :create_metamap?
after_update :after_updated after_update :after_updated
validates :permission, presence: true validates :permission, presence: true
validates :permission, inclusion: { in: Perm::ISSIONS.map(&:to_s) } 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 def synapses
synapses1.or(synapses2) synapses1.or(synapses2)
end end
@ -82,6 +67,19 @@ class Topic < ApplicationRecord
map_count: map_count(options[:user]), synapse_count: synapse_count(options[:user])) map_count: map_count(options[:user]), synapse_count: synapse_count(options[:user]))
end 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 def collaborator_ids
if defer_to_map if defer_to_map
defer_to_map.editors.select { |mapper| mapper != user }.map(&:id) defer_to_map.editors.select { |mapper| mapper != user }.map(&:id)
@ -137,6 +135,10 @@ class Topic < ApplicationRecord
protected protected
def set_perm_by_defer
permission = defer_to_map.permission if defer_to_map
end
def create_metamap? def create_metamap?
return unless (link == '') && (metacode.name == 'Metamap') return unless (link == '') && (metacode.name == 'Metamap')
@ -147,16 +149,16 @@ class Topic < ApplicationRecord
end end
def after_updated def after_updated
attrs = ['name', 'desc', 'link', 'metacode_id', 'permission', 'defer_to_map_id'] attrs = %w(name desc link metacode_id permission defer_to_map_id)
if attrs.any? {|k| changed_attributes.key?(k)} if attrs.any? { |k| changed_attributes.key?(k) }
new = self.attributes.select {|k| attrs.include?(k) } new = attributes.select { |k| attrs.include?(k) }
old = changed_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 = new.merge(old) # we are prioritizing the old values, keeping them
meta['changed'] = changed_attributes.keys.select {|k| attrs.include?(k) } meta['changed'] = changed_attributes.keys.select { |k| attrs.include?(k) }
Events::TopicUpdated.publish!(self, user, meta) 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 ActionCable.server.broadcast 'map_' + map.id.to_s, type: 'topicUpdated', id: id
} end
end end
end end
end end

View file

@ -89,6 +89,17 @@ class User < ApplicationRecord
}.to_a.sort{ |a, b| b[1] <=> a[1] }.map{|i| i[0]}.slice(0, 5) }.to_a.sort{ |a, b| b[1] <=> a[1] }.map{|i| i[0]}.slice(0, 5)
end 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 def all_accessible_maps
maps + shared_maps maps + shared_maps
end end
@ -115,7 +126,7 @@ class User < ApplicationRecord
if code == joinedwithcode if code == joinedwithcode
update(generation: 0) update(generation: 0)
else else
update(generation: User.find_by_code(joinedwithcode).generation + 1) update(generation: User.find_by(code: joinedwithcode).generation + 1)
end end
end end

View file

@ -6,7 +6,7 @@ class UserPreference
array = [] array = []
%w(Action Aim Idea Question Note Wildcard Subject).each do |m| %w(Action Aim Idea Question Note Wildcard Subject).each do |m|
begin begin
metacode = Metacode.find_by_name(m) metacode = Metacode.find_by(name: m)
array.push(metacode.id.to_s) if metacode array.push(metacode.id.to_s) if metacode
rescue ActiveRecord::StatementInvalid rescue ActiveRecord::StatementInvalid
if m == 'Action' if m == 'Action'

View file

@ -2,7 +2,7 @@
class Webhooks::Slack::SynapseRemovedFromMap < Webhooks::Slack::Base class Webhooks::Slack::SynapseRemovedFromMap < Webhooks::Slack::Base
def text def text
connector = eventable.desc.empty? ? '->' : eventable.desc 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}*" "\"*#{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
end end

View file

@ -35,13 +35,29 @@ module Api
Pundit.policy_scope(scope[:current_user], object.send(attr))&.map(&:id) || [] Pundit.policy_scope(scope[:current_user], object.send(attr))&.map(&:id) || []
end end
has_many(attr, opts.merge(if: -> { embeds.include?(key) })) do 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 end
else else
id_opts = opts.merge(key: "#{key}_id") id_opts = opts.merge(key: "#{key}_id")
attribute("#{attr}_id".to_sym, attribute("#{attr}_id".to_sym,
id_opts.merge(unless: -> { embeds.include?(key) })) 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 end
end end

View file

@ -1,9 +1,11 @@
# frozen_string_literal: true # frozen_string_literal: true
class MapExportService class MapExportService
attr_reader :user, :map attr_reader :user, :map, :base_url
def initialize(user, map)
def initialize(user, map, opts = {})
@user = user @user = user
@map = map @map = map
@base_url = opts[:base_url] || 'https://metamaps.cc'
end end
def json def json
@ -22,6 +24,25 @@ class MapExportService
end end
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 private
def topic_headings def topic_headings

View file

@ -12,7 +12,7 @@
Metamaps.currentPage = "mapper"; Metamaps.currentPage = "mapper";
Metamaps.ServerData.Mapper = { Metamaps.ServerData.Mapper = {
models: <%= @maps.to_json.html_safe %>, models: <%= @maps.to_json.html_safe %>,
id: <%= params[:id] %> mapperId: <%= params[:id] %>
}; };
Metamaps.GlobalUI.Search.focus(); Metamaps.GlobalUI.Search.focus();
</script> </script>

View file

@ -15,6 +15,19 @@
<title><%= yield(:title) %></title> <title><%= yield(:title) %></title>
<%= csrf_meta_tags %> <%= csrf_meta_tags %>
<meta name="viewport" content="width=device-width, user-scalable=no"> <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" %> <%= stylesheet_link_tag "application", :media => "all" %>
<%= javascript_include_tag "application" %> <%= javascript_include_tag "application" %>

View file

@ -34,6 +34,11 @@
<p class="mapCreatedAt"><span>Created by:</span> {{user_name}} on {{created_at}}</p> <p class="mapCreatedAt"><span>Created by:</span> {{user_name}} on {{created_at}}</p>
<p class="mapEditedAt"><span>Last edited:</span> {{updated_at}}</p> <p class="mapEditedAt"><span>Last edited:</span> {{updated_at}}</p>
<div class="mapInfoButtonsWrapper"> <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="mapInfoDelete">
<div class="deleteMap"></div> <div class="deleteMap"></div>
<span>Delete</span> <span>Delete</span>

View file

@ -9,7 +9,7 @@
<% if current_user %> <% if current_user %>
<div class="requestTitle"> <div class="requestTitle">
Click here to name this map! Click here to name this map
</div> </div>
<% end %> <% 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="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> <p class="mapEditedAt"><span>Last edited:</span> <%= @map.updated_at.strftime("%m/%d/%Y") %></p>
<div class="mapInfoButtonsWrapper"> <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="mapInfoDelete">
<div class="deleteMap"></div> <div class="deleteMap"></div>
<span>Delete</span> <span>Delete</span>

View file

@ -29,4 +29,7 @@ Rails.application.configure do
# Expands the lines which load the assets # Expands the lines which load the assets
config.assets.debug = false config.assets.debug = false
config.assets.quiet = true config.assets.quiet = true
# S3 file storage
config.paperclip_defaults = {} # store on local machine for dev
end end

View file

@ -3,3 +3,6 @@
# Add new mime types for use in respond_to blocks: # Add new mime types for use in respond_to blocks:
# Mime::Type.register "text/richtext", :rtf # Mime::Type.register "text/richtext", :rtf
# RDF export
Mime::Type.register 'text/turtle', :ttl

View file

@ -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

View 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

View file

@ -1,4 +1,4 @@
# frozen_string_literal: true # 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_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 METAMAPS_LAST_UPDATED = `git log -1 --pretty='%ad'`.split(' ').values_at(1, 2, 4).join(' ').freeze

View file

@ -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] scope = opts[:scope]
auth.cookies.signed["#{scope}.id"] = user.id auth.cookies.signed["#{scope}.id"] = user.id
auth.cookies.signed["#{scope}.expires_at"] = 30.minutes.from_now auth.cookies.signed["#{scope}.expires_at"] = 30.minutes.from_now
end end
Warden::Manager.before_logout do |user, auth, opts| Warden::Manager.before_logout do |_user, auth, opts|
scope = opts[:scope] scope = opts[:scope]
auth.cookies.signed["#{scope}.id"] = nil auth.cookies.signed["#{scope}.id"] = nil
auth.cookies.signed["#{scope}.expires_at"] = nil auth.cookies.signed["#{scope}.expires_at"] = nil

View file

@ -14,6 +14,7 @@ Metamaps::Application.routes.draw do
get 'starred' get 'starred'
get 'mapper/:id', action: 'mapper' get 'mapper/:id', action: 'mapper'
end end
get :explore, to: redirect('/')
resources :maps, except: [:index, :edit] do resources :maps, except: [:index, :edit] do
member do member do

View 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

View file

@ -10,7 +10,7 @@
# #
# It's strongly recommended that you check this file into your version control system. # 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 # These are extensions that must be enabled in order to support this database
enable_extension "plpgsql" 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 t.index ["user_id"], name: "index_access_requests_on_user_id", using: :btree
end 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| create_table "delayed_jobs", force: :cascade do |t|
t.integer "priority", default: 0, null: false t.integer "priority", default: 0, null: false
t.integer "attempts", 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.text "link"
t.integer "user_id" t.integer "user_id"
t.integer "metacode_id" t.integer "metacode_id"
t.datetime "created_at", null: false t.datetime "created_at", null: false
t.datetime "updated_at", null: false t.datetime "updated_at", null: false
t.text "permission" 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.integer "defer_to_map_id"
t.index ["metacode_id"], name: "index_topics_on_metacode_id", using: :btree t.index ["metacode_id"], name: "index_topics_on_metacode_id", using: :btree
t.index ["user_id"], name: "index_topics_on_user_id", using: :btree t.index ["user_id"], name: "index_topics_on_user_id", using: :btree

View file

@ -38,7 +38,7 @@ Metacode.create(name: 'Process',
manual_icon: 'https://s3.amazonaws.com/metamaps-assets/metacodes/blueprint/96px/bp_process.png', manual_icon: 'https://s3.amazonaws.com/metamaps-assets/metacodes/blueprint/96px/bp_process.png',
color: '#BDB25E') 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', manual_icon: 'https://s3.amazonaws.com/metamaps-assets/metacodes/blueprint/96px/bp_futuredev.png',
color: '#25A17F') color: '#25A17F')
@ -70,7 +70,7 @@ Metacode.create(name: 'Need',
manual_icon: 'https://s3.amazonaws.com/metamaps-assets/metacodes/blueprint/96px/bp_need.png', manual_icon: 'https://s3.amazonaws.com/metamaps-assets/metacodes/blueprint/96px/bp_need.png',
color: '#D2A7D4') 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', manual_icon: 'https://s3.amazonaws.com/metamaps-assets/metacodes/blueprint/96px/bp_openissue.png',
color: '#9BBF71') color: '#9BBF71')
@ -142,7 +142,7 @@ Metacode.create(name: 'Aim',
manual_icon: 'https://s3.amazonaws.com/metamaps-assets/metacodes/generics/96px/gen_aim.png', manual_icon: 'https://s3.amazonaws.com/metamaps-assets/metacodes/generics/96px/gen_aim.png',
color: '#B0B0B0') 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', manual_icon: 'https://s3.amazonaws.com/metamaps-assets/metacodes/generics/96px/gen_goodpractice.png',
color: '#BD9E86') color: '#BD9E86')
@ -198,6 +198,10 @@ Metacode.create(name: 'Status',
manual_icon: 'https://s3.amazonaws.com/metamaps-assets/metacodes/generics/96px/gen_status.png', manual_icon: 'https://s3.amazonaws.com/metamaps-assets/metacodes/generics/96px/gen_status.png',
color: '#EFA7C0') 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', Metacode.create(name: 'Tool',
manual_icon: 'https://s3.amazonaws.com/metamaps-assets/metacodes/generics/96px/gen_tool.png', manual_icon: 'https://s3.amazonaws.com/metamaps-assets/metacodes/generics/96px/gen_tool.png',
color: '#828282') color: '#828282')

View file

@ -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> 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 required: false
type: string 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

View file

@ -14,12 +14,28 @@
#### Setup Postgres #### 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 sudo -u postgres psql
postgres=# CREATE USER metamaps WITH PASSWORD 'mycoolpassword' CREATEDB; postgres=# CREATE USER metamaps WITH PASSWORD 'mycoolpassword' CREATEDB;
postgres=# CREATE DATABASE metamap002_production OWNER metamaps; postgres=# CREATE DATABASE metamaps_production OWNER metamaps;
postgres=# \q 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: #### Install system-wide rvm:
sudo gpg --keyserver hkp://keys.gnupg.net \ sudo gpg --keyserver hkp://keys.gnupg.net \
@ -38,8 +54,15 @@
rvm user gemsets rvm user gemsets
git clone https://github.com/metamaps/metamaps \ git clone https://github.com/metamaps/metamaps \
--branch instance/mycoolinstance --branch instance/mycoolinstance
rvm install $(cat metamaps/.ruby-version) #ensure ruby is installed cat metamaps/.ruby-version
cd metamaps
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 gem install bundler
RAILS_ENV=production bundle install RAILS_ENV=production bundle install
@ -60,16 +83,11 @@ Run this in the metamaps directory, still as metamaps:
# create, load schema, seed # create, load schema, seed
bundle exec rails db:setup 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 #### 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 npm run build
bin/build-apidocs.sh bin/build-apidocs.sh
bundle exec rails assets:precompile bundle exec rails assets:precompile
@ -93,17 +111,18 @@ server to see what problems show up:
#### Realtime server: #### Realtime server:
sudo npm install -g forever 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 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 \ -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 start /home/metamaps/metamaps/realtime/realtime-server.js
#### Upstart service for delayed_worker: #### 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" 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 sudo service metamaps_delayed_job start
tail /var/log/upstart/metamaps_delayed_job.log 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??

View file

@ -1,5 +1,7 @@
/* global $, ActionCable */ /* global $, ActionCable */
import { indexOf } from 'lodash'
import Active from './Active' import Active from './Active'
import Control from './Control' import Control from './Control'
import Create from './Create' import Create from './Create'
@ -110,7 +112,7 @@ const Cable = {
if (edge.getData('mappings').length - 1 === 0) { if (edge.getData('mappings').length - 1 === 0) {
Control.hideEdge(edge) Control.hideEdge(edge)
} }
var index = indexOf(edge.getData('synapses'), synapse) var index = indexOf(edge.getData('synapses'), synapse)
edge.getData('mappings').splice(index, 1) edge.getData('mappings').splice(index, 1)
edge.getData('synapses').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 // 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 // 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) 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)) { 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) mapper = DataModel.Mappers.get(event.topic.user_id)
if (mapper === undefined) { if (mapper === undefined) {
Mapper.get(event.topic.user_id, function(m) { Mapper.get(event.topic.user_id, function(m) {

View file

@ -1,5 +1,3 @@
/* global $ */
import _ from 'lodash' import _ from 'lodash'
import outdent from 'outdent' import outdent from 'outdent'
@ -8,7 +6,6 @@ import DataModel from './DataModel'
import Engine from './Engine' import Engine from './Engine'
import Filter from './Filter' import Filter from './Filter'
import GlobalUI from './GlobalUI' import GlobalUI from './GlobalUI'
import JIT from './JIT'
import Mouse from './Mouse' import Mouse from './Mouse'
import Selected from './Selected' import Selected from './Selected'
import Settings from './Settings' import Settings from './Settings'
@ -99,7 +96,6 @@ const Control = {
var permToDelete = Active.Mapper.id === topic.get('user_id') || Active.Mapper.get('admin') var permToDelete = Active.Mapper.id === topic.get('user_id') || Active.Mapper.get('admin')
if (permToDelete) { if (permToDelete) {
var mappableid = topic.id
var mapping = node.getData('mapping') var mapping = node.getData('mapping')
topic.destroy() topic.destroy()
DataModel.Mappings.remove(mapping) DataModel.Mappings.remove(mapping)
@ -149,7 +145,6 @@ const Control = {
} }
var topic = node.getData('topic') var topic = node.getData('topic')
var mappableid = topic.id
var mapping = node.getData('mapping') var mapping = node.getData('mapping')
mapping.destroy() mapping.destroy()
DataModel.Topics.remove(topic) DataModel.Topics.remove(topic)
@ -268,7 +263,6 @@ const Control = {
if (edge.getData('synapses').length - 1 === 0) { if (edge.getData('synapses').length - 1 === 0) {
Control.hideEdge(edge) Control.hideEdge(edge)
} }
var mappableid = synapse.id
synapse.destroy() synapse.destroy()
// the server will destroy the mapping, we just need to remove it here // 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 synapse = edge.getData('synapses')[index]
var mapping = edge.getData('mappings')[index] var mapping = edge.getData('mappings')[index]
var mappableid = synapse.id
mapping.destroy() mapping.destroy()
DataModel.Synapses.remove(synapse) DataModel.Synapses.remove(synapse)

View file

@ -335,18 +335,18 @@ console.log(codesToSwitchToIds)
}, },
source: synapseBloodhound source: synapseBloodhound
}, },
{ {
name: 'existing_synapses', name: 'existing_synapses',
limit: 50, limit: 50,
display: function(s) { return s.label }, display: function(s) { return s.label },
templates: { templates: {
suggestion: function(s) { suggestion: function(s) {
return Hogan.compile($('#synapseAutocompleteTemplate').html()).render(s) return Hogan.compile($('#synapseAutocompleteTemplate').html()).render(s)
},
header: '<h3>Existing synapses</h3>'
}, },
source: existingSynapseBloodhound header: '<h3>Existing synapses</h3>'
}] },
source: existingSynapseBloodhound
}]
) )
$('#synapse_desc').keyup(function(e) { $('#synapse_desc').keyup(function(e) {

View file

@ -7,7 +7,6 @@ try { Backbone.$ = window.$ } catch (err) {}
import Active from '../Active' import Active from '../Active'
import InfoBox from '../Map/InfoBox' import InfoBox from '../Map/InfoBox'
import Mapper from '../Mapper' import Mapper from '../Mapper'
import Realtime from '../Realtime'
const Map = Backbone.Model.extend({ const Map = Backbone.Model.extend({
urlRoot: '/maps', urlRoot: '/maps',

View file

@ -1,5 +1,3 @@
/* global $ */
import _ from 'lodash' import _ from 'lodash'
import outdent from 'outdent' import outdent from 'outdent'
import Backbone from 'backbone' import Backbone from 'backbone'
@ -7,8 +5,6 @@ try { Backbone.$ = window.$ } catch (err) {}
import Active from '../Active' import Active from '../Active'
import Filter from '../Filter' import Filter from '../Filter'
import JIT from '../JIT'
import Realtime from '../Realtime'
import SynapseCard from '../SynapseCard' import SynapseCard from '../SynapseCard'
import Visualize from '../Visualize' import Visualize from '../Visualize'

View file

@ -1,5 +1,3 @@
/* global $ */
import _ from 'lodash' import _ from 'lodash'
import Backbone from 'backbone' import Backbone from 'backbone'
try { Backbone.$ = window.$ } catch (err) {} try { Backbone.$ = window.$ } catch (err) {}
@ -7,8 +5,6 @@ try { Backbone.$ = window.$ } catch (err) {}
import Active from '../Active' import Active from '../Active'
import Engine from '../Engine' import Engine from '../Engine'
import Filter from '../Filter' import Filter from '../Filter'
import JIT from '../JIT'
import Realtime from '../Realtime'
import TopicCard from '../TopicCard' import TopicCard from '../TopicCard'
import Visualize from '../Visualize' import Visualize from '../Visualize'

View file

@ -81,11 +81,11 @@ const DataModel = {
var myCollection = serverData.Mine ? serverData.Mine : [] var myCollection = serverData.Mine ? serverData.Mine : []
var sharedCollection = serverData.Shared ? serverData.Shared : [] var sharedCollection = serverData.Shared ? serverData.Shared : []
var starredCollection = serverData.Starred ? serverData.Starred : [] var starredCollection = serverData.Starred ? serverData.Starred : []
var mapperCollection = [] var mapperCollection = serverData.Mapper ? serverData.Mapper : []
var mapperOptionsObj = { id: 'mapper', sortBy: 'updated_at' } var mapperOptionsObj = { id: 'mapper', sortBy: 'updated_at' }
if (self.Maps.Mapper.mapperId) { if (serverData.Mapper && serverData.Mapper.mapperId) {
mapperCollection = serverData.Mapper.models mapperCollection = serverData.Mapper.models
mapperOptionsObj.mapperId = serverData.Mapper.id mapperOptionsObj.mapperId = serverData.Mapper.mapperId
} }
var featuredCollection = serverData.Featured ? serverData.Featured : [] var featuredCollection = serverData.Featured ? serverData.Featured : []
var activeCollection = serverData.Active ? serverData.Active : [] var activeCollection = serverData.Active ? serverData.Active : []

View file

@ -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(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 export default Debug

View file

@ -7,6 +7,7 @@ import outdent from 'outdent'
import ImportDialogBox from '../../components/ImportDialogBox' import ImportDialogBox from '../../components/ImportDialogBox'
import PasteInput from '../PasteInput' import PasteInput from '../PasteInput'
import Map from '../Map'
const ImportDialog = { const ImportDialog = {
openLightbox: null, openLightbox: null,
@ -24,14 +25,19 @@ const ImportDialog = {
`)) `))
ReactDOM.render(React.createElement(ImportDialogBox, { ReactDOM.render(React.createElement(ImportDialogBox, {
onFileAdded: PasteInput.handleFile, onFileAdded: PasteInput.handleFile,
exampleImageUrl: serverData['import-example.png'] exampleImageUrl: serverData['import-example.png'],
downloadScreenshot: ImportDialog.downloadScreenshot
}), $('.importDialogWrapper').get(0)) }), $('.importDialogWrapper').get(0))
}, },
show: function() { show: function() {
ImportDialog.openLightbox('import-dialog') ImportDialog.openLightbox('import-dialog')
}, },
hide: function() { hide: function() {
ImportDialog.closeLightbox('import-dialog') ImportDialog.closeLightbox()
},
downloadScreenshot: function() {
ImportDialog.hide()
Map.offerScreenshotDownload()
} }
} }

View file

@ -12,9 +12,11 @@ import NotificationIcon from './NotificationIcon'
const GlobalUI = { const GlobalUI = {
notifyTimeout: null, notifyTimeout: null,
notifyQueue: [],
notifying: false,
lightbox: null, lightbox: null,
init: function(serverData) { init: function(serverData) {
var self = GlobalUI const self = GlobalUI
self.Search.init(serverData) self.Search.init(serverData)
self.CreateMap.init(serverData) self.CreateMap.init(serverData)
@ -45,7 +47,7 @@ const GlobalUI = {
}, 200, 'easeInCubic', function() { $(this).hide() }) }, 200, 'easeInCubic', function() { $(this).hide() })
}, },
openLightbox: function(which) { openLightbox: function(which) {
var self = GlobalUI const self = GlobalUI
$('.lightboxContent').hide() $('.lightboxContent').hide()
$('#' + which).show() $('#' + which).show()
@ -72,7 +74,7 @@ const GlobalUI = {
}, },
closeLightbox: function(event) { closeLightbox: function(event) {
var self = GlobalUI const self = GlobalUI
if (event) event.preventDefault() if (event) event.preventDefault()
@ -96,23 +98,45 @@ const GlobalUI = {
} }
self.lightbox = null self.lightbox = null
}, },
notifyUser: function(message, leaveOpen) { notifyUser: function(message, opts = {}) {
var self = GlobalUI 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) $('#toast').html(message)
self.showDiv('#toast') self.showDiv('#toast')
clearTimeout(self.notifyTimeOut) clearTimeout(self.notifyTimeOut)
if (!leaveOpen) { if (!leaveOpen) {
self.notifyTimeOut = setTimeout(function() { self.notifyTimeOut = setTimeout(function() {
self.hideDiv('#toast') GlobalUI.clearNotify()
}, 8000) }, timeOut)
} }
self.notifying = true
}, },
clearNotify: function() { clearNotify: function() {
var self = GlobalUI const self = GlobalUI
clearTimeout(self.notifyTimeOut) // if there are messages remaining, display them
self.hideDiv('#toast') 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) { shareInvite: function(inviteLink) {
clipboard.copy({ clipboard.copy({

View file

@ -374,7 +374,7 @@ const Import = {
$.get('/hacks/load_url_title', { $.get('/hacks/load_url_title', {
url url
}, function success(data, textStatus) { }, 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' var selector = '#showcard #topic_' + topic.get('id') + ' .best_in_place'
if ($(selector).find('form').length > 0) { if ($(selector).find('form').length > 0) {
$(selector).find('textarea, input').val(data.title) $(selector).find('textarea, input').val(data.title)

View file

@ -20,7 +20,7 @@ const Listeners = {
if (!(Active.Map || Active.Topic)) return if (!(Active.Map || Active.Topic)) return
const onCanvas = e.target.tagName === 'BODY' const onCanvas = e.target.tagName === 'BODY'
switch (e.which) { switch (e.which) {
case 13: // if enter key is pressed case 13: // if enter key is pressed
// prevent topic creation if sending a message // prevent topic creation if sending a message
@ -31,6 +31,10 @@ const Listeners = {
case 27: // if esc key is pressed case 27: // if esc key is pressed
JIT.escKeyHandler() JIT.escKeyHandler()
break break
case 46: // if DEL is pressed
e.preventDefault()
Control.deleteSelected()
break
case 65: // if a or A is pressed case 65: // if a or A is pressed
if ((e.ctrlKey || e.metaKey) && onCanvas) { if ((e.ctrlKey || e.metaKey) && onCanvas) {
const nodesCount = Object.keys(Visualize.mGraph.graph.nodes).length const nodesCount = Object.keys(Visualize.mGraph.graph.nodes).length
@ -124,7 +128,6 @@ const Listeners = {
break break
} }
}) })
$(window).resize(function() { $(window).resize(function() {
if (Visualize && Visualize.mGraph) { if (Visualize && Visualize.mGraph) {
Util.resizeCanvas(Visualize.mGraph.canvas) Util.resizeCanvas(Visualize.mGraph.canvas)

View file

@ -35,9 +35,11 @@ const InfoBox = {
data-bip-value="{{desc}}" data-bip-value="{{desc}}"
>{{desc}}</span>`, >{{desc}}</span>`,
userImageUrl: '', userImageUrl: '',
init: function(serverData) { init: function(serverData, updateThumbnail) {
var self = InfoBox var self = InfoBox
self.updateThumbnail = updateThumbnail
$('.mapInfoIcon').click(self.toggleBox) $('.mapInfoIcon').click(self.toggleBox)
$('.mapInfoBox').click(function(event) { $('.mapInfoBox').click(function(event) {
event.stopPropagation() event.stopPropagation()
@ -181,6 +183,7 @@ const InfoBox = {
$('.mapInfoBox.yourMap').unbind('.yourMap').bind('click.yourMap', self.hidePermissionSelect) $('.mapInfoBox.yourMap').unbind('.yourMap').bind('click.yourMap', self.hidePermissionSelect)
$('.yourMap .mapInfoDelete').unbind().click(self.deleteActiveMap) $('.yourMap .mapInfoDelete').unbind().click(self.deleteActiveMap)
$('.mapInfoThumbnail').unbind().click(self.updateThumbnail)
$('.mapContributors span, #mapContribs').unbind().click(function(event) { $('.mapContributors span, #mapContribs').unbind().click(function(event) {
$('.mapContributors .tip').toggle() $('.mapContributors .tip').toggle()

View file

@ -46,7 +46,10 @@ const Map = {
GlobalUI.CreateMap.emptyForkMapForm = $('#fork_map').html() GlobalUI.CreateMap.emptyForkMapForm = $('#fork_map').html()
self.updateStar() self.updateStar()
InfoBox.init(serverData)
InfoBox.init(serverData, function updateThumbnail() {
self.uploadMapScreenshot()
})
CheatSheet.init(serverData) CheatSheet.init(serverData)
$('.viewOnly .requestAccess').click(self.requestAccess) $('.viewOnly .requestAccess').click(self.requestAccess)
@ -253,6 +256,48 @@ const Map = {
} }
}, },
exportImage: function() { 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 = {} var canvas = {}
canvas.canvas = document.createElement('canvas') canvas.canvas = document.createElement('canvas')
@ -338,8 +383,9 @@ const Map = {
node.visited = !T node.visited = !T
}) })
var map = Active.Map return canvas
},
getMapScreenshotFilename: map => {
var today = new Date() var today = new Date()
var dd = today.getDate() var dd = today.getDate()
var mm = today.getMonth() + 1 // January is 0! var mm = today.getMonth() + 1 // January is 0!
@ -354,30 +400,7 @@ const Map = {
var mapName = map.get('name').split(' ').join(['-']) var mapName = map.get('name').split(' ').join(['-'])
const filename = `metamap-${map.id}-${mapName}-${today}.png` const filename = `metamap-${map.id}-${mapName}-${today}.png`
return filename
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')
}
})
})
} }
} }

View file

@ -6,6 +6,7 @@ import Visualize from './Visualize'
const PasteInput = { const PasteInput = {
// thanks to https://github.com/kevva/url-regex // 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"]*)?$'), 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() { init: function() {

View file

@ -14,7 +14,7 @@ module.exports = {
SEND_COORDS: 'SEND_COORDS', SEND_COORDS: 'SEND_COORDS',
DRAG_TOPIC: 'DRAG_TOPIC', DRAG_TOPIC: 'DRAG_TOPIC',
/* EVENTS RECEIVABLE FROM NODE SERVER*/ /* EVENTS RECEIVABLE FROM NODE SERVER */
JUNTO_UPDATED: 'JUNTO_UPDATED', JUNTO_UPDATED: 'JUNTO_UPDATED',
INVITED_TO_CALL: 'INVITED_TO_CALL', INVITED_TO_CALL: 'INVITED_TO_CALL',
INVITED_TO_JOIN: 'INVITED_TO_JOIN', INVITED_TO_JOIN: 'INVITED_TO_JOIN',
@ -29,5 +29,5 @@ module.exports = {
NEW_MAPPER: 'NEW_MAPPER', NEW_MAPPER: 'NEW_MAPPER',
LOST_MAPPER: 'LOST_MAPPER', LOST_MAPPER: 'LOST_MAPPER',
TOPIC_DRAGGED: 'TOPIC_DRAGGED', TOPIC_DRAGGED: 'TOPIC_DRAGGED',
PEER_COORDS_UPDATED: 'PEER_COORDS_UPDATED', PEER_COORDS_UPDATED: 'PEER_COORDS_UPDATED'
} }

View file

@ -9,8 +9,7 @@ import Create from '../Create'
import DataModel from '../DataModel' import DataModel from '../DataModel'
import JIT from '../JIT' import JIT from '../JIT'
import Util from '../Util' import Util from '../Util'
import Views from '../Views' import Views, { ChatView } from '../Views'
import { ChatView } from '../Views'
import Visualize from '../Visualize' import Visualize from '../Visualize'
import { import {
@ -153,7 +152,7 @@ let Realtime = {
config: { DOUBLE_CLICK_TOLERANCE: 200 } config: { DOUBLE_CLICK_TOLERANCE: 200 }
}) })
self.room.videoAdded(self.handleVideoAdded) self.room.videoAdded(self.handleVideoAdded)
self.startActiveMap() self.startActiveMap()
} // if Active.Mapper } // if Active.Mapper
}, },

View file

@ -4,18 +4,12 @@
everthing in this file happens as a result of websocket events everthing in this file happens as a result of websocket events
*/ */
import { indexOf } from 'lodash'
import { JUNTO_UPDATED } from './events' import { JUNTO_UPDATED } from './events'
import Active from '../Active' import Active from '../Active'
import { ChatView } from '../Views' import { ChatView } from '../Views'
import DataModel from '../DataModel' import DataModel from '../DataModel'
import GlobalUI from '../GlobalUI' 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 Util from '../Util'
import Visualize from '../Visualize' import Visualize from '../Visualize'
@ -155,7 +149,7 @@ export const invitedToCall = self => inviter => {
notifyText += username + ' is inviting you to a conversation. Join live?' 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 yes">Yes</button>'
notifyText += ' <button type="button" class="toast-button button btn-no no">No</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.yes').click(e => self.acceptCall(inviter))
$('#toast button.no').click(e => self.denyCall(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?' 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 yes">Yes</button>'
notifyText += ' <button type="button" class="toast-button button btn-no no">No</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.yes').click(e => self.joinCall())
$('#toast button.no').click(e => self.denyInvite(inviter)) $('#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?" 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 yes">Yes</button>'
notifyText += ' <button type="button" class="toast-button button btn-no no">No</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.yes').click(e => self.joinCall())
$('#toast button.no').click(e => GlobalUI.clearNotify()) $('#toast button.no').click(e => GlobalUI.clearNotify())
ChatView.conversationInProgress() ChatView.conversationInProgress()
} }
export const callStarted = self => () => { export const callStarted = self => () => {
@ -218,7 +212,7 @@ export const callStarted = self => () => {
var notifyText = "There's a conversation starting, want to join?" 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">Yes</button>'
notifyText += ' <button type="button" class="toast-button button btn-no">No</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.yes').click(e => self.joinCall())
$('#toast button.no').click(e => GlobalUI.clearNotify()) $('#toast button.no').click(e => GlobalUI.clearNotify())
ChatView.conversationInProgress() ChatView.conversationInProgress()

View file

@ -39,7 +39,7 @@ const _Router = Backbone.Router.extend({
var navigate = function() { var navigate = function() {
self.timeoutId = setTimeout(function() { self.timeoutId = setTimeout(function() {
self.navigate('') self.navigateAndTrack('')
}, 300) }, 300)
} }
@ -121,7 +121,7 @@ const _Router = Backbone.Router.extend({
path += '/' + DataModel.Maps.Mapper.mapperId path += '/' + DataModel.Maps.Mapper.mapperId
} }
self.navigate(path) self.navigateAndTrack(path)
} }
var navigateTimeout = function() { var navigateTimeout = function() {
self.timeoutId = setTimeout(navigate, 300) self.timeoutId = setTimeout(navigate, 300)
@ -202,6 +202,11 @@ const _Router = Backbone.Router.extend({
const Router = new _Router() 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) { Router.intercept = function(evt) {
var segments var segments

View file

@ -1,6 +1,14 @@
/* global $ */ /* global $ */
import { Parser, HtmlRenderer } from 'commonmark' 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 = { const Util = {
// helper function to determine how many lines are needed // helper function to determine how many lines are needed
@ -150,6 +158,29 @@ const Util = {
canvas.scale(oldAttr.scaleX, oldAttr.scaleY) canvas.scale(oldAttr.scaleX, oldAttr.scaleY)
const newAttr = Util.logCanvasAttributes(canvas) const newAttr = Util.logCanvasAttributes(canvas)
canvas.translate(newAttr.centreCoords.x - oldAttr.centreCoords.x, newAttr.centreCoords.y - oldAttr.centreCoords.y) 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
} }
} }

View file

@ -49,7 +49,7 @@ const ChatView = {
$('#' + ChatView.domId).hide() $('#' + ChatView.domId).hide()
}, },
render: () => { render: () => {
if (!Active.Map) return if (!Active.Map) return
const self = ChatView const self = ChatView
self.mapChat = ReactDOM.render(React.createElement(MapChat, { self.mapChat = ReactDOM.render(React.createElement(MapChat, {
conversationLive: self.conversationLive, conversationLive: self.conversationLive,
@ -111,7 +111,7 @@ const ChatView = {
conversationInProgress: participating => { conversationInProgress: participating => {
ChatView.conversationLive = true ChatView.conversationLive = true
ChatView.isParticipating = participating ChatView.isParticipating = participating
ChatView.render() ChatView.render()
}, },
conversationEnded: () => { conversationEnded: () => {
ChatView.conversationLive = false ChatView.conversationLive = false
@ -144,7 +144,7 @@ const ChatView = {
}, },
addMessage: (message, isInitial, wasMe) => { addMessage: (message, isInitial, wasMe) => {
const self = ChatView const self = ChatView
if (!isInitial) self.mapChat.newMessage() if (!isInitial) self.mapChat.newMessage()
if (!wasMe && !isInitial && self.alertSound) self.sound.play('receivechat') if (!wasMe && !isInitial && self.alertSound) self.sound.play('receivechat')
self.messages.add(message) self.messages.add(message)
self.render() self.render()

View file

@ -1,13 +1,7 @@
/* global $ */ /* global $ */
import Backbone from 'backbone'
import attachMediaStream from 'attachmediastream' 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 Realtime from '../Realtime'
import VideoView from './VideoView' import VideoView from './VideoView'

View file

@ -9,7 +9,7 @@ import { JUNTO_UPDATED } from '../Realtime/events'
const Views = { const Views = {
init: (serverData) => { init: (serverData) => {
$(document).on(JUNTO_UPDATED, () => ExploreMaps.render()) $(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, ExploreMaps,
ChatView, ChatView,

View file

@ -154,8 +154,6 @@ const Visualize = {
self.mGraph.graph.empty() self.mGraph.graph.empty()
} }
if (self.type === 'ForceDirected' && Active.Mapper) $.post('/maps/' + Active.Map.id + '/events/user_presence')
function runAnimation() { function runAnimation() {
Loading.hide() Loading.hide()
$('#new_topic').show() $('#new_topic').show()
@ -216,9 +214,9 @@ const Visualize = {
var t = Active.Topic var t = Active.Topic
if (m && window.location.pathname !== '/maps/' + m.id) { 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) { } else if (t && window.location.pathname !== '/topics/' + t.id) {
Router.navigate('/topics/' + t.id) Router.navigateAndTrack('/topics/' + t.id)
} }
}, 800) }, 800)
}, },

View file

@ -6,7 +6,6 @@ class ImportDialogBox extends Component {
super(props) super(props)
this.state = { this.state = {
showImportInstructions: false
} }
} }
@ -15,21 +14,9 @@ class ImportDialogBox extends Component {
} }
handleFile = (files, e) => { 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]) this.props.onFileAdded(files[0])
} }
toggleShowInstructions = e => {
this.setState({
showImportInstructions: !this.state.showImportInstructions
})
}
render = () => { render = () => {
return ( return (
<div className="import-dialog"> <div className="import-dialog">
@ -40,6 +27,9 @@ class ImportDialogBox extends Component {
<div className="import-blue-button" onClick={this.handleExport('json')}> <div className="import-blue-button" onClick={this.handleExport('json')}>
Export as JSON Export as JSON
</div> </div>
<div className="import-blue-button" onClick={this.props.downloadScreenshot}>
Download screenshot
</div>
<h3>IMPORT</h3> <h3>IMPORT</h3>
<p>To upload a file, drop it here:</p> <p>To upload a file, drop it here:</p>
<Dropzone onDropAccepted={this.handleFile} <Dropzone onDropAccepted={this.handleFile}
@ -47,23 +37,7 @@ class ImportDialogBox extends Component {
> >
Drop files here! Drop files here!
</Dropzone> </Dropzone>
<p> <p>See <a href="https://docs.metamaps.cc/importing_and_exporting_data.html">docs.metamaps.cc</a> for instructions.</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>&nbsp;</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>)}
</div> </div>
) )
} }
@ -71,7 +45,8 @@ class ImportDialogBox extends Component {
ImportDialogBox.propTypes = { ImportDialogBox.propTypes = {
onFileAdded: PropTypes.func, onFileAdded: PropTypes.func,
exampleImageUrl: PropTypes.string exampleImageUrl: PropTypes.string,
downloadScreenshot: PropTypes.func
} }
export default ImportDialogBox export default ImportDialogBox

View file

@ -1,5 +1,6 @@
import React from 'react' import React from 'react'
import Autolinker from 'autolinker' import Autolinker from 'autolinker'
import Util from '../../Metamaps/Util'
const linker = new Autolinker({ newWindow: true, truncate: 50, email: false, phone: false }) const linker = new Autolinker({ newWindow: true, truncate: 50, email: false, phone: false })
@ -10,24 +11,25 @@ function addZero(i) {
return i return i
} }
function formatDate(created_at) { function formatDate(createdAt) {
let date = new Date(created_at) let date = new Date(createdAt)
let formatted = (date.getMonth() + 1) + '/' + date.getDate() let formatted = (date.getMonth() + 1) + '/' + date.getDate()
formatted += ' ' + addZero(date.getHours()) + ':' + addZero(date.getMinutes()) formatted += ' ' + addZero(date.getHours()) + ':' + addZero(date.getMinutes())
return formatted return formatted
} }
const Message = props => { const Message = props => {
const { user_image, user_name, message, created_at, heading } = props const { user_image: userImage, user_name: userName, message, created_at: createdAt, heading } = props
const messageHtml = {__html: linker.link(message)} const messageHtml = {__html: linker.link(Util.addEmoji(message, { emoticons: false }))}
return ( return (
<div className="chat-message"> <div className="chat-message">
<div className="chat-message-user"> <div className="chat-message-user">
{heading && <img src={user_image} />} {heading && <img src={userImage} />}
</div> </div>
{heading && <div className="chat-message-meta"> {heading && <div className="chat-message-meta">
<span className='chat-message-username'>{user_name}</span>&nbsp; <span className='chat-message-username'>{userName}</span>&nbsp;
<span className='chat-message-time'>{formatDate(created_at)}</span> <span className='chat-message-time'>{formatDate(createdAt)}</span>
</div>} </div>}
<div className="chat-message-text" dangerouslySetInnerHTML={messageHtml}></div> <div className="chat-message-text" dangerouslySetInnerHTML={messageHtml}></div>
<div className="clearfloat"></div> <div className="clearfloat"></div>

View 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

View file

@ -2,28 +2,28 @@ import React, { PropTypes, Component } from 'react'
class Participant extends Component { class Participant extends Component {
render() { 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 ( return (
<div className={`participant participant-${id} ${self ? 'is-self' : ''}`}> <div className={`participant participant-${id} ${self ? 'is-self' : ''}`}>
<div className="chat-participant-image"> <div className="chat-participant-image">
<img src={image} style={{ border: `2px solid ${color}`}} /> <img src={image} style={{ border: `2px solid ${color}` }} />
</div> </div>
<div className="chat-participant-name"> <div className="chat-participant-name">
{username} {self ? '(me)' : ''} {username} {self ? '(me)' : ''}
</div> </div>
{!self && !conversationLive && <button {!self && !conversationLive && <button
className={`button chat-participant-invite-call ${isPending ? 'pending' : ''}`} className={`button chat-participant-invite-call ${isPending ? 'pending' : ''}`}
onClick={() => !isPending && this.props.inviteACall(id)} // Realtime.inviteACall(id) onClick={() => !isPending && this.props.inviteACall(id)} // Realtime.inviteACall(id)
/>} />}
{!self && mapperIsLive && !isParticipating && <button {!self && mapperIsLive && !isParticipating && <button
className={`button chat-participant-invite-join ${isPending ? 'pending' : ''}`} className={`button chat-participant-invite-join ${isPending ? 'pending' : ''}`}
onClick={() => !isPending && this.props.inviteToJoin(id)} // Realtime.inviteToJoin(id) onClick={() => !isPending && this.props.inviteToJoin(id)} // Realtime.inviteToJoin(id)
/>} />}
{isParticipating && <span className="chat-participant-participating"> {isParticipating && <span className="chat-participant-participating">
<div className="green-dot"></div> <div className="green-dot"></div>
</span>} </span>}
<div className="clearfloat"></div> <div className="clearfloat"></div>
</div> </div>
) )
} }
} }

View file

@ -2,6 +2,8 @@ import React, { PropTypes, Component } from 'react'
import Unread from './Unread' import Unread from './Unread'
import Participant from './Participant' import Participant from './Participant'
import Message from './Message' import Message from './Message'
import NewMessage from './NewMessage'
import Util from '../../Metamaps/Util'
function makeList(messages) { function makeList(messages) {
let currentHeader let currentHeader
@ -29,7 +31,7 @@ class MapChat extends Component {
messageText: '', messageText: '',
alertSound: true, // whether to play sounds on arrival of new messages or not alertSound: true, // whether to play sounds on arrival of new messages or not
cursorsShowing: true, cursorsShowing: true,
videosShowing: true videosShowing: true
} }
} }
@ -40,7 +42,7 @@ class MapChat extends Component {
messageText: '', messageText: '',
alertSound: true, // whether to play sounds on arrival of new messages or not alertSound: true, // whether to play sounds on arrival of new messages or not
cursorsShowing: true, cursorsShowing: true,
videosShowing: true videosShowing: true
}) })
} }
@ -94,12 +96,17 @@ class MapChat extends Component {
handleTextareaKeyUp = e => { handleTextareaKeyUp = e => {
if (e.which === 13) { if (e.which === 13) {
e.preventDefault() e.preventDefault()
const text = this.state.messageText const text = Util.removeEmoji(this.state.messageText)
this.props.handleInputMessage(text) this.props.handleInputMessage(text)
this.setState({ messageText: '' }) this.setState({ messageText: '' })
} }
} }
focusMessageInput = () => {
if (!this.messageInput) return
this.messageInput.focus()
}
render = () => { render = () => {
const rightOffset = this.state.open ? '0' : '-300px' const rightOffset = this.state.open ? '0' : '-300px'
const { conversationLive, isParticipating, participants, messages, inviteACall, inviteToJoin } = this.props const { conversationLive, isParticipating, participants, messages, inviteACall, inviteToJoin } = this.props
@ -116,12 +123,12 @@ class MapChat extends Component {
<div className="participants"> <div className="participants">
{conversationLive && <div className="conversation-live"> {conversationLive && <div className="conversation-live">
LIVE LIVE
{isParticipating && <span className="call-action leave" onClick={this.props.leaveCall}> {isParticipating && <span className="call-action leave" onClick={this.props.leaveCall}>
LEAVE LEAVE
</span>} </span>}
{!isParticipating && <span className="call-action join" onClick={this.props.joinCall}> {!isParticipating && <span className="call-action join" onClick={this.props.joinCall}>
JOIN JOIN
</span>} </span>}
</div>} </div>}
{participants.map(participant => <Participant {participants.map(participant => <Participant
key={participant.id} key={participant.id}
@ -140,17 +147,20 @@ class MapChat extends Component {
<div className="tooltips">Chat</div> <div className="tooltips">Chat</div>
<Unread count={unreadMessages} /> <Unread count={unreadMessages} />
</div> </div>
<div className="chat-messages" ref={div => this.messagesDiv = div}> <div className="chat-messages" ref={div => { this.messagesDiv = div }}>
{makeList(messages)} {makeList(messages)}
</div> </div>
<textarea className="chat-input" <NewMessage messageText={this.state.messageText}
ref={textarea => this.messageInput = textarea} focusMessageInput={this.focusMessageInput}
placeholder="Send a message..." handleChange={this.handleChange('messageText')}
value={this.state.messageText} textAreaProps={{
onChange={this.handleChange('messageText')} className: 'chat-input',
onKeyUp={this.handleTextareaKeyUp} ref: textarea => { this.messageInput = textarea },
onFocus={this.props.inputFocus} placeholder: 'Send a message...',
onBlur={this.props.inputBlur} onKeyUp: this.handleTextareaKeyUp,
onFocus: this.props.inputFocus,
onBlur: this.props.inputBlur
}}
/> />
</div> </div>
) )
@ -168,7 +178,7 @@ MapChat.propTypes = {
inviteToJoin: PropTypes.func, inviteToJoin: PropTypes.func,
videoToggleClick: PropTypes.func, videoToggleClick: PropTypes.func,
cursorToggleClick: PropTypes.func, cursorToggleClick: PropTypes.func,
soundToggleClick: PropTypes.func, soundToggleClick: PropTypes.func,
participants: PropTypes.arrayOf(PropTypes.shape({ participants: PropTypes.arrayOf(PropTypes.shape({
color: PropTypes.string, // css color color: PropTypes.string, // css color
id: PropTypes.number, id: PropTypes.number,

View file

@ -41,7 +41,7 @@ class Header extends Component {
linkClass={activeClass('active')} linkClass={activeClass('active')}
data-router="true" data-router="true"
text="All Maps" text="All Maps"
/> />
<MapLink show={signedIn && explore} <MapLink show={signedIn && explore}
href="/explore/mine" href="/explore/mine"
linkClass={activeClass('my')} linkClass={activeClass('my')}

View file

@ -112,7 +112,7 @@ class MapCard extends Component {
{ mobile && hasConversation && <div className='mobileHasConversation'><MapperList mappers={ mapperList } /></div> } { mobile && hasConversation && <div className='mobileHasConversation'><MapperList mappers={ mapperList } /></div> }
{ mobile && d && <div className="desc">{ d }</div> } { mobile && d && <div className="desc">{ d }</div> }
{ mobile && <div className='mobileMetadata'><Metadata map={ map } /></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') } /> <img className='creatorImage' src={ map.get('user_image') } />
<span className='creatorName'>{ map.get('user_name') }</span> <span className='creatorName'>{ map.get('user_name') }</span>
{ !map.authorizeToEdit(currentUser) && <div className='cardViewOnly'>View Only</div> } { !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