mirror of
https://github.com/wekan/wekan.git
synced 2025-04-20 12:07:11 -04:00
Implement presence indicators
This commit is contained in:
parent
6db01bb3c7
commit
f4c80d1315
18 changed files with 116 additions and 228 deletions
|
@ -55,6 +55,8 @@
|
|||
"Mousetrap": false,
|
||||
"Avatar": true,
|
||||
"Ps": true,
|
||||
"Presence": true,
|
||||
"Presences": true,
|
||||
|
||||
// Our collections
|
||||
"Boards": true,
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
|
@ -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')
|
||||
});
|
|
@ -1,4 +1,3 @@
|
|||
|
||||
_.each(['signIn', 'signUp', 'resetPwd',
|
||||
'forgotPwd', 'enrollAccount', 'changePwd'], function(routeName) {
|
||||
AccountsTemplates.configureRoute(routeName, {
|
||||
|
|
23
client/components/users/userAvatar.jade
Normal file
23
client/components/users/userAvatar.jade
Normal 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 }})
|
|
@ -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';
|
||||
}
|
||||
});
|
|
@ -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
|
|
@ -7,7 +7,6 @@
|
|||
img
|
||||
width: 275px
|
||||
|
||||
|
||||
.at-form
|
||||
margin: auto
|
||||
width: 275px
|
|
@ -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
|
39
client/components/users/userHeader.js
Normal file
39
client/components/users/userHeader.js
Normal 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();
|
||||
}
|
||||
});
|
|
@ -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>
|
|
@ -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);
|
|
@ -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 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');
|
||||
// }
|
||||
// });
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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')
|
||||
};
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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});
|
||||
}
|
||||
}]
|
||||
}
|
||||
]
|
||||
};
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue