diff --git a/examples/webchat/NOTES.md b/examples/webchat/NOTES.md new file mode 100644 index 0000000..abbf50d --- /dev/null +++ b/examples/webchat/NOTES.md @@ -0,0 +1,38 @@ +## Sorting out contact states + +### Design + +Contacts are symmetric: If A follows B, then B follows A. + +Let's look at how the state of the A/B relationship changes: + + - Initial state: neither A nor B follows the other. + - ACTION: A adds B to their contacts + - A proposes an A/B link. + - ACTION: A may cancel the proposition + - Return to initial state. + - ACTION: B may approve the proposition + - A/B link established. + - ACTION: B may reject the proposition + - Return to initial state. + - ACTION: B may ignore the proposition + - B's user interface no longer displays the request, + but if B subsequently proposes an A/B link, it is + as if B approved the previously-proposed link. + + - From "A/B link established": + - ACTION: A may cancel the link + - Return to initial state. + - ACTION: B may cancel the link + - Return to initial state. + +B should appear in A's contact list in any of these cases: + + 1. A has proposed an A/B link. + 2. An A/B link exists. + +In the first case, B should appear as a "pending link request": as +offline, with a "cancel link request" action available. + +In the second case, B should appear as fully linked, either offline or +online, with a "delete contact" action available. diff --git a/examples/webchat/htdocs/templates/contact-entry.html b/examples/webchat/htdocs/templates/contact-entry.html index d910656..cf6bff2 100644 --- a/examples/webchat/htdocs/templates/contact-entry.html +++ b/examples/webchat/htdocs/templates/contact-entry.html @@ -2,10 +2,10 @@ diff --git a/examples/webchat/htdocs/templates/mainpage.html b/examples/webchat/htdocs/templates/mainpage.html index 6401e22..c0c1761 100644 --- a/examples/webchat/htdocs/templates/mainpage.html +++ b/examples/webchat/htdocs/templates/mainpage.html @@ -4,8 +4,6 @@
- -
@@ -59,12 +57,14 @@

- +

+ {{#showRequestsFromOthers}}

All requests from others

+ {{/showRequestsFromOthers}}
diff --git a/examples/webchat/htdocs/webchat.syndicate.js b/examples/webchat/htdocs/webchat.syndicate.js index c6d5b96..0c85d88 100644 --- a/examples/webchat/htdocs/webchat.syndicate.js +++ b/examples/webchat/htdocs/webchat.syndicate.js @@ -94,6 +94,7 @@ field this.questionCount = 0; // questions from the system field this.globallyVisible = false; // mirrors *other people's experience of us* field this.locallyVisible = true; + field this.showRequestsFromOthers = false; assert brokerConnection(brokerUrl); @@ -108,7 +109,8 @@ questionCount: this.questionCount, myRequestCount: this.myRequestCount, otherRequestCount: this.otherRequestCount, - globallyVisible: this.globallyVisible + globallyVisible: this.globallyVisible, + showRequestsFromOthers: this.showRequestsFromOthers })); } @@ -154,45 +156,48 @@ during inbound(uiTemplate("contact-entry.html", $entry)) { during Syndicate.UI.locationHash('/contacts') { during inbound(contactListEntry(sessionInfo.email, $contact)) { + field this.pendingContactRequest = false; field this.isPresent = false; - on asserted inbound(present(contact)) { this.isPresent = true; } - on retracted inbound(present(contact)) { this.isPresent = false; } + during inbound(present(contact)) { + on start { this.isPresent = true; } + on stop { this.isPresent = false; } + } + during inbound(permissionRequest(contact, sessionInfo.email, pFollow(contact))) { + on start { this.pendingContactRequest = true; } + on stop { this.pendingContactRequest = false; } + } var c = this.ui.context(mainpageVersion, 'all-contacts', contact); assert c.html('#main-tab-body-contacts .contact-list', Mustache.render(entry, { email: contact, avatar: avatar(contact), + pendingContactRequest: this.pendingContactRequest, isPresent: this.isPresent })); - on message c.event('.do-hi', 'click', $e) { - alert(contact); + on message c.event('.delete-contact', 'click', _) { + if (confirm((this.pendingContactRequest + ? "Cancel contact request to " + : "Delete contact ") + + contact + "?")) { + :: outbound(deleteResource(permitted(sessionInfo.email, + contact, + pFollow(sessionInfo.email), + false))); // TODO: true too?! + } } } } } - during inputValue('#add-contact-email', $contact) { - during inputValue('#reciprocate', $reciprocate) { + during inputValue('#add-contact-email', $rawContact) { + var contact = rawContact.trim(); + if (contact) { on message mainpage_c.event('#add-contact', 'click', _) { - if (reciprocate) { - :: outbound(createResource(grant(sessionInfo.email, - sessionInfo.email, - contact, - pFollow(sessionInfo.email), - false))); - } - - :: outbound(createResource(contactListEntry(sessionInfo.email, contact))); - :: outbound(createResource(permissionRequest(contact, - sessionInfo.email, - pFollow(contact)))); - - // :: outbound(createResource(permissionRequest(contact, - // sessionInfo.email, - // pInvite(contact)))); - // :: outbound(createResource(permissionRequest(contact, - // sessionInfo.email, - // pSeePresence(contact)))); + :: outbound(createResource(grant(sessionInfo.email, + sessionInfo.email, + contact, + pFollow(sessionInfo.email), + false))); $('#add-contact-email').val(''); } } @@ -253,10 +258,7 @@ } during inputValue('#show-all-requests-from-others', $showRequestsFromOthers) { - on start { - var d = $('#all-requests-from-others-div'); - if (showRequestsFromOthers) { d.show(); } else { d.hide(); } - } + on start { this.showRequestsFromOthers = showRequestsFromOthers; } } during inbound(uiTemplate("permission-request-in-GENERIC.html", $genericEntry)) { diff --git a/examples/webchat/server/api.rkt b/examples/webchat/server/api.rkt index c95cee1..76b9934 100644 --- a/examples/webchat/server/api.rkt +++ b/examples/webchat/server/api.rkt @@ -57,22 +57,17 @@ (actor #:name 'take-trust-instructions (stop-when-reloaded) - (on (message (api (session $owner _) (create-resource (? contact-list-entry? $e)))) - (when (equal? owner (contact-list-entry-owner e)) - (send! (create-resource e)))) - (on (message (api (session $owner _) (delete-resource (? contact-list-entry? $e)))) - (when (equal? owner (contact-list-entry-owner e)) - (send! (delete-resource e)))) - (on (message (api (session $grantor _) (create-resource (? grant? $g)))) (when (equal? grantor (grant-grantor g)) (send! (create-resource g)))) (on (message (api (session $grantor _) (delete-resource (? grant? $g)))) - (when (equal? grantor (grant-grantor g)) + (when (or (equal? grantor (grant-grantor g)) + (equal? grantor (grant-issuer g))) (send! (delete-resource g)))) - (on (message (api (session $grantee _) (delete-resource (? permitted? $p)))) - (when (equal? grantee (permitted-email p)) + (on (message (api (session $principal _) (delete-resource (? permitted? $p)))) + (when (or (equal? principal (permitted-email p)) ;; relinquish + (equal? principal (permitted-issuer p))) ;; revoke; TODO: deal with delegation (send! (delete-resource p)))) (on (message (api (session $grantee _) (create-resource (? permission-request? $r)))) diff --git a/examples/webchat/server/contacts.rkt b/examples/webchat/server/contacts.rkt index 99328f1..2be4a64 100644 --- a/examples/webchat/server/contacts.rkt +++ b/examples/webchat/server/contacts.rkt @@ -12,72 +12,75 @@ (struct present (email) #:prefab) (supervise - (actor #:name 'reflect-contacts + (actor #:name 'reflect-presence (stop-when-reloaded) (during (api (session $who _) (online)) - (during (permitted who $grantee (p:follow #;p:see-presence who) _) + (during (permitted who $grantee (p:follow who) _) ;; `who` allows `grantee` to follow them (assert (api (session grantee _) (present who))))))) -(actor #:name 'contact-list-factory - (stop-when-reloaded) - (on (message (create-resource ($ e (contact-list-entry $owner $member)))) - (actor #:name e - (on-start (log-info "~s adds ~s to their contact list" owner member)) - (on-stop (log-info "~s removes ~s from their contact list" owner member)) - (assert e) - (stop-when-duplicate e) - (stop-when (message (delete-resource e))) - (stop-when (asserted (delete-account owner))) - (stop-when (asserted (delete-account member)))))) +(supervise + (actor #:name 'ensure-p:follow-symmetric + (stop-when-reloaded) + (on (asserted (permitted $A $B (p:follow $maybe-A) _)) + (when (equal? A maybe-A) + (send! (create-resource (permission-request B A (p:follow B)))))) + (on (retracted (permitted $A $B (p:follow $maybe-A) _)) + (when (equal? A maybe-A) + (send! (delete-resource (permission-request B A (p:follow B)))) + (send! (delete-resource (permitted B A (p:follow B) ?))))) + (on (retracted (permission-request $A $B (p:follow $maybe-A))) + (when (equal? A maybe-A) + (when (not (immediate-query [query-value #f (permitted A B (p:follow A) _) #t])) + (send! (delete-resource (permitted B A (p:follow B) ?)))))))) + +(supervise + (actor #:name 'contact-list-factory + (stop-when-reloaded) + (during (permission-request $A $B (p:follow $maybe-A)) + (when (equal? A maybe-A) + (assert (contact-list-entry B A)))) + (during (permitted $A $B (p:follow $maybe-A) _) + (when (equal? A maybe-A) + (when (string