PWA plugin

This commit is contained in:
Aleksander Machniak 2019-09-01 10:42:15 +02:00
parent d429e2dc49
commit 8c9533a273
13 changed files with 462 additions and 0 deletions

19
plugins/pwa/README.md Normal file
View file

@ -0,0 +1,19 @@
The plugin "converts" Roundcube into a Progressive Web Application
which can be "installed" into user device's operating system.
This is proof-of-concept.
CONFIGURATION
-------------
1. Replace images in `/assets` directory to your Company logo. Note, that some
images contain background and some are transpartent. This is to support various
operating systems and the way they use PWAs.
https://realfavicongenerator.net/ will help you with creating images automatically
and choosing some colors.
2. You can also modify `/assets/manifest.json` to your liking.
3. Enable the plugin in Roundcube configuration file.

Binary file not shown.

After

Width:  |  Height:  |  Size: 2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 589 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 851 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View file

@ -0,0 +1,28 @@
{
"name": "Roundcube",
"short_name": "Roundcube",
"description": "Free and Open Source Webmail.",
"lang": "en-US",
"start_url": ".",
"display": "standalone",
"theme_color": "#2e3135",
"background_color": "#2e3135",
"scope": "/",
"icons": [
{
"src": "android-chrome-192x192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "android-chrome-512x512.png",
"sizes": "512x512",
"type": "image/png"
}
],
"permissions": {
"desktop-notification": {
"description": "Needed for notifying you of any changes to your account."
}
}
}

View file

@ -0,0 +1 @@
<svg version="1" xmlns="http://www.w3.org/2000/svg" width="933.333" height="933.333" viewBox="0 0 700.000000 700.000000"><path d="M326.5 1.1c-29.8 3-57.3 11-84.5 24.4-49.5 24.3-91.8 68.7-113.4 119-2.1 4.9-4.5 10.3-5.2 12-1.6 3.8-8.1 25.3-9.5 31.4-1.3 5.6-2.8 14.2-3.5 19.6-.3 2.2-.7 4.9-.9 6-.2 1.1-.4 2.8-.4 3.7-.1 1-3.7 3.8-9.3 7.1-9.1 5.4-10.8 6.3-23.6 13.6-3.4 2-14.4 8.3-24.3 14.1-10 5.8-21 12.1-24.5 14.1-3.6 1.9-7 4.1-7.6 4.7-.7.7-1.5 1.2-1.9 1.2-.4 0-4.6 2.3-9.3 5.1l-8.6 5v215.7l6.1 3.6c3.3 2 6.2 3.6 6.5 3.6.2 0 2.1 1.1 4.2 2.3 2 1.3 5.7 3.5 8.2 4.9 2.5 1.4 13.2 7.6 23.8 13.7 10.6 6.1 20.7 12 22.5 13 1.7 1 17.4 10 34.7 20.1 17.3 10.1 33.1 19.2 35 20.3 1.9 1 15 8.6 29 16.7s27.1 15.7 29 16.8c10.1 5.7 50.3 29 55.5 32.2 3.4 2 6.9 3.9 7.8 4.3.9.3 4.2 2.2 7.2 4.2 3.1 1.9 5.7 3.5 5.9 3.5.3 0 3.7 1.9 7.8 4.3 4 2.4 9.3 5.5 11.8 6.9 2.5 1.4 14.4 8.3 26.5 15.3s23.5 13.6 25.3 14.6l3.3 1.9 12.2-7.1c6.7-4 14.2-8.3 16.7-9.7 4.4-2.5 9.6-5.5 13.5-7.9 1.1-.7 4.3-2.4 7-3.9 2.8-1.4 6.2-3.5 7.7-4.5 1.4-1.1 2.9-1.9 3.2-1.9.3 0 3.4-1.7 6.8-3.8 3.5-2.1 8.3-5 10.8-6.4 3.5-2 36.6-21 66.7-38.4 44.5-25.8 40.4-23.4 67.8-39.2 11-6.3 29.7-17.1 41.5-23.9 11.8-6.9 27.8-16.1 35.5-20.6 7.7-4.4 19.2-11.1 25.5-14.8 6.3-3.6 13.1-7.6 15.1-8.7 1.9-1.1 7.2-4.1 11.7-6.7l8.2-4.8-.2-108-.3-107.9-20.5-11.9c-11.3-6.5-22.7-13.2-25.5-14.8-2.7-1.6-6.6-3.8-8.5-4.9-1.9-1.1-14.7-8.4-28.5-16.4l-24.9-14.5-1.8-11.9c-7.6-51.5-31.1-98.5-67.3-134.9-14.2-14.3-18.9-18.3-34.1-29-30-21.1-65.3-35.3-101.9-41-11.9-1.9-47.5-2.7-60-1.4z"/></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

29
plugins/pwa/composer.json Normal file
View file

@ -0,0 +1,29 @@
{
"name": "kolab/pwa",
"type": "roundcube-plugin",
"description": "COnverts Roundcube into a so-called Progressive Web App for mobile",
"license": "GPLv3+",
"version": "0.1",
"authors": [
{
"name": "Aleksander Machniak",
"email": "machniak@kolabsys.com",
"role": "Lead"
},
{
"name": "Christian Mollekopf",
"email": "mollekopf@kolabsys.com",
"role": "Lead"
}
],
"repositories": [
{
"type": "composer",
"url": "https://plugins.roundcube.net"
}
],
"require": {
"php": ">=5.3.0",
"roundcube/plugin-installer": ">=0.1.3"
}
}

101
plugins/pwa/js/pwa.js Normal file
View file

@ -0,0 +1,101 @@
/**
* PWA plugin engine
*
* @author Christian Mollekopf <mollekopf@kolabsys.com>
*
* @licstart The following is the entire license notice for the
* JavaScript code in this file.
*
* Copyright (C) 2019, Kolab Systems AG <contact@kolabsys.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* @licend The above is the entire license notice
* for the JavaScript code in this file.
*/
/* SERVICE WORKER - REQUIRED */
if ('serviceWorker' in navigator) {
navigator.serviceWorker
.register('?PWA=sw.js')
.then(function(reg) {
console.log("ServiceWorker registered", reg);
})
.catch(function(error) {
console.log("Failed to register ServiceWorker", error);
});
}
function registerOneTimeSync() {
if (navigator.serviceWorker.controller) {
navigator.serviceWorker.ready.then(function(reg) {
if (reg.sync) {
reg.sync.register({
tag: 'oneTimeSync'
})
.then(function(event) {
console.log('Sync registration successful', event);
})
.catch(function(error) {
console.log('Sync registration failed', error);
});
} else {
console.log("One time Sync not supported");
}
});
} else {
console.log("No active ServiceWorker");
}
}
/* OFFLINE BANNER */
function updateOnlineStatus() {
// FIXME fill in something that makes sense in roundcube
// var d = document.body;
// d.className = d.className.replace(/\ offline\b/,'');
// if (!navigator.onLine) {
// d.className += " offline";
// }
}
updateOnlineStatus();
window.addEventListener('load', function() {
window.addEventListener('online', updateOnlineStatus);
window.addEventListener('offline', updateOnlineStatus);
});
/* CHANGE PAGE TITLE BASED ON PAGE VISIBILITY */
function handleVisibilityChange() {
if (document.visibilityState == "hidden") {
document.title = "Hey! Come back!";
} else {
document.title = original_title;
}
}
var original_title = document.title;
document.addEventListener('visibilitychange', handleVisibilityChange, false);
/* NOTIFICATIONS */
window.addEventListener('load', function () {
// At first, let's check if we have permission for notification
// If not, let's ask for it
if (window.Notification && Notification.permission !== "granted") {
Notification.requestPermission(function (status) {
if (Notification.permission !== status) {
Notification.permission = status;
}
});
}
});

100
plugins/pwa/js/sw.js Normal file
View file

@ -0,0 +1,100 @@
/**
* PWA service worker
*
* @author Christian Mollekopf <mollekopf@kolabsys.com>
*
* @licstart The following is the entire license notice for the
* JavaScript code in this file.
*
* Copyright (C) 2019, Kolab Systems AG <contact@kolabsys.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* @licend The above is the entire license notice
* for the JavaScript code in this file.
*/
// Warning: cacheName and assetsToCache vars are set by the PWA plugin
// when sending this file content to the browser
self.addEventListener('sync', function(event) {
if (event.registration.tag == "oneTimeSync") {
console.dir(self.registration);
console.log("One Time Sync Fired");
}
});
self.addEventListener('install', function(event) {
// waitUntil() ensures that the Service Worker will not
// install until the code inside has successfully occurred
event.waitUntil(
// Create cache with the name supplied above and
// return a promise for it
caches.open(cacheName).then(function(cache) {
// Important to `return` the promise here to have `skipWaiting()`
// fire after the cache has been updated.
return cache.addAll(assetsToCache);
}).then(function() {
// `skipWaiting()` forces the waiting ServiceWorker to become the
// active ServiceWorker, triggering the `onactivate` event.
// Together with `Clients.claim()` this allows a worker to take effect
// immediately in the client(s).
return self.skipWaiting();
})
);
});
// Activate event
// Be sure to call self.clients.claim()
self.addEventListener('activate', function(event) {
// `claim()` sets this worker as the active worker for all clients that
// match the workers scope and triggers an `oncontrollerchange` event for
// the clients.
return self.clients.claim();
});
self.addEventListener('fetch', function(event) {
// Ignore non-get request like when accessing the admin panel
// if (event.request.method !== 'GET') { return; }
// Don't try to handle non-secure assets because fetch will fail
if (/http:/.test(event.request.url)) { return; }
// Here's where we cache all the things!
event.respondWith(
// Open the cache created when install
caches.open(cacheName).then(function(cache) {
// Go to the network to ask for that resource
return fetch(event.request).then(function(networkResponse) {
// Add a copy of the response to the cache (updating the old version)
cache.put(event.request, networkResponse.clone());
// Respond with it
return networkResponse;
}).catch(function() {
// If there is no internet connection, try to match the request
// to some of our cached resources
return cache.match(event.request);
})
})
);
});
self.addEventListener('beforeinstallprompt', function(e) {
e.userChoice.then(function(choiceResult) {
if(choiceResult.outcome == 'dismissed') {
alert('User cancelled home screen install');
} else {
alert('User added to home screen');
}
});
});

184
plugins/pwa/pwa.php Normal file
View file

@ -0,0 +1,184 @@
<?php
/**
* "Converts" Roundcube into a Progressive Web Application
*
* @author Aleksander Machniak <machniak@kolabsys.com>
* @author Christian Mollekopf <mollekopf@kolabsys.com>
*
* Copyright (C) 2019, Kolab Systems AG <contact@kolabsys.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
class pwa extends rcube_plugin
{
public $noajax = true;
public $noframe = true;
/** @var string $version Plugin version */
public static $version = '0.1';
/** @var array|null $config Plugin config (from manifest.json) */
private $config;
/**
* Plugin initialization
*/
function init()
{
$this->add_hook('template_object_links', array($this, 'template_object_links'));
$this->add_hook('template_object_meta', array($this, 'template_object_meta'));
$this->include_script('js/pwa.js');
}
/**
* Adds <link> elements to the HTML output
*/
public function template_object_links($args)
{
$config = $this->get_config();
$content = '';
$links = array(
array(
'rel' => 'manifest',
'href' => $this->urlbase . 'assets/manifest.json',
),
array(
'rel' => 'apple-touch-icon',
'sizes' => '180x180',
'href' => $this->urlbase . 'assets/apple-touch-icon.png'
),
array(
'rel' => 'icon',
'type' => 'image/png',
'sizes' => '32x32',
'href' => $this->urlbase . 'assets/favicon-32x32.png'
),
array(
'rel' => 'icon',
'type' => 'image/png',
'sizes' => '16x16',
'href' => $this->urlbase . 'assets/favicon-16x16.png'
),
array(
'rel' => 'mask-icon',
'href' => $this->urlbase . 'assets/safari-pinned-tab.svg',
'color' => $config['theme_color'] ?: '#5bbad5',
),
);
foreach ($links as $link) {
$content .= html::tag('link', $link) . "\n";
}
$args['content'] .= $content;
// replace favicon.ico
$args['content'] = preg_replace(
'/(<link rel="shortcut icon" href=")([^"]+)/',
'\\1' . $this->urlbase . 'assets/favicon.ico',
$args['content']
);
return $args;
}
/**
* Adds <meta> elements to the HTML output
*/
public function template_object_meta($args)
{
$config = $this->get_config();
$meta_content = '';
$meta_list = array(
'apple-mobile-web-app-title' => 'name',
'application-name' => 'name',
'msapplication-TileColor' => 'tile_color',
// todo: theme-color meta is already added by the skin, overwrite?
'theme-color' => 'theme_color',
);
foreach ($meta_list as $name => $opt_name) {
if ($content = $config[$opt_name]) {
$meta_content .= html::tag('meta', array('name' => $name, 'content' => $content)) . "\n";
}
}
$args['content'] .= $meta_content;
return $args;
}
/**
* Hijack HTTP requests to plugin assets e.g. service worker
*/
public static function http_request()
{
// We register service worker file from location specified
// as ?PWA=sw.js. This way we don't need to put it in Roundcube root
// and we can set some javascript variables like cache version, etc.
if ($_GET['PWA'] === 'sw.js') {
$rcube = rcube::get_instance();
$rcube->task = 'pwa';
if ($file = $rcube->find_asset('plugins/pwa/js/sw.js')) {
header('Content-Type: application/javascript');
// TODO: What assets do we want to cache?
// TODO: assets_dir support
$assets = array(
// 'plugins/pwa/assets/manifest.json',
);
echo "var cacheName = 'v" . self::$version . "';\n";
echo "var assetsToCache = " . json_encode($assets) . "\n";
readfile($file);
exit;
}
header('HTTP/1.0 404 PWA plugin error');
exit;
}
}
/**
* Read configuration from manifest.json
*
* @return array Key-value configuration
*/
private function get_config()
{
if (is_array($this->config)) {
return $this->config;
}
$config = array();
$defaults = array(
'tile_color' => '#2d89ef',
'theme_color' => '#2e3135',
);
if ($file = rcube::get_instance()->find_asset('plugins/pwa/assets/manifest.json')) {
$config = json_decode(file_get_contents(INSTALL_PATH . $file), true);
}
return $this->config = array_merge($defaults, $config);
}
}
// Hijack HTTP requests to plugin assets e.g. service worker
pwa::http_request();