Conversations

This commit is contained in:
Tony Garnock-Jones 2016-12-07 19:19:32 +13:00
parent c7db9f2543
commit 7067c06961
14 changed files with 692 additions and 61 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 417 B

View File

@ -0,0 +1,85 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="4.5155554mm"
height="5.6444445mm"
viewBox="0 0 15.999999 20"
id="svg2"
version="1.1"
inkscape:version="0.91 r13725"
sodipodi:docname="speechbubble2-l.svg"
inkscape:export-filename="/home/tonyg/src/syndicate/examples/webchat/htdocs/speechbubble-l.png"
inkscape:export-xdpi="90"
inkscape:export-ydpi="90">
<defs
id="defs4" />
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="22.627417"
inkscape:cx="2.6426767"
inkscape:cy="9.8662922"
inkscape:document-units="px"
inkscape:current-layer="layer1"
showgrid="true"
inkscape:window-width="1908"
inkscape:window-height="1027"
inkscape:window-x="0"
inkscape:window-y="28"
inkscape:window-maximized="1"
inkscape:object-nodes="true"
inkscape:snap-bbox="true"
inkscape:snap-nodes="false"
inkscape:bbox-nodes="true"
fit-margin-top="0"
fit-margin-left="0"
fit-margin-right="0"
fit-margin-bottom="0">
<inkscape:grid
type="xygrid"
id="grid4140"
originx="0"
originy="-4.7244096e-06" />
</sodipodi:namedview>
<metadata
id="metadata7">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(0,-1032.3622)">
<path
style="fill:#ffffff;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="m 16,1032.3622 -16,10 16,10 z"
id="path4138"
inkscape:connector-curvature="0"
sodipodi:nodetypes="cccc" />
<path
style="fill:none;fill-opacity:1;fill-rule:evenodd;stroke:#d3d3d3;stroke-width:0.99999994px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="m 23.500001,1028.2643 -22.556417,14.0979 22.556417,14.098 z"
id="path4142"
inkscape:connector-curvature="0" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 500 B

View File

@ -0,0 +1,85 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="4.5155554mm"
height="5.6444445mm"
viewBox="0 0 15.999999 20"
id="svg2"
version="1.1"
inkscape:version="0.91 r13725"
sodipodi:docname="speechbubble2-r.svg"
inkscape:export-filename="/home/tonyg/src/syndicate/examples/webchat/htdocs/speechbubble-r.png"
inkscape:export-xdpi="90"
inkscape:export-ydpi="90">
<defs
id="defs4" />
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="22.627417"
inkscape:cx="2.6426767"
inkscape:cy="9.8662922"
inkscape:document-units="px"
inkscape:current-layer="layer1"
showgrid="true"
inkscape:window-width="1908"
inkscape:window-height="1027"
inkscape:window-x="0"
inkscape:window-y="28"
inkscape:window-maximized="1"
inkscape:object-nodes="true"
inkscape:snap-bbox="true"
inkscape:snap-nodes="false"
inkscape:bbox-nodes="true"
fit-margin-top="0"
fit-margin-left="0"
fit-margin-right="0"
fit-margin-bottom="0">
<inkscape:grid
type="xygrid"
id="grid4140"
originx="0"
originy="-4.7244096e-06" />
</sodipodi:namedview>
<metadata
id="metadata7">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(0,-1032.3622)">
<path
style="fill:#e8e8ff;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="m 0,1032.3622 16,10 -16,10 z"
id="path4138"
inkscape:connector-curvature="0"
sodipodi:nodetypes="cccc" />
<path
style="fill:none;fill-opacity:1;fill-rule:evenodd;stroke:#d3d3d3;stroke-width:0.99999994px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="m -7.5000015,1028.2643 22.5564175,14.0979 -22.5564175,14.098 z"
id="path4142"
inkscape:connector-curvature="0" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.7 KiB

View File

@ -8,6 +8,29 @@ img.avatar {
/* --------------------------------------------------------------------------- */
.main-container {
display: flex;
height: 100vh;
flex-direction: column;
}
#main-div {
flex: 1;
overflow: auto;
}
.column-container {
display: flex;
flex-direction: column;
}
.column-fill {
flex: 1;
overflow: auto;
}
/* --------------------------------------------------------------------------- */
.alert-count {
background: red;
color: white;
@ -73,3 +96,76 @@ img.avatar {
position: relative;
top: 0.5rem;
}
.blurb-box {
}
.float-right { float: right; }
.main-container footer {
padding-top: 1rem;
text-align: right;
}
/* --------------------------------------------------------------------------- */
.conversation-control-panel {
font-size: 2rem;
}
.post-backdrop {
overflow-y: scroll;
}
.post {
margin: 20px;
}
.post .post-body {
background: white;
border: solid #d3d3d3 1px;
border-radius: 1.5rem;
padding: 1rem;
margin: 0 0px;
}
.post p {
margin-bottom: 0;
}
.post.from-me .post-body {
background: #e8e8ff;
margin-left: 4rem;
margin-right: -1px;
}
.post.to-me .post-body {
margin-left: -1px;
margin-right: 4rem;
}
.post.from-me:after {
content: url('/speechbubble-r.png');
position: relative;
/* left: 100%; */
right: -100%;
top: -40px;
height: 0px;
width: 0px;
display: block;
}
.post.to-me:after {
content: url('/speechbubble-l.png');
position: relative;
left: -16px;
top: -40px;
height: 0px;
width: 0px;
display: block;
}
.post-body > img {
max-width: 100%;
max-height: 100%;
}

View File

@ -1,8 +1,8 @@
<div class="card conversation-card">
<div class="card-block {{#isSelected}}bg-primary text-white{{/isSelected}}">
<div class="card-title">{{title}}</div>
<div class="card-title">{{title}}{{^title}}<i>Untitled</i>{{/title}}</div>
{{#members}}
{{.}}
<img src="{{avatar}}">
{{/members}}
</div>
</div>

View File

@ -3,7 +3,7 @@
<img class="avatar" src="{{avatar}}">
<span class="alert-count hide-zero-count count{{questionCount}}">{{questionCount}}</span>
<span class="forcewrap">{{email}}</span></span>
<div class="dropdown-menu dropdown-menu-right" area-labelledby="nav-account">
<div class="dropdown-menu dropdown-menu-right" aria-labelledby="nav-account">
<button class="dropdown-item toggleInvisible"><i class="icon ion-checkmark dropdown-marginal" {{#locallyVisible}}hidden{{/locallyVisible}}></i>Be invisible</button>
<div class="dropdown-divider"></div>
<a class="dropdown-item" href="#/conversations">Conversations</a>

View File

@ -1,22 +1,153 @@
<div class="container">
<div class="row">
<div class="col-xs-4">
<div id="conversation-list">
<div class="modal fade" id="invitation-modal" tabindex="-1" role="dialog" aria-hidden="true">
<div class="modal-dialog" role="document">
<form class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
<h4 class="modal-title" id="myModalLabel">Invite User</h4>
</div>
<div class="modal-body">
<label for="invited-username">User to invite:</label>
<input type="email" class="form-control" id="invited-username" placeholder="username@example.com">
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">Cancel</button>
<button class="btn btn-primary btn-default send-invitation">Invite</button>
</div>
</form>
</div>
</div>
<div class="container h-100">
<div class="row h-100">
{{#showConversationList}}
<div class="col-md-4 h-100 column-container">
<div id="conversation-list" class="column-fill">
</div>
<div class="align-center">
<a class="big-icon text-gray-dark" href="#/new-chat"><i class="cursor-interactive icon ion-plus-circled"></i></a>
</div>
</div>
<div class="col-xs-8">
{{#selectedCid}}
{{selectedCid}}
{{/selectedCid}}
{{^selectedCid}}
{{/showConversationList}}
{{#showConversationMain}}
<div class="col-md-8 h-100 column-container">
{{#selected}}
<div class="column-fill post-backdrop">
{{#miniMode}}
<div class="conversation-control-panel bg-primary text-white px-1 mb-1">
<div class="float-right dropdown">
<i class="cursor-interactive icon ion-more" data-toggle="dropdown"></i>
<div class="dropdown-menu dropdown-menu-right">
{{#overflowMenuItems}}
{{#separator}}
<div class="dropdown-divider"></div>
{{/separator}}
{{^separator}}
<button class="dropdown-item {{action}}">{{label}}</button>
{{/separator}}
{{/overflowMenuItems}}
</div>
</div>
<i class="toggle-info-mode float-right icon ion-information-circled pr-1"></i>
{{#showConversationInfo}}
<i class="end-info-mode icon ion-arrow-left-c" style="padding-right: 0.5rem"></i>
{{/showConversationInfo}}
{{^showConversationInfo}}
<a class="text-white" style="padding-right: 0.5rem" href="#/conversations"><i class="icon ion-arrow-left-c"></i></a>
{{/showConversationInfo}}
<span>{{title}}{{^title}}<i class="text-muted">Untitled</i>{{/title}}</span>
</div>
{{/miniMode}}
{{#showConversationInfo}}
<div>
<div class="float-right dropdown mr-1">
<i class="cursor-interactive big-icon icon ion-more" data-toggle="dropdown"></i>
<div class="dropdown-menu dropdown-menu-right">
{{#overflowMenuItems}}
{{^hidden}}
{{#separator}}
<div class="dropdown-divider"></div>
{{/separator}}
{{^separator}}
<button class="dropdown-item {{action}}">{{label}}</button>
{{/separator}}
{{/hidden}}
{{/overflowMenuItems}}
</div>
</div>
{{#editingTitle}}
<h2 class="mr-1">
<form class="form-inline">
<input type="text" autocomplete="off" class="form-control" id="conversation-title" value="{{title}}">
<button class="form-control btn btn-primary btn-default" id="accept-conversation-title"><i class="icon ion-checkmark"></i></button>
<button class="form-control btn btn-secondary" id="cancel-edit-conversation-title"><i class="icon ion-close"></i></button>
</form>
</h2>
{{/editingTitle}}
{{^editingTitle}}
<form class="form-inline float-right">
<button class="form-control btn" id="edit-conversation-title"><i class="icon ion-edit"></i></button>
</form>
<h2 id="title-heading">{{title}}{{^title}}<i class="text-muted">Untitled</i>{{/title}}</h2>
{{/editingTitle}}
<hr>
{{#editingBlurb}}
<div class="mr-1">
<textarea rows="3" class="form-control" id="conversation-blurb">{{blurb}}</textarea>
<form class="form-inline align-right pb-1">
<button class="form-control btn btn-primary btn-default" id="accept-conversation-blurb"><i class="icon ion-checkmark"></i></button>
<button class="form-control btn btn-secondary" id="cancel-edit-conversation-blurb"><i class="icon ion-close"></i></button>
</form>
</div>
{{/editingBlurb}}
{{^editingBlurb}}
<div>
<form class="form-inline float-right">
<button class="form-control btn" id="edit-conversation-blurb"><i class="icon ion-edit"></i></button>
</form>
<div id="blurb" class="blurb-box">
{{#blurb}}
<p>{{blurb}}</p>
{{/blurb}}
{{^blurb}}
<p><i class="text-muted">Set a conversation topic here</i></p>
{{/blurb}}
</div>
</div>
{{/editingBlurb}}
</div>
{{/showConversationInfo}}
{{#showConversationPosts}}
<div class="posts"></div>
{{/showConversationPosts}}
</div>
{{#showConversationPosts}}
<form id="message-input-form" class="form-inline pt-1" style="display: flex;">
<input type="text" autocomplete="off" id="message-input" class="form-control" style="flex: 1">
<button id="send-message-button" class="form-control btn btn-primary" style="max-width: 3em"><i class="icon ion-paper-airplane"></i></button>
</form>
{{/showConversationPosts}}
{{/selected}}
{{^selected}}
<p class="align-center">
Select a conversation from the column to the left,
or <a href="#/new-chat">create a new conversation</a>.
</p>
{{/selectedCid}}
{{/selected}}
</div>
{{/showConversationMain}}
</div>
</div>
{{#miniMode}}
<style>
footer { display: none; }
#message-input-form { margin-bottom: 1rem; }
</style>
{{/miniMode}}

View File

@ -19,7 +19,7 @@
<form>
<div class="form-group">
<label for="conversation-title">Conversation Title</label>
<input type="text" class="form-control" id="conversation-title" placeholder="{{suggestedTitle}}">
<input type="text" autocomplete="off" class="form-control" id="conversation-title">
</div>
<div class="form-group">
<label for="conversation-blurb">Conversation Description</label>

View File

@ -0,0 +1,6 @@
<div id="post-{{postId}}" class="post {{postClass}}">
<div class="post-body {{contentType}}">
<p>{{content}}</p>
<p class="align-right"><small>{{author}}<br>{{date}}</small></p>
</div>
</div>

View File

@ -14,7 +14,7 @@
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);
assertion type post(id, timestamp, conversationId, author, contentType, content);
message type createResource(description) = "create-resource";
message type updateResource(description) = "update-resource";
@ -34,6 +34,12 @@
assertion type textQuestion(isMultiline) = "text-question";
assertion type acknowledgeQuestion() = "acknowledge-question";
//---------------------------------------------------------------------------
// Local assertions and messages
assertion type selectedCid(cid); // currently-selected conversation ID, or null
message type windowWidthChanged(newWidth);
//---------------------------------------------------------------------------
var brokerConnected = Syndicate.Broker.brokerConnected;
@ -104,6 +110,15 @@
field this.globallyVisible = false; // mirrors *other people's experience of us*
field this.locallyVisible = true;
field this.showRequestsFromOthers = false;
field this.miniMode = $(window).width() < 768;
window.addEventListener('resize', Syndicate.Dataspace.wrap(function () {
:: windowWidthChanged($(window).width());
}));
on message windowWidthChanged($newWidth) {
this.miniMode = newWidth < 768;
}
assert brokerConnection(brokerUrl);
@ -355,44 +370,228 @@
during Syndicate.UI.locationHash($locationHash) {
var m = locationHash.match(conversations_re);
if (m) {
var selectedCid = m[2] || null;
assert selectedCid(m[2] || false);
}
}
during inbound(uiTemplate("page-conversations.html", $mainEntry)) {
assert mainpage_c.html('div#main-div', Mustache.render(mainEntry, {
selectedCid: selectedCid
}));
}
during inbound(uiTemplate("page-conversations.html", $mainEntry)) {
during selectedCid(false) {
assert mainpage_c.html('div#main-div', Mustache.render(mainEntry, {
miniMode: this.miniMode,
showConversationList: true,
showConversationMain: !this.miniMode,
showConversationInfo: false,
showConversationPosts: false,
selected: false
}));
}
}
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);
// Move to the conversation index page when we leave a
// conversation (which also happens automatically when it is
// deleted)
during selectedCid($selected) {
on retracted inbound(inConversation(selected, sessionInfo.email)) {
:: Syndicate.UI.setLocationHash('/conversations');
}
}
during inbound(inConversation($cid, sessionInfo.email)) {
field this.members = Immutable.Set();
field this.title = '';
field this.creator = '';
field this.blurb = '';
field this.editingTitle = false;
field this.editingBlurb = false;
field this.membersJSON = [];
dataflow {
this.membersJSON = this.members.map(function (m) { return {
email: m,
avatar: avatar(m)
}; }).toArray();
}
on asserted inbound(inConversation(cid, $who)) {
this.members = this.members.add(who);
}
on retracted inbound(inConversation(cid, $who)) {
this.members = this.members.remove(who);
}
on asserted inbound(conversation(cid, $title, $creator, $blurb)) {
this.title = title;
this.creator = creator;
this.blurb = blurb;
}
during inbound(uiTemplate("page-conversations.html", $mainEntry)) {
during selectedCid($selected) {
if (selected === cid) {
field this.showInfoMode = false;
field this.latestPostTimestamp = 0;
field this.latestPostId = null;
assert mainpage_c.html('div#main-div', Mustache.render(mainEntry, {
miniMode: this.miniMode,
showConversationList: !this.miniMode,
showConversationMain: true,
showConversationInfo: !this.miniMode || this.showInfoMode,
showConversationPosts: !this.miniMode || !this.showInfoMode,
selected: selected,
title: this.title,
blurb: this.blurb,
members: this.membersJSON,
editingTitle: this.editingTitle,
editingBlurb: this.editingBlurb,
overflowMenuItems: [
{label: "Invite user...", action: "invite-to-conversation"},
{label: "Leave conversation", action: "leave-conversation"},
{separator: true,
hidden: sessionInfo.email !== this.creator},
{label: "Delete conversation", action: "delete-conversation",
hidden: sessionInfo.email !== this.creator}
]
}));
on message mainpage_c.event('#message-input', 'focus', $e) {
setTimeout(function () { e.target.scrollIntoView(false); }, 500);
}
on message mainpage_c.event('#send-message-button', 'click', _) {
var message = ($("#message-input").val() || '').trim();
if (message) {
:: outbound(createResource(post(random_hex_string(16),
+(new Date()),
cid,
sessionInfo.email,
"text/plain",
message)));
}
on retracted inbound(inConversation(cid, $who)) {
this.members = this.members.remove(who);
$("#message-input").val('').focus();
}
on message mainpage_c.event('.invite-to-conversation', 'click', _) {
$('#invitation-modal').modal({});
}
on message mainpage_c.event('.send-invitation', 'click', _) {
var invitee = $('#invited-username').val().trim();
if (invitee) {
:: outbound(createResource(invitation(cid, sessionInfo.email, invitee)));
$('#invited-username').val('');
$('#invitation-modal').modal('hide');
}
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);
}
on message mainpage_c.event('.leave-conversation', 'click', _) {
:: outbound(deleteResource(inConversation(cid, sessionInfo.email)));
}
on message mainpage_c.event('.delete-conversation', 'click', _) {
if (confirm("Delete this conversation?")) {
:: outbound(deleteResource(conversation(cid,
this.title,
this.creator,
this.blurb)));
}
}
on message mainpage_c.event('.toggle-info-mode', 'click', _) {
this.showInfoMode = !this.showInfoMode;
}
on message mainpage_c.event('.end-info-mode', 'click', _) {
this.showInfoMode = false;
}
on message mainpage_c.event('#edit-conversation-title', 'click', _) {
this.editingTitle = true;
}
on message mainpage_c.event('#title-heading', 'dblclick', _) {
this.editingTitle = true;
}
on message mainpage_c.event('#accept-conversation-title', 'click', _) {
this.title = $('#conversation-title').val();
:: outbound(updateResource(conversation(cid,
this.title,
this.creator,
this.blurb)));
this.editingTitle = false;
}
on message mainpage_c.event('#cancel-edit-conversation-title', 'click', _) {
this.editingTitle = false;
}
on message mainpage_c.event('#edit-conversation-blurb', 'click', _) {
this.editingBlurb = true;
}
on message mainpage_c.event('#blurb', 'dblclick', _) {
this.editingBlurb = true;
}
on message mainpage_c.event('#accept-conversation-blurb', 'click', _) {
this.blurb = $('#conversation-blurb').val();
:: outbound(updateResource(conversation(cid,
this.title,
this.creator,
this.blurb)));
this.editingBlurb = false;
}
on message mainpage_c.event('#cancel-edit-conversation-blurb', 'click', _) {
this.editingBlurb = false;
}
during inbound(post($pid, $timestamp, cid, $author, $contentType, $content)) {
if (timestamp > this.latestPostTimestamp) {
this.latestPostTimestamp = timestamp;
this.latestPostId = pid;
}
during mainpage_c.fragmentVersion($mainpageVersion) {
function cleanContentType(t) {
return t.replace('/', '-');
}
during inbound(
uiTemplate("post-entry-" + cleanContentType(contentType) + ".html", $entry))
{
var c = this.ui.context(mainpageVersion, 'post', timestamp, pid);
assert c.html('.posts', Mustache.render(entry, {
postId: pid,
date: new Date(timestamp).toString(),
postClass: (author === sessionInfo.email) ? "from-me" : "to-me",
author: author,
contentType: cleanContentType(contentType),
content: content
}));
on asserted c.fragmentVersion(_) {
if ((this.latestPostTimestamp === timestamp) &&
(this.latestPostId === pid)) {
$("#post-" + pid)[0].scrollIntoView(false);
}
}
}
}
}
}
during inbound(uiTemplate("conversation-index-entry.html", $indexEntry)) {
during mainpage_c.fragmentVersion($mainpageVersion) {
var c = this.ui.context(mainpageVersion, 'conversationIndex', cid);
assert c.html('#conversation-list', Mustache.render(indexEntry, {
isSelected: selected === cid,
selected: selected,
cid: cid,
title: this.title,
creator: this.creator,
members: this.membersJSON
}));
on message c.event('.card-block', 'click', _) {
if (selected === cid) {
:: Syndicate.UI.setLocationHash('/conversations');
} else {
:: Syndicate.UI.setLocationHash('/conversations/' + cid);
}
}
}
}
}
}
}
@ -401,17 +600,11 @@
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)) {
assert mainpage_c.html('div#main-div', Mustache.render(mainEntry, {
noInvitees: this.invitees.isEmpty(),
searchString: this.displayedSearchString,
suggestedTitle: this.suggestedTitle
searchString: this.displayedSearchString
}));
}
@ -420,12 +613,9 @@
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
on message mainpage_c.event('.create-conversation', 'click', _) {
if (!this.invitees.isEmpty()) {
var title = $('#conversation-title').val() || this.suggestedTitle;
var title = $('#conversation-title').val();
var blurb = $('#conversation-blurb').val();
var cid = random_hex_string(32);
:: outbound(createResource(conversation(cid, title, sessionInfo.email, blurb)));

View File

@ -11,6 +11,9 @@
(require "duplicate.rkt")
(require "util.rkt")
(define (user-in-conversation? who cid)
(immediate-query [query-value #f (in-conversation cid who) #t]))
(supervise
(actor #:name 'take-conversation-instructions
(stop-when-reloaded)
@ -18,6 +21,9 @@
(on (message (api (session $creator _) (create-resource (? conversation? $c))))
(when (equal? creator (conversation-creator c))
(send! (create-resource c))))
(on (message (api (session $updater _) (update-resource (? conversation? $c))))
(when (user-in-conversation? updater (conversation-id c))
(send! (update-resource c))))
(on (message (api (session $creator _) (delete-resource (? conversation? $c))))
(when (equal? creator (conversation-creator c))
(send! (delete-resource c))))
@ -35,7 +41,18 @@
(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))))))
(send! (delete-resource i))))
(on (message (api (session $who _) (create-resource (? post? $p))))
(when (and (user-in-conversation? who (post-conversation-id p))
(equal? who (post-author p)))
(send! (create-resource p))))
(on (message (api (session $who _) (update-resource (? post? $p))))
(when (equal? who (post-author p))
(send! (update-resource p))))
(on (message (api (session $who _) (delete-resource (? post? $p))))
(when (equal? who (post-author p))
(send! (delete-resource p))))))
(supervise
(actor #:name 'relay-conversation-state
@ -53,7 +70,7 @@
(assert (api (session who _) i)))
(during ($ c (conversation cid _ _ _))
(assert (api (session who _) c)))
(during ($ p (post _ _ cid _ _))
(during ($ p (post _ _ cid _ _ _))
(assert (api (session who _) p))))))
(supervise
@ -68,7 +85,10 @@
(on-stop (log-info "~v deleted" (c)))
(assert (c))
(stop-when-duplicate (list 'conversation cid))
(stop-when (message (delete-resource (conversation cid _ _ _))))))))
(stop-when (message (delete-resource (conversation cid _ _ _))))
(on (message (update-resource (conversation cid $newtitle _ $newblurb)))
(title newtitle)
(blurb newblurb))))))
(supervise
(actor #:name 'in-conversation-factory
@ -79,7 +99,8 @@
(on-stop (log-info "~s leaves conversation ~a" who cid))
(assert i)
(stop-when-duplicate i)
(stop-when (message (delete-resource i)))))))
(stop-when (message (delete-resource i)))
(stop-when (message (delete-resource (conversation cid _ _ _))))))))
(supervise
(actor #:name 'invitation-factory
@ -92,8 +113,24 @@
(assert i)
(stop-when-duplicate i)
(stop-when (message (delete-resource i)))
(stop-when (message (delete-resource (conversation cid _ _ _))))
(stop-when (asserted (in-conversation cid invitee)))))))
(supervise
(actor #:name 'post-factory
(stop-when-reloaded)
(on (message (create-resource
($ p0 (post $pid $timestamp $cid $author $content-type $content0))))
(actor #:name p0
(field [content content0])
(define/dataflow p (post pid timestamp cid author content-type (content)))
(assert (p))
(stop-when-duplicate (list 'post cid pid))
(stop-when (message (delete-resource (post pid _ cid _ _ _))))
(stop-when (message (delete-resource (conversation cid _ _ _))))
(on (message (update-resource (post pid _ cid _ _ $newcontent)))
(content newcontent))))))
(supervise
(actor #:name 'conversation:questions
(stop-when-reloaded)

View File

@ -60,7 +60,7 @@
(body (,@(if body-id
`((id ,body-id))
`()))
(div ((class "container"))
(div ((class "container main-container"))
(div ((class "header clearfix"))
(nav ((class "navbar bg-faded"))
;; (span ((id "nav-heading") (class "navbar-brand text-muted")) ,nav-heading)

View File

@ -151,6 +151,7 @@
(struct post (id ;; String
timestamp ;; Seconds
conversation-id ;; String
author ;; Principal
content-type ;; MimeTypeString
content ;; Any
) #:prefab) ;; ASSERTION