Merge branch 'develop' (v3.2)
This commit is contained in:
commit
2c60d7335c
75 changed files with 1759 additions and 1528 deletions
8
.github/ISSUE_TEMPLATE.md
vendored
8
.github/ISSUE_TEMPLATE.md
vendored
|
@ -1,4 +1,12 @@
|
|||
please link to related trello cards, if they exist, from the following two boards respectively
|
||||
|
||||
https://trello.com/b/8HlCikOX/metamaps-design
|
||||
|
||||
https://trello.com/b/uFOA6a2x/metamaps-feedback-feature-ideas-requests
|
||||
|
||||
[the issue as framed for design]()
|
||||
|
||||
[the issue as framed from the users perspective]()
|
||||
|
||||
|
||||
============
|
||||
|
|
1
Gemfile
1
Gemfile
|
@ -27,6 +27,7 @@ gem 'rack-cors'
|
|||
gem 'redis'
|
||||
gem 'slack-notifier'
|
||||
gem 'snorlax'
|
||||
gem 'puma'
|
||||
|
||||
# asset stuff
|
||||
gem 'jquery-rails'
|
||||
|
|
|
@ -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
|
||||
|
|
2
Procfile
2
Procfile
|
@ -1,3 +1,3 @@
|
|||
web: bundle exec rails server -p $PORT
|
||||
web: bundle exec puma -p $PORT
|
||||
worker: bundle exec rake jobs:work
|
||||
|
||||
|
|
11
README.md
11
README.md
|
@ -2,6 +2,7 @@ Metamaps
|
|||
=======
|
||||
|
||||
[![Build Status](https://travis-ci.org/metamaps/metamaps.svg?branch=develop)](https://travis-ci.org/metamaps/metamaps)
|
||||
[![Code Climate](https://codeclimate.com/github/metamaps/metamaps/badges/gpa.svg)](https://codeclimate.com/github/metamaps/metamaps)
|
||||
|
||||
## What is Metamaps?
|
||||
|
||||
|
@ -16,8 +17,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
|
||||
|
|
|
@ -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
|
||||
|
|
263
app/assets/stylesheets/emoji-mart-0.3.5.css
Normal file
263
app/assets/stylesheets/emoji-mart-0.3.5.css
Normal file
|
@ -0,0 +1,263 @@
|
|||
.emoji-mart,
|
||||
.emoji-mart * {
|
||||
box-sizing: border-box;
|
||||
line-height: 1.15;
|
||||
}
|
||||
|
||||
.emoji-mart {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Helvetica Neue", sans-serif;
|
||||
font-size: 16px;
|
||||
display: inline-block;
|
||||
color: #222427;
|
||||
border: 1px solid #d9d9d9;
|
||||
border-radius: 5px;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.emoji-mart .emoji-mart-emoji {
|
||||
padding: 6px;
|
||||
}
|
||||
|
||||
.emoji-mart-bar:first-child {
|
||||
border-top-left-radius: 5px;
|
||||
border-top-right-radius: 5px;
|
||||
}
|
||||
.emoji-mart-bar:last-child {
|
||||
border-bottom-left-radius: 5px;
|
||||
border-bottom-right-radius: 5px;
|
||||
}
|
||||
|
||||
.emoji-mart-anchors {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 0 6px;
|
||||
color: #858585;
|
||||
line-height: 0;
|
||||
}
|
||||
|
||||
.emoji-mart-anchor {
|
||||
position: relative;
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
padding: 12px 4px;
|
||||
overflow: hidden;
|
||||
transition: color .1s ease-out;
|
||||
}
|
||||
.emoji-mart-anchor:hover,
|
||||
.emoji-mart-anchor-selected {
|
||||
color: #464646;
|
||||
}
|
||||
|
||||
.emoji-mart-anchor-selected .emoji-mart-anchor-bar {
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
.emoji-mart-anchor-bar {
|
||||
position: absolute;
|
||||
bottom: -3px; left: 0;
|
||||
width: 100%; height: 3px;
|
||||
background-color: #464646;
|
||||
}
|
||||
|
||||
.emoji-mart-anchors i {
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
max-width: 22px;
|
||||
}
|
||||
|
||||
.emoji-mart-anchors svg {
|
||||
fill: currentColor;
|
||||
}
|
||||
|
||||
.emoji-mart-scroll {
|
||||
overflow-y: scroll;
|
||||
height: 270px;
|
||||
padding: 0 6px 6px 6px;
|
||||
border: solid #d9d9d9;
|
||||
border-width: 1px 0;
|
||||
}
|
||||
|
||||
.emoji-mart-search {
|
||||
font-size: 16px;
|
||||
display: block;
|
||||
width: 100%;
|
||||
padding: .2em .6em;
|
||||
margin-top: 6px;
|
||||
border-radius: 25px;
|
||||
border: 1px solid #d9d9d9;
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
.emoji-mart-category .emoji-mart-emoji span {
|
||||
z-index: 1;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.emoji-mart-category .emoji-mart-emoji:hover:before {
|
||||
z-index: 0;
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0; left: 0;
|
||||
width: 100%; height: 100%;
|
||||
background-color: #f4f4f4;
|
||||
border-radius: 100%;
|
||||
}
|
||||
|
||||
.emoji-mart-category-label {
|
||||
z-index: 2;
|
||||
position: relative;
|
||||
position: -webkit-sticky;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.emoji-mart-category-label span {
|
||||
display: block;
|
||||
width: 100%;
|
||||
font-weight: 500;
|
||||
padding: 5px 6px;
|
||||
background-color: #fff;
|
||||
background-color: rgba(255, 255, 255, .95);
|
||||
}
|
||||
|
||||
.emoji-mart-emoji {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
font-size: 0;
|
||||
}
|
||||
|
||||
.emoji-mart-no-results {
|
||||
font-size: 14px;
|
||||
text-align: center;
|
||||
padding-top: 70px;
|
||||
color: #858585;
|
||||
}
|
||||
.emoji-mart-no-results span {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.emoji-mart-preview {
|
||||
position: relative;
|
||||
height: 70px;
|
||||
}
|
||||
|
||||
.emoji-mart-preview-emoji,
|
||||
.emoji-mart-preview-data,
|
||||
.emoji-mart-preview-skins {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
.emoji-mart-preview-emoji {
|
||||
left: 12px;
|
||||
}
|
||||
|
||||
.emoji-mart-preview-data {
|
||||
left: 68px; right: 12px;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.emoji-mart-preview-skins {
|
||||
right: 30px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.emoji-mart-preview-name {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.emoji-mart-preview-shortname {
|
||||
font-size: 12px;
|
||||
color: #888;
|
||||
}
|
||||
.emoji-mart-preview-shortname + .emoji-mart-preview-shortname,
|
||||
.emoji-mart-preview-shortname + .emoji-mart-preview-emoticon,
|
||||
.emoji-mart-preview-emoticon + .emoji-mart-preview-emoticon {
|
||||
margin-left: .5em;
|
||||
}
|
||||
|
||||
.emoji-mart-preview-emoticon {
|
||||
font-size: 11px;
|
||||
color: #bbb;
|
||||
}
|
||||
|
||||
.emoji-mart-title span {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.emoji-mart-title .emoji-mart-emoji {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.emoji-mart-title-label {
|
||||
color: #999A9C;
|
||||
font-size: 26px;
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
.emoji-mart-skin-swatches {
|
||||
font-size: 0;
|
||||
padding: 2px 0;
|
||||
border: 1px solid #d9d9d9;
|
||||
border-radius: 12px;
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
.emoji-mart-skin-swatches-opened .emoji-mart-skin-swatch {
|
||||
width: 16px;
|
||||
padding: 0 2px;
|
||||
}
|
||||
|
||||
.emoji-mart-skin-swatches-opened .emoji-mart-skin-swatch-selected:after {
|
||||
opacity: .75;
|
||||
}
|
||||
|
||||
.emoji-mart-skin-swatch {
|
||||
display: inline-block;
|
||||
width: 0;
|
||||
vertical-align: middle;
|
||||
transition-property: width, padding;
|
||||
transition-duration: .125s;
|
||||
transition-timing-function: ease-out;
|
||||
}
|
||||
|
||||
.emoji-mart-skin-swatch:nth-child(1) { transition-delay: 0 }
|
||||
.emoji-mart-skin-swatch:nth-child(2) { transition-delay: .03s }
|
||||
.emoji-mart-skin-swatch:nth-child(3) { transition-delay: .06s }
|
||||
.emoji-mart-skin-swatch:nth-child(4) { transition-delay: .09s }
|
||||
.emoji-mart-skin-swatch:nth-child(5) { transition-delay: .12s }
|
||||
.emoji-mart-skin-swatch:nth-child(6) { transition-delay: .15s }
|
||||
|
||||
.emoji-mart-skin-swatch-selected {
|
||||
position: relative;
|
||||
width: 16px;
|
||||
padding: 0 2px;
|
||||
}
|
||||
.emoji-mart-skin-swatch-selected:after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 50%; left: 50%;
|
||||
width: 4px; height: 4px;
|
||||
margin: -2px 0 0 -2px;
|
||||
background-color: #fff;
|
||||
border-radius: 100%;
|
||||
pointer-events: none;
|
||||
opacity: 0;
|
||||
transition: opacity .2s ease-out;
|
||||
}
|
||||
|
||||
.emoji-mart-skin {
|
||||
display: inline-block;
|
||||
width: 100%; padding-top: 100%;
|
||||
max-width: 12px;
|
||||
border-radius: 100%;
|
||||
}
|
||||
|
||||
.emoji-mart-skin-tone-1 { background-color: #ffc93a }
|
||||
.emoji-mart-skin-tone-2 { background-color: #fadcbc }
|
||||
.emoji-mart-skin-tone-3 { background-color: #e0bb95 }
|
||||
.emoji-mart-skin-tone-4 { background-color: #bf8f68 }
|
||||
.emoji-mart-skin-tone-5 { background-color: #9b643d }
|
||||
.emoji-mart-skin-tone-6 { background-color: #594539 }
|
|
@ -1,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;
|
||||
}
|
375
app/assets/stylesheets/junto.scss.erb
Normal file
375
app/assets/stylesheets/junto.scss.erb
Normal file
|
@ -0,0 +1,375 @@
|
|||
.collaborator-video {
|
||||
z-index: 1;
|
||||
position: absolute;
|
||||
width: 150px;
|
||||
height: 150px;
|
||||
cursor: default;
|
||||
color: #FFF;
|
||||
|
||||
.video-receive {
|
||||
position: absolute;
|
||||
width: 160px;
|
||||
padding: 20px 20px 20px 170px;
|
||||
background: #424242;
|
||||
height: 110px;
|
||||
border-top-left-radius: 75px;
|
||||
border-bottom-left-radius: 75px;
|
||||
border-top-right-radius: 2px;
|
||||
border-bottom-right-radius: 2px;
|
||||
|
||||
.video-statement {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.btn-group {
|
||||
.btn-yes {
|
||||
margin-right: 10px;
|
||||
}
|
||||
.btn-no {
|
||||
background-color: #c04f4f;
|
||||
&:hover {
|
||||
background-color: #A54242;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.video-cutoff {
|
||||
width: 150px;
|
||||
height: 150px;
|
||||
overflow: hidden;
|
||||
border-radius: 75px;
|
||||
z-index: 0;
|
||||
position: relative;
|
||||
-webkit-box-shadow: 0px 6px 3px rgba(0, 0, 0, 0.23), 10px 10px 10px rgba(0, 0, 0, 0.19);
|
||||
-moz-box-shadow: 0px 6px 3px rgba(0, 0, 0, 0.23), 10px 10px 10px rgba(0, 0, 0, 0.19);
|
||||
box-shadow: 0px 6px 3px rgba(0, 0, 0, 0.23), 10px 10px 10px rgba(0, 0, 0, 0.19);
|
||||
|
||||
video {
|
||||
height: 150px;
|
||||
margin-left: -25px;
|
||||
}
|
||||
.collaborator-video-avatar {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
-webkit-user-select: none;
|
||||
-khtml-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-o-user-select: none;
|
||||
user-select: none;
|
||||
-webkit-user-drag: none;
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
.video-audio {
|
||||
position: absolute;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
top: 85%;
|
||||
right: 0px;
|
||||
cursor: pointer;
|
||||
background: url(<%= asset_path 'audio_sprite.png' %>) no-repeat;
|
||||
}
|
||||
.video-audio:hover {
|
||||
background-position-x: -24px;
|
||||
}
|
||||
.video-audio.active {
|
||||
background-position-y: -24px;
|
||||
}
|
||||
.video-video {
|
||||
position: absolute;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
top: 85%;
|
||||
left: 0px;
|
||||
cursor: pointer;
|
||||
background: url(<%= asset_path 'camera_sprite.png' %>) no-repeat;
|
||||
}
|
||||
.video-video:hover {
|
||||
background-position-x: -24px;
|
||||
}
|
||||
.video-video.active {
|
||||
background-position-y: -24px;
|
||||
}
|
||||
}
|
||||
|
||||
.collaborator-video.my-video {
|
||||
left: 30px;
|
||||
top: 72px;
|
||||
}
|
||||
|
||||
#chat-box-wrapper {
|
||||
height: 100%;
|
||||
float: right;
|
||||
}
|
||||
|
||||
.chat-box {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
z-index: 1;
|
||||
width: 300px;
|
||||
height: 100%;
|
||||
background: #424242;
|
||||
box-shadow: -8px 0px 16px 2px rgba(0, 0, 0, 0.23);
|
||||
|
||||
.chat-button {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: -36px;
|
||||
width: 36px;
|
||||
height: 49px;
|
||||
background: url(<%= asset_path 'junto.png' %>) no-repeat 2px 9px, url(<%= asset_path 'tray_tab.png' %>) no-repeat;
|
||||
cursor: pointer;
|
||||
|
||||
&.active {
|
||||
background: url(<%= asset_path 'junto_spinner_dark.gif' %>) no-repeat 2px 8px, url(<%= asset_path 'tray_tab.png' %>) no-repeat !important;
|
||||
}
|
||||
.chat-unread {
|
||||
background: #DAB539;
|
||||
position: absolute;
|
||||
top: -3px;
|
||||
left: -11px;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 11px;
|
||||
border: 2px solid #424242;
|
||||
color: #424242;
|
||||
text-align: center;
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
line-height: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.junto-header {
|
||||
width: 276px;
|
||||
padding: 16px 8px 16px 16px;
|
||||
font-size: 16px;
|
||||
text-align: left;
|
||||
font-weight: bold;
|
||||
background-color: #000000;
|
||||
color: #f5f5f5;
|
||||
box-shadow: 0px 6px 3px rgba(0, 0, 0, 0.23);
|
||||
|
||||
.cursor-toggle {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
margin-right: 8px;
|
||||
margin-top: -8px;
|
||||
float: right;
|
||||
background: url(<%= asset_path 'cursor_sprite.png' %>) no-repeat;
|
||||
}
|
||||
.cursor-toggle:hover {
|
||||
background-position-x: -32px;
|
||||
}
|
||||
.cursor-toggle.active {
|
||||
background-position-y: -32px;
|
||||
}
|
||||
.video-toggle {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
margin-right: 10px;
|
||||
margin-top: -8px;
|
||||
float: right;
|
||||
background: url(<%= asset_path 'video_sprite.png' %>) no-repeat;
|
||||
}
|
||||
.video-toggle:hover {
|
||||
background-position-x: -32px;
|
||||
}
|
||||
.video-toggle.active {
|
||||
background-position-y: -32px;
|
||||
}
|
||||
}
|
||||
|
||||
.participants {
|
||||
width: 100%;
|
||||
min-height: 150px;
|
||||
padding: 16px 0px 16px 0px;
|
||||
text-align: left;
|
||||
color: #f5f5f5;
|
||||
overflow-y: auto;
|
||||
|
||||
.conversation-live {
|
||||
padding: 5px 10px 5px 10px;
|
||||
background: #c04f4f;
|
||||
margin: 5px 10px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
.conversation-live .call-action {
|
||||
float: right;
|
||||
cursor: pointer;
|
||||
color: #EBFF00;
|
||||
}
|
||||
.participant {
|
||||
width: 89%;
|
||||
padding: 8px 8px 2px 8px;
|
||||
color: #f5f5f5;
|
||||
font-family: arial, sans-serif;
|
||||
font-size: 13px;
|
||||
line-height: 14px;
|
||||
|
||||
.chat-participant-image {
|
||||
width: 15%;
|
||||
float: left;
|
||||
overflow: hidden;
|
||||
color: #BBB;
|
||||
padding-top: 2px;
|
||||
}
|
||||
.chat-participant-image img {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 18px;
|
||||
}
|
||||
.chat-participant-name {
|
||||
width: 53%;
|
||||
float: left;
|
||||
font-size: 13px;
|
||||
font-weight: bold;
|
||||
margin-top: 12px;
|
||||
padding: 2px 8px 0;
|
||||
text-align: left;
|
||||
}
|
||||
.chat-participant-invite-call,
|
||||
.chat-participant-invite-join
|
||||
{
|
||||
float: right;
|
||||
background: #4FC059 url(<%= asset_path 'invitepeer16.png' %>) no-repeat center center;
|
||||
}
|
||||
.chat-participant-invite-call.pending,
|
||||
.chat-participant-invite-join.pending {
|
||||
background: #dab539 url(<%= asset_path 'ellipsis.gif' %>) no-repeat center center;
|
||||
}
|
||||
.chat-participant-participating {
|
||||
float: right;
|
||||
margin-top: 14px;
|
||||
}
|
||||
.chat-participant-participating .green-dot {
|
||||
background: #4fc059;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 6px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.chat-header {
|
||||
width: 276px;
|
||||
padding: 16px 8px 16px 16px;
|
||||
font-size: 16px;
|
||||
text-align: left;
|
||||
font-weight: bold;
|
||||
background-color: #000000;
|
||||
color: #f5f5f5;
|
||||
box-shadow: 0px 6px 3px rgba(0, 0, 0, 0.23);
|
||||
|
||||
.sound-toggle {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
margin-right: 10px;
|
||||
margin-top: -2px;
|
||||
float: right;
|
||||
background: url(<%= asset_path 'sound_sprite.png' %>) no-repeat;
|
||||
}
|
||||
.sound-toggle:hover {
|
||||
background-position-x: -24px;
|
||||
}
|
||||
.sound-toggle.active {
|
||||
background-position-y: -24px;
|
||||
}
|
||||
}
|
||||
|
||||
$chat_font_size: 16px;
|
||||
|
||||
.chat-input {
|
||||
min-height: 80px;
|
||||
width: 88%;
|
||||
padding: 8px 9% 8px 3%;
|
||||
font-size: $chat_font_size;
|
||||
outline: none;
|
||||
resize: none;
|
||||
}
|
||||
|
||||
.chat-messages {
|
||||
width: 100%;
|
||||
padding: 16px 0px;
|
||||
overflow-y: auto;
|
||||
flex-grow: 1;
|
||||
|
||||
.chat-message {
|
||||
width: 89%;
|
||||
padding: 8px 8px 2px 8px;
|
||||
color: #f5f5f5;
|
||||
font-family: arial, sans-serif;
|
||||
font-size: $chat_font_size;
|
||||
line-height: $chat_font_size + 1px;
|
||||
|
||||
a:link {
|
||||
color: #4fb5c0;
|
||||
text-decoration: underline;
|
||||
}
|
||||
a:visited {
|
||||
color: #aea9fd;
|
||||
text-decoration: underline;
|
||||
}
|
||||
a:hover {
|
||||
color: #dab539;
|
||||
text-decoration: underline;
|
||||
}
|
||||
.chat-message-user {
|
||||
width: 12%;
|
||||
float: left;
|
||||
overflow: hidden;
|
||||
color: #BBB;
|
||||
padding-top: 2px;
|
||||
}
|
||||
.chat-message-user img {
|
||||
border: 2px solid #424242;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: 16px;
|
||||
}
|
||||
.chat-message-meta {
|
||||
padding: 0 8px;
|
||||
float: left;
|
||||
}
|
||||
.chat-message-username {
|
||||
color: #4fc059;
|
||||
}
|
||||
.chat-message-text {
|
||||
width: 80%;
|
||||
float: left;
|
||||
padding: 2px 8px 0;
|
||||
text-align: left;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
.chat-message-time {
|
||||
font-size: 12px;
|
||||
color: #757575;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.new-message-area {
|
||||
position: relative;
|
||||
|
||||
.emoji-mart {
|
||||
position: absolute;
|
||||
bottom: 98px;
|
||||
}
|
||||
|
||||
.extra-message-options {
|
||||
height: 20px;
|
||||
position: absolute;
|
||||
right: 2px;
|
||||
bottom: 74px;
|
||||
|
||||
.emoji-picker-button {
|
||||
font-size: 16px;
|
||||
line-height: 20px;
|
||||
cursor: pointer;
|
||||
padding: 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -211,6 +211,16 @@
|
|||
|
||||
span.creatorName {
|
||||
margin-left: 8px;
|
||||
max-width: 162px;
|
||||
display: inline-block;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.creatorAndPerm.cardHasViewOnly span.creatorName {
|
||||
max-width: 95px;
|
||||
}
|
||||
|
||||
.cardViewOnly {
|
||||
|
|
|
@ -93,7 +93,7 @@
|
|||
|
||||
.sidebarSearchField {
|
||||
float: left;
|
||||
width: 380px;
|
||||
width: 379px;
|
||||
padding: 7px 10px 3px 10px;
|
||||
height: 20px;
|
||||
border-top: 1px solid #BDBDBD;
|
||||
|
|
4
app/channels/application_cable/channel.rb
Normal file
4
app/channels/application_cable/channel.rb
Normal file
|
@ -0,0 +1,4 @@
|
|||
module ApplicationCable
|
||||
class Channel < ActionCable::Channel::Base
|
||||
end
|
||||
end
|
22
app/channels/application_cable/connection.rb
Normal file
22
app/channels/application_cable/connection.rb
Normal 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
|
8
app/channels/map_channel.rb
Normal file
8
app/channels/map_channel.rb
Normal 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
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -18,6 +18,7 @@ module Api
|
|||
def self.embeddable
|
||||
{
|
||||
user: {},
|
||||
source: {},
|
||||
topics: {},
|
||||
synapses: {},
|
||||
mappings: {},
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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" %>
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
|
||||
<% if current_user %>
|
||||
<div class="requestTitle">
|
||||
Click here to name this map!
|
||||
Click here to name this map
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
|
|
|
@ -6,4 +6,4 @@ test:
|
|||
|
||||
production:
|
||||
adapter: redis
|
||||
url: redis://localhost:6379/1
|
||||
url: <%= ENV['REDISTOGO_URL'] %>
|
||||
|
|
|
@ -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
|
||||
|
|
10
config/initializers/warden_hooks.rb
Normal file
10
config/initializers/warden_hooks.rb
Normal 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
|
|
@ -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
|
||||
|
|
5
db/migrate/20161218183817_add_source_to_maps.rb
Normal file
5
db/migrate/20161218183817_add_source_to_maps.rb
Normal file
|
@ -0,0 +1,5 @@
|
|||
class AddSourceToMaps < ActiveRecord::Migration[5.0]
|
||||
def change
|
||||
add_reference :maps, :source, foreign_key: {to_table: :maps}
|
||||
end
|
||||
end
|
|
@ -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
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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" ] },
|
||||
|
|
3
doc/api/schemas/_optid.json
Normal file
3
doc/api/schemas/_optid.json
Normal file
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"type": "integer|nil"
|
||||
}
|
|
@ -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??
|
||||
|
|
234
frontend/src/Metamaps/Cable.js
Normal file
234
frontend/src/Metamaps/Cable.js
Normal 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
|
|
@ -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
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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 && (
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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?")
|
||||
}
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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'
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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')
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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> </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>
|
||||
)
|
||||
}
|
||||
|
|
40
frontend/src/components/MapChat/Message.js
Normal file
40
frontend/src/components/MapChat/Message.js
Normal 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>
|
||||
<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
|
65
frontend/src/components/MapChat/NewMessage.js
Normal file
65
frontend/src/components/MapChat/NewMessage.js
Normal file
|
@ -0,0 +1,65 @@
|
|||
import React, { PropTypes, Component } from 'react'
|
||||
import { Emoji, Picker } from 'emoji-mart'
|
||||
|
||||
class NewMessage extends Component {
|
||||
constructor(props) {
|
||||
super(props)
|
||||
|
||||
this.state = {
|
||||
showEmojiPicker: false
|
||||
}
|
||||
}
|
||||
|
||||
toggleEmojiPicker = () => {
|
||||
this.setState({ showEmojiPicker: !this.state.showEmojiPicker })
|
||||
}
|
||||
|
||||
handleClick = (emoji, event) => {
|
||||
const { messageText } = this.props
|
||||
this.props.handleChange({ target: {
|
||||
value: messageText + emoji.colons
|
||||
}})
|
||||
|
||||
this.setState({ showEmojiPicker: false })
|
||||
this.props.focusMessageInput()
|
||||
}
|
||||
|
||||
render = () => {
|
||||
return (
|
||||
<div className="new-message-area">
|
||||
<Picker set="emojione"
|
||||
onClick={this.handleClick}
|
||||
style={{
|
||||
display: this.state.showEmojiPicker ? 'block' : 'none',
|
||||
maxWidth: '100%'
|
||||
}}
|
||||
emoji="upside_down_face"
|
||||
title="Emoji"
|
||||
/>
|
||||
<div className="extra-message-options">
|
||||
<span className="emoji-picker-button" onClick={this.toggleEmojiPicker}><Emoji size={24} emoji="upside_down_face" /></span>
|
||||
</div>
|
||||
<textarea value={this.props.messageText}
|
||||
onChange={this.props.handleChange}
|
||||
{...this.props.textAreaProps}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
NewMessage.propTypes = {
|
||||
messageText: PropTypes.string,
|
||||
handleChange: PropTypes.func,
|
||||
focusMessageInput: PropTypes.func,
|
||||
textAreaProps: PropTypes.shape({
|
||||
className: PropTypes.string,
|
||||
ref: PropTypes.func,
|
||||
placeholder: PropTypes.string,
|
||||
onKeyUp: PropTypes.func,
|
||||
onFocus: PropTypes.func,
|
||||
onBlur: PropTypes.func
|
||||
})
|
||||
}
|
||||
|
||||
export default NewMessage
|
45
frontend/src/components/MapChat/Participant.js
Normal file
45
frontend/src/components/MapChat/Participant.js
Normal 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
|
7
frontend/src/components/MapChat/Unread.js
Normal file
7
frontend/src/components/MapChat/Unread.js
Normal 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
|
193
frontend/src/components/MapChat/index.js
Normal file
193
frontend/src/components/MapChat/index.js
Normal 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
|
|
@ -41,7 +41,7 @@ class Header extends Component {
|
|||
linkClass={activeClass('active')}
|
||||
data-router="true"
|
||||
text="All Maps"
|
||||
/>
|
||||
/>
|
||||
<MapLink show={signedIn && explore}
|
||||
href="/explore/mine"
|
||||
linkClass={activeClass('my')}
|
||||
|
|
|
@ -112,7 +112,7 @@ class MapCard extends Component {
|
|||
{ mobile && hasConversation && <div className='mobileHasConversation'><MapperList mappers={ mapperList } /></div> }
|
||||
{ mobile && d && <div className="desc">{ d }</div> }
|
||||
{ mobile && <div className='mobileMetadata'><Metadata map={ map } /></div> }
|
||||
<div className='creatorAndPerm'>
|
||||
<div className={`creatorAndPerm ${map.authorizeToEdit(currentUser) ? '' : 'cardHasViewOnly'}`}>
|
||||
<img className='creatorImage' src={ map.get('user_image') } />
|
||||
<span className='creatorName'>{ map.get('user_name') }</span>
|
||||
{ !map.authorizeToEdit(currentUser) && <div className='cardViewOnly'>View Only</div> }
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -4,6 +4,7 @@ FactoryGirl.define do
|
|||
sequence(:name) { |n| "Cool Map ##{n}" }
|
||||
permission :commons
|
||||
arranged { false }
|
||||
source_id nil
|
||||
desc ''
|
||||
user
|
||||
end
|
||||
|
|
Loading…
Reference in a new issue