summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--README.md51
-rw-r--r--client/components/sidebar/sidebar.jade9
-rw-r--r--client/components/sidebar/sidebar.js3
-rw-r--r--client/components/sidebar/sidebar.styl1
-rw-r--r--client/components/sidebar/sidebarFilters.jade12
-rw-r--r--client/components/users/userHeader.jade2
-rw-r--r--client/components/users/userHeader.js15
-rw-r--r--client/lib/filter.js25
-rw-r--r--client/lib/i18n.js29
-rwxr-xr-xi18n/en.i18n.json3
-rw-r--r--meta/dwrensha-pgp-sigbin0 -> 430 bytes
-rw-r--r--meta/keyringbin3357 -> 4892 bytes
-rw-r--r--models/users.js40
-rw-r--r--sandstorm-pkgdef.capnp56
-rw-r--r--sandstorm.js298
15 files changed, 457 insertions, 87 deletions
diff --git a/README.md b/README.md
index 7b3eea26..756103bd 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,11 @@
-# Wekan
+# Wekan fork
-[![Join the chat][gitter_badge]][gitter_chat]
+[![Join the chat][rocket_badge]][rocket_chat]
+[![Build Status][travis_badge]][travis_status]
+
+[Wekan fork FAQ][fork_faq]
+
+[Wekan fork announcement][fork_announcement]
Wekan is an open-source and collaborative kanban board application.
@@ -10,7 +15,7 @@ boards are an unbeatable tool to keep your things organized. They give you a
visual overview of the current state of your project, and make you productive by
allowing you to focus on the few items that matter the most.
-[![Our roadmap is self-hosted on Wekan][screenshot]][roadmap]
+[![Our roadmap is self-hosted on Wekan fork][screenshot]][roadmap]
Wekan supports most features you would expect of it including a real-time user
interface, cards comments, member assignations, customizable labels, filtered
@@ -18,31 +23,45 @@ views, and more.
Since it is a free software, you don’t have to trust us with your data and can
install Wekan on your own computer or server. In fact we encourage you to do
-that by providing one-click installation on Heroku or [Sandstorm]
-[sandstorm_market] platforms and verified [Docker][docker_image] images. You can
-also install Wekan on the following Platforms:
+that by providing one-click installation on various platforms.
+
+Currently you can install Wekan fork on the following Platforms:
+
+[Debian Wheezy 64bit][debian_wheezy]
[![Deploy][heroku_button]][heroku_deploy]
[![SignUp][indiehosters_button]][indiehosters_saas]
[![Deploy to Scalingo][scalingo_button]][scalingo_deploy]
[![Install on Cloudron][cloudron_button]][cloudron_install]
+[![Try on Sandstorm][sandstorm_button]][sandstorm_appdemo]
+
+These Platforms have not been tested yet, but are coming sometime, so links
+don't work yet:
+
+[Docker][docker_image]
-Wekan is released under the very permissive [MIT license](LICENSE), and made
+Wekan fork is released under the very permissive [MIT license](LICENSE), and made
with [Meteor](https://www.meteor.com).
-[Our roadmap is self-hosted on Wekan][roadmap]
+[Our roadmap is self-hosted on Wekan fork][roadmap]
-[screenshot]: http://i.imgur.com/cI4jW2h.png
-[gitter_badge]: https://badges.gitter.im/Join%20Chat.svg
-[gitter_chat]: https://gitter.im/wekan/wekan
-[roadmap]: http://try.wekan.io/b/MeSsFJaSqeuo9M6bs/wekan-roadmap
-[sandstorm_market]: https://oasis.sandstorm.io/appdemo/m86q05rdvj14yvn78ghaxynqz7u2svw6rnttptxx49g1785cdv1h
-[docker_image]: https://hub.docker.com/r/mquandalle/wekan/
+[fork_faq]: https://github.com/wefork/wekan/wiki/FAQ
+[fork_announcement]: https://github.com/wekan/wekan/issues/640#issuecomment-255091832
+[screenshot]: http://i.imgur.com/ShX2OTk.png
+[rocket_badge]: https://chat.indie.host/images/join-chat.svg
+[rocket_chat]: https://chat.indie.host/channel/wekan
+[roadmap]: https://wekan.indie.host/b/t2YaGmyXgNkppcFBq/wekan-fork-roadmap
+[sandstorm_button]: https://img.shields.io/badge/try-Wekan%20on%20Sandstorm-783189.svg
+[sandstorm_appdemo]: https://demo.sandstorm.io/appdemo/m86q05rdvj14yvn78ghaxynqz7u2svw6rnttptxx49g1785cdv1h
+[docker_image]: https://hub.docker.com/r/...
[heroku_button]: https://www.herokucdn.com/deploy/button.png
-[heroku_deploy]: https://heroku.com/deploy?template=https://github.com/wekan/wekan/tree/master
+[heroku_deploy]: https://heroku.com/deploy?template=https://github.com/wefork/wekan/tree/master
[indiehosters_button]: https://indie.host/signup.png
[indiehosters_saas]: https://indiehosters.net/shop/product/wekan-20
[scalingo_button]: https://cdn.scalingo.com/deploy/button.svg
-[scalingo_deploy]: https://my.scalingo.com/deploy?source=https://github.com/wekan/wekan#master
+[scalingo_deploy]: https://my.scalingo.com/deploy?source=https://github.com/wefork/wekan#master
[cloudron_button]: https://cloudron.io/img/button.svg
[cloudron_install]: https://cloudron.io/button.html?app=io.wekan.cloudronapp
+[debian_wheezy]: https://github.com/soohwa/sps/blob/master/example/docs/1/wekan.md
+[travis_badge]: https://travis-ci.org/wefork/wekan.svg?branch=devel
+[travis_status]: https://travis-ci.org/wefork/wekan
diff --git a/client/components/sidebar/sidebar.jade b/client/components/sidebar/sidebar.jade
index 4f5586cb..f3fdd1bc 100644
--- a/client/components/sidebar/sidebar.jade
+++ b/client/components/sidebar/sidebar.jade
@@ -30,10 +30,13 @@ template(name="membersWidget")
.board-widget-content
each currentBoard.activeMembers
+userAvatar(userId=this.userId showStatus=true)
- unless isSandstorm
- if currentUser.isBoardAdmin
- a.member.add-member.js-manage-board-members
+ if isSandstorm
+ if currentUser.isBoardMember
+ a.member.add-member.sandstorm-powerbox-request-identity
i.fa.fa-plus
+ else if currentUser.isBoardAdmin
+ a.member.add-member.js-manage-board-members
+ i.fa.fa-plus
.clearfix
if isInvited
hr
diff --git a/client/components/sidebar/sidebar.js b/client/components/sidebar/sidebar.js
index 0af32f8f..f32a27c5 100644
--- a/client/components/sidebar/sidebar.js
+++ b/client/components/sidebar/sidebar.js
@@ -163,6 +163,9 @@ Template.membersWidget.helpers({
Template.membersWidget.events({
'click .js-member': Popup.open('member'),
'click .js-manage-board-members': Popup.open('addMember'),
+ 'click .sandstorm-powerbox-request-identity'() {
+ window.sandstormRequestIdentity();
+ },
'click .js-member-invite-accept'() {
const boardId = Session.get('currentBoard');
Meteor.user().removeInvite(boardId);
diff --git a/client/components/sidebar/sidebar.styl b/client/components/sidebar/sidebar.styl
index 9007af59..24abe990 100644
--- a/client/components/sidebar/sidebar.styl
+++ b/client/components/sidebar/sidebar.styl
@@ -51,6 +51,7 @@
.member, .card-label
margin-right: 7px
+ margin-top: 5px
.sidebar-list-item-description
flex: 1
diff --git a/client/components/sidebar/sidebarFilters.jade b/client/components/sidebar/sidebarFilters.jade
index 2b326493..deefde82 100644
--- a/client/components/sidebar/sidebarFilters.jade
+++ b/client/components/sidebar/sidebarFilters.jade
@@ -5,6 +5,12 @@
template(name="filterSidebar")
ul.sidebar-list
+ li(class="{{#if Filter.labelIds.isSelected undefined}}active{{/if}}")
+ a.name.js-toggle-label-filter
+ span.sidebar-list-item-description
+ {{_ 'filter-no-label'}}
+ if Filter.labelIds.isSelected undefined
+ i.fa.fa-check
each currentBoard.labels
li
a.name.js-toggle-label-filter
@@ -18,6 +24,12 @@ template(name="filterSidebar")
i.fa.fa-check
hr
ul.sidebar-list
+ li(class="{{#if Filter.members.isSelected undefined}}active{{/if}}")
+ a.name.js-toggle-member-filter
+ span.sidebar-list-item-description
+ {{_ 'filter-no-member'}}
+ if Filter.members.isSelected undefined
+ i.fa.fa-check
each currentBoard.activeMembers
with getUser userId
li(class="{{#if Filter.members.isSelected _id}}active{{/if}}")
diff --git a/client/components/users/userHeader.jade b/client/components/users/userHeader.jade
index a71e7fc7..ce8cf1af 100644
--- a/client/components/users/userHeader.jade
+++ b/client/components/users/userHeader.jade
@@ -27,6 +27,8 @@ template(name="editProfilePopup")
input.js-profile-fullname(type="text" value=profile.fullname autofocus)
label
| {{_ 'username'}}
+ span.error.hide.username-taken
+ | {{_ 'error-username-taken'}}
input.js-profile-username(type="text" value=username)
label
| {{_ 'initials'}}
diff --git a/client/components/users/userHeader.js b/client/components/users/userHeader.js
index 10fdf699..17d9eb5f 100644
--- a/client/components/users/userHeader.js
+++ b/client/components/users/userHeader.js
@@ -26,11 +26,18 @@ Template.editProfilePopup.events({
'profile.fullname': fullname,
'profile.initials': initials,
}});
- // XXX We should report the error to the user.
+
if (username !== Meteor.user().username) {
- Meteor.call('setUsername', username);
- }
- Popup.back();
+ Meteor.call('setUsername', username, function(error) {
+ const messageElement = tpl.$('.username-taken');
+ if (error) {
+ messageElement.show();
+ } else {
+ messageElement.hide();
+ Popup.back();
+ }
+ });
+ } else Popup.back();
},
});
diff --git a/client/lib/filter.js b/client/lib/filter.js
index 74305284..8129776b 100644
--- a/client/lib/filter.js
+++ b/client/lib/filter.js
@@ -63,6 +63,17 @@ class SetFilter {
this._dep.depend();
return { $in: this._selectedElements };
}
+
+ _getEmptySelector() {
+ this._dep.depend();
+ let includeEmpty = false;
+ this._selectedElements.forEach((el) => {
+ if (el === undefined) {
+ includeEmpty = true;
+ }
+ });
+ return includeEmpty ? { $eq: [] } : null;
+ }
}
// The global Filter object.
@@ -95,16 +106,26 @@ Filter = {
return {};
const filterSelector = {};
+ const emptySelector = {};
+ let includeEmptySelectors = false;
this._fields.forEach((fieldName) => {
const filter = this[fieldName];
- if (filter._isActive())
+ if (filter._isActive()) {
filterSelector[fieldName] = filter._getMongoSelector();
+ emptySelector[fieldName] = filter._getEmptySelector();
+ if (emptySelector[fieldName] !== null) {
+ includeEmptySelectors = true;
+ }
+ }
});
const exceptionsSelector = {_id: {$in: this._exceptions}};
this._exceptionsDep.depend();
- return {$or: [filterSelector, exceptionsSelector]};
+ if (includeEmptySelectors)
+ return {$or: [filterSelector, exceptionsSelector, emptySelector]};
+ else
+ return {$or: [filterSelector, exceptionsSelector]};
},
mongoSelector(additionalSelector) {
diff --git a/client/lib/i18n.js b/client/lib/i18n.js
index a03fb398..34a789e1 100644
--- a/client/lib/i18n.js
+++ b/client/lib/i18n.js
@@ -2,20 +2,19 @@
// the language reactively. If the user is not connected we use the language
// information provided by the browser, and default to english.
-Tracker.autorun(() => {
- const currentUser = Meteor.user();
- let language;
- if (currentUser) {
- language = currentUser.profile && currentUser.profile.language;
- } else {
- language = navigator.language || navigator.userLanguage;
- }
+Meteor.startup(() => {
+ Tracker.autorun(() => {
+ const currentUser = Meteor.user();
+ let language;
+ if (currentUser && currentUser.profile && currentUser.profile.language) {
+ language = currentUser.profile.language;
+ } else {
+ language = navigator.language || navigator.userLanguage;
+ }
- if (language) {
- TAPi18n.setLanguage(language);
-
- // XXX
- const shortLanguage = language.split('-')[0];
- T9n.setLanguage(shortLanguage);
- }
+ if (language) {
+ TAPi18n.setLanguage(language);
+ T9n.setLanguage(language);
+ }
+ });
});
diff --git a/i18n/en.i18n.json b/i18n/en.i18n.json
index f9cf7e82..54e6b394 100755
--- a/i18n/en.i18n.json
+++ b/i18n/en.i18n.json
@@ -169,10 +169,13 @@
"error-user-doesNotExist": "This user does not exist",
"error-user-notAllowSelf": "This action on self is not allowed",
"error-user-notCreated": "This user is not created",
+ "error-username-taken": "This username is already taken",
"export-board": "Export board",
"filter": "Filter",
"filter-cards": "Filter Cards",
"filter-clear": "Clear filter",
+ "filter-no-label": "No label",
+ "filter-no-member": "No member",
"filter-on": "Filter is on",
"filter-on-desc": "You are filtering cards on this board. Click here to edit filter.",
"filter-to-selection": "Filter to selection",
diff --git a/meta/dwrensha-pgp-sig b/meta/dwrensha-pgp-sig
new file mode 100644
index 00000000..0ee50fa4
--- /dev/null
+++ b/meta/dwrensha-pgp-sig
Binary files differ
diff --git a/meta/keyring b/meta/keyring
index e34b146d..b4d58533 100644
--- a/meta/keyring
+++ b/meta/keyring
Binary files differ
diff --git a/models/users.js b/models/users.js
index 790ee0a1..bdc5ddfe 100644
--- a/models/users.js
+++ b/models/users.js
@@ -1,3 +1,7 @@
+// Sandstorm context is detected using the METEOR_SETTINGS environment variable
+// in the package definition.
+const isSandstorm = Meteor.settings && Meteor.settings.public &&
+ Meteor.settings.public.sandstorm;
Users = Meteor.users;
Users.attachSchema(new SimpleSchema({
@@ -394,24 +398,26 @@ if (Meteor.isServer) {
return fakeUserId.get() || getUserId();
};
- Users.after.insert((userId, doc) => {
- const fakeUser = {
- extendAutoValueContext: {
- userId: doc._id,
- },
- };
-
- fakeUserId.withValue(doc._id, () => {
- // Insert the Welcome Board
- Boards.insert({
- title: TAPi18n.__('welcome-board'),
- permission: 'private',
- }, fakeUser, (err, boardId) => {
-
- ['welcome-list1', 'welcome-list2'].forEach((title) => {
- Lists.insert({ title: TAPi18n.__(title), boardId }, fakeUser);
+ if (!isSandstorm) {
+ Users.after.insert((userId, doc) => {
+ const fakeUser = {
+ extendAutoValueContext: {
+ userId: doc._id,
+ },
+ };
+
+ fakeUserId.withValue(doc._id, () => {
+ // Insert the Welcome Board
+ Boards.insert({
+ title: TAPi18n.__('welcome-board'),
+ permission: 'private',
+ }, fakeUser, (err, boardId) => {
+
+ ['welcome-list1', 'welcome-list2'].forEach((title) => {
+ Lists.insert({ title: TAPi18n.__(title), boardId }, fakeUser);
+ });
});
});
});
- });
+ }
}
diff --git a/sandstorm-pkgdef.capnp b/sandstorm-pkgdef.capnp
index d9e7da3d..5c0961f8 100644
--- a/sandstorm-pkgdef.capnp
+++ b/sandstorm-pkgdef.capnp
@@ -22,10 +22,10 @@ const pkgdef :Spk.PackageDefinition = (
appTitle = (defaultText = "Wekan"),
# The name of the app as it is displayed to the user.
- appVersion = 12,
+ appVersion = 16,
# Increment this for every release.
- appMarketingVersion = (defaultText = "0.11.0-rc2"),
+ appMarketingVersion = (defaultText = "0.11.0~2016-11-15"),
# Human-readable presentation of the app version.
minUpgradableAppVersion = 0,
@@ -58,13 +58,14 @@ const pkgdef :Spk.PackageDefinition = (
),
website = "https://wekan.io",
- codeUrl = "https://github.com/wekan/wekan",
+ codeUrl = "https://github.com/wefork/wekan",
license = (openSource = mit),
categories = [productivity, office],
author = (
- contactEmail = "mquandalle@wekan.io",
- pgpSignature = embed "meta/mquandalle-pgp-sig",
+ upstreamAuthor = "Maxime Quandalle",
+ contactEmail = "david@sandstorm.io",
+ pgpSignature = embed "meta/dwrensha-pgp-sig",
),
pgpKeyring = embed "meta/keyring",
@@ -173,8 +174,48 @@ const pkgdef :Spk.PackageDefinition = (
#
# XXX Administrators configuration options aren’t implemented yet, so this
# role is currently useless.
- )]
- )
+ )],
+
+ eventTypes = [(
+ name = "addBoardMember",
+ verbPhrase = (defaultText = "added to board"),
+ ), (
+ name = "createList",
+ verbPhrase = (defaultText = "created new list"),
+ ), (
+ name = "archivedList",
+ verbPhrase = (defaultText = "archived list"),
+ ), (
+ name = "restoredList",
+ verbPhrase = (defaultText = "restored list"),
+ ), (
+ name = "createCard",
+ verbPhrase = (defaultText = "created new card"),
+ ), (
+ name = "moveCard",
+ verbPhrase = (defaultText = "moved card"),
+ ), (
+ name = "archivedCard",
+ verbPhrase = (defaultText = "archived card"),
+ ), (
+ name = "restoredCard",
+ verbPhrase = (defaultText = "restored card"),
+ ), (
+ name = "addComment",
+ verbPhrase = (defaultText = "added comment"),
+ ), (
+ name = "addAttachement",
+ verbPhrase = (defaultText = "added attachment"),
+ ), (
+ name = "joinMember",
+ verbPhrase = (defaultText = "added to card"),
+ ), (
+ name = "unjoinMember",
+ verbPhrase = (defaultText = "removed from card"),
+ ), ],
+ ),
+
+ saveIdentityCaps = true,
),
);
@@ -184,6 +225,7 @@ const myCommand :Spk.Manifest.Command = (
environ = [
# Note that this defines the *entire* environment seen by your app.
(key = "PATH", value = "/usr/local/bin:/usr/bin:/bin"),
+ (key = "SANDSTORM", value = "1"),
(key = "METEOR_SETTINGS", value = "{\"public\": {\"sandstorm\": true}}")
]
);
diff --git a/sandstorm.js b/sandstorm.js
index e7a67f76..dc5b10d6 100644
--- a/sandstorm.js
+++ b/sandstorm.js
@@ -21,6 +21,187 @@ const sandstormBoard = {
};
if (isSandstorm && Meteor.isServer) {
+ const fs = require('fs');
+ const Capnp = require('capnp');
+ const Package = Capnp.importSystem('sandstorm/package.capnp');
+ const Powerbox = Capnp.importSystem('sandstorm/powerbox.capnp');
+ const Identity = Capnp.importSystem('sandstorm/identity.capnp');
+ const SandstormHttpBridge =
+ Capnp.importSystem('sandstorm/sandstorm-http-bridge.capnp').SandstormHttpBridge;
+
+ let httpBridge = null;
+ let capnpConnection = null;
+
+ const bridgeConfig = Capnp.parse(
+ Package.BridgeConfig,
+ fs.readFileSync('/sandstorm-http-bridge-config'));
+
+ function getHttpBridge() {
+ if (!httpBridge) {
+ capnpConnection = Capnp.connect('unix:/tmp/sandstorm-api');
+ httpBridge = capnpConnection.restore(null, SandstormHttpBridge);
+ }
+ return httpBridge;
+ }
+
+ Meteor.methods({
+ sandstormClaimIdentityRequest(token, descriptor) {
+ check(token, String);
+ check(descriptor, String);
+
+ const parsedDescriptor = Capnp.parse(
+ Powerbox.PowerboxDescriptor,
+ new Buffer(descriptor, 'base64'),
+ { packed: true });
+
+ const tag = Capnp.parse(Identity.Identity.PowerboxTag, parsedDescriptor.tags[0].value);
+ const permissions = [];
+ if (tag.permissions[1]) {
+ permissions.push('configure');
+ }
+
+ if (tag.permissions[0]) {
+ permissions.push('participate');
+ }
+
+ const sessionId = this.connection.sandstormSessionId();
+ const httpBridge = getHttpBridge();
+ const session = httpBridge.getSessionContext(sessionId).context;
+ const api = httpBridge.getSandstormApi(sessionId).api;
+
+ Meteor.wrapAsync((done) => {
+ session.claimRequest(token).then((response) => {
+ const identity = response.cap.castAs(Identity.Identity);
+ const promises = [api.getIdentityId(identity), identity.getProfile(),
+ httpBridge.saveIdentity(identity)];
+ return Promise.all(promises).then((responses) => {
+ const identityId = responses[0].id.toString('hex').slice(0, 32);
+ const profile = responses[1].profile;
+ return profile.picture.getUrl().then((response) => {
+ const sandstormInfo = {
+ id: identityId,
+ name: profile.displayName.defaultText,
+ permissions,
+ picture: `${response.protocol}://${response.hostPath}`,
+ preferredHandle: profile.preferredHandle,
+ pronouns: profile.pronouns,
+ };
+
+ const login = Accounts.updateOrCreateUserFromExternalService(
+ 'sandstorm', sandstormInfo,
+ { profile: { name: sandstormInfo.name } });
+
+ updateUserPermissions(login.userId, permissions);
+ done();
+ });
+ });
+ }).catch((e) => {
+ done(e, null);
+ });
+ })();
+ },
+ });
+
+ function reportActivity(sessionId, path, type, users, caption) {
+ const httpBridge = getHttpBridge();
+ const session = httpBridge.getSessionContext(sessionId).context;
+ Meteor.wrapAsync((done) => {
+ return Promise.all(users.map((user) => {
+ return httpBridge.getSavedIdentity(user.id).then((response) => {
+ // Call getProfile() to make sure that the identity successfully resolves.
+ // (In C++ we would instead call whenResolved() here.)
+ const identity = response.identity;
+ return identity.getProfile().then(() => {
+ return { identity,
+ mentioned: !!user.mentioned,
+ subscribed: !!user.subscribed,
+ };
+ });
+ }).catch(() => {
+ // Ignore identities that fail to restore. Either they were added before we set
+ // `saveIdentityCaps` to true, or they have lost access to the board.
+ });
+ })).then((maybeUsers) => {
+ const users = maybeUsers.filter((u) => !!u);
+ const event = { path, type, users };
+ if (caption) {
+ event.notification = { caption };
+ }
+
+ return session.activity(event);
+ }).then(() => done(),
+ (e) => done(e));
+ })();
+ }
+
+ Meteor.startup(() => {
+ Activities.after.insert((userId, doc) => {
+ // HACK: We need the connection that's making the request in order to read the
+ // Sandstorm session ID.
+ const invocation = DDP._CurrentInvocation.get(); // eslint-disable-line no-undef
+ if (invocation) {
+ const sessionId = invocation.connection.sandstormSessionId();
+
+ const eventTypes = bridgeConfig.viewInfo.eventTypes;
+
+ const defIdx = eventTypes.findIndex((def) => def.name === doc.activityType );
+ if (defIdx >= 0) {
+ const users = {};
+ function ensureUserListed(userId) {
+ if (!users[userId]) {
+ const user = Meteor.users.findOne(userId);
+ if (user) {
+ users[userId] = { id: user.services.sandstorm.id };
+ } else {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ function mentionedUser(userId) {
+ if (ensureUserListed(userId)) {
+ users[userId].mentioned = true;
+ }
+ }
+
+ function subscribedUser(userId) {
+ if (ensureUserListed(userId)) {
+ users[userId].subscribed = true;
+ }
+ }
+
+ let path = '';
+ let caption = null;
+
+ if (doc.cardId) {
+ path = `b/sandstorm/libreboard/${doc.cardId}`;
+ Cards.findOne(doc.cardId).members.map(subscribedUser);
+ }
+
+ if (doc.memberId) {
+ mentionedUser(doc.memberId);
+ }
+
+ if (doc.activityType === 'addComment') {
+ const comment = CardComments.findOne(doc.commentId);
+ caption = { defaultText: comment.text };
+ const activeMembers =
+ _.pluck(Boards.findOne(sandstormBoard._id).activeMembers(), 'userId');
+ (comment.text.match(/\B@(\w*)/g) || []).forEach((username) => {
+ const user = Meteor.users.findOne({ username: username.slice(1)});
+ if (user && activeMembers.indexOf(user._id) !== -1) {
+ mentionedUser(user._id);
+ }
+ });
+ }
+
+ reportActivity(sessionId, path, defIdx, _.values(users), caption);
+ }
+ }
+ });
+ });
+
function updateUserPermissions(userId, permissions) {
const isActive = permissions.indexOf('participate') > -1;
const isAdmin = permissions.indexOf('configure') > -1;
@@ -58,29 +239,6 @@ if (isSandstorm && Meteor.isServer) {
Location: base + boardPath,
});
res.end();
-
- // `accounts-sandstorm` populate the Users collection when new users
- // accesses the document, but in case a already known user comes back, we
- // need to update his associated document to match the request HTTP headers
- // informations.
- // XXX We need to update this document even if the initial route is not `/`.
- // Unfortuanlty I wasn't able to make the Webapp.rawConnectHandlers solution
- // work.
- const user = Users.findOne({
- 'services.sandstorm.id': req.headers['x-sandstorm-user-id'],
- });
- if (user) {
- // XXX At this point the user.services.sandstorm credentials haven't been
- // updated, which mean that the user will have to restart the application
- // a second time to see its updated name and avatar.
- Users.update(user._id, {
- $set: {
- 'profile.fullname': user.services.sandstorm.name,
- 'profile.avatarUrl': user.services.sandstorm.picture,
- },
- });
- updateUserPermissions(user._id, user.services.sandstorm.permissions);
- }
});
// On the first launch of the instance a user is automatically created thanks
@@ -126,6 +284,29 @@ if (isSandstorm && Meteor.isServer) {
updateUserPermissions(doc._id, doc.services.sandstorm.permissions);
});
+ Meteor.startup(() => {
+ Users.find().observeChanges({
+ changed(userId, fields) {
+ const sandstormData = (fields.services || {}).sandstorm || {};
+ if (sandstormData.name) {
+ Users.update(userId, {
+ $set: { 'profile.fullname': sandstormData.name },
+ });
+ }
+
+ if (sandstormData.picture) {
+ Users.update(userId, {
+ $set: { 'profile.avatarUrl': sandstormData.picture },
+ });
+ }
+
+ if (sandstormData.permissions) {
+ updateUserPermissions(userId, sandstormData.permissions);
+ }
+ },
+ });
+ });
+
// Wekan v0.8 didn’t implement the Sandstorm sharing model and instead kept
// the visibility setting (“public” or “private”) in the UI as does the main
// Meteor application. We need to enforce “public” visibility as the sharing
@@ -137,6 +318,77 @@ if (isSandstorm && Meteor.isServer) {
}
if (isSandstorm && Meteor.isClient) {
+ let rpcCounter = 0;
+ const rpcs = {};
+
+ window.addEventListener('message', (event) => {
+ if (event.source === window) {
+ // Meteor likes to postmessage itself.
+ return;
+ }
+
+ if ((event.source !== window.parent) ||
+ typeof event.data !== 'object' ||
+ typeof event.data.rpcId !== 'number') {
+ throw new Error(`got unexpected postMessage: ${event}`);
+ }
+
+ const handler = rpcs[event.data.rpcId];
+ if (!handler) {
+ throw new Error(`no such rpc ID for event ${event}`);
+ }
+
+ delete rpcs[event.data.rpcId];
+ handler(event.data);
+ });
+
+ function sendRpc(name, message) {
+ const id = rpcCounter++;
+ message.rpcId = id;
+ const obj = {};
+ obj[name] = message;
+ window.parent.postMessage(obj, '*');
+ return new Promise((resolve, reject) => {
+ rpcs[id] = (response) => {
+ if (response.error) {
+ reject(new Error(response.error));
+ } else {
+ resolve(response);
+ }
+ };
+ });
+ }
+
+ const powerboxDescriptors = {
+ identity: 'EAhQAQEAABEBF1EEAQH_GN1RqXqYhMAAQAERAREBAQ',
+ // Generated using the following code:
+ //
+ // Capnp.serializePacked(
+ // Powerbox.PowerboxDescriptor,
+ // { tags: [ {
+ // id: "13872380404802116888",
+ // value: Capnp.serialize(Identity.PowerboxTag, { permissions: [true, false] })
+ // }]}).toString('base64')
+ // .replace(/\//g, "_")
+ // .replace(/\+/g, "-");
+ };
+
+ function doRequest(serializedPowerboxDescriptor, onSuccess) {
+ return sendRpc('powerboxRequest', {
+ query: [serializedPowerboxDescriptor],
+ }).then((response) => {
+ if (!response.canceled) {
+ onSuccess(response);
+ }
+ });
+ }
+
+ window.sandstormRequestIdentity = function () {
+ doRequest(powerboxDescriptors.identity, (response) => {
+ Meteor.call('sandstormClaimIdentityRequest', response.token, response.descriptor);
+ });
+ };
+
// Since the Sandstorm grain is displayed in an iframe of the Sandstorm shell,
// we need to explicitly expose meta data like the page title or the URL path
// so that they could appear in the browser window.