feat: Installable PWA (#1882)

This commit is contained in:
Tom Moor 2021-02-15 15:19:51 -08:00 committed by GitHub
parent 4b603460cb
commit 7e922d8716
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 1093 additions and 75 deletions

View File

@ -11,7 +11,7 @@ type Props = {|
const Container = styled.div`
width: 100%;
max-width: 100vw;
padding: ${(props) => (props.withStickyHeader ? "4px 20px" : "60px 20px")};
padding: ${(props) => (props.withStickyHeader ? "4px 12px" : "60px 12px")};
${breakpoint("tablet")`
padding: ${(props) => (props.withStickyHeader ? "4px 60px" : "60px")};

View File

@ -163,8 +163,11 @@ const DocumentLink = styled(Link)`
padding: 6px 8px;
border-radius: 8px;
max-height: 50vh;
min-width: 100%;
max-width: calc(100vw - 40px);
width: calc(100vw - 8px);
${breakpoint("tablet")`
width: auto;
`};
${Actions} {
opacity: 0;

View File

@ -7,7 +7,7 @@ import { Helmet } from "react-helmet";
import { withTranslation, type TFunction } from "react-i18next";
import keydown from "react-keydown";
import { Switch, Route, Redirect } from "react-router-dom";
import styled, { withTheme } from "styled-components";
import styled from "styled-components";
import breakpoint from "styled-components-breakpoint";
import AuthStore from "stores/AuthStore";
import DocumentsStore from "stores/DocumentsStore";
@ -24,7 +24,6 @@ import Sidebar from "components/Sidebar";
import SettingsSidebar from "components/Sidebar/Settings";
import SkipNavContent from "components/SkipNavContent";
import SkipNavLink from "components/SkipNavLink";
import { type Theme } from "types";
import { meta } from "utils/keyboard";
import {
homeUrl,
@ -40,7 +39,6 @@ type Props = {
auth: AuthStore,
ui: UiStore,
notifications?: React.Node,
theme: Theme,
i18n: Object,
t: TFunction,
};
@ -51,24 +49,12 @@ class Layout extends React.Component<Props> {
@observable redirectTo: ?string;
@observable keyboardShortcutsOpen: boolean = false;
constructor(props: Props) {
super();
this.updateBackground(props);
}
componentDidUpdate() {
this.updateBackground(this.props);
if (this.redirectTo) {
this.redirectTo = undefined;
}
}
updateBackground(props: Props) {
// ensure the wider page color always matches the theme
window.document.body.style.background = props.theme.background;
}
@keydown(`${meta}+.`)
handleToggleSidebar() {
this.props.ui.toggleCollapsedSidebar();
@ -212,5 +198,5 @@ const Content = styled(Flex)`
`;
export default withTranslation()<Layout>(
inject("auth", "ui", "documents")(withTheme(Layout))
inject("auth", "ui", "documents")(Layout)
);

View File

@ -0,0 +1,35 @@
// @flow
import * as React from "react";
import { useTheme } from "styled-components";
import useStores from "hooks/useStores";
export default function PageTheme() {
const { ui } = useStores();
const theme = useTheme();
React.useEffect(() => {
// wider page background beyond the React root
if (document.body) {
document.body.style.background = theme.background;
}
// theme-color adjusts the title bar color for desktop PWA
const themeElement = document.querySelector('meta[name="theme-color"]');
if (themeElement) {
themeElement.setAttribute("content", theme.background);
}
// status bar color for iOS PWA
const statusElement = document.querySelector(
'meta[name="apple-mobile-web-app-status-bar-style"]'
);
if (statusElement) {
statusElement.setAttribute(
"content",
ui.resolvedTheme === "dark" ? "black-translucent" : "default"
);
}
}, [theme, ui.resolvedTheme]);
return null;
}

View File

@ -46,6 +46,7 @@ export const ToggleButton = styled.button`
`;
export const Positioner = styled.div`
display: none;
z-index: 2;
position: absolute;
top: 0;
@ -56,6 +57,10 @@ export const Positioner = styled.div`
&:hover ${ToggleButton}, &:focus-within ${ToggleButton} {
opacity: 1;
}
${breakpoint("tablet")`
display: block;
`}
`;
export default Toggle;

View File

@ -33,7 +33,7 @@ const Sticky = styled.div`
padding: 0 8px;
background: ${(props) => props.theme.background};
transition: ${(props) => props.theme.backgroundTransition};
z-index: ${(props) => props.theme.depths.stickyHeader};
z-index: 1;
`;
const Subheading = ({ children, ...rest }: Props) => {

View File

@ -19,7 +19,7 @@ const Sticky = styled.div`
padding: 0 8px;
background: ${(props) => props.theme.background};
transition: ${(props) => props.theme.backgroundTransition};
z-index: ${(props) => props.theme.depths.stickyHeader};
z-index: 1;
`;
export const Separator = styled.span`

View File

@ -10,6 +10,7 @@ import { Router } from "react-router-dom";
import { initI18n } from "shared/i18n";
import stores from "stores";
import ErrorBoundary from "components/ErrorBoundary";
import PageTheme from "components/PageTheme";
import ScrollToTop from "components/ScrollToTop";
import Theme from "components/Theme";
import Toasts from "components/Toasts";
@ -19,13 +20,28 @@ import { initSentry } from "utils/sentry";
initI18n();
const element = document.getElementById("root");
const element = window.document.getElementById("root");
const history = createBrowserHistory();
if (env.SENTRY_DSN) {
initSentry(history);
}
if ("serviceWorker" in window.navigator) {
window.addEventListener("load", () => {
window.navigator.serviceWorker
.register("/static/service-worker.js", {
scope: "/",
})
.then((registration) => {
console.log("SW registered: ", registration);
})
.catch((registrationError) => {
console.log("SW registration failed: ", registrationError);
});
});
}
if (element) {
render(
<Provider {...stores}>
@ -34,6 +50,7 @@ if (element) {
<DndProvider backend={HTML5Backend}>
<Router history={history}>
<>
<PageTheme />
<ScrollToTop>
<Routes />
</ScrollToTop>

View File

@ -7,7 +7,6 @@ import { observer, inject } from "mobx-react";
import * as React from "react";
import type { RouterHistory, Match } from "react-router-dom";
import { withRouter } from "react-router-dom";
import { withTheme } from "styled-components";
import parseDocumentSlug from "shared/utils/parseDocumentSlug";
import DocumentsStore from "stores/DocumentsStore";
import PoliciesStore from "stores/PoliciesStore";
@ -22,7 +21,7 @@ import DocumentComponent from "./Document";
import HideSidebar from "./HideSidebar";
import Loading from "./Loading";
import SocketPresence from "./SocketPresence";
import { type LocationWithState, type Theme } from "types";
import { type LocationWithState } from "types";
import { NotFoundError, OfflineError } from "utils/errors";
import { matchDocumentEdit, updateDocumentUrl } from "utils/routeHelpers";
import { isInternalUrl } from "utils/urls";
@ -35,7 +34,6 @@ type Props = {|
policies: PoliciesStore,
revisions: RevisionsStore,
ui: UiStore,
theme: Theme,
history: RouterHistory,
|};
@ -49,7 +47,6 @@ class DataLoader extends React.Component<Props> {
const { documents, match } = this.props;
this.document = documents.getByUrl(match.params.documentSlug);
this.loadDocument();
this.updateBackground();
}
componentDidUpdate(prevProps: Props) {
@ -74,13 +71,6 @@ class DataLoader extends React.Component<Props> {
) {
this.loadRevision();
}
this.updateBackground();
}
updateBackground() {
// ensure the wider page color always matches the theme. This is to
// account for share links which don't sit in the wider Layout component
window.document.body.style.background = this.props.theme.background;
}
get isEditing() {
@ -266,5 +256,5 @@ export default withRouter(
"revisions",
"policies",
"shares"
)(withTheme(DataLoader))
)(DataLoader)
);

View File

@ -480,7 +480,7 @@ const ReferencesWrapper = styled("div")`
const MaxWidth = styled(Flex)`
${(props) =>
props.archived && `* { color: ${props.theme.textSecondary} !important; } `};
padding: 0 16px;
padding: 0 12px;
max-width: 100vw;
width: 100%;

View File

@ -206,11 +206,13 @@
"url-loader": "^0.6.2",
"webpack": "4.44.1",
"webpack-cli": "^3.3.12",
"webpack-manifest-plugin": "^3.0.0"
"webpack-manifest-plugin": "^3.0.0",
"webpack-pwa-manifest": "^4.3.0",
"workbox-webpack-plugin": "^6.1.0"
},
"resolutions": {
"dot-prop": "^5.2.0",
"js-yaml": "^3.13.1"
},
"version": "0.52.0"
}
}

BIN
public/apple-touch-icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

BIN
public/icon-256.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 102 KiB

After

Width:  |  Height:  |  Size: 48 KiB

View File

@ -1,20 +0,0 @@
{
"short_name": "Outline",
"name": "Outline",
"icons": [
{
"src": "/icon-192.png",
"type": "image/png",
"sizes": "192x192"
},
{
"src": "/icon-512.png",
"type": "image/png",
"sizes": "512x512"
}
],
"start_url": "/home?source=pwa",
"background_color": "#FFFFFF",
"display": "standalone",
"theme_color": "#FFFFFF"
}

View File

@ -67,6 +67,8 @@ router.get("/_health", (ctx) => (ctx.body = "OK"));
if (process.env.NODE_ENV === "production") {
router.get("/static/*", async (ctx) => {
ctx.set({
"Service-Worker-Allowed": "/",
"Access-Control-Allow-Origin": "*",
"Cache-Control": `max-age=${356 * 24 * 60 * 60}`,
});

View File

@ -2,8 +2,11 @@
<html lang="en">
<head>
<title>Outline</title>
<meta name="theme-color" content="#FFF" />
<meta name="slack-app-id" content="//inject-slack-app-id//" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<meta name="description" content="A modern team knowledge base for your internal documentation, product specs, support answers, meeting notes, onboarding, &amp; more…">
//inject-prefetch//
<link
@ -12,7 +15,12 @@
href="/favicon-32.png"
sizes="32x32"
/>
<link rel="manifest" href="/manifest.json" crossorigin="use-credentials" />
<link
rel="apple-touch-icon"
type="image/png"
href="/apple-touch-icon.png"
sizes="192x192"
/>
<link
rel="search"
type="application/opensearchdescription+xml"
@ -46,7 +54,9 @@
</script>
<script>
if (window.localStorage && window.localStorage.getItem("theme") === "dark") {
window.document.querySelector("#root").style.background = "#111319";
var color = "#111319";
document.querySelector("#root").style.background = color;
document.querySelector('meta[name="theme-color"]').setAttribute("content", color);
}
</script>
</body>

View File

@ -37,6 +37,21 @@ export default createGlobalStyle`
text-rendering: optimizeLegibility;
}
@media (min-width: ${(props) =>
props.theme.breakpoints.tablet}px) and (display-mode: standalone) {
body:after {
content: "";
display: block;
position: fixed;
top: 0;
left: 0;
right: 0;
height: 1px;
background: ${(props) => props.theme.divider};
z-index: ${(props) => props.theme.depths.pwaSeparator};
}
}
a {
color: ${(props) => props.theme.link};
text-decoration: none;

View File

@ -108,13 +108,13 @@ export const base = {
depths: {
sidebar: 1000,
stickyHeader: 1500,
modalOverlay: 2000,
modal: 3000,
menu: 4000,
toasts: 5000,
loadingIndicatorBar: 6000,
popover: 9000,
pwaSeparator: 10000,
},
};

View File

@ -4,6 +4,8 @@ const webpack = require('webpack');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const { RelativeCiAgentWebpackPlugin } = require('@relative-ci/agent');
const pkg = require("rich-markdown-editor/package.json");
const WebpackPwaManifest = require("webpack-pwa-manifest");
const WorkboxPlugin = require("workbox-webpack-plugin");
require('dotenv').config({ silent: true });
@ -59,6 +61,30 @@ module.exports = {
new HtmlWebpackPlugin({
template: 'server/static/index.html',
}),
new WebpackPwaManifest({
name: "Outline",
short_name: "Outline",
background_color: "#fff",
theme_color: "#fff",
start_url: process.env.URL,
display: "standalone",
icons: [
{
src: path.resolve("public/icon-512.png"),
// For Chrome, you must provide at least a 192x192 pixel icon, and a 512x512 pixel icon.
// If only those two icon sizes are provided, Chrome will automatically scale the icons
// to fit the device. If you'd prefer to scale your own icons, and adjust them for
// pixel-perfection, provide icons in increments of 48dp.
sizes: [512, 192],
purpose: "any maskable",
},
]
}),
new WorkboxPlugin.GenerateSW({
clientsClaim: true,
skipWaiting: true,
maximumFileSizeToCacheInBytes: 15 * 1024 * 1024, // For large bundles
}),
new RelativeCiAgentWebpackPlugin(),
],
stats: {

977
yarn.lock

File diff suppressed because it is too large Load Diff