Greatly simplify and improve contact management

This commit is contained in:
Tony Garnock-Jones 2016-12-06 17:58:52 +13:00
parent cbdc19fc8e
commit db0282ca72
8 changed files with 150 additions and 116 deletions

38
examples/webchat/NOTES.md Normal file
View File

@ -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.

View File

@ -2,10 +2,10 @@
<div class="cursor-interactive contact-list-present-{{isPresent}} dropdown-toggle" data-toggle="dropdown">
<img class="avatar" src="{{avatar}}">
<span class="forcewrap">{{email}}</span>
{{#isPresent}}<span>(online)</span>{{/isPresent}}
{{#pendingContactRequest}}(pending){{/pendingContactRequest}}
</div>
<div class="dropdown-menu pt-0 w-100">
<!-- <img src="{{avatar}}&s=512" class="w-100"> -->
<img src="{{avatar}}&s=512" class="w-100">
<div class="my-1 mx-2">
<h3 class="forcewrap">{{email}}</h3>
<!-- <p> -->
@ -15,10 +15,8 @@
<!-- <hr> -->
<!-- <p>Rest of text.</p> -->
</div>
<!-- <button class="dropdown-item">Follows you</button> -->
<!-- <button class="dropdown-item">Cancel pending follow request</button> -->
<!-- <button class="dropdown-item"><i class="dropdown-marginal icon ion-person-add"></i>Follow this person</button> -->
<!-- <div class="dropdown-divider"></div> -->
<!-- <button class="dropdown-item"><i class="dropdown-marginal icon ion-trash-b"></i>Delete contact</button> -->
<div class="dropdown-divider"></div>
{{#pendingContactRequest}}<button class="dropdown-item delete-contact"><i class="dropdown-marginal icon ion-help"></i>Cancel pending contact request</button>{{/pendingContactRequest}}
{{^pendingContactRequest}}<button class="dropdown-item delete-contact"><i class="dropdown-marginal icon ion-trash-b"></i>Delete contact</button>{{/pendingContactRequest}}
</div>
</div>

View File

@ -4,8 +4,6 @@
<form class="form-inline">
<label for="add-contact-email">New contact email: </label>
<input class="form-control" id="add-contact-email" type="email">
<label for="reciprocate">Automatically allow them to follow you? </label>
<input class="form-control" id="reciprocate" type="checkbox" checked>
<button class="btn btn-default" id="add-contact">Add contact</button>
</form>
@ -59,12 +57,14 @@
<div class="hide-zero-count count{{otherRequestCount}}">
<p>
<label for="show-all-requests-from-others">Show all pending requests from others? </label>
<input type="checkbox" id="show-all-requests-from-others">
<input type="checkbox" id="show-all-requests-from-others" {{#showRequestsFromOthers}}checked{{/showRequestsFromOthers}}>
</p>
{{#showRequestsFromOthers}}
<div id="all-requests-from-others-div">
<h2>All requests from others</h2>
<ul id="others-permission-requests"></ul>
</div>
{{/showRequestsFromOthers}}
</div>
</div>

View File

@ -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)) {

View File

@ -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))))

View File

@ -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)])))))))

View File

@ -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)

View File

@ -48,4 +48,4 @@
grantee
permission
the-issuer
(if delegable? ", delegably," ""))))))
(if delegable? ", delegably" ""))))))