=200&&o<300||304===o||1223===o;if(void 0!==e.onbeforeunload&&s.Event.detach(e,"beforeunload",c),n.onreadystatechange=function(){},n=null,!u)return r._handleError(t);try{i=JSON.parse(a)}catch(t){}i?r._receive(i):r._handleError(t)}},n.send(this.encode(t)),n}}),{isUsable:function(t,e,n,i){var s="ReactNative"===navigator.product||r.isSameOrigin(e);n.call(i,s)}});t.exports=u}).call(e,function(){return this}())},function(t,e,n){(function(e){"use strict";var i=n(21),r=n(38),s=n(23),o=n(22),a=n(19),c=n(35),u=o(i(c,{encode:function(t){return"message="+encodeURIComponent(a(t))},request:function(t){var n,i=e.XDomainRequest?XDomainRequest:XMLHttpRequest,r=new i,o=++u._id,a=this._dispatcher.headers,c=this;if(r.open("POST",s.stringify(this.endpoint),!0),r.setRequestHeader){r.setRequestHeader("Pragma","no-cache");for(n in a)a.hasOwnProperty(n)&&r.setRequestHeader(n,a[n])}var h=function(){return!!r&&(u._pending.remove(o),r.onload=r.onerror=r.ontimeout=r.onprogress=null,void(r=null))};return r.onload=function(){var e;try{e=JSON.parse(r.responseText)}catch(t){}h(),e?c._receive(e):c._handleError(t)},r.onerror=r.ontimeout=function(){h(),c._handleError(t)},r.onprogress=function(){},i===e.XDomainRequest&&u._pending.add({id:o,xhr:r}),r.send(this.encode(t)),r}}),{_id:0,_pending:new r,isUsable:function(t,n,i,r){if(s.isSameOrigin(n))return i.call(r,!1);if(e.XDomainRequest)return i.call(r,n.protocol===location.protocol);if(e.XMLHttpRequest){var o=new XMLHttpRequest;return i.call(r,void 0!==o.withCredentials)}return i.call(r,!1)}});t.exports=u}).call(e,function(){return this}())},function(t,e,n){(function(e){"use strict";var i=n(21),r=n(23),s=n(39),o=n(22),a=n(19),c=n(35),u=o(i(c,{encode:function(t){var e=s(this.endpoint);return e.query.message=a(t),e.query.jsonp="__jsonp"+u._cbCount+"__",r.stringify(e)},request:function(t){var n=document.getElementsByTagName("head")[0],i=document.createElement("script"),o=u.getCallbackName(),c=s(this.endpoint),h=this;c.query.message=a(t),c.query.jsonp=o;var l=function(){if(!e[o])return!1;e[o]=void 0;try{delete e[o]}catch(t){}i.parentNode.removeChild(i)};return e[o]=function(t){l(),h._receive(t)},i.type="text/javascript",i.src=r.stringify(c),n.appendChild(i),i.onerror=function(){l(),h._handleError(t)},{abort:l}}}),{_cbCount:0,getCallbackName:function(){return this._cbCount+=1,"__jsonp"+this._cbCount+"__"},isUsable:function(t,e,n,i){n.call(i,!0)}});t.exports=u}).call(e,function(){return this}())},function(t,e,n){"use strict";var i=n(22),r=function(t,e){this.message=t,this.options=e,this.attempts=0};i(r.prototype,{getTimeout:function(){return this.options.timeout},getInterval:function(){return this.options.interval},isDeliverable:function(){var t=this.options.attempts,e=this.attempts,n=this.options.deadline,i=(new Date).getTime();return!(void 0!==t&&e>=t)&&!(void 0!==n&&i>n)},send:function(){this.attempts+=1},succeed:function(){},fail:function(){},abort:function(){}}),t.exports=r},function(t,e,n){"use strict";var i=n(21),r=n(31),s=i({initialize:function(t,e,n){this.code=t,this.params=Array.prototype.slice.call(e),this.message=n},toString:function(){return this.code+":"+this.params.join(",")+":"+this.message}});s.parse=function(t){if(t=t||"",!r.ERROR.test(t))return new s(null,[],t);var e=t.split(":"),n=parseInt(e[0]),i=e[1].split(","),t=e[2];return new s(n,i,t)};var o={versionMismatch:[300,"Version mismatch"],conntypeMismatch:[301,"Connection types not supported"],extMismatch:[302,"Extension mismatch"],badRequest:[400,"Bad request"],clientUnknown:[401,"Unknown client"],parameterMissing:[402,"Missing required parameter"],channelForbidden:[403,"Forbidden channel"],channelUnknown:[404,"Unknown channel"],channelInvalid:[405,"Invalid channel"],extUnknown:[406,"Unknown extension"],publishFailed:[407,"Failed to publish"],serverError:[500,"Internal server error"]};for(var a in o)(function(t){s[t]=function(){return new s(o[t][0],arguments,o[t][1]).toString()}})(a);t.exports=s},function(t,e,n){"use strict";var i=n(22),r=n(18),s={addExtension:function(t){this._extensions=this._extensions||[],this._extensions.push(t),t.added&&t.added(this)},removeExtension:function(t){if(this._extensions)for(var e=this._extensions.length;e--;)this._extensions[e]===t&&(this._extensions.splice(e,1),t.removed&&t.removed(this))},pipeThroughExtensions:function(t,e,n,i,r){if(this.debug("Passing through ? extensions: ?",t,e),!this._extensions)return i.call(r,e);var s=this._extensions.slice(),o=function(e){if(!e)return i.call(r,e);var a=s.shift();if(!a)return i.call(r,e);var c=a[t];return c?void(c.length>=3?a[t](e,n,o):a[t](e,o)):o(e)};o(e)}};i(s,r),t.exports=s},function(t,e,n){"use strict";var i=n(21),r=n(27);t.exports=i(r)},function(t,e,n){"use strict";var i=n(21),r=n(22),s=n(27),o=i({initialize:function(t,e,n,i){this._client=t,this._channels=e,this._callback=n,this._context=i,this._cancelled=!1},withChannel:function(t,e){return this._withChannel=[t,e],this},apply:function(t,e){var n=e[0];this._callback&&this._callback.call(this._context,n.data),this._withChannel&&this._withChannel[0].call(this._withChannel[1],n.channel,n.data)},cancel:function(){this._cancelled||(this._client.unsubscribe(this._channels,this),this._cancelled=!0)},unsubscribe:function(){this.cancel()}});r(o.prototype,s),t.exports=o}])});
--------------------------------------------------------------------------------
/src/FsTweet.Web/assets/js/profile.js:
--------------------------------------------------------------------------------
1 | $(function(){
2 | let client = stream.connect(fsTweet.stream.apiKey, null, fsTweet.stream.appId);
3 | let userFeed = client.feed("user", fsTweet.user.id, fsTweet.user.feedToken);
4 |
5 | userFeed.get({
6 | limit: 25
7 | }).then(function(body) {
8 | $(body.results.reverse()).each(function(index, tweet){
9 | renderTweet($("#tweets"), tweet);
10 | });
11 | })
12 | });
--------------------------------------------------------------------------------
/src/FsTweet.Web/assets/js/social.js:
--------------------------------------------------------------------------------
1 | $(function(){
2 |
3 | $("#follow").on('click', function(){
4 | var $this = $(this);
5 | var userId = $this.data('user-id');
6 | $.ajax({
7 | url : "/follow",
8 | type: "post",
9 | data: JSON.stringify({userId : userId}),
10 | contentType: "application/json"
11 | }).done(function(){
12 | $this.attr('id', 'unfollow');
13 | $this.html('Following');
14 | $this.addClass('disabled');
15 | }).fail(function(jqXHR, textStatus, errorThrown) {
16 | console.log({jqXHR : jqXHR, textStatus : textStatus, errorThrown: errorThrown})
17 | alert("something went wrong!")
18 | });
19 | });
20 |
21 | var usersTemplate = `
22 | {{#users}}
23 |
26 | {{/users}}`;
27 |
28 |
29 | function renderUsers(data, $body, $count) {
30 | var htmlOutput = Mustache.render(usersTemplate, data);
31 | $body.html(htmlOutput);
32 | $count.html(data.users.length);
33 | }
34 |
35 |
36 | (function loadFollowers () {
37 | var url = "/" + fsTweet.user.id + "/followers"
38 | $.getJSON(url, function(data){
39 | renderUsers(data, $("#followers"), $("#followersCount"))
40 | })
41 | })();
42 |
43 | (function loadFollowingUsers() {
44 | var url = "/" + fsTweet.user.id + "/following"
45 | $.getJSON(url, function(data){
46 | renderUsers(data, $("#following"), $("#followingCount"))
47 | })
48 | })();
49 |
50 | });
--------------------------------------------------------------------------------
/src/FsTweet.Web/assets/js/tweet.js:
--------------------------------------------------------------------------------
1 | $(function(){
2 | var timeAgo = function () {
3 | return function(val, render) {
4 | return moment(render(val) + "Z").fromNow()
5 | };
6 | }
7 |
8 | var template = `
9 |
13 | `
14 |
15 | window.renderTweet = function($parent, tweet) {
16 | var htmlOutput = Mustache.render(template, {
17 | "tweet" : tweet,
18 | "timeAgo" : timeAgo
19 | });
20 | $parent.prepend(htmlOutput);
21 | };
22 |
23 | $body = $("body");
24 |
25 | $(document).on({
26 | ajaxStart: function() { $body.addClass("loading"); },
27 | ajaxStop: function() { $body.removeClass("loading"); }
28 | });
29 |
30 | });
--------------------------------------------------------------------------------
/src/FsTweet.Web/assets/js/wall.js:
--------------------------------------------------------------------------------
1 | $(function(){
2 | $("#tweetForm").submit(function(event){
3 | var $this = $(this);
4 | var $tweet = $("#tweet");
5 | event.preventDefault();
6 | $this.prop('disabled', true);
7 | $.ajax({
8 | url : "/tweets",
9 | type: "post",
10 | data: JSON.stringify({post : $tweet.val()}),
11 | contentType: "application/json"
12 | }).done(function(){
13 | $this.prop('disabled', false);
14 | $tweet.val('');
15 | }).fail(function(jqXHR, textStatus, errorThrown) {
16 | console.log({jqXHR : jqXHR, textStatus : textStatus, errorThrown: errorThrown})
17 | alert("something went wrong!")
18 | });
19 |
20 | });
21 |
22 | $("textarea[maxlength]").on("propertychange input", function() {
23 | if (this.value.length > this.maxlength) {
24 | this.value = this.value.substring(0, this.maxlength);
25 | }
26 | });
27 |
28 | let client = stream.connect(fsTweet.stream.apiKey, null, fsTweet.stream.appId);
29 | let userFeed = client.feed("user", fsTweet.user.id, fsTweet.user.feedToken);
30 | let timelineFeed = client.feed("timeline", fsTweet.user.id, fsTweet.user.timelineToken);
31 |
32 | userFeed.subscribe(function(data){
33 | renderTweet($("#wall"),data.new[0]);
34 | });
35 | timelineFeed.subscribe(function(data){
36 | renderTweet($("#wall"),data.new[0]);
37 | });
38 |
39 | timelineFeed.get({
40 | limit: 25
41 | }).then(function(body) {
42 | var timelineTweets = body.results
43 | userFeed.get({
44 | limit : 25
45 | }).then(function(body){
46 | var userTweets = body.results
47 | var allTweets = $.merge(timelineTweets, userTweets)
48 | allTweets.sort(function(t1, t2){
49 | return new Date(t2.time) - new Date(t1.time);
50 | })
51 | $(allTweets.reverse()).each(function(index, tweet){
52 | renderTweet($("#wall"), tweet);
53 | });
54 | })
55 | })
56 | });
--------------------------------------------------------------------------------
/src/FsTweet.Web/paket.references:
--------------------------------------------------------------------------------
1 | FSharp.Core
2 | Suave
3 | DotLiquid
4 | Suave.DotLiquid
5 | Suave.Experimental
6 | Chessie
7 | BCrypt.Net-Next
8 | Chiron
9 | stream-net
10 | NodaTime
11 | Logary
12 | group Database
13 | SQLProvider
14 | Npgsql
15 | group Email
16 | Postmark
--------------------------------------------------------------------------------
/src/FsTweet.Web/views/guest/home.liquid:
--------------------------------------------------------------------------------
1 | {% extends "master_page.liquid" %}
2 |
3 | {% block head %}
4 | FsTweet - Powered by F#
5 | {% endblock %}
6 |
7 | {% block content %}
8 |
28 | {% endblock %}
--------------------------------------------------------------------------------
/src/FsTweet.Web/views/master_page.liquid:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 | {% block head %}
11 | {% endblock %}
12 |
13 |
14 |
15 | {% block content %}
16 | {% endblock %}
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 | {% block scripts %}
28 | {% endblock %}
29 |
30 |
31 |
--------------------------------------------------------------------------------
/src/FsTweet.Web/views/not_found.liquid:
--------------------------------------------------------------------------------
1 | {% extends "master_page.liquid" %}
2 |
3 | {% block head %}
4 | Not Found :(
5 | {% endblock %}
6 |
7 | {% block content %}
8 |
11 | {% endblock %}
--------------------------------------------------------------------------------
/src/FsTweet.Web/views/server_error.liquid:
--------------------------------------------------------------------------------
1 | {% extends "master_page.liquid" %}
2 |
3 | {% block head %}
4 | Internal Error :(
5 | {% endblock %}
6 |
7 | {% block content %}
8 |
11 | {% endblock %}
--------------------------------------------------------------------------------
/src/FsTweet.Web/views/user/login.liquid:
--------------------------------------------------------------------------------
1 | {% extends "master_page.liquid" %}
2 |
3 | {% block head %}
4 | Login
5 | {% endblock %}
6 |
7 | {% block content %}
8 |
9 |
10 |
11 | {% if model.Error %}
12 |
13 | {{ model.Error.Value }}
14 |
15 | {% endif %}
16 |
27 |
28 |
29 |
30 | {% endblock %}
--------------------------------------------------------------------------------
/src/FsTweet.Web/views/user/profile.liquid:
--------------------------------------------------------------------------------
1 | {% extends "master_page.liquid" %}
2 |
3 | {% block head %}
4 | {{model.Username}} - FsTweet
5 | {% endblock %}
6 |
7 | {% block content %}
8 |
9 |
10 |
11 |

12 |
@{{model.Username}}
13 | {% if model.IsLoggedIn %}
14 | {% unless model.IsSelf %}
15 | {% if model.IsFollowing %}
16 |
Following
17 | {% else %}
18 |
Follow
19 | {% endif %}
20 | {% endunless %}
21 |
Logout
22 | {% endif %}
23 |
24 |
25 |
26 |
37 |
38 |
39 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 | {% endblock %}
51 |
52 | {% block scripts %}
53 |
54 |
55 |
68 |
69 |
70 |
71 |
72 | {% endblock %}
--------------------------------------------------------------------------------
/src/FsTweet.Web/views/user/signup.liquid:
--------------------------------------------------------------------------------
1 | {% extends "master_page.liquid" %}
2 |
3 | {% block head %}
4 | Sign Up - FsTweet
5 | {% endblock %}
6 |
7 | {% block content %}
8 |
28 | {% endblock %}
--------------------------------------------------------------------------------
/src/FsTweet.Web/views/user/signup_success.liquid:
--------------------------------------------------------------------------------
1 | {% extends "master_page.liquid" %}
2 |
3 | {% block head %}
4 | Signup Success
5 | {% endblock %}
6 |
7 | {% block content %}
8 |
9 |
10 | Hi {{ model }}, Your account has been created.
11 | Check your email to activate the account.
12 |
13 |
14 | {% endblock %}
--------------------------------------------------------------------------------
/src/FsTweet.Web/views/user/verification_success.liquid:
--------------------------------------------------------------------------------
1 | {% extends "master_page.liquid" %}
2 |
3 | {% block head %}
4 | Email Verified
5 | {% endblock %}
6 |
7 | {% block content %}
8 |
9 |
10 | Hi {{ model }}, Your email address has been verified.
11 | Now you can login!
12 |
13 |
14 | {% endblock %}
--------------------------------------------------------------------------------
/src/FsTweet.Web/views/user/wall.liquid:
--------------------------------------------------------------------------------
1 | {% extends "master_page.liquid" %}
2 |
3 | {% block head %}
4 | {{model.Username}}
5 | {% endblock %}
6 |
7 | {% block content %}
8 |
9 |
27 | {% endblock %}
28 |
29 |
30 | {% block scripts %}
31 |
32 |
33 |
47 |
48 |
49 |
50 | {% endblock %}
--------------------------------------------------------------------------------
/web.config:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
14 |
15 |
16 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
--------------------------------------------------------------------------------