aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorMatthew Somerville <matthew@mysociety.org>2020-02-14 12:55:15 +0000
committerMatthew Somerville <matthew@mysociety.org>2020-02-14 12:55:15 +0000
commita5373a4ce9e966f70cd5edf0334c27781d7a2f04 (patch)
tree9bf59ec3fd7b9de0194dc8e997e57e18f787629c
parent2530538ca6e36b659a63cc3b67099972fe0116af (diff)
parentf0edab7ee84e4358c279a5a8ec37dbb0ae8f1b3d (diff)
Merge branch 'pwa'
-rw-r--r--.gitignore2
-rw-r--r--CHANGELOG.md1
-rwxr-xr-xbin/update-schema1
-rw-r--r--conf/general.yml-docker3
-rw-r--r--conf/general.yml-example3
-rw-r--r--db/downgrade_0071---0070.sql18
-rw-r--r--db/schema.sql11
-rw-r--r--db/schema_0071-add-manifest-theme.sql28
-rw-r--r--docs/customising/config.md5
-rw-r--r--perllib/FixMyStreet/App/Controller/Offline.pm73
-rw-r--r--perllib/FixMyStreet/App/Controller/Root.pm5
-rw-r--r--perllib/FixMyStreet/Cobrand/FixMyStreet.pm9
-rw-r--r--perllib/FixMyStreet/DB/Result/ManifestTheme.pm47
-rw-r--r--perllib/FixMyStreet/TestMech.pm2
-rw-r--r--t/app/controller/offline.t48
-rw-r--r--t/app/controller/root.t12
-rw-r--r--templates/web/base/common_header_tags.html9
-rw-r--r--templates/web/base/common_scripts.html1
-rw-r--r--templates/web/base/header.html5
-rw-r--r--templates/web/base/offline/fallback.html (renamed from templates/web/base/offline/appcache.html)8
-rw-r--r--templates/web/base/offline/manifest.html16
-rw-r--r--templates/web/base/offline/service_worker.html104
-rw-r--r--templates/web/oxfordshire/header.html5
-rwxr-xr-xweb/cobrands/fixmystreet/images/192.pngbin0 -> 6327 bytes
-rwxr-xr-xweb/cobrands/fixmystreet/images/512.pngbin0 -> 20416 bytes
-rw-r--r--web/cobrands/fixmystreet/offline.js400
26 files changed, 519 insertions, 297 deletions
diff --git a/.gitignore b/.gitignore
index 9c6297b3d..974f82187 100644
--- a/.gitignore
+++ b/.gitignore
@@ -23,6 +23,8 @@ FixMyBarangay.po
# Photo upload cache
/web/photo
+# And theme upload
+/web/theme
# Installation
/local
diff --git a/CHANGELOG.md b/CHANGELOG.md
index c4f7bde7a..38cafc2ad 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -29,6 +29,7 @@
- Display map inline with duplicate suggestions on mobile. #2668
- Improved try again process on mobile. #2863
- Improve messaging/display of private reports.
+ - Add a web manifest and service worker. #2220
- Admin improvements:
- Add new roles system, to group permissions and apply to users. #2483
- Contact form emails now include user admin links.
diff --git a/bin/update-schema b/bin/update-schema
index 807e144eb..7941bc542 100755
--- a/bin/update-schema
+++ b/bin/update-schema
@@ -212,6 +212,7 @@ else {
# (assuming schema change files are never half-applied, which should be the case)
sub get_db_version {
return 'EMPTY' if ! table_exists('problem');
+ return '0071' if table_exists('manifest_theme');
return '0070' if column_like('alert_type', "ref='new_problems'", 'head_title', '{{SITE_NAME}}');
return '0069' if constraint_contains('admin_log_object_type_check', 'template');
return '0068' if column_exists('users', 'oidc_ids');
diff --git a/conf/general.yml-docker b/conf/general.yml-docker
index c73bd3657..33dcad67b 100644
--- a/conf/general.yml-docker
+++ b/conf/general.yml-docker
@@ -52,13 +52,10 @@ STAGING_SITE: 1
# this to 1 if you want a staging site to route reports as normal.
# - skip_checks: Manual testing of multiple cobrands can be made easier by
# skipping some checks they have in them, if this variable is set.
-# - enable_appcache: Whether the appcache should be active. NB: Only affects
-# superuser sessions.
STAGING_FLAGS:
send_reports: 0
skip_checks: 0
hide_staging_banner: 0
- enable_appcache: 0
# What to use as front page/alert example places placeholder
# Defaults to High Street, Main Street
diff --git a/conf/general.yml-example b/conf/general.yml-example
index 9bf6d1d05..8e8d0f76a 100644
--- a/conf/general.yml-example
+++ b/conf/general.yml-example
@@ -52,13 +52,10 @@ STAGING_SITE: 1
# this to 1 if you want a staging site to route reports as normal.
# - skip_checks: Manual testing of multiple cobrands can be made easier by
# skipping some checks they have in them, if this variable is set.
-# - enable_appcache: Whether the appcache should be active. NB: Only affects
-# superuser sessions.
STAGING_FLAGS:
send_reports: 0
skip_checks: 0
hide_staging_banner: 0
- enable_appcache: 0
# What to use as front page/alert example places placeholder
# Defaults to High Street, Main Street
diff --git a/db/downgrade_0071---0070.sql b/db/downgrade_0071---0070.sql
new file mode 100644
index 000000000..ebf206624
--- /dev/null
+++ b/db/downgrade_0071---0070.sql
@@ -0,0 +1,18 @@
+BEGIN;
+
+DROP TABLE manifest_theme;
+
+ALTER TABLE admin_log DROP CONSTRAINT admin_log_object_type_check;
+
+ALTER TABLE admin_log ADD CONSTRAINT admin_log_object_type_check CHECK (
+ object_type = 'problem'
+ OR object_type = 'update'
+ OR object_type = 'user'
+ OR object_type = 'moderation'
+ OR object_type = 'template'
+ OR object_type = 'body'
+ OR object_type = 'category'
+ OR object_type = 'role'
+);
+
+COMMIT;
diff --git a/db/schema.sql b/db/schema.sql
index cf2914467..0e38ad862 100644
--- a/db/schema.sql
+++ b/db/schema.sql
@@ -449,6 +449,7 @@ create table admin_log (
or object_type = 'body'
or object_type = 'category'
or object_type = 'role'
+ or object_type = 'manifesttheme'
),
object_id integer not null,
action text not null,
@@ -562,3 +563,13 @@ CREATE TABLE state (
type text not null check (type = 'open' OR type = 'closed' OR type = 'fixed'),
name text not null unique
);
+
+CREATE TABLE manifest_theme (
+ id serial not null primary key,
+ cobrand text not null unique,
+ name text not null,
+ short_name text not null,
+ background_colour text,
+ theme_colour text,
+ images text ARRAY
+);
diff --git a/db/schema_0071-add-manifest-theme.sql b/db/schema_0071-add-manifest-theme.sql
new file mode 100644
index 000000000..308d345f5
--- /dev/null
+++ b/db/schema_0071-add-manifest-theme.sql
@@ -0,0 +1,28 @@
+BEGIN;
+
+CREATE TABLE manifest_theme (
+ id serial not null primary key,
+ cobrand text not null unique,
+ name text not null,
+ short_name text not null,
+ background_colour text,
+ theme_colour text,
+ images text ARRAY
+);
+
+ALTER TABLE admin_log DROP CONSTRAINT admin_log_object_type_check;
+
+ALTER TABLE admin_log ADD CONSTRAINT admin_log_object_type_check CHECK (
+ object_type = 'problem'
+ OR object_type = 'update'
+ OR object_type = 'user'
+ OR object_type = 'moderation'
+ OR object_type = 'template'
+ OR object_type = 'body'
+ OR object_type = 'category'
+ OR object_type = 'role'
+ OR object_type = 'manifesttheme'
+);
+
+
+COMMIT;
diff --git a/docs/customising/config.md b/docs/customising/config.md
index ff994a25b..c5ac015b7 100644
--- a/docs/customising/config.md
+++ b/docs/customising/config.md
@@ -348,13 +348,12 @@ STAGING_FLAGS:
send_reports: 0
skip_checks: 1
hide_staging_banner: 1
- enable_appcache: 0
</pre>
</div>
<p>
Any reports created will now be sent to the email of the reporter
- and <em>not</em> the body's; any location checks are skipped, and
- we won't ever use appcache. Great for testing!
+ and <em>not</em> the body's; and any location checks are skipped.
+ Great for testing!
</p>
</dd>
diff --git a/perllib/FixMyStreet/App/Controller/Offline.pm b/perllib/FixMyStreet/App/Controller/Offline.pm
index f5d895318..57cbe201c 100644
--- a/perllib/FixMyStreet/App/Controller/Offline.pm
+++ b/perllib/FixMyStreet/App/Controller/Offline.pm
@@ -1,5 +1,9 @@
package FixMyStreet::App::Controller::Offline;
+
+use Image::Size;
+use JSON::MaybeXS;
use Moose;
+use Path::Tiny;
use namespace::autoclean;
BEGIN { extends 'Catalyst::Controller'; }
@@ -10,35 +14,72 @@ FixMyStreet::App::Controller::Offline - Catalyst Controller
=head1 DESCRIPTION
-Offline pages Catalyst Controller.
+Offline pages Catalyst Controller - service worker handling
=head1 METHODS
=cut
-sub have_appcache : Private {
+sub service_worker : Path("/service-worker.js") {
my ($self, $c) = @_;
- return $c->user_exists && $c->user->has_body_permission_to('planned_reports')
- && !($c->user->is_superuser && FixMyStreet->staging_flag('enable_appcache', 0));
+ $c->res->content_type('application/javascript');
}
-sub manifest : Path("/offline/appcache.manifest") {
+sub fallback : Local {
my ($self, $c) = @_;
- unless ($c->forward('have_appcache')) {
- $c->response->status(404);
- $c->response->body('NOT FOUND');
- }
- $c->res->content_type('text/cache-manifest; charset=utf-8');
- $c->res->header(Cache_Control => 'no-cache, no-store');
}
-sub appcache : Path("/offline/appcache") {
+sub manifest: Path("/.well-known/manifest.webmanifest") {
my ($self, $c) = @_;
- $c->detach('/page_error_404_not_found', []) if keys %{$c->req->params} && !$c->req->query_keywords;
- unless ($c->forward('have_appcache')) {
- $c->response->status(404);
- $c->response->body('NOT FOUND');
+ $c->res->content_type('application/manifest+json');
+
+ my $theme = $c->model('DB::ManifestTheme')->find({ cobrand => $c->cobrand->moniker });
+ unless ( $theme ) {
+ $theme = $c->model('DB::ManifestTheme')->new({
+ name => $c->stash->{site_name},
+ short_name => $c->stash->{site_name},
+ background_colour => '#ffffff',
+ theme_colour => '#ffd000',
+ });
}
+
+ my @icons;
+ my $uri = '/theme/' . $c->cobrand->moniker;
+ my $theme_path = path(FixMyStreet->path_to('web' . $uri));
+ $theme_path->visit(
+ sub {
+ my ($x, $y, $typ) = Image::Size::imgsize($_->stringify);
+ push @icons, {
+ src => join('/', $uri, $_->basename),
+ sizes => join('x', $x, $y),
+ type => $typ eq 'PNG' ? 'image/png' : $typ eq 'GIF' ? 'image/gif' : $typ eq 'JPG' ? 'image/jpeg' : '',
+ };
+ }
+ );
+
+ unless (@icons) {
+ push @icons,
+ { src => "/cobrands/fixmystreet/images/192.png", sizes => "192x192", type => "image/png" },
+ { src => "/cobrands/fixmystreet/images/512.png", sizes => "512x512", type => "image/png" };
+ }
+
+ my $data = {
+ name => $theme->name,
+ short_name => $theme->short_name,
+ background_color => $theme->background_colour,
+ theme_color => $theme->theme_colour,
+ icons => \@icons,
+ lang => $c->stash->{lang_code},
+ display => "minimal-ui",
+ start_url => "/?pwa",
+ scope => "/",
+ };
+ if ($c->cobrand->can('manifest')) {
+ $data = { %$data, %{$c->cobrand->manifest} };
+ }
+
+ my $json = encode_json($data);
+ $c->res->body($json);
}
__PACKAGE__->meta->make_immutable;
diff --git a/perllib/FixMyStreet/App/Controller/Root.pm b/perllib/FixMyStreet/App/Controller/Root.pm
index fb6d063be..caaa260ff 100644
--- a/perllib/FixMyStreet/App/Controller/Root.pm
+++ b/perllib/FixMyStreet/App/Controller/Root.pm
@@ -159,11 +159,6 @@ sub check_login_required : Private {
}x;
return if $c->request->path =~ $whitelist;
- # Blacklisted URLs immediately 404
- # This is primarily to work around a Safari bug where the appcache
- # URL is requested in an infinite loop if it returns a 302 redirect.
- $c->detach('/page_error_404_not_found', []) if $c->request->path =~ /^offline/;
-
$c->detach( '/auth/redirect' );
}
diff --git a/perllib/FixMyStreet/Cobrand/FixMyStreet.pm b/perllib/FixMyStreet/Cobrand/FixMyStreet.pm
index 8339ceede..ba82949c7 100644
--- a/perllib/FixMyStreet/Cobrand/FixMyStreet.pm
+++ b/perllib/FixMyStreet/Cobrand/FixMyStreet.pm
@@ -351,4 +351,13 @@ sub update_email_shortlisted_user {
FixMyStreet::Cobrand::TfL::update_email_shortlisted_user($self, $update);
}
+sub manifest {
+ return {
+ related_applications => [
+ { platform => 'play', url => 'https://play.google.com/store/apps/details?id=org.mysociety.FixMyStreet', id => 'org.mysociety.FixMyStreet' },
+ { platform => 'itunes', url => 'https://apps.apple.com/gb/app/fixmystreet/id297456545', id => 'id297456545' },
+ ],
+ };
+}
+
1;
diff --git a/perllib/FixMyStreet/DB/Result/ManifestTheme.pm b/perllib/FixMyStreet/DB/Result/ManifestTheme.pm
new file mode 100644
index 000000000..a2f49eacb
--- /dev/null
+++ b/perllib/FixMyStreet/DB/Result/ManifestTheme.pm
@@ -0,0 +1,47 @@
+use utf8;
+package FixMyStreet::DB::Result::ManifestTheme;
+
+# Created by DBIx::Class::Schema::Loader
+# DO NOT MODIFY THE FIRST PART OF THIS FILE
+
+use strict;
+use warnings;
+
+use base 'DBIx::Class::Core';
+__PACKAGE__->load_components(
+ "FilterColumn",
+ "FixMyStreet::InflateColumn::DateTime",
+ "FixMyStreet::EncodedColumn",
+);
+__PACKAGE__->table("manifest_theme");
+__PACKAGE__->add_columns(
+ "id",
+ {
+ data_type => "integer",
+ is_auto_increment => 1,
+ is_nullable => 0,
+ sequence => "manifest_theme_id_seq",
+ },
+ "cobrand",
+ { data_type => "text", is_nullable => 0 },
+ "name",
+ { data_type => "text", is_nullable => 0 },
+ "short_name",
+ { data_type => "text", is_nullable => 0 },
+ "background_colour",
+ { data_type => "text", is_nullable => 1 },
+ "theme_colour",
+ { data_type => "text", is_nullable => 1 },
+ "images",
+ { data_type => "text[]", is_nullable => 1 },
+);
+__PACKAGE__->set_primary_key("id");
+__PACKAGE__->add_unique_constraint("manifest_theme_cobrand_key", ["cobrand"]);
+
+
+# Created by DBIx::Class::Schema::Loader v0.07035 @ 2020-01-30 14:30:42
+# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:Sgbva7nEVkjqG/+lQL/ryw
+
+
+# You can replace this text with custom code or comments, and it will be preserved on regeneration
+1;
diff --git a/perllib/FixMyStreet/TestMech.pm b/perllib/FixMyStreet/TestMech.pm
index 5bb975104..1b7fba1bd 100644
--- a/perllib/FixMyStreet/TestMech.pm
+++ b/perllib/FixMyStreet/TestMech.pm
@@ -585,7 +585,7 @@ sub get_ok_json {
# check that the content-type of response is correct
croak "Response was not JSON"
- unless $res->header('Content-Type') =~ m{^application/json\b};
+ unless $res->header('Content-Type') =~ m{^application/(?:[a-z]+\+)?json\b};
return decode_json( $res->content );
}
diff --git a/t/app/controller/offline.t b/t/app/controller/offline.t
new file mode 100644
index 000000000..d2a5009ec
--- /dev/null
+++ b/t/app/controller/offline.t
@@ -0,0 +1,48 @@
+use FixMyStreet::TestMech;
+use Path::Tiny;
+
+my $mech = FixMyStreet::TestMech->new;
+
+FixMyStreet::override_config {
+ ALLOWED_COBRANDS => 'test'
+}, sub {
+ my $theme_dir = path(FixMyStreet->path_to('web/theme/test'));
+ $theme_dir->mkpath;
+ my $image_path = path('t/app/controller/sample.jpg');
+ $image_path->copy($theme_dir->child('sample.jpg'));
+ subtest 'manifest' => sub {
+ my $j = $mech->get_ok_json('/.well-known/manifest.webmanifest');
+ is $j->{name}, 'FixMyStreet', 'correct name';
+ is $j->{theme_color}, '#ffd000', 'correct theme colour';
+ is_deeply $j->{icons}[0], {
+ type => 'image/jpeg',
+ src => '/theme/test/sample.jpg',
+ sizes => '133x100'
+ }, 'correct icon';
+ };
+ $theme_dir->remove_tree;
+};
+
+FixMyStreet::override_config {
+ ALLOWED_COBRANDS => 'fixmystreet'
+}, sub {
+ subtest '.com manifest' => sub {
+ my $j = $mech->get_ok_json('/.well-known/manifest.webmanifest');
+ is $j->{related_applications}[0]{platform}, 'play', 'correct app';
+ is $j->{icons}[0]{sizes}, '192x192', 'correct fallback size';
+ };
+};
+
+subtest 'service worker' => sub {
+ $mech->get_ok('/service-worker.js');
+ $mech->content_contains('translation_strings');
+ $mech->content_contains('offline/fallback');
+};
+
+subtest 'offline fallback page' => sub {
+ $mech->get_ok('/offline/fallback');
+ $mech->content_contains('Offline');
+ $mech->content_contains('offline_list');
+};
+
+done_testing(); \ No newline at end of file
diff --git a/t/app/controller/root.t b/t/app/controller/root.t
index 85119da24..0263b65d7 100644
--- a/t/app/controller/root.t
+++ b/t/app/controller/root.t
@@ -60,18 +60,6 @@ FixMyStreet::override_config {
is $mech->res->previous, undef, 'No redirect';
}
};
-
- subtest 'LOGIN_REQUIRED = 1 404s blacklisted URLs' => sub {
- my @blacklist = (
- '/offline/appcache',
- );
-
- foreach my $url (@blacklist) {
- $mech->get($url);
- ok !$mech->res->is_success(), "want a bad response";
- is $mech->res->code, 404, "got 404";
- }
- };
};
subtest "check_login_disallowed cobrand hook" => sub {
diff --git a/templates/web/base/common_header_tags.html b/templates/web/base/common_header_tags.html
index 728b81363..279f561df 100644
--- a/templates/web/base/common_header_tags.html
+++ b/templates/web/base/common_header_tags.html
@@ -1,6 +1,7 @@
[% SET start = c.config.ADMIN_BASE_URL IF admin %]
<meta http-equiv="content-type" content="text/html; charset=utf-8">
+<link rel="manifest" href="/.well-known/manifest.webmanifest">
[% IF csrf_token %]
<meta content="[% csrf_token %]" name="csrf-token" />
@@ -15,6 +16,14 @@
(function(a){a=a.documentElement;a.className=a.className.replace(/\bno-js\b/,"js");var b=-1<a.className.indexOf("ie8");b=Modernizr.mq("(min-width: 48em)")||b?"desktop":"mobile";"IntersectionObserver"in window&&(a.className+=" lazyload");"mobile"==b&&(a.className+=' mobile[% " map-fullscreen only-map map-reporting" IF page == "around" %]')})(document);
</script>
+<script nonce="[% csp_nonce %]">
+if ('serviceWorker' in navigator) {
+ window.addEventListener('load', function() {
+ navigator.serviceWorker.register('/service-worker.js');
+ });
+}
+</script>
+
[% IF robots %]
<meta name="robots" content="[% robots %]">
[% ELSIF c.config.STAGING_SITE %]
diff --git a/templates/web/base/common_scripts.html b/templates/web/base/common_scripts.html
index 7df8feaa0..f94b3d464 100644
--- a/templates/web/base/common_scripts.html
+++ b/templates/web/base/common_scripts.html
@@ -46,6 +46,7 @@ IF c.user_exists AND (c.user.from_body OR c.user.is_superuser);
END;
IF c.user.has_body_permission_to('planned_reports');
scripts.push(
+ 'https://cdn.jsdelivr.net/npm/idb-keyval@3/dist/idb-keyval-iife.min.js',
version('/cobrands/fixmystreet/offline.js'),
);
END;
diff --git a/templates/web/base/header.html b/templates/web/base/header.html
index 74ec2d16a..b3d77b3a6 100644
--- a/templates/web/base/header.html
+++ b/templates/web/base/header.html
@@ -6,10 +6,7 @@
<!doctype html>
<!--[if IE 8]> <html class="no-js ie8"[% html_att | safe %]><![endif]-->
<!--[if IE 9]> <html class="no-js ie9"[% html_att | safe %]><![endif]-->
-<!--[if gt IE 9]><!--><html class="no-js"[% html_att | safe %]
-[% IF appcache ~%]
- manifest="/offline/appcache.manifest"
-[%~ END %]><!--<![endif]-->
+<!--[if gt IE 9]><!--><html class="no-js"[% html_att | safe %]><!--<![endif]-->
<head>
<meta name="viewport" content="initial-scale=1.0">
diff --git a/templates/web/base/offline/appcache.html b/templates/web/base/offline/fallback.html
index ed48b7a00..b8e1ee9b9 100644
--- a/templates/web/base/offline/appcache.html
+++ b/templates/web/base/offline/fallback.html
@@ -1,11 +1,9 @@
[% SET bodyclass = "fullwidthpage offlinepage" ~%]
-[% INCLUDE 'header.html' appcache = 1 %]
+[% INCLUDE 'header.html' %]
-<h1>[% loc('Internet glitch') %]</h1>
+<h1>[% loc('Offline') %]</h1>
-<p>[% loc('Sorry, we don’t have a good enough connection to fetch that page, or the
-page wasn’t found or there was a server error. Please try again later.') %]
-</p>
+<p>[% loc('Sorry, we don’t have a good enough connection to fetch that page.') %]</p>
<ul class="item-list item-list--reports" id="offline_list"></ul>
diff --git a/templates/web/base/offline/manifest.html b/templates/web/base/offline/manifest.html
deleted file mode 100644
index 93d26cb94..000000000
--- a/templates/web/base/offline/manifest.html
+++ /dev/null
@@ -1,16 +0,0 @@
-CACHE MANIFEST
-
-[% PROCESS 'common_scripts.html' ~%]
-
-CACHE:
-[% version('/cobrands/' _ c.cobrand.asset_moniker _ '/base.css') %]
-[% version('/cobrands/' _ c.cobrand.asset_moniker _ '/layout.css') %]
-[% FOR script IN scripts ~%]
- [%- script %]
-[% END %]
-
-NETWORK:
-*
-
-FALLBACK:
-/ [% version('../templates/web/base/offline/appcache.html', '/offline/appcache') %]
diff --git a/templates/web/base/offline/service_worker.html b/templates/web/base/offline/service_worker.html
new file mode 100644
index 000000000..0feb26ce6
--- /dev/null
+++ b/templates/web/base/offline/service_worker.html
@@ -0,0 +1,104 @@
+[%
+SET bodyclass = "offlinepage"; # For selection of scripts
+PROCESS 'common_scripts.html';
+SET offline_html = version('../templates/web/base/offline/fallback.html', '/offline/fallback');
+SET scripts_seen = {};
+
+~%]
+
+importScripts('https://cdn.jsdelivr.net/npm/idb-keyval@3/dist/idb-keyval-iife.min.js');
+
+const requiredOffline = [
+ "[% version('/cobrands/' _ c.cobrand.asset_moniker _ '/base.css') %]",
+ "[% version('/cobrands/' _ c.cobrand.asset_moniker _ '/layout.css') %]",
+ "[% version('/vendor/OpenLayers/theme/default/style.css') %]",
+ "[% version('/vendor/fancybox/jquery.fancybox-1.3.4.css') %]",
+ [%
+ FOR script IN scripts;
+ NEXT IF scripts_seen.${script};
+ scripts_seen.${script} = 1;
+ ~%]
+ "[%- script %]",
+ [% END %]
+ "[% offline_html %]"
+];
+
+const staticCache = 'static';
+// const pageCache = 'pages';
+
+addEventListener('install', function(evt) {
+ evt.waitUntil(precache());
+});
+
+async function precache() {
+ const cache = await caches.open(staticCache);
+ return cache.addAll(requiredOffline);
+}
+
+addEventListener('fetch', fetchEvent => {
+ const request = fetchEvent.request;
+ const url = new URL(request.url);
+
+ if (url.origin !== location.origin) {
+ return;
+ }
+
+ // Handle inspection form submission if offline...
+ if (request.method === 'POST' && RegExp('/report/\\d+$').test(url)) {
+ fetchEvent.respondWith(async function() {
+ const fetchPromise = fetch(request.clone());
+ try {
+ return await fetchPromise;
+ }
+ catch {
+ fetchEvent.waitUntil(async function() {
+ var text = await request.text();
+ let formData = new URLSearchParams(text);
+ formData.set('save', 2);
+ formData.set('saved_at', Math.floor(+new Date() / 1000));
+ formData = formData.toString();
+
+ var data = await idbKeyval.get('offlineData') || { cachedReports: {}, forms: [] };
+ var forms = data.forms;
+ if (!forms.length || formData != forms[forms.length - 1][1]) {
+ forms.push([request.url, formData]);
+ }
+ return idbKeyval.set('offlineData', data);
+ }());
+
+ return Response.redirect('/my/planned?saved=1');
+ };
+ }());
+ }
+
+ if (request.method !== "GET") {
+ return;
+ }
+
+ fetchEvent.respondWith(async function() {
+ if (request.mode === 'navigate') {
+ const fetchPromise = fetch(request);
+
+// For now, only save pages manually for inspectors
+// fetchEvent.waitUntil(async function() {
+// const responseCopy = (await fetchPromise).clone();
+// const cache = await caches.open(pageCache);
+// await responseCopy.ok ? cache.put(request, responseCopy) : cache.delete(request);
+// }());
+
+ try {
+ return await fetchPromise;
+ }
+ catch {
+ let cached = await caches.match(request) || await caches.match("[% offline_html %]");
+ return cached || offlineResponse();
+ }
+ } else {
+ const responseFromCache = await caches.match(request);
+ return responseFromCache || fetch(request);
+ }
+ }());
+});
+
+var offlineResponse = () =>
+ new Response('Service Unavailable', { status: 503, statusText: 'Service Unavailable', headers: { 'Content-Type': 'text/html' }});
diff --git a/templates/web/oxfordshire/header.html b/templates/web/oxfordshire/header.html
index 70d7e4b18..72cba2aec 100644
--- a/templates/web/oxfordshire/header.html
+++ b/templates/web/oxfordshire/header.html
@@ -1,10 +1,7 @@
<!doctype html>
<!--[if IE 8]> <html class="no-js ie8" lang="[% lang_code %]"><![endif]-->
<!--[if IE 9]> <html class="no-js ie9" lang="[% lang_code %]"><![endif]-->
-<!--[if gt IE 9]><!--><html class="no-js" lang="[% lang_code %]"
-[% IF appcache ~%]
- manifest="/offline/appcache.manifest"
-[%~ END %]><!--<![endif]-->
+<!--[if gt IE 9]><!--><html class="no-js" lang="[% lang_code %]"><!--<![endif]-->
<head>
<meta name="viewport" content="initial-scale=1.0">
diff --git a/web/cobrands/fixmystreet/images/192.png b/web/cobrands/fixmystreet/images/192.png
new file mode 100755
index 000000000..e43a3bb37
--- /dev/null
+++ b/web/cobrands/fixmystreet/images/192.png
Binary files differ
diff --git a/web/cobrands/fixmystreet/images/512.png b/web/cobrands/fixmystreet/images/512.png
new file mode 100755
index 000000000..13cf165d3
--- /dev/null
+++ b/web/cobrands/fixmystreet/images/512.png
Binary files differ
diff --git a/web/cobrands/fixmystreet/offline.js b/web/cobrands/fixmystreet/offline.js
index 4d7c471aa..908326a69 100644
--- a/web/cobrands/fixmystreet/offline.js
+++ b/web/cobrands/fixmystreet/offline.js
@@ -8,8 +8,7 @@ fixmystreet.offlineBanner = (function() {
}
// Note this non-global way of handling plurals may need looking at in future
- function formText() {
- var num = fixmystreet.offlineData.getForms().length;
+ function formText(num) {
if ( num === 1 ) {
return num + ' ' + translation_strings.offline.update_single;
} else {
@@ -17,12 +16,12 @@ fixmystreet.offlineBanner = (function() {
}
}
- function onlineText() {
- return sprintf(translation_strings.offline.saved_to_submit, formText());
+ function onlineText(num) {
+ return sprintf(translation_strings.offline.saved_to_submit, formText(num));
}
- function offlineText() {
- return translation_strings.offline.you_are_offline + ' \u2013 ' + sprintf(translation_strings.offline.N_saved, formText());
+ function offlineText(num) {
+ return translation_strings.offline.you_are_offline + ' \u2013 ' + sprintf(translation_strings.offline.N_saved, formText(num));
}
function remove() {
@@ -31,24 +30,29 @@ fixmystreet.offlineBanner = (function() {
return {
make: function(offline) {
- var num = fixmystreet.offlineData.getForms().length;
- var banner = ['<div class="top_banner top_banner--offline"><p><span id="offline_saving"></span> <span id="offline_forms">'];
- if (offline || num > 0) {
- banner.push(offline ? offlineText() : onlineText());
- }
- banner.push('</span></p></div>');
- banner = $(banner.join(''));
- banner.prependTo('.content');
- if (num === 0) {
- banner.hide();
- }
+ fixmystreet.offlineData.getFormsLength().then(function(num) {
+ var banner = ['<div class="top_banner top_banner--offline"><p><span id="offline_saving"></span> <span id="offline_forms">'];
+ if (offline || num > 0) {
+ banner.push(offline ? offlineText(num) : onlineText(num));
+ }
+ banner.push('</span></p></div>');
+ banner = $(banner.join(''));
+ banner.prependTo('.content');
+ if (num === 0) {
+ banner.hide();
+ }
+ });
window.addEventListener("offline", function(e) {
- $('#offline_forms').html(offlineText());
+ fixmystreet.offlineData.getFormsLength().then(function(num) {
+ $('#offline_forms').html(offlineText(num));
+ });
});
window.addEventListener("online", function(e) {
- $('#offline_forms').html(onlineText());
+ fixmystreet.offlineData.getFormsLength().then(function(num) {
+ $('#offline_forms').html(onlineText(num));
+ });
});
function nextForm(DataOrJqXHR, textStatus, jqXHROrErrorThrown) {
@@ -62,37 +66,40 @@ fixmystreet.offlineBanner = (function() {
$(document).on('click', '#oFN', function(e) {
e.preventDefault();
- fixmystreet.offlineData.getForms().forEach(function(form) {
- $(document).queue('postForm', function() {
- postForm(form[0], form[1]).fail(function(jqXHR) {
- if (jqXHR.status !== 400) {
- return nextForm();
- }
- // In case the request failed due to out-of-date CSRF token,
- // try once more with a new token given in the error response.
- var m = jqXHR.responseText.match(/content="([^"]*)" name="csrf-token"/);
- if (!m) {
- return nextForm();
- }
- var token = m[1];
- if (!token) {
- return nextForm();
- }
- var param = form[1].replace(/&token=[^&]*/, '&token=' + token);
- return postForm(form[0], param).fail(nextForm);
+ fixmystreet.offlineData.getForms().then(function(forms) {
+ forms.forEach(function(form) {
+ $(document).queue('postForm', function() {
+ postForm(form[0], form[1]).fail(function(jqXHR) {
+ if (jqXHR.status !== 400) {
+ return nextForm();
+ }
+ // In case the request failed due to out-of-date CSRF token,
+ // try once more with a new token given in the error response.
+ var m = jqXHR.responseText.match(/content="([^"]*)" name="csrf-token"/);
+ if (!m) {
+ return nextForm();
+ }
+ var token = m[1];
+ if (!token) {
+ return nextForm();
+ }
+ var param = form[1].replace(/&token=[^&]*/, '&token=' + token);
+ return postForm(form[0], param).fail(nextForm);
+ });
});
});
+ $(document).dequeue('postForm');
});
- $(document).dequeue('postForm');
});
},
update: function() {
$('.top_banner--offline').slideDown();
- $('#offline_forms span').text(formText());
- var num = fixmystreet.offlineData.getForms().length;
- if (num === 0) {
- window.setTimeout(remove, 3000);
- }
+ fixmystreet.offlineData.getFormsLength().then(function(num) {
+ $('#offline_forms span').text(formText(num));
+ if (num === 0) {
+ window.setTimeout(remove, 3000);
+ }
+ });
},
startProgress: function(l) {
$('.top_banner--offline').slideDown();
@@ -115,61 +122,55 @@ fixmystreet.offlineData = (function() {
var data;
function getData() {
- if (data === undefined) {
- data = JSON.parse(localStorage.getItem('offlineData'));
- if (!data) {
- data = { cachedReports: {}, forms: [] };
- }
+ if (data !== undefined) {
+ return Promise.resolve(data);
}
- return data;
+ return idbKeyval.get('offlineData').then(function(d) {
+ data = d || { cachedReports: {}, forms: [] };
+ return data;
+ });
}
- function saveData() {
- localStorage.setItem('offlineData', JSON.stringify(getData()));
+ function updateData(cb) {
+ getData().then(function(data) {
+ cb(data);
+ idbKeyval.set('offlineData', data);
+ });
}
return {
- getForms: function() {
- return getData().forms;
+ getFormsLength: function() {
+ return getData().then(function(data) { return data.forms.length; });
},
- addForm: function(action, formData) {
- var forms = getData().forms;
- if (!forms.length || formData != forms[forms.length - 1][1]) {
- forms.push([action, formData]);
- saveData();
- }
- fixmystreet.offlineBanner.update();
+ getForms: function() {
+ return getData().then(function(data) { return data.forms; });
},
shiftForm: function(idx) {
- getData().forms.shift();
- saveData();
- fixmystreet.offlineBanner.update();
+ updateData(function(data) {
+ data.forms.shift();
+ fixmystreet.offlineBanner.update();
+ });
},
clearForms: function(idx) {
- getData().forms = [];
- saveData();
- fixmystreet.offlineBanner.update();
+ updateData(function(data) {
+ data.forms = [];
+ fixmystreet.offlineBanner.update();
+ });
},
- getCachedUrls: function() {
- return Object.keys(getData().cachedReports);
- },
- isIndexed: function(url, lastupdate) {
- if (lastupdate) {
- return getData().cachedReports[url] === lastupdate;
- }
- return !!getData().cachedReports[url];
+ getCachedReports: function() {
+ return getData().then(function(data) { return data.cachedReports; });
},
add: function(url, lastupdate) {
- var data = getData();
- data.cachedReports[url] = lastupdate || "-";
- saveData();
+ updateData(function(data) {
+ data.cachedReports[url] = lastupdate || "-";
+ });
},
remove: function(urls) {
- var data = getData();
- urls.forEach(function(url) {
- delete data.cachedReports[url];
+ updateData(function(data) {
+ urls.forEach(function(url) {
+ delete data.cachedReports[url];
+ });
});
- saveData();
}
};
})();
@@ -177,57 +178,39 @@ fixmystreet.offlineData = (function() {
fixmystreet.cachet = (function(){
var urlsInProgress = {};
- function cacheURL(url, type) {
+ function cacheURL(url) {
urlsInProgress[url] = 1;
-
- var ret;
- if (type === 'image') {
- ret = $.Deferred(function(deferred) {
- var oReq = new XMLHttpRequest();
- oReq.open("GET", url, true);
- oReq.responseType = "blob";
- oReq.onload = function(oEvent) {
- var blob = oReq.response;
- var reader = new window.FileReader();
- reader.readAsDataURL(blob);
- reader.onloadend = function() {
- localStorage.setItem(url, reader.result);
+ return caches.open('pages').then(function(cache) {
+ return fetch(url).then(function(response) {
+ if (response.ok) {
+ cache.put(url, response.clone()).then(function() {
delete urlsInProgress[url];
- deferred.resolve(blob);
- };
- };
- oReq.send();
- });
- } else {
- ret = $.ajax(url).pipe(function(content, textStatus, jqXHR) {
- localStorage.setItem(url, content);
- delete urlsInProgress[url];
- return content;
+ });
+ }
+ return response;
});
- }
- return ret;
+ });
}
function cacheReport(item) {
- return cacheURL(item.url, 'html').pipe(function(html) {
+ return cacheURL(item.url).then(function(response) {
+ return response.text();
+ }).then(function(html) {
var $reportPage = $(html);
var imagesToGet = [
item.url + '/map' // Static map image
];
$reportPage.find('img').each(function(i, img) {
- if (img.src.indexOf('/photo/') === -1 || fixmystreet.offlineData.isIndexed(img.src) || urlsInProgress[img.src]) {
+ if (img.src.indexOf('/photo/') === -1 || urlsInProgress[img.src]) {
return;
}
imagesToGet.push(img.src);
imagesToGet.push(img.src.replace('.jpeg', '.fp.jpeg'));
});
var imagePromises = imagesToGet.map(function(url) {
- return cacheURL(url, 'image');
+ return cacheURL(url);
});
- return $.when.apply(undefined, imagePromises).pipe(function() {
- fixmystreet.offlineBanner.progress();
- fixmystreet.offlineData.add(item.url, item.lastupdate);
- }, function() {
+ return Promise.all(imagePromises).finally(function() {
fixmystreet.offlineBanner.progress();
fixmystreet.offlineData.add(item.url, item.lastupdate);
});
@@ -241,7 +224,7 @@ fixmystreet.cachet = (function(){
var promises = items.map(function(item) {
return cacheReport(item);
});
- return $.when.apply(undefined, promises);
+ return Promise.all(promises);
}
return {
@@ -265,130 +248,101 @@ fixmystreet.offline = (function() {
var toRemove = [];
var shouldBeCached = {};
- localStorage.setItem('/my/planned', $('.item-list').html());
+ idbKeyval.set('/my/planned', $('.item-list').html());
- getReportsFromList().forEach(function(item, i) {
- if (!fixmystreet.offlineData.isIndexed(item.url, item.lastupdate)) {
- toCache.push(item);
- }
- shouldBeCached[item.url] = 1;
- });
+ fixmystreet.offlineData.getCachedReports().then(function(reports) {
+ getReportsFromList().forEach(function(item, i) {
+ var t = reports[item.url];
+ if (t !== item.lastupdate) {
+ toCache.push(item);
+ }
+ shouldBeCached[item.url] = 1;
+ });
+
+ Object.keys(reports).forEach(function(url) {
+ if ( !shouldBeCached[url] ) {
+ toRemove.push(url);
+ }
+ });
- fixmystreet.offlineData.getCachedUrls().forEach(function(url) {
- if ( !shouldBeCached[url] ) {
- toRemove.push(url);
+ if (toRemove[0]) {
+ removeReports(toRemove);
+ }
+ if (toCache[0]) {
+ fixmystreet.cachet.cacheReports(toCache);
}
});
-
- if (toRemove[0]) {
- removeReports(toRemove);
- }
- if (toCache[0]) {
- fixmystreet.cachet.cacheReports(toCache);
- }
}
// Remove a list of reports from the offline cache
function removeReports(urls) {
- var pathsRemoved = [];
- urls.forEach(function(url) {
- var html = localStorage.getItem(url);
- var $reportPage = $(html);
- localStorage.removeItem(url + '/map');
- $reportPage.find('img').each(function(i, img) {
- if (img.src.indexOf('/photo/') === -1) {
- return;
- }
- localStorage.removeItem(img.src);
- localStorage.removeItem(img.src.replace('.jpeg', '.fp.jpeg'));
+ caches.open('pages').then(function(cache) {
+ urls.forEach(function(url) {
+ fetch(url).then(function(response) {
+ return response.text();
+ }).then(function(html) {
+ var $reportPage = $(html);
+ cache.delete(url + '/map');
+ $reportPage.find('img').each(function(i, img) {
+ if (img.src.indexOf('/photo/') === -1) {
+ return;
+ }
+ cache.delete(img.src);
+ cache.delete(img.src.replace('.jpeg', '.fp.jpeg'));
+ });
+ cache.delete(url);
+ });
});
- localStorage.removeItem(url);
+ fixmystreet.offlineData.remove(urls);
});
- fixmystreet.offlineData.remove(urls);
}
- function showReportFromCache(url) {
- var html = localStorage.getItem(url);
- if (!html) {
- return false;
- }
- var map = localStorage.getItem(url + '/map');
- var found = html.match(/<body[^>]*>[\s\S]*<\/body>/);
- document.body.outerHTML = found[0];
- $('#map_box').html('<img src="' + map + '">').css({ textAlign: 'center', height: 'auto' });
- replaceImages('img');
-
+ function showReportOffline(url) {
+ $('#map_box').html('<img src="' + url + '/map">').css({ textAlign: 'center', height: 'auto' });
$('.moderate-display.segmented-control, .shadow-wrap, #update_form, #report-cta, .mysoc-footer, .nav-wrapper').hide();
-
$('.js-back-to-report-list').attr('href', '/my/planned');
// Refill form with saved data if there is any
- var savedForm;
- fixmystreet.offlineData.getForms().forEach(function(form) {
- if (form[0].endsWith(url)) {
- savedForm = form[1];
- }
- });
- if (savedForm) {
- savedForm.replace(/\+/g, '%20').split('&').forEach(function(kv) {
- kv = kv.split('=', 2);
- if (kv[0] != 'include_update' && kv[0] != 'public_update' && kv[0] != 'save') {
- $('[name=' + kv[0] + ']').val(decodeURIComponent(kv[1]));
+ fixmystreet.offlineData.getForms().then(function(forms) {
+ var savedForm;
+ forms.forEach(function(form) {
+ if (form[0].endsWith(url)) {
+ savedForm = form[1];
}
});
- }
-
- // If we catch the form submit, e.g. Chrome still seems to
- // try and submit and we get the Chrome offline error page
- var btn = $('#report_inspect_form input[type=submit]');
- btn.click(function() {
- var form = $(this).closest('form');
- var data = form.serialize() + '&save=1&saved_at=' + Math.floor(+new Date() / 1000);
- fixmystreet.offlineData.addForm(form.attr('action'), data);
- location.href = '/my/planned?saved=1';
- return false;
- });
- btn[0].type = 'button';
-
- return true;
- }
-
- function replaceImages(selector) {
- $(selector).each(function(i, img) {
- if (img.src.indexOf('/photo/') > -1) {
- var dataImg = localStorage.getItem(img.src);
- if (dataImg) {
- img.src = dataImg;
- }
+ if (savedForm) {
+ savedForm.replace(/\+/g, '%20').split('&').forEach(function(kv) {
+ kv = kv.split('=', 2);
+ if (kv[0] != 'include_update' && kv[0] != 'public_update' && kv[0] != 'save') {
+ $('[name=' + kv[0] + ']').val(decodeURIComponent(kv[1]));
+ }
+ });
}
});
}
return {
- replaceImages: replaceImages,
- showReportFromCache: showReportFromCache,
+ showReportOffline: showReportOffline,
removeReports: removeReports,
updateCachedReports: updateCachedReports
};
})();
+if (!navigator.onLine && location.pathname.indexOf('/report') === 0) {
+ fixmystreet.offline.showReportOffline(location.pathname);
+}
+
if ($('#offline_list').length) {
// We are OFFLINE
- var success = false;
- if (location.pathname.indexOf('/report') === 0) {
- success = fixmystreet.offline.showReportFromCache(location.pathname);
- }
- if (!success) {
- var html = localStorage.getItem('/my/planned');
- if (html) {
- $('#offline_list').before('<h2>'+translation_strings.offline.your_reports+'</h2>');
- $('#offline_list').html(html);
- if (location.search.indexOf('saved=1') > 0) {
- $('#offline_list').before('<p class="form-success">'+translation_strings.offline.update_saved+'</p>');
- }
- fixmystreet.offline.replaceImages('#offline_list img');
- var offlineForms = fixmystreet.offlineData.getForms();
+ idbKeyval.get('/my/planned').then(function(html) {
+ if (!html) { return; }
+ $('#offline_list').before('<h2>'+translation_strings.offline.your_reports+'</h2>');
+ $('#offline_list').html(html);
+ if (location.search.indexOf('saved=1') > 0) {
+ $('#offline_list').before('<p class="form-success">'+translation_strings.offline.update_saved+'</p>');
+ }
+ fixmystreet.offlineData.getForms().then(function(offlineForms) {
var savedForms = {};
offlineForms.forEach(function(form) {
savedForms[form[0]] = 1;
@@ -398,25 +352,21 @@ if ($('#offline_list').length) {
$(this).find('h3').prepend('<em>'+translation_strings.offline.update_data_saved+'</em> ');
}
});
- $('#offline_clear').css('margin-top', '5em').html('<button id="js-clear-localStorage">'+translation_strings.offline.clear_data+'</button>');
- $('#js-clear-localStorage').click(function() {
- if (window.confirm(translation_strings.offline.are_you_sure)) {
- fixmystreet.offline.removeReports(fixmystreet.offlineData.getCachedUrls());
- fixmystreet.offlineData.clearForms();
- localStorage.removeItem('/my/planned');
- alert(translation_strings.offline.data_cleared);
- }
- });
- }
- }
+ });
+ $('#offline_clear').css('margin-top', '5em').html('<button id="js-clear-storage">'+translation_strings.offline.clear_data+'</button>');
+ $('#js-clear-storage').click(function() {
+ if (window.confirm(translation_strings.offline.are_you_sure)) {
+ fixmystreet.offlineData.getCachedReports().then(function(reports) {
+ fixmystreet.offline.removeReports(Object.keys(reports));
+ });
+ fixmystreet.offlineData.clearForms();
+ idbKeyval.del('/my/planned');
+ alert(translation_strings.offline.data_cleared);
+ }
+ });
+ });
fixmystreet.offlineBanner.make(true);
} else {
- // Put the appcache manifest in a page in an iframe so that HTML pages
- // aren't cached (thanks to Jake Archibald for documenting this!)
- if (window.applicationCache && window.localStorage) {
- $(document.body).prepend('<iframe src="/offline/appcache" style="position:absolute;top:-999em;visibility:hidden"></iframe>');
- }
-
fixmystreet.offlineBanner.make(false);
// On /my/planned, when online, cache all shortlisted