瀏覽代碼

Implement presence indicators

Maxime Quandalle 10 年之前
父節點
當前提交
f4c80d1315

+ 2 - 0
.jshintrc

@@ -55,6 +55,8 @@
     "Mousetrap": false,
     "Mousetrap": false,
     "Avatar": true,
     "Avatar": true,
     "Ps": true,
     "Ps": true,
+    "Presence": true,
+    "Presences": true,
 
 
     // Our collections
     // Our collections
     "Boards": true,
     "Boards": true,

+ 3 - 0
.meteor/packages

@@ -2,6 +2,9 @@
 #
 #
 # 'meteor add' and 'meteor remove' will edit this file for you,
 # 'meteor add' and 'meteor remove' will edit this file for you,
 # but you can also edit it by hand.
 # but you can also edit it by hand.
+#
+# XXX Should we replace tmeasday:presence by 3stack:presence? Or maybe the
+# packages will merge in the future?
 
 
 meteor-platform
 meteor-platform
 
 

+ 2 - 1
client/components/sidebar/sidebar.jade

@@ -31,10 +31,11 @@ template(name="membersWidget")
           userId=this.userId
           userId=this.userId
           draggable=true
           draggable=true
           size="small"
           size="small"
-          showBadges=true)
+          showStatus=true)
       unless isSandstorm
       unless isSandstorm
         if currentUser.isBoardAdmin
         if currentUser.isBoardAdmin
           a.js-open-manage-board-members
           a.js-open-manage-board-members
+      .clearfix
 
 
 template(name="labelsWidget")
 template(name="labelsWidget")
   .board-widget.board-widget-labels
   .board-widget.board-widget-labels

+ 0 - 7
client/components/users/avatar.jade

@@ -1,7 +0,0 @@
-template(name="userAvatar")
-  .member(class="{{class}} {{# if draggable }}js-member{{else}}js-member-on-card-menu{{/if}}"
-    title="{{userData.profile.name}} ({{userData.username}})")
-    +avatar(user=userData size=size)
-    if showBadges
-      span.member-status(class="{{# if userData.profile.status}}active{{/if}}")
-      span.member-type(class=memberType)

+ 0 - 5
client/components/users/headerButtons.js

@@ -1,5 +0,0 @@
-Template.headerUserBar.events({
-  'click .js-sign-in': Popup.open('signup'),
-  'click .js-log-in': Popup.open('login'),
-  'click .js-open-header-member-menu': Popup.open('memberMenu')
-});

+ 0 - 1
client/components/users/router.js

@@ -1,4 +1,3 @@
-
 _.each(['signIn', 'signUp', 'resetPwd',
 _.each(['signIn', 'signUp', 'resetPwd',
   'forgotPwd', 'enrollAccount', 'changePwd'], function(routeName) {
   'forgotPwd', 'enrollAccount', 'changePwd'], function(routeName) {
   AccountsTemplates.configureRoute(routeName, {
   AccountsTemplates.configureRoute(routeName, {

+ 23 - 0
client/components/users/userAvatar.jade

@@ -0,0 +1,23 @@
+template(name="userAvatar")
+  .member(class="{{class}} {{# if draggable }}js-member{{else}}js-member-on-card-menu{{/if}}"
+    title="{{userData.profile.name}} ({{userData.username}})")
+    +avatar(user=userData size=size)
+    if showStatus
+      span.member-presence-status(class=presenceStatusClassName)
+      span.member-type(class=memberType)
+
+
+template(name="userPopup")
+  .board-member-menu
+    .mini-profile-info
+      +userAvatar(user=user)
+        .info
+          h3.bottom
+            a.js-profile(href="{{ pathFor route='Profile' username=user.username }}")= user.profile.name
+            p.quiet.bottom @{{ user.username }}
+
+template(name="memberName")
+  a.inline-object.js-show-mem-menu(href="{{ pathFor route='Profile' username=user.username }}")
+    = user.profile.name
+    if username
+      | ({{ user.username }})

+ 8 - 13
client/components/users/helpers.js → client/components/users/userAvatar.js

@@ -9,19 +9,14 @@ Template.userAvatar.helpers({
     var userId = this.userId || this.user._id;
     var userId = this.userId || this.user._id;
     var user = Users.findOne(userId);
     var user = Users.findOne(userId);
     return user && user.isBoardAdmin() ? 'admin' : 'normal';
     return user && user.isBoardAdmin() ? 'admin' : 'normal';
-  }
-});
-
-Template.setLanguagePopup.helpers({
-  languages: function() {
-    return _.map(TAPi18n.getLanguages(), function(lang, tag) {
-      return {
-        tag: tag,
-        name: lang.name
-      };
-    });
   },
   },
-  isCurrentLanguage: function() {
-    return this.tag === TAPi18n.getLanguage();
+  presenceStatusClassName: function() {
+    var userPresence = Presences.findOne({ userId: this.user._id });
+    if (! userPresence)
+      return 'disconnected';
+    else if (Session.equals('currentBoard', userPresence.state.currentBoardId))
+      return 'active';
+    else
+      return 'idle';
   }
   }
 });
 });

+ 5 - 4
client/components/users/member.styl → client/components/users/userAvatar.styl

@@ -38,16 +38,17 @@ avatar-radius = 50%
       max-width: 100%
       max-width: 100%
       max-height: 100%
       max-height: 100%
 
 
-  .member-status
+  .member-presence-status
     background-color: #b3b3b3
     background-color: #b3b3b3
     border: 1px solid #fff
     border: 1px solid #fff
     border-radius: 50%
     border-radius: 50%
-    height: 8px
+    height: 7px
     width: @height
     width: @height
     position: absolute
     position: absolute
-    right: 0px
-    bottom: 0px
+    right: -1px
+    bottom: -1px
     border: 1px solid white
     border: 1px solid white
+    z-index: 15
 
 
     &.active
     &.active
       background: #64c464
       background: #64c464

+ 0 - 1
client/components/users/form.styl → client/components/users/userForm.styl

@@ -7,7 +7,6 @@
   img
   img
     width: 275px
     width: 275px
 
 
-
 .at-form
 .at-form
   margin: auto
   margin: auto
   width: 275px
   width: 275px

+ 9 - 5
client/components/users/headerButtons.jade → client/components/users/userHeader.jade

@@ -8,11 +8,6 @@ template(name="headerUserBar")
         = currentUser.username
         = currentUser.username
     +userAvatar(user=currentUser)
     +userAvatar(user=currentUser)
 
 
-template(name="memberHeader")
-  a.header-member.js-open-header-member-menu
-    span= currentUser.profile.name
-    +userAvatar(user=currentUser size="small")
-
 template(name="memberMenuPopup")
 template(name="memberMenuPopup")
   ul.pop-over-list
   ul.pop-over-list
     li: a(href="{{pathFor route='Profile' username=currentUser.username}}") {{_ 'profile'}}
     li: a(href="{{pathFor route='Profile' username=currentUser.username}}") {{_ 'profile'}}
@@ -21,3 +16,12 @@ template(name="memberMenuPopup")
   hr
   hr
   ul.pop-over-list
   ul.pop-over-list
     li: a.js-logout {{_ 'log-out'}}
     li: a.js-logout {{_ 'log-out'}}
+
+template(name="setLanguagePopup")
+  ul.pop-over-list
+    each languages
+      li(class="{{# if isCurrentLanguage}}active{{/if}}")
+        a.js-set-language
+          = name
+          if isCurrentLanguage
+            i.fa.fa-check

+ 39 - 0
client/components/users/userHeader.js

@@ -0,0 +1,39 @@
+Template.headerUserBar.events({
+  'click .js-open-header-member-menu': Popup.open('memberMenu')
+});
+
+Template.setLanguagePopup.helpers({
+  languages: function() {
+    return _.map(TAPi18n.getLanguages(), function(lang, tag) {
+      return {
+        tag: tag,
+        name: lang.name
+      };
+    });
+  },
+  isCurrentLanguage: function() {
+    return this.tag === TAPi18n.getLanguage();
+  }
+});
+
+Template.memberMenuPopup.events({
+  'click .js-language': Popup.open('setLanguage'),
+  'click .js-logout': function(evt) {
+    evt.preventDefault();
+
+    Meteor.logout(function() {
+      Router.go('Home');
+    });
+  }
+});
+
+Template.setLanguagePopup.events({
+  'click .js-set-language': function(evt) {
+    Users.update(Meteor.userId(), {
+      $set: {
+        'profile.language': this.tag
+      }
+    });
+    evt.preventDefault();
+  }
+});

+ 0 - 39
client/components/users/templates.html → client/components/users/userProfile.html

@@ -1,18 +1,3 @@
-<template name="setLanguagePopup">
-<ul class="pop-over-list">
-    {{#each languages}}
-        <li class="{{# if isCurrentLanguage}}active{{/if}}">
-            <a class="js-set-language">
-                {{name}}
-                {{# if isCurrentLanguage}}
-                    <span class="icon-sm fa fa-check"></span>
-                {{/if}}
-            </a>
-        </li>
-    {{/each}}
-</ul>
-</template>
-
 <template name='profile'>
 <template name='profile'>
     {{ # if profile }}
     {{ # if profile }}
         <div class="tabbed-pane-header">
         <div class="tabbed-pane-header">
@@ -92,27 +77,3 @@
         {{ /if }}
         {{ /if }}
     {{ /if }}
     {{ /if }}
 </template>
 </template>
-
-<template name="userPopup">
-    <div class="board-member-menu">
-        <div class="mini-profile-info">
-            {{> userAvatar user=user}}
-            <div class="info">
-                <h3 class="bottom" style="margin-right: 40px;">
-                    <a class="js-profile" href="{{ pathFor route='Profile' username=user.username }}">{{ user.profile.name }}</a>
-                </h3>
-                <p class="quiet bottom">@{{ user.username }}</p>
-            </div>
-        </div>
-    </div>
-</template>
-
-
-<template name="memberName">
-    <a class="inline-object js-show-mem-menu" href="{{ pathFor route='Profile' username=user.username }}">
-        {{ user.profile.name }}
-        {{# if username }}
-            ({{ user.username }})
-        {{ /if }}
-    </a>
-</template>

+ 0 - 22
client/components/users/events.js → client/components/users/userProfile.js

@@ -1,25 +1,3 @@
-Template.memberMenuPopup.events({
-  'click .js-language': Popup.open('setLanguage'),
-  'click .js-logout': function(evt) {
-    evt.preventDefault();
-
-    Meteor.logout(function() {
-      Router.go('Home');
-    });
-  }
-});
-
-Template.setLanguagePopup.events({
-  'click .js-set-language': function(evt) {
-    Users.update(Meteor.userId(), {
-      $set: {
-        'profile.language': this.tag
-      }
-    });
-    evt.preventDefault();
-  }
-});
-
 Template.profileEditForm.events({
 Template.profileEditForm.events({
   'click .js-edit-profile': function() {
   'click .js-edit-profile': function() {
     Session.set('ProfileEditForm', true);
     Session.set('ProfileEditForm', true);

+ 7 - 14
client/config/router.js

@@ -24,6 +24,13 @@ Router.configure({
       return this.redirect('atSignIn');
       return this.redirect('atSignIn');
     }
     }
 
 
+    // We want to execute our EscapeActions.executeLowerThan method any time the
+    // route is changed, but not if the stays the same but only the parameters
+    // change (eg when a user is navigation from a card A to a card B). Iron-
+    // Router onBeforeAction is a reactive context (which is a bad desig choice
+    // as explained in
+    // https://github.com/meteorhacks/flow-router#routercurrent-is-evil) so we
+    // need to use Tracker.nonreactive
     Tracker.nonreactive(function() {
     Tracker.nonreactive(function() {
       if (! options.noEscapeActions &&
       if (! options.noEscapeActions &&
           ! (previousRoute && previousRoute.options.noEscapeActions))
           ! (previousRoute && previousRoute.options.noEscapeActions))
@@ -35,17 +42,3 @@ Router.configure({
     this.next();
     this.next();
   }
   }
 });
 });
-
-// We want to execute our EscapeActions.executeLowerThan method any time the
-// route is changed, but not if the stays the same but only the parameters
-// change (eg when a user is navigation from a card A to a card B). This is why
-// we can’t put this function in the above `onBeforeAction` that is being run
-// too many times, instead we register a dependency only on the route name and
-// use Tracker.autorun. The following paragraph explains the problem quite well:
-// https://github.com/meteorhacks/flow-router#routercurrent-is-evil
-// Tracker.autorun(function(computation) {
-//   routeName.get();
-//   if (! computation.firstRun) {
-//     EscapeActions.executeLowerThan('inlinedForm');
-//   }
-// });

+ 0 - 110
client/styles/temp.styl

@@ -1,110 +0,0 @@
-/**
- * We should merge these declarations in the appropriate stylus files.
- */
-
-.dn {
-    display:none;
-}
-
-.header-btn-btn {
-    padding-left:23px!important;
-}
-
-.bgnone {
-    background:none!important;
-}
-
-.tac {
-    text-align:center;
-
-    h1 {
-        font-size: 2em;
-    }
-}
-
-.tdn {
-    text-decoration:none;
-}
-
-.header-member {
-    min-width:105px!important;
-    text-align:center;
-}
-
-.primarys {
-    font-size:20px;
-    line-height: 1.44em;
-    padding: .6em 1.3em!important;
-    border-radius: 3px!important;
-    box-shadow: 0 2px 0 #4d4d4d!important;
-}
-
-.layout-twothirds-center {
-    display: block;
-    max-width: 585px;
-    margin: 0 auto;
-    position: relative;
-    font-size:20px;
-    line-height: 100px;
-}
-
-#WindowTitleEdit .single-line, .single-line2 {
-    overflow: hidden;
-    word-wrap: break-word;
-    resize: none;
-    height: 60px;
-}
-
-.single-line2 {
-    overflow: hidden;
-    word-wrap: break-word;
-    resize: none;
-    height: 108px;
-}
-
-#header-search {
-    float: left;
-    margin: 1px 8px 0 0;
-    position: relative;
-    z-index: 1;
-
-    label {
-        display:none;
-    }
-    input[type="text"] {
-        background:rgba(255,255,255,0.5);
-        border-top-left-radius:3px;
-        border-top-right-radius:0;
-        border-bottom-right-radius:0;
-        border-bottom-left-radius:3px;
-        border:none;
-        float:left;
-        font-size:13px;
-        height:29px;
-        min-height:29px;
-        line-height:19px;
-        width:160px;
-        margin:0;
-
-        &:hover{
-            background:rgba(255,255,255,0.7);
-        }
-
-        &:focus{
-            background:#e8ebee;
-            -webkit-box-shadow:none;
-            box-shadow:none
-        }
-    }
-
-    .header-btn{
-        border-top-left-radius:0;
-        border-top-right-radius:3px;
-        border-bottom-right-radius:3px;
-        border-bottom-left-radius:0
-    }
-
-    input[type="submit"]{
-        display:none
-    }
-}

+ 9 - 3
collections/users.js

@@ -43,9 +43,6 @@ Users.helpers({
 
 
 Users.before.insert(function(userId, doc) {
 Users.before.insert(function(userId, doc) {
   doc.profile = doc.profile || {};
   doc.profile = doc.profile || {};
-
-  // connect profile.status default
-  doc.profile.status = 'offline';
 });
 });
 
 
 if (Meteor.isServer) {
 if (Meteor.isServer) {
@@ -110,3 +107,12 @@ if (Meteor.isServer) {
     });
     });
   });
   });
 }
 }
+
+// Presence indicator
+if (Meteor.isClient) {
+  Presence.state = function() {
+    return {
+      currentBoardId: Session.get('currentBoard')
+    };
+  };
+}

+ 9 - 3
server/publications/boards.js

@@ -108,14 +108,20 @@ Meteor.publishComposite('board', function(boardId, slug) {
       },
       },
 
 
       // Board members. This publication also includes former board members that
       // Board members. This publication also includes former board members that
-      // are no more members of the board but may have some activities attached
-      // to them.
+      // aren't members anymore but may have some activities attached to them in
+      // the history.
       {
       {
         find: function(board) {
         find: function(board) {
           return Users.find({
           return Users.find({
             _id: { $in: _.pluck(board.members, 'userId') }
             _id: { $in: _.pluck(board.members, 'userId') }
           });
           });
-        }
+        },
+        // Presence indicators
+        children: [{
+          find: function(user) {
+            return Presences.find({userId: user._id});
+          }
+        }]
       }
       }
     ]
     ]
   };
   };