Merge branch 'develop' (v3.2)

This commit is contained in:
Devin Howard 2017-01-16 11:14:04 -05:00
commit 2c60d7335c
75 changed files with 1759 additions and 1528 deletions

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

@ -27,6 +27,7 @@ gem 'rack-cors'
gem 'redis' gem 'redis'
gem 'slack-notifier' gem 'slack-notifier'
gem 'snorlax' gem 'snorlax'
gem 'puma'
# asset stuff # asset stuff
gem 'jquery-rails' gem 'jquery-rails'

View file

@ -167,6 +167,7 @@ GEM
pry (~> 0.10) pry (~> 0.10)
pry-rails (0.3.4) pry-rails (0.3.4)
pry (>= 0.9.10) pry (>= 0.9.10)
puma (3.6.2)
pundit (1.1.0) pundit (1.1.0)
activesupport (>= 3.0.0) activesupport (>= 3.0.0)
pundit_extra (0.3.0) pundit_extra (0.3.0)
@ -298,6 +299,7 @@ DEPENDENCIES
pg pg
pry-byebug pry-byebug
pry-rails pry-rails
puma
pundit pundit
pundit_extra pundit_extra
rack-attack rack-attack

View file

@ -1,3 +1,3 @@
web: bundle exec rails server -p $PORT web: bundle exec puma -p $PORT
worker: bundle exec rake jobs:work worker: bundle exec rake jobs:work

View file

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

View file

@ -14,6 +14,7 @@
//= require jquery //= require jquery
//= require jquery-ui //= require jquery-ui
//= require jquery_ujs //= require jquery_ujs
//= require action_cable
//= require_directory ./lib //= require_directory ./lib
//= require ./webpacked/metamaps.bundle //= require ./webpacked/metamaps.bundle
//= require ./Metamaps.ServerData //= require ./Metamaps.ServerData

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,348 +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 {
position: relative;
display: flex;
flex-direction: column;
z-index: 1;
width: 300px;
float: right;
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 {
display: none;
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 {
display: none;
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 .conversation-live .leave {
display: none;
}
.chat-box .participants.is-participating .conversation-live .leave {
display: block;
}
.chat-box .participants.is-participating .conversation-live .join {
display: none;
}
.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.is-self .chat-participant-invite-call,
.chat-box .participants .participant.is-self .chat-participant-invite-join {
display: none !important;
}
.chat-box .participants.is-live .participant .chat-participant-invite-call {
display: none;
}
.chat-box .participants .participant .chat-participant-invite-join {
display: none;
}
.chat-box .participants.is-live.is-participating .participant:not(.active) .chat-participant-invite-join {
display: block;
}
.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.pending .chat-participant-invite-call,
.chat-box .participants .participant.pending .chat-participant-invite-join {
background: #dab539 url(<%= asset_path 'ellipsis.gif' %>) no-repeat center center;
}
.chat-box .participants .participant .chat-participant-participating {
float: right;
display: none;
margin-top: 14px;
}
.chat-box .participants .participant .chat-participant-participating .green-dot {
background: #4fc059;
width: 12px;
height: 12px;
border-radius: 6px;
}
.chat-box .participants .participant.active .chat-participant-participating {
display: block;
}
.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 0px 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: 15%;
float: left;
overflow: hidden;
color: #BBB;
padding-top: 2px;
}
.chat-box .chat-messages .chat-message .chat-message-user img {
border: 2px solid #424242;
width: 32px;
height: 32px;
border-radius: 18px;
}
.chat-box .chat-messages .chat-message .chat-message-text {
width: 73%;
float: left;
margin-top: 12px;
padding: 2px 8px 0;
text-align: left;
word-wrap: break-word;
}
.chat-box .chat-messages .chat-message .chat-message-time {
float: right;
font-size: 10px;
color: #757575;
}

View file

@ -0,0 +1,375 @@
.collaborator-video {
z-index: 1;
position: absolute;
width: 150px;
height: 150px;
cursor: default;
color: #FFF;
.video-receive {
position: absolute;
width: 160px;
padding: 20px 20px 20px 170px;
background: #424242;
height: 110px;
border-top-left-radius: 75px;
border-bottom-left-radius: 75px;
border-top-right-radius: 2px;
border-bottom-right-radius: 2px;
.video-statement {
margin-bottom: 10px;
}
.btn-group {
.btn-yes {
margin-right: 10px;
}
.btn-no {
background-color: #c04f4f;
&:hover {
background-color: #A54242;
}
}
}
}
.video-cutoff {
width: 150px;
height: 150px;
overflow: hidden;
border-radius: 75px;
z-index: 0;
position: relative;
-webkit-box-shadow: 0px 6px 3px rgba(0, 0, 0, 0.23), 10px 10px 10px rgba(0, 0, 0, 0.19);
-moz-box-shadow: 0px 6px 3px rgba(0, 0, 0, 0.23), 10px 10px 10px rgba(0, 0, 0, 0.19);
box-shadow: 0px 6px 3px rgba(0, 0, 0, 0.23), 10px 10px 10px rgba(0, 0, 0, 0.19);
video {
height: 150px;
margin-left: -25px;
}
.collaborator-video-avatar {
position: absolute;
top: 0;
left: 0;
-webkit-user-select: none;
-khtml-user-select: none;
-moz-user-select: none;
-o-user-select: none;
user-select: none;
-webkit-user-drag: none;
display: none;
}
}
.video-audio {
position: absolute;
width: 24px;
height: 24px;
top: 85%;
right: 0px;
cursor: pointer;
background: url(<%= asset_path 'audio_sprite.png' %>) no-repeat;
}
.video-audio:hover {
background-position-x: -24px;
}
.video-audio.active {
background-position-y: -24px;
}
.video-video {
position: absolute;
width: 24px;
height: 24px;
top: 85%;
left: 0px;
cursor: pointer;
background: url(<%= asset_path 'camera_sprite.png' %>) no-repeat;
}
.video-video:hover {
background-position-x: -24px;
}
.video-video.active {
background-position-y: -24px;
}
}
.collaborator-video.my-video {
left: 30px;
top: 72px;
}
#chat-box-wrapper {
height: 100%;
float: right;
}
.chat-box {
position: relative;
display: flex;
flex-direction: column;
z-index: 1;
width: 300px;
height: 100%;
background: #424242;
box-shadow: -8px 0px 16px 2px rgba(0, 0, 0, 0.23);
.chat-button {
position: absolute;
top: 50%;
left: -36px;
width: 36px;
height: 49px;
background: url(<%= asset_path 'junto.png' %>) no-repeat 2px 9px, url(<%= asset_path 'tray_tab.png' %>) no-repeat;
cursor: pointer;
&.active {
background: url(<%= asset_path 'junto_spinner_dark.gif' %>) no-repeat 2px 8px, url(<%= asset_path 'tray_tab.png' %>) no-repeat !important;
}
.chat-unread {
background: #DAB539;
position: absolute;
top: -3px;
left: -11px;
width: 20px;
height: 20px;
border-radius: 11px;
border: 2px solid #424242;
color: #424242;
text-align: center;
font-size: 12px;
font-weight: bold;
line-height: 20px;
}
}
.junto-header {
width: 276px;
padding: 16px 8px 16px 16px;
font-size: 16px;
text-align: left;
font-weight: bold;
background-color: #000000;
color: #f5f5f5;
box-shadow: 0px 6px 3px rgba(0, 0, 0, 0.23);
.cursor-toggle {
width: 32px;
height: 32px;
margin-right: 8px;
margin-top: -8px;
float: right;
background: url(<%= asset_path 'cursor_sprite.png' %>) no-repeat;
}
.cursor-toggle:hover {
background-position-x: -32px;
}
.cursor-toggle.active {
background-position-y: -32px;
}
.video-toggle {
width: 32px;
height: 32px;
margin-right: 10px;
margin-top: -8px;
float: right;
background: url(<%= asset_path 'video_sprite.png' %>) no-repeat;
}
.video-toggle:hover {
background-position-x: -32px;
}
.video-toggle.active {
background-position-y: -32px;
}
}
.participants {
width: 100%;
min-height: 150px;
padding: 16px 0px 16px 0px;
text-align: left;
color: #f5f5f5;
overflow-y: auto;
.conversation-live {
padding: 5px 10px 5px 10px;
background: #c04f4f;
margin: 5px 10px;
border-radius: 2px;
}
.conversation-live .call-action {
float: right;
cursor: pointer;
color: #EBFF00;
}
.participant {
width: 89%;
padding: 8px 8px 2px 8px;
color: #f5f5f5;
font-family: arial, sans-serif;
font-size: 13px;
line-height: 14px;
.chat-participant-image {
width: 15%;
float: left;
overflow: hidden;
color: #BBB;
padding-top: 2px;
}
.chat-participant-image img {
width: 32px;
height: 32px;
border-radius: 18px;
}
.chat-participant-name {
width: 53%;
float: left;
font-size: 13px;
font-weight: bold;
margin-top: 12px;
padding: 2px 8px 0;
text-align: left;
}
.chat-participant-invite-call,
.chat-participant-invite-join
{
float: right;
background: #4FC059 url(<%= asset_path 'invitepeer16.png' %>) no-repeat center center;
}
.chat-participant-invite-call.pending,
.chat-participant-invite-join.pending {
background: #dab539 url(<%= asset_path 'ellipsis.gif' %>) no-repeat center center;
}
.chat-participant-participating {
float: right;
margin-top: 14px;
}
.chat-participant-participating .green-dot {
background: #4fc059;
width: 12px;
height: 12px;
border-radius: 6px;
}
}
}
.chat-header {
width: 276px;
padding: 16px 8px 16px 16px;
font-size: 16px;
text-align: left;
font-weight: bold;
background-color: #000000;
color: #f5f5f5;
box-shadow: 0px 6px 3px rgba(0, 0, 0, 0.23);
.sound-toggle {
width: 24px;
height: 24px;
margin-right: 10px;
margin-top: -2px;
float: right;
background: url(<%= asset_path 'sound_sprite.png' %>) no-repeat;
}
.sound-toggle:hover {
background-position-x: -24px;
}
.sound-toggle.active {
background-position-y: -24px;
}
}
$chat_font_size: 16px;
.chat-input {
min-height: 80px;
width: 88%;
padding: 8px 9% 8px 3%;
font-size: $chat_font_size;
outline: none;
resize: none;
}
.chat-messages {
width: 100%;
padding: 16px 0px;
overflow-y: auto;
flex-grow: 1;
.chat-message {
width: 89%;
padding: 8px 8px 2px 8px;
color: #f5f5f5;
font-family: arial, sans-serif;
font-size: $chat_font_size;
line-height: $chat_font_size + 1px;
a:link {
color: #4fb5c0;
text-decoration: underline;
}
a:visited {
color: #aea9fd;
text-decoration: underline;
}
a:hover {
color: #dab539;
text-decoration: underline;
}
.chat-message-user {
width: 12%;
float: left;
overflow: hidden;
color: #BBB;
padding-top: 2px;
}
.chat-message-user img {
border: 2px solid #424242;
width: 28px;
height: 28px;
border-radius: 16px;
}
.chat-message-meta {
padding: 0 8px;
float: left;
}
.chat-message-username {
color: #4fc059;
}
.chat-message-text {
width: 80%;
float: left;
padding: 2px 8px 0;
text-align: left;
word-wrap: break-word;
}
.chat-message-time {
font-size: 12px;
color: #757575;
}
}
}
.new-message-area {
position: relative;
.emoji-mart {
position: absolute;
bottom: 98px;
}
.extra-message-options {
height: 20px;
position: absolute;
right: 2px;
bottom: 74px;
.emoji-picker-button {
font-size: 16px;
line-height: 20px;
cursor: pointer;
padding: 4px;
}
}
}
}

View file

@ -211,6 +211,16 @@
span.creatorName { span.creatorName {
margin-left: 8px; margin-left: 8px;
max-width: 162px;
display: inline-block;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
vertical-align: middle;
}
.creatorAndPerm.cardHasViewOnly span.creatorName {
max-width: 95px;
} }
.cardViewOnly { .cardViewOnly {

View file

@ -93,7 +93,7 @@
.sidebarSearchField { .sidebarSearchField {
float: left; float: left;
width: 380px; width: 379px;
padding: 7px 10px 3px 10px; padding: 7px 10px 3px 10px;
height: 20px; height: 20px;
border-top: 1px solid #BDBDBD; border-top: 1px solid #BDBDBD;

View file

@ -0,0 +1,4 @@
module ApplicationCable
class Channel < ActionCable::Channel::Base
end
end

View file

@ -0,0 +1,22 @@
# frozen_string_literal: true
module ApplicationCable
class Connection < ActionCable::Connection::Base
identified_by :current_user
def connect
self.current_user = find_verified_user
logger.add_tags 'ActionCable', current_user.name
end
protected
def find_verified_user
verified_user = User.find_by(id: cookies.signed['user.id'])
if verified_user && cookies.signed['user.expires_at'] > Time.now.getlocal
verified_user
else
reject_unauthorized_connection
end
end
end
end

View file

@ -0,0 +1,8 @@
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?
stream_from "map_#{params[:id]}"
end
end

View file

@ -124,7 +124,7 @@ class MapsController < ApplicationController
end end
def create_map_params def create_map_params
params.permit(:name, :desc, :permission) params.permit(:name, :desc, :permission, :source_id)
end end
def update_map_params def update_map_params

View file

@ -1,6 +1,7 @@
# frozen_string_literal: true # frozen_string_literal: true
class Map < ApplicationRecord class Map < ApplicationRecord
belongs_to :user belongs_to :user
belongs_to :source, class_name: :Map
has_many :topicmappings, -> { Mapping.topicmapping }, 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 :synapsemappings, -> { Mapping.synapsemapping }, class_name: :Mapping, dependent: :destroy
@ -32,6 +33,7 @@ class Map < ApplicationRecord
# Validate the attached image is image/jpg, image/png, etc # Validate the attached image is image/jpg, image/png, etc
validates_attachment_content_type :screenshot, content_type: /\Aimage\/.*\Z/ validates_attachment_content_type :screenshot, content_type: /\Aimage\/.*\Z/
after_update :after_updated
after_save :update_deferring_topics_and_synapses, if: :permission_changed? after_save :update_deferring_topics_and_synapses, if: :permission_changed?
delegate :count, to: :topics, prefix: :topic # same as `def topic_count; topics.count; end` delegate :count, to: :topics, prefix: :topic # same as `def topic_count; topics.count; end`
@ -118,6 +120,13 @@ class Map < ApplicationRecord
end end
removed.compact removed.compact
end 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
end
def update_deferring_topics_and_synapses def update_deferring_topics_and_synapses
Topic.where(defer_to_map_id: id).update_all(permission: permission) Topic.where(defer_to_map_id: id).update_all(permission: permission)

View file

@ -33,8 +33,16 @@ class Mapping < ApplicationRecord
if mappable_type == 'Topic' if mappable_type == 'Topic'
meta = {'x': xloc, 'y': yloc, 'mapping_id': id} meta = {'x': xloc, 'y': yloc, 'mapping_id': id}
Events::TopicAddedToMap.publish!(mappable, map, user, meta) Events::TopicAddedToMap.publish!(mappable, map, user, meta)
ActionCable.server.broadcast 'map_' + map.id.to_s, type: 'topicAdded', topic: mappable.filtered, mapping_id: id
elsif mappable_type == 'Synapse' elsif mappable_type == 'Synapse'
Events::SynapseAddedToMap.publish!(mappable, map, user, meta) Events::SynapseAddedToMap.publish!(mappable, map, user, meta)
ActionCable.server.broadcast(
'map_' + map.id.to_s,
type: 'synapseAdded',
synapse: mappable.filtered,
topic1: mappable.topic1.filtered,
topic2: mappable.topic2.filtered,
mapping_id: id)
end end
end end
@ -42,6 +50,7 @@ class Mapping < ApplicationRecord
if mappable_type == 'Topic' and (xloc_changed? or yloc_changed?) if mappable_type == 'Topic' and (xloc_changed? or yloc_changed?)
meta = {'x': xloc, 'y': yloc, 'mapping_id': id} meta = {'x': xloc, 'y': yloc, 'mapping_id': id}
Events::TopicMovedOnMap.publish!(mappable, map, updated_by, meta) Events::TopicMovedOnMap.publish!(mappable, map, updated_by, meta)
ActionCable.server.broadcast 'map_' + map.id.to_s, type: 'topicMoved', id: mappable.id, mapping_id: id, x: xloc, y: yloc
end end
end end
@ -55,8 +64,10 @@ class Mapping < ApplicationRecord
meta = {'mapping_id': id} meta = {'mapping_id': id}
if mappable_type == 'Topic' if mappable_type == 'Topic'
Events::TopicRemovedFromMap.publish!(mappable, map, updated_by, meta) Events::TopicRemovedFromMap.publish!(mappable, map, updated_by, meta)
ActionCable.server.broadcast 'map_' + map.id.to_s, type: 'topicRemoved', id: mappable.id, mapping_id: id
elsif mappable_type == 'Synapse' elsif mappable_type == 'Synapse'
Events::SynapseRemovedFromMap.publish!(mappable, map, updated_by, meta) Events::SynapseRemovedFromMap.publish!(mappable, map, updated_by, meta)
ActionCable.server.broadcast 'map_' + map.id.to_s, type: 'synapseRemoved', id: mappable.id, mapping_id: id
end end
end end
end end

View file

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

View file

@ -38,6 +38,15 @@ class Synapse < ApplicationRecord
end end
end end
def filtered
{
id: id,
permission: permission,
user_id: user_id,
collaborator_ids: collaborator_ids
}
end
def as_json(_options = {}) def as_json(_options = {})
super(methods: [:user_name, :user_image, :collaborator_ids]) super(methods: [:user_name, :user_image, :collaborator_ids])
end end
@ -50,6 +59,9 @@ class Synapse < ApplicationRecord
meta = new.merge(old) # we are prioritizing the old values, keeping them meta = new.merge(old) # we are prioritizing the old values, keeping them
meta['changed'] = changed_attributes.keys.select {|k| attrs.include?(k) } meta['changed'] = changed_attributes.keys.select {|k| attrs.include?(k) }
Events::SynapseUpdated.publish!(self, user, meta) Events::SynapseUpdated.publish!(self, user, meta)
maps.each {|map|
ActionCable.server.broadcast 'map_' + map.id.to_s, type: 'synapseUpdated', id: id
}
end end
end end
end end

View file

@ -90,6 +90,15 @@ class Topic < ApplicationRecord
end end
end end
def filtered
{
id: id,
permission: permission,
user_id: user_id,
collaborator_ids: collaborator_ids
}
end
# TODO: move to a decorator? # TODO: move to a decorator?
def synapses_csv(output_format = 'array') def synapses_csv(output_format = 'array')
output = [] output = []
@ -145,6 +154,9 @@ class Topic < ApplicationRecord
meta = new.merge(old) # we are prioritizing the old values, keeping them meta = new.merge(old) # we are prioritizing the old values, keeping them
meta['changed'] = changed_attributes.keys.select {|k| attrs.include?(k) } meta['changed'] = changed_attributes.keys.select {|k| attrs.include?(k) }
Events::TopicUpdated.publish!(self, user, meta) Events::TopicUpdated.publish!(self, user, meta)
maps.each {|map|
ActionCable.server.broadcast 'map_' + map.id.to_s, type: 'topicUpdated', id: id
}
end end
end end
end end

View file

@ -18,6 +18,7 @@ module Api
def self.embeddable def self.embeddable
{ {
user: {}, user: {},
source: {},
topics: {}, topics: {},
synapses: {}, synapses: {},
mappings: {}, mappings: {},

View file

@ -10,6 +10,6 @@
Metamaps.Loading.setup() Metamaps.Loading.setup()
</script> </script>
<%= render :partial => 'layouts/googleanalytics' if Rails.env.production? %> <%= render :partial => 'layouts/googleanalytics' if ENV["GA_TRACKING_CODE"].present? %>
</body> </body>
</html> </html>

View file

@ -3,16 +3,13 @@
# Google analytics, rendered on every page # Google analytics, rendered on every page
#%> #%>
<script type="text/javascript"> <script>
(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
})(window,document,'script','https://www.google-analytics.com/analytics.js','ga');
var _gaq = _gaq || []; ga('create', '<%= ENV["GA_TRACKING_CODE"] %>', 'auto');
_gaq.push(['_setAccount', 'UA-35984510-1']); ga('send', 'pageview');
_gaq.push(['_trackPageview']);
(function() {
var ga = document.createElement('script'); ga.type = 'text/javascript'; ga.async = true;
ga.src = ('https:' == document.location.protocol ? 'https://ssl.' : 'http://') + 'stats.g.doubleclick.net/dc.js';
var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s);
})();
</script> </script>

View file

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

View file

@ -5,7 +5,7 @@
<div class="templates"> <div class="templates">
<script type="text/template" id="mapInfoBoxTemplate"> <script type="text/template" id="mapInfoBoxTemplate">
<div class="requestTitle">Click here to name this map!</div> <div class="requestTitle">Click here to name this map</div>
<div class="mapInfoName" id="mapInfoName">{{{name}}}</div> <div class="mapInfoName" id="mapInfoName">{{{name}}}</div>
<div class="mapInfoStat"> <div class="mapInfoStat">

View file

@ -9,6 +9,8 @@
<body class="<%= authenticated? ? "authenticated" : "unauthenticated" %> controller-<%= controller_name %> action-<%= action_name %>"> <body class="<%= authenticated? ? "authenticated" : "unauthenticated" %> controller-<%= controller_name %> action-<%= action_name %>">
<div id="chat-box-wrapper"></div>
<a class='feedback-icon' target='_blank' href='https://hylo.com/c/metamaps'></a> <a class='feedback-icon' target='_blank' href='https://hylo.com/c/metamaps'></a>
<%= content_tag :div, class: "main" do %> <%= content_tag :div, class: "main" do %>
@ -48,7 +50,7 @@
<div id="instructions"> <div id="instructions">
<div class="addTopic"> <div class="addTopic">
Double-click to<br>add a topic! Double-click to<br>add a topic
</div> </div>
<div class="tabKey"> <div class="tabKey">
Use Tab & Shift+Tab to select a metacode Use Tab & Shift+Tab to select a metacode

View file

@ -9,7 +9,7 @@
<% if current_user %> <% if current_user %>
<div class="requestTitle"> <div class="requestTitle">
Click here to name this map! Click here to name this map
</div> </div>
<% end %> <% end %>

View file

@ -6,4 +6,4 @@ test:
production: production:
adapter: redis adapter: redis
url: redis://localhost:6379/1 url: <%= ENV['REDISTOGO_URL'] %>

View file

@ -3,6 +3,8 @@
Rails.application.configure do Rails.application.configure do
# Settings specified here will take precedence over those in config/application.rb # Settings specified here will take precedence over those in config/application.rb
config.action_cable.allowed_request_origins = ['https://metamaps.herokuapp.com', 'http://metamaps.herokuapp.com', 'https://metamaps.cc']
# log to stdout # log to stdout
logger = Logger.new(STDOUT) logger = Logger.new(STDOUT)
logger.formatter = config.log_formatter logger.formatter = config.log_formatter

View file

@ -0,0 +1,10 @@
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|
scope = opts[:scope]
auth.cookies.signed["#{scope}.id"] = nil
auth.cookies.signed["#{scope}.expires_at"] = nil
end

View file

@ -1,6 +1,7 @@
# frozen_string_literal: true # frozen_string_literal: true
Metamaps::Application.routes.draw do Metamaps::Application.routes.draw do
use_doorkeeper use_doorkeeper
mount ActionCable.server => '/cable'
root to: 'main#home', via: :get root to: 'main#home', via: :get
get 'request', to: 'main#requestinvite', as: :request get 'request', to: 'main#requestinvite', as: :request

View file

@ -0,0 +1,5 @@
class AddSourceToMaps < ActiveRecord::Migration[5.0]
def change
add_reference :maps, :source, foreign_key: {to_table: :maps}
end
end

View file

@ -10,7 +10,7 @@
# #
# It's strongly recommended that you check this file into your version control system. # It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 20161216174257) do ActiveRecord::Schema.define(version: 20161218183817) do
# These are extensions that must be enabled in order to support this database # These are extensions that must be enabled in order to support this database
enable_extension "plpgsql" enable_extension "plpgsql"
@ -151,6 +151,8 @@ ActiveRecord::Schema.define(version: 20161216174257) do
t.string "screenshot_content_type", limit: 255 t.string "screenshot_content_type", limit: 255
t.integer "screenshot_file_size" t.integer "screenshot_file_size"
t.datetime "screenshot_updated_at" t.datetime "screenshot_updated_at"
t.integer "source_id"
t.index ["source_id"], name: "index_maps_on_source_id", using: :btree
t.index ["user_id"], name: "index_maps_on_user_id", using: :btree t.index ["user_id"], name: "index_maps_on_user_id", using: :btree
end end
@ -340,5 +342,6 @@ ActiveRecord::Schema.define(version: 20161216174257) do
add_foreign_key "mailboxer_notifications", "mailboxer_conversations", column: "conversation_id", name: "notifications_on_conversation_id" add_foreign_key "mailboxer_notifications", "mailboxer_conversations", column: "conversation_id", name: "notifications_on_conversation_id"
add_foreign_key "mailboxer_receipts", "mailboxer_notifications", column: "notification_id", name: "receipts_on_notification_id" add_foreign_key "mailboxer_receipts", "mailboxer_notifications", column: "notification_id", name: "receipts_on_notification_id"
add_foreign_key "mappings", "users", column: "updated_by_id" add_foreign_key "mappings", "users", column: "updated_by_id"
add_foreign_key "maps", "maps", column: "source_id"
add_foreign_key "tokens", "users" add_foreign_key "tokens", "users"
end end

View file

@ -1,6 +1,6 @@
#type: collection #type: collection
get: get:
is: [ searchable: { searchFields: "name, desc" }, embeddable: { embedFields: "user,topics,synapses,mappings,contributors,collaborators" }, orderable, pageable ] is: [ searchable: { searchFields: "name, desc" }, embeddable: { embedFields: "user,source,topics,synapses,mappings,contributors,collaborators" }, orderable, pageable ]
securedBy: [ null, token, oauth_2_0, cookie ] securedBy: [ null, token, oauth_2_0, cookie ]
queryParameters: queryParameters:
user_id: user_id:
@ -23,6 +23,8 @@ post:
description: description description: description
permission: permission:
description: commons, public, or private description: commons, public, or private
source_id:
description: the id of the map this map is a fork of
screenshot: screenshot:
description: url to a screenshot of the map description: url to a screenshot of the map
contributor_ids: contributor_ids:
@ -37,7 +39,7 @@ post:
/{id}: /{id}:
#type: item #type: item
get: get:
is: [ embeddable: { embedFields: "user,topics,synapses,mappings,contributors,collaborators" } ] is: [ embeddable: { embedFields: "user,source,topics,synapses,mappings,contributors,collaborators" } ]
securedBy: [ null, token, oauth_2_0, cookie ] securedBy: [ null, token, oauth_2_0, cookie ]
responses: responses:
200: 200:
@ -60,6 +62,9 @@ post:
screenshot: screenshot:
description: url to a screenshot of the map description: url to a screenshot of the map
required: false required: false
source_id:
description: the id of the map this map is a fork of
required: false
responses: responses:
200: 200:
body: body:
@ -81,6 +86,9 @@ post:
screenshot: screenshot:
description: url to a screenshot of the map description: url to a screenshot of the map
required: false required: false
source_id:
description: the id of the map this map is a fork of
required: false
responses: responses:
200: 200:
body: body:

View file

@ -9,6 +9,7 @@
"created_at": "2016-03-26T08:02:05.379Z", "created_at": "2016-03-26T08:02:05.379Z",
"updated_at": "2016-03-27T07:20:18.047Z", "updated_at": "2016-03-27T07:20:18.047Z",
"user_id": 1234, "user_id": 1234,
"source_id": null,
"topic_ids": [ "topic_ids": [
58, 58,
59 59

View file

@ -9,6 +9,7 @@
"created_at": "2016-03-26T08:02:05.379Z", "created_at": "2016-03-26T08:02:05.379Z",
"updated_at": "2016-03-27T07:20:18.047Z", "updated_at": "2016-03-27T07:20:18.047Z",
"user_id": 1234, "user_id": 1234,
"source_id": null,
"topic_ids": [ "topic_ids": [
58, 58,
59 59

View file

@ -10,6 +10,7 @@
"created_at": "2016-03-26T08:02:05.379Z", "created_at": "2016-03-26T08:02:05.379Z",
"updated_at": "2016-03-27T07:20:18.047Z", "updated_at": "2016-03-27T07:20:18.047Z",
"user_id": 1234, "user_id": 1234,
"source_id": 2,
"topic_ids": [ "topic_ids": [
58, 58,
59 59

View file

@ -33,6 +33,12 @@
"user": { "user": {
"$ref": "_user.json" "$ref": "_user.json"
}, },
"source_id": {
"$ref": "_optid.json"
},
"source": {
"$ref": "_map.json"
},
"topic_ids": { "topic_ids": {
"type": "array", "type": "array",
"items": { "items": {
@ -111,6 +117,12 @@
{ "required": [ "user" ] } { "required": [ "user" ] }
] ]
}, },
{
"oneOf": [
{ "required": [ "source_id" ] },
{ "required": [ "source" ] }
]
},
{ {
"oneOf": [ "oneOf": [
{ "required": [ "topic_ids" ] }, { "required": [ "topic_ids" ] },

View file

@ -0,0 +1,3 @@
{
"type": "integer|nil"
}

View file

@ -103,7 +103,7 @@ server to see what problems show up:
#### Upstart service for delayed_worker: #### Upstart service for delayed_worker:
Put the following code into `/etc/init/metamaps_delayed_worker.conf`: If your system uses upstart for init scripts, put the following code into `/etc/init/metamaps_delayed_job.conf`:
description "Delayed Jobs Worker for Metamaps" description "Delayed Jobs Worker for Metamaps"
@ -128,3 +128,30 @@ Then start the service and check the last ten lines of the log file to make sure
sudo service metamaps_delayed_job start sudo service metamaps_delayed_job start
tail /var/log/upstart/metamaps_delayed_job.log tail /var/log/upstart/metamaps_delayed_job.log
#### Systemd service for delayed_worker:
If your system uses systemd for init scripts, ptu the following code into `/etc/systemd/system/metamaps_delayed_job.service`:
[Unit]
Description=metamaps delayed job service
After=network-online.target
[Service]
ExecStart=/usr/local/rvm/gems/ruby-2.3.0@metamaps/bin/bundle exec rails jobs:work
WorkingDirectory=/home/metamaps/metamaps
Restart=always
User=metamaps
Group=metamaps
Environment=HOME=/home/metamaps
Environment=PATH="/usr/local/rvm/gems/ruby-2.3.0@metamaps/bin:/usr/local/rvm/gems/ruby-2.3.0@global/bin:/usr/local/rvm/rubies/ruby-2.3.0/bin:/usr/local/rvm/bin:/usr/local/bin:/usr/bin:/bin"
Environment=GEM_PATH="/usr/local/rvm/gems/ruby-2.3.0@metamaps:/usr/local/rvm/gems/ruby-2.3.0@global"
Environment=RAILS_ENV="production"
[Install]
WantedBy=multi-user.target
Then start the service and check the last ten lines of the log file to make sure it's running OK:
sudo systemctl start metamaps_delayed_job
# ??? how the heck do you check systemd logs??

View file

@ -0,0 +1,234 @@
/* global $, ActionCable */
import { indexOf } from 'lodash'
import Active from './Active'
import Control from './Control'
import DataModel from './DataModel'
import Map from './Map'
import Mapper from './Mapper'
import Synapse from './Synapse'
import Topic from './Topic'
import { ChatView } from './Views'
import Visualize from './Visualize'
const Cable = {
init: () => {
let self = Cable
self.cable = ActionCable.createConsumer()
},
subscribeToMap: id => {
let self = Cable
self.sub = self.cable.subscriptions.create({
channel: 'MapChannel',
id: id
}, {
received: event => self[event.type](event)
})
},
unsubscribeFromMap: () => {
let self = Cable
self.sub.unsubscribe()
delete self.sub
},
synapseAdded: event => {
// we receive contentless models from the server
// containing only the information we need to determine whether the active mapper
// can view this synapse and the two topics it connects,
// then if we determine it can, we make a call for the full model
const m = Active.Mapper
const s = new DataModel.Synapse(event.synapse)
const t1 = new DataModel.Topic(event.topic1)
const t2 = new DataModel.Topic(event.topic2)
if (t1.authorizeToShow(m) && t2.authorizeToShow(m) && s.authorizeToShow(m) && !DataModel.Synapses.get(event.synapse.id)) {
// refactor the heck outta this, its adding wicked wait time
var topic1, topic2, node1, node2, synapse, mapping, cancel, mapper
const waitThenRenderSynapse = () => {
if (synapse && mapping && mapper) {
topic1 = synapse.getTopic1()
node1 = topic1.get('node')
topic2 = synapse.getTopic2()
node2 = topic2.get('node')
Synapse.renderSynapse(mapping, synapse, node1, node2, false)
} else if (!cancel) {
setTimeout(waitThenRenderSynapse, 10)
}
}
mapper = DataModel.Mappers.get(event.synapse.user_id)
if (mapper === undefined) {
Mapper.get(event.synapse.user_id, function(m) {
DataModel.Mappers.add(m)
mapper = m
})
}
$.ajax({
url: '/synapses/' + event.synapse.id + '.json',
success: function(response) {
DataModel.Synapses.add(response)
synapse = DataModel.Synapses.get(response.id)
},
error: function() {
cancel = true
}
})
$.ajax({
url: '/mappings/' + event.mapping_id + '.json',
success: function(response) {
DataModel.Mappings.add(response)
mapping = DataModel.Mappings.get(response.id)
},
error: function() {
cancel = true
}
})
waitThenRenderSynapse()
}
},
synapseUpdated: event => {
// TODO: handle case where permission changed
var synapse = DataModel.Synapses.get(event.id)
if (synapse) {
// edge reset necessary because fetch causes model reset
var edge = synapse.get('edge')
synapse.fetch({
success: function(model) {
model.set({ edge: edge })
model.trigger('changeByOther')
}
})
}
},
synapseRemoved: event => {
var synapse = DataModel.Synapses.get(event.id)
if (synapse) {
var edge = synapse.get('edge')
var mapping = synapse.getMapping()
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)
if (edge.getData('displayIndex')) {
delete edge.data.$displayIndex
}
DataModel.Synapses.remove(synapse)
DataModel.Mappings.remove(mapping)
}
},
topicAdded: event => {
const m = Active.Mapper
// we receive a contentless model from the server
// 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)
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, false, false)
} 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) {
DataModel.Mappers.add(m)
mapper = m
})
}
$.ajax({
url: '/topics/' + event.topic.id + '.json',
success: function(response) {
DataModel.Topics.add(response)
topic = DataModel.Topics.get(response.id)
},
error: function() {
cancel = true
}
})
$.ajax({
url: '/mappings/' + event.mapping_id + '.json',
success: function(response) {
DataModel.Mappings.add(response)
mapping = DataModel.Mappings.get(response.id)
},
error: function() {
cancel = true
}
})
waitThenRenderTopic()
}
},
topicUpdated: event => {
// TODO: handle case where permission changed
var topic = DataModel.Topics.get(event.id)
if (topic) {
var node = topic.get('node')
topic.fetch({
success: function(model) {
model.set({ node: node })
model.trigger('changeByOther')
}
})
}
},
topicMoved: event => {
var topic, node, mapping
if (Active.Map) {
topic = DataModel.Topics.get(event.id)
mapping = DataModel.Mappings.get(event.mapping_id)
mapping.set('xloc', event.x)
mapping.set('yloc', event.y)
if (topic) node = topic.get('node')
if (node) node.pos.setc(event.x, event.y)
Visualize.mGraph.plot()
}
},
topicRemoved: event => {
var topic = DataModel.Topics.get(event.id)
if (topic) {
var node = topic.get('node')
var mapping = topic.getMapping()
Control.hideNode(node.id)
DataModel.Topics.remove(topic)
DataModel.Mappings.remove(mapping)
}
},
messageCreated: event => {
if (Active.Mapper && Active.Mapper.id === event.message.user_id) return
ChatView.addMessages(new DataModel.MessageCollection(event.message))
},
mapUpdated: event => {
var map = Active.Map
var couldEditBefore = map.authorizeToEdit(Active.Mapper)
var idBefore = map.id
map.fetch({
success: function(model, response) {
var idNow = model.id
var canEditNow = model.authorizeToEdit(Active.Mapper)
if (idNow !== idBefore) {
Map.leavePrivateMap() // this means the map has been changed to private
} else if (couldEditBefore && !canEditNow) {
Map.cantEditNow()
} else if (!couldEditBefore && canEditNow) {
Map.canEditNow()
} else {
model.trigger('changeByOther')
}
}
})
}
}
export default Cable

View file

@ -1,5 +1,3 @@
/* global $ */
import _ from 'lodash' import _ from 'lodash'
import outdent from 'outdent' import outdent from 'outdent'
@ -7,7 +5,6 @@ import Active from './Active'
import DataModel from './DataModel' import DataModel from './DataModel'
import Filter from './Filter' import Filter from './Filter'
import GlobalUI from './GlobalUI' import GlobalUI from './GlobalUI'
import JIT from './JIT'
import Mouse from './Mouse' import Mouse from './Mouse'
import Selected from './Selected' import Selected from './Selected'
import Settings from './Settings' import Settings from './Settings'
@ -98,13 +95,9 @@ const Control = {
var permToDelete = Active.Mapper.id === topic.get('user_id') || Active.Mapper.get('admin') var permToDelete = Active.Mapper.id === topic.get('user_id') || Active.Mapper.get('admin')
if (permToDelete) { if (permToDelete) {
var mappableid = topic.id
var mapping = node.getData('mapping') var mapping = node.getData('mapping')
topic.destroy() topic.destroy()
DataModel.Mappings.remove(mapping) DataModel.Mappings.remove(mapping)
$(document).trigger(JIT.events.deleteTopic, [{
mappableid: mappableid
}])
Control.hideNode(nodeid) Control.hideNode(nodeid)
} else { } else {
GlobalUI.notifyUser('Only topics you created can be deleted') GlobalUI.notifyUser('Only topics you created can be deleted')
@ -151,13 +144,9 @@ const Control = {
} }
var topic = node.getData('topic') var topic = node.getData('topic')
var mappableid = topic.id
var mapping = node.getData('mapping') var mapping = node.getData('mapping')
mapping.destroy() mapping.destroy()
DataModel.Topics.remove(topic) DataModel.Topics.remove(topic)
$(document).trigger(JIT.events.removeTopic, [{
mappableid: mappableid
}])
Control.hideNode(nodeid) Control.hideNode(nodeid)
}, },
hideSelectedNodes: function() { hideSelectedNodes: function() {
@ -273,7 +262,6 @@ const Control = {
if (edge.getData('synapses').length - 1 === 0) { if (edge.getData('synapses').length - 1 === 0) {
Control.hideEdge(edge) Control.hideEdge(edge)
} }
var mappableid = synapse.id
synapse.destroy() synapse.destroy()
// the server will destroy the mapping, we just need to remove it here // the server will destroy the mapping, we just need to remove it here
@ -283,9 +271,6 @@ const Control = {
if (edge.getData('displayIndex')) { if (edge.getData('displayIndex')) {
delete edge.data.$displayIndex delete edge.data.$displayIndex
} }
$(document).trigger(JIT.events.deleteSynapse, [{
mappableid: mappableid
}])
} else { } else {
GlobalUI.notifyUser('Only synapses you created can be deleted') GlobalUI.notifyUser('Only synapses you created can be deleted')
} }
@ -327,7 +312,6 @@ const Control = {
var synapse = edge.getData('synapses')[index] var synapse = edge.getData('synapses')[index]
var mapping = edge.getData('mappings')[index] var mapping = edge.getData('mappings')[index]
var mappableid = synapse.id
mapping.destroy() mapping.destroy()
DataModel.Synapses.remove(synapse) DataModel.Synapses.remove(synapse)
@ -337,9 +321,6 @@ const Control = {
if (edge.getData('displayIndex')) { if (edge.getData('displayIndex')) {
delete edge.data.$displayIndex delete edge.data.$displayIndex
} }
$(document).trigger(JIT.events.removeSynapse, [{
mappableid: mappableid
}])
}, },
hideSelectedEdges: function() { hideSelectedEdges: function() {
const l = Selected.Edges.length const l = Selected.Edges.length

View file

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

View file

@ -7,7 +7,6 @@ try { Backbone.$ = window.$ } catch (err) {}
import Active from '../Active' import Active from '../Active'
import InfoBox from '../Map/InfoBox' import InfoBox from '../Map/InfoBox'
import Mapper from '../Mapper' import Mapper from '../Mapper'
import Realtime from '../Realtime'
const Map = Backbone.Model.extend({ const Map = Backbone.Model.extend({
urlRoot: '/maps', urlRoot: '/maps',
@ -15,32 +14,8 @@ const Map = Backbone.Model.extend({
toJSON: function(options) { toJSON: function(options) {
return _.omit(this.attributes, this.blacklist) return _.omit(this.attributes, this.blacklist)
}, },
save: function(key, val, options) {
var attrs
// Handle both `"key", value` and `{key: value}` -style arguments.
if (key == null || typeof key === 'object') {
attrs = key
options = val
} else {
(attrs = {})[key] = val
}
var newOptions = options || {}
var s = newOptions.success
newOptions.success = function(model, response, opt) {
if (s) s(model, response, opt)
model.trigger('saved')
}
return Backbone.Model.prototype.save.call(this, attrs, newOptions)
},
initialize: function() { initialize: function() {
this.on('changeByOther', this.updateView) this.on('changeByOther', this.updateView)
this.on('saved', this.savedEvent)
},
savedEvent: function() {
Realtime.updateMap(this)
}, },
authorizeToEdit: function(mapper) { authorizeToEdit: function(mapper) {
if (mapper && ( if (mapper && (

View file

@ -1,5 +1,3 @@
/* global $ */
import _ from 'lodash' import _ from 'lodash'
import outdent from 'outdent' import outdent from 'outdent'
import Backbone from 'backbone' import Backbone from 'backbone'
@ -7,8 +5,6 @@ try { Backbone.$ = window.$ } catch (err) {}
import Active from '../Active' import Active from '../Active'
import Filter from '../Filter' import Filter from '../Filter'
import JIT from '../JIT'
import Realtime from '../Realtime'
import SynapseCard from '../SynapseCard' import SynapseCard from '../SynapseCard'
import Visualize from '../Visualize' import Visualize from '../Visualize'
@ -20,34 +16,6 @@ const Synapse = Backbone.Model.extend({
toJSON: function(options) { toJSON: function(options) {
return _.omit(this.attributes, this.blacklist) return _.omit(this.attributes, this.blacklist)
}, },
save: function(key, val, options) {
var attrs
// Handle both `"key", value` and `{key: value}` -style arguments.
if (key == null || typeof key === 'object') {
attrs = key
options = val
} else {
(attrs = {})[key] = val
}
var newOptions = options || {}
var s = newOptions.success
var permBefore = this.get('permission')
newOptions.success = function(model, response, opt) {
if (s) s(model, response, opt)
model.trigger('saved')
if (permBefore === 'private' && model.get('permission') !== 'private') {
model.trigger('noLongerPrivate')
} else if (permBefore !== 'private' && model.get('permission') === 'private') {
model.trigger('nowPrivate')
}
}
return Backbone.Model.prototype.save.call(this, attrs, newOptions)
},
initialize: function() { initialize: function() {
if (this.isNew()) { if (this.isNew()) {
this.set({ this.set({
@ -56,24 +24,8 @@ const Synapse = Backbone.Model.extend({
'category': 'from-to' 'category': 'from-to'
}) })
} }
this.on('changeByOther', this.updateCardView) this.on('changeByOther', this.updateCardView)
this.on('change', this.updateEdgeView) this.on('change', this.updateEdgeView)
this.on('saved', this.savedEvent)
this.on('noLongerPrivate', function() {
var newSynapseData = {
mappingid: this.getMapping().id,
mappableid: this.id
}
$(document).trigger(JIT.events.newSynapse, [newSynapseData])
})
this.on('nowPrivate', function() {
$(document).trigger(JIT.events.removeSynapse, [{
mappableid: this.id
}])
})
this.on('change:desc', Filter.checkSynapses, this) this.on('change:desc', Filter.checkSynapses, this)
}, },
prepareLiForFilter: function() { prepareLiForFilter: function() {
@ -87,6 +39,10 @@ const Synapse = Backbone.Model.extend({
if (mapper && (this.get('permission') === 'commons' || this.get('collaborator_ids').includes(mapper.get('id')) || this.get('user_id') === mapper.get('id'))) return true if (mapper && (this.get('permission') === 'commons' || this.get('collaborator_ids').includes(mapper.get('id')) || this.get('user_id') === mapper.get('id'))) return true
else return false else return false
}, },
authorizeToShow: function(mapper) {
if (this.get('permission') !== 'private' || (mapper && this.get('collaborator_ids').includes(mapper.get('id')) || this.get('user_id') === mapper.get('id'))) return true
else return false
},
authorizePermissionChange: function(mapper) { authorizePermissionChange: function(mapper) {
if (mapper && this.get('user_id') === mapper.get('id')) return true if (mapper && this.get('user_id') === mapper.get('id')) return true
else return false else return false
@ -149,9 +105,6 @@ const Synapse = Backbone.Model.extend({
return edge return edge
}, },
savedEvent: function() {
Realtime.updateSynapse(this)
},
updateViews: function() { updateViews: function() {
this.updateCardView() this.updateCardView()
this.updateEdgeView() this.updateEdgeView()

View file

@ -1,13 +1,9 @@
/* global $ */
import _ from 'lodash' import _ from 'lodash'
import Backbone from 'backbone' import Backbone from 'backbone'
try { Backbone.$ = window.$ } catch (err) {} try { Backbone.$ = window.$ } catch (err) {}
import Active from '../Active' import Active from '../Active'
import Filter from '../Filter' import Filter from '../Filter'
import JIT from '../JIT'
import Realtime from '../Realtime'
import TopicCard from '../TopicCard' import TopicCard from '../TopicCard'
import Visualize from '../Visualize' import Visualize from '../Visualize'
@ -19,34 +15,6 @@ const Topic = Backbone.Model.extend({
toJSON: function(options) { toJSON: function(options) {
return _.omit(this.attributes, this.blacklist) return _.omit(this.attributes, this.blacklist)
}, },
save: function(key, val, options) {
var attrs
// Handle both `"key", value` and `{key: value}` -style arguments.
if (key == null || typeof key === 'object') {
attrs = key
options = val
} else {
(attrs = {})[key] = val
}
var newOptions = options || {}
var s = newOptions.success
var permBefore = this.get('permission')
newOptions.success = function(model, response, opt) {
if (s) s(model, response, opt)
model.trigger('saved')
if (permBefore === 'private' && model.get('permission') !== 'private') {
model.trigger('noLongerPrivate')
} else if (permBefore !== 'private' && model.get('permission') === 'private') {
model.trigger('nowPrivate')
}
}
return Backbone.Model.prototype.save.call(this, attrs, newOptions)
},
initialize: function() { initialize: function() {
if (this.isNew()) { if (this.isNew()) {
this.set({ this.set({
@ -59,23 +27,6 @@ const Topic = Backbone.Model.extend({
this.on('changeByOther', this.updateCardView) this.on('changeByOther', this.updateCardView)
this.on('change', this.updateNodeView) this.on('change', this.updateNodeView)
this.on('saved', this.savedEvent)
this.on('nowPrivate', function() {
var removeTopicData = {
mappableid: this.id
}
$(document).trigger(JIT.events.removeTopic, [removeTopicData])
})
this.on('noLongerPrivate', function() {
var newTopicData = {
mappingid: this.getMapping().id,
mappableid: this.id
}
$(document).trigger(JIT.events.newTopic, [newTopicData])
})
this.on('change:metacode_id', Filter.checkMetacodes, this) this.on('change:metacode_id', Filter.checkMetacodes, this)
}, },
authorizeToEdit: function(mapper) { authorizeToEdit: function(mapper) {
@ -88,6 +39,10 @@ const Topic = Backbone.Model.extend({
return false return false
} }
}, },
authorizeToShow: function(mapper) {
if (this.get('permission') !== 'private' || (mapper && this.get('collaborator_ids').includes(mapper.get('id')) || this.get('user_id') === mapper.get('id'))) return true
else return false
},
authorizePermissionChange: function(mapper) { authorizePermissionChange: function(mapper) {
if (mapper && this.get('user_id') === mapper.get('id')) return true if (mapper && this.get('user_id') === mapper.get('id')) return true
else return false else return false
@ -135,9 +90,6 @@ const Topic = Backbone.Model.extend({
return node return node
}, },
savedEvent: function() {
Realtime.updateTopic(this)
},
updateViews: function() { updateViews: function() {
var onPageWithTopicCard = Active.Map || Active.Topic var onPageWithTopicCard = Active.Map || Active.Topic
var node = this.get('node') var node = this.get('node')

View file

@ -61,6 +61,7 @@ const CreateMap = {
if (GlobalUI.lightbox === 'forkmap') { if (GlobalUI.lightbox === 'forkmap') {
self.newMap.set('topicsToMap', self.topicsToMap) self.newMap.set('topicsToMap', self.topicsToMap)
self.newMap.set('synapsesToMap', self.synapsesToMap) self.newMap.set('synapsesToMap', self.synapsesToMap)
self.newMap.set('source_id', Active.Map.id)
} }
var formId = GlobalUI.lightbox === 'forkmap' ? '#fork_map' : '#new_map' var formId = GlobalUI.lightbox === 'forkmap' ? '#fork_map' : '#new_map'

View file

@ -36,12 +36,6 @@ const JIT = {
events: { events: {
topicDrag: 'Metamaps:JIT:events:topicDrag', topicDrag: 'Metamaps:JIT:events:topicDrag',
newTopic: 'Metamaps:JIT:events:newTopic',
deleteTopic: 'Metamaps:JIT:events:deleteTopic',
removeTopic: 'Metamaps:JIT:events:removeTopic',
newSynapse: 'Metamaps:JIT:events:newSynapse',
deleteSynapse: 'Metamaps:JIT:events:deleteSynapse',
removeSynapse: 'Metamaps:JIT:events:removeSynapse',
pan: 'Metamaps:JIT:events:pan', pan: 'Metamaps:JIT:events:pan',
zoom: 'Metamaps:JIT:events:zoom', zoom: 'Metamaps:JIT:events:zoom',
animationDone: 'Metamaps:JIT:events:animationDone' animationDone: 'Metamaps:JIT:events:animationDone'

View file

@ -300,7 +300,7 @@ const InfoBox = {
string += '</ul>' string += '</ul>'
if (activeMapperIsCreator) { if (activeMapperIsCreator) {
string += '<div class="collabSearchField"><span class="addCollab"></span><input class="collaboratorSearchField" placeholder="Add a collaborator!"></input></div>' string += '<div class="collabSearchField"><span class="addCollab"></span><input class="collaboratorSearchField" placeholder="Add a collaborator"></input></div>'
} }
return string return string
}, },
@ -391,7 +391,7 @@ const InfoBox = {
DataModel.Maps.Shared.remove(map) DataModel.Maps.Shared.remove(map)
map.destroy() map.destroy()
Router.home() Router.home()
GlobalUI.notifyUser('Map eliminated!') GlobalUI.notifyUser('Map eliminated')
} else if (!authorized) { } else if (!authorized) {
window.alert("Hey now. We can't just go around willy nilly deleting other people's maps now can we? Run off and find something constructive to do, eh?") window.alert("Hey now. We can't just go around willy nilly deleting other people's maps now can we? Run off and find something constructive to do, eh?")
} }

View file

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

View file

@ -12,19 +12,9 @@ module.exports = {
LEAVE_CALL: 'LEAVE_CALL', LEAVE_CALL: 'LEAVE_CALL',
SEND_MAPPER_INFO: 'SEND_MAPPER_INFO', SEND_MAPPER_INFO: 'SEND_MAPPER_INFO',
SEND_COORDS: 'SEND_COORDS', SEND_COORDS: 'SEND_COORDS',
CREATE_MESSAGE: 'CREATE_MESSAGE',
DRAG_TOPIC: 'DRAG_TOPIC', DRAG_TOPIC: 'DRAG_TOPIC',
CREATE_TOPIC: 'CREATE_TOPIC',
UPDATE_TOPIC: 'UPDATE_TOPIC',
REMOVE_TOPIC: 'REMOVE_TOPIC',
DELETE_TOPIC: 'DELETE_TOPIC',
CREATE_SYNAPSE: 'CREATE_SYNAPSE',
UPDATE_SYNAPSE: 'UPDATE_SYNAPSE',
REMOVE_SYNAPSE: 'REMOVE_SYNAPSE',
DELETE_SYNAPSE: 'DELETE_SYNAPSE',
UPDATE_MAP: 'UPDATE_MAP',
/* EVENTS RECEIVABLE */ /* EVENTS RECEIVABLE FROM NODE SERVER */
JUNTO_UPDATED: 'JUNTO_UPDATED', JUNTO_UPDATED: 'JUNTO_UPDATED',
INVITED_TO_CALL: 'INVITED_TO_CALL', INVITED_TO_CALL: 'INVITED_TO_CALL',
INVITED_TO_JOIN: 'INVITED_TO_JOIN', INVITED_TO_JOIN: 'INVITED_TO_JOIN',
@ -38,16 +28,6 @@ module.exports = {
MAPPER_LIST_UPDATED: 'MAPPER_LIST_UPDATED', MAPPER_LIST_UPDATED: 'MAPPER_LIST_UPDATED',
NEW_MAPPER: 'NEW_MAPPER', NEW_MAPPER: 'NEW_MAPPER',
LOST_MAPPER: 'LOST_MAPPER', LOST_MAPPER: 'LOST_MAPPER',
MESSAGE_CREATED: 'MESSAGE_CREATED',
TOPIC_DRAGGED: 'TOPIC_DRAGGED', TOPIC_DRAGGED: 'TOPIC_DRAGGED',
TOPIC_CREATED: 'TOPIC_CREATED', PEER_COORDS_UPDATED: 'PEER_COORDS_UPDATED'
TOPIC_UPDATED: 'TOPIC_UPDATED',
TOPIC_REMOVED: 'TOPIC_REMOVED',
TOPIC_DELETED: 'TOPIC_DELETED',
SYNAPSE_CREATED: 'SYNAPSE_CREATED',
SYNAPSE_UPDATED: 'SYNAPSE_UPDATED',
SYNAPSE_REMOVED: 'SYNAPSE_REMOVED',
SYNAPSE_DELETED: 'SYNAPSE_DELETED',
PEER_COORDS_UPDATED: 'PEER_COORDS_UPDATED',
MAP_UPDATED: 'MAP_UPDATED'
} }

View file

@ -4,10 +4,11 @@ import SimpleWebRTC from 'simplewebrtc'
import SocketIoConnection from 'simplewebrtc/socketioconnection' import SocketIoConnection from 'simplewebrtc/socketioconnection'
import Active from '../Active' import Active from '../Active'
import Cable from '../Cable'
import DataModel from '../DataModel' import DataModel from '../DataModel'
import JIT from '../JIT' import JIT from '../JIT'
import Util from '../Util' import Util from '../Util'
import Views from '../Views' import Views, { ChatView } from '../Views'
import Visualize from '../Visualize' import Visualize from '../Visualize'
import { import {
@ -24,18 +25,8 @@ import {
MAPPER_LEFT_CALL, MAPPER_LEFT_CALL,
NEW_MAPPER, NEW_MAPPER,
LOST_MAPPER, LOST_MAPPER,
MESSAGE_CREATED,
TOPIC_DRAGGED,
TOPIC_CREATED,
TOPIC_UPDATED,
TOPIC_REMOVED,
TOPIC_DELETED,
SYNAPSE_CREATED,
SYNAPSE_UPDATED,
SYNAPSE_REMOVED,
SYNAPSE_DELETED,
PEER_COORDS_UPDATED, PEER_COORDS_UPDATED,
MAP_UPDATED TOPIC_DRAGGED
} from './events' } from './events'
import { import {
@ -53,17 +44,7 @@ import {
peerCoordsUpdated, peerCoordsUpdated,
newMapper, newMapper,
lostMapper, lostMapper,
messageCreated, topicDragged
topicDragged,
topicCreated,
topicUpdated,
topicRemoved,
topicDeleted,
synapseCreated,
synapseUpdated,
synapseRemoved,
synapseDeleted,
mapUpdated
} from './receivable' } from './receivable'
import { import {
@ -79,17 +60,7 @@ import {
leaveCall, leaveCall,
sendCoords, sendCoords,
sendMapperInfo, sendMapperInfo,
createMessage, dragTopic
dragTopic,
createTopic,
updateTopic,
removeTopic,
deleteTopic,
createSynapse,
updateSynapse,
removeSynapse,
deleteSynapse,
updateMap
} from './sendable' } from './sendable'
let Realtime = { let Realtime = {
@ -122,11 +93,12 @@ let Realtime = {
self.socket.on('connect', function() { self.socket.on('connect', function() {
console.log('connected') console.log('connected')
if (Active.Map && Active.Mapper && Active.Map.authorizeToEdit(Active.Mapper)) {
self.checkForCall()
self.joinMap()
}
subscribeToEvents(self, self.socket) subscribeToEvents(self, self.socket)
self.disconnected = false
if (!self.disconnected) {
self.startActiveMap()
} else self.disconnected = false
}) })
self.socket.on('disconnect', function() { self.socket.on('disconnect', function() {
self.disconnected = true self.disconnected = true
@ -173,48 +145,39 @@ let Realtime = {
self.room = new Views.Room({ self.room = new Views.Room({
webrtc: self.webrtc, webrtc: self.webrtc,
socket: self.socket, socket: self.socket,
username: Active.Mapper ? Active.Mapper.get('name') : '',
image: Active.Mapper ? Active.Mapper.get('image') : '',
room: 'global', room: 'global',
$video: self.localVideo.$video, $video: self.localVideo.$video,
myVideoView: self.localVideo.view, myVideoView: self.localVideo.view,
config: { DOUBLE_CLICK_TOLERANCE: 200 }, config: { DOUBLE_CLICK_TOLERANCE: 200 }
soundUrls: [
serverData['sounds/MM_sounds.mp3'],
serverData['sounds/MM_sounds.ogg']
]
}) })
self.room.videoAdded(self.handleVideoAdded) self.room.videoAdded(self.handleVideoAdded)
if (!Active.Map) { self.startActiveMap()
self.room.chat.$container.hide()
}
$('body').prepend(self.room.chat.$container)
} // if Active.Mapper } // if Active.Mapper
}, },
addJuntoListeners: function() { addJuntoListeners: function() {
var self = Realtime var self = Realtime
$(document).on(Views.ChatView.events.openTray, function() { $(document).on(ChatView.events.openTray, function() {
$('.main').addClass('compressed') $('.main').addClass('compressed')
self.chatOpen = true self.chatOpen = true
self.positionPeerIcons() self.positionPeerIcons()
}) })
$(document).on(Views.ChatView.events.closeTray, function() { $(document).on(ChatView.events.closeTray, function() {
$('.main').removeClass('compressed') $('.main').removeClass('compressed')
self.chatOpen = false self.chatOpen = false
self.positionPeerIcons() self.positionPeerIcons()
}) })
$(document).on(Views.ChatView.events.videosOn, function() { $(document).on(ChatView.events.videosOn, function() {
$('#wrapper').removeClass('hideVideos') $('#wrapper').removeClass('hideVideos')
}) })
$(document).on(Views.ChatView.events.videosOff, function() { $(document).on(ChatView.events.videosOff, function() {
$('#wrapper').addClass('hideVideos') $('#wrapper').addClass('hideVideos')
}) })
$(document).on(Views.ChatView.events.cursorsOn, function() { $(document).on(ChatView.events.cursorsOn, function() {
$('#wrapper').removeClass('hideCursors') $('#wrapper').removeClass('hideCursors')
}) })
$(document).on(Views.ChatView.events.cursorsOff, function() { $(document).on(ChatView.events.cursorsOff, function() {
$('#wrapper').addClass('hideCursors') $('#wrapper').addClass('hideCursors')
}) })
}, },
@ -223,10 +186,11 @@ let Realtime = {
if (Active.Map && Active.Mapper) { if (Active.Map && Active.Mapper) {
if (Active.Map.authorizeToEdit(Active.Mapper)) { if (Active.Map.authorizeToEdit(Active.Mapper)) {
self.turnOn() self.turnOn()
self.setupSocket() self.checkForCall()
self.setupLocalSendables() self.joinMap()
} }
self.room.addMessages(new DataModel.MessageCollection(DataModel.Messages), true) self.setupChat() // chat can happen on public maps too
Cable.subscribeToMap(Active.Map.id) // people with edit rights can still see live updates
} }
}, },
endActiveMap: function() { endActiveMap: function() {
@ -236,16 +200,15 @@ let Realtime = {
if (self.inConversation) self.leaveCall() if (self.inConversation) self.leaveCall()
self.leaveMap() self.leaveMap()
$('.collabCompass').remove() $('.collabCompass').remove()
if (self.room) { if (self.room) self.room.leave()
self.room.leave() ChatView.hide()
self.room.chat.$container.hide() ChatView.close()
self.room.chat.close() ChatView.reset()
} Cable.unsubscribeFromMap()
}, },
turnOn: function(notify) { turnOn: function(notify) {
var self = Realtime var self = Realtime
$('.collabCompass').show() $('.collabCompass').show()
self.room.chat.$container.show()
self.room.room = 'map-' + Active.Map.id self.room.room = 'map-' + Active.Map.id
self.activeMapper = { self.activeMapper = {
id: Active.Mapper.id, id: Active.Mapper.id,
@ -258,81 +221,31 @@ let Realtime = {
self.localVideo.view.$container.find('.video-cutoff').css({ self.localVideo.view.$container.find('.video-cutoff').css({
border: '4px solid ' + self.activeMapper.color border: '4px solid ' + self.activeMapper.color
}) })
self.room.chat.addParticipant(self.activeMapper) self.setupLocalEvents()
}, },
setupSocket: function() { setupChat: function() {
var self = Realtime const self = Realtime
self.checkForCall() ChatView.setNewMap()
self.joinMap() ChatView.addParticipant(self.activeMapper)
ChatView.addMessages(new DataModel.MessageCollection(DataModel.Messages), true)
ChatView.show()
}, },
setupLocalSendables: function() { setupLocalEvents: function() {
var self = Realtime var self = Realtime
// local event listeners that trigger events // local event listeners that trigger events
var sendCoords = function(event) { $(document).on(JIT.events.zoom + '.map', self.positionPeerIcons)
$(document).on(JIT.events.pan + '.map', self.positionPeerIcons)
$(document).on('mousemove.map', function(event) {
var pixels = { var pixels = {
x: event.pageX, x: event.pageX,
y: event.pageY y: event.pageY
} }
var coords = Util.pixelsToCoords(Visualize.mGraph, pixels) var coords = Util.pixelsToCoords(Visualize.mGraph, pixels)
self.sendCoords(coords) self.sendCoords(coords)
} })
$(document).on('mousemove.map', sendCoords) $(document).on(JIT.events.topicDrag + '.map', function(event, positions) {
var zoom = function(event, e) {
if (e) {
var pixels = {
x: e.pageX,
y: e.pageY
}
var coords = Util.pixelsToCoords(Visualize.mGraph, pixels)
self.sendCoords(coords)
}
self.positionPeerIcons()
}
$(document).on(JIT.events.zoom + '.map', zoom)
$(document).on(JIT.events.pan + '.map', self.positionPeerIcons)
var dragTopic = function(event, positions) {
self.dragTopic(positions) self.dragTopic(positions)
} })
$(document).on(JIT.events.topicDrag + '.map', dragTopic)
var createTopic = function(event, data) {
self.createTopic(data)
}
$(document).on(JIT.events.newTopic + '.map', createTopic)
var deleteTopic = function(event, data) {
self.deleteTopic(data)
}
$(document).on(JIT.events.deleteTopic + '.map', deleteTopic)
var removeTopic = function(event, data) {
self.removeTopic(data)
}
$(document).on(JIT.events.removeTopic + '.map', removeTopic)
var createSynapse = function(event, data) {
self.createSynapse(data)
}
$(document).on(JIT.events.newSynapse + '.map', createSynapse)
var deleteSynapse = function(event, data) {
self.deleteSynapse(data)
}
$(document).on(JIT.events.deleteSynapse + '.map', deleteSynapse)
var removeSynapse = function(event, data) {
self.removeSynapse(data)
}
$(document).on(JIT.events.removeSynapse + '.map', removeSynapse)
var createMessage = function(event, data) {
self.createMessage(data)
}
$(document).on(Views.Room.events.newMessage + '.map', createMessage)
}, },
countOthersInConversation: function() { countOthersInConversation: function() {
var self = Realtime var self = Realtime
@ -403,7 +316,7 @@ let Realtime = {
callEnded: function() { callEnded: function() {
var self = Realtime var self = Realtime
self.room.conversationEnding() ChatView.conversationEnded()
self.room.leaveVideoOnly() self.room.leaveVideoOnly()
self.inConversation = false self.inConversation = false
self.localVideo.view.$container.hide().css({ self.localVideo.view.$container.hide().css({
@ -495,17 +408,7 @@ const sendables = [
['leaveCall', leaveCall], ['leaveCall', leaveCall],
['sendMapperInfo', sendMapperInfo], ['sendMapperInfo', sendMapperInfo],
['sendCoords', sendCoords], ['sendCoords', sendCoords],
['createMessage', createMessage], ['dragTopic', dragTopic]
['dragTopic', dragTopic],
['createTopic', createTopic],
['updateTopic', updateTopic],
['removeTopic', removeTopic],
['deleteTopic', deleteTopic],
['createSynapse', createSynapse],
['updateSynapse', updateSynapse],
['removeSynapse', removeSynapse],
['deleteSynapse', deleteSynapse],
['updateMap', updateMap]
] ]
sendables.forEach(sendable => { sendables.forEach(sendable => {
Realtime[sendable[0]] = sendable[1](Realtime) Realtime[sendable[0]] = sendable[1](Realtime)
@ -526,17 +429,7 @@ const subscribeToEvents = (Realtime, socket) => {
socket.on(PEER_COORDS_UPDATED, peerCoordsUpdated(Realtime)) socket.on(PEER_COORDS_UPDATED, peerCoordsUpdated(Realtime))
socket.on(NEW_MAPPER, newMapper(Realtime)) socket.on(NEW_MAPPER, newMapper(Realtime))
socket.on(LOST_MAPPER, lostMapper(Realtime)) socket.on(LOST_MAPPER, lostMapper(Realtime))
socket.on(MESSAGE_CREATED, messageCreated(Realtime))
socket.on(TOPIC_DRAGGED, topicDragged(Realtime)) socket.on(TOPIC_DRAGGED, topicDragged(Realtime))
socket.on(TOPIC_CREATED, topicCreated(Realtime))
socket.on(TOPIC_UPDATED, topicUpdated(Realtime))
socket.on(TOPIC_REMOVED, topicRemoved(Realtime))
socket.on(TOPIC_DELETED, topicDeleted(Realtime))
socket.on(SYNAPSE_CREATED, synapseCreated(Realtime))
socket.on(SYNAPSE_UPDATED, synapseUpdated(Realtime))
socket.on(SYNAPSE_REMOVED, synapseRemoved(Realtime))
socket.on(SYNAPSE_DELETED, synapseDeleted(Realtime))
socket.on(MAP_UPDATED, mapUpdated(Realtime))
} }
export default Realtime export default Realtime

View file

@ -4,18 +4,12 @@
everthing in this file happens as a result of websocket events everthing in this file happens as a result of websocket events
*/ */
import { indexOf } from 'lodash'
import { JUNTO_UPDATED } from './events' import { JUNTO_UPDATED } from './events'
import Active from '../Active' import Active from '../Active'
import { ChatView } from '../Views'
import DataModel from '../DataModel' import DataModel from '../DataModel'
import GlobalUI from '../GlobalUI' import GlobalUI from '../GlobalUI'
import Control from '../Control'
import Map from '../Map'
import Mapper from '../Mapper'
import Topic from '../Topic'
import Synapse from '../Synapse'
import Util from '../Util' import Util from '../Util'
import Visualize from '../Visualize' import Visualize from '../Visualize'
@ -24,188 +18,8 @@ export const juntoUpdated = self => state => {
$(document).trigger(JUNTO_UPDATED) $(document).trigger(JUNTO_UPDATED)
} }
export const synapseRemoved = self => data => { /* All the following events are received through the nodejs realtime server
var synapse = DataModel.Synapses.get(data.mappableid) and are done this way because they are transient data, not persisted to the server */
if (synapse) {
var edge = synapse.get('edge')
var mapping = synapse.getMapping()
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)
if (edge.getData('displayIndex')) {
delete edge.data.$displayIndex
}
DataModel.Synapses.remove(synapse)
DataModel.Mappings.remove(mapping)
}
}
export const synapseDeleted = self => data => {
synapseRemoved(self)(data)
}
export const synapseCreated = self => data => {
var topic1, topic2, node1, node2, synapse, mapping, cancel, mapper
function waitThenRenderSynapse() {
if (synapse && mapping && mapper) {
topic1 = synapse.getTopic1()
node1 = topic1.get('node')
topic2 = synapse.getTopic2()
node2 = topic2.get('node')
Synapse.renderSynapse(mapping, synapse, node1, node2, false)
} else if (!cancel) {
setTimeout(waitThenRenderSynapse, 10)
}
}
mapper = DataModel.Mappers.get(data.mapperid)
if (mapper === undefined) {
Mapper.get(data.mapperid, function(m) {
DataModel.Mappers.add(m)
mapper = m
})
}
$.ajax({
url: '/synapses/' + data.mappableid + '.json',
success: function(response) {
DataModel.Synapses.add(response)
synapse = DataModel.Synapses.get(response.id)
},
error: function() {
cancel = true
}
})
$.ajax({
url: '/mappings/' + data.mappingid + '.json',
success: function(response) {
DataModel.Mappings.add(response)
mapping = DataModel.Mappings.get(response.id)
},
error: function() {
cancel = true
}
})
waitThenRenderSynapse()
}
export const topicRemoved = self => data => {
var topic = DataModel.Topics.get(data.mappableid)
if (topic) {
var node = topic.get('node')
var mapping = topic.getMapping()
Control.hideNode(node.id)
DataModel.Topics.remove(topic)
DataModel.Mappings.remove(mapping)
}
}
export const topicDeleted = self => data => {
topicRemoved(self)(data)
}
export const topicCreated = self => data => {
var topic, mapping, mapper, cancel
function waitThenRenderTopic() {
if (topic && mapping && mapper) {
Topic.renderTopic(mapping, topic, false, false)
} else if (!cancel) {
setTimeout(waitThenRenderTopic, 10)
}
}
mapper = DataModel.Mappers.get(data.mapperid)
if (mapper === undefined) {
Mapper.get(data.mapperid, function(m) {
DataModel.Mappers.add(m)
mapper = m
})
}
$.ajax({
url: '/topics/' + data.mappableid + '.json',
success: function(response) {
DataModel.Topics.add(response)
topic = DataModel.Topics.get(response.id)
},
error: function() {
cancel = true
}
})
$.ajax({
url: '/mappings/' + data.mappingid + '.json',
success: function(response) {
DataModel.Mappings.add(response)
mapping = DataModel.Mappings.get(response.id)
},
error: function() {
cancel = true
}
})
waitThenRenderTopic()
}
export const messageCreated = self => data => {
self.room.addMessages(new DataModel.MessageCollection(data))
}
export const mapUpdated = self => data => {
var map = Active.Map
var isActiveMap = map && data.mapId === map.id
if (isActiveMap) {
var couldEditBefore = map.authorizeToEdit(Active.Mapper)
var idBefore = map.id
map.fetch({
success: function(model, response) {
var idNow = model.id
var canEditNow = model.authorizeToEdit(Active.Mapper)
if (idNow !== idBefore) {
Map.leavePrivateMap() // this means the map has been changed to private
} else if (couldEditBefore && !canEditNow) {
Map.cantEditNow()
} else if (!couldEditBefore && canEditNow) {
Map.canEditNow()
} else {
model.trigger('changeByOther')
}
}
})
}
}
export const topicUpdated = self => data => {
var topic = DataModel.Topics.get(data.topicId)
if (topic) {
var node = topic.get('node')
topic.fetch({
success: function(model) {
model.set({ node: node })
model.trigger('changeByOther')
}
})
}
}
export const synapseUpdated = self => data => {
var synapse = DataModel.Synapses.get(data.synapseId)
if (synapse) {
// edge reset necessary because fetch causes model reset
var edge = synapse.get('edge')
synapse.fetch({
success: function(model) {
model.set({ edge: edge })
model.trigger('changeByOther')
}
})
}
}
export const topicDragged = self => positions => { export const topicDragged = self => positions => {
var topic var topic
var node var node
@ -230,10 +44,10 @@ export const lostMapper = self => data => {
// data.userid // data.userid
// data.username // data.username
delete self.mappersOnMap[data.userid] delete self.mappersOnMap[data.userid]
self.room.chat.sound.play('leavemap') ChatView.sound.play('leavemap')
// $('#mapper' + data.userid).remove() // $('#mapper' + data.userid).remove()
$('#compass' + data.userid).remove() $('#compass' + data.userid).remove()
self.room.chat.removeParticipant(data.username) ChatView.removeParticipant(ChatView.participants.findWhere({id: data.userid}))
GlobalUI.notifyUser(data.username + ' just left the map') GlobalUI.notifyUser(data.username + ' just left the map')
@ -262,8 +76,8 @@ export const mapperListUpdated = self => data => {
} }
if (data.userid !== Active.Mapper.id) { if (data.userid !== Active.Mapper.id) {
self.room.chat.addParticipant(self.mappersOnMap[data.userid]) ChatView.addParticipant(self.mappersOnMap[data.userid])
if (data.userinconversation) self.room.chat.mapperJoinedCall(data.userid) if (data.userinconversation) ChatView.mapperJoinedCall(data.userid)
// create a div for the collaborators compass // create a div for the collaborators compass
self.createCompass(data.username, data.userid, data.avatar, self.mappersOnMap[data.userid].color) self.createCompass(data.username, data.userid, data.avatar, self.mappersOnMap[data.userid].color)
@ -291,8 +105,8 @@ export const newMapper = self => data => {
// create an item for them in the realtime box // create an item for them in the realtime box
if (data.userid !== Active.Mapper.id) { if (data.userid !== Active.Mapper.id) {
self.room.chat.sound.play('joinmap') ChatView.sound.play('joinmap')
self.room.chat.addParticipant(self.mappersOnMap[data.userid]) ChatView.addParticipant(self.mappersOnMap[data.userid])
// create a div for the collaborators compass // create a div for the collaborators compass
self.createCompass(data.username, data.userid, data.avatar, self.mappersOnMap[data.userid].color) self.createCompass(data.username, data.userid, data.avatar, self.mappersOnMap[data.userid].color)
@ -311,24 +125,24 @@ export const callAccepted = self => userid => {
// const username = self.mappersOnMap[userid].name // const username = self.mappersOnMap[userid].name
GlobalUI.notifyUser('Conversation starting...') GlobalUI.notifyUser('Conversation starting...')
self.joinCall() self.joinCall()
self.room.chat.invitationAnswered(userid) ChatView.invitationAnswered(userid)
} }
export const callDenied = self => userid => { export const callDenied = self => userid => {
var username = self.mappersOnMap[userid].name var username = self.mappersOnMap[userid].name
GlobalUI.notifyUser(username + " didn't accept your invitation") GlobalUI.notifyUser(username + " didn't accept your invitation")
self.room.chat.invitationAnswered(userid) ChatView.invitationAnswered(userid)
} }
export const inviteDenied = self => userid => { export const inviteDenied = self => userid => {
var username = self.mappersOnMap[userid].name var username = self.mappersOnMap[userid].name
GlobalUI.notifyUser(username + " didn't accept your invitation") GlobalUI.notifyUser(username + " didn't accept your invitation")
self.room.chat.invitationAnswered(userid) ChatView.invitationAnswered(userid)
} }
export const invitedToCall = self => inviter => { export const invitedToCall = self => inviter => {
self.room.chat.sound.stop(self.soundId) ChatView.sound.stop(self.soundId)
self.soundId = self.room.chat.sound.play('sessioninvite') self.soundId = ChatView.sound.play('sessioninvite')
var username = self.mappersOnMap[inviter].name var username = self.mappersOnMap[inviter].name
var notifyText = '<img src="' + self['junto_spinner_darkgrey.gif'] + '" style="display: inline-block; margin-top: -12px; margin-bottom: -6px; vertical-align: top;" />' var notifyText = '<img src="' + self['junto_spinner_darkgrey.gif'] + '" style="display: inline-block; margin-top: -12px; margin-bottom: -6px; vertical-align: top;" />'
@ -341,8 +155,8 @@ export const invitedToCall = self => inviter => {
} }
export const invitedToJoin = self => inviter => { export const invitedToJoin = self => inviter => {
self.room.chat.sound.stop(self.soundId) ChatView.sound.stop(self.soundId)
self.soundId = self.room.chat.sound.play('sessioninvite') self.soundId = ChatView.sound.play('sessioninvite')
var username = self.mappersOnMap[inviter].name var username = self.mappersOnMap[inviter].name
var notifyText = username + ' is inviting you to the conversation. Join?' var notifyText = username + ' is inviting you to the conversation. Join?'
@ -355,16 +169,14 @@ export const invitedToJoin = self => inviter => {
export const mapperJoinedCall = self => id => { export const mapperJoinedCall = self => id => {
var mapper = self.mappersOnMap[id] var mapper = self.mappersOnMap[id]
if (mapper) { if (mapper) {
if (self.inConversation) { if (self.inConversation) {
var username = mapper.name var username = mapper.name
var notifyText = username + ' joined the call' var notifyText = username + ' joined the call'
GlobalUI.notifyUser(notifyText) GlobalUI.notifyUser(notifyText)
} }
mapper.inConversation = true mapper.inConversation = true
self.room.chat.mapperJoinedCall(id) ChatView.mapperJoinedCall(id)
} }
} }
@ -377,7 +189,7 @@ export const mapperLeftCall = self => id => {
GlobalUI.notifyUser(notifyText) GlobalUI.notifyUser(notifyText)
} }
mapper.inConversation = false mapper.inConversation = false
self.room.chat.mapperLeftCall(id) ChatView.mapperLeftCall(id)
if ((self.inConversation && self.countOthersInConversation() === 0) || if ((self.inConversation && self.countOthersInConversation() === 0) ||
(!self.inConversation && self.countOthersInConversation() === 1)) { (!self.inConversation && self.countOthersInConversation() === 1)) {
self.callEnded() self.callEnded()
@ -392,8 +204,7 @@ export const callInProgress = self => () => {
GlobalUI.notifyUser(notifyText, true) GlobalUI.notifyUser(notifyText, true)
$('#toast button.yes').click(e => self.joinCall()) $('#toast button.yes').click(e => self.joinCall())
$('#toast button.no').click(e => GlobalUI.clearNotify()) $('#toast button.no').click(e => GlobalUI.clearNotify())
ChatView.conversationInProgress()
self.room.conversationInProgress()
} }
export const callStarted = self => () => { export const callStarted = self => () => {
@ -404,7 +215,6 @@ export const callStarted = self => () => {
GlobalUI.notifyUser(notifyText, true) GlobalUI.notifyUser(notifyText, true)
$('#toast button.yes').click(e => self.joinCall()) $('#toast button.yes').click(e => self.joinCall())
$('#toast button.no').click(e => GlobalUI.clearNotify()) $('#toast button.no').click(e => GlobalUI.clearNotify())
ChatView.conversationInProgress()
self.room.conversationInProgress()
} }

View file

@ -1,6 +1,7 @@
/* global $ */ /* global $ */
import Active from '../Active' import Active from '../Active'
import { ChatView } from '../Views'
import GlobalUI from '../GlobalUI' import GlobalUI from '../GlobalUI'
import { import {
@ -16,17 +17,7 @@ import {
LEAVE_CALL, LEAVE_CALL,
SEND_MAPPER_INFO, SEND_MAPPER_INFO,
SEND_COORDS, SEND_COORDS,
CREATE_MESSAGE, DRAG_TOPIC
DRAG_TOPIC,
CREATE_TOPIC,
UPDATE_TOPIC,
REMOVE_TOPIC,
DELETE_TOPIC,
CREATE_SYNAPSE,
UPDATE_SYNAPSE,
REMOVE_SYNAPSE,
DELETE_SYNAPSE,
UPDATE_MAP
} from './events' } from './events'
export const joinMap = self => () => { export const joinMap = self => () => {
@ -72,6 +63,7 @@ export const joinCall = self => () => {
$('#wrapper').append(self.localVideo.view.$container) $('#wrapper').append(self.localVideo.view.$container)
} }
self.room.join() self.room.join()
ChatView.conversationInProgress(true)
}) })
self.inConversation = true self.inConversation = true
self.socket.emit(JOIN_CALL, { self.socket.emit(JOIN_CALL, {
@ -80,7 +72,7 @@ export const joinCall = self => () => {
}) })
self.webrtc.startLocalVideo() self.webrtc.startLocalVideo()
GlobalUI.clearNotify() GlobalUI.clearNotify()
self.room.chat.mapperJoinedCall(Active.Mapper.id) ChatView.mapperJoinedCall(Active.Mapper.id)
} }
export const leaveCall = self => () => { export const leaveCall = self => () => {
@ -89,7 +81,8 @@ export const leaveCall = self => () => {
id: Active.Mapper.id id: Active.Mapper.id
}) })
self.room.chat.mapperLeftCall(Active.Mapper.id) ChatView.mapperLeftCall(Active.Mapper.id)
ChatView.leaveConversation() // the conversation will carry on without you
self.room.leaveVideoOnly() self.room.leaveVideoOnly()
self.inConversation = false self.inConversation = false
self.localVideo.view.$container.hide() self.localVideo.view.$container.hide()
@ -102,7 +95,7 @@ export const leaveCall = self => () => {
} }
export const acceptCall = self => userid => { export const acceptCall = self => userid => {
self.room.chat.sound.stop(self.soundId) ChatView.sound.stop(self.soundId)
self.socket.emit(ACCEPT_CALL, { self.socket.emit(ACCEPT_CALL, {
mapid: Active.Map.id, mapid: Active.Map.id,
invited: Active.Mapper.id, invited: Active.Mapper.id,
@ -114,7 +107,7 @@ export const acceptCall = self => userid => {
} }
export const denyCall = self => userid => { export const denyCall = self => userid => {
self.room.chat.sound.stop(self.soundId) ChatView.sound.stop(self.soundId)
self.socket.emit(DENY_CALL, { self.socket.emit(DENY_CALL, {
mapid: Active.Map.id, mapid: Active.Map.id,
invited: Active.Mapper.id, invited: Active.Mapper.id,
@ -124,7 +117,7 @@ export const denyCall = self => userid => {
} }
export const denyInvite = self => userid => { export const denyInvite = self => userid => {
self.room.chat.sound.stop(self.soundId) ChatView.sound.stop(self.soundId)
self.socket.emit(DENY_INVITE, { self.socket.emit(DENY_INVITE, {
mapid: Active.Map.id, mapid: Active.Map.id,
invited: Active.Mapper.id, invited: Active.Mapper.id,
@ -139,7 +132,7 @@ export const inviteACall = self => userid => {
inviter: Active.Mapper.id, inviter: Active.Mapper.id,
invited: userid invited: userid
}) })
self.room.chat.invitationPending(userid) ChatView.invitationPending(userid)
GlobalUI.clearNotify() GlobalUI.clearNotify()
} }
@ -149,13 +142,13 @@ export const inviteToJoin = self => userid => {
inviter: Active.Mapper.id, inviter: Active.Mapper.id,
invited: userid invited: userid
}) })
self.room.chat.invitationPending(userid) ChatView.invitationPending(userid)
} }
export const sendCoords = self => coords => { export const sendCoords = self => coords => {
var map = Active.Map var map = Active.Map
var mapper = Active.Mapper var mapper = Active.Mapper
if (map.authorizeToEdit(mapper)) { if (map && map.authorizeToEdit(mapper)) {
var update = { var update = {
usercoords: coords, usercoords: coords,
userid: Active.Mapper.id, userid: Active.Mapper.id,
@ -172,72 +165,3 @@ export const dragTopic = self => positions => {
} }
} }
export const updateTopic = self => topic => {
var data = {
topicId: topic.id
}
self.socket.emit(UPDATE_TOPIC, data)
}
export const updateSynapse = self => synapse => {
var data = {
synapseId: synapse.id
}
self.socket.emit(UPDATE_SYNAPSE, data)
}
export const updateMap = self => map => {
var data = {
mapId: map.id
}
self.socket.emit(UPDATE_MAP, data)
}
export const createMessage = self => data => {
var message = data.attributes
message.mapid = Active.Map.id
self.socket.emit(CREATE_MESSAGE, message)
}
export const createTopic = self => data => {
if (Active.Map) {
data.mapperid = Active.Mapper.id
data.mapid = Active.Map.id
self.socket.emit(CREATE_TOPIC, data)
}
}
export const deleteTopic = self => data => {
if (Active.Map) {
self.socket.emit(DELETE_TOPIC, data)
}
}
export const removeTopic = self => data => {
if (Active.Map) {
data.mapid = Active.Map.id
self.socket.emit(REMOVE_TOPIC, data)
}
}
export const createSynapse = self => data => {
if (Active.Map) {
data.mapperid = Active.Mapper.id
data.mapid = Active.Map.id
self.socket.emit(CREATE_SYNAPSE, data)
}
}
export const deleteSynapse = self => data => {
if (Active.Map) {
data.mapid = Active.Map.id
self.socket.emit(DELETE_SYNAPSE, data)
}
}
export const removeSynapse = self => data => {
if (Active.Map) {
data.mapid = Active.Map.id
self.socket.emit(REMOVE_SYNAPSE, data)
}
}

View file

@ -4,7 +4,6 @@ import Active from './Active'
import Control from './Control' import Control from './Control'
import Create from './Create' import Create from './Create'
import DataModel from './DataModel' import DataModel from './DataModel'
import JIT from './JIT'
import Map from './Map' import Map from './Map'
import Selected from './Selected' import Selected from './Selected'
import Settings from './Settings' import Settings from './Settings'
@ -40,18 +39,12 @@ const Synapse = {
Control.selectEdge(edgeOnViz) Control.selectEdge(edgeOnViz)
var mappingSuccessCallback = function(mappingModel, response) {
var newSynapseData = {
mappingid: mappingModel.id,
mappableid: mappingModel.get('mappable_id')
}
$(document).trigger(JIT.events.newSynapse, [newSynapseData])
}
var synapseSuccessCallback = function(synapseModel, response) { var synapseSuccessCallback = function(synapseModel, response) {
if (Active.Map) { if (Active.Map) {
mapping.save({ mappable_id: synapseModel.id }, { mapping.save({ mappable_id: synapseModel.id }, {
success: mappingSuccessCallback error: function(model, response) {
console.log('error saving mapping to database')
}
}) })
} }
} }
@ -66,7 +59,9 @@ const Synapse = {
}) })
} else if (!synapse.isNew() && Active.Map) { } else if (!synapse.isNew() && Active.Map) {
mapping.save(null, { mapping.save(null, {
success: mappingSuccessCallback error: function(model, response) {
console.log('error saving mapping to database')
}
}) })
} }
} }

View file

@ -242,12 +242,6 @@ const Topic = {
} }
var mappingSuccessCallback = function(mappingModel, response, topicModel) { var mappingSuccessCallback = function(mappingModel, response, topicModel) {
var newTopicData = {
mappingid: mappingModel.id,
mappableid: mappingModel.get('mappable_id')
}
$(document).trigger(JIT.events.newTopic, [newTopicData])
// call a success callback if provided // call a success callback if provided
if (opts.success) { if (opts.success) {
opts.success(topicModel) opts.success(topicModel)

View file

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

View file

@ -2,128 +2,27 @@
import Backbone from 'backbone' import Backbone from 'backbone'
import { Howl } from 'howler' import { Howl } from 'howler'
import Autolinker from 'autolinker' import React from 'react'
import { clone, template as lodashTemplate } from 'lodash' import ReactDOM from 'react-dom'
import outdent from 'outdent'
// TODO is this line good or bad // TODO is this line good or bad
// Backbone.$ = window.$ // Backbone.$ = window.$
const linker = new Autolinker({ newWindow: true, truncate: 50, email: false, phone: false }) import Active from '../Active'
import DataModel from '../DataModel'
import Realtime from '../Realtime'
import MapChat from '../../components/MapChat'
var Private = { const ChatView = {
messageHTML: outdent` isOpen: false,
<div class='chat-message'> messages: new Backbone.Collection(),
<div class='chat-message-user'><img src='{{ user_image }}' title='{{user_name }}'/></div> conversationLive: false,
<div class='chat-message-text'>{{ message }}</div> isParticipating: false,
<div class='chat-message-time'>{{ timestamp }}</div> mapChat: null,
<div class='clearfloat'></div> domId: 'chat-box-wrapper',
</div>`, init: function(urls) {
participantHTML: outdent` const self = ChatView
<div class='participant participant-{{ id }} {{ selfClass }}'> self.sound = new Howl({
<div class='chat-participant-image'> src: urls,
<img src='{{ image }}' style='border: 2px solid {{ color }};' />
</div>
<div class='chat-participant-name'>
{{ username }} {{ selfName }}
</div>
<button type='button'
class='button chat-participant-invite-call'
onclick='Metamaps.Realtime.inviteACall({{ id}});'
></button>
<button type='button'
class='button chat-participant-invite-join'
onclick='Metamaps.Realtime.inviteToJoin({{ id}});'
></button>
<span class='chat-participant-participating'>
<div class='green-dot'></div>
</span>
<div class='clearfloat'></div>
</div>`,
templates: function() {
const templateSettings = {
interpolate: /\{\{(.+?)\}\}/g
}
this.messageTemplate = lodashTemplate(Private.messageHTML, templateSettings)
this.participantTemplate = lodashTemplate(Private.participantHTML, templateSettings)
},
createElements: function() {
this.$unread = $('<div class="chat-unread"></div>')
this.$button = $('<div class="chat-button"><div class="tooltips">Chat</div></div>')
this.$messageInput = $('<textarea placeholder="Send a message..." class="chat-input"></textarea>')
this.$juntoHeader = $('<div class="junto-header">PARTICIPANTS</div>')
this.$videoToggle = $('<div class="video-toggle"></div>')
this.$cursorToggle = $('<div class="cursor-toggle"></div>')
this.$participants = $('<div class="participants"></div>')
this.$conversationInProgress = $(outdent`
<div class="conversation-live">
LIVE
<span class="call-action leave" onclick="Metamaps.Realtime.leaveCall();">
LEAVE
</span>
<span class="call-action join" onclick="Metamaps.Realtime.joinCall();">
JOIN
</span>
</div>`)
this.$chatHeader = $('<div class="chat-header">CHAT</div>')
this.$soundToggle = $('<div class="sound-toggle"></div>')
this.$messages = $('<div class="chat-messages"></div>')
this.$container = $('<div class="chat-box"></div>')
},
attachElements: function() {
this.$button.append(this.$unread)
this.$juntoHeader.append(this.$videoToggle)
this.$juntoHeader.append(this.$cursorToggle)
this.$chatHeader.append(this.$soundToggle)
this.$participants.append(this.$conversationInProgress)
this.$container.append(this.$juntoHeader)
this.$container.append(this.$participants)
this.$container.append(this.$chatHeader)
this.$container.append(this.$button)
this.$container.append(this.$messages)
this.$container.append(this.$messageInput)
},
addEventListeners: function() {
var self = this
this.participants.on('add', function(participant) {
Private.addParticipant.call(self, participant)
})
this.participants.on('remove', function(participant) {
Private.removeParticipant.call(self, participant)
})
this.$button.on('click', function() {
Handlers.buttonClick.call(self)
})
this.$videoToggle.on('click', function() {
Handlers.videoToggleClick.call(self)
})
this.$cursorToggle.on('click', function() {
Handlers.cursorToggleClick.call(self)
})
this.$soundToggle.on('click', function() {
Handlers.soundToggleClick.call(self)
})
this.$messageInput.on('keyup', function(event) {
Handlers.keyUp.call(self, event)
})
this.$messageInput.on('focus', function() {
Handlers.inputFocus.call(self)
})
this.$messageInput.on('blur', function() {
Handlers.inputBlur.call(self)
})
},
initializeSounds: function(soundUrls) {
this.sound = new Howl({
src: soundUrls,
sprite: { sprite: {
joinmap: [0, 561], joinmap: [0, 561],
leavemap: [1000, 592], leavemap: [1000, 592],
@ -133,226 +32,170 @@ var Private = {
} }
}) })
}, },
incrementUnread: function() { setNewMap: function() {
this.unreadMessages++ const self = ChatView
this.$unread.html(this.unreadMessages) self.conversationLive = false
this.$unread.show() self.isParticipating = false
self.alertSound = true // whether to play sounds on arrival of new messages or not
self.cursorsShowing = true
self.videosShowing = true
self.participants = new Backbone.Collection()
self.render()
}, },
addMessage: function(message, isInitial, wasMe) { show: () => {
if (!this.isOpen && !isInitial) Private.incrementUnread.call(this) $('#' + ChatView.domId).show()
function addZero(i) {
if (i < 10) {
i = '0' + i
}
return i
}
var m = clone(message.attributes)
m.timestamp = new Date(m.created_at)
var date = (m.timestamp.getMonth() + 1) + '/' + m.timestamp.getDate()
date += ' ' + addZero(m.timestamp.getHours()) + ':' + addZero(m.timestamp.getMinutes())
m.timestamp = date
m.image = m.user_image
m.message = linker.link(m.message)
var $html = $(this.messageTemplate(m))
this.$messages.append($html)
if (!isInitial) this.scrollMessages(200)
if (!wasMe && !isInitial && this.alertSound) this.sound.play('receivechat')
}, },
initialMessages: function() { hide: () => {
var messages = this.messages.models $('#' + ChatView.domId).hide()
for (var i = 0; i < messages.length; i++) {
Private.addMessage.call(this, messages[i], true)
}
}, },
handleInputMessage: function() { render: () => {
var message = { if (!Active.Map) return
message: this.$messageInput.val() const self = ChatView
} self.mapChat = ReactDOM.render(React.createElement(MapChat, {
this.$messageInput.val('') conversationLive: self.conversationLive,
$(document).trigger(ChatView.events.message + '-' + this.room, [message]) isParticipating: self.isParticipating,
onOpen: self.onOpen,
onClose: self.onClose,
leaveCall: Realtime.leaveCall,
joinCall: Realtime.joinCall,
inviteACall: Realtime.inviteACall,
inviteToJoin: Realtime.inviteToJoin,
participants: self.participants.models.map(p => p.attributes),
messages: self.messages.models.map(m => m.attributes),
videoToggleClick: self.videoToggleClick,
cursorToggleClick: self.cursorToggleClick,
soundToggleClick: self.soundToggleClick,
inputBlur: self.inputBlur,
inputFocus: self.inputFocus,
handleInputMessage: self.handleInputMessage
}), document.getElementById(ChatView.domId))
}, },
addParticipant: function(participant) { onOpen: () => {
var p = clone(participant.attributes) $(document).trigger(ChatView.events.openTray)
if (p.self) {
p.selfClass = 'is-self'
p.selfName = '(me)'
} else {
p.selfClass = ''
p.selfName = ''
}
var html = this.participantTemplate(p)
this.$participants.append(html)
}, },
removeParticipant: function(participant) { onClose: () => {
this.$container.find('.participant-' + participant.get('id')).remove() $(document).trigger(ChatView.events.closeTray)
} },
} addParticipant: participant => {
ChatView.participants.add(participant)
var Handlers = { ChatView.render()
buttonClick: function() { },
if (this.isOpen) this.close() removeParticipant: participant => {
else if (!this.isOpen) this.open() ChatView.participants.remove(participant)
ChatView.render()
},
leaveConversation: () => {
ChatView.isParticipating = false
ChatView.render()
},
mapperJoinedCall: id => {
const mapper = ChatView.participants.findWhere({id})
mapper && mapper.set('isParticipating', true)
ChatView.render()
},
mapperLeftCall: id => {
const mapper = ChatView.participants.findWhere({id})
mapper && mapper.set('isParticipating', false)
ChatView.render()
},
invitationPending: id => {
const mapper = ChatView.participants.findWhere({id})
mapper && mapper.set('isPending', true)
ChatView.render()
},
invitationAnswered: id => {
const mapper = ChatView.participants.findWhere({id})
mapper && mapper.set('isPending', false)
ChatView.render()
},
conversationInProgress: participating => {
ChatView.conversationLive = true
ChatView.isParticipating = participating
ChatView.render()
},
conversationEnded: () => {
ChatView.conversationLive = false
ChatView.isParticipating = false
ChatView.participants.forEach(p => p.set({isParticipating: false, isPending: false}))
ChatView.render()
},
close: () => {
ChatView.mapChat && ChatView.mapChat.close()
},
open: () => {
ChatView.mapChat && ChatView.mapChat.open()
}, },
videoToggleClick: function() { videoToggleClick: function() {
this.$videoToggle.toggleClass('active') ChatView.videosShowing = !ChatView.videosShowing
this.videosShowing = !this.videosShowing $(document).trigger(ChatView.videosShowing ? ChatView.events.videosOn : ChatView.events.videosOff)
$(document).trigger(this.videosShowing ? ChatView.events.videosOn : ChatView.events.videosOff)
}, },
cursorToggleClick: function() { cursorToggleClick: function() {
this.$cursorToggle.toggleClass('active') ChatView.cursorsShowing = !ChatView.cursorsShowing
this.cursorsShowing = !this.cursorsShowing $(document).trigger(ChatView.cursorsShowing ? ChatView.events.cursorsOn : ChatView.events.cursorsOff)
$(document).trigger(this.cursorsShowing ? ChatView.events.cursorsOn : ChatView.events.cursorsOff)
}, },
soundToggleClick: function() { soundToggleClick: function() {
this.alertSound = !this.alertSound ChatView.alertSound = !ChatView.alertSound
this.$soundToggle.toggleClass('active')
}, },
keyUp: function(event) { inputFocus: () => {
switch (event.which) {
case 13: // enter
Private.handleInputMessage.call(this)
break
}
},
inputFocus: function() {
$(document).trigger(ChatView.events.inputFocus) $(document).trigger(ChatView.events.inputFocus)
}, },
inputBlur: function() { inputBlur: () => {
$(document).trigger(ChatView.events.inputBlur) $(document).trigger(ChatView.events.inputBlur)
},
addMessage: (message, isInitial, wasMe) => {
const self = ChatView
if (!isInitial) self.mapChat.newMessage()
if (!wasMe && !isInitial && self.alertSound) self.sound.play('receivechat')
self.messages.add(message)
self.render()
if (!isInitial) self.mapChat.scroll()
},
sendChatMessage: message => {
var self = ChatView
if (ChatView.alertSound) ChatView.sound.play('sendchat')
var m = new DataModel.Message({
message: message.message,
resource_id: Active.Map.id,
resource_type: 'Map'
})
m.save(null, {
success: function(model, response) {
self.addMessages(new DataModel.MessageCollection(model), false, true)
},
error: function(model, response) {
console.log('error!', response)
}
})
},
handleInputMessage: text => {
ChatView.sendChatMessage({message: text})
},
// they should be instantiated as backbone models before they get
// passed to this function
addMessages: (messages, isInitial, wasMe) => {
messages.models.forEach(m => ChatView.addMessage(m, isInitial, wasMe))
},
reset: () => {
ChatView.mapChat && ChatView.mapChat.reset()
ChatView.participants && ChatView.participants.reset()
ChatView.messages && ChatView.messages.reset()
ChatView.render()
} }
} }
const ChatView = function(messages, mapper, room, opts = {}) { // ChatView.prototype.scrollMessages = function(duration) {
this.room = room // duration = duration || 0
this.mapper = mapper
this.messages = messages // backbone collection
this.isOpen = false // this.$messages.animate({
this.alertSound = true // whether to play sounds on arrival of new messages or not // scrollTop: this.$messages[0].scrollHeight
this.cursorsShowing = true // }, duration)
this.videosShowing = true // }
this.unreadMessages = 0
this.participants = new Backbone.Collection()
Private.templates.call(this)
Private.createElements.call(this)
Private.attachElements.call(this)
Private.addEventListeners.call(this)
Private.initialMessages.call(this)
Private.initializeSounds.call(this, opts.soundUrls)
this.$container.css({
right: '-300px'
})
}
ChatView.prototype.conversationInProgress = function(participating) {
this.$conversationInProgress.show()
this.$participants.addClass('is-live')
if (participating) this.$participants.addClass('is-participating')
this.$button.addClass('active')
// hide invite to call buttons
}
ChatView.prototype.conversationEnded = function() {
this.$conversationInProgress.hide()
this.$participants.removeClass('is-live')
this.$participants.removeClass('is-participating')
this.$button.removeClass('active')
this.$participants.find('.participant').removeClass('active')
this.$participants.find('.participant').removeClass('pending')
}
ChatView.prototype.leaveConversation = function() {
this.$participants.removeClass('is-participating')
}
ChatView.prototype.mapperJoinedCall = function(id) {
this.$participants.find('.participant-' + id).addClass('active')
}
ChatView.prototype.mapperLeftCall = function(id) {
this.$participants.find('.participant-' + id).removeClass('active')
}
ChatView.prototype.invitationPending = function(id) {
this.$participants.find('.participant-' + id).addClass('pending')
}
ChatView.prototype.invitationAnswered = function(id) {
this.$participants.find('.participant-' + id).removeClass('pending')
}
ChatView.prototype.addParticipant = function(participant) {
this.participants.add(participant)
}
ChatView.prototype.removeParticipant = function(username) {
var p = this.participants.find(p => p.get('username') === username)
if (p) {
this.participants.remove(p)
}
}
ChatView.prototype.removeParticipants = function() {
this.participants.remove(this.participants.models)
}
ChatView.prototype.open = function() {
this.$container.css({
right: '0'
})
this.$messageInput.focus()
this.isOpen = true
this.unreadMessages = 0
this.$unread.hide()
this.scrollMessages(0)
$(document).trigger(ChatView.events.openTray)
}
ChatView.prototype.addMessage = function(message, isInitial, wasMe) {
this.messages.add(message)
Private.addMessage.call(this, message, isInitial, wasMe)
}
ChatView.prototype.scrollMessages = function(duration) {
duration = duration || 0
this.$messages.animate({
scrollTop: this.$messages[0].scrollHeight
}, duration)
}
ChatView.prototype.clearMessages = function() {
this.unreadMessages = 0
this.$unread.hide()
this.$messages.empty()
}
ChatView.prototype.close = function() {
this.$container.css({
right: '-300px'
})
this.$messageInput.blur()
this.isOpen = false
$(document).trigger(ChatView.events.closeTray)
}
ChatView.prototype.remove = function() {
this.$button.off()
this.$container.remove()
}
/** /**
* @class * @class
* @static * @static
*/ */
ChatView.events = { ChatView.events = {
message: 'ChatView:message',
openTray: 'ChatView:openTray', openTray: 'ChatView:openTray',
closeTray: 'ChatView:closeTray', closeTray: 'ChatView:closeTray',
inputFocus: 'ChatView:inputFocus', inputFocus: 'ChatView:inputFocus',

View file

@ -1,16 +1,8 @@
/* global $ */ /* global $ */
import Backbone from 'backbone'
import attachMediaStream from 'attachmediastream' import attachMediaStream from 'attachmediastream'
// TODO is this line good or bad
// Backbone.$ = window.$
import Active from '../Active'
import DataModel from '../DataModel'
import Realtime from '../Realtime' import Realtime from '../Realtime'
import ChatView from './ChatView'
import VideoView from './VideoView' import VideoView from './VideoView'
const Room = function(opts = {}) { const Room = function(opts = {}) {
@ -19,38 +11,18 @@ const Room = function(opts = {}) {
this.webrtc = opts.webrtc this.webrtc = opts.webrtc
this.room = opts.room this.room = opts.room
this.config = opts.config this.config = opts.config
this.peopleCount = 0
this.$myVideo = opts.$video this.$myVideo = opts.$video
this.myVideo = opts.myVideoView this.myVideo = opts.myVideoView
this.messages = new Backbone.Collection()
this.currentMapper = new Backbone.Model({ name: opts.username, image: opts.image })
this.chat = new ChatView(this.messages, this.currentMapper, this.room, {
soundUrls: opts.soundUrls
})
this.videos = {} this.videos = {}
this.init() this.init()
} }
Room.prototype.join = function(cb) { Room.prototype.join = function(cb) {
this.isActiveRoom = true this.isActiveRoom = true
this.webrtc.joinRoom(this.room, cb) this.webrtc.joinRoom(this.room, cb)
this.chat.conversationInProgress(true) // true indicates participation
}
Room.prototype.conversationInProgress = function() {
this.chat.conversationInProgress(false) // false indicates not participating
}
Room.prototype.conversationEnding = function() {
this.chat.conversationEnded()
} }
Room.prototype.leaveVideoOnly = function() { Room.prototype.leaveVideoOnly = function() {
this.chat.leaveConversation() // the conversation will carry on without you
for (var id in this.videos) { for (var id in this.videos) {
this.removeVideo(id) this.removeVideo(id)
} }
@ -66,14 +38,6 @@ Room.prototype.leave = function() {
this.isActiveRoom = false this.isActiveRoom = false
this.webrtc.leaveRoom() this.webrtc.leaveRoom()
this.webrtc.stopLocalVideo() this.webrtc.stopLocalVideo()
this.chat.conversationEnded()
this.chat.removeParticipants()
this.chat.clearMessages()
this.messages.reset()
}
Room.prototype.setPeopleCount = function(count) {
this.peopleCount = count
} }
Room.prototype.init = function() { Room.prototype.init = function() {
@ -129,11 +93,6 @@ Room.prototype.init = function() {
} }
v.$container.show() v.$container.show()
}) })
var sendChatMessage = function(event, data) {
self.sendChatMessage(data)
}
$(document).on(ChatView.events.message + '-' + this.room, sendChatMessage)
} }
Room.prototype.videoAdded = function(callback) { Room.prototype.videoAdded = function(callback) {
@ -158,42 +117,4 @@ Room.prototype.removeVideo = function(peer) {
} }
} }
Room.prototype.sendChatMessage = function(data) {
var self = this
// this.roomRef.child('messages').push(data)
if (self.chat.alertSound) self.chat.sound.play('sendchat')
var m = new DataModel.Message({
message: data.message,
resource_id: Active.Map.id,
resource_type: 'Map'
})
m.save(null, {
success: function(model, response) {
self.addMessages(new DataModel.MessageCollection(model), false, true)
$(document).trigger(Room.events.newMessage, [model])
},
error: function(model, response) {
console.log('error!', response)
}
})
}
// they should be instantiated as backbone models before they get
// passed to this function
Room.prototype.addMessages = function(messages, isInitial, wasMe) {
var self = this
messages.models.forEach(function(message) {
self.chat.addMessage(message, isInitial, wasMe)
})
}
/**
* @class
* @static
*/
Room.events = {
newMessage: 'Room:newMessage'
}
export default Room export default Room

View file

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

View file

@ -2,9 +2,10 @@ import Account from './Account'
import Active from './Active' import Active from './Active'
import Admin from './Admin' import Admin from './Admin'
import AutoLayout from './AutoLayout' import AutoLayout from './AutoLayout'
import DataModel from './DataModel' import Cable from './Cable'
import Control from './Control' import Control from './Control'
import Create from './Create' import Create from './Create'
import DataModel from './DataModel'
import Debug from './Debug' import Debug from './Debug'
import Filter from './Filter' import Filter from './Filter'
import GlobalUI, { import GlobalUI, {
@ -38,9 +39,10 @@ Metamaps.Account = Account
Metamaps.Active = Active Metamaps.Active = Active
Metamaps.Admin = Admin Metamaps.Admin = Admin
Metamaps.AutoLayout = AutoLayout Metamaps.AutoLayout = AutoLayout
Metamaps.DataModel = DataModel Metamaps.Cable = Cable
Metamaps.Control = Control Metamaps.Control = Control
Metamaps.Create = Create Metamaps.Create = Create
Metamaps.DataModel = DataModel
Metamaps.Debug = Debug Metamaps.Debug = Debug
Metamaps.Filter = Filter Metamaps.Filter = Filter
Metamaps.GlobalUI = GlobalUI Metamaps.GlobalUI = GlobalUI

View file

@ -6,7 +6,6 @@ class ImportDialogBox extends Component {
super(props) super(props)
this.state = { this.state = {
showImportInstructions: false
} }
} }
@ -15,21 +14,9 @@ class ImportDialogBox extends Component {
} }
handleFile = (files, e) => { handleFile = (files, e) => {
// // for some reason it uploads twice, so we need this debouncer
// // eslint-disable-next-line no-return-assign
// this.debouncer = this.debouncer || window.setTimeout(() => this.debouncer = null, 10)
// if (!this.debouncer) {
// this.props.onFileAdded(files[0])
// }
this.props.onFileAdded(files[0]) this.props.onFileAdded(files[0])
} }
toggleShowInstructions = e => {
this.setState({
showImportInstructions: !this.state.showImportInstructions
})
}
render = () => { render = () => {
return ( return (
<div className="import-dialog"> <div className="import-dialog">
@ -47,23 +34,7 @@ class ImportDialogBox extends Component {
> >
Drop files here! Drop files here!
</Dropzone> </Dropzone>
<p> <p>See <a href="https://docs.metamaps.cc/importing_and_exporting_data.html">docs.metamaps.cc</a> for instructions.</p>
<a onClick={this.toggleShowInstructions} style={{ textDecoration: 'underline', cursor: 'pointer' }}>
Show/hide import instructions
</a>
</p>
{!this.state.showImportInstructions ? null : (<div>
<p>
You can import topics and synapses by uploading a spreadsheet here.
The file should be in comma-separated format (when you save, change the
filetype from .xls to .csv).
</p>
<img src={this.props.exampleImageUrl} style={{ width: '100%' }} />
<p style={{ marginTop: '1em' }}>You can choose which columns to include in your data. Topics must have a name field. Synapses must have Topic 1 and Topic 2.</p>
<p>&nbsp;</p>
<p> * There are many valid import formats. Try exporting a map to see what columns you can include in your import data. You can also copy-paste from Excel to import, or import JSON.</p>
<p> * If you are importing a list of links, you can use a Link column in place of the Name column.</p>
</div>)}
</div> </div>
) )
} }

View file

@ -0,0 +1,40 @@
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 })
function addZero(i) {
if (i < 10) {
i = '0' + i
}
return i
}
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: 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={userImage} />}
</div>
{heading && <div className="chat-message-meta">
<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>
</div>
)
}
export default Message

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

@ -0,0 +1,45 @@
import React, { PropTypes, Component } from 'react'
class Participant extends Component {
render() {
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>
)
}
}
Participant.propTypes = {
conversationLive: PropTypes.bool,
mapperIsLive: PropTypes.bool,
isParticipating: PropTypes.bool,
isPending: PropTypes.bool,
color: PropTypes.string, // css color
id: PropTypes.number,
image: PropTypes.string, // image url
self: PropTypes.bool,
username: PropTypes.string,
inviteACall: PropTypes.func,
inviteToJoin: PropTypes.func
}
export default Participant

View file

@ -0,0 +1,7 @@
import React from 'react'
const Unread = props => {
return props.count ? <div className="chat-unread">{props.count}</div> : null
}
export default Unread

View file

@ -0,0 +1,193 @@
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
return messages ? messages.map(m => {
let heading = false
if (!currentHeader) {
heading = true
currentHeader = m
} else {
// not same user or time diff of greater than 3 minutes
heading = m.user_id !== currentHeader.user_id || Math.floor(Math.abs(new Date(currentHeader.created_at) - new Date(m.created_at)) / 60000) > 3
currentHeader = heading ? m : currentHeader
}
return <Message {...m} key={m.id} heading={heading}/>
}) : null
}
class MapChat extends Component {
constructor(props) {
super(props)
this.state = {
unreadMessages: 0,
open: false,
messageText: '',
alertSound: true, // whether to play sounds on arrival of new messages or not
cursorsShowing: true,
videosShowing: true
}
}
reset = () => {
this.setState({
unreadMessages: 0,
open: false,
messageText: '',
alertSound: true, // whether to play sounds on arrival of new messages or not
cursorsShowing: true,
videosShowing: true
})
}
close = () => {
this.setState({open: false})
this.props.onClose()
this.messageInput.blur()
}
open = () => {
this.scroll()
this.setState({open: true, unreadMessages: 0})
this.props.onOpen()
this.messageInput.focus()
}
newMessage = () => {
if (!this.state.open) this.setState({unreadMessages: this.state.unreadMessages + 1})
}
scroll = () => {
this.messagesDiv.scrollTop = this.messagesDiv.scrollHeight
}
toggleDrawer = () => {
if (this.state.open) this.close()
else if (!this.state.open) this.open()
}
toggleAlertSound = () => {
this.setState({alertSound: !this.state.alertSound})
this.props.soundToggleClick()
}
toggleCursorsShowing = () => {
this.setState({cursorsShowing: !this.state.cursorsShowing})
this.props.cursorToggleClick()
}
toggleVideosShowing = () => {
this.setState({videosShowing: !this.state.videosShowing})
this.props.videoToggleClick()
}
handleChange = key => e => {
this.setState({
[key]: e.target.value
})
}
handleTextareaKeyUp = e => {
if (e.which === 13) {
e.preventDefault()
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
const { videosShowing, cursorsShowing, alertSound, unreadMessages } = this.state
return (
<div className="chat-box"
style={{ right: rightOffset }}
>
<div className="junto-header">
PARTICIPANTS
<div onClick={this.toggleVideosShowing} className={`video-toggle ${videosShowing ? '' : 'active'}`} />
<div onClick={this.toggleCursorsShowing} className={`cursor-toggle ${cursorsShowing ? '' : 'active'}`} />
</div>
<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>}
</div>}
{participants.map(participant => <Participant
key={participant.id}
{...participant}
inviteACall={inviteACall}
inviteToJoin={inviteToJoin}
conversationLive={conversationLive}
mapperIsLive={isParticipating}/>
)}
</div>
<div className="chat-header">
CHAT
<div onClick={this.toggleAlertSound} className={`sound-toggle ${alertSound ? '' : 'active'}`}></div>
</div>
<div className={`chat-button ${conversationLive ? 'active' : ''}`} onClick={this.toggleDrawer}>
<div className="tooltips">Chat</div>
<Unread count={unreadMessages} />
</div>
<div className="chat-messages" ref={div => { this.messagesDiv = div }}>
{makeList(messages)}
</div>
<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>
)
}
}
MapChat.propTypes = {
conversationLive: PropTypes.bool,
isParticipating: PropTypes.bool,
onOpen: PropTypes.func,
onClose: PropTypes.func,
leaveCall: PropTypes.func,
joinCall: PropTypes.func,
inviteACall: PropTypes.func,
inviteToJoin: PropTypes.func,
videoToggleClick: PropTypes.func,
cursorToggleClick: PropTypes.func,
soundToggleClick: PropTypes.func,
participants: PropTypes.arrayOf(PropTypes.shape({
color: PropTypes.string, // css color
id: PropTypes.number,
image: PropTypes.string, // image url
self: PropTypes.bool,
username: PropTypes.string,
isParticipating: PropTypes.bool,
isPending: PropTypes.bool
}))
}
export default MapChat

View file

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

View file

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

View file

@ -5,7 +5,9 @@
"scripts": { "scripts": {
"build": "webpack", "build": "webpack",
"build:watch": "webpack --watch", "build:watch": "webpack --watch",
"test": "mocha --compilers js:babel-core/register frontend/test" "test": "mocha --compilers js:babel-core/register frontend/test",
"eslint": "eslint frontend",
"eslint:fix": "eslint --fix frontend"
}, },
"repository": { "repository": {
"type": "git", "type": "git",
@ -27,9 +29,10 @@
"babel-preset-es2015": "6.18.0", "babel-preset-es2015": "6.18.0",
"babel-preset-react": "6.16.0", "babel-preset-react": "6.16.0",
"backbone": "1.0.0", "backbone": "1.0.0",
"clipboard-js": "git://github.com/devvmh/clipboard.js#patch-1", "clipboard-js": "0.3.2",
"commonmark": "0.27.0", "commonmark": "0.27.0",
"csv-parse": "1.1.7", "csv-parse": "1.1.7",
"emoji-mart": "^0.3.5",
"getScreenMedia": "git://github.com/devvmh/getScreenMedia#patch-1", "getScreenMedia": "git://github.com/devvmh/getScreenMedia#patch-1",
"hark": "git://github.com/devvmh/hark#patch-1", "hark": "git://github.com/devvmh/hark#patch-1",
"howler": "2.0.2", "howler": "2.0.2",

View file

@ -1,22 +1,12 @@
const { const {
// server sendable, client receivable // server sendable, client receivable
TOPIC_UPDATED,
TOPIC_DELETED,
SYNAPSE_UPDATED,
SYNAPSE_DELETED,
MAP_UPDATED,
JUNTO_UPDATED, JUNTO_UPDATED,
// server receivable, client sendable // server receivable, client sendable
JOIN_CALL, JOIN_CALL,
LEAVE_CALL, LEAVE_CALL,
JOIN_MAP, JOIN_MAP,
LEAVE_MAP, LEAVE_MAP
UPDATE_TOPIC,
DELETE_TOPIC,
UPDATE_SYNAPSE,
DELETE_SYNAPSE,
UPDATE_MAP
} = require('../frontend/src/Metamaps/Realtime/events') } = require('../frontend/src/Metamaps/Realtime/events')
module.exports = function(io, store) { module.exports = function(io, store) {
@ -33,25 +23,5 @@ module.exports = function(io, store) {
socket.on(JOIN_CALL, data => store.dispatch({ type: JOIN_CALL, payload: data })) socket.on(JOIN_CALL, data => store.dispatch({ type: JOIN_CALL, payload: data }))
socket.on(LEAVE_CALL, () => store.dispatch({ type: LEAVE_CALL, payload: socket })) socket.on(LEAVE_CALL, () => store.dispatch({ type: LEAVE_CALL, payload: socket }))
socket.on('disconnect', () => store.dispatch({ type: 'DISCONNECT', payload: socket })) socket.on('disconnect', () => store.dispatch({ type: 'DISCONNECT', payload: socket }))
socket.on(UPDATE_TOPIC, function(data) {
socket.broadcast.emit(TOPIC_UPDATED, data)
})
socket.on(DELETE_TOPIC, function(data) {
socket.broadcast.emit(TOPIC_DELETED, data)
})
socket.on(UPDATE_SYNAPSE, function(data) {
socket.broadcast.emit(SYNAPSE_UPDATED, data)
})
socket.on(DELETE_SYNAPSE, function(data) {
socket.broadcast.emit(SYNAPSE_DELETED, data)
})
socket.on(UPDATE_MAP, function(data) {
socket.broadcast.emit(MAP_UPDATED, data)
})
}) })
} }

View file

@ -2,24 +2,14 @@ const {
MAPPER_LIST_UPDATED, MAPPER_LIST_UPDATED,
NEW_MAPPER, NEW_MAPPER,
LOST_MAPPER, LOST_MAPPER,
MESSAGE_CREATED,
TOPIC_DRAGGED, TOPIC_DRAGGED,
TOPIC_CREATED,
TOPIC_REMOVED,
SYNAPSE_CREATED,
SYNAPSE_REMOVED,
PEER_COORDS_UPDATED, PEER_COORDS_UPDATED,
JOIN_MAP, JOIN_MAP,
LEAVE_MAP, LEAVE_MAP,
SEND_COORDS, SEND_COORDS,
SEND_MAPPER_INFO, SEND_MAPPER_INFO,
CREATE_MESSAGE,
DRAG_TOPIC, DRAG_TOPIC,
CREATE_TOPIC,
REMOVE_TOPIC,
CREATE_SYNAPSE,
REMOVE_SYNAPSE
} = require('../frontend/src/Metamaps/Realtime/events') } = require('../frontend/src/Metamaps/Realtime/events')
const { mapRoom, userMapRoom } = require('./rooms') const { mapRoom, userMapRoom } = require('./rooms')
@ -74,40 +64,11 @@ module.exports = function(io, store) {
socket.broadcast.in(mapRoom(data.mapid)).emit(PEER_COORDS_UPDATED, peer) socket.broadcast.in(mapRoom(data.mapid)).emit(PEER_COORDS_UPDATED, peer)
}) })
socket.on(CREATE_MESSAGE, function(data) {
var mapId = data.mapid
delete data.mapid
socket.broadcast.in(mapRoom(mapId)).emit(MESSAGE_CREATED, data)
})
socket.on(DRAG_TOPIC, function(data) { socket.on(DRAG_TOPIC, function(data) {
var mapId = data.mapid var mapId = data.mapid
delete data.mapid delete data.mapid
socket.broadcast.in(mapRoom(mapId)).emit(TOPIC_DRAGGED, data) socket.broadcast.in(mapRoom(mapId)).emit(TOPIC_DRAGGED, data)
}) })
socket.on(CREATE_TOPIC, function(data) {
var mapId = data.mapid
delete data.mapid
socket.broadcast.in(mapRoom(mapId)).emit(TOPIC_CREATED, data)
})
socket.on(REMOVE_TOPIC, function(data) {
var mapId = data.mapid
delete data.mapid
socket.broadcast.in(mapRoom(mapId)).emit(TOPIC_REMOVED, data)
})
socket.on(CREATE_SYNAPSE, function(data) {
var mapId = data.mapid
delete data.mapid
socket.broadcast.in(mapRoom(mapId)).emit(SYNAPSE_CREATED, data)
})
socket.on(REMOVE_SYNAPSE, function(data) {
var mapId = data.mapid
delete data.mapid
socket.broadcast.in(mapRoom(mapId)).emit(SYNAPSE_REMOVED, data)
})
}) })
} }

View file

@ -16,3 +16,4 @@ junto(io, store)
map(io, store) map(io, store)
io.listen(parseInt(process.env.NODE_REALTIME_PORT) || 5000) io.listen(parseInt(process.env.NODE_REALTIME_PORT) || 5000)
console.log('booting up', process.env.NODE_REALTIME_PORT || 5000)

View file

@ -5,7 +5,8 @@ require 'rails_helper'
RSpec.describe 'maps API', type: :request do RSpec.describe 'maps API', type: :request do
let(:user) { create(:user, admin: true) } let(:user) { create(:user, admin: true) }
let(:token) { create(:token, user: user).token } let(:token) { create(:token, user: user).token }
let(:map) { create(:map, user: user) } let(:source) { create(:map, user: user) }
let(:map) { create(:map, user: user, source: source) }
describe 'GET /api/v2/maps' do describe 'GET /api/v2/maps' do
it 'returns all maps' do it 'returns all maps' do
@ -42,7 +43,7 @@ RSpec.describe 'maps API', type: :request do
expect(response).to have_http_status(:success) expect(response).to have_http_status(:success)
expect(response).to match_json_schema(:map) expect(response).to match_json_schema(:map)
expect(Map.count).to eq 2 expect(Map.count).to eq 3
end end
it 'PATCH /api/v2/maps/:id' do it 'PATCH /api/v2/maps/:id' do
@ -56,7 +57,7 @@ RSpec.describe 'maps API', type: :request do
delete "/api/v2/maps/#{map.id}", params: { access_token: token } delete "/api/v2/maps/#{map.id}", params: { access_token: token }
expect(response).to have_http_status(:no_content) expect(response).to have_http_status(:no_content)
expect(Map.count).to eq 0 expect(Map.count).to eq 1
end end
it 'POST /api/v2/maps/:id/stars' do it 'POST /api/v2/maps/:id/stars' do

View file

@ -4,6 +4,7 @@ FactoryGirl.define do
sequence(:name) { |n| "Cool Map ##{n}" } sequence(:name) { |n| "Cool Map ##{n}" }
permission :commons permission :commons
arranged { false } arranged { false }
source_id nil
desc '' desc ''
user user
end end