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 'slack-notifier'
gem 'snorlax'
gem 'puma'
# asset stuff
gem 'jquery-rails'

View file

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

View file

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

View file

@ -14,6 +14,7 @@
//= require jquery
//= require jquery-ui
//= require jquery_ujs
//= require action_cable
//= require_directory ./lib
//= require ./webpacked/metamaps.bundle
//= 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 {
margin-left: 8px;
max-width: 162px;
display: inline-block;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
vertical-align: middle;
}
.creatorAndPerm.cardHasViewOnly span.creatorName {
max-width: 95px;
}
.cardViewOnly {

View file

@ -93,7 +93,7 @@
.sidebarSearchField {
float: left;
width: 380px;
width: 379px;
padding: 7px 10px 3px 10px;
height: 20px;
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
def create_map_params
params.permit(:name, :desc, :permission)
params.permit(:name, :desc, :permission, :source_id)
end
def update_map_params

View file

@ -1,6 +1,7 @@
# frozen_string_literal: true
class Map < ApplicationRecord
belongs_to :user
belongs_to :source, class_name: :Map
has_many :topicmappings, -> { Mapping.topicmapping }, class_name: :Mapping, dependent: :destroy
has_many :synapsemappings, -> { Mapping.synapsemapping }, class_name: :Mapping, dependent: :destroy
@ -32,6 +33,7 @@ class Map < ApplicationRecord
# Validate the attached image is image/jpg, image/png, etc
validates_attachment_content_type :screenshot, content_type: /\Aimage\/.*\Z/
after_update :after_updated
after_save :update_deferring_topics_and_synapses, if: :permission_changed?
delegate :count, to: :topics, prefix: :topic # same as `def topic_count; topics.count; end`
@ -118,6 +120,13 @@ class Map < ApplicationRecord
end
removed.compact
end
def after_updated
attrs = ['name', 'desc', 'permission']
if attrs.any? {|k| changed_attributes.key?(k)}
ActionCable.server.broadcast 'map_' + id.to_s, type: 'mapUpdated'
end
end
def update_deferring_topics_and_synapses
Topic.where(defer_to_map_id: id).update_all(permission: permission)

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -3,16 +3,13 @@
# 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 || [];
_gaq.push(['_setAccount', 'UA-35984510-1']);
_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);
})();
ga('create', '<%= ENV["GA_TRACKING_CODE"] %>', 'auto');
ga('send', 'pageview');
</script>

View file

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

View file

@ -5,7 +5,7 @@
<div class="templates">
<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="mapInfoStat">

View file

@ -9,6 +9,8 @@
<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>
<%= content_tag :div, class: "main" do %>
@ -48,7 +50,7 @@
<div id="instructions">
<div class="addTopic">
Double-click to<br>add a topic!
Double-click to<br>add a topic
</div>
<div class="tabKey">
Use Tab & Shift+Tab to select a metacode

View file

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

View file

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

View file

@ -3,6 +3,8 @@
Rails.application.configure do
# 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
logger = Logger.new(STDOUT)
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
Metamaps::Application.routes.draw do
use_doorkeeper
mount ActionCable.server => '/cable'
root to: 'main#home', via: :get
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.
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
enable_extension "plpgsql"
@ -151,6 +151,8 @@ ActiveRecord::Schema.define(version: 20161216174257) do
t.string "screenshot_content_type", limit: 255
t.integer "screenshot_file_size"
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
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_receipts", "mailboxer_notifications", column: "notification_id", name: "receipts_on_notification_id"
add_foreign_key "mappings", "users", column: "updated_by_id"
add_foreign_key "maps", "maps", column: "source_id"
add_foreign_key "tokens", "users"
end

View file

@ -1,6 +1,6 @@
#type: collection
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 ]
queryParameters:
user_id:
@ -23,6 +23,8 @@ post:
description: description
permission:
description: commons, public, or private
source_id:
description: the id of the map this map is a fork of
screenshot:
description: url to a screenshot of the map
contributor_ids:
@ -37,7 +39,7 @@ post:
/{id}:
#type: item
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 ]
responses:
200:
@ -60,6 +62,9 @@ post:
screenshot:
description: url to a screenshot of the map
required: false
source_id:
description: the id of the map this map is a fork of
required: false
responses:
200:
body:
@ -81,6 +86,9 @@ post:
screenshot:
description: url to a screenshot of the map
required: false
source_id:
description: the id of the map this map is a fork of
required: false
responses:
200:
body:

View file

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

View file

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

View file

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

View file

@ -33,6 +33,12 @@
"user": {
"$ref": "_user.json"
},
"source_id": {
"$ref": "_optid.json"
},
"source": {
"$ref": "_map.json"
},
"topic_ids": {
"type": "array",
"items": {
@ -111,6 +117,12 @@
{ "required": [ "user" ] }
]
},
{
"oneOf": [
{ "required": [ "source_id" ] },
{ "required": [ "source" ] }
]
},
{
"oneOf": [
{ "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:
Put the following code into `/etc/init/metamaps_delayed_worker.conf`:
If your system uses upstart for init scripts, put the following code into `/etc/init/metamaps_delayed_job.conf`:
description "Delayed Jobs Worker for Metamaps"
@ -128,3 +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
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 outdent from 'outdent'
@ -7,7 +5,6 @@ import Active from './Active'
import DataModel from './DataModel'
import Filter from './Filter'
import GlobalUI from './GlobalUI'
import JIT from './JIT'
import Mouse from './Mouse'
import Selected from './Selected'
import Settings from './Settings'
@ -98,13 +95,9 @@ const Control = {
var permToDelete = Active.Mapper.id === topic.get('user_id') || Active.Mapper.get('admin')
if (permToDelete) {
var mappableid = topic.id
var mapping = node.getData('mapping')
topic.destroy()
DataModel.Mappings.remove(mapping)
$(document).trigger(JIT.events.deleteTopic, [{
mappableid: mappableid
}])
Control.hideNode(nodeid)
} else {
GlobalUI.notifyUser('Only topics you created can be deleted')
@ -151,13 +144,9 @@ const Control = {
}
var topic = node.getData('topic')
var mappableid = topic.id
var mapping = node.getData('mapping')
mapping.destroy()
DataModel.Topics.remove(topic)
$(document).trigger(JIT.events.removeTopic, [{
mappableid: mappableid
}])
Control.hideNode(nodeid)
},
hideSelectedNodes: function() {
@ -273,7 +262,6 @@ const Control = {
if (edge.getData('synapses').length - 1 === 0) {
Control.hideEdge(edge)
}
var mappableid = synapse.id
synapse.destroy()
// the server will destroy the mapping, we just need to remove it here
@ -283,9 +271,6 @@ const Control = {
if (edge.getData('displayIndex')) {
delete edge.data.$displayIndex
}
$(document).trigger(JIT.events.deleteSynapse, [{
mappableid: mappableid
}])
} else {
GlobalUI.notifyUser('Only synapses you created can be deleted')
}
@ -327,7 +312,6 @@ const Control = {
var synapse = edge.getData('synapses')[index]
var mapping = edge.getData('mappings')[index]
var mappableid = synapse.id
mapping.destroy()
DataModel.Synapses.remove(synapse)
@ -337,9 +321,6 @@ const Control = {
if (edge.getData('displayIndex')) {
delete edge.data.$displayIndex
}
$(document).trigger(JIT.events.removeSynapse, [{
mappableid: mappableid
}])
},
hideSelectedEdges: function() {
const l = Selected.Edges.length

View file

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

View file

@ -7,7 +7,6 @@ try { Backbone.$ = window.$ } catch (err) {}
import Active from '../Active'
import InfoBox from '../Map/InfoBox'
import Mapper from '../Mapper'
import Realtime from '../Realtime'
const Map = Backbone.Model.extend({
urlRoot: '/maps',
@ -15,32 +14,8 @@ const Map = Backbone.Model.extend({
toJSON: function(options) {
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() {
this.on('changeByOther', this.updateView)
this.on('saved', this.savedEvent)
},
savedEvent: function() {
Realtime.updateMap(this)
},
authorizeToEdit: function(mapper) {
if (mapper && (

View file

@ -1,5 +1,3 @@
/* global $ */
import _ from 'lodash'
import outdent from 'outdent'
import Backbone from 'backbone'
@ -7,8 +5,6 @@ try { Backbone.$ = window.$ } catch (err) {}
import Active from '../Active'
import Filter from '../Filter'
import JIT from '../JIT'
import Realtime from '../Realtime'
import SynapseCard from '../SynapseCard'
import Visualize from '../Visualize'
@ -20,34 +16,6 @@ const Synapse = Backbone.Model.extend({
toJSON: function(options) {
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() {
if (this.isNew()) {
this.set({
@ -56,24 +24,8 @@ const Synapse = Backbone.Model.extend({
'category': 'from-to'
})
}
this.on('changeByOther', this.updateCardView)
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)
},
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
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) {
if (mapper && this.get('user_id') === mapper.get('id')) return true
else return false
@ -149,9 +105,6 @@ const Synapse = Backbone.Model.extend({
return edge
},
savedEvent: function() {
Realtime.updateSynapse(this)
},
updateViews: function() {
this.updateCardView()
this.updateEdgeView()

View file

@ -1,13 +1,9 @@
/* global $ */
import _ from 'lodash'
import Backbone from 'backbone'
try { Backbone.$ = window.$ } catch (err) {}
import Active from '../Active'
import Filter from '../Filter'
import JIT from '../JIT'
import Realtime from '../Realtime'
import TopicCard from '../TopicCard'
import Visualize from '../Visualize'
@ -19,34 +15,6 @@ const Topic = Backbone.Model.extend({
toJSON: function(options) {
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() {
if (this.isNew()) {
this.set({
@ -59,23 +27,6 @@ const Topic = Backbone.Model.extend({
this.on('changeByOther', this.updateCardView)
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)
},
authorizeToEdit: function(mapper) {
@ -88,6 +39,10 @@ const Topic = Backbone.Model.extend({
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) {
if (mapper && this.get('user_id') === mapper.get('id')) return true
else return false
@ -135,9 +90,6 @@ const Topic = Backbone.Model.extend({
return node
},
savedEvent: function() {
Realtime.updateTopic(this)
},
updateViews: function() {
var onPageWithTopicCard = Active.Map || Active.Topic
var node = this.get('node')

View file

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

View file

@ -36,12 +36,6 @@ const JIT = {
events: {
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',
zoom: 'Metamaps:JIT:events:zoom',
animationDone: 'Metamaps:JIT:events:animationDone'

View file

@ -300,7 +300,7 @@ const InfoBox = {
string += '</ul>'
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
},
@ -391,7 +391,7 @@ const InfoBox = {
DataModel.Maps.Shared.remove(map)
map.destroy()
Router.home()
GlobalUI.notifyUser('Map eliminated!')
GlobalUI.notifyUser('Map eliminated')
} 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?")
}

View file

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

View file

@ -12,19 +12,9 @@ module.exports = {
LEAVE_CALL: 'LEAVE_CALL',
SEND_MAPPER_INFO: 'SEND_MAPPER_INFO',
SEND_COORDS: 'SEND_COORDS',
CREATE_MESSAGE: 'CREATE_MESSAGE',
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',
INVITED_TO_CALL: 'INVITED_TO_CALL',
INVITED_TO_JOIN: 'INVITED_TO_JOIN',
@ -38,16 +28,6 @@ module.exports = {
MAPPER_LIST_UPDATED: 'MAPPER_LIST_UPDATED',
NEW_MAPPER: 'NEW_MAPPER',
LOST_MAPPER: 'LOST_MAPPER',
MESSAGE_CREATED: 'MESSAGE_CREATED',
TOPIC_DRAGGED: 'TOPIC_DRAGGED',
TOPIC_CREATED: 'TOPIC_CREATED',
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'
PEER_COORDS_UPDATED: 'PEER_COORDS_UPDATED'
}

View file

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

View file

@ -4,18 +4,12 @@
everthing in this file happens as a result of websocket events
*/
import { indexOf } from 'lodash'
import { JUNTO_UPDATED } from './events'
import Active from '../Active'
import { ChatView } from '../Views'
import DataModel from '../DataModel'
import GlobalUI from '../GlobalUI'
import Control from '../Control'
import Map from '../Map'
import Mapper from '../Mapper'
import Topic from '../Topic'
import Synapse from '../Synapse'
import Util from '../Util'
import Visualize from '../Visualize'
@ -24,188 +18,8 @@ export const juntoUpdated = self => state => {
$(document).trigger(JUNTO_UPDATED)
}
export const synapseRemoved = self => data => {
var synapse = DataModel.Synapses.get(data.mappableid)
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')
}
})
}
}
/* All the following events are received through the nodejs realtime server
and are done this way because they are transient data, not persisted to the server */
export const topicDragged = self => positions => {
var topic
var node
@ -230,10 +44,10 @@ export const lostMapper = self => data => {
// data.userid
// data.username
delete self.mappersOnMap[data.userid]
self.room.chat.sound.play('leavemap')
ChatView.sound.play('leavemap')
// $('#mapper' + 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')
@ -262,8 +76,8 @@ export const mapperListUpdated = self => data => {
}
if (data.userid !== Active.Mapper.id) {
self.room.chat.addParticipant(self.mappersOnMap[data.userid])
if (data.userinconversation) self.room.chat.mapperJoinedCall(data.userid)
ChatView.addParticipant(self.mappersOnMap[data.userid])
if (data.userinconversation) ChatView.mapperJoinedCall(data.userid)
// create a div for the collaborators compass
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
if (data.userid !== Active.Mapper.id) {
self.room.chat.sound.play('joinmap')
self.room.chat.addParticipant(self.mappersOnMap[data.userid])
ChatView.sound.play('joinmap')
ChatView.addParticipant(self.mappersOnMap[data.userid])
// create a div for the collaborators compass
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
GlobalUI.notifyUser('Conversation starting...')
self.joinCall()
self.room.chat.invitationAnswered(userid)
ChatView.invitationAnswered(userid)
}
export const callDenied = self => userid => {
var username = self.mappersOnMap[userid].name
GlobalUI.notifyUser(username + " didn't accept your invitation")
self.room.chat.invitationAnswered(userid)
ChatView.invitationAnswered(userid)
}
export const inviteDenied = self => userid => {
var username = self.mappersOnMap[userid].name
GlobalUI.notifyUser(username + " didn't accept your invitation")
self.room.chat.invitationAnswered(userid)
ChatView.invitationAnswered(userid)
}
export const invitedToCall = self => inviter => {
self.room.chat.sound.stop(self.soundId)
self.soundId = self.room.chat.sound.play('sessioninvite')
ChatView.sound.stop(self.soundId)
self.soundId = ChatView.sound.play('sessioninvite')
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;" />'
@ -341,8 +155,8 @@ export const invitedToCall = self => inviter => {
}
export const invitedToJoin = self => inviter => {
self.room.chat.sound.stop(self.soundId)
self.soundId = self.room.chat.sound.play('sessioninvite')
ChatView.sound.stop(self.soundId)
self.soundId = ChatView.sound.play('sessioninvite')
var username = self.mappersOnMap[inviter].name
var notifyText = username + ' is inviting you to the conversation. Join?'
@ -355,16 +169,14 @@ export const invitedToJoin = self => inviter => {
export const mapperJoinedCall = self => id => {
var mapper = self.mappersOnMap[id]
if (mapper) {
if (self.inConversation) {
var username = mapper.name
var notifyText = username + ' joined the call'
GlobalUI.notifyUser(notifyText)
}
mapper.inConversation = true
self.room.chat.mapperJoinedCall(id)
ChatView.mapperJoinedCall(id)
}
}
@ -377,7 +189,7 @@ export const mapperLeftCall = self => id => {
GlobalUI.notifyUser(notifyText)
}
mapper.inConversation = false
self.room.chat.mapperLeftCall(id)
ChatView.mapperLeftCall(id)
if ((self.inConversation && self.countOthersInConversation() === 0) ||
(!self.inConversation && self.countOthersInConversation() === 1)) {
self.callEnded()
@ -392,8 +204,7 @@ export const callInProgress = self => () => {
GlobalUI.notifyUser(notifyText, true)
$('#toast button.yes').click(e => self.joinCall())
$('#toast button.no').click(e => GlobalUI.clearNotify())
self.room.conversationInProgress()
ChatView.conversationInProgress()
}
export const callStarted = self => () => {
@ -404,7 +215,6 @@ export const callStarted = self => () => {
GlobalUI.notifyUser(notifyText, true)
$('#toast button.yes').click(e => self.joinCall())
$('#toast button.no').click(e => GlobalUI.clearNotify())
self.room.conversationInProgress()
ChatView.conversationInProgress()
}

View file

@ -1,6 +1,7 @@
/* global $ */
import Active from '../Active'
import { ChatView } from '../Views'
import GlobalUI from '../GlobalUI'
import {
@ -16,17 +17,7 @@ import {
LEAVE_CALL,
SEND_MAPPER_INFO,
SEND_COORDS,
CREATE_MESSAGE,
DRAG_TOPIC,
CREATE_TOPIC,
UPDATE_TOPIC,
REMOVE_TOPIC,
DELETE_TOPIC,
CREATE_SYNAPSE,
UPDATE_SYNAPSE,
REMOVE_SYNAPSE,
DELETE_SYNAPSE,
UPDATE_MAP
DRAG_TOPIC
} from './events'
export const joinMap = self => () => {
@ -72,6 +63,7 @@ export const joinCall = self => () => {
$('#wrapper').append(self.localVideo.view.$container)
}
self.room.join()
ChatView.conversationInProgress(true)
})
self.inConversation = true
self.socket.emit(JOIN_CALL, {
@ -80,7 +72,7 @@ export const joinCall = self => () => {
})
self.webrtc.startLocalVideo()
GlobalUI.clearNotify()
self.room.chat.mapperJoinedCall(Active.Mapper.id)
ChatView.mapperJoinedCall(Active.Mapper.id)
}
export const leaveCall = self => () => {
@ -89,7 +81,8 @@ export const leaveCall = self => () => {
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.inConversation = false
self.localVideo.view.$container.hide()
@ -102,7 +95,7 @@ export const leaveCall = self => () => {
}
export const acceptCall = self => userid => {
self.room.chat.sound.stop(self.soundId)
ChatView.sound.stop(self.soundId)
self.socket.emit(ACCEPT_CALL, {
mapid: Active.Map.id,
invited: Active.Mapper.id,
@ -114,7 +107,7 @@ export const acceptCall = self => userid => {
}
export const denyCall = self => userid => {
self.room.chat.sound.stop(self.soundId)
ChatView.sound.stop(self.soundId)
self.socket.emit(DENY_CALL, {
mapid: Active.Map.id,
invited: Active.Mapper.id,
@ -124,7 +117,7 @@ export const denyCall = self => userid => {
}
export const denyInvite = self => userid => {
self.room.chat.sound.stop(self.soundId)
ChatView.sound.stop(self.soundId)
self.socket.emit(DENY_INVITE, {
mapid: Active.Map.id,
invited: Active.Mapper.id,
@ -139,7 +132,7 @@ export const inviteACall = self => userid => {
inviter: Active.Mapper.id,
invited: userid
})
self.room.chat.invitationPending(userid)
ChatView.invitationPending(userid)
GlobalUI.clearNotify()
}
@ -149,13 +142,13 @@ export const inviteToJoin = self => userid => {
inviter: Active.Mapper.id,
invited: userid
})
self.room.chat.invitationPending(userid)
ChatView.invitationPending(userid)
}
export const sendCoords = self => coords => {
var map = Active.Map
var mapper = Active.Mapper
if (map.authorizeToEdit(mapper)) {
if (map && map.authorizeToEdit(mapper)) {
var update = {
usercoords: coords,
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 Create from './Create'
import DataModel from './DataModel'
import JIT from './JIT'
import Map from './Map'
import Selected from './Selected'
import Settings from './Settings'
@ -40,18 +39,12 @@ const Synapse = {
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) {
if (Active.Map) {
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) {
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 newTopicData = {
mappingid: mappingModel.id,
mappableid: mappingModel.get('mappable_id')
}
$(document).trigger(JIT.events.newTopic, [newTopicData])
// call a success callback if provided
if (opts.success) {
opts.success(topicModel)

View file

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

View file

@ -2,128 +2,27 @@
import Backbone from 'backbone'
import { Howl } from 'howler'
import Autolinker from 'autolinker'
import { clone, template as lodashTemplate } from 'lodash'
import outdent from 'outdent'
import React from 'react'
import ReactDOM from 'react-dom'
// TODO is this line good or bad
// 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 = {
messageHTML: outdent`
<div class='chat-message'>
<div class='chat-message-user'><img src='{{ user_image }}' title='{{user_name }}'/></div>
<div class='chat-message-text'>{{ message }}</div>
<div class='chat-message-time'>{{ timestamp }}</div>
<div class='clearfloat'></div>
</div>`,
participantHTML: outdent`
<div class='participant participant-{{ id }} {{ selfClass }}'>
<div class='chat-participant-image'>
<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,
const ChatView = {
isOpen: false,
messages: new Backbone.Collection(),
conversationLive: false,
isParticipating: false,
mapChat: null,
domId: 'chat-box-wrapper',
init: function(urls) {
const self = ChatView
self.sound = new Howl({
src: urls,
sprite: {
joinmap: [0, 561],
leavemap: [1000, 592],
@ -133,226 +32,170 @@ var Private = {
}
})
},
incrementUnread: function() {
this.unreadMessages++
this.$unread.html(this.unreadMessages)
this.$unread.show()
setNewMap: function() {
const self = ChatView
self.conversationLive = false
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) {
if (!this.isOpen && !isInitial) Private.incrementUnread.call(this)
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')
show: () => {
$('#' + ChatView.domId).show()
},
initialMessages: function() {
var messages = this.messages.models
for (var i = 0; i < messages.length; i++) {
Private.addMessage.call(this, messages[i], true)
}
hide: () => {
$('#' + ChatView.domId).hide()
},
handleInputMessage: function() {
var message = {
message: this.$messageInput.val()
}
this.$messageInput.val('')
$(document).trigger(ChatView.events.message + '-' + this.room, [message])
render: () => {
if (!Active.Map) return
const self = ChatView
self.mapChat = ReactDOM.render(React.createElement(MapChat, {
conversationLive: self.conversationLive,
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) {
var p = clone(participant.attributes)
if (p.self) {
p.selfClass = 'is-self'
p.selfName = '(me)'
} else {
p.selfClass = ''
p.selfName = ''
}
var html = this.participantTemplate(p)
this.$participants.append(html)
onOpen: () => {
$(document).trigger(ChatView.events.openTray)
},
removeParticipant: function(participant) {
this.$container.find('.participant-' + participant.get('id')).remove()
}
}
var Handlers = {
buttonClick: function() {
if (this.isOpen) this.close()
else if (!this.isOpen) this.open()
onClose: () => {
$(document).trigger(ChatView.events.closeTray)
},
addParticipant: participant => {
ChatView.participants.add(participant)
ChatView.render()
},
removeParticipant: participant => {
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() {
this.$videoToggle.toggleClass('active')
this.videosShowing = !this.videosShowing
$(document).trigger(this.videosShowing ? ChatView.events.videosOn : ChatView.events.videosOff)
ChatView.videosShowing = !ChatView.videosShowing
$(document).trigger(ChatView.videosShowing ? ChatView.events.videosOn : ChatView.events.videosOff)
},
cursorToggleClick: function() {
this.$cursorToggle.toggleClass('active')
this.cursorsShowing = !this.cursorsShowing
$(document).trigger(this.cursorsShowing ? ChatView.events.cursorsOn : ChatView.events.cursorsOff)
ChatView.cursorsShowing = !ChatView.cursorsShowing
$(document).trigger(ChatView.cursorsShowing ? ChatView.events.cursorsOn : ChatView.events.cursorsOff)
},
soundToggleClick: function() {
this.alertSound = !this.alertSound
this.$soundToggle.toggleClass('active')
ChatView.alertSound = !ChatView.alertSound
},
keyUp: function(event) {
switch (event.which) {
case 13: // enter
Private.handleInputMessage.call(this)
break
}
},
inputFocus: function() {
inputFocus: () => {
$(document).trigger(ChatView.events.inputFocus)
},
inputBlur: function() {
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 = {}) {
this.room = room
this.mapper = mapper
this.messages = messages // backbone collection
// ChatView.prototype.scrollMessages = function(duration) {
// duration = duration || 0
this.isOpen = false
this.alertSound = true // whether to play sounds on arrival of new messages or not
this.cursorsShowing = true
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()
}
// this.$messages.animate({
// scrollTop: this.$messages[0].scrollHeight
// }, duration)
// }
/**
* @class
* @static
*/
ChatView.events = {
message: 'ChatView:message',
openTray: 'ChatView:openTray',
closeTray: 'ChatView:closeTray',
inputFocus: 'ChatView:inputFocus',

View file

@ -1,16 +1,8 @@
/* global $ */
import Backbone from 'backbone'
import attachMediaStream from 'attachmediastream'
// TODO is this line good or bad
// Backbone.$ = window.$
import Active from '../Active'
import DataModel from '../DataModel'
import Realtime from '../Realtime'
import ChatView from './ChatView'
import VideoView from './VideoView'
const Room = function(opts = {}) {
@ -19,38 +11,18 @@ const Room = function(opts = {}) {
this.webrtc = opts.webrtc
this.room = opts.room
this.config = opts.config
this.peopleCount = 0
this.$myVideo = opts.$video
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.init()
}
Room.prototype.join = function(cb) {
this.isActiveRoom = true
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() {
this.chat.leaveConversation() // the conversation will carry on without you
for (var id in this.videos) {
this.removeVideo(id)
}
@ -66,14 +38,6 @@ Room.prototype.leave = function() {
this.isActiveRoom = false
this.webrtc.leaveRoom()
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() {
@ -129,11 +93,6 @@ Room.prototype.init = function() {
}
v.$container.show()
})
var sendChatMessage = function(event, data) {
self.sendChatMessage(data)
}
$(document).on(ChatView.events.message + '-' + this.room, sendChatMessage)
}
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

View file

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

View file

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

View file

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

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')}
data-router="true"
text="All Maps"
/>
/>
<MapLink show={signedIn && explore}
href="/explore/mine"
linkClass={activeClass('my')}

View file

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

View file

@ -5,7 +5,9 @@
"scripts": {
"build": "webpack",
"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": {
"type": "git",
@ -27,9 +29,10 @@
"babel-preset-es2015": "6.18.0",
"babel-preset-react": "6.16.0",
"backbone": "1.0.0",
"clipboard-js": "git://github.com/devvmh/clipboard.js#patch-1",
"clipboard-js": "0.3.2",
"commonmark": "0.27.0",
"csv-parse": "1.1.7",
"emoji-mart": "^0.3.5",
"getScreenMedia": "git://github.com/devvmh/getScreenMedia#patch-1",
"hark": "git://github.com/devvmh/hark#patch-1",
"howler": "2.0.2",

View file

@ -1,22 +1,12 @@
const {
// server sendable, client receivable
TOPIC_UPDATED,
TOPIC_DELETED,
SYNAPSE_UPDATED,
SYNAPSE_DELETED,
MAP_UPDATED,
JUNTO_UPDATED,
// server receivable, client sendable
JOIN_CALL,
LEAVE_CALL,
JOIN_MAP,
LEAVE_MAP,
UPDATE_TOPIC,
DELETE_TOPIC,
UPDATE_SYNAPSE,
DELETE_SYNAPSE,
UPDATE_MAP
LEAVE_MAP
} = require('../frontend/src/Metamaps/Realtime/events')
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(LEAVE_CALL, () => store.dispatch({ type: LEAVE_CALL, 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,
NEW_MAPPER,
LOST_MAPPER,
MESSAGE_CREATED,
TOPIC_DRAGGED,
TOPIC_CREATED,
TOPIC_REMOVED,
SYNAPSE_CREATED,
SYNAPSE_REMOVED,
PEER_COORDS_UPDATED,
JOIN_MAP,
LEAVE_MAP,
SEND_COORDS,
SEND_MAPPER_INFO,
CREATE_MESSAGE,
DRAG_TOPIC,
CREATE_TOPIC,
REMOVE_TOPIC,
CREATE_SYNAPSE,
REMOVE_SYNAPSE
} = require('../frontend/src/Metamaps/Realtime/events')
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.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) {
var mapId = data.mapid
delete data.mapid
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)
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
let(:user) { create(:user, admin: true) }
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
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 match_json_schema(:map)
expect(Map.count).to eq 2
expect(Map.count).to eq 3
end
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 }
expect(response).to have_http_status(:no_content)
expect(Map.count).to eq 0
expect(Map.count).to eq 1
end
it 'POST /api/v2/maps/:id/stars' do

View file

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