From 932293850f16f033da86e406aba29686fc5afe3f Mon Sep 17 00:00:00 2001 From: Kyle Mahan Date: Sun, 5 Jun 2016 13:46:28 -0700 Subject: [PATCH] working on super simple offline behavior --- frontend/feed.js | 135 ++++++++++++++++++++++++++++++ frontend/indieconfig.js | 70 ++++++++++++++++ frontend/manifest.json | 16 ++++ frontend/package.json | 33 ++++++++ frontend/sw.js | 31 +++++++ frontend/webaction.js | 63 ++++++++++++++ woodwind/templates/offline.jinja2 | 9 ++ 7 files changed, 357 insertions(+) create mode 100644 frontend/feed.js create mode 100644 frontend/indieconfig.js create mode 100644 frontend/manifest.json create mode 100644 frontend/package.json create mode 100644 frontend/sw.js create mode 100644 frontend/webaction.js create mode 100644 woodwind/templates/offline.jinja2 diff --git a/frontend/feed.js b/frontend/feed.js new file mode 100644 index 0000000..1158a15 --- /dev/null +++ b/frontend/feed.js @@ -0,0 +1,135 @@ +if (navigator.serviceWorker) { + navigator.serviceWorker.register('./sw.js') +} + +$(function(){ + function updateTimestamps() { + $(".permalink time").each(function() { + var absolute = $(this).attr('datetime'); + var formatted = moment.utc(absolute).fromNow(); + $(this).text(formatted); + }) + } + + function clickOlderLink(evt) { + evt.preventDefault(); + $.get(this.href, function(result) { + var $newElements = $("article,.pager", $(result)); + $(".pager").replaceWith($newElements); + $newElements.each(function () { + twttr.widgets.load(this); + }); + attachListeners(); + }); + } + + function submitMicropubForm(evt) { + evt.preventDefault(); + + var button = this; + var form = $(button).closest('form'); + var replyArea = form.parent(); + var endpoint = form.attr('action'); + var responseArea = $('.micropub-response', replyArea); + var formData = form.serializeArray(); + formData.push({name: button.name, value: button.value}); + + $.post( + endpoint, + formData, + function(result) { + if (Math.floor(result.code / 100) == 2) { + responseArea.html('Success!'); + $("textarea", form).val(""); + + if (button.value === 'rsvp-yes') { + $(".rsvps", form).html('✓ Going'); + } else if (button.value === 'rsvp-maybe') { + $(".rsvps", form).html('? Interested'); + } else if (button.value === 'rsvp-no') { + $(".rsvps", form).html('✗ Not Going'); + } + + } else { + responseArea.html('Failure'); + } + }, + 'json' + ); + + + responseArea.html('Posting…'); + } + + function attachListeners() { + $("#older-link").off('click').click(clickOlderLink); + $(".micropub-form button[type='submit']").off('click').click(submitMicropubForm); + + // Post by ctrl/cmd + enter in the text area + $(".micropub-form textarea.content").keyup(function(e) { + if ((e.ctrlKey || e.metaKey) && (e.keyCode == 13 || e.keyCode == 10)) { + var button = $(e.target).closest('form').find('button[value=reply]'); + button[0].click(); + } + }); + + $(".micropub-form .content").focus(function () { + $(this).animate({ height: "4em" }, 200); + var $target = $(evt.target); + }); + } + + + function clickUnfoldLink(evt) { + $('#fold').after($('#fold').children()) + $('#unfold-link').hide(); + } + + + function foldNewEntries(entries) { + $('#fold').prepend(entries.join('\n')); + attachListeners(); + $('#unfold-link').text($('#fold>article:not(.reply-context)').length + " New Posts"); + $('#unfold-link').off('click').click(clickUnfoldLink); + $('#unfold-link').show(); + + // load twitter embeds + twttr.widgets.load($('#fold').get(0)); + } + + // topic will be user:id or feed:id + function webSocketSubscribe(topic) { + if ('WebSocket' in window) { + var ws = new WebSocket(window.location.origin + .replace(/http:\/\//, 'ws://') + .replace(/https:\/\//, 'wss://') + + '/_updates'); + + ws.onopen = function(event) { + // send the topic + console.log('subscribing to topic: ' + topic); + ws.send(topic); + }; + ws.onmessage = function(event) { + var data = JSON.parse(event.data); + foldNewEntries(data.entries); + }; + } + } + + attachListeners(); + + $(document).on("keypress", function(e) { + if (e.which === 46) { + clickUnfoldLink(); + } + }); + + if (WS_TOPIC) { + webSocketSubscribe(WS_TOPIC); + } + + updateTimestamps(); + window.setInterval(updateTimestamps, 60 * 1000); + +}); diff --git a/frontend/indieconfig.js b/frontend/indieconfig.js new file mode 100644 index 0000000..dedce86 --- /dev/null +++ b/frontend/indieconfig.js @@ -0,0 +1,70 @@ +/*jslint browser: true, plusplus: true, vars: true, indent: 2 */ +window.loadIndieConfig = (function () { + 'use strict'; + + // Indie-Config Loading script + // by Pelle Wessman, voxpelli.com + // MIT-licensed + // http://indiewebcamp.com/indie-config + + var config, configFrame, configTimeout, + callbacks = [], + handleConfig, parseConfig; + + // When the configuration has been loaded – deregister all loading mechanics and call all callbacks + handleConfig = function () { + config = config || {}; + + configFrame.parentNode.removeChild(configFrame); + configFrame = undefined; + + window.removeEventListener('message', parseConfig); + + clearTimeout(configTimeout); + + while (callbacks[0]) { + callbacks.shift()(config); + } + }; + + // When we receive a message, check if the source is right and try to parse it + parseConfig = function (message) { + var correctSource = (configFrame && message.source === configFrame.contentWindow); + + if (correctSource && config === undefined) { + try { + config = JSON.parse(message.data); + } catch (ignore) {} + + handleConfig(); + } + }; + + return function (callback) { + // If the config is already loaded, call callback right away + if (config) { + callback(config); + return; + } + + // Otherwise add the callback to the queue + callbacks.push(callback); + + // Are we already trying to load the Indie-Config, then wait + if (configFrame) { + return; + } + + // Create the iframe that will load the Indie-Config + configFrame = document.createElement('iframe'); + configFrame.src = 'web+action:load'; + document.getElementsByTagName('body')[0].appendChild(configFrame); + configFrame.style.display = 'none'; + + // Listen for messages so we will catch the Indie-Config message + window.addEventListener('message', parseConfig); + + // And if no such Indie-Config message has been loaded in a while, abort the loading + configTimeout = setTimeout(handleConfig, 3000); + }; +}()); diff --git a/frontend/manifest.json b/frontend/manifest.json new file mode 100644 index 0000000..5cbbf53 --- /dev/null +++ b/frontend/manifest.json @@ -0,0 +1,16 @@ +{ + "name": "Woodwind", + "short_name": "Woodwind", + "start_url": "/", + "scope": "/", + "display": "standalone", + "theme_color": "#9b6137", + "background_color": "#9b6137", + "icons": [ + { + "src": "/static/logo.png", + "sizes": "512x512", + "type": "image/png", + } + ] +} \ No newline at end of file diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000..816a14b --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,33 @@ +{ + "name": "woodwind-fe", + "version": "1.0.0", + "description": "", + "main": "main.js", + "scripts": { + "test": "mocha" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/kylewm/woodwind.git" + }, + "keywords": [ + "indieweb", + "reader" + ], + "author": "Kyle Mahan", + "license": "BSD-2-Clause", + "bugs": { + "url": "https://github.com/kylewm/woodwind/issues" + }, + "homepage": "https://github.com/kylewm/woodwind#readme", + "devDependencies": { + "babel": "^6.5.2", + "babel-cli": "^6.9.0", + "babel-preset-es2015": "^6.9.0", + "mocha": "^2.5.3" + }, + "dependencies": { + "jquery": "^2.2.4", + "moment": "^2.13.0" + } +} diff --git a/frontend/sw.js b/frontend/sw.js new file mode 100644 index 0000000..6709a6c --- /dev/null +++ b/frontend/sw.js @@ -0,0 +1,31 @@ +var version = 'v2'; + +this.addEventListener('install', function (event) { + event.waitUntil( + caches.open(version).then(function (cache) { + return cache.addAll([ + '/static/logo.png', + '/static/style.css', + '/offline', + ]) + }) + ); +}) + +this.addEventListener('fetch', function (event) { + console.log('caught fetch: ' + event) + event.respondWith( + caches.match(event.request) + .then(function (response) { + console.log('cache got response: ' + response) + return response || fetch(event.request); + }) + .then(function (response) { + console.log('fetch got response: ' + response) + return response + }) + .catch(function (err) { + return caches.match('/offline') + }) + ) +}) \ No newline at end of file diff --git a/frontend/webaction.js b/frontend/webaction.js new file mode 100644 index 0000000..262ad91 --- /dev/null +++ b/frontend/webaction.js @@ -0,0 +1,63 @@ +/*jslint browser: true, plusplus: true, vars: true, indent: 2 */ +(function () { + 'use strict'; + + var loadingClassRegexp = /(^|\s)indieconfig-loading(\s|$)/; + + var doTheAction = function (indieConfig) { + var href, action, anchors; + + // Don't block the tag anymore as the queued action is now handled + this.className = this.className.replace(loadingClassRegexp, ' '); + + // Pick the correct endpoint for the correct action + action = this.getAttribute('do'); + href = indieConfig[action]; + + // If no endpoint is found, try the URL of the first a-tag within it + if (!href) { + anchors = this.getElementsByTagName('a'); + if (anchors[0]) { + href = anchors[0].href; + } + } + + // We have found an endpoint! + if (href) { + //Resolve a relative target + var target = document.createElement('a'); + target.href = this.getAttribute('with'); + target = target.href; + + // Insert the target into the endpoint + href = href.replace('{url}', encodeURIComponent(target || window.location.href)); + + // And redirect to it + window.open( href, '_blank'); + } + }; + + // Event handler for a click on an indie-action tag + var handleTheAction = function (e) { + // Prevent the default of eg. any a-tag fallback within the indie-action tag + e.preventDefault(); + + // Make sure this tag hasn't already been queued for the indieconfig-load + if (!loadingClassRegexp.test(this.className)) { + this.className += ' indieconfig-loading'; + // Set "doTheAction" to be called when the indie-config has been loaded + window.loadIndieConfig(doTheAction.bind(this)); + } + }; + + // Once the page is loased add click event listeners to all indie-action tags + window.addEventListener('DOMContentLoaded', function () { + var actions = document.querySelectorAll('indie-action'), + i, + length = actions.length; + + for (i = 0; i < length; i++) { + actions[i].addEventListener('click', handleTheAction); + } + }); +}()); diff --git a/woodwind/templates/offline.jinja2 b/woodwind/templates/offline.jinja2 new file mode 100644 index 0000000..751c6ea --- /dev/null +++ b/woodwind/templates/offline.jinja2 @@ -0,0 +1,9 @@ +{% extends "base.jinja2" %} + +{% block login %}{% endblock login %} + +{% block body %} + +Offline, and it feels so good + +{% endblock body %} \ No newline at end of file