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_HOST='localhost'
export DB_PORT='5432'
export DB_NAME='metamap002'
export DB_NAME='metamaps'
export REALTIME_SERVER='http://localhost:5000'
export MAILER_DEFAULT_URL='localhost:3000'

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:
Enabled: false
Style/EmptyMethod:
EnforcedStyle: expanded

View file

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

View file

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

View file

@ -2,6 +2,7 @@ Metamaps
=======
[![Build Status](https://travis-ci.org/metamaps/metamaps.svg?branch=develop)](https://travis-ci.org/metamaps/metamaps)
[![Code Climate](https://codeclimate.com/github/metamaps/metamaps/badges/gpa.svg)](https://codeclimate.com/github/metamaps/metamaps)
## What is Metamaps?
@ -16,8 +17,14 @@ Metamaps is developed and maintained by a distributed, nomadic community compris
- Contact: [team@metamaps.cc](mailto:team@metamaps.cc) or [@metamapps](https://twitter.com/metamapps) on Twitter
- User Documentation: [docs.metamaps.cc](https://docs.metamaps.cc)
- User Community: [hylo.com/c/metamaps](https://www.hylo.com/c/metamaps)
- Development Roadmap: [github.com/metamaps/metamaps/milestones](https://github.com/metamaps/metamaps/milestones)
- To send us a personal message or request an invite to the open beta, get in touch with us via email, Twitter, or Hylo
- To see what we're developing, or to weigh in on what you'd like to see developed, see our [Metamaps Feedback and Features](https://trello.com/b/uFOA6a2x/metamaps-feedback-feature-ideas-requests) board on trello
- To follow along with, or contribute,to our design process, see our [Metamaps Design](https://trello.com/b/8HlCikOX/metamaps-design) board on trello
- To follow along with, or contribute to, our development process, see our [Github Issues and Pull Requests](https://github.com/metamaps/metamaps/issues)
- Request an invite to the open beta [here](https://metamaps.cc/request)
<!-- markdown hack to split two lists -->
- To send us a personal message get in touch with us via email, Twitter, or Hylo
- If you would like to report a bug, please check the [issues][contributing-issues] section in our [contributing instructions][contributing].
- If you would like to get set up as a developer, that's great! Read on for help getting your development environment set up.

View file

@ -1,3 +1,4 @@
// eslint-disable spaced-comment
// JS and CSS bundles
//= link_directory ../javascripts .js
//= 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
// listed below.
//
@ -10,7 +9,8 @@
//
// WARNING: THE FIRST BLANK LINE MARKS THE END OF WHAT'S TO BE PROCESSED, ANY BLANK LINE SHOULD
// GO AFTER THE REQUIRES BELOW.
//
//
/* eslint-disable spaced-comment */
//= require jquery
//= require jquery-ui
//= require jquery_ujs
@ -19,3 +19,4 @@
//= require ./webpacked/metamaps.bundle
//= require ./Metamaps.ServerData
//= require homepageVimeoFallback
/* eslint-enable spaced-comment */

View file

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

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;
margin: 0.75em;
padding: 0.75em;
padding-top: 0.85em;
height: 3em;
background-color: #AAB0FB;
border-radius: 0.3em;
@ -2029,6 +2030,7 @@ input.collaboratorSearchField {
position: relative;
cursor: pointer;
display: none;
padding: 0 2px;
}
.mapInfoShareIcon {
width: 24px;
@ -2068,6 +2070,43 @@ and it won't be important on password protected instances */
.yourMap .mapInfoDelete {
display: block;
}
.mapInfoButtonsWrapper .mapInfoThumbnail {
display: block;
background-image: url(<%= asset_path('screenshot_sprite.png') %>);
width: 32px;
height: 32px;
& > span {
bottom: -8px;
right: 2px;
font-size: 12px;
color: #e0e0e0;
&:hover {
color: white;
}
}
.tooltip {
display: none;
}
&:hover {
background-position: -32px 0;
.tooltip {
display: block;
position: absolute;
bottom: 30px;
background: black;
color: white;
border-radius: 2px;
padding: 3px 5px 2px 5px;
}
}
}
.mapInfoButtonsWrapper span {
position: absolute;
width: 100%;

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

View file

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

View file

@ -1,8 +1,17 @@
# frozen_string_literal: true
class MapChannel < ApplicationCable::Channel
# Called when the consumer has successfully
# become a subscriber of this channel.
def subscribed
return unless Pundit.policy(current_user, Map.find(params[:id])).show?
map = Map.find(params[:id])
return unless Pundit.policy(current_user, map).show?
stream_from "map_#{params[:id]}"
Events::UserPresentOnMap.publish!(map, current_user)
end
def unsubscribed
map = Map.find(params[:id])
return unless Pundit.policy(current_user, map).show?
Events::UserNotPresentOnMap.publish!(map, current_user)
end
end

View file

@ -1,8 +1,10 @@
# frozen_string_literal: true
class AccessController < ApplicationController
before_action :require_user, only: [:access, :access_request, :approve_access, :approve_access_post,
before_action :require_user, only: [:access, :access_request,
:approve_access, :approve_access_post,
:deny_access, :deny_access_post, :request_access]
before_action :set_map, only: [:access, :access_request, :approve_access, :approve_access_post,
before_action :set_map, only: [:access, :access_request,
:approve_access, :approve_access_post,
:deny_access, :deny_access_post, :request_access]
after_action :verify_authorized

View file

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

View file

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

View file

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

View file

@ -55,8 +55,13 @@ class MetacodeSetsController < ApplicationController
@metacodes.each do |m|
InMetacodeSet.create(metacode_id: m, metacode_set_id: @metacode_set.id)
end
format.html { redirect_to metacode_sets_url, notice: 'Metacode set was successfully created.' }
format.json { render json: @metacode_set, status: :created, location: metacode_sets_url }
format.html do
redirect_to metacode_sets_url,
notice: 'Metacode set was successfully created.'
end
format.json do
render json: @metacode_set, status: :created, location: metacode_sets_url
end
else
format.html { render action: 'new' }
format.json { render json: @metacode_set.errors, status: :unprocessable_entity }
@ -73,20 +78,20 @@ class MetacodeSetsController < ApplicationController
if @metacode_set.update_attributes(metacode_set_params)
# build an array of the IDs of the metacodes currently in the set
@currentMetacodes = @metacode_set.metacodes.map { |m| m.id.to_s }
current_metacodes = @metacode_set.metacodes.map { |m| m.id.to_s }
# get the list of desired metacodes for the set from the user input and build an array out of it
@newMetacodes = params[:metacodes][:value].split(',')
new_metacodes = params[:metacodes][:value].split(',')
# remove the metacodes that were in it, but now aren't
@removedMetacodes = @currentMetacodes - @newMetacodes
@removedMetacodes.each do |m|
@inmetacodeset = InMetacodeSet.find_by_metacode_id_and_metacode_set_id(m, @metacode_set.id)
@inmetacodeset.destroy
removed_metacodes = current_metacodes - new_metacodes
removed_metacodes.each do |m|
inmetacodeset = InMetacodeSet.find_by(metacode_id: m, metacode_set_id: @metacode_set.id)
inmetacodeset.destroy
end
# add the new metacodes
@addedMetacodes = @newMetacodes - @currentMetacodes
@addedMetacodes.each do |m|
added_metacodes = new_metacodes - current_metacodes
added_metacodes.each do |m|
InMetacodeSet.create(metacode_id: m, metacode_set_id: @metacode_set.id)
end

View file

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

View file

@ -71,6 +71,8 @@ class SynapsesController < ApplicationController
private
def synapse_params
params.require(:synapse).permit(:id, :desc, :category, :weight, :permission, :topic1_id, :topic2_id, :user_id)
params.require(:synapse).permit(
:id, :desc, :category, :weight, :permission, :topic1_id, :topic2_id, :user_id
)
end
end

View file

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

View file

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

View file

@ -1,37 +1,39 @@
# frozen_string_literal: true
class Users::RegistrationsController < Devise::RegistrationsController
before_action :configure_sign_up_params, only: [:create]
before_action :configure_account_update_params, only: [:update]
after_action :store_location, only: [:new]
module Users
class RegistrationsController < Devise::RegistrationsController
before_action :configure_sign_up_params, only: [:create]
before_action :configure_account_update_params, only: [:update]
after_action :store_location, only: [:new]
protected
protected
def after_update_path_for(resource)
signed_in_root_path(resource)
end
def after_update_path_for(resource)
signed_in_root_path(resource)
end
def after_sign_in_path_for(resource)
stored = stored_location_for(User)
return stored if stored
def after_sign_in_path_for(resource)
stored = stored_location_for(User)
return stored if stored
if request.referer&.match(sign_in_url) || request.referer&.match(sign_up_url)
super
else
request.referer || root_path
if request.referer&.match(sign_in_url) || request.referer&.match(sign_up_url)
super
else
request.referer || root_path
end
end
private
def store_location
store_location_for(User, params[:redirect_to]) if params[:redirect_to]
end
def configure_sign_up_params
devise_parameter_sanitizer.permit(:sign_up, keys: [:name, :joinedwithcode])
end
def configure_account_update_params
devise_parameter_sanitizer.permit(:account_update, keys: [:image])
end
end
private
def store_location
store_location_for(User, params[:redirect_to]) if params[:redirect_to]
end
def configure_sign_up_params
devise_parameter_sanitizer.permit(:sign_up, keys: [:name, :joinedwithcode])
end
def configure_account_update_params
devise_parameter_sanitizer.permit(:account_update, keys: [:image])
end
end

View file

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

View file

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

View file

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

38
app/models/attachment.rb Normal file
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
class Event < ApplicationRecord
KINDS = %w(user_present_on_map conversation_started_on_map
topic_added_to_map topic_moved_on_map topic_removed_from_map
synapse_added_to_map synapse_removed_from_map
topic_updated synapse_updated).freeze
KINDS = %w(user_present_on_map user_not_present_on_map
conversation_started_on_map
topic_added_to_map topic_moved_on_map topic_removed_from_map
synapse_added_to_map synapse_removed_from_map
topic_updated synapse_updated).freeze
belongs_to :eventable, polymorphic: true
belongs_to :map

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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
class Events::UserPresentOnMap < Event
# after_create :notify_users!
module Events
class UserPresentOnMap < Event
# after_create :notify_users!
def self.publish!(map, user)
create!(kind: 'user_present_on_map',
eventable: map,
map: map,
user: user)
def self.publish!(map, user)
create!(kind: 'user_present_on_map',
eventable: map,
map: map,
user: user)
end
end
end

View file

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

View file

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

View file

@ -4,7 +4,7 @@ class Message < ApplicationRecord
belongs_to :resource, polymorphic: true
delegate :name, to: :user, prefix: true
after_create :after_created
def user_image
@ -15,8 +15,8 @@ class Message < ApplicationRecord
json = super(methods: [:user_name, :user_image])
json
end
def after_created
ActionCable.server.broadcast 'map_' + resource.id.to_s, type: 'messageCreated', message: self.as_json
ActionCable.server.broadcast 'map_' + resource.id.to_s, type: 'messageCreated', message: as_json
end
end

View file

@ -22,6 +22,7 @@ class Synapse < ApplicationRecord
where(topic1_id: topic_id).or(where(topic2_id: topic_id))
}
before_create :set_perm_by_defer
after_update :after_updated
delegate :name, to: :user, prefix: true
@ -51,17 +52,35 @@ class Synapse < ApplicationRecord
super(methods: [:user_name, :user_image, :collaborator_ids])
end
def as_rdf
output = ''
output += %(d:synapse_#{id} a mm:Synapse ;\n)
output += %( mm:topic1 d:topic_#{topic1_id} ;\n)
output += %( mm:topic2 d:topic_#{topic2_id} ;\n)
output += %( mm:direction "#{category}" ;\n)
output += %( rdfs:comment "#{desc}" ;\n) if desc.present?
output[-2] = '.'
output += %(\n)
output
end
protected
def set_perm_by_defer
permission = defer_to_map.permission if defer_to_map
end
def after_updated
attrs = ['desc', 'category', 'permission', 'defer_to_map_id']
if attrs.any? {|k| changed_attributes.key?(k)}
new = self.attributes.select {|k| attrs.include?(k) }
old = changed_attributes.select {|k| attrs.include?(k) }
meta = new.merge(old) # we are prioritizing the old values, keeping them
meta['changed'] = changed_attributes.keys.select {|k| attrs.include?(k) }
attrs = %w(desc category permission defer_to_map_id)
if attrs.any? { |k| changed_attributes.key?(k) }
new = attributes.select { |k| attrs.include?(k) }
old = changed_attributes.select { |k| attrs.include?(k) }
meta = new.merge(old) # we are prioritizing the old values, keeping them
meta['changed'] = changed_attributes.keys.select { |k| attrs.include?(k) }
Events::SynapseUpdated.publish!(self, user, meta)
maps.each {|map|
maps.each do |map|
ActionCable.server.broadcast 'map_' + map.id.to_s, type: 'synapseUpdated', id: id
}
end
end
end
end

View file

@ -1,6 +1,7 @@
# frozen_string_literal: true
class Topic < ApplicationRecord
include TopicsHelper
include Attachable
belongs_to :user
belongs_to :defer_to_map, class_name: 'Map', foreign_key: 'defer_to_map_id'
@ -15,29 +16,13 @@ class Topic < ApplicationRecord
belongs_to :metacode
before_create :set_perm_by_defer
before_create :create_metamap?
after_update :after_updated
validates :permission, presence: true
validates :permission, inclusion: { in: Perm::ISSIONS.map(&:to_s) }
# This method associates the attribute ":image" with a file attachment
has_attached_file :image
# , styles: {
# thumb: '100x100>',
# square: '200x200#',
# medium: '300x300>'
# }
# Validate the attached image is image/jpg, image/png, etc
validates_attachment_content_type :image, content_type: /\Aimage\/.*\Z/
# This method associates the attribute ":image" with a file attachment
has_attached_file :audio
# Validate the attached audio is audio/wav, audio/mp3, etc
validates_attachment_content_type :audio, content_type: /\Aaudio\/.*\Z/
def synapses
synapses1.or(synapses2)
end
@ -82,6 +67,19 @@ class Topic < ApplicationRecord
map_count: map_count(options[:user]), synapse_count: synapse_count(options[:user]))
end
def as_rdf
output = ''
output += %(d:topic_#{id} a mm:Topic ;\n)
output += %( rdfs:label "#{name}" ;\n)
output += %( rdfs:comment "#{desc}" ;\n) if desc.present?
output += %( foaf:homepage <#{link}> ;\n) if link.present?
output += %( mm:mapper d:mapper_#{user_id} ;\n)
output += %( mm:metacode "#{metacode.name}" ;\n)
output[-2] = '.' # change last ; to a .
output += %(\n)
output
end
def collaborator_ids
if defer_to_map
defer_to_map.editors.select { |mapper| mapper != user }.map(&:id)
@ -137,6 +135,10 @@ class Topic < ApplicationRecord
protected
def set_perm_by_defer
permission = defer_to_map.permission if defer_to_map
end
def create_metamap?
return unless (link == '') && (metacode.name == 'Metamap')
@ -147,16 +149,16 @@ class Topic < ApplicationRecord
end
def after_updated
attrs = ['name', 'desc', 'link', 'metacode_id', 'permission', 'defer_to_map_id']
if attrs.any? {|k| changed_attributes.key?(k)}
new = self.attributes.select {|k| attrs.include?(k) }
old = changed_attributes.select {|k| attrs.include?(k) }
meta = new.merge(old) # we are prioritizing the old values, keeping them
meta['changed'] = changed_attributes.keys.select {|k| attrs.include?(k) }
attrs = %w(name desc link metacode_id permission defer_to_map_id)
if attrs.any? { |k| changed_attributes.key?(k) }
new = attributes.select { |k| attrs.include?(k) }
old = changed_attributes.select { |k| attrs.include?(k) }
meta = new.merge(old) # we are prioritizing the old values, keeping them
meta['changed'] = changed_attributes.keys.select { |k| attrs.include?(k) }
Events::TopicUpdated.publish!(self, user, meta)
maps.each {|map|
maps.each do |map|
ActionCable.server.broadcast 'map_' + map.id.to_s, type: 'topicUpdated', id: id
}
end
end
end
end

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)
end
def as_rdf(opts = {})
base_url = opts[:base_url] || 'https://metamaps.cc'
output = ''
output += %(d:mapper_#{id} a foaf:OnlineAccount ;\n)
output += %( foaf:accountName "#{name}" ;\n)
output += %( foaf:accountServiceHomepage "#{base_url}/mapper/#{id}" ;\n)
output[-2] = '.' # change last ; to a .
output += %(\n)
output
end
def all_accessible_maps
maps + shared_maps
end
@ -115,7 +126,7 @@ class User < ApplicationRecord
if code == joinedwithcode
update(generation: 0)
else
update(generation: User.find_by_code(joinedwithcode).generation + 1)
update(generation: User.find_by(code: joinedwithcode).generation + 1)
end
end

View file

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

View file

@ -2,7 +2,7 @@
class Webhooks::Slack::SynapseRemovedFromMap < Webhooks::Slack::Base
def text
connector = eventable.desc.empty? ? '->' : eventable.desc
# todo express correct directionality of arrows when desc is empty
# TODO: express correct directionality of arrows when desc is empty
"\"*#{eventable.topic1.name}* #{connector} *#{eventable.topic2.name}*\" was removed by *#{event.user.name}* as a connection from the map *#{view_map_on_metamaps}*"
end
end

View file

@ -35,13 +35,29 @@ module Api
Pundit.policy_scope(scope[:current_user], object.send(attr))&.map(&:id) || []
end
has_many(attr, opts.merge(if: -> { embeds.include?(key) })) do
Pundit.policy_scope(scope[:current_user], object.send(attr)) || []
list = Pundit.policy_scope(scope[:current_user], object.send(attr)) || []
child_serializer = "Api::V2::#{attr.to_s.singularize.camelize}Serializer".constantize
resource = ActiveModelSerializers::SerializableResource.new(
list,
each_serializer: child_serializer,
scope: scope.merge(embeds: [])
)
resource.as_json
end
else
id_opts = opts.merge(key: "#{key}_id")
attribute("#{attr}_id".to_sym,
id_opts.merge(unless: -> { embeds.include?(key) }))
attribute(key, opts.merge(if: -> { embeds.include?(key) }))
attribute(key, opts.merge(if: -> { embeds.include?(key) })) do |serializer|
object = serializer.object.send(key)
child_serializer = "Api::V2::#{object.class.name}Serializer".constantize
resource = ActiveModelSerializers::SerializableResource.new(
object,
serializer: child_serializer,
scope: scope.merge(embeds: [])
)
resource.as_json
end
end
end
end

View file

@ -1,9 +1,11 @@
# frozen_string_literal: true
class MapExportService
attr_reader :user, :map
def initialize(user, map)
attr_reader :user, :map, :base_url
def initialize(user, map, opts = {})
@user = user
@map = map
@base_url = opts[:base_url] || 'https://metamaps.cc'
end
def json
@ -22,6 +24,25 @@ class MapExportService
end
end
def rdf
output = ''
output += "PREFIX d: <#{base_url}/maps/#{map.id}>\n"
output += "PREFIX mm: <#{base_url}/owl/map.owl.ttl>\n"
output += "PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>\n"
output += "PREFIX foaf: <http://xmlns.com/foaf/0.1/>\n"
output += "\n"
map.contributors.each do |mapper|
output += mapper.as_rdf(base_url: base_url)
end
map.topics.each do |topic|
output += topic.as_rdf
end
map.synapses.each do |synapse|
output += synapse.as_rdf
end
output
end
private
def topic_headings

View file

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

View file

@ -15,6 +15,19 @@
<title><%= yield(:title) %></title>
<%= csrf_meta_tags %>
<meta name="viewport" content="width=device-width, user-scalable=no">
<% if controller.class.name == 'MapsController' && @map %>
<meta property="og:title" content="<%= @map.name %>" />
<meta property="og:type" content="website" />
<meta property="og:image" content="<%= @map.screenshot_url %>" />
<meta property="og:description" content="<%= @map.desc %>" />
<meta property="og:url" content="<%= request.original_url %>" />
<meta name="twitter:title" content="<%= @map.name %>" />
<meta name="twitter:image" content="<%= @map.screenshot_url %>" />
<meta name="twitter:description" content="<%= @map.desc %>" />
<meta name="twitter:url" content="<%= request.original_url %>" />
<% end %>
<%= stylesheet_link_tag "application", :media => "all" %>
<%= javascript_include_tag "application" %>

View file

@ -34,6 +34,11 @@
<p class="mapCreatedAt"><span>Created by:</span> {{user_name}} on {{created_at}}</p>
<p class="mapEditedAt"><span>Last edited:</span> {{updated_at}}</p>
<div class="mapInfoButtonsWrapper">
<div class="mapInfoThumbnail">
<div class="thumbnail"></div>
<div class="tooltip">Update Thumbnail</div>
<span>Thumb</span>
</div>
<div class="mapInfoDelete">
<div class="deleteMap"></div>
<span>Delete</span>

View file

@ -9,7 +9,7 @@
<% if current_user %>
<div class="requestTitle">
Click here to name this map!
Click here to name this map
</div>
<% end %>
@ -77,6 +77,11 @@
<p class="mapCreatedAt"><span>Created by:</span> <%= @map.user == user ? "You" : @map.user.name %> on <%= @map.created_at.strftime("%m/%d/%Y") %></p>
<p class="mapEditedAt"><span>Last edited:</span> <%= @map.updated_at.strftime("%m/%d/%Y") %></p>
<div class="mapInfoButtonsWrapper">
<div class="mapInfoThumbnail">
<div class="thumbnail"></div>
<div class="tooltip">Update Thumbnail</div>
<span>Thumb</span>
</div>
<div class="mapInfoDelete">
<div class="deleteMap"></div>
<span>Delete</span>

View file

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

View file

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

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
METAMAPS_VERSION = '3.0.1'
METAMAPS_VERSION = '3.2'
METAMAPS_BUILD = `git log -1 --pretty=%H`.chomp[0..11].freeze
METAMAPS_LAST_UPDATED = `git log -1 --pretty='%ad'`.split(' ').values_at(1, 2, 4).join(' ').freeze

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

View file

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

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.
ActiveRecord::Schema.define(version: 20161218183817) do
ActiveRecord::Schema.define(version: 20170122201451) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@ -26,6 +26,18 @@ ActiveRecord::Schema.define(version: 20161218183817) do
t.index ["user_id"], name: "index_access_requests_on_user_id", using: :btree
end
create_table "attachments", force: :cascade do |t|
t.string "attachable_type"
t.integer "attachable_id"
t.string "file_file_name"
t.string "file_content_type"
t.integer "file_file_size"
t.datetime "file_updated_at"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["attachable_type", "attachable_id"], name: "index_attachments_on_attachable_type_and_attachable_id", using: :btree
end
create_table "delayed_jobs", force: :cascade do |t|
t.integer "priority", default: 0, null: false
t.integer "attempts", default: 0, null: false
@ -269,17 +281,9 @@ ActiveRecord::Schema.define(version: 20161218183817) do
t.text "link"
t.integer "user_id"
t.integer "metacode_id"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.text "permission"
t.string "image_file_name", limit: 255
t.string "image_content_type", limit: 255
t.integer "image_file_size"
t.datetime "image_updated_at"
t.string "audio_file_name", limit: 255
t.string "audio_content_type", limit: 255
t.integer "audio_file_size"
t.datetime "audio_updated_at"
t.integer "defer_to_map_id"
t.index ["metacode_id"], name: "index_topics_on_metacode_id", using: :btree
t.index ["user_id"], name: "index_topics_on_user_id", using: :btree

View file

@ -38,7 +38,7 @@ Metacode.create(name: 'Process',
manual_icon: 'https://s3.amazonaws.com/metamaps-assets/metacodes/blueprint/96px/bp_process.png',
color: '#BDB25E')
Metacode.create(name: 'Future',
Metacode.create(name: 'Future Dev',
manual_icon: 'https://s3.amazonaws.com/metamaps-assets/metacodes/blueprint/96px/bp_futuredev.png',
color: '#25A17F')
@ -70,7 +70,7 @@ Metacode.create(name: 'Need',
manual_icon: 'https://s3.amazonaws.com/metamaps-assets/metacodes/blueprint/96px/bp_need.png',
color: '#D2A7D4')
Metacode.create(name: 'Open',
Metacode.create(name: 'Open Issue',
manual_icon: 'https://s3.amazonaws.com/metamaps-assets/metacodes/blueprint/96px/bp_openissue.png',
color: '#9BBF71')
@ -142,7 +142,7 @@ Metacode.create(name: 'Aim',
manual_icon: 'https://s3.amazonaws.com/metamaps-assets/metacodes/generics/96px/gen_aim.png',
color: '#B0B0B0')
Metacode.create(name: 'Good',
Metacode.create(name: 'Good Practice',
manual_icon: 'https://s3.amazonaws.com/metamaps-assets/metacodes/generics/96px/gen_goodpractice.png',
color: '#BD9E86')
@ -198,6 +198,10 @@ Metacode.create(name: 'Status',
manual_icon: 'https://s3.amazonaws.com/metamaps-assets/metacodes/generics/96px/gen_status.png',
color: '#EFA7C0')
Metacode.create(name: 'Story',
manual_icon: 'https://s3.amazonaws.com/metamaps-assets/metacodes/generics/96px/gen_story.png',
color: '#A7A2DC')
Metacode.create(name: 'Tool',
manual_icon: 'https://s3.amazonaws.com/metamaps-assets/metacodes/generics/96px/gen_tool.png',
color: '#828282')

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>
required: false
type: string
searchfields:
description: |
A comma-seperated list of columns to search. For instance, to search a topic's name and description (but not link field) for the string "cognition", you could use `?q=cognition&searchfields=name,desc`.
required: false
type: string

View file

@ -14,12 +14,28 @@
#### Setup Postgres
sudo apt-get install postgresql-9.4 #specify version!!
sudo apt-get install postgresql-9.4
# make sure you have development headers for postgres. The package name might be different on your distribution.
sudo apt-get install libpq-dev
sudo -u postgres psql
postgres=# CREATE USER metamaps WITH PASSWORD 'mycoolpassword' CREATEDB;
postgres=# CREATE DATABASE metamap002_production OWNER metamaps;
postgres=# CREATE DATABASE metamaps_production OWNER metamaps;
postgres=# \q
On some deploys, we have had problems with unicode encoding when trying to run `db:setup`. Running the commands in this Github gist resolved the issue: https://gist.github.com/amolkhanorkar/8706915. Try this link if you have problems
#### Install Node for javascript building
# this first line lets us use up-to-date versions of node.js
# instead of the old versions in the Ubuntu repositories
curl -sL https://deb.nodesource.com/setup_7.x | sudo -E bash -
sudo apt-get install nodejs
sudo ln -s /usr/bin/nodejs /usr/bin/node
### Install redis server for action cable
sudo apt-get install redis-server
#### Install system-wide rvm:
sudo gpg --keyserver hkp://keys.gnupg.net \
@ -38,8 +54,15 @@
rvm user gemsets
git clone https://github.com/metamaps/metamaps \
--branch instance/mycoolinstance
rvm install $(cat metamaps/.ruby-version) #ensure ruby is installed
cd metamaps
cat metamaps/.ruby-version
The last line tells you what version of ruby you need to install. For example, at the time of writing the version is 2.3.0. As your normal sudo-enabled user, run
sudo rvm install 2.3.0
Now switch back to the metamaps user and continue
cd /home/metamaps/metamaps
gem install bundler
RAILS_ENV=production bundle install
@ -60,16 +83,11 @@ Run this in the metamaps directory, still as metamaps:
# create, load schema, seed
bundle exec rails db:setup
#### Install node & ES6 modules
sudo aptitude install nodejs npm
sudo ln -s /usr/bin/nodejs /usr/bin/node
npm install
#### Precompile assets
This step depends on running npm install first; assets:precompile calls `npm install` and `bin/build-apidocs.sh`, both of which require node_modules to be installed. We suggest you run the commands separately this time to better catch any errors.
Note that `rails assets:precompile` will normally call `npm install` and `bin/build-apidocs.sh` as part of its process. Both of these latter commands require `npm install` to be run first. We suggest you run all five commands separately this time (like below) to better catch any errors. In the future, you won't need to run the second and third commands separately.
npm install
npm run build
bin/build-apidocs.sh
bundle exec rails assets:precompile
@ -93,17 +111,18 @@ server to see what problems show up:
#### Realtime server:
sudo npm install -g forever
(crontab -u metamaps -l 2>/dev/null; echo "@reboot env NODE_REALTIME_PORT=5000 $(which forever) --append -l /home/metamaps/logs/forever.realtime.log start /home/metamaps/metamaps/realtime/realtime-server.js") | crontab -u metamaps -
(sudo crontab -u metamaps -l 2>/dev/null; echo "@reboot NODE_REALTIME_PORT=5000 /usr/bin/forever --minUptime 1000 --spinSleepTime 1000 --append -l /home/metamaps/logs/forever.realtime.log -c /home/metamaps/metamaps/node_modules/.bin/babel-node --workingDir /home/metamaps/metamaps start /home/metamaps/metamaps/realtime/realtime-server.js") | sudo crontab -u metamaps
mkdir -p /home/metamaps/logs
env NODE_REALTIME_PORT=5000 forever --append \
/usr/bin/forever --minUptime 1000 --spinSleepTime 1000 \
--append -l /home/metamaps/logs/forever.realtime.log \
-c /home/metamaps/metamaps/node_modules/.bin/babel-node \
-l /home/metamaps/logs/forever.realtime.log \
--workingDir /home/metamaps/metamaps \
start /home/metamaps/metamaps/realtime/realtime-server.js
#### Upstart service for delayed_worker:
Put the following code into `/etc/init/metamaps_delayed_worker.conf`:
If your system uses upstart for init scripts, put the following code into `/etc/init/metamaps_delayed_job.conf`:
description "Delayed Jobs Worker for Metamaps"
@ -128,3 +147,30 @@ Then start the service and check the last ten lines of the log file to make sure
sudo service metamaps_delayed_job start
tail /var/log/upstart/metamaps_delayed_job.log
#### Systemd service for delayed_worker:
If your system uses systemd for init scripts, ptu the following code into `/etc/systemd/system/metamaps_delayed_job.service`:
[Unit]
Description=metamaps delayed job service
After=network-online.target
[Service]
ExecStart=/usr/local/rvm/gems/ruby-2.3.0@metamaps/bin/bundle exec rails jobs:work
WorkingDirectory=/home/metamaps/metamaps
Restart=always
User=metamaps
Group=metamaps
Environment=HOME=/home/metamaps
Environment=PATH="/usr/local/rvm/gems/ruby-2.3.0@metamaps/bin:/usr/local/rvm/gems/ruby-2.3.0@global/bin:/usr/local/rvm/rubies/ruby-2.3.0/bin:/usr/local/rvm/bin:/usr/local/bin:/usr/bin:/bin"
Environment=GEM_PATH="/usr/local/rvm/gems/ruby-2.3.0@metamaps:/usr/local/rvm/gems/ruby-2.3.0@global"
Environment=RAILS_ENV="production"
[Install]
WantedBy=multi-user.target
Then start the service and check the last ten lines of the log file to make sure it's running OK:
sudo systemctl start metamaps_delayed_job
# ??? how the heck do you check systemd logs??

View file

@ -1,5 +1,7 @@
/* global $, ActionCable */
import { indexOf } from 'lodash'
import Active from './Active'
import Control from './Control'
import Create from './Create'
@ -110,7 +112,7 @@ const Cable = {
if (edge.getData('mappings').length - 1 === 0) {
Control.hideEdge(edge)
}
var index = indexOf(edge.getData('synapses'), synapse)
edge.getData('mappings').splice(index, 1)
edge.getData('synapses').splice(index, 1)
@ -128,19 +130,20 @@ const Cable = {
// containing only the information we need to determine whether the active mapper
// can view this topic, then if we determine it can, we make a call for the full model
const t = new DataModel.Topic(event.topic)
// refactor the heck outta this, its adding wicked wait time
var topic, mapping, mapper, cancel
function waitThenRenderTopic() {
if (topic && mapping && mapper) {
Topic.renderTopic(mapping, topic, true)
Engine.runLayout()
} else if (!cancel) {
setTimeout(waitThenRenderTopic, 10)
}
}
if (t.authorizeToShow(m) && !DataModel.Topics.get(event.topic.id)) {
// refactor the heck outta this, its adding wicked wait time
var topic, mapping, mapper, cancel
const waitThenRenderTopic = () => {
if (topic && mapping && mapper) {
Topic.renderTopic(mapping, topic, true)
Engine.runLayout()
} else if (!cancel) {
setTimeout(waitThenRenderTopic, 10)
}
}
mapper = DataModel.Mappers.get(event.topic.user_id)
if (mapper === undefined) {
Mapper.get(event.topic.user_id, function(m) {

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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(`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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -46,7 +46,10 @@ const Map = {
GlobalUI.CreateMap.emptyForkMapForm = $('#fork_map').html()
self.updateStar()
InfoBox.init(serverData)
InfoBox.init(serverData, function updateThumbnail() {
self.uploadMapScreenshot()
})
CheatSheet.init(serverData)
$('.viewOnly .requestAccess').click(self.requestAccess)
@ -253,6 +256,48 @@ const Map = {
}
},
exportImage: function() {
Map.uploadMapScreenshot()
Map.offerScreenshotDownload()
GlobalUI.notifyUser('Note: this button is going away. Check the map card or the import box for setting the map thumbnail or downloading a screenshot.')
},
offerScreenshotDownload: () => {
const canvas = Map.getMapCanvasForScreenshots()
const filename = Map.getMapScreenshotFilename(Active.Map)
var downloadMessage = outdent`
Captured map screenshot!
<a id="map-screenshot-download-link"
href="${canvas.canvas.toDataURL()}"
download="${filename}"
>
DOWNLOAD
</a>`
GlobalUI.notifyUser(downloadMessage)
},
uploadMapScreenshot: () => {
const canvas = Map.getMapCanvasForScreenshots()
const filename = Map.getMapScreenshotFilename(Active.Map)
canvas.canvas.toBlob(imageBlob => {
const formData = new window.FormData()
formData.append('map[screenshot]', imageBlob, filename)
$.ajax({
type: 'PATCH',
dataType: 'json',
url: `/maps/${Active.Map.id}`,
data: formData,
processData: false,
contentType: false,
success: function(data) {
GlobalUI.notifyUser('Successfully updated map screenshot.')
},
error: function() {
GlobalUI.notifyUser('Failed to update map screenshot.')
}
})
})
},
getMapCanvasForScreenshots: () => {
var canvas = {}
canvas.canvas = document.createElement('canvas')
@ -338,8 +383,9 @@ const Map = {
node.visited = !T
})
var map = Active.Map
return canvas
},
getMapScreenshotFilename: map => {
var today = new Date()
var dd = today.getDate()
var mm = today.getMonth() + 1 // January is 0!
@ -354,30 +400,7 @@ const Map = {
var mapName = map.get('name').split(' ').join(['-'])
const filename = `metamap-${map.id}-${mapName}-${today}.png`
var downloadMessage = outdent`
Captured map screenshot!
<a href="${canvas.canvas.toDataURL()}" download="${filename}">DOWNLOAD</a>`
GlobalUI.notifyUser(downloadMessage)
canvas.canvas.toBlob(imageBlob => {
const formData = new window.FormData()
formData.append('map[screenshot]', imageBlob, filename)
$.ajax({
type: 'PATCH',
dataType: 'json',
url: `/maps/${map.id}`,
data: formData,
processData: false,
contentType: false,
success: function(data) {
console.log('successfully uploaded map screenshot')
},
error: function() {
console.log('failed to save map screenshot')
}
})
})
return filename
}
}

View file

@ -6,6 +6,7 @@ import Visualize from './Visualize'
const PasteInput = {
// thanks to https://github.com/kevva/url-regex
// eslint-disable-next-line no-useless-escape
URL_REGEX: new RegExp('^(?:(?:(?:[a-z]+:)?//)|www\.)(?:\S+(?::\S*)?@)?(?:localhost|(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])(?:\.(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])){3}|(?:(?:[a-z\u00a1-\uffff0-9]-*)*[a-z\u00a1-\uffff0-9]+)(?:\.(?:[a-z\u00a1-\uffff0-9]-*)*[a-z\u00a1-\uffff0-9]+)*(?:\.(?:[a-z\u00a1-\uffff]{2,}))\.?)(?::\d{2,5})?(?:[/?#][^\s"]*)?$'),
init: function() {

View file

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

View file

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

View file

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

View file

@ -39,7 +39,7 @@ const _Router = Backbone.Router.extend({
var navigate = function() {
self.timeoutId = setTimeout(function() {
self.navigate('')
self.navigateAndTrack('')
}, 300)
}
@ -121,7 +121,7 @@ const _Router = Backbone.Router.extend({
path += '/' + DataModel.Maps.Mapper.mapperId
}
self.navigate(path)
self.navigateAndTrack(path)
}
var navigateTimeout = function() {
self.timeoutId = setTimeout(navigate, 300)
@ -202,6 +202,11 @@ const _Router = Backbone.Router.extend({
const Router = new _Router()
Router.navigateAndTrack = (fragment, options) => {
Router.navigate(fragment, options)
window.ga && window.ga('send', 'pageview', location.pathname, {title: document.title})
}
Router.intercept = function(evt) {
var segments

View file

@ -1,6 +1,14 @@
/* global $ */
import { Parser, HtmlRenderer } from 'commonmark'
import { emojiIndex } from 'emoji-mart'
import { escapeRegExp } from 'lodash'
const emojiToShortcodes = {}
Object.keys(emojiIndex.emojis).forEach(key => {
const emoji = emojiIndex.emojis[key]
emojiToShortcodes[emoji.native] = emoji.colons
})
const Util = {
// helper function to determine how many lines are needed
@ -150,6 +158,29 @@ const Util = {
canvas.scale(oldAttr.scaleX, oldAttr.scaleY)
const newAttr = Util.logCanvasAttributes(canvas)
canvas.translate(newAttr.centreCoords.x - oldAttr.centreCoords.x, newAttr.centreCoords.y - oldAttr.centreCoords.y)
},
removeEmoji: function(withEmoji) {
let text = withEmoji
Object.keys(emojiIndex.emojis).forEach(key => {
const emoji = emojiIndex.emojis[key]
text = text.replace(new RegExp(escapeRegExp(emoji.native), 'g'), emoji.colons)
})
return text
},
addEmoji: function(withoutEmoji, opts = { emoticons: true }) {
let text = withoutEmoji
Object.keys(emojiIndex.emojis).forEach(key => {
const emoji = emojiIndex.emojis[key]
text = text.replace(new RegExp(escapeRegExp(emoji.colons), 'g'), emoji.native)
})
if (opts.emoticons) {
Object.keys(emojiIndex.emoticons).forEach(emoticon => {
const key = emojiIndex.emoticons[emoticon]
const emoji = emojiIndex.emojis[key]
text = text.replace(new RegExp(escapeRegExp(emoticon), 'g'), emoji.native)
})
}
return text
}
}

View file

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

View file

@ -1,13 +1,7 @@
/* global $ */
import Backbone from 'backbone'
import attachMediaStream from 'attachmediastream'
// TODO is this line good or bad
// Backbone.$ = window.$
import Active from '../Active'
import DataModel from '../DataModel'
import Realtime from '../Realtime'
import VideoView from './VideoView'

View file

@ -9,7 +9,7 @@ import { JUNTO_UPDATED } from '../Realtime/events'
const Views = {
init: (serverData) => {
$(document).on(JUNTO_UPDATED, () => ExploreMaps.render())
ChatView.init([serverData['sounds/MM_sounds.mp3'],serverData['sounds/MM_sounds.ogg']])
ChatView.init([serverData['sounds/MM_sounds.mp3'], serverData['sounds/MM_sounds.ogg']])
},
ExploreMaps,
ChatView,

View file

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

View file

@ -6,7 +6,6 @@ class ImportDialogBox extends Component {
super(props)
this.state = {
showImportInstructions: false
}
}
@ -15,21 +14,9 @@ class ImportDialogBox extends Component {
}
handleFile = (files, e) => {
// // for some reason it uploads twice, so we need this debouncer
// // eslint-disable-next-line no-return-assign
// this.debouncer = this.debouncer || window.setTimeout(() => this.debouncer = null, 10)
// if (!this.debouncer) {
// this.props.onFileAdded(files[0])
// }
this.props.onFileAdded(files[0])
}
toggleShowInstructions = e => {
this.setState({
showImportInstructions: !this.state.showImportInstructions
})
}
render = () => {
return (
<div className="import-dialog">
@ -40,6 +27,9 @@ class ImportDialogBox extends Component {
<div className="import-blue-button" onClick={this.handleExport('json')}>
Export as JSON
</div>
<div className="import-blue-button" onClick={this.props.downloadScreenshot}>
Download screenshot
</div>
<h3>IMPORT</h3>
<p>To upload a file, drop it here:</p>
<Dropzone onDropAccepted={this.handleFile}
@ -47,23 +37,7 @@ class ImportDialogBox extends Component {
>
Drop files here!
</Dropzone>
<p>
<a onClick={this.toggleShowInstructions} style={{ textDecoration: 'underline', cursor: 'pointer' }}>
Show/hide import instructions
</a>
</p>
{!this.state.showImportInstructions ? null : (<div>
<p>
You can import topics and synapses by uploading a spreadsheet here.
The file should be in comma-separated format (when you save, change the
filetype from .xls to .csv).
</p>
<img src={this.props.exampleImageUrl} style={{ width: '100%' }} />
<p style={{ marginTop: '1em' }}>You can choose which columns to include in your data. Topics must have a name field. Synapses must have Topic 1 and Topic 2.</p>
<p>&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>)}
<p>See <a href="https://docs.metamaps.cc/importing_and_exporting_data.html">docs.metamaps.cc</a> for instructions.</p>
</div>
)
}
@ -71,7 +45,8 @@ class ImportDialogBox extends Component {
ImportDialogBox.propTypes = {
onFileAdded: PropTypes.func,
exampleImageUrl: PropTypes.string
exampleImageUrl: PropTypes.string,
downloadScreenshot: PropTypes.func
}
export default ImportDialogBox

View file

@ -1,5 +1,6 @@
import React from 'react'
import Autolinker from 'autolinker'
import Util from '../../Metamaps/Util'
const linker = new Autolinker({ newWindow: true, truncate: 50, email: false, phone: false })
@ -10,24 +11,25 @@ function addZero(i) {
return i
}
function formatDate(created_at) {
let date = new Date(created_at)
function formatDate(createdAt) {
let date = new Date(createdAt)
let formatted = (date.getMonth() + 1) + '/' + date.getDate()
formatted += ' ' + addZero(date.getHours()) + ':' + addZero(date.getMinutes())
return formatted
}
const Message = props => {
const { user_image, user_name, message, created_at, heading } = props
const messageHtml = {__html: linker.link(message)}
const { user_image: userImage, user_name: userName, message, created_at: createdAt, heading } = props
const messageHtml = {__html: linker.link(Util.addEmoji(message, { emoticons: false }))}
return (
<div className="chat-message">
<div className="chat-message-user">
{heading && <img src={user_image} />}
{heading && <img src={userImage} />}
</div>
{heading && <div className="chat-message-meta">
<span className='chat-message-username'>{user_name}</span>&nbsp;
<span className='chat-message-time'>{formatDate(created_at)}</span>
<span className='chat-message-username'>{userName}</span>&nbsp;
<span className='chat-message-time'>{formatDate(createdAt)}</span>
</div>}
<div className="chat-message-text" dangerouslySetInnerHTML={messageHtml}></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 {
render() {
const { conversationLive, mapperIsLive, isParticipating, isPending, id, self, image, username, selfName, color } = this.props
const { conversationLive, mapperIsLive, isParticipating, isPending, id, self, image, username, color } = this.props
return (
<div className={`participant participant-${id} ${self ? 'is-self' : ''}`}>
<div className="chat-participant-image">
<img src={image} style={{ border: `2px solid ${color}`}} />
</div>
<div className="chat-participant-name">
{username} {self ? '(me)' : ''}
</div>
{!self && !conversationLive && <button
className={`button chat-participant-invite-call ${isPending ? 'pending' : ''}`}
onClick={() => !isPending && this.props.inviteACall(id)} // Realtime.inviteACall(id)
/>}
{!self && mapperIsLive && !isParticipating && <button
className={`button chat-participant-invite-join ${isPending ? 'pending' : ''}`}
onClick={() => !isPending && this.props.inviteToJoin(id)} // Realtime.inviteToJoin(id)
/>}
{isParticipating && <span className="chat-participant-participating">
<div className="green-dot"></div>
</span>}
<div className="clearfloat"></div>
</div>
<div className={`participant participant-${id} ${self ? 'is-self' : ''}`}>
<div className="chat-participant-image">
<img src={image} style={{ border: `2px solid ${color}` }} />
</div>
<div className="chat-participant-name">
{username} {self ? '(me)' : ''}
</div>
{!self && !conversationLive && <button
className={`button chat-participant-invite-call ${isPending ? 'pending' : ''}`}
onClick={() => !isPending && this.props.inviteACall(id)} // Realtime.inviteACall(id)
/>}
{!self && mapperIsLive && !isParticipating && <button
className={`button chat-participant-invite-join ${isPending ? 'pending' : ''}`}
onClick={() => !isPending && this.props.inviteToJoin(id)} // Realtime.inviteToJoin(id)
/>}
{isParticipating && <span className="chat-participant-participating">
<div className="green-dot"></div>
</span>}
<div className="clearfloat"></div>
</div>
)
}
}

View file

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

View file

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

View file

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

Some files were not shown because too many files have changed in this diff Show more