mirror of
https://github.com/wekan/wekan.git
synced 2025-12-16 07:20:12 +01:00
Add notification, allow watch boards / lists / cards
This commit is contained in:
parent
9ef8ebaf09
commit
9bbdacc79a
24 changed files with 579 additions and 16 deletions
|
|
@ -115,6 +115,7 @@ globals:
|
||||||
Utils: true
|
Utils: true
|
||||||
InlinedForm: true
|
InlinedForm: true
|
||||||
UnsavedEdits: true
|
UnsavedEdits: true
|
||||||
|
Notifications: true
|
||||||
|
|
||||||
# XXX Temp, we should remove these
|
# XXX Temp, we should remove these
|
||||||
allowIsBoardAdmin: true
|
allowIsBoardAdmin: true
|
||||||
|
|
|
||||||
|
|
@ -51,7 +51,7 @@ BlazeComponent.extendComponent({
|
||||||
cardLink() {
|
cardLink() {
|
||||||
const card = this.currentData().card();
|
const card = this.currentData().card();
|
||||||
return card && Blaze.toHTML(HTML.A({
|
return card && Blaze.toHTML(HTML.A({
|
||||||
href: FlowRouter.path(card.absoluteUrl()),
|
href: card.absoluteUrl(),
|
||||||
'class': 'action-card',
|
'class': 'action-card',
|
||||||
}, card.title));
|
}, card.title));
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,17 @@ template(name="boardHeaderBar")
|
||||||
i.fa(class="{{#if currentBoard.isPublic}}fa-globe{{else}}fa-lock{{/if}}")
|
i.fa(class="{{#if currentBoard.isPublic}}fa-globe{{else}}fa-lock{{/if}}")
|
||||||
span {{_ currentBoard.permission}}
|
span {{_ currentBoard.permission}}
|
||||||
|
|
||||||
|
a.board-header-btn.js-watch-board
|
||||||
|
if $eq watchLevel "watching"
|
||||||
|
i.fa.fa-eye
|
||||||
|
span {{_ 'watching'}}
|
||||||
|
if $eq watchLevel "tracking"
|
||||||
|
i.fa.fa-user
|
||||||
|
span {{_ 'tracking'}}
|
||||||
|
if $eq watchLevel "muted"
|
||||||
|
i.fa.fa-times-circle
|
||||||
|
span {{_ 'muted'}}
|
||||||
|
|
||||||
.board-header-btns.right
|
.board-header-btns.right
|
||||||
if isMiniScreen
|
if isMiniScreen
|
||||||
unless isSandstorm
|
unless isSandstorm
|
||||||
|
|
@ -34,6 +45,17 @@ template(name="boardHeaderBar")
|
||||||
i.fa(class="{{#if currentBoard.isPublic}}fa-globe{{else}}fa-lock{{/if}}")
|
i.fa(class="{{#if currentBoard.isPublic}}fa-globe{{else}}fa-lock{{/if}}")
|
||||||
span {{_ currentBoard.permission}}
|
span {{_ currentBoard.permission}}
|
||||||
|
|
||||||
|
a.board-header-btn.js-watch-board
|
||||||
|
if $eq watchLevel "watching"
|
||||||
|
i.fa.fa-eye
|
||||||
|
span {{_ 'watching'}}
|
||||||
|
if $eq watchLevel "tracking"
|
||||||
|
i.fa.fa-user
|
||||||
|
span {{_ 'tracking'}}
|
||||||
|
if $eq watchLevel "muted"
|
||||||
|
i.fa.fa-times-circle
|
||||||
|
span {{_ 'muted'}}
|
||||||
|
|
||||||
a.board-header-btn.js-open-filter-view(
|
a.board-header-btn.js-open-filter-view(
|
||||||
title="{{#if Filter.isActive}}{{_ 'filter-on-desc'}}{{/if}}"
|
title="{{#if Filter.isActive}}{{_ 'filter-on-desc'}}{{/if}}"
|
||||||
class="{{#if Filter.isActive}}emphasis{{/if}}")
|
class="{{#if Filter.isActive}}emphasis{{/if}}")
|
||||||
|
|
@ -97,6 +119,33 @@ template(name="boardVisibilityList")
|
||||||
template(name="boardChangeVisibilityPopup")
|
template(name="boardChangeVisibilityPopup")
|
||||||
+boardVisibilityList
|
+boardVisibilityList
|
||||||
|
|
||||||
|
template(name="boardChangeWatchPopup")
|
||||||
|
ul.pop-over-list
|
||||||
|
li
|
||||||
|
with "watching"
|
||||||
|
a.js-select-watch
|
||||||
|
i.fa.fa-eye.colorful
|
||||||
|
| {{_ 'watching'}}
|
||||||
|
if watchCheck
|
||||||
|
i.fa.fa-check
|
||||||
|
span.sub-name {{_ 'watching-info'}}
|
||||||
|
li
|
||||||
|
with "tracking"
|
||||||
|
a.js-select-watch
|
||||||
|
i.fa.fa-user.colorful
|
||||||
|
| {{_ 'tracking'}}
|
||||||
|
if watchCheck
|
||||||
|
i.fa.fa-check
|
||||||
|
span.sub-name {{_ 'tracking-info'}}
|
||||||
|
li
|
||||||
|
with "muted"
|
||||||
|
a.js-select-watch
|
||||||
|
i.fa.fa-times-circle.colorful
|
||||||
|
| {{_ 'muted'}}
|
||||||
|
if watchCheck
|
||||||
|
i.fa.fa-check
|
||||||
|
span.sub-name {{_ 'muted-info'}}
|
||||||
|
|
||||||
template(name="boardChangeColorPopup")
|
template(name="boardChangeColorPopup")
|
||||||
.board-backgrounds-list.clearfix
|
.board-backgrounds-list.clearfix
|
||||||
each backgroundColors
|
each backgroundColors
|
||||||
|
|
|
||||||
|
|
@ -41,6 +41,11 @@ Template.boardChangeTitlePopup.events({
|
||||||
});
|
});
|
||||||
|
|
||||||
BlazeComponent.extendComponent({
|
BlazeComponent.extendComponent({
|
||||||
|
watchLevel() {
|
||||||
|
const currentBoard = Boards.findOne(Session.get('currentBoard'));
|
||||||
|
return currentBoard.getWatchLevel(Meteor.userId());
|
||||||
|
},
|
||||||
|
|
||||||
isStarred() {
|
isStarred() {
|
||||||
const boardId = Session.get('currentBoard');
|
const boardId = Session.get('currentBoard');
|
||||||
const user = Meteor.user();
|
const user = Meteor.user();
|
||||||
|
|
@ -65,6 +70,7 @@ BlazeComponent.extendComponent({
|
||||||
},
|
},
|
||||||
'click .js-open-board-menu': Popup.open('boardMenu'),
|
'click .js-open-board-menu': Popup.open('boardMenu'),
|
||||||
'click .js-change-visibility': Popup.open('boardChangeVisibility'),
|
'click .js-change-visibility': Popup.open('boardChangeVisibility'),
|
||||||
|
'click .js-watch-board': Popup.open('boardChangeWatch'),
|
||||||
'click .js-open-filter-view'() {
|
'click .js-open-filter-view'() {
|
||||||
Sidebar.setView('filter');
|
Sidebar.setView('filter');
|
||||||
},
|
},
|
||||||
|
|
@ -176,3 +182,25 @@ BlazeComponent.extendComponent({
|
||||||
}];
|
}];
|
||||||
},
|
},
|
||||||
}).register('boardChangeVisibilityPopup');
|
}).register('boardChangeVisibilityPopup');
|
||||||
|
|
||||||
|
BlazeComponent.extendComponent({
|
||||||
|
watchLevel() {
|
||||||
|
const currentBoard = Boards.findOne(Session.get('currentBoard'));
|
||||||
|
return currentBoard.getWatchLevel(Meteor.userId());
|
||||||
|
},
|
||||||
|
|
||||||
|
watchCheck() {
|
||||||
|
return this.currentData() === this.watchLevel();
|
||||||
|
},
|
||||||
|
|
||||||
|
events() {
|
||||||
|
return [{
|
||||||
|
'click .js-select-watch'() {
|
||||||
|
const level = this.currentData();
|
||||||
|
Meteor.call('watch', 'board', Session.get('currentBoard'), level, (err, ret) => {
|
||||||
|
if (!err && ret) Popup.close();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
}];
|
||||||
|
},
|
||||||
|
}).register('boardChangeWatchPopup');
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,8 @@ template(name="cardDetails")
|
||||||
h2.card-details-title.js-card-title(
|
h2.card-details-title.js-card-title(
|
||||||
class="{{#if currentUser.isBoardMember}}js-open-inlined-form is-editable{{/if}}")
|
class="{{#if currentUser.isBoardMember}}js-open-inlined-form is-editable{{/if}}")
|
||||||
= title
|
= title
|
||||||
|
if isWatching
|
||||||
|
i.fa.fa-eye.card-details-watch
|
||||||
|
|
||||||
if archived
|
if archived
|
||||||
p.warning {{_ 'card-archived'}}
|
p.warning {{_ 'card-archived'}}
|
||||||
|
|
@ -82,6 +84,9 @@ template(name="editCardTitleForm")
|
||||||
a.fa.fa-times-thin.js-close-inlined-form
|
a.fa.fa-times-thin.js-close-inlined-form
|
||||||
|
|
||||||
template(name="cardDetailsActionsPopup")
|
template(name="cardDetailsActionsPopup")
|
||||||
|
ul.pop-over-list
|
||||||
|
li: a.js-toggle-watch-card {{#if isWatching}}{{_ 'unwatch'}}{{else}}{{_ 'watch'}}{{/if}}
|
||||||
|
hr
|
||||||
ul.pop-over-list
|
ul.pop-over-list
|
||||||
li: a.js-members {{_ 'card-edit-members'}}
|
li: a.js-members {{_ 'card-edit-members'}}
|
||||||
li: a.js-labels {{_ 'card-edit-labels'}}
|
li: a.js-labels {{_ 'card-edit-labels'}}
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,11 @@ BlazeComponent.extendComponent({
|
||||||
this.calculateNextPeak();
|
this.calculateNextPeak();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
isWatching() {
|
||||||
|
const card = this.currentData();
|
||||||
|
return card.findWatcher(Meteor.userId());
|
||||||
|
},
|
||||||
|
|
||||||
scrollParentContainer() {
|
scrollParentContainer() {
|
||||||
const cardPanelWidth = 510;
|
const cardPanelWidth = 510;
|
||||||
const bodyBoardComponent = this.parentComponent();
|
const bodyBoardComponent = this.parentComponent();
|
||||||
|
|
@ -128,6 +133,12 @@ BlazeComponent.extendComponent({
|
||||||
}
|
}
|
||||||
}).register('inlinedCardDescription');
|
}).register('inlinedCardDescription');
|
||||||
|
|
||||||
|
Template.cardDetailsActionsPopup.helpers({
|
||||||
|
isWatching() {
|
||||||
|
return this.findWatcher(Meteor.userId());
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
Template.cardDetailsActionsPopup.events({
|
Template.cardDetailsActionsPopup.events({
|
||||||
'click .js-members': Popup.open('cardMembers'),
|
'click .js-members': Popup.open('cardMembers'),
|
||||||
'click .js-labels': Popup.open('cardLabels'),
|
'click .js-labels': Popup.open('cardLabels'),
|
||||||
|
|
@ -139,6 +150,13 @@ Template.cardDetailsActionsPopup.events({
|
||||||
Popup.close();
|
Popup.close();
|
||||||
},
|
},
|
||||||
'click .js-more': Popup.open('cardMore'),
|
'click .js-more': Popup.open('cardMore'),
|
||||||
|
'click .js-toggle-watch-card'() {
|
||||||
|
const currentCard = this;
|
||||||
|
const level = currentCard.findWatcher(Meteor.userId()) ? null : 'watching';
|
||||||
|
Meteor.call('watch', 'card', currentCard._id, level, (err, ret) => {
|
||||||
|
if (!err && ret) Popup.close();
|
||||||
|
});
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
Template.editCardTitleForm.onRendered(function() {
|
Template.editCardTitleForm.onRendered(function() {
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,11 @@
|
||||||
font-size: 17px
|
font-size: 17px
|
||||||
padding: 10px
|
padding: 10px
|
||||||
|
|
||||||
|
.card-details-watch
|
||||||
|
font-size: 17px
|
||||||
|
padding-left: 7px
|
||||||
|
color: #a6a6a6
|
||||||
|
|
||||||
.card-details-title
|
.card-details-title
|
||||||
font-weight: bold
|
font-weight: bold
|
||||||
font-size: 1.33em
|
font-size: 1.33em
|
||||||
|
|
|
||||||
|
|
@ -65,6 +65,10 @@
|
||||||
text-overflow: ellipsis
|
text-overflow: ellipsis
|
||||||
word-wrap: break-word
|
word-wrap: break-word
|
||||||
|
|
||||||
|
.list-header-watch-icon
|
||||||
|
padding-left: 10px
|
||||||
|
color: #a6a6a6
|
||||||
|
|
||||||
.list-header-menu-icon
|
.list-header-menu-icon
|
||||||
position: absolute
|
position: absolute
|
||||||
padding: 7px
|
padding: 7px
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,8 @@ template(name="listHeader")
|
||||||
class="{{#if currentUser.isBoardMember}}js-open-inlined-form is-editable{{/if}}")
|
class="{{#if currentUser.isBoardMember}}js-open-inlined-form is-editable{{/if}}")
|
||||||
= title
|
= title
|
||||||
if currentUser.isBoardMember
|
if currentUser.isBoardMember
|
||||||
|
if isWatching
|
||||||
|
i.list-header-watch-icon.fa.fa-eye
|
||||||
a.list-header-menu-icon.fa.fa-navicon.js-open-list-menu
|
a.list-header-menu-icon.fa.fa-navicon.js-open-list-menu
|
||||||
|
|
||||||
template(name="editListTitleForm")
|
template(name="editListTitleForm")
|
||||||
|
|
@ -17,6 +19,9 @@ template(name="editListTitleForm")
|
||||||
a.fa.fa-times-thin.js-close-inlined-form
|
a.fa.fa-times-thin.js-close-inlined-form
|
||||||
|
|
||||||
template(name="listActionPopup")
|
template(name="listActionPopup")
|
||||||
|
ul.pop-over-list
|
||||||
|
li: a.js-toggle-watch-list {{#if isWatching}}{{_ 'unwatch'}}{{else}}{{_ 'watch'}}{{/if}}
|
||||||
|
hr
|
||||||
ul.pop-over-list
|
ul.pop-over-list
|
||||||
li: a.js-add-card {{_ 'add-card'}}
|
li: a.js-add-card {{_ 'add-card'}}
|
||||||
if cards.count
|
if cards.count
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,11 @@ BlazeComponent.extendComponent({
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
isWatching() {
|
||||||
|
const list = this.currentData();
|
||||||
|
return list.findWatcher(Meteor.userId());
|
||||||
|
},
|
||||||
|
|
||||||
events() {
|
events() {
|
||||||
return [{
|
return [{
|
||||||
'click .js-open-list-menu': Popup.open('listAction'),
|
'click .js-open-list-menu': Popup.open('listAction'),
|
||||||
|
|
@ -16,6 +21,12 @@ BlazeComponent.extendComponent({
|
||||||
},
|
},
|
||||||
}).register('listHeader');
|
}).register('listHeader');
|
||||||
|
|
||||||
|
Template.listActionPopup.helpers({
|
||||||
|
isWatching() {
|
||||||
|
return this.findWatcher(Meteor.userId());
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
Template.listActionPopup.events({
|
Template.listActionPopup.events({
|
||||||
'click .js-add-card'() {
|
'click .js-add-card'() {
|
||||||
const listDom = document.getElementById(`js-list-${this._id}`);
|
const listDom = document.getElementById(`js-list-${this._id}`);
|
||||||
|
|
@ -29,6 +40,13 @@ Template.listActionPopup.events({
|
||||||
MultiSelection.add(cardIds);
|
MultiSelection.add(cardIds);
|
||||||
Popup.close();
|
Popup.close();
|
||||||
},
|
},
|
||||||
|
'click .js-toggle-watch-list'() {
|
||||||
|
const currentList = this;
|
||||||
|
const level = currentList.findWatcher(Meteor.userId()) ? null : 'watching';
|
||||||
|
Meteor.call('watch', 'list', currentList._id, level, (err, ret) => {
|
||||||
|
if (!err && ret) Popup.close();
|
||||||
|
});
|
||||||
|
},
|
||||||
'click .js-close-list'(evt) {
|
'click .js-close-list'(evt) {
|
||||||
evt.preventDefault();
|
evt.preventDefault();
|
||||||
this.archive();
|
this.archive();
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,9 @@
|
||||||
float: left
|
float: left
|
||||||
border-radius: 3px
|
border-radius: 3px
|
||||||
|
|
||||||
|
.board-header-watch-icon
|
||||||
|
padding-left: 7px
|
||||||
|
|
||||||
a.fa, a i.fa
|
a.fa, a i.fa
|
||||||
color: white
|
color: white
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -279,9 +279,12 @@ kbd
|
||||||
.fa.fa-globe.colorful
|
.fa.fa-globe.colorful
|
||||||
color: #4caf50
|
color: #4caf50
|
||||||
|
|
||||||
.fa.fa-lock.colorful
|
.fa.fa-lock.colorful, .fa.fa-times-circle.colorful
|
||||||
color: #f44336
|
color: #f44336
|
||||||
|
|
||||||
|
.fa.fa-user.colorful, .fa.fa-eye.colorful, .fa.fa-circle.colorful
|
||||||
|
color: #4336f4
|
||||||
|
|
||||||
.pop-over .pop-over-list li a:not(.disabled):hover
|
.pop-over .pop-over-list li a:not(.disabled):hover
|
||||||
.fa, .fa.colorful
|
.fa, .fa.colorful
|
||||||
color: white
|
color: white
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ template(name="memberMenuPopup")
|
||||||
li: a.js-change-avatar {{_ 'edit-avatar'}}
|
li: a.js-change-avatar {{_ 'edit-avatar'}}
|
||||||
li: a.js-change-password {{_ 'changePasswordPopup-title'}}
|
li: a.js-change-password {{_ 'changePasswordPopup-title'}}
|
||||||
li: a.js-change-language {{_ 'changeLanguagePopup-title'}}
|
li: a.js-change-language {{_ 'changeLanguagePopup-title'}}
|
||||||
|
li: a.js-edit-notification {{_ 'editNotificationPopup-title'}}
|
||||||
hr
|
hr
|
||||||
ul.pop-over-list
|
ul.pop-over-list
|
||||||
li: a.js-logout {{_ 'log-out'}}
|
li: a.js-logout {{_ 'log-out'}}
|
||||||
|
|
@ -32,6 +33,23 @@ template(name="editProfilePopup")
|
||||||
input.js-profile-initials(type="text" value=profile.initials)
|
input.js-profile-initials(type="text" value=profile.initials)
|
||||||
input.primary.wide(type="submit" value="{{_ 'save'}}")
|
input.primary.wide(type="submit" value="{{_ 'save'}}")
|
||||||
|
|
||||||
|
template(name="editNotificationPopup")
|
||||||
|
ul.pop-over-list
|
||||||
|
li
|
||||||
|
a.js-toggle-tag-notify-watch
|
||||||
|
i.fa.fa-eye.colorful
|
||||||
|
| {{_ 'watching'}}
|
||||||
|
if hasTag "notify-watch"
|
||||||
|
i.fa.fa-check
|
||||||
|
span.sub-name {{_ 'notify-watch'}}
|
||||||
|
li
|
||||||
|
a.js-toggle-tag-notify-participate
|
||||||
|
i.fa.fa-user.colorful
|
||||||
|
| {{_ 'tracking'}}
|
||||||
|
if hasTag "notify-participate"
|
||||||
|
i.fa.fa-check
|
||||||
|
span.sub-name {{_ 'notify-participate'}}
|
||||||
|
|
||||||
template(name="changePasswordPopup")
|
template(name="changePasswordPopup")
|
||||||
+atForm(state='changePwd')
|
+atForm(state='changePwd')
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ Template.memberMenuPopup.events({
|
||||||
'click .js-change-avatar': Popup.open('changeAvatar'),
|
'click .js-change-avatar': Popup.open('changeAvatar'),
|
||||||
'click .js-change-password': Popup.open('changePassword'),
|
'click .js-change-password': Popup.open('changePassword'),
|
||||||
'click .js-change-language': Popup.open('changeLanguage'),
|
'click .js-change-language': Popup.open('changeLanguage'),
|
||||||
|
'click .js-edit-notification': Popup.open('editNotification'),
|
||||||
'click .js-logout'(evt) {
|
'click .js-logout'(evt) {
|
||||||
evt.preventDefault();
|
evt.preventDefault();
|
||||||
|
|
||||||
|
|
@ -33,6 +34,25 @@ Template.editProfilePopup.events({
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Template.editNotificationPopup.helpers({
|
||||||
|
hasTag(tag) {
|
||||||
|
const user = Meteor.user();
|
||||||
|
return user && user.hasTag(tag);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// we defined github like rules, see: https://github.com/settings/notifications
|
||||||
|
Template.editNotificationPopup.events({
|
||||||
|
'click .js-toggle-tag-notify-participate'() {
|
||||||
|
const user = Meteor.user();
|
||||||
|
if (user) user.toggleTag('notify-participate');
|
||||||
|
},
|
||||||
|
'click .js-toggle-tag-notify-watch'() {
|
||||||
|
const user = Meteor.user();
|
||||||
|
if (user) user.toggleTag('notify-watch');
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
// XXX For some reason the useraccounts autofocus isnt working in this case.
|
// XXX For some reason the useraccounts autofocus isnt working in this case.
|
||||||
// See https://github.com/meteor-useraccounts/core/issues/384
|
// See https://github.com/meteor-useraccounts/core/issues/384
|
||||||
Template.changePasswordPopup.onRendered(function() {
|
Template.changePasswordPopup.onRendered(function() {
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,25 @@
|
||||||
{
|
{
|
||||||
"accept": "Accept",
|
"accept": "Accept",
|
||||||
|
"act-activity-notify": "[Wekan] Activity Notification",
|
||||||
|
"act-addAttachment": "attached __attachment__ to __card__",
|
||||||
|
"act-addComment": "commented on __card__: __comment__",
|
||||||
|
"act-createBoard": "created __board__",
|
||||||
|
"act-createCard": "added __card__ to __list__",
|
||||||
|
"act-createList": "added __list__ to __board__",
|
||||||
|
"act-addBoardMember": "added __member__ to __board__",
|
||||||
|
"act-archivedBoard": "archived __board__",
|
||||||
|
"act-archivedCard": "archived __card__",
|
||||||
|
"act-archivedList": "archived __list__",
|
||||||
|
"act-importBoard": "imported __board__",
|
||||||
|
"act-importCard": "imported __card__",
|
||||||
|
"act-importList": "imported __list__",
|
||||||
|
"act-joinMember": "added __member__ to __card__",
|
||||||
|
"act-moveCard": "moved __card__ from __oldList__ to __list__",
|
||||||
|
"act-removeBoardMember": "removed __member__ from __board__",
|
||||||
|
"act-restoredCard": "restored __card__ to __board__",
|
||||||
|
"act-unjoinMember": "removed __member__ from __card__",
|
||||||
|
"act-withBoardTitle": "[Wekan] __board__",
|
||||||
|
"act-withCardTitle": "[__board__] __card__",
|
||||||
"actions": "Actions",
|
"actions": "Actions",
|
||||||
"activities": "Activities",
|
"activities": "Activities",
|
||||||
"activity": "Activity",
|
"activity": "Activity",
|
||||||
|
|
@ -46,6 +66,7 @@
|
||||||
"attachment-delete-pop": "Deleting an attachment is permanent. There is no undo.",
|
"attachment-delete-pop": "Deleting an attachment is permanent. There is no undo.",
|
||||||
"attachmentDeletePopup-title": "Delete Attachment?",
|
"attachmentDeletePopup-title": "Delete Attachment?",
|
||||||
"attachments": "Attachments",
|
"attachments": "Attachments",
|
||||||
|
"auto-watch": "Automatically watch boards when create it",
|
||||||
"avatar-too-big": "The avatar is too large (70Kb max)",
|
"avatar-too-big": "The avatar is too large (70Kb max)",
|
||||||
"back": "Back",
|
"back": "Back",
|
||||||
"board-change-color": "Change color",
|
"board-change-color": "Change color",
|
||||||
|
|
@ -56,6 +77,7 @@
|
||||||
"boardChangeColorPopup-title": "Change Board Background",
|
"boardChangeColorPopup-title": "Change Board Background",
|
||||||
"boardChangeTitlePopup-title": "Rename Board",
|
"boardChangeTitlePopup-title": "Rename Board",
|
||||||
"boardChangeVisibilityPopup-title": "Change Visibility",
|
"boardChangeVisibilityPopup-title": "Change Visibility",
|
||||||
|
"boardChangeWatchPopup-title": "Change Watch",
|
||||||
"boardImportBoardPopup-title": "Import board from Trello",
|
"boardImportBoardPopup-title": "Import board from Trello",
|
||||||
"boardMenuPopup-title": "Board Menu",
|
"boardMenuPopup-title": "Board Menu",
|
||||||
"boards": "Boards",
|
"boards": "Boards",
|
||||||
|
|
@ -121,9 +143,9 @@
|
||||||
"download": "Download",
|
"download": "Download",
|
||||||
"edit": "Edit",
|
"edit": "Edit",
|
||||||
"edit-avatar": "Change Avatar",
|
"edit-avatar": "Change Avatar",
|
||||||
"edit-profile": "Edit profile",
|
|
||||||
"edit-profile": "Edit Profile",
|
"edit-profile": "Edit Profile",
|
||||||
"editLabelPopup-title": "Change Label",
|
"editLabelPopup-title": "Change Label",
|
||||||
|
"editNotificationPopup-title": "Edit Notification",
|
||||||
"editProfilePopup-title": "Edit Profile",
|
"editProfilePopup-title": "Edit Profile",
|
||||||
"email": "Email",
|
"email": "Email",
|
||||||
"email-enrollAccount-subject": "An account created for you on __siteName__",
|
"email-enrollAccount-subject": "An account created for you on __siteName__",
|
||||||
|
|
@ -198,6 +220,8 @@
|
||||||
"moveSelectionPopup-title": "Move selection",
|
"moveSelectionPopup-title": "Move selection",
|
||||||
"multi-selection": "Multi-Selection",
|
"multi-selection": "Multi-Selection",
|
||||||
"multi-selection-on": "Multi-Selection is on",
|
"multi-selection-on": "Multi-Selection is on",
|
||||||
|
"muted": "Muted",
|
||||||
|
"muted-info": "You will never be notified of any changes in this board",
|
||||||
"my-boards": "My Boards",
|
"my-boards": "My Boards",
|
||||||
"name": "Name",
|
"name": "Name",
|
||||||
"name": "Name",
|
"name": "Name",
|
||||||
|
|
@ -207,12 +231,15 @@
|
||||||
"normal": "Normal",
|
"normal": "Normal",
|
||||||
"normal-desc": "Can view and edit cards. Can't change settings.",
|
"normal-desc": "Can view and edit cards. Can't change settings.",
|
||||||
"not-accepted-yet": "Invitation not accepted yet",
|
"not-accepted-yet": "Invitation not accepted yet",
|
||||||
|
"notify-participate": "Receive updates to any cards you participate as creater or member",
|
||||||
|
"notify-watch": "Receive updates to any boards, lists, or cards you’re watching",
|
||||||
"optional": "optional",
|
"optional": "optional",
|
||||||
"or": "or",
|
"or": "or",
|
||||||
"page-maybe-private": "This page may be private. You may be able to view it by <a href='%s'>logging in</a>.",
|
"page-maybe-private": "This page may be private. You may be able to view it by <a href='%s'>logging in</a>.",
|
||||||
"page-not-found": "Page not found.",
|
"page-not-found": "Page not found.",
|
||||||
"password": "Password",
|
"password": "Password",
|
||||||
"paste-or-dragdrop": "to paste, or drag & drop image file to it (image only)",
|
"paste-or-dragdrop": "to paste, or drag & drop image file to it (image only)",
|
||||||
|
"participating": "Participating",
|
||||||
"preview": "Preview",
|
"preview": "Preview",
|
||||||
"previewAttachedImagePopup-title": "Preview",
|
"previewAttachedImagePopup-title": "Preview",
|
||||||
"previewClipboardImagePopup-title": "Preview",
|
"previewClipboardImagePopup-title": "Preview",
|
||||||
|
|
@ -253,13 +280,19 @@
|
||||||
"this-board": "this board",
|
"this-board": "this board",
|
||||||
"this-card": "this card",
|
"this-card": "this card",
|
||||||
"title": "Title",
|
"title": "Title",
|
||||||
|
"tracking": "Tracking",
|
||||||
|
"tracking-info": "You will be notified of any changes to those cards you are involved as creator or member.",
|
||||||
"unassign-member": "Unassign member",
|
"unassign-member": "Unassign member",
|
||||||
"unsaved-description": "You have an unsaved description.",
|
"unsaved-description": "You have an unsaved description.",
|
||||||
|
"unwatch": "Unwatch",
|
||||||
"upload": "Upload",
|
"upload": "Upload",
|
||||||
"upload-avatar": "Upload an avatar",
|
"upload-avatar": "Upload an avatar",
|
||||||
"uploaded-avatar": "Uploaded an avatar",
|
"uploaded-avatar": "Uploaded an avatar",
|
||||||
"username": "Username",
|
"username": "Username",
|
||||||
"view-it": "View it",
|
"view-it": "View it",
|
||||||
"warn-list-archived": "warning: this card is in an archived list",
|
"warn-list-archived": "warning: this card is in an archived list",
|
||||||
|
"watch": "Watch",
|
||||||
|
"watching": "Watching",
|
||||||
|
"watching-info": "You will be notified of any change in this board",
|
||||||
"what-to-do": "What do you want to do?"
|
"what-to-do": "What do you want to do?"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -48,4 +48,77 @@ if (Meteor.isServer) {
|
||||||
createdAt: -1,
|
createdAt: -1,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Activities.after.insert((userId, doc) => {
|
||||||
|
const activity = Activities.findOne(doc._id);
|
||||||
|
let participants = [];
|
||||||
|
let watchers = [];
|
||||||
|
let title = 'Wekan Notification';
|
||||||
|
let board = null;
|
||||||
|
const description = `act-${activity.activityType}`;
|
||||||
|
const params = {
|
||||||
|
activityId: activity._id,
|
||||||
|
};
|
||||||
|
if (activity.userId) {
|
||||||
|
// No need send notification to user of activity
|
||||||
|
// participants = _.union(participants, [activity.userId]);
|
||||||
|
params.user = activity.user().getName();
|
||||||
|
}
|
||||||
|
if (activity.boardId) {
|
||||||
|
board = activity.board();
|
||||||
|
params.board = board.title;
|
||||||
|
title = 'act-withBoardTitle';
|
||||||
|
params.url = board.absoluteUrl();
|
||||||
|
}
|
||||||
|
if (activity.memberId) {
|
||||||
|
participants = _.union(participants, [activity.memberId]);
|
||||||
|
params.member = activity.member().getName();
|
||||||
|
}
|
||||||
|
if (activity.listId) {
|
||||||
|
const list = activity.list();
|
||||||
|
watchers = _.union(watchers, list.watchers || []);
|
||||||
|
params.list = list.title;
|
||||||
|
}
|
||||||
|
if (activity.oldListId) {
|
||||||
|
const oldList = activity.oldList();
|
||||||
|
watchers = _.union(watchers, oldList.watchers || []);
|
||||||
|
params.oldList = oldList.title;
|
||||||
|
}
|
||||||
|
if (activity.cardId) {
|
||||||
|
const card = activity.card();
|
||||||
|
participants = _.union(participants, [card.userId], card.members || []);
|
||||||
|
watchers = _.union(watchers, card.watchers || []);
|
||||||
|
params.card = card.title;
|
||||||
|
title = 'act-withCardTitle';
|
||||||
|
params.url = card.absoluteUrl();
|
||||||
|
}
|
||||||
|
if (activity.commentId) {
|
||||||
|
const comment = activity.comment();
|
||||||
|
params.comment = comment.text;
|
||||||
|
}
|
||||||
|
if (activity.attachmentId) {
|
||||||
|
const attachment = activity.attachment();
|
||||||
|
params.attachment = attachment._id;
|
||||||
|
}
|
||||||
|
if (board) {
|
||||||
|
const boardWatching = _.pluck(_.where(board.watchers, {level: 'watching'}), 'userId');
|
||||||
|
const boardTracking = _.pluck(_.where(board.watchers, {level: 'tracking'}), 'userId');
|
||||||
|
const boardMuted = _.pluck(_.where(board.watchers, {level: 'muted'}), 'userId');
|
||||||
|
switch(board.getWatchDefault()) {
|
||||||
|
case 'muted':
|
||||||
|
participants = _.intersection(participants, boardTracking);
|
||||||
|
watchers = _.intersection(watchers, boardTracking);
|
||||||
|
break;
|
||||||
|
case 'tracking':
|
||||||
|
participants = _.difference(participants, boardMuted);
|
||||||
|
watchers = _.difference(watchers, boardMuted);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
watchers = _.union(watchers, boardWatching || []);
|
||||||
|
}
|
||||||
|
|
||||||
|
Notifications.getUsers(participants, watchers).forEach((user) => {
|
||||||
|
Notifications.notify(user, title, description, params);
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -151,7 +151,7 @@ Boards.helpers({
|
||||||
},
|
},
|
||||||
|
|
||||||
absoluteUrl() {
|
absoluteUrl() {
|
||||||
return FlowRouter.path('board', { id: this._id, slug: this.slug });
|
return FlowRouter.url('board', { id: this._id, slug: this.slug });
|
||||||
},
|
},
|
||||||
|
|
||||||
colorClass() {
|
colorClass() {
|
||||||
|
|
|
||||||
|
|
@ -116,16 +116,12 @@ Cards.helpers({
|
||||||
|
|
||||||
absoluteUrl() {
|
absoluteUrl() {
|
||||||
const board = this.board();
|
const board = this.board();
|
||||||
return FlowRouter.path('card', {
|
return FlowRouter.url('card', {
|
||||||
boardId: board._id,
|
boardId: board._id,
|
||||||
slug: board.slug,
|
slug: board.slug,
|
||||||
cardId: this._id,
|
cardId: this._id,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
rootUrl() {
|
|
||||||
return Meteor.absoluteUrl(this.absoluteUrl().replace('/', ''));
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
Cards.mutations({
|
Cards.mutations({
|
||||||
|
|
|
||||||
|
|
@ -47,6 +47,21 @@ Users.helpers({
|
||||||
return _.contains(invitedBoards, boardId);
|
return _.contains(invitedBoards, boardId);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
hasTag(tag) {
|
||||||
|
const {tags = []} = this.profile;
|
||||||
|
return _.contains(tags, tag);
|
||||||
|
},
|
||||||
|
|
||||||
|
hasNotification(activityId) {
|
||||||
|
const {notifications = []} = this.profile;
|
||||||
|
return _.contains(notifications, activityId);
|
||||||
|
},
|
||||||
|
|
||||||
|
getEmailCache() {
|
||||||
|
const {emailCache = []} = this.profile;
|
||||||
|
return emailCache;
|
||||||
|
},
|
||||||
|
|
||||||
getInitials() {
|
getInitials() {
|
||||||
const profile = this.profile || {};
|
const profile = this.profile || {};
|
||||||
if (profile.initials)
|
if (profile.initials)
|
||||||
|
|
@ -99,6 +114,61 @@ Users.mutations({
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
addTag(tag) {
|
||||||
|
return {
|
||||||
|
$addToSet: {
|
||||||
|
'profile.tags': tag,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
removeTag(tag) {
|
||||||
|
return {
|
||||||
|
$pull: {
|
||||||
|
'profile.tags': tag,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
toggleTag(tag) {
|
||||||
|
if (this.hasTag(tag))
|
||||||
|
this.removeTag(tag);
|
||||||
|
else
|
||||||
|
this.addTag(tag);
|
||||||
|
},
|
||||||
|
|
||||||
|
addNotification(activityId) {
|
||||||
|
return {
|
||||||
|
$addToSet: {
|
||||||
|
'profile.notifications': activityId,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
removeNotification(activityId) {
|
||||||
|
return {
|
||||||
|
$pull: {
|
||||||
|
'profile.notifications': activityId,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
addEmailCache(text) {
|
||||||
|
return {
|
||||||
|
$addToSet: {
|
||||||
|
'profile.emailCache': text,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
clearEmailCache() {
|
||||||
|
return {
|
||||||
|
$set: {
|
||||||
|
'profile.emailCache': [],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
setAvatarUrl(avatarUrl) {
|
setAvatarUrl(avatarUrl) {
|
||||||
return { $set: { 'profile.avatarUrl': avatarUrl }};
|
return { $set: { 'profile.avatarUrl': avatarUrl }};
|
||||||
},
|
},
|
||||||
|
|
@ -167,21 +237,18 @@ if (Meteor.isServer) {
|
||||||
user.addInvite(boardId);
|
user.addInvite(boardId);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { _id, slug } = board;
|
const params = {
|
||||||
const boardUrl = FlowRouter.url('board', { id: _id, slug });
|
|
||||||
|
|
||||||
const vars = {
|
|
||||||
user: user.username,
|
user: user.username,
|
||||||
inviter: inviter.username,
|
inviter: inviter.username,
|
||||||
board: board.title,
|
board: board.title,
|
||||||
url: boardUrl,
|
url: board.absoluteUrl(),
|
||||||
};
|
};
|
||||||
const lang = user.getLanguage();
|
const lang = user.getLanguage();
|
||||||
Email.send({
|
Email.send({
|
||||||
to: user.emails[0].address,
|
to: user.emails[0].address,
|
||||||
from: Accounts.emailTemplates.from,
|
from: Accounts.emailTemplates.from,
|
||||||
subject: TAPi18n.__('email-invite-subject', vars, lang),
|
subject: TAPi18n.__('email-invite-subject', params, lang),
|
||||||
text: TAPi18n.__('email-invite-text', vars, lang),
|
text: TAPi18n.__('email-invite-text', params, lang),
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
throw new Meteor.Error('email-fail', e.message);
|
throw new Meteor.Error('email-fail', e.message);
|
||||||
|
|
|
||||||
89
models/watchable.js
Normal file
89
models/watchable.js
Normal file
|
|
@ -0,0 +1,89 @@
|
||||||
|
// simple version, only toggle watch / unwatch
|
||||||
|
const simpleWatchable = (collection) => {
|
||||||
|
collection.attachSchema({
|
||||||
|
watchers: {
|
||||||
|
type: [String],
|
||||||
|
optional: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
collection.helpers({
|
||||||
|
getWatchLevels() {
|
||||||
|
return [true, false];
|
||||||
|
},
|
||||||
|
|
||||||
|
watcherIndex(userId) {
|
||||||
|
return this.watchers.indexOf(userId);
|
||||||
|
},
|
||||||
|
|
||||||
|
findWatcher(userId) {
|
||||||
|
return _.contains(this.watchers, userId);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
collection.mutations({
|
||||||
|
setWatcher(userId, level) {
|
||||||
|
// if level undefined or null or false, then remove
|
||||||
|
if (!level) return { $pull: { watchers: userId }};
|
||||||
|
return { $addToSet: { watchers: userId }};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// more complex version of same interface, with 3 watching levels
|
||||||
|
const complexWatchOptions = ['watching', 'tracking', 'muted'];
|
||||||
|
const complexWatchDefault = 'muted';
|
||||||
|
|
||||||
|
const complexWatchable = (collection) => {
|
||||||
|
collection.attachSchema({
|
||||||
|
'watchers.$.userId': {
|
||||||
|
type: String,
|
||||||
|
},
|
||||||
|
'watchers.$.level': {
|
||||||
|
type: String,
|
||||||
|
allowedValues: complexWatchOptions,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
collection.helpers({
|
||||||
|
getWatchOptions() {
|
||||||
|
return complexWatchOptions;
|
||||||
|
},
|
||||||
|
|
||||||
|
getWatchDefault() {
|
||||||
|
return complexWatchDefault;
|
||||||
|
},
|
||||||
|
|
||||||
|
watcherIndex(userId) {
|
||||||
|
return _.pluck(this.watchers, 'userId').indexOf(userId);
|
||||||
|
},
|
||||||
|
|
||||||
|
findWatcher(userId) {
|
||||||
|
return _.findWhere(this.watchers, { userId });
|
||||||
|
},
|
||||||
|
|
||||||
|
getWatchLevel(userId) {
|
||||||
|
const watcher = this.findWatcher(userId);
|
||||||
|
return watcher ? watcher.level : complexWatchDefault;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
collection.mutations({
|
||||||
|
setWatcher(userId, level) {
|
||||||
|
// if level undefined or null or false, then remove
|
||||||
|
if (level === complexWatchDefault) level = null;
|
||||||
|
if (!level) return { $pull: { watchers: { userId }}};
|
||||||
|
const index = this.watcherIndex(userId);
|
||||||
|
if (index<0) return { $push: { watchers: { userId, level }}};
|
||||||
|
return {
|
||||||
|
$set: {
|
||||||
|
[`watchers.${index}.level`]: level,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
complexWatchable(Boards);
|
||||||
|
simpleWatchable(Lists);
|
||||||
|
simpleWatchable(Cards);
|
||||||
35
server/notifications/email.js
Normal file
35
server/notifications/email.js
Normal file
|
|
@ -0,0 +1,35 @@
|
||||||
|
// cache the email text in a queue, and send them in a batch
|
||||||
|
Meteor.startup(() => {
|
||||||
|
Notifications.subscribe('cachedEmail', (user, title, description, params) => {
|
||||||
|
// add quote to make titles easier to read in email text
|
||||||
|
const quoteParams = _.clone(params);
|
||||||
|
['card', 'list', 'oldList', 'board', 'comment'].forEach((key) => {
|
||||||
|
if (quoteParams[key]) quoteParams[key] = `"${params[key]}"`;
|
||||||
|
});
|
||||||
|
|
||||||
|
const text = `${params.user} ${TAPi18n.__(description, quoteParams, user.getLanguage())}\n${params.url}`;
|
||||||
|
user.addEmailCache(text);
|
||||||
|
|
||||||
|
const userId = user._id;
|
||||||
|
Meteor.setTimeout(() => {
|
||||||
|
const user = Users.findOne(userId);
|
||||||
|
|
||||||
|
const emailCache = user.getEmailCache();
|
||||||
|
if (emailCache.length === 0) return;
|
||||||
|
|
||||||
|
const text = emailCache.join('\n\n');
|
||||||
|
user.clearEmailCache();
|
||||||
|
|
||||||
|
try {
|
||||||
|
Email.send({
|
||||||
|
to: user.emails[0].address,
|
||||||
|
from: Accounts.emailTemplates.from,
|
||||||
|
subject : TAPi18n.__('act-activity-notify', {}, user.getLanguage()),
|
||||||
|
text,
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}, 30000, user._id);
|
||||||
|
});
|
||||||
|
});
|
||||||
48
server/notifications/notifications.js
Normal file
48
server/notifications/notifications.js
Normal file
|
|
@ -0,0 +1,48 @@
|
||||||
|
// a map of notification service, like email, web, IM, qq, etc.
|
||||||
|
|
||||||
|
// serviceName -> callback(user, title, description, params)
|
||||||
|
// expected arguments to callback:
|
||||||
|
// - user: Meteor user object
|
||||||
|
// - title: String, TAPi18n key
|
||||||
|
// - description, String, TAPi18n key
|
||||||
|
// - params: Object, values extracted from context, to used for above two TAPi18n keys
|
||||||
|
// see example call to Notifications.notify() in models/activities.js
|
||||||
|
const notifyServices = {};
|
||||||
|
|
||||||
|
Notifications = {
|
||||||
|
subscribe: (serviceName, callback) => {
|
||||||
|
notifyServices[serviceName] = callback;
|
||||||
|
},
|
||||||
|
|
||||||
|
unsubscribe: (serviceName) => {
|
||||||
|
if (typeof notifyServices[serviceName] === 'function')
|
||||||
|
delete notifyServices[serviceName];
|
||||||
|
},
|
||||||
|
|
||||||
|
// filter recipients according to user settings for notification
|
||||||
|
getUsers: (participants, watchers) => {
|
||||||
|
const userMap = {};
|
||||||
|
participants.forEach((userId) => {
|
||||||
|
if (userMap[userId]) return;
|
||||||
|
const user = Users.findOne(userId);
|
||||||
|
if (user && user.hasTag('notify-participate')) {
|
||||||
|
userMap[userId] = user;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
watchers.forEach((userId) => {
|
||||||
|
if (userMap[userId]) return;
|
||||||
|
const user = Users.findOne(userId);
|
||||||
|
if (user && user.hasTag('notify-watch')) {
|
||||||
|
userMap[userId] = user;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return _.map(userMap, (v) => v);
|
||||||
|
},
|
||||||
|
|
||||||
|
notify: (user, title, description, params) => {
|
||||||
|
for(const k in notifyServices) {
|
||||||
|
const notifyImpl = notifyServices[k];
|
||||||
|
if (notifyImpl && typeof notifyImpl === 'function') notifyImpl(user, title, description, params);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
9
server/notifications/profile.js
Normal file
9
server/notifications/profile.js
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
Meteor.startup(() => {
|
||||||
|
// XXX: add activity id to profile.notifications,
|
||||||
|
// it can be displayed and rendered on web or mobile UI
|
||||||
|
// will uncomment the following code once UI implemented
|
||||||
|
//
|
||||||
|
// Notifications.subscribe('profile', (user, title, description, params) => {
|
||||||
|
// user.addNotification(params.activityId);
|
||||||
|
// });
|
||||||
|
});
|
||||||
36
server/notifications/watch.js
Normal file
36
server/notifications/watch.js
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
Meteor.methods({
|
||||||
|
watch(watchableType, id, level) {
|
||||||
|
check(watchableType, String);
|
||||||
|
check(id, String);
|
||||||
|
check(level, Match.OneOf(String, null));
|
||||||
|
|
||||||
|
const userId = Meteor.userId();
|
||||||
|
|
||||||
|
let watchableObj = null;
|
||||||
|
let board = null;
|
||||||
|
if (watchableType === 'board') {
|
||||||
|
watchableObj = Boards.findOne(id);
|
||||||
|
if (!watchableObj) throw new Meteor.Error('error-board-doesNotExist');
|
||||||
|
board = watchableObj;
|
||||||
|
|
||||||
|
} else if (watchableType === 'list') {
|
||||||
|
watchableObj = Lists.findOne(id);
|
||||||
|
if (!watchableObj) throw new Meteor.Error('error-list-doesNotExist');
|
||||||
|
board = watchableObj.board();
|
||||||
|
|
||||||
|
} else if (watchableType === 'card') {
|
||||||
|
watchableObj = Cards.findOne(id);
|
||||||
|
if (!watchableObj) throw new Meteor.Error('error-card-doesNotExist');
|
||||||
|
board = watchableObj.board();
|
||||||
|
|
||||||
|
} else {
|
||||||
|
throw new Meteor.Error('error-json-schema');
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((board.permission === 'private') && !board.hasMember(userId))
|
||||||
|
throw new Meteor.Error('error-board-notAMember');
|
||||||
|
|
||||||
|
watchableObj.setWatcher(userId, level);
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
});
|
||||||
Loading…
Add table
Add a link
Reference in a new issue