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 @@
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 A B)
+ (during (permitted B A (p:follow B) _)
+ (assert (contact-list-entry A B))
+ (assert (contact-list-entry B A))))))))
+
+(supervise
+ (actor #:name 'contact-list-change-log
+ (stop-when-reloaded)
+ (on (asserted (contact-list-entry $owner $member))
+ (log-info "~s adds ~s to their contact list" owner member))
+ (on (retracted (contact-list-entry $owner $member))
+ (log-info "~s removes ~s from their contact list" owner member))))
(supervise
(actor #:name 'contacts:questions
(stop-when-reloaded)
- ;; TODO: NOTE: When the `permission-request` vanishes (due to
- ;; satisfaction or rejection), this should remove the question
- ;; from all eligible answerers at once
+ ;; TODO: CHECK THE FOLLOWING: When the `permission-request` vanishes (due to
+ ;; satisfaction or rejection), this should remove the question from all eligible
+ ;; answerers at once
(during (permission-request $who $grantee ($ p (p:follow _)))
(when (equal? who (p:follow-email p))
;; `grantee` wants to follow `who`
(during (permitted who $grantor p #t)
;; `grantor` can make that decision
- (on-start
- (define-values (title blurb)
- (if (equal? who grantor)
- (values (format "Follow request from ~a" grantee)
- `(p "User " (b ,grantee) " wants to be able to invite you "
- "to conversations and see when you are online."))
- (values (format "Request from ~a to follow ~a" grantee who)
- `(p "User " (b ,grantee) " wants to be able to invite "
- (b ,who) " to conversations and see when they are online."))))
- (define base-options
- (list (list "deny" "Reject")
- (list "ignore" "Ignore")))
- (match (ask-question #:title title #:blurb blurb #:target grantor #:class "q-follow"
- (option-question
- ;; If who == grantor, then the grantor is directly
- ;; the person being followed, and should be offered
- ;; the option to follow back, unless they've already
- ;; taken that option, which can be deduced if BOTH
- ;; the grantee has declared that the grantor may
- ;; follow the grantee AND the grantor has declared
- ;; that the grantee is a member of their contact
- ;; list.
- (if (and (equal? who grantor)
- (not (and
- (immediate-query [query-value #f (permitted grantee grantor (p:follow grantee) _) #t])
- (immediate-query [query-value #f (contact-list-entry grantor grantee) #t]))))
- (list* (list "allow-and-return" "Accept and follow back")
- (list "allow" "Accept, but do not follow back")
- base-options)
- (cons (list "allow" "Accept")
- base-options))))
- ["allow-and-return"
- (send! (create-resource (grant who grantor grantee p #f)))
- (send! (create-resource (contact-list-entry grantor grantee)))
- (send! (create-resource (permission-request grantee grantor (p:follow grantee))))]
- ["allow" (send! (create-resource (grant who grantor grantee p #f)))]
- ["deny" (send! (delete-resource (permission-request who grantee p)))]
- ["ignore" (void)])))))))
-
+ (define-values (title blurb)
+ (if (equal? who grantor)
+ (values (format "Contact request from ~a" grantee)
+ `(p "User " (b ,grantee) " wants to be able to invite you "
+ "to conversations and see when you are online."))
+ (values (format "Contact request from ~a to ~a" grantee who)
+ `(p "User " (b ,grantee) " wants to be able to invite "
+ (b ,who) " to conversations and see when they are online."))))
+ (define qid
+ (ask-question! #:title title #:blurb blurb #:target grantor #:class "q-follow"
+ (option-question (list (list "allow" "Accept")
+ (list "deny" "Reject")
+ (list "ignore" "Ignore")))))
+ (stop-when (asserted (answer qid $v))
+ (match v
+ ["allow" (send! (create-resource (grant who grantor grantee p #f)))]
+ ["deny" (send! (delete-resource (permission-request who grantee p)))]
+ ["ignore" (void)])))))))
diff --git a/examples/webchat/server/qa.rkt b/examples/webchat/server/qa.rkt
index 14bd729..bd43727 100644
--- a/examples/webchat/server/qa.rkt
+++ b/examples/webchat/server/qa.rkt
@@ -1,6 +1,6 @@
#lang syndicate/actor
-(provide ask-question)
+(provide ask-question!)
(require racket/port)
(require markdown)
@@ -21,11 +21,11 @@
(during (api (session target _) (answer qid $value))
(assert (answer qid value))))))
-(define (ask-question #:title title
- #:blurb blurb
- #:class [q-class "q-generic"]
- #:target target
- question-type)
+(define (ask-question! #:title title
+ #:blurb blurb
+ #:class [q-class "q-generic"]
+ #:target target
+ question-type)
(define qid (random-hex-string 32))
(define q (question qid
(current-seconds)
@@ -36,8 +36,6 @@
(lambda ()
(display-xexpr blurb)))
question-type))
- (react/suspend (k)
- (assert q)
- (stop-when (asserted (answer qid $v))
- (k v))))
+ (assert q)
+ qid)
diff --git a/examples/webchat/server/trust.rkt b/examples/webchat/server/trust.rkt
index 2e8fd34..ebc1549 100644
--- a/examples/webchat/server/trust.rkt
+++ b/examples/webchat/server/trust.rkt
@@ -48,4 +48,4 @@
grantee
permission
the-issuer
- (if delegable? ", delegably," ""))))))
+ (if delegable? ", delegably" ""))))))