├── app ├── assets │ ├── si1.jpg │ ├── h960.jpg │ ├── i512.png │ ├── sw.js │ └── home.html ├── apps │ ├── home │ │ ├── manifest.json │ │ ├── library.js │ │ └── views │ │ │ └── home.js │ ├── settings │ │ ├── manifest.json │ │ └── views │ │ │ ├── about.js │ │ │ ├── feedback.js │ │ │ └── home.js │ ├── messages │ │ ├── manifest.json │ │ ├── views │ │ │ ├── select.js │ │ │ ├── thread.js │ │ │ └── home.js │ │ ├── inbox.js │ │ └── library.js │ ├── profile │ │ ├── manifest.json │ │ ├── actions │ │ │ ├── getData.js │ │ │ ├── setData.js │ │ │ └── getDetails.js │ │ ├── views │ │ │ └── form.js │ │ └── library.js │ ├── contacts │ │ ├── actions │ │ │ ├── get.js │ │ │ ├── exists.js │ │ │ ├── getAccessKey.js │ │ │ ├── getRequest.js │ │ │ ├── setConnectKey.js │ │ │ ├── getIdentityKey.js │ │ │ ├── getProvidedAccessKeys.js │ │ │ └── getList.js │ │ ├── manifest.json │ │ ├── views │ │ │ ├── connectSettingsOpenConnect.js │ │ │ ├── connectKey.js │ │ │ ├── form.js │ │ │ ├── connectSettingsGroups.js │ │ │ ├── connectSettingsGroup.js │ │ │ ├── connectSettingsKeys.js │ │ │ ├── connectSettings.js │ │ │ ├── connectSettingsKey.js │ │ │ ├── home.js │ │ │ └── connect.js │ │ ├── inbox.js │ │ └── library.js │ ├── groups │ │ ├── actions │ │ │ ├── join.js │ │ │ ├── leave.js │ │ │ ├── exists.js │ │ │ ├── getDetails.js │ │ │ ├── getProvidedAccessKeys.js │ │ │ ├── isAdministrator.js │ │ │ ├── getInvitationURL.js │ │ │ ├── addURLInvitation.js │ │ │ ├── getList.js │ │ │ ├── getMembersConnectStatus.js │ │ │ ├── getMembersConnectAccessKey.js │ │ │ ├── setMembersConnectStatus.js │ │ │ └── invitePickedContact.js │ │ ├── propertyObserver.js │ │ ├── manifest.json │ │ ├── views │ │ │ ├── createForm.js │ │ │ └── home.js │ │ └── inbox.js │ ├── explore │ │ ├── actions │ │ │ ├── follow.js │ │ │ ├── unfollow.js │ │ │ └── isFollowing.js │ │ ├── manifest.json │ │ ├── propertyObserver.js │ │ ├── views │ │ │ ├── followForm.js │ │ │ ├── following.js │ │ │ └── home.js │ │ └── library.js │ ├── group │ │ ├── actions │ │ │ ├── isMember.js │ │ │ ├── getMemberData.js │ │ │ ├── getProfileImage.js │ │ │ ├── _getMemberDetails.js │ │ │ ├── deleteInvitation.js │ │ │ ├── modifyGroupPostsNotification.js │ │ │ ├── modifyPendingMembersNotification.js │ │ │ ├── modifyGroupPostReactionsNotification.js │ │ │ └── updateGroupPostReactionsNotification.js │ │ ├── manifest.json │ │ ├── views │ │ │ ├── remove.js │ │ │ ├── inviteGetURL.js │ │ │ ├── approve.js │ │ │ ├── invite.js │ │ │ ├── invitationDetails.js │ │ │ ├── members.js │ │ │ ├── membership.js │ │ │ ├── member.js │ │ │ ├── invitations.js │ │ │ └── home.js │ │ └── propertyObserver.js │ ├── user │ │ ├── actions │ │ │ ├── getProfileImage.js │ │ │ └── modifyUserPostsNotification.js │ │ ├── manifest.json │ │ ├── propertyObserver.js │ │ ├── views │ │ │ ├── changePassword.js │ │ │ └── home.js │ │ └── library.js │ ├── posts │ │ ├── manifest.json │ │ ├── actions │ │ │ ├── getRawPosts.js │ │ │ ├── getPostReactionsIDs.js │ │ │ └── getPostResource.js │ │ └── views │ │ │ ├── form.js │ │ │ └── post.js │ └── system │ │ ├── manifest.json │ │ └── views │ │ ├── message.js │ │ ├── confirm.js │ │ ├── manageNotification.js │ │ ├── richTextareaURL.js │ │ ├── previewURL.js │ │ ├── share.js │ │ ├── manageDeviceNotifications.js │ │ ├── reportError.js │ │ └── pick.js ├── library │ ├── sandboxWorker.js │ └── library.js ├── appjs-builder.php └── index.php └── composer.json /app/assets/si1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dotsmesh/dotsmesh-web-app/HEAD/app/assets/si1.jpg -------------------------------------------------------------------------------- /app/assets/h960.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dotsmesh/dotsmesh-web-app/HEAD/app/assets/h960.jpg -------------------------------------------------------------------------------- /app/assets/i512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dotsmesh/dotsmesh-web-app/HEAD/app/assets/i512.png -------------------------------------------------------------------------------- /app/apps/home/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Notifications", 3 | "views": [ 4 | "home" 5 | ], 6 | "library": true 7 | } -------------------------------------------------------------------------------- /app/apps/settings/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Settings", 3 | "views": [ 4 | "home", 5 | "about", 6 | "feedback" 7 | ] 8 | } -------------------------------------------------------------------------------- /app/apps/home/library.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Dots Mesh Web App 3 | * https://github.com/dotsmesh/dotsmesh-web-app 4 | * Free to use under the GPL-3.0 license. 5 | */ 6 | 7 | () => { 8 | 9 | }; -------------------------------------------------------------------------------- /app/apps/messages/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Messages", 3 | "views": [ 4 | "home", 5 | "select", 6 | "thread" 7 | ], 8 | "library": true, 9 | "inbox": true 10 | } -------------------------------------------------------------------------------- /app/apps/profile/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Profile", 3 | "views": [ 4 | "form" 5 | ], 6 | "actions": [ 7 | "getDetails", 8 | "getData", 9 | "setData" 10 | ], 11 | "library": true 12 | } -------------------------------------------------------------------------------- /app/apps/contacts/actions/get.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Dots Mesh Web App 3 | * https://github.com/dotsmesh/dotsmesh-web-app 4 | * Free to use under the GPL-3.0 license. 5 | */ 6 | 7 | async (args, library) => { 8 | return await library.get(args.userID); 9 | }; -------------------------------------------------------------------------------- /app/apps/groups/actions/join.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Dots Mesh Web App 3 | * https://github.com/dotsmesh/dotsmesh-web-app 4 | * Free to use under the GPL-3.0 license. 5 | */ 6 | 7 | async (args, library) => { 8 | return await library.join(args.groupID); 9 | }; -------------------------------------------------------------------------------- /app/apps/groups/actions/leave.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Dots Mesh Web App 3 | * https://github.com/dotsmesh/dotsmesh-web-app 4 | * Free to use under the GPL-3.0 license. 5 | */ 6 | 7 | async (args, library) => { 8 | return await library.leave(args.groupID); 9 | }; -------------------------------------------------------------------------------- /app/apps/contacts/actions/exists.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Dots Mesh Web App 3 | * https://github.com/dotsmesh/dotsmesh-web-app 4 | * Free to use under the GPL-3.0 license. 5 | */ 6 | 7 | async (args, library) => { 8 | return await library.exists(args.userID); 9 | }; -------------------------------------------------------------------------------- /app/apps/groups/actions/exists.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Dots Mesh Web App 3 | * https://github.com/dotsmesh/dotsmesh-web-app 4 | * Free to use under the GPL-3.0 license. 5 | */ 6 | 7 | async (args, library) => { 8 | return await library.exists(args.groupID); 9 | }; -------------------------------------------------------------------------------- /app/apps/explore/actions/follow.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Dots Mesh Web App 3 | * https://github.com/dotsmesh/dotsmesh-web-app 4 | * Free to use under the GPL-3.0 license. 5 | */ 6 | 7 | async (args, library) => { 8 | return await library.follow(args.type, args.id); 9 | }; -------------------------------------------------------------------------------- /app/apps/contacts/actions/getAccessKey.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Dots Mesh Web App 3 | * https://github.com/dotsmesh/dotsmesh-web-app 4 | * Free to use under the GPL-3.0 license. 5 | */ 6 | 7 | async (args, library) => { 8 | return await library.getAccessKey(args.userID); 9 | }; -------------------------------------------------------------------------------- /app/apps/contacts/actions/getRequest.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Dots Mesh Web App 3 | * https://github.com/dotsmesh/dotsmesh-web-app 4 | * Free to use under the GPL-3.0 license. 5 | */ 6 | 7 | async (args, library) => { 8 | return await library.getRequest(args.userID); 9 | }; -------------------------------------------------------------------------------- /app/apps/contacts/actions/setConnectKey.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Dots Mesh Web App 3 | * https://github.com/dotsmesh/dotsmesh-web-app 4 | * Free to use under the GPL-3.0 license. 5 | */ 6 | 7 | async (args, library) => { 8 | return await library.setConnectKey(args.key); 9 | }; -------------------------------------------------------------------------------- /app/apps/explore/actions/unfollow.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Dots Mesh Web App 3 | * https://github.com/dotsmesh/dotsmesh-web-app 4 | * Free to use under the GPL-3.0 license. 5 | */ 6 | 7 | async (args, library) => { 8 | return await library.unfollow(args.type, args.id); 9 | }; -------------------------------------------------------------------------------- /app/apps/contacts/actions/getIdentityKey.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Dots Mesh Web App 3 | * https://github.com/dotsmesh/dotsmesh-web-app 4 | * Free to use under the GPL-3.0 license. 5 | */ 6 | 7 | async (args, library) => { 8 | return await library.getIdentityKey(args.userID); 9 | }; -------------------------------------------------------------------------------- /app/apps/explore/actions/isFollowing.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Dots Mesh Web App 3 | * https://github.com/dotsmesh/dotsmesh-web-app 4 | * Free to use under the GPL-3.0 license. 5 | */ 6 | 7 | async (args, library) => { 8 | return await library.isFollowing(args.type, args.id); 9 | }; -------------------------------------------------------------------------------- /app/apps/group/actions/isMember.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Dots Mesh Web App 3 | * https://github.com/dotsmesh/dotsmesh-web-app 4 | * Free to use under the GPL-3.0 license. 5 | */ 6 | 7 | async (args, library) => { 8 | return await library.isMember(args.groupID, args.typedID); 9 | }; -------------------------------------------------------------------------------- /app/apps/groups/actions/getDetails.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Dots Mesh Web App 3 | * https://github.com/dotsmesh/dotsmesh-web-app 4 | * Free to use under the GPL-3.0 license. 5 | */ 6 | 7 | async (args, library) => { 8 | return await library.get(args.groupID, args.details); 9 | }; -------------------------------------------------------------------------------- /app/apps/groups/actions/getProvidedAccessKeys.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Dots Mesh Web App 3 | * https://github.com/dotsmesh/dotsmesh-web-app 4 | * Free to use under the GPL-3.0 license. 5 | */ 6 | 7 | async (args, library) => { 8 | return await library.getProvidedAccessKeys(); 9 | }; -------------------------------------------------------------------------------- /app/apps/groups/actions/isAdministrator.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Dots Mesh Web App 3 | * https://github.com/dotsmesh/dotsmesh-web-app 4 | * Free to use under the GPL-3.0 license. 5 | */ 6 | 7 | async (args, library) => { 8 | return await library.isAdministrator(args.groupID); 9 | }; -------------------------------------------------------------------------------- /app/apps/contacts/actions/getProvidedAccessKeys.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Dots Mesh Web App 3 | * https://github.com/dotsmesh/dotsmesh-web-app 4 | * Free to use under the GPL-3.0 license. 5 | */ 6 | 7 | async (args, library) => { 8 | return await library.getProvidedAccessKeys(); 9 | }; -------------------------------------------------------------------------------- /app/apps/groups/actions/getInvitationURL.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Dots Mesh Web App 3 | * https://github.com/dotsmesh/dotsmesh-web-app 4 | * Free to use under the GPL-3.0 license. 5 | */ 6 | 7 | async (args, library) => { 8 | return await library.getInvitationURL(args.groupID); 9 | }; -------------------------------------------------------------------------------- /app/apps/group/actions/getMemberData.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Dots Mesh Web App 3 | * https://github.com/dotsmesh/dotsmesh-web-app 4 | * Free to use under the GPL-3.0 license. 5 | */ 6 | 7 | async (args, library) => { 8 | return await library.getMemberData(args.groupID, args.userID); 9 | }; -------------------------------------------------------------------------------- /app/apps/group/actions/getProfileImage.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Dots Mesh Web App 3 | * https://github.com/dotsmesh/dotsmesh-web-app 4 | * Free to use under the GPL-3.0 license. 5 | */ 6 | 7 | async (args, library) => { 8 | return await library.getProfileImage(args.groupID, args.size); 9 | }; -------------------------------------------------------------------------------- /app/apps/profile/actions/getData.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Dots Mesh Web App 3 | * https://github.com/dotsmesh/dotsmesh-web-app 4 | * Free to use under the GPL-3.0 license. 5 | */ 6 | 7 | async (args, library) => { 8 | return await library.getData(args.propertyType, args.propertyID); 9 | }; -------------------------------------------------------------------------------- /app/apps/user/actions/getProfileImage.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Dots Mesh Web App 3 | * https://github.com/dotsmesh/dotsmesh-web-app 4 | * Free to use under the GPL-3.0 license. 5 | */ 6 | 7 | async (args, library) => { 8 | return await library.getProfileImage(args.userID, args.size); 9 | }; -------------------------------------------------------------------------------- /app/apps/posts/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Posts", 3 | "views": [ 4 | "post", 5 | "form" 6 | ], 7 | "actions": [ 8 | "getPostResource", 9 | "getRawPosts", 10 | "getPostReactionsIDs" 11 | ], 12 | "library": true 13 | } -------------------------------------------------------------------------------- /app/apps/profile/actions/setData.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Dots Mesh Web App 3 | * https://github.com/dotsmesh/dotsmesh-web-app 4 | * Free to use under the GPL-3.0 license. 5 | */ 6 | 7 | async (args, library) => { 8 | await library.setData(args.propertyType, args.propertyID, args.data); 9 | }; -------------------------------------------------------------------------------- /app/apps/group/actions/_getMemberDetails.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Dots Mesh Web App 3 | * https://github.com/dotsmesh/dotsmesh-web-app 4 | * Free to use under the GPL-3.0 license. 5 | */ 6 | 7 | async (args, library) => { 8 | return await library.getMemberDetails(args.groupID, args.typedID); 9 | }; -------------------------------------------------------------------------------- /app/apps/group/actions/deleteInvitation.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Dots Mesh Web App 3 | * https://github.com/dotsmesh/dotsmesh-web-app 4 | * Free to use under the GPL-3.0 license. 5 | */ 6 | 7 | async (args, library) => { 8 | return await library.deleteInvitation(args.groupID, args.invitationID); 9 | }; -------------------------------------------------------------------------------- /app/apps/groups/actions/addURLInvitation.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Dots Mesh Web App 3 | * https://github.com/dotsmesh/dotsmesh-web-app 4 | * Free to use under the GPL-3.0 license. 5 | */ 6 | 7 | async (args, library) => { 8 | return await library.addURLInvitation(args.groupID, args.accessKey); 9 | }; -------------------------------------------------------------------------------- /app/apps/groups/actions/getList.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Dots Mesh Web App 3 | * https://github.com/dotsmesh/dotsmesh-web-app 4 | * Free to use under the GPL-3.0 license. 5 | */ 6 | 7 | async (args, library) => { 8 | return await library.getList(args.details !== undefined ? args.details : []); 9 | }; -------------------------------------------------------------------------------- /app/apps/groups/actions/getMembersConnectStatus.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Dots Mesh Web App 3 | * https://github.com/dotsmesh/dotsmesh-web-app 4 | * Free to use under the GPL-3.0 license. 5 | */ 6 | 7 | async (args, library) => { 8 | return await library.getMembersConnectStatus(args.groupID); 9 | }; -------------------------------------------------------------------------------- /app/apps/groups/actions/getMembersConnectAccessKey.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Dots Mesh Web App 3 | * https://github.com/dotsmesh/dotsmesh-web-app 4 | * Free to use under the GPL-3.0 license. 5 | */ 6 | 7 | async (args, library) => { 8 | return await library.getMembersConnectAccessKey(args.groupID); 9 | }; -------------------------------------------------------------------------------- /app/apps/contacts/actions/getList.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Dots Mesh Web App 3 | * https://github.com/dotsmesh/dotsmesh-web-app 4 | * Free to use under the GPL-3.0 license. 5 | */ 6 | 7 | async (args, library) => { 8 | return await library.getList(typeof args.details !== 'undefined' ? args.details : []); 9 | }; -------------------------------------------------------------------------------- /app/apps/groups/actions/setMembersConnectStatus.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Dots Mesh Web App 3 | * https://github.com/dotsmesh/dotsmesh-web-app 4 | * Free to use under the GPL-3.0 license. 5 | */ 6 | 7 | async (args, library) => { 8 | return await library.setMembersConnectStatus(args.groupID, args.allow); 9 | }; -------------------------------------------------------------------------------- /app/apps/posts/actions/getRawPosts.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Dots Mesh Web App 3 | * https://github.com/dotsmesh/dotsmesh-web-app 4 | * Free to use under the GPL-3.0 license. 5 | */ 6 | 7 | async (args, library) => { 8 | return await library.getRawPosts(args.propertyType, args.propertyID, args.options); 9 | }; -------------------------------------------------------------------------------- /app/apps/user/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "User", 3 | "views": [ 4 | "home", 5 | "changePassword" 6 | ], 7 | "actions": [ 8 | "modifyUserPostsNotification", 9 | "getProfileImage" 10 | ], 11 | "library": true, 12 | "propertyObserver": true 13 | } -------------------------------------------------------------------------------- /app/apps/posts/actions/getPostReactionsIDs.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Dots Mesh Web App 3 | * https://github.com/dotsmesh/dotsmesh-web-app 4 | * Free to use under the GPL-3.0 license. 5 | */ 6 | 7 | async (args, library) => { 8 | return await library.getPostReactionsIDs(args.propertyType, args.propertyID, args.postID); 9 | }; -------------------------------------------------------------------------------- /app/apps/posts/actions/getPostResource.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Dots Mesh Web App 3 | * https://github.com/dotsmesh/dotsmesh-web-app 4 | * Free to use under the GPL-3.0 license. 5 | */ 6 | 7 | async (args, library) => { 8 | return await library.getPostResource(args.propertyType, args.propertyID, args.postID, args.resourceID); 9 | }; -------------------------------------------------------------------------------- /app/apps/user/actions/modifyUserPostsNotification.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Dots Mesh Web App 3 | * https://github.com/dotsmesh/dotsmesh-web-app 4 | * Free to use under the GPL-3.0 license. 5 | */ 6 | 7 | async (args, library) => { 8 | return await library.modifyUserPostsNotification(args.action, args.userID, args.lastSeenPosts); 9 | }; -------------------------------------------------------------------------------- /app/apps/group/actions/modifyGroupPostsNotification.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Dots Mesh Web App 3 | * https://github.com/dotsmesh/dotsmesh-web-app 4 | * Free to use under the GPL-3.0 license. 5 | */ 6 | 7 | async (args, library) => { 8 | return await library.modifyGroupPostsNotification(args.action, args.groupID, args.lastSeenPosts); 9 | }; -------------------------------------------------------------------------------- /app/apps/explore/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Explore", 3 | "views": [ 4 | "home", 5 | "followForm", 6 | "following" 7 | ], 8 | "actions": [ 9 | "follow", 10 | "unfollow", 11 | "isFollowing" 12 | ], 13 | "library": true, 14 | "propertyObserver": true 15 | } -------------------------------------------------------------------------------- /app/apps/group/actions/modifyPendingMembersNotification.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Dots Mesh Web App 3 | * https://github.com/dotsmesh/dotsmesh-web-app 4 | * Free to use under the GPL-3.0 license. 5 | */ 6 | 7 | async (args, library) => { 8 | return await library.modifyPendingMembersNotification(args.action, args.groupID, args.lastSeenUsersIDs); 9 | }; -------------------------------------------------------------------------------- /app/apps/system/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "System", 3 | "views": [ 4 | "confirm", 5 | "pick", 6 | "share", 7 | "reportError", 8 | "manageNotification", 9 | "manageDeviceNotifications", 10 | "message", 11 | "richTextareaURL", 12 | "previewURL" 13 | ] 14 | } -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "authors": [ 3 | { 4 | "name": "Ivo Petkov", 5 | "email": "ivo@dotsmesh.com", 6 | "homepage": "https://about.dotsmesh.com" 7 | } 8 | ], 9 | "require": { 10 | "php": "7.4.*|8.0.*|8.1.*", 11 | "bearframework/bearframework": "1.*" 12 | } 13 | } -------------------------------------------------------------------------------- /app/apps/group/actions/modifyGroupPostReactionsNotification.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Dots Mesh Web App 3 | * https://github.com/dotsmesh/dotsmesh-web-app 4 | * Free to use under the GPL-3.0 license. 5 | */ 6 | 7 | async (args, library) => { 8 | return await library.modifyGroupPostReactionsNotification(args.action, args.groupID, args.postID, args.lastSeenPostReactions); 9 | }; -------------------------------------------------------------------------------- /app/apps/group/actions/updateGroupPostReactionsNotification.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Dots Mesh Web App 3 | * https://github.com/dotsmesh/dotsmesh-web-app 4 | * Free to use under the GPL-3.0 license. 5 | */ 6 | 7 | async (args, library) => { 8 | return await library.updateGroupPostReactionsNotification(args.groupID, args.postID, { lastSeenPostReactions: args.lastSeenPostReactions }); 9 | }; -------------------------------------------------------------------------------- /app/apps/groups/propertyObserver.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Dots Mesh Web App 3 | * https://github.com/dotsmesh/dotsmesh-web-app 4 | * Free to use under the GPL-3.0 license. 5 | */ 6 | 7 | async (args, library) => { 8 | //console.log(args); 9 | var changes = args.changes; 10 | //console.table(changes); 11 | 12 | for (var change of changes) { 13 | if (change.key.indexOf('gm/') === 0) { 14 | var groupID = change.propertyID; 15 | await library.checkIfApproved(groupID); 16 | } 17 | } 18 | } -------------------------------------------------------------------------------- /app/apps/messages/views/select.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Dots Mesh Web App 3 | * https://github.com/dotsmesh/dotsmesh-web-app 4 | * Free to use under the GPL-3.0 license. 5 | */ 6 | 7 | async (args, library) => { 8 | x.setTitle('Select recipient'); 9 | var fieldRecipient = x.addFieldTextbox('Recipient'); 10 | fieldRecipient.focus(); 11 | x.addSaveButton(async () => { 12 | var recipientID = fieldRecipient.getValue(); 13 | var threadID = await library.getOrMakeThreadID([recipientID]); 14 | x.open('thread', { 'threadID': threadID }); 15 | }); 16 | }; -------------------------------------------------------------------------------- /app/library/sandboxWorker.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Dots Mesh Web App 3 | * https://github.com/dotsmesh/dotsmesh-web-app 4 | * Free to use under the GPL-3.0 license. 5 | */ 6 | 7 | (x) => { 8 | 9 | // MESSAGING 10 | 11 | let channel = x.createMessagingChannel('worker-window'); 12 | channel.addListener('run', async args => { 13 | return await self.main(args); 14 | }); 15 | 16 | x.proxyCall = async (method, ...args) => { 17 | return await channel.send('call', { 18 | method: method, 19 | args: args 20 | }); 21 | }; 22 | 23 | } -------------------------------------------------------------------------------- /app/apps/explore/propertyObserver.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Dots Mesh Web App 3 | * https://github.com/dotsmesh/dotsmesh-web-app 4 | * Free to use under the GPL-3.0 license. 5 | */ 6 | 7 | async (args, library) => { 8 | //console.log(args); 9 | var changes = args.changes; 10 | var propertyIDsToAdd = []; 11 | for (var change of changes) { 12 | if (change.key === 'up' || change.key === 'gp') { 13 | propertyIDsToAdd.push(change.propertyID); 14 | } 15 | } 16 | if (propertyIDsToAdd.length > 0) { 17 | await library.addPropertiesToUpdateQueue(propertyIDsToAdd); 18 | } 19 | } -------------------------------------------------------------------------------- /app/apps/groups/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Groups", 3 | "views": [ 4 | "home", 5 | "createForm" 6 | ], 7 | "actions": [ 8 | "getList", 9 | "getDetails", 10 | "invitePickedContact", 11 | "join", 12 | "leave", 13 | "getInvitationURL", 14 | "addURLInvitation", 15 | "isAdministrator", 16 | "exists", 17 | "getMembersConnectStatus", 18 | "setMembersConnectStatus", 19 | "getMembersConnectAccessKey", 20 | "getProvidedAccessKeys" 21 | ], 22 | "library": true, 23 | "inbox": true, 24 | "propertyObserver": true 25 | } -------------------------------------------------------------------------------- /app/apps/system/views/message.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Dots Mesh Web App 3 | * https://github.com/dotsmesh/dotsmesh-web-app 4 | * Free to use under the GPL-3.0 license. 5 | */ 6 | 7 | async (args, library) => { 8 | 9 | var text = args.text; 10 | var options = args.options; 11 | var icon = options.icon !== undefined ? options.icon : null; 12 | 13 | if (icon !== null) { 14 | x.add(x.makeIcon(icon)); 15 | } 16 | 17 | x.add(x.makeText(text, { 18 | align: 'center', 19 | marginTop: 'modalFirst' 20 | })); 21 | 22 | x.add(x.makeButton('OK', async () => { 23 | await x.back(); 24 | }, { marginTop: 'big' })); 25 | }; 26 | -------------------------------------------------------------------------------- /app/apps/contacts/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Contacts", 3 | "views": [ 4 | "home", 5 | "form", 6 | "connect", 7 | "connectKey", 8 | "connectSettings", 9 | "connectSettingsGroup", 10 | "connectSettingsGroups", 11 | "connectSettingsKey", 12 | "connectSettingsKeys", 13 | "connectSettingsOpenConnect" 14 | ], 15 | "actions": [ 16 | "exists", 17 | "get", 18 | "getList", 19 | "getAccessKey", 20 | "getProvidedAccessKeys", 21 | "getIdentityKey", 22 | "getRequest", 23 | "setConnectKey" 24 | ], 25 | "library": true, 26 | "inbox": true 27 | } -------------------------------------------------------------------------------- /app/apps/profile/actions/getDetails.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Dots Mesh Web App 3 | * https://github.com/dotsmesh/dotsmesh-web-app 4 | * Free to use under the GPL-3.0 license. 5 | */ 6 | 7 | async (args, library) => { 8 | var details = args.details; 9 | var imageSize = typeof args.imageSize !== 'undefined' ? args.imageSize : null; 10 | var profile = await x.property.getProfile(args.propertyType, args.propertyID); 11 | var result = {}; 12 | for (var i = 0; i < details.length; i++) { 13 | var detail = details[i]; 14 | result[detail] = profile[detail]; 15 | } 16 | if (imageSize !== null) { 17 | result['image'] = await profile.getImage(imageSize); 18 | } 19 | return result; 20 | }; -------------------------------------------------------------------------------- /app/apps/system/views/confirm.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Dots Mesh Web App 3 | * https://github.com/dotsmesh/dotsmesh-web-app 4 | * Free to use under the GPL-3.0 license. 5 | */ 6 | 7 | async (args, library) => { 8 | 9 | var text = args.text; 10 | var options = args.options; 11 | var icon = options.icon !== undefined ? options.icon : null; 12 | 13 | if (icon !== null) { 14 | x.add(x.makeIcon(icon)); 15 | } 16 | 17 | x.add(x.makeText(text, { align: 'center', marginTop: 'modalFirst' })); 18 | 19 | x.add(x.makeButton('OK', async () => { 20 | await x.back('ok'); 21 | }, { marginTop: 'big' })); 22 | x.add(x.makeButton('Cancel', async () => { 23 | await x.back(); 24 | })); 25 | 26 | }; -------------------------------------------------------------------------------- /app/apps/group/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Group", 3 | "views": [ 4 | "home", 5 | "members", 6 | "invitations", 7 | "member", 8 | "membership", 9 | "approve", 10 | "invite", 11 | "inviteGetURL", 12 | "remove", 13 | "invitationDetails" 14 | ], 15 | "actions": [ 16 | "deleteInvitation", 17 | "isMember", 18 | "getMemberData", 19 | "modifyPendingMembersNotification", 20 | "modifyGroupPostsNotification", 21 | "updateGroupPostReactionsNotification", 22 | "modifyGroupPostReactionsNotification", 23 | "getProfileImage" 24 | ], 25 | "library": true, 26 | "propertyObserver": true 27 | } -------------------------------------------------------------------------------- /app/apps/group/views/remove.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Dots Mesh Web App 3 | * https://github.com/dotsmesh/dotsmesh-web-app 4 | * Free to use under the GPL-3.0 license. 5 | */ 6 | 7 | async (args, library) => { 8 | var groupID = args.groupID; 9 | var userID = args.userID; 10 | 11 | x.setTitle('Remove member'); 12 | 13 | x.add(x.makeProfilePreviewComponent('groupMember', groupID + '$' + userID, { 14 | theme: 'light', 15 | size: 'medium' 16 | })); 17 | 18 | x.add(x.makeText('Are you sure, you want to remove this member from the group?', { align: 'center' })); 19 | 20 | x.add(x.makeButton('Yes, remove', async () => { 21 | x.showLoading(); 22 | await library.removeMember(groupID, userID); 23 | await x.back('removed'); 24 | }, { marginTop: 'big' })); 25 | 26 | }; -------------------------------------------------------------------------------- /app/apps/system/views/manageNotification.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Dots Mesh Web App 3 | * https://github.com/dotsmesh/dotsmesh-web-app 4 | * Free to use under the GPL-3.0 license. 5 | */ 6 | 7 | async (args, library) => { 8 | var action = args.action; 9 | var serviceData = args.serviceData; 10 | 11 | x.setTitle('Notifications'); 12 | 13 | x.add(x.makeIcon('notification')); 14 | 15 | x.add(x.makeText(args.text, { align: 'center' })); 16 | 17 | var call = async () => { 18 | x.showLoading(); 19 | await x.services.call(serviceData.appID, serviceData.name, serviceData.args); 20 | //await x.backPrepare(); 21 | await x.back(); 22 | }; 23 | x.add(x.makeButton(action === 'add' ? 'Enable' : 'Disable', async () => { 24 | await call(); 25 | }, { marginTop: 'big' })); 26 | }; 27 | -------------------------------------------------------------------------------- /app/apps/contacts/views/connectSettingsOpenConnect.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Dots Mesh Web App 3 | * https://github.com/dotsmesh/dotsmesh-web-app 4 | * Free to use under the GPL-3.0 license. 5 | */ 6 | 7 | async (args, library) => { 8 | 9 | x.setTitle('Request from everyone'); 10 | 11 | x.add(x.makeIcon('globe')); 12 | 13 | var allowed = await library.getOpenConnectStatus(); 14 | x.add(x.makeText(allowed ? 'Everyone is allowed to send you connection requests.' : 'Connection requests from everyone are disabled.', { align: 'center' })); 15 | x.add(x.makeButton(allowed ? 'Disable' : 'Enable', async () => { 16 | x.showLoading(); 17 | if (allowed) { 18 | await library.setOpenConnectStatus(false); 19 | } else { 20 | await library.setOpenConnectStatus(true); 21 | } 22 | await x.back(); 23 | }, { marginTop: 'big' })); 24 | }; -------------------------------------------------------------------------------- /app/apps/groups/actions/invitePickedContact.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Dots Mesh Web App 3 | * https://github.com/dotsmesh/dotsmesh-web-app 4 | * Free to use under the GPL-3.0 license. 5 | */ 6 | 7 | async (args, library) => { 8 | var userID = args.pickedID; 9 | var profile = await x.user.getProfile(userID); 10 | var status = await library.invite(args.groupID, args.pickedID); 11 | if (status === 'ok') { 12 | return { 13 | text: 'An invitation has been sent to ' + profile.name + '!' 14 | } 15 | } else if (status === 'alreadyMember') { 16 | return { 17 | text: profile.name + ' is already a member of this group!' 18 | } 19 | } else if (status === 'alreadyInvited') { 20 | return { 21 | text: profile.name + ' is already invited to join this group!' 22 | } 23 | } else { 24 | throw new Error(); 25 | } 26 | }; -------------------------------------------------------------------------------- /app/apps/settings/views/about.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Dots Mesh Web App 3 | * https://github.com/dotsmesh/dotsmesh-web-app 4 | * Free to use under the GPL-3.0 license. 5 | */ 6 | 7 | async (args, library) => { 8 | try { 9 | x.setTitle('About'); 10 | 11 | x.add(x.makeText('Dots Mesh is an open social platform. Visit about.dotsmesh.com for all the details.', { marginTop: 'modalFirst', isHTML: true })); 12 | x.add(x.makeText('The source code of this web app is available at github.com/dotsmesh', { isHTML: true })); 13 | x.add(x.makeText('App version: ' + x.version)); 14 | x.add(x.makeButton('OK', async () => { 15 | await x.back(); 16 | }, { marginTop: 'big' })); 17 | } catch (e) { 18 | // prevent errors here 19 | } 20 | }; 21 | -------------------------------------------------------------------------------- /app/apps/system/views/richTextareaURL.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Dots Mesh Web App 3 | * https://github.com/dotsmesh/dotsmesh-web-app 4 | * Free to use under the GPL-3.0 license. 5 | */ 6 | 7 | async (args, library) => { 8 | 9 | var url = args.url !== undefined && args.url !== null ? args.url : ''; 10 | var title = args.title !== undefined && args.title !== null ? args.title : ''; 11 | 12 | x.setTitle(url.length > 0 ? 'Modify link' : 'Create link'); 13 | 14 | var fieldURL = x.makeFieldTextbox('URL', { placeholder: 'https://' }); 15 | fieldURL.setValue(url); 16 | x.add(fieldURL); 17 | 18 | var fieldTitle = x.makeFieldTextbox('Title');//, { placeholder: 'optional' } 19 | fieldTitle.setValue(title); 20 | x.add(fieldTitle); 21 | 22 | x.add(x.makeButton('Apply', async () => { 23 | x.back({ 24 | url: fieldURL.getValue(), 25 | title: fieldTitle.getValue(), 26 | }); 27 | })); 28 | }; -------------------------------------------------------------------------------- /app/apps/group/views/inviteGetURL.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Dots Mesh Web App 3 | * https://github.com/dotsmesh/dotsmesh-web-app 4 | * Free to use under the GPL-3.0 license. 5 | */ 6 | 7 | async (args, library) => { 8 | var groupID = args.groupID; 9 | x.setTitle('Invitation URL'); 10 | 11 | x.add(x.makeIcon('globe')); 12 | 13 | x.add(x.makeText('Send the following URL to the people you want to invite to this group:', { align: 'center' })); 14 | 15 | var fieldURL = x.makeFieldTextarea('', { readonly: true, breakWords: true }); 16 | x.add(fieldURL); 17 | 18 | x.add(x.makeButton('OK', () => { 19 | x.back(null, { closeAllModals: true }); 20 | }, { marginTop: 'big' })); 21 | 22 | x.showLoading(); 23 | (async () => { 24 | var result = await x.services.call('groups', 'getInvitationURL', { groupID: groupID }); 25 | fieldURL.setValue('https://dotsmesh.com/#g:' + result); 26 | x.hideLoading(); 27 | })(); 28 | 29 | }; -------------------------------------------------------------------------------- /app/apps/system/views/previewURL.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Dots Mesh Web App 3 | * https://github.com/dotsmesh/dotsmesh-web-app 4 | * Free to use under the GPL-3.0 license. 5 | */ 6 | 7 | async (args, library) => { 8 | 9 | var url = args.url !== undefined ? args.url : ''; 10 | //var title = args.title !== undefined ? args.title : ''; 11 | 12 | x.setTitle('Open link'); 13 | 14 | if (url.indexOf('://') === -1) { 15 | url = 'https://' + url; 16 | } 17 | 18 | x.add(x.makeText('You are about to visit the following website:\n\n' + url, { align: 'center', marginTop: 'modalFirst' })); 19 | // if (title.length > 0) { 20 | // x.add(x.makeText('Title:' + "\n" + title)); 21 | // } 22 | 23 | //x.add(x.makeText('URL:' + "\n" + url)); 24 | x.add(x.makeButton('Open', async () => { 25 | await x.openURL(url); 26 | await x.back(); 27 | }, { marginTop: 'big' })); 28 | // x.add(x.makeButton('Copy URL', async () => { 29 | // })); 30 | }; -------------------------------------------------------------------------------- /app/apps/messages/inbox.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Dots Mesh Web App 3 | * https://github.com/dotsmesh/dotsmesh-web-app 4 | * Free to use under the GPL-3.0 license. 5 | */ 6 | 7 | async (args, library) => { 8 | if (args.type === 'mm') { 9 | var data = x.unpack(args.data); 10 | if (data.name === 'm') { 11 | data = data.value; 12 | var message = x.posts.unpack(data.i, data.v); // validate 13 | var senderID = message.userID; 14 | var recipients = data.o; // other recipients// validate 15 | recipients.push(x.currentUser.getID()); 16 | recipients.push(senderID); 17 | var threadID = await library.getOrMakeThreadID(recipients); 18 | //message.id = x.generateDateBasedID(); // id must stay so it's referenced in the future (reactions) 19 | message.date = Date.now(); // date is updated so it shows last 20 | await library.addIncomingMessage(threadID, message, args.resources); 21 | } 22 | } 23 | } -------------------------------------------------------------------------------- /app/library/library.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Dots Mesh Web App 3 | * https://github.com/dotsmesh/dotsmesh-web-app 4 | * Free to use under the GPL-3.0 license. 5 | */ 6 | 7 | (x, sourceCode) => { 8 | x.library = {}; 9 | 10 | x.library.load = modules => { 11 | modules.forEach(module => { 12 | if (typeof sourceCode[module] !== 'string') { 13 | sourceCode[module] = sourceCode[module].toString(); 14 | } 15 | ((new Function('return ' + sourceCode[module]))())(x); 16 | }); 17 | }; 18 | 19 | x.library.get = (modules, xLocation) => { 20 | var result = ''; 21 | modules.forEach(module => { 22 | if (typeof sourceCode[module] !== 'string') { 23 | sourceCode[module] = sourceCode[module].toString(); 24 | } 25 | result += '(' + sourceCode[module] + ')(' + xLocation + ');'; 26 | }); 27 | return result; 28 | }; 29 | 30 | x.library.getApps = () => { 31 | return sourceCode.apps; 32 | }; 33 | 34 | }; -------------------------------------------------------------------------------- /app/apps/user/propertyObserver.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Dots Mesh Web App 3 | * https://github.com/dotsmesh/dotsmesh-web-app 4 | * Free to use under the GPL-3.0 license. 5 | */ 6 | 7 | async (args, library) => { 8 | //console.log(args); 9 | var changes = args.changes; 10 | for (var change of changes) { 11 | if (change.key === 'up') { 12 | var userID = change.propertyID; 13 | var notificationID = 'up$' + userID; 14 | if (await x.notifications.exists(notificationID)) { 15 | try { 16 | var posts = await x.services.call('posts', 'getRawPosts', { propertyType: 'user', propertyID: userID, options: { order: 'desc', offset: 0, limit: 200, ignoreValues: true, cacheList: true, ignoreListCache: true } }); // todo update limit 17 | var postsIDs = Object.keys(posts); 18 | await library.updateUserPostsNotification(userID, { lastPosts: postsIDs }); 19 | } catch (e) { 20 | if (e.name === 'propertyUnavailable') { 21 | // ignore 22 | } else { 23 | throw e; 24 | } 25 | } 26 | } 27 | } 28 | } 29 | 30 | } -------------------------------------------------------------------------------- /app/apps/contacts/views/connectKey.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Dots Mesh Web App 3 | * https://github.com/dotsmesh/dotsmesh-web-app 4 | * Free to use under the GPL-3.0 license. 5 | */ 6 | 7 | async (args, library) => { 8 | var userID = args.userID; 9 | 10 | x.setTitle('Do you have a key?'); 11 | 12 | x.add(x.makeIcon('key')); 13 | 14 | x.add(x.makeText('You\'ll need a key to send your connection request to this profile. This is an anti-spam mechanism.', { align: 'center' })); 15 | 16 | var fieldKey = x.makeFieldTextbox(null, { 17 | placeholder: 'secret key', 18 | align: 'center', 19 | uppercase: true 20 | }); 21 | x.add(fieldKey); 22 | 23 | x.add(x.makeButton('Send connection request', async () => { 24 | x.showLoading(); 25 | var key = fieldKey.getValue(); 26 | var result = await library.sendRequest(userID, 'k/' + key); 27 | if (!result) { 28 | result = await library.sendRequest(userID, key); 29 | } 30 | x.hideLoading(); 31 | if (result === true) { 32 | x.showMessage('Connection request sent!'); 33 | } else { 34 | x.showMessage('Sorry! The connection key is not valid!'); 35 | } 36 | }, { marginTop: 'big' })); 37 | }; -------------------------------------------------------------------------------- /app/apps/group/views/approve.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Dots Mesh Web App 3 | * https://github.com/dotsmesh/dotsmesh-web-app 4 | * Free to use under the GPL-3.0 license. 5 | */ 6 | 7 | async (args, library) => { 8 | var groupID = args.groupID; 9 | var userID = args.userID; 10 | x.setTitle('Approve or deny membership'); 11 | 12 | //x.add(x.makeText('')); 13 | 14 | var container = x.makeContainer(); 15 | 16 | x.add(x.makeProfilePreviewComponent('groupMember', groupID + '$' + userID, { 17 | theme: 'light', 18 | size: 'medium' 19 | })); 20 | 21 | container.add(x.makeButton('Approve', async () => { 22 | if (await x.confirm('Are you sure you want to add this person to the group?')) { 23 | x.showLoading(); 24 | await library.approveMember(groupID, userID); 25 | await x.back('approved'); 26 | } 27 | }, { marginTop: 'big' })); 28 | 29 | container.add(x.makeButton('Deny', async () => { 30 | if (await x.confirm('Are you sure you want to remove this person from the group?')) { 31 | x.showLoading(); 32 | await library.removeMember(groupID, userID); 33 | await x.back('removed'); 34 | } 35 | })); 36 | 37 | x.add(container); 38 | 39 | }; -------------------------------------------------------------------------------- /app/apps/contacts/views/form.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Dots Mesh Web App 3 | * https://github.com/dotsmesh/dotsmesh-web-app 4 | * Free to use under the GPL-3.0 license. 5 | */ 6 | 7 | async (args, library) => { 8 | x.setTitle('New contact'); 9 | 10 | x.add(x.makeIcon('search')); 11 | 12 | x.add(x.makeText('Every public profile has a unique ID. Enter the ID of the profile you are looking for.', { align: 'center' })); 13 | 14 | var fieldID = x.makeFieldTextbox('', { placeholder: 'ID', align: 'center' }); 15 | x.add(fieldID); 16 | 17 | x.add(x.makeButton('Search', async () => { 18 | var value = fieldID.getValue().trim().toLowerCase(); 19 | if (value.length === 0) { 20 | fieldID.focus(); 21 | return; 22 | } 23 | x.showLoading(); 24 | var id = x.getFullID(value); 25 | if (!x.isPublicID(id)) { 26 | x.hideLoading(); 27 | x.showMessage('The ID provided is not valid!'); 28 | return; 29 | } 30 | var userProfile = await x.user.getProfile(id); 31 | if (!userProfile.exists) { 32 | x.hideLoading(); 33 | x.showMessage('There is no such profile!'); 34 | return; 35 | } 36 | x.open('user/home', { userID: id }); 37 | }, { marginTop: 'big' })); 38 | }; -------------------------------------------------------------------------------- /app/assets/sw.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Dots Mesh Web App 3 | * https://github.com/dotsmesh/dotsmesh-web-app 4 | * Free to use under the GPL-3.0 license. 5 | */ 6 | 7 | self.x = {}; 8 | self.importScripts('?app&a'); 9 | 10 | self.addEventListener('push', async event => { 11 | //var payload = event.data ? event.data.text() : null; 12 | x.library.load(['utilities', 'core']); 13 | if (await x.currentUser.autoLogin()) { 14 | await x.runBackgroundTasks(); 15 | } 16 | }); 17 | 18 | self.addEventListener('notificationclick', event => { 19 | event.notification.close(); 20 | var notificationData = event.notification.data; 21 | event.waitUntil(async function () { 22 | var allClients = await clients.matchAll({ type: "window", includeUncontrolled: true }); 23 | var clientToUse = null; 24 | for (var client of allClients) { 25 | client.focus(); 26 | clientToUse = client; 27 | break; 28 | } 29 | if (!clientToUse) { 30 | clientToUse = await clients.openWindow('/'); // todo open notification 31 | } 32 | if (clientToUse) { 33 | await clientToUse.postMessage({ type: 'notificationClick', data: notificationData }); 34 | } 35 | }()); 36 | }); 37 | 38 | self.addEventListener('fetch', (event) => { 39 | event.respondWith( 40 | fetch(event.request) 41 | ); 42 | }); -------------------------------------------------------------------------------- /app/apps/explore/views/followForm.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Dots Mesh Web App 3 | * https://github.com/dotsmesh/dotsmesh-web-app 4 | * Free to use under the GPL-3.0 license. 5 | */ 6 | 7 | async (args, library) => { 8 | x.setTitle('Follow posts'); 9 | 10 | var typedID = args.id; 11 | var parsedTypedID = x.parseTypedID(typedID); 12 | 13 | x.add(x.makeProfilePreviewComponent(parsedTypedID.type, parsedTypedID.id, { 14 | theme: 'light', 15 | size: 'medium' 16 | })); 17 | 18 | var isFollowing = await library.isFollowing(parsedTypedID.type, parsedTypedID.id); 19 | if (parsedTypedID.type === 'user') { 20 | x.add(x.makeText(isFollowing ? 'This profile\'s posts are visible in your Explore screen.' : 'Get all the posts from this profile in your Explore screen.', { align: 'center' })); 21 | } else { 22 | x.add(x.makeText(isFollowing ? 'This group\'s posts are visible in your Explore screen.' : 'Get all the posts from this group in your Explore screen.', { align: 'center' })); 23 | } 24 | 25 | x.add(x.makeButton(isFollowing ? 'Unfollow' : 'Follow', async () => { 26 | x.showLoading(); 27 | if (isFollowing) { 28 | await library.unfollow(parsedTypedID.type, parsedTypedID.id); 29 | } else { 30 | await library.follow(parsedTypedID.type, parsedTypedID.id); 31 | } 32 | await x.back(); 33 | }, { marginTop: 'big' })); 34 | 35 | } -------------------------------------------------------------------------------- /app/apps/group/views/invite.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Dots Mesh Web App 3 | * https://github.com/dotsmesh/dotsmesh-web-app 4 | * Free to use under the GPL-3.0 license. 5 | */ 6 | 7 | async (args, library) => { 8 | var groupID = args.id; 9 | x.setTitle('Invite'); 10 | 11 | x.add(x.makeProfilePreviewComponent('group', groupID, { 12 | theme: 'light', 13 | size: 'medium' 14 | })); 15 | 16 | x.add(x.makeText('Invite new members to this group.', { align: 'center' })); 17 | 18 | var buttonAdded = false; 19 | if (x.currentUser.isPublic()) { 20 | x.add(x.makeButton('Invite a contact', async () => { 21 | x.pickContact(null, { 22 | service: { 23 | appID: 'groups', 24 | name: 'invitePickedContact', 25 | args: { groupID: groupID } 26 | }, 27 | closeAllModals: true 28 | }); 29 | }, { marginTop: 'big' })); 30 | buttonAdded = true; 31 | } 32 | 33 | x.add(x.makeButton('Get invitation URL', () => { 34 | x.open('group/inviteGetURL', { groupID: groupID }, { modal: true, width: 400 }) 35 | // var result = await x.services.call('groups', 'getInvitationURL', { groupID: groupID }); 36 | // //x.alert('https://dotsmesh.com/#g:' + result); 37 | // x.add(x.makeText('https://dotsmesh.com/#g:' + result)); 38 | }, { marginTop: buttonAdded ? null : 'big' })); 39 | 40 | }; -------------------------------------------------------------------------------- /app/apps/settings/views/feedback.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Dots Mesh Web App 3 | * https://github.com/dotsmesh/dotsmesh-web-app 4 | * Free to use under the GPL-3.0 license. 5 | */ 6 | 7 | async (args, library) => { 8 | 9 | x.setTitle('Feedback'); 10 | 11 | x.add(x.makeText('Help make Dots Mesh even more awesome. Your ideas and suggestions are highly appreciated.', { marginTop: 'modalFirst' })); 12 | 13 | var fieldFeedback = x.makeFieldTextarea('', { placeholder: 'Your feedback', height: '200px' }); 14 | x.add(fieldFeedback); 15 | 16 | x.add(x.makeButton('Send', async () => { 17 | var value = fieldFeedback.getValue().trim(); 18 | if (value.length === 0) { 19 | x.alert('The feedback field is empty!'); 20 | return; 21 | } 22 | x.showLoading(); 23 | try { 24 | await fetch('https://about.dotsmesh.com/submitFeedback', { 25 | method: 'POST', 26 | cache: 'no-cache', 27 | headers: { 'Content-Type': 'text/plain', 'Accept': 'text/plain,application/json' }, 28 | referrerPolicy: 'no-referrer', 29 | body: JSON.stringify({ content: value }) 30 | }); 31 | } catch (e) { 32 | 33 | } 34 | x.showMessage('Your feedback is successfully submitted! Thank you!'); 35 | x.hideLoading(); 36 | }, { marginTop: 'big' })); 37 | 38 | x.add(x.makeHint('No additional or identifiable information will be sent.', { align: 'center' })); 39 | }; -------------------------------------------------------------------------------- /app/apps/groups/views/createForm.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Dots Mesh Web App 3 | * https://github.com/dotsmesh/dotsmesh-web-app 4 | * Free to use under the GPL-3.0 license. 5 | */ 6 | 7 | async (args, library) => { 8 | x.setTitle('New group'); 9 | 10 | // x.add(x.makeProfilePreviewComponent('group', null, { 11 | // theme: 'light', 12 | // mode: 'image', 13 | // imageSize: 80 14 | // })); 15 | 16 | x.add(x.makeIcon('groups')); 17 | 18 | x.add(x.makeText('Just like the public profiles, a group requires a key. It points to the place where the data will be stored. Get one at hosting.dotsmesh.com.\n\nAlready got one?', { align: 'center', isHTML: true })); 19 | 20 | var fieldGroupKey = x.makeFieldTextbox(null, { 21 | placeholder: 'Group key', 22 | align: 'center', 23 | maxLength: 200, 24 | uppercase: true 25 | }); 26 | x.add(fieldGroupKey); 27 | 28 | x.add(x.makeButton('Create', async () => { 29 | x.showLoading(); 30 | var groupKey = fieldGroupKey.getValue(); 31 | try { 32 | var groupID = await library.createGroup(groupKey); 33 | await x.back(groupID); 34 | } catch (e) { 35 | x.hideLoading(); 36 | x.showMessage('The group key is not valid!'); 37 | // if (['invalidInvitationCode', 'networkError'].indexOf(e.name) !== -1) { 38 | // x.showMessage('The hosting key is not valid!'); 39 | // } else { 40 | // throw e; 41 | // } 42 | }; 43 | }, { marginTop: 'big' })); 44 | 45 | } -------------------------------------------------------------------------------- /app/apps/contacts/views/connectSettingsGroups.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Dots Mesh Web App 3 | * https://github.com/dotsmesh/dotsmesh-web-app 4 | * Free to use under the GPL-3.0 license. 5 | */ 6 | 7 | async (args, library) => { 8 | 9 | x.setTitle('Requests from groups'); 10 | 11 | x.addToProfile(x.makeAppPreviewComponent('groups', { 12 | emptyTitle: 'Requests from groups', 13 | hint: 'Allow specific group members to send you connection requests.', 14 | emptyText: 'The groups you are part of will be visible here. There are none yet.' 15 | })); 16 | 17 | //x.add(x.makeText('Allow specific group members to send you connection requests.')); 18 | 19 | x.add(x.makeComponent(async () => { 20 | var list = x.makeList({ type: 'blocks' }); 21 | var groups = await x.services.call('groups', 'getList', { details: ['allowConnectRequests'] }); 22 | var itemsCount = 0; 23 | for (let groupID in groups) { 24 | var group = groups[groupID]; 25 | list.add(await x.makeProfileButton('group', groupID, { 26 | onClick: (async groupID => { 27 | x.open('contacts/connectSettingsGroup', { groupID: groupID }, { modal: true, width: 300 }); 28 | }).bind(null, groupID), 29 | details: group.allowConnectRequests === 1 ? 'Allowed' : 'Forbidden' 30 | })); 31 | itemsCount++; 32 | } 33 | if (itemsCount === 0) { 34 | x.setTemplate('empty'); 35 | } else { 36 | x.setTemplate(); 37 | } 38 | return list; 39 | }, { observeChanges: ['groups/membersConnectStatus'] })); 40 | 41 | }; -------------------------------------------------------------------------------- /app/apps/groups/inbox.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Dots Mesh Web App 3 | * https://github.com/dotsmesh/dotsmesh-web-app 4 | * Free to use under the GPL-3.0 license. 5 | */ 6 | 7 | async (args, library) => { 8 | if (args.type === 'gi') { 9 | var data = x.unpack(args.data); 10 | if (data.name === 'i') { 11 | data = data.value; 12 | var groupID = data.g; // validate 13 | var userID = data.u; // validate 14 | var details = {}; 15 | details.invitedBy = userID; 16 | details.memberAccessKey = data.a; // validate 17 | details.membersKeys = data.k; // validate 18 | details.membersIDSalt = data.s; // validate 19 | try { 20 | await library.addInvitation(groupID, details); 21 | var userProfile = await x.user.getProfile(userID); 22 | var groupProfile = await x.group.getProfile(groupID); 23 | 24 | var notification = await x.notifications.make(); 25 | notification.visible = true; 26 | notification.title = 'Invitation from ' + userProfile.name; 27 | notification.text = 'You are invited to join the private group ' + groupProfile.name; 28 | notification.image = { type: 'userProfile', id: userID }; 29 | notification.onClick = { location: 'group/home', args: { id: groupID } }; 30 | notification.deleteOnClick = true; 31 | notification.tags = ['g']; 32 | await x.notifications.set(notification); 33 | 34 | } catch (e) { 35 | // access key may be valid no more 36 | } 37 | } 38 | } 39 | } -------------------------------------------------------------------------------- /app/apps/contacts/views/connectSettingsGroup.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Dots Mesh Web App 3 | * https://github.com/dotsmesh/dotsmesh-web-app 4 | * Free to use under the GPL-3.0 license. 5 | */ 6 | 7 | async (args, library) => { 8 | 9 | var groupID = args.groupID; 10 | 11 | x.setTitle('Group connection requests'); 12 | 13 | x.add(x.makeProfilePreviewComponent('group', groupID, { 14 | theme: 'light', 15 | size: 'medium' 16 | })); 17 | 18 | var allowed = await x.services.call('groups', 'getMembersConnectStatus', { groupID: groupID }); 19 | x.add(x.makeText(allowed ? 'Group members are allowed to send you connection requests.' : 'Connection requests from this group\'s members are forbidden.', { align: 'center' })); 20 | x.add(x.makeButton(allowed ? 'Disable' : 'Allow', async () => { 21 | x.showLoading(); 22 | await x.services.call('groups', 'setMembersConnectStatus', { groupID: groupID, allow: !allowed }) 23 | await x.back(); 24 | }, { marginTop: 'big' })); 25 | 26 | //var group = await x.group.getProfile(groupID); 27 | 28 | // x.add(x.makeText('When allowed, every member of ' + group.name + ' will be able to send you connection requests.')); 29 | 30 | // var allowed = await x.services.call('groups', 'getMembersConnectStatus', { groupID: groupID }); 31 | 32 | // var fieldAllow = x.makeFieldCheckbox('Allow'); 33 | // x.add(fieldAllow); 34 | // fieldAllow.setChecked(allowed); 35 | 36 | // x.add(x.makeButton('Save changes', async () => { 37 | // x.showLoading(); 38 | // await x.services.call('groups', 'setMembersConnectStatus', { groupID: groupID, allow: fieldAllow.isChecked() }) 39 | // await x.back(); 40 | // })); 41 | 42 | }; -------------------------------------------------------------------------------- /app/apps/system/views/share.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Dots Mesh Web App 3 | * https://github.com/dotsmesh/dotsmesh-web-app 4 | * Free to use under the GPL-3.0 license. 5 | */ 6 | 7 | async (args, library) => { 8 | var type = args.type; 9 | var value = args.value; 10 | 11 | var attachment = x.attachment.make(); 12 | attachment.type = type; 13 | attachment.value = value; 14 | 15 | if (type === 'u') { 16 | x.setTitle('Share a profile'); 17 | } else if (type === 'p') { 18 | x.setTitle('Share a post'); 19 | } else { 20 | x.setTitle('Share'); 21 | } 22 | 23 | x.add(x.makeAttachmentPreviewComponent(attachment, { theme: 'light' })); 24 | 25 | x.add(x.makeButton('Share publicly', async () => { 26 | var openResult = await x.open('posts/form', { 27 | userID: x.currentUser.getID(), 28 | attachment: await attachment.pack(), 29 | }, { modal: true }); 30 | // if (openResult !== null && typeof openResult.status !== 'undefined' && openResult.status === 'posted') { 31 | // x.back(); 32 | // } 33 | 34 | }, { marginTop: 'big' })); 35 | 36 | x.add(x.makeButton('Send as message', () => { 37 | x.pickContact(async userID => { 38 | x.open('messages/thread', { 39 | userID: userID, 40 | attachment: await attachment.pack() 41 | }); 42 | }); 43 | })); 44 | 45 | x.add(x.makeButton('Share in a group', () => { 46 | x.pickGroup(async groupID => { 47 | var openResult = await x.open('posts/form', { 48 | groupID: groupID, 49 | attachment: await attachment.pack(), 50 | }, { modal: true }); 51 | // if (openResult !== null && typeof openResult.status !== 'undefined' && openResult.status === 'posted') { 52 | // x.back(); 53 | // } 54 | }); 55 | })); 56 | }; -------------------------------------------------------------------------------- /app/apps/user/views/changePassword.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Dots Mesh Web App 3 | * https://github.com/dotsmesh/dotsmesh-web-app 4 | * Free to use under the GPL-3.0 license. 5 | */ 6 | 7 | async (args, library) => { 8 | x.setTitle('Change your password'); 9 | 10 | var fieldOldPassword = x.makeFieldTextbox('Old password', { maxLength: 100, type: 'password' }); 11 | x.add(fieldOldPassword); 12 | 13 | var fieldNewPassword = x.makeFieldTextbox('New password', { maxLength: 100, type: 'password' }); 14 | x.add(fieldNewPassword); 15 | 16 | var fieldNewRepeatPassword = x.makeFieldTextbox('Repeat new password', { maxLength: 100, type: 'password' }); 17 | x.add(fieldNewRepeatPassword); 18 | 19 | x.add(x.makeSubmitButton('Save Changes', async () => { 20 | var oldPassword = fieldOldPassword.getValue(); 21 | var newPassword = fieldNewPassword.getValue(); 22 | var newPassword2 = fieldNewRepeatPassword.getValue(); 23 | if (newPassword.length === 0) { 24 | x.alert('The password is required!'); 25 | return; 26 | } 27 | if (newPassword.length < 6) { 28 | x.alert('The password must be atleast 6 characters long!'); 29 | return; 30 | } 31 | if (newPassword !== newPassword2) { 32 | x.alert('Passwords does not match!'); 33 | return; 34 | } 35 | 36 | x.showLoading(); 37 | var result = await x.currentUser.setNewPassword(oldPassword, newPassword); 38 | if (result === 'ok') { 39 | x.alert('Password changed successfully!'); 40 | await x.back(); 41 | } else if (result === 'invalidAuth') { 42 | x.hideLoading(); 43 | x.alert('The old password is not valid!'); 44 | } else if (result === 'noChange') { 45 | x.hideLoading(); 46 | x.alert('The old and the new passwords are the same!'); 47 | } else { 48 | throw new Error(result); 49 | } 50 | })); 51 | 52 | }; -------------------------------------------------------------------------------- /app/apps/posts/views/form.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Dots Mesh Web App 3 | * https://github.com/dotsmesh/dotsmesh-web-app 4 | * Free to use under the GPL-3.0 license. 5 | */ 6 | 7 | async (args, library) => { 8 | 9 | var postID = args.postID !== undefined ? args.postID : null; 10 | var editMode = postID !== null; 11 | 12 | var propertyType = null; 13 | var propertyID = null; 14 | if (args.userID !== undefined) { // USER 15 | propertyType = 'user'; 16 | propertyID = args.userID; 17 | x.setTitle(editMode ? 'Edit post' : 'New public post'); 18 | } else if (args.groupID !== undefined) { // GROUP 19 | propertyType = 'group'; 20 | propertyID = args.groupID; 21 | var profile = await x.group.getProfile(propertyID); 22 | x.setTitle(editMode ? 'Edit post' : 'New post in ' + profile.name); 23 | } 24 | 25 | var attachment = args.attachment !== undefined ? x.attachment.unpack(null, args.attachment.value) : null; 26 | 27 | x.add(x.makeComponent(async () => { 28 | if (editMode) { 29 | var post = await library.getPost(propertyType, propertyID, postID, { cache: false }); 30 | } else { 31 | var post = x.posts.make(); 32 | if (attachment !== null) { 33 | post.attachments.add(attachment); 34 | } 35 | } 36 | var options = {}; 37 | //options.placeholder = 'What will you share today?'; // todo random texts 38 | if (propertyType === 'group') { 39 | options.profilePropertyType = 'groupMember'; 40 | options.profilePropertyID = propertyID + '$' + x.currentUser.getID(); 41 | } 42 | var postForm = await x.makePostForm(post, options); 43 | postForm.onSubmit = async post => { 44 | x.showLoading(); 45 | await library.setPost(editMode, propertyType, propertyID, post); 46 | await x.back(null, { closeAllModals: true }); // needed for the share windows { status: 'posted' } 47 | }; 48 | return postForm; 49 | })); 50 | } -------------------------------------------------------------------------------- /app/apps/system/views/manageDeviceNotifications.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Dots Mesh Web App 3 | * https://github.com/dotsmesh/dotsmesh-web-app 4 | * Free to use under the GPL-3.0 license. 5 | */ 6 | 7 | async (args, library) => { 8 | 9 | var isReminderMode = args.mode === 'r'; 10 | 11 | x.setTitle('Device notifications'); 12 | 13 | x.add(x.makeIcon('push-notifications')); 14 | 15 | if (x.deviceHasPushManagerSupport()) { 16 | var enabled = await x.currentUser.getDeviceNotificationsStatus() === 'enabled'; 17 | 18 | x.add(x.makeText(enabled ? 'Device notifications are currently enabled!' : 'Device notifications are currently disabled!', { align: 'center' })); 19 | if (isReminderMode && !enabled) { 20 | x.add(x.makeText('Enable them to get the updates you\'ve subscribed to instantly, without having to manually open the app. You can disable them later from the settings.', { align: 'center' })); 21 | } 22 | 23 | x.add(x.makeButton(enabled ? 'Disable' : 'Enable', async () => { 24 | x.showLoading(); 25 | if (enabled) { 26 | if (await x.currentUser.disableDeviceNotifications()) { 27 | await x.back(); 28 | } else { 29 | x.hideLoading(); 30 | x.showMessage('Cannot disable device notifications! Please, try again later.'); 31 | } 32 | } else { 33 | if (await x.currentUser.enableDeviceNotifications()) { 34 | await x.back(); 35 | } else { 36 | x.hideLoading(); 37 | x.showMessage('Cannot enable device notifications! Please, check your browser settings or try again later.'); 38 | } 39 | } 40 | }, { marginTop: 'big' })); 41 | } else { 42 | x.add(x.makeText('Your device/browser does not support notifications!', { align: 'center' })); 43 | x.add(x.makeButton('OK', () => { 44 | x.back(); 45 | }, { marginTop: 'big' })); 46 | } 47 | }; 48 | -------------------------------------------------------------------------------- /app/apps/contacts/views/connectSettingsKeys.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Dots Mesh Web App 3 | * https://github.com/dotsmesh/dotsmesh-web-app 4 | * Free to use under the GPL-3.0 license. 5 | */ 6 | 7 | async (args, library) => { 8 | 9 | x.setTitle('Connection keys'); 10 | 11 | x.addToProfile(x.makeAppPreviewComponent('key', { 12 | emptyTitle: 'Connection keys', 13 | hint: 'Create connection keys and share them with the people you want to connect with. They will be able to send you connection requests.', 14 | emptyText: 'Create connection keys and share them with the people you want to connect with. They will be able to send you connection requests.', 15 | actionButton: async () => { 16 | return { 17 | onClick: async () => { 18 | x.open('contacts/connectSettingsKey', {}, { modal: true, width: 300 }); 19 | }, 20 | text: 'New key' 21 | } 22 | }, 23 | })); 24 | 25 | //x.add(x.makeText('Create a new key and share it with the people you want to connect with. They will be able to send you connection requests.')); 26 | 27 | x.add(x.makeComponent(async () => { 28 | let connectKeys = await library.getConnectKeysList(); 29 | var list = x.makeList({ type: 'blocks' }); 30 | var itemsCount = 0; 31 | for (var connectKey of connectKeys) { 32 | list.add(await x.makeTextButton(((connectKey) => { 33 | x.open('contacts/connectSettingsKey', { key: connectKey.key }, { modal: true, width: 300 }); 34 | }).bind(null, connectKey), connectKey.key.toUpperCase(), { 35 | details: x.getHumanDate(x.parseDateID(connectKey.dateCreated)) 36 | })); 37 | itemsCount++; 38 | } 39 | if (itemsCount === 0) { 40 | x.setTemplate('empty'); 41 | } else { 42 | x.setTemplate(); 43 | } 44 | return list; 45 | }, { observeChanges: ['contactsConnectKeys'] })); 46 | 47 | // x.add(x.makeTextButton(async () => { 48 | // x.open('contacts/connectSettingsKey', {}, { modal: true, width: 300 }); 49 | // }, 'Create key')); 50 | 51 | }; -------------------------------------------------------------------------------- /app/apps/system/views/reportError.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Dots Mesh Web App 3 | * https://github.com/dotsmesh/dotsmesh-web-app 4 | * Free to use under the GPL-3.0 license. 5 | */ 6 | 7 | async (args, library) => { 8 | try { 9 | var error = args.error; 10 | var errorText = ''; 11 | try { 12 | errorText = []; 13 | var jsData = JSON.parse(error); 14 | errorText.push('location:\n' + jsData.location); 15 | errorText.push('state:\n' + JSON.stringify(jsData.state)); 16 | errorText.push('date:\n' + jsData.date); 17 | errorText.push('user agent:\n' + navigator.userAgent); 18 | errorText.push('name:\n' + jsData.name); 19 | for (var k in jsData.details) { 20 | var value = jsData.details[k]; 21 | if (typeof value !== 'string' && typeof value !== 'number') { 22 | value = JSON.stringify(value); 23 | } 24 | errorText.push(k + ':\n' + value); 25 | } 26 | errorText = errorText.join('\n\n'); 27 | } catch (e) { 28 | errorText = error; 29 | } 30 | x.setTitle('Oops, an error occurred!'); 31 | 32 | x.add(x.makeHint('You can help the Dots Mesh developers fix this problem by sending them the following text:')); 33 | 34 | var fieldTextarea = x.makeFieldTextarea('', { height: '400px', readonly: true }); 35 | fieldTextarea.setValue(errorText); 36 | x.add(fieldTextarea); 37 | 38 | x.add(x.makeButton('Submit error report', async () => { 39 | x.showLoading(); 40 | try { 41 | await fetch('https://about.dotsmesh.com/logAppError', { 42 | method: 'POST', 43 | cache: 'no-cache', 44 | headers: { 'Content-Type': 'text/plain', 'Accept': 'text/plain,application/json' }, 45 | referrerPolicy: 'no-referrer', 46 | body: JSON.stringify({ content: errorText }) 47 | }); 48 | } catch (e) { 49 | 50 | } 51 | x.back(); 52 | })); 53 | } catch (e) { 54 | // prevent errors here 55 | } 56 | }; 57 | -------------------------------------------------------------------------------- /app/apps/contacts/inbox.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Dots Mesh Web App 3 | * https://github.com/dotsmesh/dotsmesh-web-app 4 | * Free to use under the GPL-3.0 license. 5 | */ 6 | 7 | async (args, library) => { 8 | if (args.type === 'cc') { // Contacts connect 9 | var userID = args.sender; 10 | var context = args.context; 11 | var data = x.unpack(args.data); 12 | if (data.name === 'c') { // connect 13 | accessKey = data.value; 14 | if (typeof accessKey === 'string') { // better validate 15 | var invitationSource = null; 16 | if (context.type === 'contact') { 17 | invitationSource = x.pack('u', context.id); 18 | } else if (context.type === 'connectKey') { 19 | invitationSource = x.pack('k', context.key); 20 | } else if (context.type === 'group') { 21 | invitationSource = x.pack('g', context.id); 22 | } 23 | if (await library.addRequest(userID, accessKey, invitationSource)) { 24 | var contact = await library.get(userID); 25 | var profile = await x.user.getProfile(userID); 26 | 27 | var notification = await x.notifications.make('c$' + userID); 28 | notification.visible = true; 29 | if (contact !== null && contact.providedAccessKey !== null) { 30 | notification.title = 'Connected to ' + profile.name; 31 | notification.text = 'Now you can message privately' 32 | } else { 33 | notification.title = profile.name + ' wants to connect'; 34 | notification.text = 'When connected you will be able to message privately'; 35 | } 36 | notification.image = { type: 'userProfile', id: userID }; 37 | notification.onClick = { location: 'user/home', args: { userID: userID } }; 38 | notification.deleteOnClick = true; 39 | notification.tags = ['c']; 40 | await x.notifications.set(notification); 41 | } 42 | } 43 | } 44 | } 45 | } -------------------------------------------------------------------------------- /app/apps/contacts/views/connectSettings.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Dots Mesh Web App 3 | * https://github.com/dotsmesh/dotsmesh-web-app 4 | * Free to use under the GPL-3.0 license. 5 | */ 6 | 7 | async (args, library) => { 8 | var title = 'Ways to connect'; 9 | x.setTitle(title); 10 | 11 | x.addToProfile(x.makeAppPreviewComponent('contacts', { 12 | //title: title, 13 | hint: 'Precisely configure the ways other people send you connection requests.' 14 | })); 15 | 16 | //x.add(x.makeHint('Precisely configure the ways other people send you connection requests.')); 17 | 18 | x.add(x.makeComponent(async () => { 19 | 20 | var anonymousConnectIsAllowed = await library.getOpenConnectStatus(); 21 | var list = x.makeList({ type: 'blocks' }); 22 | 23 | list.add(x.makeTextButton(async () => { 24 | x.open('contacts/connectSettingsOpenConnect', {}, { modal: true, width: 300 }); 25 | }, 'Requests from everyone', { 26 | details: (anonymousConnectIsAllowed ? 'Allowed' : 'Forbidden') 27 | })); 28 | 29 | var enabledGroupCount = 0; 30 | var totalGroupCount = 0; 31 | var groups = await x.services.call('groups', 'getList', { details: ['allowConnectRequests'] }); 32 | for (let groupID in groups) { 33 | var group = groups[groupID]; 34 | if (group.allowConnectRequests === 1) { 35 | enabledGroupCount++; 36 | } 37 | totalGroupCount++; 38 | } 39 | list.add(x.makeTextButton(async () => { 40 | x.open('contacts/connectSettingsGroups'); 41 | }, 'Requests from groups', { 42 | details: 'Allowed for ' + enabledGroupCount + ' of ' + totalGroupCount + ' ' + (enabledGroupCount === 1 ? 'group' : 'groups') 43 | })); 44 | 45 | var connectKeys = await library.getConnectKeysList(); 46 | var connectKeysCount = connectKeys.length; 47 | list.add(x.makeTextButton(async () => { 48 | x.open('contacts/connectSettingsKeys'); 49 | }, 'Connection keys', { 50 | details: connectKeysCount + ' active ' + (connectKeysCount === 1 ? 'key' : 'keys') 51 | })); 52 | 53 | return list; 54 | }, { observeChanges: ['contactsOpenStatus', 'contactsConnectKeys', 'groups/membersConnectStatus'] })); 55 | 56 | }; -------------------------------------------------------------------------------- /app/apps/group/views/invitationDetails.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Dots Mesh Web App 3 | * https://github.com/dotsmesh/dotsmesh-web-app 4 | * Free to use under the GPL-3.0 license. 5 | */ 6 | 7 | async (args, library) => { 8 | var groupID = args.groupID; 9 | var invitationID = args.invitationID; 10 | x.setTitle('Invitation'); 11 | 12 | var invitation = await library.getInvitation(groupID, invitationID); 13 | if (invitation === null) { 14 | throw new Error(); 15 | } 16 | 17 | var deleteButtonText = ''; 18 | var deleteConfirmText = ''; 19 | if (invitation.type === 'personalInvitation') { 20 | var list = x.makeList(); 21 | var userID = invitation.userID; 22 | list.add(x.makeText('Invited profile:')); 23 | list.add(await x.makeProfileButton('user', userID)); 24 | x.add(list); 25 | var list = x.makeList(); 26 | list.add(x.makeText('Invited by:')); 27 | list.add(await x.makeProfileButton('groupMember', groupID + '$' + invitation.invitedBy, { 28 | onClick: { location: 'group/member', args: { groupID: groupID, userID: invitation.invitedBy }, preload: true } 29 | })); 30 | x.add(list); 31 | x.add(x.makeText('Date invited' + "\n" + x.getHumanDate(invitation.dateInvited))); 32 | deleteButtonText = 'Delete invitation'; 33 | deleteConfirmText = 'Are you sure you want to delete this personal invitation?'; 34 | } else if (invitation.type === 'urlInvitation') { 35 | var list = x.makeList(); 36 | list.add(x.makeText('URL created by:')); 37 | list.add(await x.makeProfileButton('groupMember', groupID + '$' + invitation.createdBy, { 38 | onClick: { location: 'group/member', args: { groupID: groupID, userID: invitation.createdBy }, preload: true } 39 | })); 40 | x.add(list); 41 | x.add(x.makeText('Date created:' + "\n" + x.getHumanDate(invitation.dateCreated))); 42 | deleteButtonText = 'Delete URL'; 43 | deleteConfirmText = 'Are you sure you want to delete this invitation URL?'; 44 | } else { 45 | throw new Error(); 46 | } 47 | 48 | x.add(x.makeButton(deleteButtonText, async () => { 49 | if (await x.confirm(deleteConfirmText)) { 50 | x.showLoading(); 51 | await x.services.call('group', 'deleteInvitation', { groupID: groupID, invitationID: invitationID }); 52 | await x.back(); 53 | } 54 | }, { marginTop: 'big' })); 55 | 56 | }; -------------------------------------------------------------------------------- /app/apps/explore/views/following.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Dots Mesh Web App 3 | * https://github.com/dotsmesh/dotsmesh-web-app 4 | * Free to use under the GPL-3.0 license. 5 | */ 6 | 7 | async (args, library) => { 8 | x.setTitle('Followed profiles and groups'); 9 | //x.add(x.makeTitle('Followed profiles and groups')); 10 | 11 | x.addToProfile(x.makeAppPreviewComponent('explore', { 12 | hint: 'Followed profiles and groups', 13 | emptyText: 'You are not following any profiles or groups, yet.' 14 | })); 15 | 16 | // todo sort by date 17 | var component = x.makeComponent(async () => { 18 | var following = await library.getFollowing(); 19 | var followingCount = following.length; 20 | if (followingCount > 0) { 21 | var profilesIDs = []; 22 | var groupsIDs = []; 23 | for (var typedPropertyID of following) { 24 | var propertyData = x.parseTypedID(typedPropertyID); 25 | if (propertyData.type === 'group') { 26 | groupsIDs.push(propertyData.id); 27 | } else { 28 | profilesIDs.push(propertyData.id); 29 | } 30 | } 31 | //var result = []; 32 | var list = x.makeList({ type: 'blocks' }); 33 | if (profilesIDs.length > 0) { 34 | //result.push(x.makeSmallTitle('Profiles')); 35 | //var list = x.makeList({ type: 'blocks' }); 36 | for (var userID of profilesIDs) { 37 | list.add(await x.makeProfileButton('user', userID, { details: x.getShortID(userID) })); 38 | } 39 | //result.push(list); 40 | } 41 | if (groupsIDs.length > 0) { 42 | //result.push(x.makeSmallTitle('Groups')); 43 | //var list = x.makeList({ type: 'blocks' }); 44 | for (var groupID of groupsIDs) { 45 | list.add(await x.makeProfileButton('group', groupID, { details: 'group' })); 46 | } 47 | //result.push(list); 48 | component.observeChanges(['group/' + groupID + '/profile']); 49 | } 50 | x.setTemplate(); 51 | return list; 52 | } else { 53 | x.setTemplate('empty'); 54 | //return x.makeHint('There are no profiles and groups here'); 55 | return null; 56 | } 57 | }); 58 | component.observeChanges(['explore/following']); 59 | x.add(component); 60 | 61 | 62 | }; -------------------------------------------------------------------------------- /app/apps/profile/views/form.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Dots Mesh Web App 3 | * https://github.com/dotsmesh/dotsmesh-web-app 4 | * Free to use under the GPL-3.0 license. 5 | */ 6 | 7 | async (args, library) => { 8 | 9 | var propertyType = null; 10 | var propertyID = null; 11 | var defaultImage = null; 12 | var defaultTitle = 'Edit profile'; 13 | if (args.userID !== undefined) { // USER 14 | propertyType = 'user'; 15 | propertyID = args.userID; 16 | if (propertyID !== x.currentUser.getID()) { 17 | throw new Error(); 18 | } 19 | defaultImage = x.getDefaultUserImage(propertyID); 20 | } else if (args.groupID !== undefined) { // GROUP 21 | propertyType = 'group'; 22 | propertyID = args.groupID; 23 | defaultImage = x.getDefaultGroupImage() 24 | defaultTitle = 'Customize group'; 25 | } else if (args.groupUserID !== undefined) { // GROUP MEMBER 26 | propertyType = 'groupMember'; 27 | propertyID = args.groupUserID; 28 | defaultImage = x.getDefaultUserImage(propertyID); 29 | } 30 | 31 | x.setTitle(args.title !== undefined ? args.title : defaultTitle); 32 | 33 | if (propertyType === null || propertyID === null) { 34 | throw new Error(); 35 | } 36 | 37 | var argImage = args.image !== undefined ? args.image : null; 38 | var argName = args.name !== undefined ? args.name : null; 39 | var argDescription = args.description !== undefined ? args.description : null; 40 | 41 | var fieldImage = x.makeFieldImage('Image', { 42 | emptyValue: await x.image.make(defaultImage, 1, 1, 1) 43 | }); 44 | x.add(fieldImage); 45 | 46 | var fieldName = x.makeFieldTextbox('Name', { maxLength: 100 }); 47 | x.add(fieldName); 48 | 49 | var fieldDescription = x.makeFieldTextarea('Description', { maxLength: 1000 }); 50 | x.add(fieldDescription); 51 | 52 | x.add(x.makeSubmitButton(args.buttonText !== undefined ? args.buttonText : 'Save Changes', async () => { 53 | x.showLoading(); 54 | var data = { 55 | image: fieldImage.getValue(), 56 | name: fieldName.getValue(), 57 | description: fieldDescription.getValue() 58 | }; 59 | if (args.callServiceBefore !== undefined) { 60 | var service = args.callServiceBefore; 61 | await x.services.call(service.appID, service.name, service.args); 62 | } 63 | await library.setData(propertyType, propertyID, data); 64 | await x.back('saved'); 65 | })); 66 | 67 | x.wait(async () => { 68 | var data = await library.getData(propertyType, propertyID); 69 | fieldImage.setValue(argImage !== null ? argImage : data.image); 70 | fieldName.setValue(argName !== null ? argName : data.name); 71 | fieldDescription.setValue(argDescription !== null ? argDescription : data.description); 72 | }); 73 | }; -------------------------------------------------------------------------------- /app/apps/contacts/views/connectSettingsKey.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Dots Mesh Web App 3 | * https://github.com/dotsmesh/dotsmesh-web-app 4 | * Free to use under the GPL-3.0 license. 5 | */ 6 | 7 | async (args, library) => { 8 | var key = args.key !== undefined ? args.key : null; 9 | var resultMode = key !== null; 10 | 11 | x.add(x.makeIcon('key')); 12 | 13 | if (resultMode) { 14 | x.setTitle('Key details'); 15 | } else { 16 | x.setTitle('New connection key'); 17 | 18 | x.add(x.makeText('Your secret key may contain letters and numbers only.', { align: 'center' })); 19 | } 20 | 21 | var currentUserID = x.getShortID(x.currentUser.getID()); 22 | 23 | var fieldKey = x.makeFieldTextbox(resultMode ? 'Secret key' : '', { 24 | readonly: resultMode, 25 | uppercase: true, 26 | align: resultMode ? null : 'center', 27 | placeholder: 'secret key' 28 | }); 29 | x.add(fieldKey); 30 | if (resultMode) { 31 | fieldKey.setValue(key); 32 | } 33 | 34 | if (resultMode) { 35 | var fieldURL = x.makeFieldTextarea('URL', { readonly: true, breakWords: true }); 36 | x.add(fieldURL); 37 | fieldURL.setValue('https://dotsmesh.com/#' + currentUserID + '/c/' + key); 38 | } 39 | 40 | if (resultMode) { 41 | x.add(x.makeButton('OK', () => { 42 | x.back(); 43 | }, { marginTop: 'big' })); 44 | x.addToolbarButton('Delete this connection key', async () => { 45 | if (await x.confirm('Are you sure you want to delete this connection key?')) { 46 | x.showLoading(); 47 | await library.deleteConnectKey(key); 48 | //await x.backPrepare(); 49 | await x.back(); 50 | } 51 | }, 'delete'); 52 | } else { 53 | var container = x.makeContainer(); 54 | container.add(x.makeButton('Create', async () => { 55 | var key = fieldKey.getValue().trim().toLowerCase(); 56 | if (key.length === 0) { 57 | fieldKey.focus(); 58 | // x.hideLoading(); 59 | // x.alert('The key cannot be empty!'); 60 | return; 61 | } 62 | x.showLoading(); 63 | if (key.length > 200) { 64 | x.hideLoading(); 65 | x.alert('The key cannot be longer than 200 chars!'); 66 | return; 67 | } 68 | if (key.match(/^[a-z0-9]+$/) === null) { 69 | x.hideLoading(); 70 | x.alert('The key must contain only letters and numbers!'); 71 | return; 72 | } 73 | if (await library.getConnectKey(key) !== null) { 74 | x.hideLoading(); 75 | x.alert('There is such key already!'); 76 | return; 77 | } 78 | await library.setConnectKey(key); 79 | await x.back(); 80 | }, { marginTop: 'big' })); 81 | x.add(container); 82 | } 83 | }; -------------------------------------------------------------------------------- /app/apps/messages/views/thread.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Dots Mesh Web App 3 | * https://github.com/dotsmesh/dotsmesh-web-app 4 | * Free to use under the GPL-3.0 license. 5 | */ 6 | 7 | async (args, library) => { 8 | x.setTemplate('message'); 9 | x.scrollBottom(); 10 | if (typeof args.userID !== 'undefined') { 11 | var userID = args.userID; 12 | var threadID = await library.getOrMakeThreadID([userID]); 13 | } else { 14 | var threadID = args.threadID; 15 | } 16 | var attachment = typeof args.attachment !== 'undefined' && args.attachment !== null ? x.attachment.unpack(null, args.attachment.value) : null; 17 | 18 | var recipients = await library.getThreadRecipients(threadID); 19 | 20 | var names = []; 21 | var firstRecipientID = null; 22 | for (var i = 0; i < recipients.length; i++) { 23 | var recipientID = recipients[i]; 24 | var profile = await x.user.getProfile(recipientID); 25 | names.push(profile.name); 26 | firstRecipientID = recipientID; // only one is supported 27 | } 28 | x.setTitle('Messaging with ' + names.join(', '), true); 29 | 30 | if (firstRecipientID !== null) { 31 | x.addToProfile(x.makeSmallProfilePreviewComponent('user', firstRecipientID)); 32 | } 33 | 34 | var discussionComponent = x.makeDiscussionComponent(async listOptions => { 35 | var posts = await library.getThreadMessages(threadID, listOptions); 36 | return posts; 37 | }); 38 | discussionComponent.observeChanges(['messages/' + threadID]); 39 | x.add(discussionComponent, { template: 'content' });//, { template: 'column1' } 40 | 41 | x.add(x.makeComponent(async () => { 42 | var post = x.posts.make(); 43 | if (attachment !== null) { 44 | post.attachments.add(attachment); 45 | } 46 | var postForm = await x.makePostForm(post, { 47 | placeholder: 'Your message', 48 | clearOnSubmit: true, 49 | submitText: 'Send', 50 | type: 'small' 51 | }); 52 | postForm.onSubmit = message => { 53 | var firstOnChange = true; 54 | library.addMessage(threadID, message, async message => { 55 | await discussionComponent.setMessage(message); 56 | if (firstOnChange) { 57 | firstOnChange = false; 58 | x.scrollBottom();//'column1' 59 | } 60 | }); 61 | }; 62 | return postForm; 63 | }), { template: 'content' });//, { template: 'column1' } 64 | 65 | x.windowEvents.addEventListener('show', async () => { 66 | await x.notifications.delete('m$' + threadID); 67 | }); 68 | 69 | // x.add(x.makeTitle('Participants')); 70 | // for (var i = 0; i < recipients.length; i++) { 71 | // var userID = recipients[i]; 72 | // x.add(await x.makeProfileButton('user', userID, { text: x.getShortID(userID) })); 73 | // } 74 | // var userID = x.currentUser.getID(); 75 | // x.add(await x.makeProfileButton('user', userID, { text: x.getShortID(userID) })); 76 | 77 | }; -------------------------------------------------------------------------------- /app/apps/group/views/members.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Dots Mesh Web App 3 | * https://github.com/dotsmesh/dotsmesh-web-app 4 | * Free to use under the GPL-3.0 license. 5 | */ 6 | 7 | async (args, library) => { 8 | var groupID = args.id; 9 | var mode = args.mode !== undefined ? args.mode : 'members'; 10 | var isMembersMode = mode === 'members'; 11 | var title = isMembersMode ? 'Group members' : 'Pending approval'; 12 | x.setTitle(title); 13 | //x.add(x.makeTitle(title)); 14 | 15 | x.addToProfile(x.makeSmallProfilePreviewComponent('group', groupID, { 16 | emptyTitle: title, 17 | hint: title, 18 | emptyText: 'No new members are waiting to be approved' 19 | })); 20 | 21 | if (!isMembersMode) { 22 | // todo check if admin 23 | x.addToolbarSecretButton('This list is visible only to the group administrators!'); 24 | } 25 | 26 | var lastSeen = []; 27 | var listComponent = x.makeComponent(async () => { 28 | lastSeen = []; 29 | var container = x.makeContainer(); 30 | if (mode === 'members') { 31 | var members = await library.getMembersList(groupID); 32 | } else if (mode === 'pendingApproval') { 33 | var members = await library.getPendingMembersList(groupID); 34 | } 35 | if (members.length === 0) { 36 | if (!isMembersMode) { 37 | x.setTemplate('empty'); 38 | return null; 39 | } 40 | } else { 41 | let list = x.makeList({ type: 'blocks' }); 42 | for (var i = 0; i < members.length; i++) { 43 | var member = members[i]; 44 | var userID = member.userID; 45 | list.add(await x.makeProfileButton('groupMember', groupID + '$' + userID, { 46 | details: x.isPublicID(userID) ? x.getShortID(userID) : 'private profile', 47 | onClick: { location: 'group/member', args: { groupID: groupID, userID: userID }, preload: true } 48 | })); 49 | lastSeen.push(userID); 50 | listComponent.observeChanges(['groupMember/' + groupID + '$' + userID + '/profile']); 51 | } 52 | container.add(list); 53 | } 54 | x.setTemplate(); 55 | return container; 56 | }, { 57 | observeChanges: ['group/' + groupID + '/members'] 58 | }); 59 | 60 | x.add(listComponent); 61 | 62 | if (!isMembersMode) { 63 | x.addToolbarNotificationsButton('gmp$' + groupID, action => { 64 | return { 65 | appID: 'group', 66 | name: 'modifyPendingMembersNotification', 67 | args: { action: action, groupID: groupID, lastSeenUsersIDs: lastSeen } 68 | } 69 | }, 'Get notified when a new member is pending approval.', 'You\'ll receive a notification when a new member is pending approval.'); 70 | x.windowEvents.addEventListener('show', async () => { 71 | await library.updatePendingMembersNotification(groupID, { lastSeenUsersIDs: lastSeen }); 72 | }); 73 | } 74 | 75 | }; -------------------------------------------------------------------------------- /app/apps/group/views/membership.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Dots Mesh Web App 3 | * https://github.com/dotsmesh/dotsmesh-web-app 4 | * Free to use under the GPL-3.0 license. 5 | */ 6 | 7 | async (args, library) => { 8 | var groupID = args.id; 9 | 10 | x.setTitle('Group membership'); 11 | 12 | var showJoinButton = false; 13 | var showLeaveButton = false; 14 | 15 | x.add(x.makeProfilePreviewComponent('group', groupID, { 16 | theme: 'light', 17 | size: 'medium' 18 | })); 19 | 20 | var details = await x.services.call('groups', 'getDetails', { groupID: groupID, details: ['status', 'invitedBy'] }); 21 | let status = details.status; 22 | if (status === 'joined') { 23 | x.add(x.makeText('You are currently a member of this group!', { align: 'center' })); 24 | showLeaveButton = true; 25 | } else if (status === 'pendingApproval') { 26 | x.add(x.makeText('Your membership must be approved by an administrator!', { align: 'center' })); 27 | showLeaveButton = true; 28 | } else { 29 | x.add(x.makeText('Welcome to the group! You can look around, but you cannot post or comment until joined!', { align: 'center' })); 30 | showJoinButton = true; 31 | showLeaveButton = true; 32 | } 33 | 34 | var container = x.makeContainer(); 35 | if (showJoinButton) { 36 | container.add(x.makeButton('Join', async () => { 37 | var userID = x.currentUser.getID(); 38 | if (x.currentUser.isPublic()) { 39 | if (await x.confirm('Are you sure you want to join this group?')) { 40 | x.showLoading(); 41 | await x.services.call('groups', 'join', { groupID: groupID }); 42 | await x.back('joined'); 43 | } 44 | } else { 45 | x.showLoading(); 46 | var data = await x.services.call('profile', 'getData', { propertyType: 'user', propertyID: userID }); 47 | x.hideLoading(); 48 | var result = await x.open('profile/form', { 49 | groupUserID: groupID + '$' + userID, 50 | image: data.image, 51 | name: data.name, 52 | description: data.description, 53 | title: 'Customize your profile', 54 | buttonText: 'Save and join group', 55 | callServiceBefore: { 56 | appID: 'groups', 57 | name: 'join', 58 | args: { groupID: groupID } 59 | } 60 | }, { modal: true, width: 300 }); 61 | await x.back(result === 'saved' ? 'joined' : null); 62 | } 63 | }, { marginTop: 'big' })); 64 | } 65 | if (showLeaveButton) { 66 | container.add(x.makeButton('Leave group', async () => { 67 | if (await x.confirm('Are you sure you want to leave this group?')) { 68 | x.showLoading(); 69 | await x.services.call('groups', 'leave', { groupID: groupID }); 70 | await x.back('left'); 71 | } 72 | }, { marginTop: showJoinButton ? null : 'big' })); 73 | } 74 | x.add(container); 75 | 76 | }; -------------------------------------------------------------------------------- /app/apps/system/views/pick.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Dots Mesh Web App 3 | * https://github.com/dotsmesh/dotsmesh-web-app 4 | * Free to use under the GPL-3.0 license. 5 | */ 6 | 7 | async (args, library) => { 8 | 9 | var type = args.type; 10 | var options = args.options !== undefined ? args.options : {}; 11 | 12 | var callService = async pickedID => { 13 | if (options.service !== undefined) { 14 | x.showLoading(); 15 | var service = options.service; 16 | var args = service.args; 17 | args.pickedID = pickedID; 18 | var result = await x.services.call(service.appID, service.name, args); 19 | x.hideLoading(); 20 | if (result.text !== undefined) { 21 | x.showMessage(result.text, { 22 | buttonText: 'OK', 23 | buttonClick: () => { 24 | x.back(result, { closeAllModals: options.closeAllModals !== undefined && options.closeAllModals }); 25 | } 26 | }); 27 | return false; 28 | } 29 | } 30 | return true; 31 | }; 32 | 33 | if (type === 'user') { 34 | x.setTitle('Pick a contact'); 35 | } else if (type === 'group') { 36 | x.setTitle('Pick a group'); 37 | } 38 | 39 | var list = x.makeList(); 40 | var addedItemsCount = 0; 41 | var emptyText = ''; 42 | if (type === 'user') { 43 | emptyText = 'There are no contacts to show!'; 44 | var contacts = await x.services.call('contacts', 'getList', { details: ['name'] }); // todo just id is needed 45 | for (let i in contacts) { 46 | var contact = contacts[i]; 47 | if (contact.providedAccessKey !== null && contact.accessKey !== null) { 48 | var userID = contact.id; 49 | list.add(await x.makeProfileButton('user', userID, { 50 | onClick: (async userID => { 51 | if (await callService(userID)) { 52 | x.back({ id: userID }); 53 | } 54 | }).bind(null, userID), 55 | details: x.getShortID(userID), 56 | style: 'style3' 57 | })); 58 | addedItemsCount++; 59 | } 60 | } 61 | } else if (type === 'group') { 62 | emptyText = 'There are no groups to show!'; 63 | var groups = await x.services.call('groups', 'getList'); 64 | for (let groupID in groups) { 65 | list.add(await x.makeProfileButton('group', groupID, { 66 | onClick: (async groupID => { 67 | if (await callService(groupID)) { 68 | x.back({ id: groupID }); 69 | } 70 | }).bind(null, groupID), 71 | style: 'style3' 72 | })); 73 | addedItemsCount++; 74 | } 75 | } 76 | if (addedItemsCount > 0) { 77 | x.add(list); 78 | } else { 79 | x.add(x.makeIcon('contacts')); 80 | x.add(x.makeText(emptyText, { align: 'center' })); 81 | x.add(x.makeButton('OK', async () => { 82 | await x.back(); 83 | }, { marginTop: 'big' })); 84 | } 85 | 86 | }; -------------------------------------------------------------------------------- /app/apps/group/propertyObserver.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Dots Mesh Web App 3 | * https://github.com/dotsmesh/dotsmesh-web-app 4 | * Free to use under the GPL-3.0 license. 5 | */ 6 | 7 | async (args, library) => { 8 | //console.log(args); 9 | var changes = args.changes; 10 | //console.table(changes); 11 | 12 | for (var change of changes) { 13 | if (change.key === 'gmp') { 14 | await x.announceChanges(['group/' + groupID + '/members']); 15 | var groupID = change.propertyID; 16 | var notificationID = 'gmp$' + groupID; 17 | if (await x.notifications.exists(notificationID)) { 18 | var usersIDs = await library.getPendingMembersUserIDs(groupID); 19 | await library.updatePendingMembersNotification(groupID, { usersIDs: usersIDs }); 20 | } 21 | } else if (change.key === 'gma') { 22 | var groupID = change.propertyID; 23 | await x.announceChanges(['group/' + groupID + '/members']); 24 | } else if (change.key === 'gm') { 25 | var groupID = change.propertyID; 26 | await x.announceChanges(['group/' + groupID + '/members']); 27 | } else if (change.key === 'gi') { 28 | var groupID = change.propertyID; 29 | await x.announceChanges(['group/' + groupID + '/invitations']); 30 | } else if (change.key === 'gp') { 31 | var groupID = change.propertyID; 32 | await x.announceChanges(['group/' + groupID + '/posts']); 33 | var notificationID = 'gp$' + groupID; 34 | if (await x.notifications.exists(notificationID)) { 35 | try { 36 | var posts = await x.services.call('posts', 'getRawPosts', { propertyType: 'group', propertyID: groupID, options: { order: 'desc', offset: 0, limit: 200, ignoreValues: true, cacheList: true, ignoreListCache: true } }); // todo update limit 37 | var postsIDs = Object.keys(posts); 38 | await library.updateGroupPostsNotification(groupID, { lastPosts: postsIDs }); 39 | } catch (e) { 40 | if (e.name === 'propertyUnavailable') { 41 | // ignore 42 | } else { 43 | throw e; 44 | } 45 | } 46 | } 47 | } else if (change.key.indexOf('gp/') === 0) { 48 | var groupID = change.propertyID; 49 | var postID = change.key.substr('gp/'.length); 50 | await x.announceChanges(['group/' + groupID + '/posts/' + postID]); 51 | var notificationID = 'gpr$' + groupID + '$' + postID; 52 | if (await x.notifications.exists(notificationID)) { 53 | try { 54 | var postReactionsIDs = await x.services.call('posts', 'getPostReactionsIDs', { propertyType: 'group', propertyID: groupID, postID: postID }); 55 | await library.updateGroupPostReactionsNotification(groupID, postID, { lastPostReactions: postReactionsIDs }); 56 | } catch (e) { 57 | if (e.name === 'propertyUnavailable') { 58 | // ignore 59 | } else { 60 | throw e; 61 | } 62 | } 63 | } 64 | } 65 | } 66 | } -------------------------------------------------------------------------------- /app/apps/settings/views/home.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Dots Mesh Web App 3 | * https://github.com/dotsmesh/dotsmesh-web-app 4 | * Free to use under the GPL-3.0 license. 5 | */ 6 | 7 | async (args, library) => { 8 | var title = 'Settings'; 9 | x.setTitle(title); 10 | x.setTemplate('column'); 11 | 12 | x.addToProfile(x.makeAppPreviewComponent('settings', { 13 | title: title 14 | })); 15 | 16 | var list = x.makeList({ type: 'blocks' }); 17 | 18 | list.add(x.makeTextButton(async () => { 19 | x.alert('Currently, English is the only supported language. Contact us and tell us which one we should add next.'); 20 | }, 'Language', { details: 'English' })); 21 | 22 | // list.add(x.makeTextButton(async () => { 23 | // x.alert('Not implemented yet!'); 24 | // }, 'Theme', 'Default')); 25 | 26 | // if (x.currentUser.exists()) { 27 | // x.addToolbarButton(async () => { 28 | // await x.currentUser.logout(); 29 | // }, 'logout', 'right'); 30 | // } 31 | list.add(x.makeComponent(async () => { 32 | var list = x.makeList(); 33 | 34 | list.add(x.makeTextButton(async () => { 35 | x.open('system/manageDeviceNotifications', {}, { modal: true, width: 300 }); 36 | }, 'Device notifications', { details: x.deviceHasPushManagerSupport() && await x.currentUser.getDeviceNotificationsStatus() === 'enabled' ? 'Enabled' : 'Disabled' })); 37 | 38 | return list; 39 | }, { observeChanges: ['deviceNotificationsStatus'] })); 40 | 41 | list.add(x.makeTextButton(async () => { 42 | if (await x.confirm('Are you sure you want to remove the locally stored data?', { icon: 'delete' })) { 43 | x.showLoading(); 44 | // announce clear local caches too 45 | await x.currentUserCache.clear(); 46 | await x.appCache.clear(); 47 | x.hideLoading(); 48 | x.alert('Cache is cleared successfully!'); 49 | } 50 | }, 'Clear cache', { details: 'Remove locally stored data' })); 51 | 52 | //list.add(x.makeSeparator()); 53 | 54 | list.add(x.makeTextButton(async () => { 55 | x.open('settings/feedback', {}, { modal: true, width: 400 }); 56 | }, 'Feedback', { details: 'Share your app experience with the Dots Mesh team' })); 57 | 58 | list.add(x.makeTextButton(async () => { 59 | x.open('settings/about', {}, { modal: true, width: 300 }); 60 | }, 'About', { details: 'Learn more about this app' })); 61 | 62 | //list.add(x.makeSeparator()); 63 | 64 | if (x.currentUser.isPublic()) { 65 | 66 | // x.add(x.makeTextButton(async () => { 67 | // x.alert('Not implemented yet!'); 68 | // }, 'View active sessions', '')); 69 | 70 | list.add(x.makeTextButton(async () => { 71 | x.open('user/changePassword', {}, { modal: true, width: 300 }); 72 | }, 'Change password', { details: 'Change your account\'s password' })); 73 | 74 | // x.add(x.makeTextButton(async () => { 75 | // x.alert('Not implemented yet!'); 76 | // }, 'Export data', '')); 77 | 78 | // x.add(x.makeTextButton(async () => { 79 | // x.alert('Not implemented yet!'); 80 | // }, 'Delete account', '')); 81 | } 82 | 83 | list.add(x.makeTextButton(async () => { 84 | await x.currentUser.logout(); 85 | }, 'Log out', { details: 'Remove your account from this device' })); 86 | 87 | x.add(list); 88 | 89 | }; -------------------------------------------------------------------------------- /app/apps/group/views/member.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Dots Mesh Web App 3 | * https://github.com/dotsmesh/dotsmesh-web-app 4 | * Free to use under the GPL-3.0 license. 5 | */ 6 | 7 | async (args, library) => { 8 | var groupID = args.groupID; 9 | var userID = args.userID; 10 | 11 | x.setTitle('Member profile'); 12 | 13 | var memberData = await library.getMemberData(groupID, userID); 14 | if (memberData === null) { 15 | x.showMessage('Member not found!'); 16 | return; 17 | } 18 | 19 | //var memberID = await library.getMemberID(groupID, userID); 20 | 21 | // var dataStorage = await x.group.getCurrentMemberSharedDataStorage(groupID); 22 | 23 | // var dataStorage = await x.group.getMemberSharedDataStorage(groupID, memberID); 24 | 25 | // console.log(await library.isMember(groupID, userID)); 26 | // console.log(await library.getMemberData(groupID, userID)); 27 | // console.log(await library.getMemberDetails(groupID, userID)); 28 | 29 | var currentUserID = x.currentUser.getID(); 30 | var isCurrentUser = x.currentUser.isMatch(userID); 31 | 32 | //x.setTemplate('columns'); 33 | 34 | var component = x.makeProfilePreviewComponent('groupMember', groupID + '$' + userID, { 35 | //userGroupMemberID: memberID, 36 | showEditButton: isCurrentUser && x.isPrivateID(currentUserID) 37 | }); 38 | x.addToProfile(component); 39 | 40 | var isAdministrator = await x.services.call('groups', 'isAdministrator', { groupID: groupID }); 41 | if (isAdministrator && !isCurrentUser) { 42 | var component = x.makeSecretComponent('Administrators only', async component2 => { 43 | var memberData = await library.getMemberData(groupID, userID); 44 | if (memberData !== null) { // just removed 45 | if (memberData.status === 'pendingApproval') { 46 | var button = x.makeButton('Approve or deny membership', async () => { 47 | var result = await x.open('group/approve', { groupID: groupID, userID: userID }, { modal: true, width: 300 }); 48 | if (result === 'removed') { 49 | x.showMessage('The membership request has been denied!'); 50 | } 51 | }); 52 | component2.add(button); 53 | } else { 54 | var button = x.makeButton('Remove from group', async () => { 55 | var result = await x.open('group/remove', { groupID: groupID, userID: userID }, { modal: true, width: 300 }); 56 | if (result === 'removed') { 57 | x.showMessage('The member has been removed!'); 58 | } 59 | }); 60 | component2.add(button); 61 | } 62 | } 63 | }); 64 | component.observeChanges(['group/' + groupID + '/members', 'group/' + groupID + '/member/' + userID, 'group/' + groupID + '/invitations']); 65 | x.addToProfile(component); 66 | } 67 | 68 | //x.add(x.makeTitle('Recent activity')); 69 | 70 | //x.add(x.makeHint('This feature is coming soon!')); 71 | 72 | x.setTemplate('empty'); 73 | 74 | // var component = await x.makePostsListComponent(async options => { 75 | // //console.table(await library.getMemberActivity(groupID, userID)); 76 | // return []; 77 | // }); 78 | // //component.observeChanges(['user/' + userID + '/profile', 'user/' + userID + '/posts']); 79 | // x.add(component); 80 | 81 | }; -------------------------------------------------------------------------------- /app/apps/groups/views/home.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Dots Mesh Web App 3 | * https://github.com/dotsmesh/dotsmesh-web-app 4 | * Free to use under the GPL-3.0 license. 5 | */ 6 | 7 | async (args, library) => { 8 | var title = 'Groups'; 9 | x.setTitle(title); 10 | 11 | // x.add(x.makeTitle('Groups', { 12 | // buttonOnClick: async () => { 13 | // var groupID = await x.open('groups/createForm', {}, { modal: true, width: 300 }); 14 | // if (groupID !== null) { 15 | // x.open('group/home', { id: groupID }); 16 | // } 17 | // } 18 | // })); 19 | 20 | x.addToProfile(x.makeAppPreviewComponent('groups', { 21 | emptyTitle: title, 22 | emptyText: 'Groups are a great place to share, collaborate, discuss, or just have fun with others.', 23 | actionButton: async () => { 24 | return { 25 | onClick: async () => { 26 | var groupID = await x.open('groups/createForm', {}, { modal: true, width: 300 }); 27 | if (groupID !== null) { 28 | x.open('group/home', { id: groupID }); 29 | } 30 | }, 31 | text: 'New group' 32 | } 33 | }, 34 | })); 35 | 36 | // x.add(x.makeButton('New group', async () => { 37 | // var groupID = await x.open('groups/createForm', {}, { modal: true, width: 300 }); 38 | // if (groupID !== null) { 39 | // x.open('group/home', { id: groupID }); 40 | // } 41 | // }, { style: 'style2' })); 42 | 43 | // x.add(x.makeIconButton(async () => { 44 | // var groupID = await x.open('groups/createForm', {}, { modal: true, width: 300 }); 45 | // if (groupID !== null) { 46 | // x.open('group/home', { id: groupID }); 47 | // } 48 | // }, 'x-icon-plus-white', 'New group'));//, null, true 49 | 50 | var component = x.makeComponent(async () => { 51 | var groups = await library.getList(['status']); 52 | var groupsCount = Object.keys(groups).length; 53 | if (groupsCount === 0) { 54 | x.setTemplate('empty'); 55 | //var container = x.makeContainer({ addSpacing: true }); 56 | //container.add(x.makeHint('Groups are a great place to share, collaborate, discuss, or just have fun with others.', { align: 'center' })); 57 | // container.add(x.makeSmallTitle('Want to try one?')); 58 | // container.add(x.makeHint('There is a private demo group you can join to take a look this feature of the platform. Get a hosting key to create your own.')); 59 | // container.add(x.makeButton('Visit', async () => { 60 | // x.showLoading(); 61 | // var groupID = 'oofiy12zxb5vvg7.xbg.dotsmesh.com'; 62 | // await x.services.call('groups', 'addURLInvitation', { groupID: groupID, accessKey: 'noktu0add35v6f76n27nmrbifgq8rvxjuhbayxozgwuq3y0a3ri9rgf' }); 63 | // await x.open('group/home', { id: groupID }); 64 | // }, { style: 'style2' })); 65 | //return container; 66 | return null; 67 | } else { 68 | x.setTemplate(); 69 | var list = x.makeList({ type: 'blocks' }); 70 | for (let groupID in groups) { 71 | var groupDetails = groups[groupID]; 72 | //var status = groupDetails['status']; 73 | list.add(await x.makeProfileButton('group', groupID)); 74 | component.observeChanges(['group/' + groupID + '/profile']); 75 | } 76 | return list; 77 | } 78 | }); 79 | component.observeChanges(['groups']); 80 | x.add(component); 81 | }; -------------------------------------------------------------------------------- /app/appjs-builder.php: -------------------------------------------------------------------------------- 1 | 0) { 58 | $js .= 'inbox:' . $getJSFunction($dir . '/inbox.js') . ','; 59 | } 60 | if (isset($manifest['propertyObserver']) && (int) $manifest['propertyObserver'] > 0) { 61 | $js .= 'propertyObserver:' . $getJSFunction($dir . '/propertyObserver.js') . ','; 62 | } 63 | if (isset($manifest['library']) && (int) $manifest['library'] > 0) { 64 | $js .= 'library:' . $getJSFunction($dir . '/library.js') . ','; 65 | } 66 | $js = rtrim($js, ','); 67 | $js .= '}'; 68 | return $js; 69 | }; 70 | 71 | $sourceCode = '{'; 72 | $sourceCode .= 'core:' . $getJSFunction(__DIR__ . '/library/core.js') . ','; 73 | $sourceCode .= 'app:' . $getJSFunction(__DIR__ . '/library/app.js') . ','; 74 | $sourceCode .= 'sandboxProxy:' . $getJSFunction(__DIR__ . '/library/sandboxProxy.js') . ','; 75 | $sourceCode .= 'sandboxWindow:' . $getJSFunction(__DIR__ . '/library/sandboxWindow.js') . ','; 76 | $sourceCode .= 'sandboxWorker:' . $getJSFunction(__DIR__ . '/library/sandboxWorker.js') . ','; 77 | $sourceCode .= 'utilities:' . $getJSFunction(__DIR__ . '/library/utilities.js') . ','; 78 | $sourceCode .= 'apps:{'; 79 | foreach ($apps as $appID => $appDir) { 80 | $sourceCode .= '"' . $appID . '":' . $getAppJS($appDir) . ','; 81 | } 82 | $sourceCode = rtrim($sourceCode, ','); 83 | $sourceCode .= '}'; 84 | $sourceCode .= '}'; 85 | $js = 'self.x=typeof self.x==="undefined"?{}:self.x;(' . $getJSFunction(__DIR__ . '/library/library.js') . ')(self.x,' . $sourceCode . ');'; 86 | return $js; 87 | } 88 | }; 89 | -------------------------------------------------------------------------------- /app/apps/group/views/invitations.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Dots Mesh Web App 3 | * https://github.com/dotsmesh/dotsmesh-web-app 4 | * Free to use under the GPL-3.0 license. 5 | */ 6 | 7 | async (args, library) => { 8 | var groupID = args.id; 9 | var title = 'Group invitations'; 10 | x.setTitle(title); 11 | 12 | x.addToProfile(x.makeSmallProfilePreviewComponent('group', groupID, { 13 | emptyTitle: title, 14 | hint: 'Group invitations', 15 | emptyText: 'There are no personal pending invitations or invitation URLs!' 16 | })); 17 | 18 | // todo check if admin 19 | x.addToolbarSecretButton('This list is visible only to the group administrators!'); 20 | 21 | //x.add(x.makeSmallTitle('Personal')); 22 | 23 | x.add(x.makeComponent(async () => { 24 | var container = x.makeContainer(); 25 | var invitations = await library.getInvitationsList(groupID); // todo optimize because there are two requests 26 | var addedCount = 0; 27 | let list = x.makeList({ type: 'blocks' }); 28 | for (var i = 0; i < invitations.length; i++) { 29 | var invitation = invitations[i]; 30 | if (invitation.type === 'personalInvitation') { 31 | var userID = invitation.userID; 32 | list.add(await x.makeProfileButton('user', userID, { 33 | onClick: (invitationID => { 34 | x.open('group/invitationDetails', { groupID: groupID, invitationID: invitationID }, { modal: true, width: 300 }); 35 | }).bind(null, invitation.id) 36 | })); 37 | addedCount++; 38 | } else if (invitation.type === 'urlInvitation') { 39 | list.add(await x.makeIconButton((invitationID => { 40 | x.open('group/invitationDetails', { groupID: groupID, invitationID: invitationID }, { modal: true, width: 300 }); 41 | }).bind(null, invitation.id), 'key', 'Created by ' + x.getShortID(invitation.createdBy), { 42 | details: x.getHumanDate(invitation.dateCreated), 43 | imageSize: 50 44 | })); 45 | addedCount++; 46 | } 47 | } 48 | if (addedCount > 0) { 49 | x.setTemplate(); 50 | container.add(list); 51 | } else { 52 | x.setTemplate('empty'); 53 | return null; 54 | } 55 | return container; 56 | }, { 57 | observeChanges: ['group/' + groupID + '/invitations'] 58 | })); 59 | 60 | //x.add(x.makeSmallTitle('Invitation URLs')); 61 | 62 | // x.add(x.makeComponent(async () => { 63 | // var container = x.makeContainer(); 64 | // var invitations = await library.getInvitationsList(groupID); // todo optimize because there are two requests 65 | // var addedCount = 0; 66 | // let list = x.makeList({ type: 'grid' }); 67 | // for (var i = 0; i < invitations.length; i++) { 68 | // var invitation = invitations[i]; 69 | // if (invitation.type === 'urlInvitation') { 70 | // list.add(await x.makeIconButton((invitationID => { 71 | // x.open('group/invitationDetails', { groupID: groupID, invitationID: invitationID }, { modal: true, width: 300 }); 72 | // }).bind(null, invitation.id), 'key', 'Created by ' + x.getShortID(invitation.createdBy), { 73 | // details: x.getHumanDate(invitation.dateCreated), 74 | // imageSize: 50 75 | // })); 76 | // addedCount++; 77 | // } 78 | // } 79 | // if (addedCount > 0) { 80 | // container.add(list); 81 | // } else { 82 | // container.add(x.makeHint('There are no active invitation URLs!')); 83 | // } 84 | // return container; 85 | // }, { 86 | // observeChanges: ['group/' + groupID + '/invitations'] 87 | // })); 88 | 89 | }; -------------------------------------------------------------------------------- /app/apps/contacts/views/home.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Dots Mesh Web App 3 | * https://github.com/dotsmesh/dotsmesh-web-app 4 | * Free to use under the GPL-3.0 license. 5 | */ 6 | 7 | async (args, library) => { 8 | var title = 'Contacts'; 9 | x.setTitle(title); 10 | //x.setTemplate('column'); 11 | 12 | // x.add(x.makeTitle('Contacts', { 13 | // buttonOnClick: () => { 14 | // x.open('contacts/form', {}, { modal: true, width: 300 }); 15 | // } 16 | // })); 17 | 18 | x.addToProfile(x.makeAppPreviewComponent('contacts', { 19 | emptyTitle: title, 20 | emptyText: 'Find a contact by entering its ID and send a connection request.', // todo for private profiles 21 | actionButton: async () => { 22 | return { 23 | onClick: async () => { 24 | x.open('contacts/form', {}, { modal: true, width: 300 }); 25 | }, 26 | text: 'New contact' 27 | } 28 | }, 29 | })); 30 | 31 | // var container = x.makeContainer({ addSpacing: true}); 32 | // container.add(x.makeButton('New contact', () => { 33 | // x.open('contacts/form', {}, { modal: true, width: 300 }); 34 | // }, { style: 'style2', icon: 'plus' })); 35 | // x.add(container); 36 | 37 | // x.add(x.makeButton('New contact', () => { 38 | // x.open('contacts/form', {}, { modal: true, width: 300 }); 39 | // }, { style: 'style2' })); 40 | 41 | //x.add(x.makeSmallTitle('Connected')); 42 | 43 | x.add(x.makeComponent(async () => { 44 | var contacts = await library.getList(); 45 | var list = x.makeList({ type: 'blocks' }); 46 | var itemsCount = 0; 47 | 48 | if (x.currentUser.isPublic()) { 49 | var requests = await library.getRequestsList(); 50 | for (var i = 0; i < requests.length; i++) { 51 | var request = requests[i]; 52 | var userID = request.userID; 53 | list.add(await x.makeProfileButton('user', userID, { details: x.getShortID(userID) })); 54 | itemsCount++; 55 | } 56 | } 57 | 58 | for (var i = 0; i < contacts.length; i++) { 59 | var contact = contacts[i]; 60 | var userID = contact.id; 61 | // if (contact.providedAccessKey !== null && contact.accessKey !== null) { 62 | // suffix = ' / Connected'; 63 | // } 64 | // + ' / ' + contact.invitationSource 65 | list.add(await x.makeProfileButton('user', userID, { details: x.getShortID(userID) })); 66 | itemsCount++; 67 | } 68 | 69 | if (itemsCount === 0) { 70 | x.setTemplate('empty'); 71 | //return x.makeHint('No added contacts. Find a contact by entering its ID and send a connection request.'); 72 | } else { 73 | x.setTemplate(); 74 | } 75 | return list; 76 | }, { observeChanges: ['contacts', 'contactsRequests'] })); 77 | 78 | if (x.currentUser.isPublic()) { 79 | 80 | x.addToolbarButton('Ways to connect', async () => { 81 | x.open('contacts/connectSettings'); 82 | }, 'settings'); 83 | 84 | // x.add(x.makeSmallTitle('Requests')); 85 | 86 | // x.add(x.makeComponent(async () => { 87 | // var requests = await library.getRequestsList(); 88 | // var list = x.makeList({ type: 'blocks' }); 89 | // var itemsCount = 0; 90 | // for (var i = 0; i < requests.length; i++) { 91 | // var request = requests[i]; 92 | // var userID = request.userID; 93 | // list.add(await x.makeProfileButton('user', userID, { details: x.getShortID(userID) })); 94 | // itemsCount++; 95 | // } 96 | // if (itemsCount === 0) { 97 | // return x.makeHint('No pending connection requests.'); 98 | // } 99 | // return list; 100 | // }, { observeChanges: ['contactsRequests'] })); 101 | 102 | } 103 | }; -------------------------------------------------------------------------------- /app/apps/messages/views/home.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Dots Mesh Web App 3 | * https://github.com/dotsmesh/dotsmesh-web-app 4 | * Free to use under the GPL-3.0 license. 5 | */ 6 | 7 | async (args, library) => { 8 | var title = 'Messages'; 9 | x.setTitle(title); 10 | //x.setTemplate('column'); 11 | 12 | if (x.currentUser.isPrivate()) { 13 | x.addToProfile(x.makeAppPreviewComponent('messages', { 14 | emptyTitle: title, 15 | emptyText: 'This feature is currently available for public profiles only.' 16 | })); 17 | x.setTemplate('empty'); 18 | } else { 19 | 20 | // x.add(x.makeTitle('Messages', { 21 | // buttonOnClick: () => { 22 | // x.pickContact((userID) => { // todo must be connected 23 | // x.open('messages/thread', { userID: userID }); 24 | // }) 25 | // } 26 | // })); 27 | 28 | x.addToProfile(x.makeAppPreviewComponent('messages', { 29 | emptyTitle: title, 30 | emptyText: 'A private communication channel with your contacts.', 31 | actionButton: async () => { 32 | return { 33 | onClick: async () => { 34 | x.pickContact((userID) => { // todo must be connected 35 | x.open('messages/thread', { userID: userID }); 36 | }) 37 | }, 38 | text: 'New chat' 39 | } 40 | }, 41 | })); 42 | 43 | // x.add(x.makeTitle('Messages', { 44 | // })); 45 | 46 | // x.add(x.makeButton('New chat', () => { 47 | // x.pickContact((userID) => { // todo must be connected 48 | // x.open('messages/thread', { userID: userID }); 49 | // }) 50 | // }, { style: 'style2' })); 51 | 52 | // x.add(x.makeIconButton(() => { 53 | // x.pickContact((userID) => { // todo must be connected 54 | // x.open('messages/thread', { userID: userID }); 55 | // }) 56 | // }, 'x-icon-plus-white', 'New message'));//, null, true 57 | 58 | var component = x.makeComponent(async () => { 59 | var threads = await library.getLatestThreads(); 60 | var threadsCount = threads.length; 61 | if (threadsCount === 0) { 62 | x.setTemplate('empty'); 63 | return null; 64 | } else { 65 | x.setTemplate(); 66 | var list = x.makeList({ type: 'blocks' }); 67 | for (var i = 0; i < threadsCount; i++) { 68 | let thread = threads[i]; 69 | let otherParticipantsIDs = thread.otherParticipantsIDs; 70 | var details = ''; 71 | if (thread.message !== null) { 72 | var message = thread.message; 73 | var senderName = (await x.user.getProfile(message.userID)).name; 74 | var text = message.text; 75 | if (message.textType === 'r') { 76 | text = x.convertRichText(text, 'text'); 77 | } 78 | details = senderName + ': ' + x.stringReplaceAll(text, "\n", " "); 79 | } 80 | let item = await x.makeProfileButton('user', otherParticipantsIDs[0], { 81 | details: details, 82 | onClick: { location: 'messages/thread', args: { threadID: thread.id }, preload: true } 83 | }); 84 | var notificationID = 'm$' + thread.id; 85 | if (await x.notifications.exists(notificationID)) { 86 | item.element.setAttribute('x-notification-badge', ''); 87 | } 88 | component.observeChanges(['notifications/' + notificationID]); 89 | list.add(item); 90 | } 91 | return list; 92 | } 93 | }, { 94 | observeChanges: ['messages'] 95 | }) 96 | x.add(component); 97 | } 98 | }; -------------------------------------------------------------------------------- /app/apps/user/views/home.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Dots Mesh Web App 3 | * https://github.com/dotsmesh/dotsmesh-web-app 4 | * Free to use under the GPL-3.0 license. 5 | */ 6 | 7 | async (args, library) => { 8 | 9 | var userID = args.userID !== undefined ? args.userID : x.currentUser.getID(); 10 | var connectKey = args.connectKey !== undefined ? args.connectKey : null; 11 | 12 | x.addErrorHandler(['userNotFound'], async () => { 13 | x.showMessage('There is no profile named ' + x.getShortID(userID) + '!'); 14 | }); 15 | x.addErrorHandler(['invalidUserID'], async () => { 16 | x.showMessage('Profile not found!'); 17 | }); 18 | x.addErrorHandler(['propertyUnavailable'], async () => { 19 | x.showMessage(x.getShortID(userID) + '\'s profile is currently unavailable!'); 20 | }); 21 | if (userID === null) { 22 | throw x.makeAppError('invalidUserID'); 23 | } 24 | 25 | var isCurrentUser = x.currentUser.isMatch(userID); 26 | var currentUserExists = x.currentUser.exists(); 27 | 28 | if (x.isPublicID(userID)) { 29 | x.setHash(x.getShortID(userID) + (connectKey !== null ? '/c/' + connectKey : '')); 30 | } 31 | 32 | if (isCurrentUser) { 33 | x.setTitle('My public profile'); 34 | } else { 35 | x.wait(async () => { 36 | var profile = await x.user.getProfile(userID); 37 | x.setTitle(profile.name + '\'s public profile'); 38 | }) 39 | } 40 | 41 | var emptyText = null; 42 | if (isCurrentUser) { 43 | if (x.currentUser.isPublic()) { 44 | emptyText = 'All your posts are publicly visible by everyone, including your contacts and followers. They can share easily with their audience too.'; 45 | } else { 46 | emptyText = 'You are currently signed in with a private profile. Your profile exists only on this device, but you can freely follow others, build a contacts list and join groups.'; 47 | x.setTemplate('empty'); 48 | } 49 | } 50 | 51 | x.addToProfile(x.makeProfilePreviewComponent('user', userID, { 52 | showEditButton: isCurrentUser, 53 | connectKey: connectKey, 54 | actionButton: async () => { 55 | if (isCurrentUser && x.currentUser.isPublic()) { 56 | return { 57 | onClick: () => { 58 | x.open('posts/form', { userID: x.currentUser.getID() }, { modal: true }); 59 | }, 60 | text: 'New post' 61 | } 62 | } 63 | return null; 64 | }, 65 | emptyText: emptyText 66 | })); 67 | 68 | if (x.isPublicID(userID)) { 69 | var listComponent = await x.makePostsListComponent(async (options) => { 70 | var result = await x.property.getPosts('user', userID, { order: options.order, offset: options.offset, limit: options.limit, cacheList: true, cacheValues: true }); 71 | if (!isCurrentUser) { 72 | x.property.checkForNewPosts('user', userID, result.length > 0 ? result[0].id : null); 73 | } 74 | x.setTemplate(result.length === 0 ? 'empty' : null); 75 | return result; 76 | }, { 77 | showUser: false 78 | }); 79 | listComponent.observeChanges(['user/' + userID + '/profile', 'user/' + userID + '/posts']); 80 | x.add(listComponent); 81 | 82 | if (!isCurrentUser && currentUserExists) { 83 | x.addToolbarNotificationsButton( 84 | 'up$' + userID, 85 | (action) => { 86 | return { 87 | appID: 'user', 88 | name: 'modifyUserPostsNotification', 89 | args: { action: action, userID: userID, lastSeenPosts: listComponent.getLastSeen() } 90 | } 91 | }, 92 | 'Get notified when there are new posts here.', 93 | 'You\'ll receive a notification when there are new posts here.' 94 | ); 95 | x.windowEvents.addEventListener('show', async () => { 96 | await library.updateUserPostsNotification(userID, { lastSeenPosts: listComponent.getLastSeen() }); 97 | }); 98 | } 99 | } 100 | 101 | }; 102 | -------------------------------------------------------------------------------- /app/apps/user/library.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Dots Mesh Web App 3 | * https://github.com/dotsmesh/dotsmesh-web-app 4 | * Free to use under the GPL-3.0 license. 5 | */ 6 | 7 | () => { 8 | 9 | var updateUserPostsNotification = async (userID, update) => { 10 | var notificationID = 'up$' + userID; 11 | var notification = await x.notifications.get(notificationID); 12 | if (notification === null) { 13 | return; 14 | } 15 | 16 | if (update.lastPosts !== undefined) { 17 | if (x.isSameContent(notification.data.i, update.lastPosts)) { 18 | return; 19 | } 20 | notification.data.i = update.lastPosts; 21 | } else if (update.lastSeenPosts !== undefined) { 22 | if (x.isSameContent(notification.data.l, update.lastSeenPosts)) { 23 | return; 24 | } 25 | notification.data.l = update.lastSeenPosts; 26 | } else { 27 | throw new Error(); 28 | } 29 | 30 | var lastPostsIDs = x.shallowCopyArray(notification.data.i); 31 | var lastSeenPostsIDs = x.shallowCopyArray(notification.data.l); 32 | 33 | lastPostsIDs.sort().reverse(); 34 | lastSeenPostsIDs.sort().reverse(); 35 | var lastSeenPostID = null; 36 | for (var postID of lastSeenPostsIDs) { 37 | if (lastPostsIDs.indexOf(postID) !== null) { 38 | lastSeenPostID = postID; 39 | break; 40 | } 41 | } 42 | var notSeenPosts = []; 43 | if (lastSeenPostID !== null) { 44 | var index = lastPostsIDs.indexOf(lastSeenPostID); 45 | if (index === 0) { 46 | // seen all 47 | } else { 48 | notSeenPosts = lastPostsIDs.slice(0, index); 49 | } 50 | } else { 51 | // cannot find 52 | notSeenPosts = lastPostsIDs.slice(0, 11); // to show 10+ 53 | } 54 | 55 | var notSeenPostsCount = notSeenPosts.length; 56 | if (notSeenPostsCount > 0) { 57 | if (!x.isSameContent(notification.data.n, notSeenPosts)) { 58 | var profile = await x.property.getProfile('user', userID); 59 | //var title = ''; 60 | var text = ''; 61 | if (notSeenPostsCount === 1) { 62 | //title = 'New post by '; 63 | text = '1 new post'; 64 | } else if (notSeenPostsCount === 11) { 65 | //title = 'News posts by '; 66 | text = '10+ new posts'; 67 | } else { 68 | //title = 'News posts by '; 69 | text = notSeenPostsCount + ' new posts'; 70 | } 71 | //title += profile.name; 72 | //text += profile.name; 73 | 74 | notification.visible = true; 75 | notification.title = profile.name; 76 | notification.text = text; 77 | notification.image = { type: 'userProfile', id: userID }; 78 | notification.data.n = notSeenPosts; 79 | notification.tags = ['u', 'p']; 80 | } 81 | } else { 82 | notification.visible = false; 83 | notification.title = null; 84 | notification.text = null; 85 | } 86 | await x.notifications.set(notification); 87 | }; 88 | 89 | var modifyUserPostsNotification = async (action, userID, lastSeenPosts) => { 90 | var notificationID = 'up$' + userID; 91 | if (action === 'add') { 92 | await x.property.observeChanges(userID, ['up'], 'n'); 93 | var notification = await x.notifications.make(notificationID); 94 | notification.data = { l: lastSeenPosts, i: [], n: [] }; 95 | notification.onClick = { location: 'user/home', args: { userID: userID } }; 96 | await x.notifications.set(notification); 97 | } else { 98 | await x.property.unobserveChanges(userID, ['up'], 'n'); 99 | await x.notifications.delete(notificationID); 100 | } 101 | }; 102 | 103 | var getProfileImage = async (userID, size) => { 104 | var profile = await x.user.getProfile(userID); 105 | return await profile.getImage(size); 106 | }; 107 | 108 | return { 109 | updateUserPostsNotification: updateUserPostsNotification, 110 | modifyUserPostsNotification: modifyUserPostsNotification, 111 | getProfileImage: getProfileImage 112 | }; 113 | }; -------------------------------------------------------------------------------- /app/apps/home/views/home.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Dots Mesh Web App 3 | * https://github.com/dotsmesh/dotsmesh-web-app 4 | * Free to use under the GPL-3.0 license. 5 | */ 6 | 7 | async (args, library) => { 8 | x.setTitle('Notifications'); 9 | 10 | x.addToProfile(x.makeAppPreviewComponent('home', { 11 | emptyTitle: 'Notifications', 12 | emptyText: 'This is the place where all the things that may require your attention will be shown. If empty, you can enjoy a cup of coffee, do a few pushups, or explore what others have shared.' 13 | })); 14 | 15 | // var profile = await x.user.getProfile('333.dotshost1.local'); 16 | // var message = {}; 17 | // message.userID = '333.dotshost1.local'; 18 | // message.text = 'asdasdasd'; 19 | // var threadID = 'r2'; 20 | // var notification = await x.notifications.make('m$' + threadID); 21 | // notification.visible = true; 22 | // notification.title = 'Message from ' + profile.name; 23 | // notification.text = message.text; 24 | // notification.image = { type: 'userProfile', id: message.userID }; 25 | // notification.onClick = { location: 'messages/thread', args: { threadID: threadID } }; 26 | // await x.notifications.set(notification); 27 | 28 | var groups = { 29 | m: { name: 'Messages', items: [] }, 30 | c: { name: 'Contacts', items: [] }, 31 | g: { name: 'Groups', items: [] }, 32 | p: { name: 'Posts', items: [] }, 33 | u: { name: 'Updates', items: [] }, 34 | o: { name: 'Others', items: [] } 35 | }; 36 | 37 | var component = x.makeComponent(async () => { 38 | let container = x.makeContainer({ addSpacing: true }); 39 | var notifications = await x.notifications.getList(); 40 | var hasContent = false; 41 | for (var notification of notifications) { 42 | if (!notification.visible) { 43 | continue; 44 | } 45 | var onClick = (async notification => { 46 | x.notifications.onClick(await x.notifications.getClickData(notification)); 47 | }).bind(null, notification); 48 | var title = notification.title; 49 | var text = notification.text; 50 | var image = notification.image; 51 | var date = x.getHumanDate(notification.date); 52 | var button = null; 53 | if (image !== null) { 54 | if (image.type === 'userProfile') { 55 | button = await x.makeProfileButton('user', image.id, { text: title, details: text, hint: date, onClick: onClick }); 56 | } else if (image.type === 'groupProfile') { 57 | button = await x.makeProfileButton('group', image.id, { text: title, details: text, hint: date, onClick: onClick }); 58 | } 59 | } 60 | if (button === null) { 61 | button = x.makeTextButton(onClick, title, text, { details: date }); 62 | } 63 | var groupID = 'o'; 64 | var tags = notification.tags; 65 | if (tags.indexOf('m') !== -1) { 66 | groupID = 'm'; 67 | } else if (tags.indexOf('c') !== -1) { 68 | groupID = 'c'; 69 | } else if (tags.indexOf('p') !== -1) { 70 | groupID = 'p'; 71 | } else if (tags.indexOf('r') !== -1) { 72 | groupID = 'u'; 73 | } else if (tags.indexOf('g') !== -1) { 74 | groupID = 'g'; 75 | } 76 | groups[groupID].items.push(button); 77 | hasContent = true; 78 | } 79 | for (var groupID in groups) { 80 | var group = groups[groupID]; 81 | var items = group.items; 82 | if (items.length > 0) { 83 | container.add(x.makeSmallTitle(group.name)); 84 | let list = x.makeList({ type: 'blocks' }); 85 | for (var item of items) { 86 | list.add(item); 87 | } 88 | container.add(list); 89 | } 90 | } 91 | if (hasContent) { 92 | x.setTemplate(); 93 | return container; 94 | } else { 95 | x.setTemplate('empty'); 96 | return null; 97 | // return x.makeHint('This is the place where all the things that require your attention will be shown. If empty, you can enjoy a cup of coffee, do a few pushups, or explore what others have shared.', { align: 'center' }) 98 | } 99 | }, { observeChanges: ['notifications'] }); 100 | x.add(component); 101 | }; -------------------------------------------------------------------------------- /app/apps/explore/views/home.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Dots Mesh Web App 3 | * https://github.com/dotsmesh/dotsmesh-web-app 4 | * Free to use under the GPL-3.0 license. 5 | */ 6 | 7 | async (args, library) => { 8 | var title = 'Explore'; 9 | x.setTitle(title); 10 | 11 | x.addToProfile(x.makeAppPreviewComponent('explore', { 12 | emptyTitle: title, 13 | emptyText: 'This is the place where the posts of all the people and groups you follow will be listed. You can easily dive into a specific post to share and discuss.' 14 | })); 15 | 16 | x.addToolbarButton('Who am I following', async () => { 17 | x.open('explore/following'); 18 | }, 'settings'); 19 | 20 | // var component = x.makeComponent(async () => { 21 | // var following = await library.getFollowing(); 22 | 23 | // var container = x.makeContainer({ addSpacing: true, align: 'center' }); 24 | 25 | // var followingCount = following.length; 26 | // if (followingCount > 0) { 27 | // var profilesCount = 0; 28 | // var groupsCount = 0; 29 | // for (var typedPropertyID of following) { 30 | // var propertyData = x.parseTypedID(typedPropertyID); 31 | // if (propertyData.type === 'group') { 32 | // groupsCount++; 33 | // } else { 34 | // profilesCount++; 35 | // } 36 | // } 37 | 38 | // var text = null; 39 | // if (profilesCount > 1) { 40 | // if (groupsCount > 1) { 41 | // text = profilesCount + ' profiles and ' + groupsCount + ' groups';//'Following ' + 42 | // } else if (groupsCount === 1) { 43 | // text = profilesCount + ' profiles and 1 group';//'Following ' + 44 | // } else { 45 | // text = profilesCount + ' profiles';//'Following ' + 46 | // } 47 | // } else if (profilesCount === 1) { 48 | // if (groupsCount > 1) { 49 | // text = '1 profile and ' + groupsCount + ' groups';//Following 50 | // } else if (groupsCount === 1) { 51 | // text = '1 profile and 1 group';//Following 52 | // } else { 53 | // text = '1 profile';//Following 54 | // } 55 | // } else if (groupsCount > 1) { 56 | // text = groupsCount + ' groups';//'Following ' + 57 | // } else if (groupsCount === 1) { 58 | // text = '1 group';//Following 59 | // } 60 | 61 | // if (text !== null) { 62 | // container.add(x.makeButton(text, () => { 63 | // x.open('explore/following'); 64 | // })); 65 | // } 66 | // } 67 | // return container; 68 | // }); 69 | // component.observeChanges(['temp_explore', 'explore/following']); 70 | 71 | //x.addToProfile(component); 72 | 73 | //var component = x.makeComponent(async () => { 74 | var following = await library.getFollowing(); 75 | 76 | //var container = x.makeContainer({ addSpacing: true }); 77 | 78 | var listComponent = await x.makePostsListComponent(async (options) => { 79 | var posts = await library.getPropertiesPosts(following, { order: options.order, offset: options.offset, limit: options.limit }); 80 | library.updateAllProperties(5 * 60); 81 | x.setTemplate(posts.length > 0 ? null : 'empty'); 82 | return posts; 83 | }, { 84 | showGroup: true 85 | }); 86 | listComponent.observeChanges(['temp_explore', 'explore/following']); 87 | x.add(listComponent); 88 | // await postsList.promise; 89 | // container.add(postsList); 90 | // return container; 91 | //}); 92 | //component.observeChanges(['temp_explore', 'explore/following']); 93 | 94 | //x.add(component); 95 | 96 | // x.add(x.makeSmallTitle('Suggestions')); 97 | // x.add(x.makeHint('These are just a few profiles you can follow to get started. Invite your friends to make this screen more personal.')); 98 | 99 | // x.add(x.makeComponent(async () => { 100 | // let list = x.makeList({ type: 'grid' }); 101 | // var userIDs = ['ivo', 'dailyquotes', 'photodobo']; 102 | // for (var i = 0; i < userIDs.length; i++) { 103 | // var userID = x.getFullID(userIDs[i]); 104 | // list.add(await x.makeProfileButton('user', userID, { details: x.getShortID(userID) })); 105 | // } 106 | // return list; 107 | // })); 108 | 109 | // x.addToolbarButton(async () => { 110 | // await library.updateAllProperties(); 111 | // x.announceChanges(['explore/list']); 112 | // }, 'refresh', 'right'); 113 | }; -------------------------------------------------------------------------------- /app/apps/group/views/home.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Dots Mesh Web App 3 | * https://github.com/dotsmesh/dotsmesh-web-app 4 | * Free to use under the GPL-3.0 license. 5 | */ 6 | 7 | async (args, library) => { 8 | var groupID = args.id; 9 | 10 | //x.setTemplate('columns'); 11 | 12 | x.addErrorHandler(['invalidMemberID', 'invalidAccessKey', 'groupNotFound'], async () => { 13 | x.showLoading(); 14 | var exists = await x.services.call('groups', 'exists', { groupID: groupID }); 15 | x.hideLoading(); 16 | var options = {}; 17 | if (exists) { 18 | options.buttonText = 'Remove from your groups'; 19 | options.buttonClick = async () => { 20 | x.showLoading(); 21 | await x.services.call('groups', 'leave', { groupID: groupID }); 22 | x.hideLoading(); 23 | x.showMessage('Done! Group removed!'); 24 | }; 25 | } 26 | x.showMessage('The group does not exist or you are not a member!', options); 27 | }); 28 | 29 | var isAdministrator = await x.services.call('groups', 'isAdministrator', { groupID: groupID }); 30 | 31 | x.wait(async () => { 32 | var profile = await x.group.getProfile(groupID); 33 | x.setTitle(profile.name + ' (private group)'); 34 | }); 35 | 36 | x.addToProfile(x.makeProfilePreviewComponent('group', groupID, { 37 | groupUserID: x.currentUser.getID(), 38 | showEditButton: isAdministrator, 39 | actionButton: async () => { 40 | if (x.currentUser.exists()) { 41 | var memberGroupDetails = await x.services.call('groups', 'getDetails', { groupID: groupID, details: ['status'] }); 42 | if (memberGroupDetails !== null && memberGroupDetails.status === 'joined') { 43 | return { 44 | onClick: () => { 45 | x.open('posts/form', { groupID: groupID }, { modal: true }); 46 | }, 47 | text: 'New post' 48 | } 49 | } 50 | } 51 | return null; 52 | }, 53 | })); 54 | 55 | if (isAdministrator) { 56 | let component = x.makeSecretComponent('Administrators only', async component2 => { 57 | var sharedDataStorage = await x.group.getSharedDataStorage(groupID); // todo move in library and add cache ??? 58 | var list = await sharedDataStorage.getList({ keyStartWith: 'm/p/', keyEndWith: '/a', limit: 101, sliceProperties: ['key'] }); 59 | var pendingCount = list.length; 60 | var button = x.makeButton('Pending approval (' + (pendingCount > 100 ? '100+' : pendingCount) + ')', () => { 61 | x.open('group/members', { id: groupID, mode: 'pendingApproval' }); 62 | }); 63 | component2.add(button); 64 | 65 | var privateDataStorage = await x.group.getFullDataStorage(groupID, 'p/i/'); // todo move in library and add cache ??? 66 | var list = await privateDataStorage.getList({ limit: 101, sliceProperties: ['key'] }); 67 | var invitationsCount = list.length; 68 | var button = x.makeButton('Invitations (' + (invitationsCount > 100 ? '100+' : invitationsCount) + ')', () => { 69 | x.open('group/invitations', { id: groupID }); 70 | }); 71 | component2.add(button); 72 | }); 73 | component.observeChanges(['group/' + groupID + '/members', 'group/' + groupID + '/invitations']); 74 | x.addToProfile(component); 75 | } 76 | 77 | //x.add(x.makeTitle('Recently published'), { template: 'column2' }); 78 | 79 | var listComponent = await x.makePostsListComponent(async options => { 80 | var posts = await x.property.getPosts('group', groupID, { order: options.order, offset: options.offset, limit: options.limit, cacheValues: true }); 81 | return posts; 82 | }, { 83 | // addButton: async () => { 84 | // if (x.currentUser.exists()) { 85 | // var memberGroupDetails = await x.services.call('groups', 'getDetails', { groupID: groupID, details: ['status'] }); 86 | // if (memberGroupDetails !== null && memberGroupDetails.status === 'joined') { 87 | // return { 88 | // onClick: () => { 89 | // x.open('posts/form', { groupID: groupID }, { modal: true }); 90 | // }, 91 | // text: 'New post' 92 | // } 93 | // } 94 | // } 95 | // return null; 96 | // }, 97 | emptyText: 'No posts have been published yet.' 98 | }); 99 | listComponent.observeChanges(['groups', 'group/' + groupID + '/posts']); 100 | x.add(listComponent); 101 | 102 | if (x.currentUser.exists()) { 103 | x.addToolbarNotificationsButton('gp$' + groupID, action => { 104 | return { 105 | appID: 'group', 106 | name: 'modifyGroupPostsNotification', 107 | args: { action: action, groupID: groupID, lastSeenPosts: listComponent.getLastSeen() } 108 | } 109 | }, 'Get notified when there are new posts in this group.', 'You\'ll receive a notification when there are new posts in this group.'); 110 | x.windowEvents.addEventListener('show', async () => { 111 | await library.updateGroupPostsNotification(groupID, { lastSeenPosts: listComponent.getLastSeen() }); 112 | }); 113 | } 114 | 115 | }; -------------------------------------------------------------------------------- /app/apps/profile/library.js: -------------------------------------------------------------------------------- 1 | 2 | /* 3 | * Dots Mesh Web App 4 | * https://github.com/dotsmesh/dotsmesh-web-app 5 | * Free to use under the GPL-3.0 license. 6 | */ 7 | 8 | () => { 9 | 10 | var getDataStorage = async (propertyType, propertyID) => { 11 | if (propertyType === 'user') { 12 | if (propertyID !== x.currentUser.getID()) { 13 | throw new Error(); 14 | } 15 | return { 16 | privateDataStorage: x.currentUser.getDataStorage('p/p/'), 17 | sharedDataStorage: x.currentUser.getDataStorage('s/p/'), 18 | encryptPrivateFunction: x.currentUser.encrypt, 19 | decryptPrivateFunction: x.currentUser.decrypt, 20 | prepareSharedFunction: async value => { 21 | return x.pack('', [value, await x.currentUser.sign(value)]); 22 | } 23 | }; 24 | } else if (propertyType === 'group') { 25 | return { 26 | privateDataStorage: await x.group.getFullDataStorage(propertyID, 'p/p/'), 27 | sharedDataStorage: await x.group.getSharedDataStorage(propertyID, 'p/'), 28 | encryptPrivateFunction: async data => { 29 | return await x.group.encryptPrivate(propertyID, data); 30 | }, 31 | decryptPrivateFunction: async data => { 32 | return await x.group.decryptPrivate(propertyID, data) 33 | }, 34 | prepareSharedFunction: async data => { 35 | return await x.group.encryptShared(propertyID, data); 36 | } 37 | }; 38 | } else if (propertyType === 'groupMember') { 39 | var parts = propertyID.split('$'); 40 | var groupID = parts[0]; 41 | //var userID = parts[1]; 42 | return { 43 | privateDataStorage: await x.group.getCurrentMemberPrivateDataStorage(groupID).getContext('p/'), 44 | sharedDataStorage: await x.group.getCurrentMemberSharedDataStorage(groupID, 'p/'), 45 | encryptPrivateFunction: x.currentUser.encrypt, 46 | decryptPrivateFunction: x.currentUser.decrypt, 47 | prepareSharedFunction: async data => { 48 | return await x.group.encryptShared(groupID, data); 49 | } 50 | }; 51 | } else { 52 | throw new Error(); 53 | } 54 | }; 55 | 56 | var getData = async (propertyType, propertyID) => { 57 | var ds = await getDataStorage(propertyType, propertyID); 58 | var data = await ds.privateDataStorage.get('d'); 59 | if (data === null) { 60 | data = {}; 61 | } else { 62 | data = x.unpack(await ds.decryptPrivateFunction(data)); 63 | if (data.name === '') { 64 | data = data.value; 65 | } 66 | } 67 | 68 | var result = {}; 69 | result.image = data.i !== undefined ? data.i : null; 70 | result.name = data.n !== undefined ? data.n : null; 71 | result.description = data.d !== undefined ? data.d : null; 72 | result.imageSizes = data.s !== undefined ? data.s : []; 73 | return result; 74 | }; 75 | 76 | var setData = async (propertyType, propertyID, data) => { 77 | var ds = await getDataStorage(propertyType, propertyID); 78 | var oldData = await getData(propertyType, propertyID); 79 | var image = data.image !== undefined && data.image !== null ? await x.image.resize(data.image, 600, 600) : null; 80 | var name = data.name !== undefined && data.name !== null && data.name.length > 0 ? data.name : null; 81 | var description = data.description !== undefined && data.description !== null && data.description.length > 0 ? data.description : null; 82 | var imageSizes = [40, 50, 60, 80, 100, 120, 150, 200, 600]; 83 | 84 | var privateData = {}; 85 | var sharedData = {}; 86 | if (image !== null) { 87 | privateData.i = image; 88 | privateData.s = imageSizes; 89 | sharedData.s = imageSizes; 90 | } 91 | if (name !== null) { 92 | privateData.n = name; 93 | sharedData.n = name; 94 | } 95 | if (description !== null) { 96 | privateData.d = description; 97 | sharedData.d = description; 98 | } 99 | 100 | if (JSON.stringify(privateData) === '{}') { 101 | await ds.privateDataStorage.delete('d'); 102 | } else { 103 | await ds.privateDataStorage.set('d', await ds.encryptPrivateFunction(x.pack('', privateData))); 104 | } 105 | 106 | var buffer = ds.sharedDataStorage.getBuffer(); 107 | 108 | if (oldData.image !== image || JSON.stringify(oldData.imageSizes) !== JSON.stringify(imageSizes)) { 109 | for (var i = 0; i < imageSizes.length; i++) { 110 | var size = imageSizes[i]; 111 | var dataKey = 'i' + size; 112 | if (image === null) { 113 | buffer.delete(dataKey); 114 | } else { 115 | var resizedImage = await x.image.resize(image, size, size, 100); //92 116 | buffer.set(dataKey, await ds.prepareSharedFunction(resizedImage)); 117 | } 118 | } 119 | } 120 | 121 | //sharedData.v = x.generateVersion(); 122 | 123 | 124 | if (JSON.stringify(sharedData) === '{}') { 125 | buffer.delete('d'); 126 | } else { 127 | buffer.set('d', await ds.prepareSharedFunction(x.pack('', sharedData))); 128 | } 129 | 130 | await buffer.flush(); 131 | 132 | await x.property.clearProfileCache(propertyType, propertyID); 133 | await x.announceChanges([propertyType + '/' + propertyID + '/profile']); 134 | }; 135 | 136 | return { 137 | getData: getData, 138 | setData: setData 139 | }; 140 | }; -------------------------------------------------------------------------------- /app/apps/contacts/views/connect.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Dots Mesh Web App 3 | * https://github.com/dotsmesh/dotsmesh-web-app 4 | * Free to use under the GPL-3.0 license. 5 | */ 6 | 7 | async (args, library) => { 8 | var userID = args.id; 9 | var connectKey = args.connectKey !== undefined ? args.connectKey : null; 10 | if (x.currentUser.isPublic()) { 11 | x.setTitle('Connect'); 12 | } else { 13 | x.setTitle('Add to contacts'); 14 | } 15 | 16 | x.add(x.makeProfilePreviewComponent('user', userID, { 17 | theme: 'light', 18 | size: 'medium' 19 | })); 20 | 21 | x.add(x.makeComponent(async () => { 22 | var contact = await library.get(userID); 23 | var request = await library.getRequest(userID); 24 | 25 | var text = null; 26 | var blocksToShow = []; 27 | if (contact !== null || request !== null) { 28 | if (contact !== null && contact.providedAccessKey !== null && contact.accessKey !== null) { 29 | text = 'Connected'; 30 | blocksToShow.push('remove'); 31 | } else if (contact !== null && contact.providedAccessKey !== null) { 32 | text = 'Connection request sent'; 33 | blocksToShow.push('requestSent'); 34 | blocksToShow.push('remove'); 35 | } else if ((contact !== null && contact.accessKey !== null) || request !== null) { 36 | text = 'Approve connection request'; 37 | blocksToShow.push('approveRequest'); 38 | blocksToShow.push('denyRequest'); 39 | } else { 40 | if (x.currentUser.isPublic()) { 41 | blocksToShow.push('sendConnectRequest'); 42 | } 43 | blocksToShow.push('remove'); 44 | } 45 | } else { 46 | if (x.currentUser.isPublic()) { 47 | blocksToShow.push('sendConnectRequest'); 48 | } 49 | blocksToShow.push('add'); 50 | } 51 | 52 | var container = x.makeContainer(); 53 | for (var i = 0; i < blocksToShow.length; i++) { 54 | var blockToShow = blocksToShow[i]; 55 | if (blockToShow === 'remove') { 56 | container.add(x.makeButton('Remove from contacts', async () => { 57 | x.showLoading(); 58 | await library.deleteRequest(userID); 59 | await library.remove(userID); 60 | await x.back(); 61 | })); 62 | } else if (blockToShow === 'requestSent') { 63 | container.add(x.makeText('Connection request sent', { align: 'center' })); 64 | } else if (blockToShow === 'denyRequest') { 65 | container.add(x.makeButton('Deny request', async () => { 66 | x.showLoading(); 67 | await library.deleteRequest(userID); 68 | await library.remove(userID); 69 | await x.back(); 70 | })); 71 | } else if (blockToShow === 'approveRequest') { 72 | container.add(x.makeButton('Approve connection request', async () => { 73 | x.showLoading(); 74 | await library.approveRequest(userID); 75 | await x.back(); 76 | })); 77 | } else if (blockToShow === 'add') { 78 | container.add(x.makeButton(x.currentUser.isPublic() ? 'Just add to contacts' : 'Add to contacts', async () => { 79 | x.showLoading(); 80 | await library.add(userID); 81 | await x.back(); 82 | })); 83 | } else if (blockToShow === 'sendConnectRequest') { 84 | container.add(x.makeButton('Send connection request', async () => { 85 | x.showLoading(); 86 | var success = false; 87 | 88 | // Try with connection key 89 | if (connectKey !== null) { 90 | var result = await library.sendRequest(userID, 'k/' + connectKey); 91 | if (result) { 92 | success = true; 93 | } 94 | 95 | // Try old format 96 | if (!success) { 97 | var result = await library.sendRequest(userID, connectKey); 98 | if (result) { 99 | success = true; 100 | } 101 | } 102 | } 103 | 104 | // Try open connect 105 | if (!success) { 106 | var result = await library.sendRequest(userID, 'o/c'); 107 | if (result) { 108 | success = true; 109 | } 110 | } 111 | 112 | // Try groups 113 | if (!success) { 114 | var groups = await x.services.call('groups', 'getList'); 115 | for (let groupID in groups) { 116 | var isMember = await x.services.call('group', 'isMember', { groupID: groupID, typedID: userID }); 117 | if (isMember) { 118 | var groupAccessKey = await x.services.call('groups', 'getMembersConnectAccessKey', { groupID: groupID }); 119 | var result = await library.sendRequest(userID, groupAccessKey); 120 | if (result) { 121 | success = true; 122 | break; 123 | } 124 | } 125 | } 126 | } 127 | 128 | if (success) { 129 | x.showMessage('Connection request sent!'); 130 | } else { 131 | // Open connection key form 132 | x.open('contacts/connectKey', { userID: userID }, { modal: true, width: 300 }); 133 | } 134 | 135 | x.hideLoading(); 136 | }, { marginTop: 'big' })); 137 | } 138 | } 139 | return container; 140 | }, { observeChanges: ['contacts'] })); 141 | 142 | }; -------------------------------------------------------------------------------- /app/assets/home.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | Dots Mesh - an open social platform 11 | 12 | 13 | 14 | 49 | 50 | 51 | 52 | 53 | 54 | 57 | 110 | 111 | 112 | 113 | -------------------------------------------------------------------------------- /app/apps/posts/views/post.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Dots Mesh Web App 3 | * https://github.com/dotsmesh/dotsmesh-web-app 4 | * Free to use under the GPL-3.0 license. 5 | */ 6 | 7 | async (args, library) => { 8 | var postID = args.postID; 9 | var currentUserID = x.currentUser.getID(); 10 | 11 | var canEdit = false; 12 | var canDelete = false; 13 | var canReact = false; 14 | 15 | var propertyType = null; 16 | var propertyID = null; 17 | if (args.userID !== undefined) { // USER 18 | propertyType = 'user'; 19 | propertyID = args.userID; 20 | x.setHash(x.getShortID(propertyID) + '/p/' + postID); 21 | var profile = await x.user.getProfile(propertyID); 22 | x.setTitle('Public post by ' + profile.name); 23 | canEdit = true; 24 | canDelete = true; 25 | } else if (args.groupID !== undefined) { // GROUP 26 | propertyType = 'group'; 27 | propertyID = args.groupID; 28 | var profile = await x.group.getProfile(propertyID); 29 | x.setTitle('Post in ' + profile.name + ' (group)'); 30 | 31 | var memberGroupDetails = await x.services.call('groups', 'getDetails', { groupID: propertyID, details: ['status'] }); 32 | if (memberGroupDetails.status === 'joined') { 33 | canEdit = true; 34 | canDelete = true; 35 | canReact = true; 36 | } 37 | } 38 | 39 | //x.setTemplate('column'); 40 | 41 | // todo wait 42 | try { 43 | var post = await library.getPost(propertyType, propertyID, postID, { cache: true }); 44 | } catch (e) { 45 | if (e.name === 'propertyUnavailable') { 46 | var post = null; 47 | } else { 48 | throw e; 49 | } 50 | } 51 | if (post === null) { 52 | if (propertyType === 'user') { 53 | x.showMessage('This post by ' + x.getShortID(propertyID) + ' cannot be found! Maybe it was deleted or it\'s temporary unavailable.'); 54 | } else { 55 | x.showMessage('The post requested cannot be found! Maybe it was deleted.'); 56 | } 57 | return; 58 | } 59 | 60 | if (post.userID === currentUserID && canEdit) { // observe user join 61 | x.addToolbarButton('Edit this post', async () => { 62 | if (propertyType === 'user') { 63 | x.open('posts/form', { userID: propertyID, postID: postID }, { modal: true }); 64 | } else { 65 | x.open('posts/form', { groupID: propertyID, postID: postID }, { modal: true }); 66 | } 67 | }, 'edit'); 68 | } 69 | 70 | if (post.userID === currentUserID && canDelete) { // observe user join 71 | x.addToolbarButton('Delete this post', async () => { 72 | if (await x.confirm('Are you sure you want to delete this post?', { icon: 'delete' })) { 73 | x.showLoading(); 74 | await library.deletePost(propertyType, propertyID, postID); 75 | await x.announceChanges(['user/' + currentUserID, 'posts/' + postID]); 76 | //await x.backPrepare(); 77 | await x.back(); 78 | } 79 | }, 'delete'); 80 | } 81 | 82 | if (propertyType === 'user') { 83 | if (x.currentUser.isPublic()) { 84 | x.addToolbarButton('Share this post', () => { 85 | x.share('p', { 86 | o: x.getTypedID(propertyType, propertyID), 87 | p: postID 88 | }); 89 | }, 'share'); 90 | } 91 | } 92 | var alreadyShown = false; 93 | var component = x.makePostPreviewComponent(async () => { 94 | if (!alreadyShown) { 95 | alreadyShown = true; 96 | return post; 97 | } else { 98 | // Updated, maybe because of an edit 99 | return await library.getPost(propertyType, propertyID, postID, { cache: true }); 100 | } 101 | }, { showGroup: true }); 102 | component.observeChanges([propertyType + '/' + propertyID + '/post/' + postID]); 103 | x.add(component);//, { template: 'column1' } 104 | 105 | if (propertyType === 'group') { 106 | x.add(x.makeSeparator()); 107 | var discussionComponent = x.makeDiscussionComponent(async options => { 108 | return await library.getPostReactions(propertyType, propertyID, postID, options); 109 | }, { groupID: propertyID }); 110 | x.add(discussionComponent);//, { template: 'column1' } 111 | 112 | if (canReact) { // observe user join 113 | x.add(x.makeComponent(async () => { 114 | var postForm = await x.makePostForm(null, { 115 | placeholder: 'Your comment', 116 | clearOnSubmit: true, 117 | submitText: 'Send', 118 | type: 'small', 119 | profilePropertyType: 'groupMember', 120 | profilePropertyID: propertyID + '$' + x.currentUser.getID() 121 | }); 122 | postForm.onSubmit = reaction => { 123 | (async () => { 124 | reaction.id = x.generateDateBasedID(); // temp id while saving 125 | var firstOnChange = true; 126 | await library.addPostReaction(propertyType, propertyID, postID, reaction, async reaction => { 127 | await discussionComponent.setMessage(reaction); 128 | if (firstOnChange) { 129 | firstOnChange = false; 130 | x.scrollBottom(); 131 | } 132 | }); 133 | await discussionComponent.update(async () => { 134 | await discussionComponent.deleteMessage(reaction.id); 135 | }); 136 | })(); 137 | }; 138 | return postForm; 139 | }));//, { template: 'column1' } 140 | } 141 | 142 | if (x.currentUser.exists()) { 143 | x.addToolbarNotificationsButton('gpr$' + propertyID + '$' + postID, action => { 144 | return { 145 | appID: 'group', 146 | name: 'modifyGroupPostReactionsNotification', 147 | args: { action: action, groupID: propertyID, postID: postID, lastSeenPostReactions: discussionComponent.getLastSeen() } 148 | } 149 | }, 'Get notified when there is activity in this post.', 'You\'ll receive a notification when there is activity in this post.'); 150 | x.windowEvents.addEventListener('show', async () => { 151 | await x.services.call('group', 'updateGroupPostReactionsNotification', { groupID: propertyID, postID: postID, lastSeenPostReactions: discussionComponent.getLastSeen() }); 152 | }); 153 | } 154 | } 155 | 156 | // x.add(x.makeTitle('Author')); 157 | // x.add(await x.makeProfileButton('user', post.userID, { text: x.getShortID(post.userID) })); 158 | // if (propertyType === 'group') { 159 | // x.add(x.makeTitle('Group')); 160 | // x.add(await x.makeProfileButton('group', propertyID)); 161 | // } 162 | //x.add(x.makeHint('Published on 21 Nov, 2020 (23 days ago)')); 163 | }; -------------------------------------------------------------------------------- /app/apps/explore/library.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Dots Mesh Web App 3 | * https://github.com/dotsmesh/dotsmesh-web-app 4 | * Free to use under the GPL-3.0 license. 5 | */ 6 | 7 | () => { 8 | 9 | var dataStorage = null; 10 | var getDataStorage = () => { 11 | if (dataStorage === null) { 12 | dataStorage = x.currentUser.getDataStorage('p/e/'); 13 | } 14 | return dataStorage; 15 | }; 16 | 17 | var followingStorage = null; 18 | var getFollowingDataStorage = () => { 19 | if (followingStorage === null) { 20 | followingStorage = getDataStorage().getDetailsContext('f-', x.currentUser.isPublic() ? x.currentUserCache.get('exploref-dc') : null); 21 | } 22 | return followingStorage; 23 | }; 24 | 25 | var follow = async (propertyType, propertyID) => { 26 | var storage = getFollowingDataStorage(); 27 | await storage.set(x.getTypedID(propertyType, propertyID)); 28 | await x.property.observeChanges(propertyID, [propertyType === 'user' ? 'up' : 'gp'], 'e'); 29 | x.announceChanges(['explore/following', 'explore/following/' + propertyType + '/' + propertyID]); 30 | }; 31 | 32 | var unfollow = async (propertyType, propertyID) => { 33 | var storage = getFollowingDataStorage(); 34 | await storage.delete(x.getTypedID(propertyType, propertyID)); 35 | await x.property.unobserveChanges(propertyID, [propertyType === 'user' ? 'up' : 'gp'], 'e'); 36 | x.announceChanges(['explore/following', 'explore/following/' + propertyType + '/' + propertyID]); 37 | }; 38 | 39 | var isFollowing = async (propertyType, propertyID) => { 40 | var storage = getFollowingDataStorage(); 41 | return await storage.exists(x.getTypedID(propertyType, propertyID)); 42 | }; 43 | 44 | var getFollowing = async () => { 45 | var storage = getFollowingDataStorage(); 46 | var list = await storage.getList(); 47 | return Object.keys(list); 48 | }; 49 | 50 | var propertiesDataStorage = null; 51 | var getPropertiesDataStorage = () => { 52 | if (propertiesDataStorage === null) { 53 | propertiesDataStorage = getDataStorage().getDetailsContext('p-', x.currentUser.isPublic() ? x.currentUserCache.get('explorep-dc') : null); 54 | } 55 | return propertiesDataStorage; 56 | }; 57 | 58 | var getPropertiesPosts = async (properties, options = {}) => { 59 | await updateAllProperties(); 60 | var storage = getPropertiesDataStorage(); 61 | var order = typeof options.order !== 'undefined' ? options.order : null; 62 | var offset = typeof options.offset !== 'undefined' ? options.offset : 0; 63 | var limit = typeof options.limit !== 'undefined' ? options.limit : null; 64 | var tempIDs = {}; 65 | for (var i = 0; i < properties.length; i++) { 66 | var typedPropertyID = properties[i]; 67 | var propertyDetails = await storage.get(typedPropertyID, ['i']); 68 | var postIDs = propertyDetails !== null && propertyDetails['i'] !== null ? propertyDetails['i'] : []; 69 | if (postIDs.length > 0) { 70 | if (order !== null) { 71 | postIDs.sort(); 72 | if (order === 'desc') { 73 | postIDs.reverse(); 74 | } 75 | } 76 | if (limit !== null) { 77 | postIDs = postIDs.slice(0, offset + limit); 78 | } 79 | postIDs.forEach(postID => { 80 | tempIDs[postID + '/' + typedPropertyID] = { typedPropertyID: typedPropertyID, postID: postID }; 81 | }) 82 | } 83 | } 84 | if (order === 'asc') { 85 | tempIDs = x.sortObjectKeyAsc(tempIDs); 86 | } else if (order === 'desc') { 87 | tempIDs = x.sortObjectKeyDesc(tempIDs); 88 | } 89 | var resultList = Object.values(tempIDs); 90 | if (limit !== null) { 91 | resultList = resultList.slice(offset, offset + limit); 92 | } 93 | 94 | var propertyPostsIDs = {}; 95 | var propertyPosts = {}; 96 | resultList.forEach(postData => { 97 | var typedPropertyID = postData.typedPropertyID; 98 | if (typeof propertyPostsIDs[typedPropertyID] === 'undefined') { 99 | propertyPostsIDs[typedPropertyID] = []; 100 | propertyPosts[typedPropertyID] = []; 101 | } 102 | propertyPostsIDs[typedPropertyID].push(postData.postID); 103 | }); 104 | for (var typedPropertyID in propertyPostsIDs) { // async maybe ???? 105 | var propertyData = x.parseTypedID(typedPropertyID); 106 | var posts = await x.property.getPosts(propertyData.type, propertyData.id, { ids: propertyPostsIDs[typedPropertyID], cacheValues: true }); 107 | posts.forEach(post => { 108 | if (post !== null) { 109 | propertyPosts[typedPropertyID][post.id] = post; 110 | } 111 | }); 112 | } 113 | var result = []; 114 | resultList.forEach(postData => { 115 | if (typeof propertyPosts[postData.typedPropertyID][postData.postID] !== 'undefined') { 116 | result.push(propertyPosts[postData.typedPropertyID][postData.postID]); 117 | } 118 | }); 119 | return result; 120 | }; 121 | 122 | var updateAllProperties = async cacheTTL => { 123 | if (typeof cacheTTL === 'undefined') { 124 | cacheTTL = 0; 125 | } 126 | cacheTTL = 10; 127 | var maxTime = Date.now() - cacheTTL * 1000; 128 | var followingIDs = await getFollowing(); 129 | var storage = getPropertiesDataStorage(); 130 | var list = await storage.getList(['d']); 131 | var datesList = {}; 132 | for (var key in list) { 133 | datesList[key] = list[key].d; 134 | }; 135 | datesList = x.sortObjectValueAsc(datesList); 136 | var knownIDs = Object.keys(datesList); 137 | var unknownIDs = x.arrayDifference(followingIDs, knownIDs); // give priority to unknown 138 | for (var i = 0; i < unknownIDs.length; i++) { 139 | await updateProperty(unknownIDs[i]); 140 | } 141 | for (var i = 0; i < knownIDs.length; i++) { 142 | var typedPropertyID = knownIDs[i]; 143 | if (datesList[typedPropertyID] < maxTime) { 144 | await updateProperty(typedPropertyID); 145 | return; 146 | } 147 | } 148 | // todo remove old 149 | }; 150 | 151 | var updateProperty = async typedPropertyID => { 152 | var storage = getPropertiesDataStorage(); 153 | var propertyIDData = x.parseTypedID(typedPropertyID); 154 | try { 155 | var posts = await x.services.call('posts', 'getRawPosts', { propertyType: propertyIDData.type, propertyID: propertyIDData.id, options: { order: 'desc', offset: 0, limit: 200, ignoreValues: true, cacheList: true, ignoreListCache: true } }); // todo update limit 156 | } catch (e) { 157 | if (['notAMember', 'invalidMemberID', 'invalidAccessKey', 'groupNotFound', 'propertyUnavailable'].indexOf(e.name) !== -1) { 158 | posts = []; 159 | } else { 160 | throw e; 161 | } 162 | } 163 | var postsIDs = Object.keys(posts); 164 | var propertyDetails = await storage.get(typedPropertyID, ['i']); 165 | var hasChange = propertyDetails === null || JSON.stringify(propertyDetails['i']) !== JSON.stringify(postsIDs); 166 | await storage.set(typedPropertyID, { d: Date.now(), i: postsIDs }); 167 | if (hasChange) { 168 | x.announceChanges(['follow/' + typedPropertyID + '/posts', 'temp_explore']); 169 | } 170 | return hasChange; 171 | }; 172 | 173 | var addPropertiesToUpdateQueue = async propertyIDs => { 174 | var followingIDs = await getFollowing(); 175 | for (var propertyID of propertyIDs) { 176 | var typedPropertyID = x.getTypedID('group', propertyID); // test is group 177 | var propertyIDToAdd = null; 178 | if (followingIDs.indexOf(propertyID) !== -1) { 179 | propertyIDToAdd = propertyID; 180 | } else if (followingIDs.indexOf(typedPropertyID) !== -1) { 181 | propertyIDToAdd = typedPropertyID; 182 | } 183 | if (propertyIDToAdd !== null) { 184 | //var dataStorage = getDataStorage(); 185 | // todo queue 186 | await updateProperty(propertyIDToAdd); 187 | } 188 | } 189 | }; 190 | 191 | return { 192 | follow: follow, 193 | unfollow: unfollow, 194 | isFollowing: isFollowing, 195 | getFollowing: getFollowing, 196 | getPropertiesPosts: getPropertiesPosts, 197 | updateAllProperties: updateAllProperties, 198 | updateProperty: updateProperty, 199 | addPropertiesToUpdateQueue: addPropertiesToUpdateQueue 200 | }; 201 | }; -------------------------------------------------------------------------------- /app/index.php: -------------------------------------------------------------------------------- 1 | request->host = DOTSMESH_WEB_APP_HOST; 61 | } 62 | 63 | $hasLogsDirs = defined('DOTSMESH_WEB_APP_LOGS_DIR'); 64 | $app->enableErrorHandler(['logErrors' => $hasLogsDirs, 'displayErrors' => DOTSMESH_WEB_APP_DEV_MODE]); 65 | 66 | if ($hasLogsDirs) { 67 | $app->logs->useFileLogger(DOTSMESH_WEB_APP_LOGS_DIR); 68 | } 69 | 70 | $app->routes 71 | ->add('/', function (App\Request $request) use ($app) { 72 | $host = $app->request->host; 73 | $isAppRequest = $request->query->exists('app'); 74 | if ($isAppRequest && $request->query->exists('sw')) { 75 | $content = file_get_contents(DOTSMESH_WEB_APP_DEV_MODE ? __DIR__ . '/assets/sw.js' : __DIR__ . '/assets/sw.min.js'); 76 | $response = new App\Response($content); 77 | $response->headers->set($response->headers->make('Content-Type', 'application/javascript')); 78 | $response->headers->set($response->headers->make('Cache-Control', DOTSMESH_WEB_APP_DEV_MODE ? 'no-store, no-cache, must-revalidate, max-age=0' : 'public, max-age=600')); 79 | $response->headers->set($response->headers->make('X-Robots-Tag', 'noindex,nofollow')); 80 | } elseif ($isAppRequest && $request->query->exists('a')) { 81 | if (DOTSMESH_WEB_APP_DEV_MODE) { 82 | $object = require __DIR__ . '/appjs-builder.php'; 83 | $content = $object::getJS(); 84 | } else { 85 | $isDebugEnabled = $request->query->exists('debug'); 86 | $content = file_get_contents(__DIR__ . '/assets/' . ($isDebugEnabled ? 'app.js' : 'app.min.js')); 87 | } 88 | $response = new App\Response($content); 89 | $response->headers->set($response->headers->make('Content-Type', 'application/javascript')); 90 | $response->headers->set($response->headers->make('Cache-Control', DOTSMESH_WEB_APP_DEV_MODE ? 'no-store, no-cache, must-revalidate, max-age=0' : 'public, max-age=600')); 91 | $response->headers->set($response->headers->make('X-Robots-Tag', 'noindex,nofollow')); 92 | } elseif ($isAppRequest && $request->query->exists('h960')) { 93 | $content = file_get_contents(__DIR__ . '/assets/h960.jpg'); 94 | $response = new App\Response($content); 95 | $response->headers->set($response->headers->make('Content-Type', 'image/jpeg')); 96 | $response->headers->set($response->headers->make('Cache-Control', DOTSMESH_WEB_APP_DEV_MODE ? 'no-store, no-cache, must-revalidate, max-age=0' : 'public, max-age=600')); 97 | $response->headers->set($response->headers->make('X-Robots-Tag', 'noindex,nofollow')); 98 | } elseif ($isAppRequest && $request->query->exists('i512')) { 99 | $content = file_get_contents(__DIR__ . '/assets/i512.png'); 100 | $response = new App\Response($content); 101 | $response->headers->set($response->headers->make('Content-Type', 'image/png')); 102 | $response->headers->set($response->headers->make('Cache-Control', DOTSMESH_WEB_APP_DEV_MODE ? 'no-store, no-cache, must-revalidate, max-age=0' : 'public, max-age=600')); 103 | $response->headers->set($response->headers->make('X-Robots-Tag', 'noindex,nofollow')); 104 | } elseif ($isAppRequest && $request->query->exists('si')) { 105 | $content = file_get_contents(__DIR__ . '/assets/si1.jpg'); 106 | $response = new App\Response($content); 107 | $response->headers->set($response->headers->make('Content-Type', 'image/jpeg')); 108 | $response->headers->set($response->headers->make('Cache-Control', DOTSMESH_WEB_APP_DEV_MODE ? 'no-store, no-cache, must-revalidate, max-age=0' : 'public, max-age=600')); 109 | $response->headers->set($response->headers->make('X-Robots-Tag', 'noindex,nofollow')); 110 | } elseif ($isAppRequest && $request->query->exists('m')) { 111 | if ($host === 'dotsmesh.com') { 112 | $name = ''; 113 | } elseif ($host === 'dev.dotsmesh.com') { 114 | $name = 'dev'; 115 | } elseif ($host === 'beta.dotsmesh.com') { 116 | $name = 'beta'; 117 | } elseif (substr($host, 0, 9) === 'dotsmesh.') { 118 | $name = substr($host, 9); 119 | } else { 120 | $name = $host; 121 | } 122 | $name = 'Dots Mesh' . (strlen($name) > 0 ? ' (' . $name . ')' : ''); 123 | $response = new App\Response\JSON(json_encode([ 124 | "short_name" => $name, 125 | "name" => $name, 126 | "start_url" => "/", 127 | "background_color" => "#111", 128 | "display" => "standalone", 129 | "theme_color" => "#111", 130 | "icons" => [ 131 | [ 132 | "src" => "data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='512' height='512'%3e%3ccircle cy='256' cx='256' r='256' fill='%2324a4f2' paint-order='stroke markers fill'/%3e%3cpath d='M150.7 201.4c-28.08 0-50.7-22.62-50.7-50.7s22.62-50.7 50.7-50.7 50.7 22.62 50.7 50.7-22.62 50.7-50.7 50.7zM114.04 256c0 14.6016 12.0627 27.3 27.3 27.3s27.3-12.0627 27.3-27.3-12.6984-27.3-27.93492-27.3S114.04 240.7627 114.04 256zM256 330.1c-41.0397 0-74.1-33.0603-74.1-74.1s33.0603-74.1 74.1-74.1 74.1 33.0603 74.1 74.1-33.0603 74.1-74.1 74.1zM150.7 412c-28.08 0-50.7-22.62-50.7-50.7s22.62-50.7 50.7-50.7 50.7 22.62 50.7 50.7-22.62 50.7-50.7 50.7zm210.6 0c-28.08 0-50.7-22.62-50.7-50.7s22.62-50.7 50.7-50.7 50.7 22.62 50.7 50.7-22.62 50.7-50.7 50.7zm0-210.6c-28.08 0-50.7-22.62-50.7-50.7s22.62-50.7 50.7-50.7 50.7 22.62 50.7 50.7-22.62 50.7-50.7 50.7zM228.7 370.66c0 14.60162 12.0627 27.3 27.3 27.3s27.3-12.0627 27.3-27.3-12.6984-27.3-27.93492-27.3S228.7 355.4227 228.7 370.66zM343.36 256c0 14.6016 12.0627 27.3 27.3 27.3s27.3-12.0627 27.3-27.3-12.69838-27.3-27.93492-27.3S343.36 240.7627 343.36 256zM228.7 141.34c0 14.6016 12.0627 27.3 27.3 27.3s27.3-12.0627 27.3-27.3-12.6984-27.3-27.93492-27.3S228.7 126.1027 228.7 141.34z' fill='%23fff'/%3e%3c/svg%3e", 133 | "type" => "image/svg+xml", 134 | "sizes" => "512x512" 135 | ], 136 | [ 137 | "src" => "?app&i512", 138 | "type" => "image/png", 139 | "sizes" => "512x512", 140 | "purpose" => "any maskable" 141 | ] 142 | ], 143 | ])); 144 | $response->headers->set($response->headers->make('Cache-Control', DOTSMESH_WEB_APP_DEV_MODE ? 'no-store, no-cache, must-revalidate, max-age=0' : 'public, max-age=600')); 145 | } else { 146 | $isDebugEnabled = $request->query->exists('debug'); 147 | $content = file_get_contents(__DIR__ . '/assets/home.html'); 148 | if ($isDebugEnabled) { 149 | $content = str_replace('src="?app&a"', 'src="?app&a&debug"', $content); 150 | } 151 | $response = new App\Response\HTML($content); 152 | $response->headers->set($response->headers->make('Cache-Control', DOTSMESH_WEB_APP_DEV_MODE ? 'no-store, no-cache, must-revalidate, max-age=0' : 'public, max-age=600')); 153 | $response->headers->set($response->headers->make('X-Robots-Tag', $host === 'dotsmesh.com' ? 'nofollow' : 'noindex,nofollow')); 154 | } 155 | if ($response !== null) { 156 | $response->headers->set($response->headers->make('Strict-Transport-Security', 'max-age=63072000; preload' . ($host === 'dotsmesh.com' ? '; includeSubDomains' : ''))); 157 | return $response; 158 | } 159 | }); 160 | 161 | $app->run(); 162 | -------------------------------------------------------------------------------- /app/apps/messages/library.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Dots Mesh Web App 3 | * https://github.com/dotsmesh/dotsmesh-web-app 4 | * Free to use under the GPL-3.0 license. 5 | */ 6 | 7 | () => { 8 | 9 | var dataStorageCache = null; 10 | var getDataStorage = () => { 11 | if (dataStorageCache === null) { 12 | dataStorageCache = x.currentUser.getDataStorage('p/m/'); 13 | } 14 | return dataStorageCache; 15 | }; 16 | 17 | let threadsCache = null; 18 | var getThreads = async () => { 19 | if (threadsCache === null) { 20 | var dataStorage = getDataStorage(); 21 | var threads = await dataStorage.get('a'); 22 | if (threads !== null) { 23 | var data = x.unpack(await x.currentUser.decrypt(threads)); 24 | if (data.name === '') { 25 | threadsCache = data.value; 26 | } else { 27 | throw new Error(); 28 | } 29 | } else { 30 | threadsCache = {}; 31 | } 32 | } 33 | return threadsCache; 34 | }; 35 | 36 | var getOrMakeThreadID = async recipientIDs => { 37 | var dataStorage = getDataStorage(); 38 | var currentUserID = x.currentUser.getID(); 39 | recipientIDs = x.removeFromArray(recipientIDs, currentUserID); 40 | recipientIDs = x.arrayUnique(recipientIDs); 41 | if (recipientIDs.length === 0) { 42 | throw new Error(); 43 | } 44 | var threads = await getThreads(); 45 | var threadIDs = []; 46 | for (var threadID in threads) { 47 | var recipients = threads[threadID]; 48 | if (recipients.length > 0) { 49 | var allFound = true; 50 | for (var i in recipientIDs) { 51 | if (recipients.indexOf(recipientIDs[i]) === -1) { 52 | allFound = false; 53 | break; 54 | } 55 | } 56 | if (allFound) { 57 | return threadID; 58 | } 59 | } 60 | threadIDs.push(threadID); 61 | } 62 | var threadID = await x.generateOtherUniqueID(threadIDs); 63 | threads[threadID] = recipientIDs; 64 | await dataStorage.set('a', await x.currentUser.encrypt(x.pack('', threads))); 65 | threadsCache = null; 66 | return threadID; 67 | }; 68 | 69 | var getThreadRecipients = async threadID => { 70 | var threads = await getThreads(); 71 | if (typeof threads[threadID] !== 'undefined') { 72 | return threads[threadID]; 73 | } 74 | throw new Error(); 75 | }; 76 | 77 | // var getLatestStorage = () => { 78 | // return getDataStorage().getOrderedListContext('l/'); 79 | // }; 80 | 81 | var addIncomingMessage = async (threadID, message, onChange) => { 82 | if (message.resourcesIDs.length > 0) { 83 | var dataStorage = x.currentUser.getDataStorage(); 84 | var buffer = dataStorage.getBuffer(); 85 | for (var resourceID of message.resourcesIDs) { 86 | if (args.resources[resourceID] !== null) { 87 | buffer.duplicate(args.resources[resourceID], 'p/m/r/' + threadID + '/' + resourceID); 88 | } 89 | } 90 | await buffer.flush(); 91 | } 92 | await addMessage(threadID, message); 93 | }; 94 | 95 | var addMessage = async (threadID, message, onChange = null) => { 96 | var hasOnChange = onChange !== null; 97 | var callOnChange = async message => { 98 | await onChange(await message.clone()); 99 | } 100 | var dataStorage = getDataStorage(); 101 | var currentUserID = x.currentUser.getID(); 102 | if (message.id === null) { 103 | message.id = x.generateDateBasedID(); 104 | } 105 | if (hasOnChange) { 106 | message.data.t = { // tasks 107 | s: null //save 108 | }; 109 | } 110 | var currentUserIsTheSender = message.userID === currentUserID; 111 | var recipients = currentUserIsTheSender ? await library.getThreadRecipients(threadID) : []; 112 | if (hasOnChange) { 113 | recipients.forEach(recipientID => { 114 | message.data.t['r-' + recipientID] = null; 115 | }); 116 | await callOnChange(message); 117 | } 118 | var messageDataKey = 't/' + threadID + '/' + message.id; 119 | try { 120 | var messageToSave = await message.clone(); 121 | if (hasOnChange) { 122 | delete messageToSave.data.t.s; // clear task data for the saved message 123 | } 124 | var serializedMessageToSave = await messageToSave.pack(); 125 | var buffer = dataStorage.getBuffer(); 126 | buffer.set(messageDataKey, await x.currentUser.encrypt(serializedMessageToSave.value)); 127 | for (var resourceID in serializedMessageToSave.resourcesToSave) { 128 | buffer.set('r/' + threadID + '/' + resourceID, await x.currentUser.encrypt(x.pack('', serializedMessageToSave.resourcesToSave[resourceID]))); 129 | } 130 | // var latestDataStorage = getLatestStorage(); // todo add to buffer 131 | // await latestDataStorage.set(threadID, { 132 | // u: messageToSave.userID !== currentUserID, // unread 133 | // i: messageToSave.id, // message id 134 | // v: serializedMessageToSave.value // message json 135 | // }, { buffer: buffer }); 136 | await buffer.flush(); 137 | if (hasOnChange) { 138 | delete message.data.t.s; // delete save task 139 | //throw new Error('Error saving!'); 140 | } 141 | } catch (e) { 142 | if (hasOnChange) { 143 | message.data.t.s = e.message; // save error 144 | // todo 145 | } else { 146 | throw e; 147 | } 148 | }; 149 | if (hasOnChange) { 150 | await callOnChange(message); 151 | } 152 | 153 | if (recipients.length > 0) { 154 | var messageToSend = await message.clone(); 155 | if (hasOnChange) { 156 | delete messageToSend.data.t; // clear task data for the sent messages 157 | } 158 | var serializedMessageToSend = await messageToSend.pack(); 159 | var promisesToWait = []; 160 | recipients.forEach(recipientID => { 161 | var dataToSend = { 162 | i: messageToSave.id, 163 | v: serializedMessageToSend.value, 164 | o: (recipients.length === 1 ? [] : recipients) 165 | }; 166 | promisesToWait.push(x.user.send('mm', recipientID, x.pack('m', dataToSend), serializedMessageToSend.resourcesToSave)); 167 | }); 168 | var results = await Promise.allSettled(promisesToWait); 169 | recipients.forEach((recipientID, index) => { 170 | var result = results[index]; 171 | if (result.status === "fulfilled") { 172 | if (hasOnChange) { 173 | delete message.data.t['r-' + recipientID]; 174 | } 175 | } else if (result.status === 'rejected') { 176 | if (hasOnChange) { 177 | message.data.t['r-' + recipientID] = x.pack('', result.reason); 178 | } 179 | } 180 | }); 181 | if (hasOnChange) { 182 | if (x.isEmptyObject(message.data.t)) { 183 | delete message.data.t; 184 | } 185 | var messageToSave = await message.clone(); 186 | var serializedMessageToSave = await messageToSave.pack(); 187 | await dataStorage.set(messageDataKey, await x.currentUser.encrypt(serializedMessageToSave.value)); // todo what if this fails ?? 188 | await callOnChange(message); 189 | } 190 | } 191 | await x.announceChanges(['messages']); 192 | if (!currentUserIsTheSender) { 193 | var profile = await x.user.getProfile(message.userID); 194 | await x.announceChanges(['messages/' + threadID]); 195 | var notification = await x.notifications.make('m$' + threadID); 196 | notification.visible = true; 197 | notification.title = 'Message from ' + profile.name; 198 | if (message.textType === 'r') { 199 | notification.text = x.convertRichText(message.text, 'text'); 200 | } else { 201 | notification.text = message.text; 202 | } 203 | notification.image = { type: 'userProfile', id: message.userID }; 204 | notification.onClick = { location: 'messages/thread', args: { threadID: threadID } }; 205 | notification.tags = ['m']; 206 | await x.notifications.set(notification); 207 | } 208 | }; 209 | 210 | // var setLastSeenMessage = async (threadID, message) => { 211 | // return;// todo 212 | // var serializedMessage = await message.pack(); 213 | // var storage = getLatestStorage(); 214 | // await storage.set(threadID, { 215 | // u: false, 216 | // i: message.id, 217 | // v: serializedMessage.value 218 | // }, { 219 | // date: message.date 220 | // }); 221 | // await x.announceChanges(['messages/latest']); 222 | // }; 223 | 224 | var unserializeMessage = (threadID, id, json) => { 225 | return x.posts.unpack(id, json, async (post, resourceID) => { 226 | var dataStorage = getDataStorage(); 227 | var value = await dataStorage.get('r/' + threadID + '/' + resourceID); 228 | if (value !== null) { 229 | value = x.unpack(await x.currentUser.decrypt(value)); 230 | if (value.name === '') { // saved by the current user 231 | return value.value; 232 | } else if (value.name === 'x') { // sent by other user 233 | return value.value; 234 | } 235 | } 236 | return null; 237 | }); 238 | }; 239 | 240 | var getLatestThreads = async () => { 241 | // var storage = getLatestStorage(); 242 | // var items = await storage.getList(); 243 | // if (items.length === 0) { 244 | // return []; 245 | // } 246 | var dataStorage = getDataStorage(); 247 | var threads = await getThreads(); 248 | var threadsIDs = Object.keys(threads); 249 | var result = []; 250 | for (var i = 0; i < threadsIDs.length; i++) { 251 | var threadID = threadsIDs[i]; 252 | var list = await dataStorage.getList({ keyStartWith: 't/' + threadID + '/', keySort: 'desc', limit: 1 }); 253 | var message = null; 254 | if (list[0] !== undefined) { 255 | message = unserializeMessage(threadID, list[0].key, await x.currentUser.decrypt(list[0].value)); 256 | } 257 | result.push({ 258 | id: threadID, 259 | otherParticipantsIDs: threads[threadID], 260 | message: message 261 | }); 262 | // var messageData = item.value; 263 | // var message = unserializeMessage(threadID, messageData.i, messageData.v); 264 | // var otherParticipantsIDs = typeof threads[threadID] === 'undefined' ? [] : threads[threadID]; 265 | // result.push({ 266 | // id: threadID, 267 | // otherParticipantsIDs: otherParticipantsIDs, 268 | // senderID: message.userID, 269 | // text: message.text, 270 | // date: message.date, 271 | // unread: messageData.u 272 | // }); 273 | } 274 | return result; 275 | }; 276 | 277 | var getThreadMessages = async (threadID, listOptions) => { 278 | var dataStorage = getDataStorage().getContext('t/' + threadID + '/'); 279 | var list = await dataStorage.getList(listOptions); 280 | var messages = []; 281 | for (var i = 0; i < list.length; i++) { 282 | var item = list[i]; 283 | messages.push(unserializeMessage(threadID, item.key, await x.currentUser.decrypt(item.value))); 284 | } 285 | return messages; 286 | }; 287 | 288 | return { 289 | getOrMakeThreadID: getOrMakeThreadID, 290 | addMessage: addMessage, 291 | addIncomingMessage: addIncomingMessage, 292 | getLatestThreads: getLatestThreads, 293 | getThreadRecipients: getThreadRecipients, 294 | getThreadMessages: getThreadMessages, 295 | //setLastSeenMessage: setLastSeenMessage 296 | } 297 | } -------------------------------------------------------------------------------- /app/apps/contacts/library.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Dots Mesh Web App 3 | * https://github.com/dotsmesh/dotsmesh-web-app 4 | * Free to use under the GPL-3.0 license. 5 | */ 6 | 7 | () => { 8 | 9 | // todo the secret access keys should not be available to other apps 10 | // todo removed contact then added -> how connect when there is no public access key 11 | // optimize public access keys add and delete 12 | 13 | // (async () => { 14 | // var firewall = x.currentUser.getFirewall(); 15 | // await firewall.add('xxx'); 16 | // await firewall.add('xxx2'); 17 | // await firewall.delete('xxx'); 18 | // await firewall.add('xxx3'); 19 | // await firewall.delete('xxx3'); 20 | // })(); 21 | 22 | // (async () => { 23 | // //console.table(await x.currentUser.getPrivateDataStorage().getList()); 24 | // })(); 25 | 26 | var contactsStorage = null; 27 | var getContactsStorage = () => { 28 | if (contactsStorage === null) { 29 | contactsStorage = x.currentUser.getDataStorage('p/c/').getDetailsContext('l-', x.currentUser.isPublic() ? x.currentUserCache.get('contacts-dc') : null); 30 | } 31 | return contactsStorage; 32 | }; 33 | 34 | var oldConnectKeysStorage = null; 35 | var getOldConnectKeysStorage = () => { 36 | if (oldConnectKeysStorage === null) { 37 | oldConnectKeysStorage = x.currentUser.getDataStorage('p/c/').getDetailsContext('k-', x.currentUser.isPublic() ? x.currentUserCache.get('contactsk-dc') : null); 38 | } 39 | return oldConnectKeysStorage; 40 | }; 41 | 42 | var connectKeysStorage = null; 43 | var getConnectKeysStorage = () => { 44 | if (connectKeysStorage === null) { 45 | connectKeysStorage = x.currentUser.getDataStorage('p/c/').getDetailsContext('y-', x.currentUser.isPublic() ? x.currentUserCache.get('contactsy-dc') : null); 46 | } 47 | return connectKeysStorage; 48 | }; 49 | 50 | var requestsStorage = null; 51 | var getRequestsStorage = () => { 52 | if (requestsStorage === null) { 53 | requestsStorage = x.currentUser.getDataStorage('p/c/').getDetailsContext('p-', x.currentUser.isPublic() ? x.currentUserCache.get('contactsp-dc') : null); 54 | } 55 | return requestsStorage; 56 | }; 57 | 58 | var cache = x.currentUserCache.get('contacts'); 59 | 60 | var contactsDetailsMap = { 61 | name: 'n', 62 | identityKey: 'p', 63 | accessKey: 's', 64 | providedAccessKey: 'r', 65 | dateAdded: 'a', 66 | dateConnected: 'b', 67 | invitationSource: 'c' 68 | }; 69 | 70 | var getDetailsContextValue = contact => { 71 | var data = {}; 72 | for (var property in contactsDetailsMap) { 73 | data[contactsDetailsMap[property]] = contact[property]; 74 | } 75 | return data; 76 | }; 77 | 78 | var makeContact = (id, detailsContextValue) => { 79 | var contact = { 80 | id: null, 81 | name: null, 82 | identityKey: null, 83 | accessKey: null, 84 | providedAccessKey: null, 85 | dateAdded: null, 86 | dateConnected: null 87 | }; 88 | if (id !== null) { 89 | contact.id = id; 90 | } 91 | if (typeof detailsContextValue !== 'undefined') { 92 | for (var property in contactsDetailsMap) { 93 | if (typeof detailsContextValue[contactsDetailsMap[property]] !== 'undefined') { 94 | contact[property] = detailsContextValue[contactsDetailsMap[property]]; 95 | } 96 | } 97 | } 98 | return contact; 99 | }; 100 | 101 | var get = async userID => { 102 | var storage = getContactsStorage(); 103 | var details = await storage.get(userID); 104 | if (details !== null) { 105 | return makeContact(userID, details); 106 | } 107 | return null; 108 | }; 109 | 110 | var set = async contact => { 111 | var storage = getContactsStorage(); 112 | await storage.set(contact.id, getDetailsContextValue(contact)); 113 | await x.announceChanges(['contacts', 'contacts/' + contact.id]); 114 | }; 115 | 116 | var remove = async userID => { 117 | var storage = getContactsStorage(); 118 | var contact = await get(userID); 119 | if (contact !== null) { 120 | if (contact.providedAccessKey !== null) { 121 | var firewall = x.currentUser.getFirewall(); 122 | await firewall.delete(contact.providedAccessKey); 123 | } 124 | await storage.delete(userID); 125 | await x.announceChanges(['contacts', 'contacts/' + userID]); 126 | } 127 | }; 128 | 129 | var exists = async userID => { 130 | var storage = getContactsStorage(); 131 | return await storage.exists(userID); 132 | }; 133 | 134 | var getList = async () => { // todo details ??? 135 | var storage = getContactsStorage(); 136 | var list = await storage.getList(Object.values(contactsDetailsMap)); 137 | var result = []; 138 | for (var userID in list) { 139 | result.push(makeContact(userID, list[userID])); 140 | } 141 | return result; 142 | }; 143 | 144 | var setContactDetails = async (userID, details = {}) => { 145 | var currentUserID = x.currentUser.getID(); 146 | if (currentUserID === userID) { 147 | return false; 148 | } 149 | var publicKeys = await x.user.getPublicKeys(userID); 150 | //console.log(publicKeys); 151 | if (publicKeys === null) { // or error 152 | return false; 153 | } 154 | var profile = await x.user.getProfile(userID); 155 | var contact = await get(userID); 156 | if (contact === null) { 157 | var contact = makeContact(userID); 158 | contact.dateAdded = x.getDateID(Date.now()) 159 | } 160 | contact.name = profile.name; 161 | contact.identityKey = publicKeys.i; 162 | for (var property in details) { 163 | contact[property] = details[property]; 164 | } 165 | await set(contact); 166 | await cache.delete('contacts/' + userID + '/accessKey'); 167 | return true; 168 | }; 169 | 170 | var add = async userID => { 171 | await setContactDetails(userID); 172 | }; 173 | 174 | var sendRequest = async (userID, accessKey) => { 175 | var contact = await get(userID); 176 | var contactExists = contact !== null; 177 | var providedAccessKey = contactExists && contact.providedAccessKey !== null ? contact.providedAccessKey : x.generateRandomString(50, true); 178 | try { 179 | var dataToSend = x.pack('c', providedAccessKey); 180 | // Send connection request. It will fail if the key is invalid. 181 | var result = await x.user.send('cc', userID, dataToSend, {}, { accessKey: accessKey }); 182 | if (result === true) { 183 | var updateFirewall = false; 184 | if (contactExists) { 185 | if (contact.providedAccessKey !== providedAccessKey) { 186 | await setContactDetails(userID, { providedAccessKey: providedAccessKey }); 187 | updateFirewall = true; 188 | } 189 | } else { 190 | await setContactDetails(userID, { providedAccessKey: providedAccessKey }); 191 | updateFirewall = true; 192 | } 193 | if (updateFirewall) { 194 | var firewall = x.currentUser.getFirewall(); 195 | await firewall.add(providedAccessKey); 196 | } 197 | } 198 | } catch (e) { 199 | if (e.name === 'invalidAccessKey') { 200 | var result = false; 201 | } else { 202 | throw e; 203 | } 204 | } 205 | return result; 206 | }; 207 | 208 | // var cancelRequest = async userID => { 209 | // var contact = await get(userID); 210 | // if (contact !== null && contact.providedAccessKey !== null) { 211 | // var firewall = x.currentUser.getFirewall(); 212 | // await firewall.delete(contact.providedAccessKey); 213 | // await setContactDetails(userID, { providedAccessKey: null }); 214 | // } 215 | // }; 216 | 217 | var removeConnectNotification = async userID => { 218 | await x.notifications.delete('c$' + userID); 219 | }; 220 | 221 | var approveRequest = async userID => { 222 | var contact = await get(userID); 223 | var contactSentRequestResult = false; 224 | if (contact !== null && contact.accessKey !== null) { 225 | var accessKey = contact.accessKey; 226 | if (accessKey !== null) { 227 | contactSentRequestResult = await sendRequest(userID, accessKey); // try this key 228 | } 229 | } 230 | var request = await getRequest(userID); 231 | if (request !== null) { 232 | var accessKey = request.accessKey; 233 | await setContactDetails(userID, { accessKey: accessKey }); 234 | if (await sendRequest(userID, accessKey) || contactSentRequestResult) { 235 | await deleteRequest(userID); 236 | } 237 | } 238 | }; 239 | 240 | var makeRequest = (userID, details) => { 241 | return { 242 | userID: userID, 243 | accessKey: (details.a !== undefined ? details.a : null), 244 | invitationSource: (details.i !== undefined ? details.i : null), 245 | dateCreated: (details.d !== undefined ? details.d : null) 246 | }; 247 | }; 248 | 249 | var getRequest = async userID => { 250 | var storage = getRequestsStorage(); 251 | var data = await storage.get(userID); 252 | return data === null ? null : makeRequest(userID, data); 253 | }; 254 | 255 | var deleteRequest = async userID => { 256 | var storage = getRequestsStorage(); 257 | await storage.delete(userID); 258 | await removeConnectNotification(userID); 259 | await x.announceChanges(['contactsRequests', 'contactsRequests/' + userID]); 260 | }; 261 | 262 | var addRequest = async (userID, accessKey, invitationSource) => { // return TRUE if new request or new connected contact 263 | var currentUserID = x.currentUser.getID(); 264 | if (currentUserID === userID) { 265 | return false; 266 | } 267 | var contact = await get(userID); 268 | if (contact === null) { 269 | var storage = getRequestsStorage(); 270 | var data = await storage.get(userID); 271 | var isNew = false; 272 | if (data === null) { 273 | data = {}; 274 | isNew = true; 275 | } 276 | data.a = accessKey; 277 | data.i = invitationSource; 278 | data.d = x.getDateID(Date.now(), 1); 279 | await storage.set(userID, data); 280 | await x.announceChanges(['contactsRequests', 'contactsRequests/' + userID]); 281 | return isNew; 282 | } else { 283 | var isNewConnection = contact.accessKey === null; 284 | if (contact.accessKey === accessKey) { 285 | return false; 286 | } 287 | await setContactDetails(userID, { accessKey: accessKey, invitationSource: invitationSource }); 288 | if (contact.providedAccessKey !== null) { // when the contact reconnects 289 | await sendRequest(userID, accessKey); 290 | } 291 | return isNewConnection; 292 | } 293 | }; 294 | 295 | var getRequestsList = async () => { 296 | var storage = getRequestsStorage(); 297 | var list = await storage.getList(['a', 'i', 'd']); 298 | var result = []; 299 | for (var userID in list) { 300 | result.push(makeRequest(userID, list[userID])); 301 | } 302 | return result; 303 | }; 304 | 305 | var getAccessKey = async userID => { 306 | var cacheKey = 'contacts/' + userID + '/accessKey'; 307 | var value = await cache.get(cacheKey); 308 | if (value === null) { 309 | var result = await get(userID); 310 | value = result !== null ? result.accessKey : null; 311 | await cache.set(cacheKey, value); // todo optimize - null case ??? 312 | } 313 | return value; 314 | }; 315 | 316 | var getIdentityKey = async userID => { 317 | var cacheKey = 'contacts/' + userID + '/identityKey'; 318 | var value = await cache.get(cacheKey); 319 | if (value === null) { 320 | var result = await get(userID); 321 | value = result !== null ? result.identityKey : null; 322 | await cache.set(cacheKey, value); // todo optimize - null case ??? 323 | } 324 | return value; 325 | }; 326 | 327 | var getProvidedAccessKeys = async () => { 328 | var result = {}; 329 | 330 | var storage = getContactsStorage(); 331 | var list = await storage.getList(['r']); 332 | for (var contactID in list) { 333 | var contactDetails = list[contactID]; 334 | if (contactDetails.r !== null) { 335 | result[contactDetails.r] = { type: 'contact', id: contactID }; 336 | } 337 | } 338 | 339 | var storage = getConnectKeysStorage(); 340 | var list = await storage.getList(); 341 | for (var key in list) { 342 | result['k/' + key] = { type: 'connectKey', key: key }; 343 | } 344 | 345 | if (await getOpenConnectStatus()) { 346 | result['o/c'] = { type: 'openConnect' }; 347 | } 348 | 349 | var storage = getOldConnectKeysStorage(); 350 | var list = await storage.getList(); 351 | for (var key in list) { 352 | result[key] = { type: 'connectKey', key: key }; 353 | } 354 | 355 | return result; 356 | }; 357 | 358 | var makeConnectKey = (key, details) => { 359 | return { 360 | key: key, 361 | //name: (typeof details.n !== 'undefined' ? details.n : ''), 362 | dateCreated: (details.d !== undefined ? details.d : null) 363 | }; 364 | }; 365 | 366 | var getConnectKey = async key => { 367 | var storage = getConnectKeysStorage(); 368 | var details = await storage.get(key); 369 | if (details !== null) { 370 | return makeConnectKey(key, details); 371 | } 372 | 373 | var storage = getOldConnectKeysStorage(); 374 | var details = await storage.get(key); 375 | if (details !== null) { 376 | return makeConnectKey(key, details); 377 | } 378 | 379 | return null; 380 | }; 381 | 382 | var setConnectKey = async key => { 383 | var storage = getConnectKeysStorage(); 384 | // if (key === null) { 385 | // for (var i = 0; i < 999; i++) { 386 | // var newKey = x.generateRandomString(10); 387 | // if (!(await storage.exists(newKey))) { 388 | // key = newKey; 389 | // break; 390 | // } 391 | // } 392 | // if (key === null) { 393 | // throw new Error(); 394 | // } 395 | // } 396 | if (!await storage.exists(key)) { 397 | await storage.set(key, { d: x.getDateID(Date.now(), 1) }); 398 | var firewall = x.currentUser.getFirewall(); 399 | await firewall.add('k/' + key); 400 | await firewall.add(key); // old format 401 | } 402 | await x.announceChanges(['contactsConnectKeys']); 403 | return key; 404 | }; 405 | 406 | var deleteConnectKey = async key => { 407 | var storage = getConnectKeysStorage(); 408 | await storage.delete(key); 409 | var firewall = x.currentUser.getFirewall(); 410 | await firewall.delete('k/' + key); 411 | await x.announceChanges(['contactsConnectKeys']); 412 | }; 413 | 414 | var getConnectKeysList = async () => { 415 | var result = []; 416 | 417 | var storage = getConnectKeysStorage(); 418 | var list = await storage.getList(['d']); 419 | var addedKeys = []; 420 | for (var key in list) { 421 | addedKeys.push(key); 422 | result.push(makeConnectKey(key, list[key])); 423 | } 424 | 425 | var storage = getOldConnectKeysStorage(); 426 | var list = await storage.getList(['d']); 427 | for (var key in list) { 428 | if (addedKeys.indexOf(key) === -1) { 429 | result.push(makeConnectKey(key, list[key])); 430 | } 431 | } 432 | 433 | return result; 434 | }; 435 | 436 | var getOpenConnectStatus = async () => { 437 | var storage = x.currentUser.getDataStorage('p/c/'); 438 | var value = await storage.get('oc'); 439 | if (value !== null) { 440 | value = x.unpack(await x.currentUser.decrypt(value)); 441 | if (value.name === '') { 442 | return value.value === 1; 443 | } else { 444 | throw new Error('') 445 | }; 446 | } 447 | return false; 448 | }; 449 | 450 | var setOpenConnectStatus = async allow => { 451 | var storage = x.currentUser.getDataStorage('p/c/'); 452 | await storage.set('oc', await x.currentUser.encrypt(x.pack('', allow ? 1 : 0))); 453 | var firewall = x.currentUser.getFirewall(); 454 | var firewallKey = 'o/c'; // open connect 455 | if (allow) { 456 | await firewall.add(firewallKey); 457 | } else { 458 | await firewall.delete(firewallKey); 459 | } 460 | await x.announceChanges(['contactsOpenStatus']); 461 | }; 462 | 463 | return { 464 | add: add, 465 | get: get, 466 | exists: exists, 467 | remove: remove, 468 | getList: getList, 469 | addRequest: addRequest, 470 | getAccessKey: getAccessKey, 471 | getIdentityKey: getIdentityKey, 472 | getProvidedAccessKeys: getProvidedAccessKeys, 473 | sendRequest: sendRequest, 474 | //cancelRequest: cancelRequest, 475 | approveRequest: approveRequest, 476 | getConnectKeysList: getConnectKeysList, 477 | getConnectKey: getConnectKey, 478 | setConnectKey: setConnectKey, 479 | deleteConnectKey: deleteConnectKey, 480 | getOpenConnectStatus: getOpenConnectStatus, 481 | setOpenConnectStatus: setOpenConnectStatus, 482 | getRequest: getRequest, 483 | getRequestsList: getRequestsList, 484 | deleteRequest: deleteRequest 485 | }; 486 | }; --------------------------------------------------------------------------------