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">
|
<div class="cursor-interactive contact-list-present-{{isPresent}} dropdown-toggle" data-toggle="dropdown">
|
||||||
<img class="avatar" src="{{avatar}}">
|
<img class="avatar" src="{{avatar}}">
|
||||||
<span class="forcewrap">{{email}}</span>
|
<span class="forcewrap">{{email}}</span>
|
||||||
{{#isPresent}}<span>(online)</span>{{/isPresent}}
|
{{#pendingContactRequest}}(pending){{/pendingContactRequest}}
|
||||||
</div>
|
</div>
|
||||||
<div class="dropdown-menu pt-0 w-100">
|
<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">
|
<div class="my-1 mx-2">
|
||||||
<h3 class="forcewrap">{{email}}</h3>
|
<h3 class="forcewrap">{{email}}</h3>
|
||||||
<!-- <p> -->
|
<!-- <p> -->
|
||||||
|
@ -15,10 +15,8 @@
|
||||||
<!-- <hr> -->
|
<!-- <hr> -->
|
||||||
<!-- <p>Rest of text.</p> -->
|
<!-- <p>Rest of text.</p> -->
|
||||||
</div>
|
</div>
|
||||||
<!-- <button class="dropdown-item">Follows you</button> -->
|
<div class="dropdown-divider"></div>
|
||||||
<!-- <button class="dropdown-item">Cancel pending follow request</button> -->
|
{{#pendingContactRequest}}<button class="dropdown-item delete-contact"><i class="dropdown-marginal icon ion-help"></i>Cancel pending contact request</button>{{/pendingContactRequest}}
|
||||||
<!-- <button class="dropdown-item"><i class="dropdown-marginal icon ion-person-add"></i>Follow this person</button> -->
|
{{^pendingContactRequest}}<button class="dropdown-item delete-contact"><i class="dropdown-marginal icon ion-trash-b"></i>Delete contact</button>{{/pendingContactRequest}}
|
||||||
<!-- <div class="dropdown-divider"></div> -->
|
|
||||||
<!-- <button class="dropdown-item"><i class="dropdown-marginal icon ion-trash-b"></i>Delete contact</button> -->
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -4,8 +4,6 @@
|
||||||
<form class="form-inline">
|
<form class="form-inline">
|
||||||
<label for="add-contact-email">New contact email: </label>
|
<label for="add-contact-email">New contact email: </label>
|
||||||
<input class="form-control" id="add-contact-email" type="email">
|
<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>
|
<button class="btn btn-default" id="add-contact">Add contact</button>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
@ -59,12 +57,14 @@
|
||||||
<div class="hide-zero-count count{{otherRequestCount}}">
|
<div class="hide-zero-count count{{otherRequestCount}}">
|
||||||
<p>
|
<p>
|
||||||
<label for="show-all-requests-from-others">Show all pending requests from others? </label>
|
<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>
|
</p>
|
||||||
|
{{#showRequestsFromOthers}}
|
||||||
<div id="all-requests-from-others-div">
|
<div id="all-requests-from-others-div">
|
||||||
<h2>All requests from others</h2>
|
<h2>All requests from others</h2>
|
||||||
<ul id="others-permission-requests"></ul>
|
<ul id="others-permission-requests"></ul>
|
||||||
</div>
|
</div>
|
||||||
|
{{/showRequestsFromOthers}}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -94,6 +94,7 @@
|
||||||
field this.questionCount = 0; // questions from the system
|
field this.questionCount = 0; // questions from the system
|
||||||
field this.globallyVisible = false; // mirrors *other people's experience of us*
|
field this.globallyVisible = false; // mirrors *other people's experience of us*
|
||||||
field this.locallyVisible = true;
|
field this.locallyVisible = true;
|
||||||
|
field this.showRequestsFromOthers = false;
|
||||||
|
|
||||||
assert brokerConnection(brokerUrl);
|
assert brokerConnection(brokerUrl);
|
||||||
|
|
||||||
|
@ -108,7 +109,8 @@
|
||||||
questionCount: this.questionCount,
|
questionCount: this.questionCount,
|
||||||
myRequestCount: this.myRequestCount,
|
myRequestCount: this.myRequestCount,
|
||||||
otherRequestCount: this.otherRequestCount,
|
otherRequestCount: this.otherRequestCount,
|
||||||
globallyVisible: this.globallyVisible
|
globallyVisible: this.globallyVisible,
|
||||||
|
showRequestsFromOthers: this.showRequestsFromOthers
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -154,45 +156,48 @@
|
||||||
during inbound(uiTemplate("contact-entry.html", $entry)) {
|
during inbound(uiTemplate("contact-entry.html", $entry)) {
|
||||||
during Syndicate.UI.locationHash('/contacts') {
|
during Syndicate.UI.locationHash('/contacts') {
|
||||||
during inbound(contactListEntry(sessionInfo.email, $contact)) {
|
during inbound(contactListEntry(sessionInfo.email, $contact)) {
|
||||||
|
field this.pendingContactRequest = false;
|
||||||
field this.isPresent = false;
|
field this.isPresent = false;
|
||||||
on asserted inbound(present(contact)) { this.isPresent = true; }
|
during inbound(present(contact)) {
|
||||||
on retracted inbound(present(contact)) { this.isPresent = false; }
|
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);
|
var c = this.ui.context(mainpageVersion, 'all-contacts', contact);
|
||||||
assert c.html('#main-tab-body-contacts .contact-list',
|
assert c.html('#main-tab-body-contacts .contact-list',
|
||||||
Mustache.render(entry, {
|
Mustache.render(entry, {
|
||||||
email: contact,
|
email: contact,
|
||||||
avatar: avatar(contact),
|
avatar: avatar(contact),
|
||||||
|
pendingContactRequest: this.pendingContactRequest,
|
||||||
isPresent: this.isPresent
|
isPresent: this.isPresent
|
||||||
}));
|
}));
|
||||||
on message c.event('.do-hi', 'click', $e) {
|
on message c.event('.delete-contact', 'click', _) {
|
||||||
alert(contact);
|
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('#add-contact-email', $rawContact) {
|
||||||
during inputValue('#reciprocate', $reciprocate) {
|
var contact = rawContact.trim();
|
||||||
|
if (contact) {
|
||||||
on message mainpage_c.event('#add-contact', 'click', _) {
|
on message mainpage_c.event('#add-contact', 'click', _) {
|
||||||
if (reciprocate) {
|
|
||||||
:: outbound(createResource(grant(sessionInfo.email,
|
:: outbound(createResource(grant(sessionInfo.email,
|
||||||
sessionInfo.email,
|
sessionInfo.email,
|
||||||
contact,
|
contact,
|
||||||
pFollow(sessionInfo.email),
|
pFollow(sessionInfo.email),
|
||||||
false)));
|
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))));
|
|
||||||
$('#add-contact-email').val('');
|
$('#add-contact-email').val('');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -253,10 +258,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
during inputValue('#show-all-requests-from-others', $showRequestsFromOthers) {
|
during inputValue('#show-all-requests-from-others', $showRequestsFromOthers) {
|
||||||
on start {
|
on start { this.showRequestsFromOthers = showRequestsFromOthers; }
|
||||||
var d = $('#all-requests-from-others-div');
|
|
||||||
if (showRequestsFromOthers) { d.show(); } else { d.hide(); }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
during inbound(uiTemplate("permission-request-in-GENERIC.html", $genericEntry)) {
|
during inbound(uiTemplate("permission-request-in-GENERIC.html", $genericEntry)) {
|
||||||
|
|
|
@ -57,22 +57,17 @@
|
||||||
(actor #:name 'take-trust-instructions
|
(actor #:name 'take-trust-instructions
|
||||||
(stop-when-reloaded)
|
(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))))
|
(on (message (api (session $grantor _) (create-resource (? grant? $g))))
|
||||||
(when (equal? grantor (grant-grantor g))
|
(when (equal? grantor (grant-grantor g))
|
||||||
(send! (create-resource g))))
|
(send! (create-resource g))))
|
||||||
(on (message (api (session $grantor _) (delete-resource (? grant? $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))))
|
(send! (delete-resource g))))
|
||||||
|
|
||||||
(on (message (api (session $grantee _) (delete-resource (? permitted? $p))))
|
(on (message (api (session $principal _) (delete-resource (? permitted? $p))))
|
||||||
(when (equal? grantee (permitted-email p))
|
(when (or (equal? principal (permitted-email p)) ;; relinquish
|
||||||
|
(equal? principal (permitted-issuer p))) ;; revoke; TODO: deal with delegation
|
||||||
(send! (delete-resource p))))
|
(send! (delete-resource p))))
|
||||||
|
|
||||||
(on (message (api (session $grantee _) (create-resource (? permission-request? $r))))
|
(on (message (api (session $grantee _) (create-resource (? permission-request? $r))))
|
||||||
|
|
|
@ -12,72 +12,75 @@
|
||||||
(struct present (email) #:prefab)
|
(struct present (email) #:prefab)
|
||||||
|
|
||||||
(supervise
|
(supervise
|
||||||
(actor #:name 'reflect-contacts
|
(actor #:name 'reflect-presence
|
||||||
(stop-when-reloaded)
|
(stop-when-reloaded)
|
||||||
(during (api (session $who _) (online))
|
(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
|
;; `who` allows `grantee` to follow them
|
||||||
(assert (api (session grantee _) (present who)))))))
|
(assert (api (session grantee _) (present who)))))))
|
||||||
|
|
||||||
(actor #:name 'contact-list-factory
|
(supervise
|
||||||
|
(actor #:name 'ensure-p:follow-symmetric
|
||||||
(stop-when-reloaded)
|
(stop-when-reloaded)
|
||||||
(on (message (create-resource ($ e (contact-list-entry $owner $member))))
|
(on (asserted (permitted $A $B (p:follow $maybe-A) _))
|
||||||
(actor #:name e
|
(when (equal? A maybe-A)
|
||||||
(on-start (log-info "~s adds ~s to their contact list" owner member))
|
(send! (create-resource (permission-request B A (p:follow B))))))
|
||||||
(on-stop (log-info "~s removes ~s from their contact list" owner member))
|
(on (retracted (permitted $A $B (p:follow $maybe-A) _))
|
||||||
(assert e)
|
(when (equal? A maybe-A)
|
||||||
(stop-when-duplicate e)
|
(send! (delete-resource (permission-request B A (p:follow B))))
|
||||||
(stop-when (message (delete-resource e)))
|
(send! (delete-resource (permitted B A (p:follow B) ?)))))
|
||||||
(stop-when (asserted (delete-account owner)))
|
(on (retracted (permission-request $A $B (p:follow $maybe-A)))
|
||||||
(stop-when (asserted (delete-account member))))))
|
(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
|
(supervise
|
||||||
(actor #:name 'contacts:questions
|
(actor #:name 'contacts:questions
|
||||||
(stop-when-reloaded)
|
(stop-when-reloaded)
|
||||||
;; TODO: NOTE: When the `permission-request` vanishes (due to
|
;; TODO: CHECK THE FOLLOWING: When the `permission-request` vanishes (due to
|
||||||
;; satisfaction or rejection), this should remove the question
|
;; satisfaction or rejection), this should remove the question from all eligible
|
||||||
;; from all eligible answerers at once
|
;; answerers at once
|
||||||
(during (permission-request $who $grantee ($ p (p:follow _)))
|
(during (permission-request $who $grantee ($ p (p:follow _)))
|
||||||
(when (equal? who (p:follow-email p))
|
(when (equal? who (p:follow-email p))
|
||||||
;; `grantee` wants to follow `who`
|
;; `grantee` wants to follow `who`
|
||||||
(during (permitted who $grantor p #t)
|
(during (permitted who $grantor p #t)
|
||||||
;; `grantor` can make that decision
|
;; `grantor` can make that decision
|
||||||
(on-start
|
|
||||||
(define-values (title blurb)
|
(define-values (title blurb)
|
||||||
(if (equal? who grantor)
|
(if (equal? who grantor)
|
||||||
(values (format "Follow request from ~a" grantee)
|
(values (format "Contact request from ~a" grantee)
|
||||||
`(p "User " (b ,grantee) " wants to be able to invite you "
|
`(p "User " (b ,grantee) " wants to be able to invite you "
|
||||||
"to conversations and see when you are online."))
|
"to conversations and see when you are online."))
|
||||||
(values (format "Request from ~a to follow ~a" grantee who)
|
(values (format "Contact request from ~a to ~a" grantee who)
|
||||||
`(p "User " (b ,grantee) " wants to be able to invite "
|
`(p "User " (b ,grantee) " wants to be able to invite "
|
||||||
(b ,who) " to conversations and see when they are online."))))
|
(b ,who) " to conversations and see when they are online."))))
|
||||||
(define base-options
|
(define qid
|
||||||
(list (list "deny" "Reject")
|
(ask-question! #:title title #:blurb blurb #:target grantor #:class "q-follow"
|
||||||
(list "ignore" "Ignore")))
|
(option-question (list (list "allow" "Accept")
|
||||||
(match (ask-question #:title title #:blurb blurb #:target grantor #:class "q-follow"
|
(list "deny" "Reject")
|
||||||
(option-question
|
(list "ignore" "Ignore")))))
|
||||||
;; If who == grantor, then the grantor is directly
|
(stop-when (asserted (answer qid $v))
|
||||||
;; the person being followed, and should be offered
|
(match v
|
||||||
;; 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)))]
|
["allow" (send! (create-resource (grant who grantor grantee p #f)))]
|
||||||
["deny" (send! (delete-resource (permission-request who grantee p)))]
|
["deny" (send! (delete-resource (permission-request who grantee p)))]
|
||||||
["ignore" (void)])))))))
|
["ignore" (void)])))))))
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
#lang syndicate/actor
|
#lang syndicate/actor
|
||||||
|
|
||||||
(provide ask-question)
|
(provide ask-question!)
|
||||||
|
|
||||||
(require racket/port)
|
(require racket/port)
|
||||||
(require markdown)
|
(require markdown)
|
||||||
|
@ -21,7 +21,7 @@
|
||||||
(during (api (session target _) (answer qid $value))
|
(during (api (session target _) (answer qid $value))
|
||||||
(assert (answer qid value))))))
|
(assert (answer qid value))))))
|
||||||
|
|
||||||
(define (ask-question #:title title
|
(define (ask-question! #:title title
|
||||||
#:blurb blurb
|
#:blurb blurb
|
||||||
#:class [q-class "q-generic"]
|
#:class [q-class "q-generic"]
|
||||||
#:target target
|
#:target target
|
||||||
|
@ -36,8 +36,6 @@
|
||||||
(lambda ()
|
(lambda ()
|
||||||
(display-xexpr blurb)))
|
(display-xexpr blurb)))
|
||||||
question-type))
|
question-type))
|
||||||
(react/suspend (k)
|
|
||||||
(assert q)
|
(assert q)
|
||||||
(stop-when (asserted (answer qid $v))
|
qid)
|
||||||
(k v))))
|
|
||||||
|
|
||||||
|
|
|
@ -48,4 +48,4 @@
|
||||||
grantee
|
grantee
|
||||||
permission
|
permission
|
||||||
the-issuer
|
the-issuer
|
||||||
(if delegable? ", delegably," ""))))))
|
(if delegable? ", delegably" ""))))))
|
||||||
|
|
Loading…
Reference in New Issue