Greatly simplify and improve contact management
This commit is contained in:
parent
cbdc19fc8e
commit
db0282ca72
|
@ -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.
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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)) {
|
||||
|
|
|
@ -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))))
|
||||
|
|
|
@ -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)])))))))
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -48,4 +48,4 @@
|
|||
grantee
|
||||
permission
|
||||
the-issuer
|
||||
(if delegable? ", delegably," ""))))))
|
||||
(if delegable? ", delegably" ""))))))
|
||||
|
|
Loading…
Reference in New Issue