Implement presence indicators

This commit is contained in:
Maxime Quandalle 2015-05-30 15:50:48 +02:00
parent 6db01bb3c7
commit f4c80d1315
18 changed files with 116 additions and 228 deletions

View file

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

View file

@ -2,6 +2,9 @@
#
# 'meteor add' and 'meteor remove' will edit this file for you,
# 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

View file

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

View file

@ -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)

View file

@ -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')
});

View file

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

View file

@ -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 }})

View file

@ -9,19 +9,14 @@ Template.userAvatar.helpers({
var userId = this.userId || this.user._id;
var user = Users.findOne(userId);
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';
}
});

View file

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

View file

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

View file

@ -8,11 +8,6 @@ template(name="headerUserBar")
= currentUser.username
+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")
ul.pop-over-list
li: a(href="{{pathFor route='Profile' username=currentUser.username}}") {{_ 'profile'}}
@ -21,3 +16,12 @@ template(name="memberMenuPopup")
hr
ul.pop-over-list
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

View file

@ -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();
}
});

View file

@ -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'>
{{ # if profile }}
<div class="tabbed-pane-header">
@ -92,27 +77,3 @@
{{ /if }}
{{ /if }}
</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>

View file

@ -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({
'click .js-edit-profile': function() {
Session.set('ProfileEditForm', true);

View file

@ -24,6 +24,13 @@ Router.configure({
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() {
if (! options.noEscapeActions &&
! (previousRoute && previousRoute.options.noEscapeActions))
@ -35,17 +42,3 @@ Router.configure({
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 cant 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');
// }
// });

View file

@ -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
}
}

View file

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

View file

@ -108,14 +108,20 @@ Meteor.publishComposite('board', function(boardId, slug) {
},
// 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) {
return Users.find({
_id: { $in: _.pluck(board.members, 'userId') }
});
}
},
// Presence indicators
children: [{
find: function(user) {
return Presences.find({userId: user._id});
}
}]
}
]
};