Conversation management and UI
This commit is contained in:
parent
b87639b7a4
commit
132032b602
|
@ -56,3 +56,20 @@ img.avatar {
|
||||||
.big-icon {
|
.big-icon {
|
||||||
font-size: 1.75rem;
|
font-size: 1.75rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.invited-tick {
|
||||||
|
font-size: 2rem;
|
||||||
|
width: 48px;
|
||||||
|
height: 48px;
|
||||||
|
display: inline-block;
|
||||||
|
border-radius: 24px;
|
||||||
|
color: white;
|
||||||
|
background: darkgreen;
|
||||||
|
text-align: center;
|
||||||
|
line-height: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.invited-tick .icon {
|
||||||
|
position: relative;
|
||||||
|
top: 0.5rem;
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,8 @@
|
||||||
|
<div class="card conversation-card">
|
||||||
|
<div class="card-block {{#isSelected}}bg-primary text-white{{/isSelected}}">
|
||||||
|
<div class="card-title">{{title}}</div>
|
||||||
|
{{#members}}
|
||||||
|
{{.}}
|
||||||
|
{{/members}}
|
||||||
|
</div>
|
||||||
|
</div>
|
|
@ -0,0 +1,7 @@
|
||||||
|
<div class="col-xs-12 col-md-6 col-lg-4 p-1">
|
||||||
|
<div class="cursor-interactive contact-list-present-{{isPresent}} toggle-invitee-status p-2 {{#isInvited}}bg-primary text-white{{/isInvited}} rounded">
|
||||||
|
{{#isInvited}}<span class="invited-tick"><i class="icon ion-checkmark"></i></span>{{/isInvited}}
|
||||||
|
{{^isInvited}}<img class="avatar" src="{{avatar}}">{{/isInvited}}
|
||||||
|
<span class="forcewrap">{{email}}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
|
@ -8,10 +8,15 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-xs-8">
|
<div class="col-xs-8">
|
||||||
|
{{#selectedCid}}
|
||||||
|
{{selectedCid}}
|
||||||
|
{{/selectedCid}}
|
||||||
|
{{^selectedCid}}
|
||||||
<p class="align-center">
|
<p class="align-center">
|
||||||
Select a conversation from the column to the left,
|
Select a conversation from the column to the left,
|
||||||
or <a href="#/new-chat">create a new conversation</a>.
|
or <a href="#/new-chat">create a new conversation</a>.
|
||||||
</p>
|
</p>
|
||||||
|
{{/selectedCid}}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,4 +1,35 @@
|
||||||
<h2>New Conversation</h2>
|
<h2>New Conversation</h2>
|
||||||
|
<hr>
|
||||||
|
|
||||||
|
<h4>Select people to add</h4>
|
||||||
|
<div class="input-group">
|
||||||
|
<input class="form-control"
|
||||||
|
type="search"
|
||||||
|
id="search-contacts"
|
||||||
|
placeholder="Search contacts"
|
||||||
|
value="{{searchString}}">
|
||||||
|
<div class="input-group-addon"><i class="icon ion-search"></i></div>
|
||||||
|
</div>
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="contact-list" class="row"></div>
|
<div class="contact-list" class="row"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
<h4>Configure the conversation</h4>
|
||||||
|
<form>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="conversation-title">Conversation Title</label>
|
||||||
|
<input type="text" class="form-control" id="conversation-title" placeholder="{{suggestedTitle}}">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="conversation-blurb">Conversation Description</label>
|
||||||
|
<textarea class="form-control" id="conversation-blurb" rows="3"></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="submit" class="btn btn-success create-conversation {{#noInvitees}}disabled{{/noInvitees}}">Create conversation</button>
|
||||||
|
{{#noInvitees}}
|
||||||
|
<div class="alert alert-danger">
|
||||||
|
You must invite at least one person to the conversation.
|
||||||
|
</div>
|
||||||
|
{{/noInvitees}}
|
||||||
|
</form>
|
||||||
|
|
|
@ -1,5 +1,11 @@
|
||||||
<h2>Questions</h2>
|
<h2>Questions</h2>
|
||||||
<p class="show-only-zero-count count{{questionCount}}">There are no questions waiting for you to answer.</p>
|
<div class="show-only-zero-count count{{questionCount}}">
|
||||||
|
<p>There are no questions waiting for you to answer.</p>
|
||||||
|
<ul>
|
||||||
|
<li><a href="#/conversations">Go to conversation list.</a></li>
|
||||||
|
<li><a href="#/contacts">Go to contacts list.</a></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div id="question-container" class="row"></div>
|
<div id="question-container" class="row"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -11,6 +11,11 @@
|
||||||
assertion type grant(issuer, grantor, grantee, permission, isDelegable);
|
assertion type grant(issuer, grantor, grantee, permission, isDelegable);
|
||||||
assertion type permissionRequest(issuer, grantee, permission) = "permission-request";
|
assertion type permissionRequest(issuer, grantee, permission) = "permission-request";
|
||||||
|
|
||||||
|
assertion type conversation(id, title, creator, blurb);
|
||||||
|
assertion type invitation(conversationId, inviter, invitee);
|
||||||
|
assertion type inConversation(conversationId, member) = "in-conversation";
|
||||||
|
assertion type post(id, timestamp, conversationId, contentType, content);
|
||||||
|
|
||||||
message type createResource(description) = "create-resource";
|
message type createResource(description) = "create-resource";
|
||||||
message type updateResource(description) = "update-resource";
|
message type updateResource(description) = "update-resource";
|
||||||
message type deleteResource(description) = "delete-resource";
|
message type deleteResource(description) = "delete-resource";
|
||||||
|
@ -29,6 +34,8 @@
|
||||||
assertion type textQuestion(isMultiline) = "text-question";
|
assertion type textQuestion(isMultiline) = "text-question";
|
||||||
assertion type acknowledgeQuestion() = "acknowledge-question";
|
assertion type acknowledgeQuestion() = "acknowledge-question";
|
||||||
|
|
||||||
|
//---------------------------------------------------------------------------
|
||||||
|
|
||||||
var brokerConnected = Syndicate.Broker.brokerConnected;
|
var brokerConnected = Syndicate.Broker.brokerConnected;
|
||||||
var brokerConnection = Syndicate.Broker.brokerConnection;
|
var brokerConnection = Syndicate.Broker.brokerConnection;
|
||||||
var toBroker = Syndicate.Broker.toBroker;
|
var toBroker = Syndicate.Broker.toBroker;
|
||||||
|
@ -108,36 +115,33 @@
|
||||||
on stop { this.globallyVisible = false; }
|
on stop { this.globallyVisible = false; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
during inbound(question($qid, _, _, sessionInfo.email, _, _, _)) {
|
||||||
|
on start { this.questionCount++; }
|
||||||
|
on stop { this.questionCount--; }
|
||||||
|
}
|
||||||
|
|
||||||
|
during inbound(permissionRequest($issuer, sessionInfo.email, $permission)) {
|
||||||
|
on start { this.myRequestCount++; }
|
||||||
|
on stop { this.myRequestCount--; }
|
||||||
|
}
|
||||||
|
|
||||||
during inbound(uiTemplate("nav-account.html", $entry)) {
|
during inbound(uiTemplate("nav-account.html", $entry)) {
|
||||||
var c = this.ui.context('nav', 0, 'account');
|
var c = this.ui.context('nav', 0, 'account');
|
||||||
assert outbound(online()) when (this.locallyVisible);
|
assert outbound(online()) when (this.locallyVisible);
|
||||||
assert c.html('#nav-ul', Mustache.render(
|
assert c.html('#nav-ul', Mustache.render(entry, {
|
||||||
entry,
|
email: sessionInfo.email,
|
||||||
{
|
avatar: avatar(sessionInfo.email),
|
||||||
email: sessionInfo.email,
|
questionCount: this.questionCount,
|
||||||
avatar: avatar(sessionInfo.email),
|
myRequestCount: this.myRequestCount,
|
||||||
questionCount: this.questionCount,
|
otherRequestCount: this.otherRequestCount,
|
||||||
myRequestCount: this.myRequestCount,
|
globallyVisible: this.globallyVisible,
|
||||||
otherRequestCount: this.otherRequestCount,
|
locallyVisible: this.locallyVisible
|
||||||
globallyVisible: this.globallyVisible,
|
}));
|
||||||
locallyVisible: this.locallyVisible
|
|
||||||
}));
|
|
||||||
on message c.event('.toggleInvisible', 'click', _) {
|
on message c.event('.toggleInvisible', 'click', _) {
|
||||||
this.locallyVisible = !this.locallyVisible;
|
this.locallyVisible = !this.locallyVisible;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// assert mainpage_c.html('div#main-div', Mustache.render(
|
|
||||||
// mainpage,
|
|
||||||
// {
|
|
||||||
// questionCount: this.questionCount,
|
|
||||||
// myRequestCount: this.myRequestCount,
|
|
||||||
// otherRequestCount: this.otherRequestCount,
|
|
||||||
// globallyVisible: this.globallyVisible,
|
|
||||||
// showRequestsFromOthers: this.showRequestsFromOthers
|
|
||||||
// }));
|
|
||||||
|
|
||||||
|
|
||||||
during Syndicate.UI.locationHash('/contacts') {
|
during Syndicate.UI.locationHash('/contacts') {
|
||||||
during inbound(uiTemplate("page-contacts.html", $mainEntry)) {
|
during inbound(uiTemplate("page-contacts.html", $mainEntry)) {
|
||||||
assert mainpage_c.html('div#main-div', Mustache.render(mainEntry, {}));
|
assert mainpage_c.html('div#main-div', Mustache.render(mainEntry, {}));
|
||||||
|
@ -180,7 +184,7 @@
|
||||||
|
|
||||||
during mainpage_c.fragmentVersion($mainpageVersion) {
|
during mainpage_c.fragmentVersion($mainpageVersion) {
|
||||||
during inputValue('#add-contact-email', $rawContact) {
|
during inputValue('#add-contact-email', $rawContact) {
|
||||||
var contact = rawContact.trim();
|
var contact = rawContact && rawContact.trim();
|
||||||
if (contact) {
|
if (contact) {
|
||||||
on message mainpage_c.event('#add-contact', 'click', _) {
|
on message mainpage_c.event('#add-contact', 'click', _) {
|
||||||
:: outbound(createResource(grant(sessionInfo.email,
|
:: outbound(createResource(grant(sessionInfo.email,
|
||||||
|
@ -245,12 +249,9 @@
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
during inbound(uiTemplate("permission-request-out-GENERIC.html", $genericEntry)) {
|
during inbound(permissionRequest($issuer, sessionInfo.email, $permission)) {
|
||||||
during mainpage_c.fragmentVersion($mainpageVersion) {
|
during inbound(uiTemplate("permission-request-out-GENERIC.html", $genericEntry)) {
|
||||||
during inbound(permissionRequest($issuer, sessionInfo.email, $permission)) {
|
during mainpage_c.fragmentVersion($mainpageVersion) {
|
||||||
on start { this.myRequestCount++; }
|
|
||||||
on stop { this.myRequestCount--; }
|
|
||||||
|
|
||||||
var c = this.ui.context(mainpageVersion, 'my-permission-request', issuer, permission);
|
var c = this.ui.context(mainpageVersion, 'my-permission-request', issuer, permission);
|
||||||
field this.entry = genericEntry;
|
field this.entry = genericEntry;
|
||||||
assert c.html('#my-permission-requests', Mustache.render(this.entry, {
|
assert c.html('#my-permission-requests', Mustache.render(this.entry, {
|
||||||
|
@ -321,13 +322,9 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
during mainpage_c.fragmentVersion($mainpageVersion) {
|
during inbound(question($qid, $timestamp, $klass, sessionInfo.email, $title, $blurb, $qt))
|
||||||
during
|
{
|
||||||
inbound(question($qid, $timestamp, $klass, sessionInfo.email, $title, $blurb, $qt))
|
during mainpage_c.fragmentVersion($mainpageVersion) {
|
||||||
{
|
|
||||||
on start { this.questionCount++; }
|
|
||||||
on stop { this.questionCount--; }
|
|
||||||
|
|
||||||
var c = this.ui.context(mainpageVersion, 'question', timestamp, qid);
|
var c = this.ui.context(mainpageVersion, 'question', timestamp, qid);
|
||||||
|
|
||||||
switch (qt.meta.label) {
|
switch (qt.meta.label) {
|
||||||
|
@ -354,15 +351,124 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
during Syndicate.UI.locationHash('/conversations') {
|
var conversations_re = /^\/conversations(\/(.*))?/;
|
||||||
during inbound(uiTemplate("page-conversations.html", $mainEntry)) {
|
during Syndicate.UI.locationHash($locationHash) {
|
||||||
assert mainpage_c.html('div#main-div', Mustache.render(mainEntry, {}));
|
var m = locationHash.match(conversations_re);
|
||||||
|
if (m) {
|
||||||
|
var selectedCid = m[2] || null;
|
||||||
|
|
||||||
|
during inbound(uiTemplate("page-conversations.html", $mainEntry)) {
|
||||||
|
assert mainpage_c.html('div#main-div', Mustache.render(mainEntry, {
|
||||||
|
selectedCid: selectedCid
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
during inbound(uiTemplate("conversation-index-entry.html", $indexEntry)) {
|
||||||
|
during mainpage_c.fragmentVersion($mainpageVersion) {
|
||||||
|
during inbound(inConversation($cid, sessionInfo.email)) {
|
||||||
|
field this.members = Immutable.Set();
|
||||||
|
on asserted inbound(inConversation(cid, $who)) {
|
||||||
|
this.members = this.members.add(who);
|
||||||
|
}
|
||||||
|
on retracted inbound(inConversation(cid, $who)) {
|
||||||
|
this.members = this.members.remove(who);
|
||||||
|
}
|
||||||
|
during inbound(conversation(cid, $title, $creator, $blurb)) {
|
||||||
|
var c = this.ui.context(mainpageVersion, 'conversationIndex', cid);
|
||||||
|
assert c.html('#conversation-list', Mustache.render(indexEntry, {
|
||||||
|
isSelected: selectedCid === cid,
|
||||||
|
selectedCid: selectedCid,
|
||||||
|
cid: cid,
|
||||||
|
title: title,
|
||||||
|
creator: creator,
|
||||||
|
members: this.members.toArray()
|
||||||
|
}));
|
||||||
|
on message c.event('.card-block', 'click', _) {
|
||||||
|
if (selectedCid === cid) {
|
||||||
|
:: Syndicate.UI.setLocationHash('/conversations');
|
||||||
|
} else {
|
||||||
|
:: Syndicate.UI.setLocationHash('/conversations/' + cid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
during Syndicate.UI.locationHash('/new-chat') {
|
during Syndicate.UI.locationHash('/new-chat') {
|
||||||
|
field this.invitees = Immutable.Set();
|
||||||
|
field this.searchString = '';
|
||||||
|
field this.displayedSearchString = ''; // avoid resetting HTML every keystroke. YUCK
|
||||||
|
field this.suggestedTitle = '';
|
||||||
|
|
||||||
|
dataflow {
|
||||||
|
this.suggestedTitle = this.invitees.toArray().join(', ');
|
||||||
|
}
|
||||||
|
|
||||||
during inbound(uiTemplate("page-new-chat.html", $mainEntry)) {
|
during inbound(uiTemplate("page-new-chat.html", $mainEntry)) {
|
||||||
assert mainpage_c.html('div#main-div', Mustache.render(mainEntry, {}));
|
assert mainpage_c.html('div#main-div', Mustache.render(mainEntry, {
|
||||||
|
noInvitees: this.invitees.isEmpty(),
|
||||||
|
searchString: this.displayedSearchString,
|
||||||
|
suggestedTitle: this.suggestedTitle
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
during mainpage_c.fragmentVersion($mainpageVersion) {
|
||||||
|
on message Syndicate.UI.globalEvent('#search-contacts', 'keyup', $e) {
|
||||||
|
this.searchString = e.target.value.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
on message Syndicate.UI.globalEvent('.create-conversation', 'click', _) {
|
||||||
|
// TODO: ^ Would like to use
|
||||||
|
// mainpage_c.event('.create-conversation', 'click', _)
|
||||||
|
// here, but the DOM nodes aren't created in time, it seems
|
||||||
|
if (!this.invitees.isEmpty()) {
|
||||||
|
var title = $('#conversation-title').val() || this.suggestedTitle;
|
||||||
|
var blurb = $('#conversation-blurb').val();
|
||||||
|
var cid = random_hex_string(32);
|
||||||
|
:: outbound(createResource(conversation(cid, title, sessionInfo.email, blurb)));
|
||||||
|
:: outbound(createResource(inConversation(cid, sessionInfo.email)));
|
||||||
|
this.invitees.forEach(function (invitee) {
|
||||||
|
:: outbound(createResource(invitation(cid, sessionInfo.email, invitee)));
|
||||||
|
});
|
||||||
|
:: Syndicate.UI.setLocationHash('/conversations/' + cid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
during inbound(uiTemplate("invitee-entry.html", $entry)) {
|
||||||
|
during mainpage_c.fragmentVersion($mainpageVersion) {
|
||||||
|
during inbound(contactListEntry(sessionInfo.email, $contact)) {
|
||||||
|
field this.isPresent = false;
|
||||||
|
field this.isInvited = false;
|
||||||
|
dataflow {
|
||||||
|
this.isInvited = this.invitees.contains(contact);
|
||||||
|
}
|
||||||
|
during inbound(present(contact)) {
|
||||||
|
on start { this.isPresent = true; }
|
||||||
|
on stop { this.isPresent = false; }
|
||||||
|
}
|
||||||
|
var c = this.ui.context(mainpageVersion, 'all-contacts', contact);
|
||||||
|
assert c.html('.contact-list', Mustache.render(entry, {
|
||||||
|
email: contact,
|
||||||
|
avatar: avatar(contact),
|
||||||
|
isPresent: this.isPresent,
|
||||||
|
isInvited: this.isInvited
|
||||||
|
})) when (this.isInvited ||
|
||||||
|
!this.searchString ||
|
||||||
|
contact.indexOf(this.searchString) !== -1);
|
||||||
|
on message c.event('.toggle-invitee-status', 'click', _) {
|
||||||
|
if (this.invitees.contains(contact)) {
|
||||||
|
this.invitees = this.invitees.remove(contact);
|
||||||
|
} else {
|
||||||
|
this.invitees = this.invitees.add(contact);
|
||||||
|
}
|
||||||
|
this.displayedSearchString = this.searchString;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -381,7 +487,7 @@ assertion type inputValue(selector, value);
|
||||||
|
|
||||||
function spawnInputChangeMonitor() {
|
function spawnInputChangeMonitor() {
|
||||||
function valOf(e) {
|
function valOf(e) {
|
||||||
return e.type === 'checkbox' ? e.checked : e.value;
|
return e ? (e.type === 'checkbox' ? e.checked : e.value) : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
actor {
|
actor {
|
||||||
|
@ -394,3 +500,16 @@ function spawnInputChangeMonitor() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
///////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
function random_hex_string(halfLength) {
|
||||||
|
var bs = new Uint8Array(halfLength);
|
||||||
|
var encoded = [];
|
||||||
|
crypto.getRandomValues(bs);
|
||||||
|
for (var i = 0; i < bs.length; i++) {
|
||||||
|
encoded.push("0123456789abcdef"[(bs[i] >> 4) & 15]);
|
||||||
|
encoded.push("0123456789abcdef"[bs[i] & 15]);
|
||||||
|
}
|
||||||
|
return encoded.join('');
|
||||||
|
}
|
||||||
|
|
|
@ -8,6 +8,7 @@
|
||||||
(require "protocol.rkt")
|
(require "protocol.rkt")
|
||||||
(require "duplicate.rkt")
|
(require "duplicate.rkt")
|
||||||
|
|
||||||
|
;; TODO: Move to protocol.rkt
|
||||||
(struct online () #:prefab)
|
(struct online () #:prefab)
|
||||||
(struct present (email) #:prefab)
|
(struct present (email) #:prefab)
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,127 @@
|
||||||
#lang syndicate/actor
|
#lang syndicate/actor
|
||||||
|
|
||||||
|
(require racket/port)
|
||||||
|
(require markdown)
|
||||||
|
|
||||||
(require/activate syndicate/reload)
|
(require/activate syndicate/reload)
|
||||||
(require/activate syndicate/supervise)
|
(require/activate syndicate/supervise)
|
||||||
(require/activate "trust.rkt")
|
(require/activate "trust.rkt")
|
||||||
|
|
||||||
(require "protocol.rkt")
|
(require "protocol.rkt")
|
||||||
|
(require "duplicate.rkt")
|
||||||
|
(require "util.rkt")
|
||||||
|
|
||||||
|
(supervise
|
||||||
|
(actor #:name 'take-conversation-instructions
|
||||||
|
(stop-when-reloaded)
|
||||||
|
|
||||||
|
(on (message (api (session $creator _) (create-resource (? conversation? $c))))
|
||||||
|
(when (equal? creator (conversation-creator c))
|
||||||
|
(send! (create-resource c))))
|
||||||
|
(on (message (api (session $creator _) (delete-resource (? conversation? $c))))
|
||||||
|
(when (equal? creator (conversation-creator c))
|
||||||
|
(send! (delete-resource c))))
|
||||||
|
|
||||||
|
(on (message (api (session $joiner _) (create-resource (? in-conversation? $i))))
|
||||||
|
(when (equal? joiner (in-conversation-member i))
|
||||||
|
(send! (create-resource i))))
|
||||||
|
(on (message (api (session $leaver _) (delete-resource (? in-conversation? $i))))
|
||||||
|
(when (equal? leaver (in-conversation-member i))
|
||||||
|
(send! (delete-resource i))))
|
||||||
|
|
||||||
|
(on (message (api (session $inviter _) (create-resource (? invitation? $i))))
|
||||||
|
(when (equal? inviter (invitation-inviter i))
|
||||||
|
(send! (create-resource i))))
|
||||||
|
(on (message (api (session $who _) (delete-resource (? invitation? $i))))
|
||||||
|
(when (or (equal? who (invitation-inviter i))
|
||||||
|
(equal? who (invitation-invitee i)))
|
||||||
|
(send! (delete-resource i))))))
|
||||||
|
|
||||||
|
(supervise
|
||||||
|
(actor #:name 'relay-conversation-state
|
||||||
|
(stop-when-reloaded)
|
||||||
|
|
||||||
|
(during (invitation $cid $inviter $invitee)
|
||||||
|
(assert (api (session invitee _) (invitation cid inviter invitee)))
|
||||||
|
(during ($ c (conversation cid _ _ _))
|
||||||
|
(assert (api (session invitee _) c))))
|
||||||
|
|
||||||
|
(during (in-conversation $cid $who)
|
||||||
|
(during ($ i (invitation cid _ _))
|
||||||
|
(assert (api (session who _) i)))
|
||||||
|
(during ($ i (in-conversation cid _))
|
||||||
|
(assert (api (session who _) i)))
|
||||||
|
(during ($ c (conversation cid _ _ _))
|
||||||
|
(assert (api (session who _) c)))
|
||||||
|
(during ($ p (post _ _ cid _ _))
|
||||||
|
(assert (api (session who _) p))))))
|
||||||
|
|
||||||
|
(supervise
|
||||||
|
(actor #:name 'conversation-factory
|
||||||
|
(stop-when-reloaded)
|
||||||
|
(on (message (create-resource ($ c0 (conversation $cid $title0 $creator $blurb0))))
|
||||||
|
(actor #:name c0
|
||||||
|
(field [title title0]
|
||||||
|
[blurb blurb0])
|
||||||
|
(define/dataflow c (conversation cid (title) creator (blurb)))
|
||||||
|
(on-start (log-info "~v created" (c)))
|
||||||
|
(on-stop (log-info "~v deleted" (c)))
|
||||||
|
(assert (c))
|
||||||
|
(stop-when-duplicate (list 'conversation cid))
|
||||||
|
(stop-when (message (delete-resource (conversation cid _ _ _))))))))
|
||||||
|
|
||||||
|
(supervise
|
||||||
|
(actor #:name 'in-conversation-factory
|
||||||
|
(stop-when-reloaded)
|
||||||
|
(on (message (create-resource ($ i (in-conversation $cid $who))))
|
||||||
|
(actor #:name i
|
||||||
|
(on-start (log-info "~s joins conversation ~a" who cid))
|
||||||
|
(on-stop (log-info "~s leaves conversation ~a" who cid))
|
||||||
|
(assert i)
|
||||||
|
(stop-when-duplicate i)
|
||||||
|
(stop-when (message (delete-resource i)))))))
|
||||||
|
|
||||||
|
(supervise
|
||||||
|
(actor #:name 'invitation-factory
|
||||||
|
(stop-when-reloaded)
|
||||||
|
(on (message (create-resource ($ i (invitation $cid $inviter $invitee))))
|
||||||
|
(actor #:name i
|
||||||
|
(on-start (log-info "~s invited to conversation ~a by ~s" invitee cid inviter))
|
||||||
|
(on-stop (log-info "invitation of ~s to conversation ~a by ~s retracted"
|
||||||
|
invitee cid inviter))
|
||||||
|
(assert i)
|
||||||
|
(stop-when-duplicate i)
|
||||||
|
(stop-when (message (delete-resource i)))
|
||||||
|
(stop-when (asserted (in-conversation cid invitee)))))))
|
||||||
|
|
||||||
|
(supervise
|
||||||
|
(actor #:name 'conversation:questions
|
||||||
|
(stop-when-reloaded)
|
||||||
|
;; TODO: CHECK THE FOLLOWING: When the `invitation` vanishes (due to satisfaction
|
||||||
|
;; or rejection), this should remove the question from all eligible answerers at once
|
||||||
|
(during (invitation $cid $inviter $invitee)
|
||||||
|
;; `inviter` has invited `invitee` to conversation `cid`...
|
||||||
|
(define qid (random-hex-string 32)) ;; Fix qid and timestamp even as title/creator vary
|
||||||
|
(define timestamp (current-seconds))
|
||||||
|
(during (conversation cid $title $creator _)
|
||||||
|
;; ...and it exists...
|
||||||
|
(during (permitted invitee inviter (p:follow invitee) _)
|
||||||
|
;; ...and they are permitted to do so
|
||||||
|
(assert (question qid timestamp "q-invitation" invitee
|
||||||
|
(format "Invitation from ~a" inviter)
|
||||||
|
(with-output-to-string
|
||||||
|
(lambda ()
|
||||||
|
(display-xexpr
|
||||||
|
`(div
|
||||||
|
(p "You have been invited by " (b ,inviter)
|
||||||
|
" to join a conversation started by " (b ,creator) ".")
|
||||||
|
(p "The conversation is titled "
|
||||||
|
(i "\"" ,title "\"") ".")))))
|
||||||
|
(option-question (list (list "join" "Join conversation")
|
||||||
|
(list "decline" "Decline invitation")))))
|
||||||
|
(stop-when (asserted (answer qid $v))
|
||||||
|
(match v
|
||||||
|
["join"
|
||||||
|
(send! (create-resource (in-conversation cid invitee)))]
|
||||||
|
["decline"
|
||||||
|
(send! (delete-resource (invitation cid inviter invitee)))])))))))
|
||||||
|
|
|
@ -46,6 +46,9 @@
|
||||||
(script ((src "https://cdnjs.cloudflare.com/ajax/libs/blueimp-md5/2.6.0/js/md5.min.js")
|
(script ((src "https://cdnjs.cloudflare.com/ajax/libs/blueimp-md5/2.6.0/js/md5.min.js")
|
||||||
(integrity "sha256-I0CACboBQ1ky299/4LVi2tzEhCOfx1e7LbCcFhn7M8Y=")
|
(integrity "sha256-I0CACboBQ1ky299/4LVi2tzEhCOfx1e7LbCcFhn7M8Y=")
|
||||||
(crossorigin "anonymous")))
|
(crossorigin "anonymous")))
|
||||||
|
(script ((src "https://cdnjs.cloudflare.com/ajax/libs/immutable/3.8.1/immutable.min.js")
|
||||||
|
(integrity "sha256-13JFytp+tj8jsxr6GQOVLCgcYfMUo2Paw4jVrnXLUPE=")
|
||||||
|
(crossorigin "anonymous")))
|
||||||
(script ((src "/linkify.min.js")))
|
(script ((src "/linkify.min.js")))
|
||||||
(script ((src "/linkify-string.min.js")))
|
(script ((src "/linkify-string.min.js")))
|
||||||
;; (script ((src "/syndicatecompiler.min.js")))
|
;; (script ((src "/syndicatecompiler.min.js")))
|
||||||
|
|
|
@ -141,8 +141,8 @@
|
||||||
;; (conversation String String Principal Markup Boolean
|
;; (conversation String String Principal Markup Boolean
|
||||||
(struct conversation (id title creator blurb) #:prefab) ;; ASSERTION
|
(struct conversation (id title creator blurb) #:prefab) ;; ASSERTION
|
||||||
|
|
||||||
;; (invitation String Principal)
|
;; (invitation String Principal Principal)
|
||||||
(struct invitation (conversation-id invitee) #:prefab) ;; ASSERTION
|
(struct invitation (conversation-id inviter invitee) #:prefab) ;; ASSERTION
|
||||||
|
|
||||||
;; (in-conversation String Principal)
|
;; (in-conversation String Principal)
|
||||||
;; Records conversation membership.
|
;; Records conversation membership.
|
||||||
|
|
Loading…
Reference in New Issue