Conversations
This commit is contained in:
parent
c7db9f2543
commit
7067c06961
Binary file not shown.
After Width: | Height: | Size: 417 B |
|
@ -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 |
|
@ -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 |
|
@ -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%;
|
||||
}
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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">×</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}}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
|
@ -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)));
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -151,6 +151,7 @@
|
|||
(struct post (id ;; String
|
||||
timestamp ;; Seconds
|
||||
conversation-id ;; String
|
||||
author ;; Principal
|
||||
content-type ;; MimeTypeString
|
||||
content ;; Any
|
||||
) #:prefab) ;; ASSERTION
|
||||
|
|
Loading…
Reference in New Issue