diff --git a/external/lang b/external/lang
index 9406c734ca2cb7b65eefcf926d9e829f9a2056d7..03b206f608b071eb26a099657d9619d32f2bb264 160000
--- a/external/lang
+++ b/external/lang
@@ -1 +1 @@
-Subproject commit 9406c734ca2cb7b65eefcf926d9e829f9a2056d7
+Subproject commit 03b206f608b071eb26a099657d9619d32f2bb264
diff --git a/src/components/common/Tooltip.tsx b/src/components/common/Tooltip.tsx
index 9bf320e57dbc3a33a1f4b41d25ad8adc38f601fd..86be55cc062a28ce1e56b156a52c21178ed1caeb 100644
--- a/src/components/common/Tooltip.tsx
+++ b/src/components/common/Tooltip.tsx
@@ -1,3 +1,5 @@
+import { Text } from "preact-i18n";
+import styled from "styled-components";
 import { Children } from "../../types/Preact";
 import Tippy, { TippyProps } from '@tippyjs/react';
 
@@ -17,3 +19,24 @@ export default function Tooltip(props: Props) {
         </Tippy>
     );
 }
+
+const PermissionTooltipBase = styled.div`
+    display: flex;
+    align-items: center;
+    flex-direction: column;
+
+    code {
+        font-family: 'Fira Mono';
+    }
+`;
+
+export function PermissionTooltip(props: Omit<Props, 'content'> & { permission: string }) {
+    const { permission, ...tooltipProps } = props;
+
+    return (
+        <Tooltip content={<PermissionTooltipBase>
+            <Text id="app.permissions.required" />
+            <code>{ permission }</code>
+        </PermissionTooltipBase>} {...tooltipProps} />
+    )
+}
diff --git a/src/components/common/UpdateIndicator.tsx b/src/components/common/UpdateIndicator.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..982b73dacb51a4011a8f92b8b900914fab0d8696
--- /dev/null
+++ b/src/components/common/UpdateIndicator.tsx
@@ -0,0 +1,25 @@
+import { updateSW } from "../../main";
+import IconButton from "../ui/IconButton";
+import { ThemeContext } from "../../context/Theme";
+import { Download } from "@styled-icons/boxicons-regular";
+import { internalSubscribe } from "../../lib/eventEmitter";
+import { useContext, useEffect, useState } from "preact/hooks";
+
+var pendingUpdate = false;
+
+export default function UpdateIndicator() {
+    const [ pending, setPending ] = useState(pendingUpdate);
+
+    useEffect(() => {
+        return internalSubscribe('PWA', 'update', () => setPending(true));
+    });
+
+    if (!pending) return;
+    const theme = useContext(ThemeContext);
+
+    return (
+        <IconButton onClick={() => updateSW(true)}>
+            <Download size={22} color={theme.success} />
+        </IconButton>
+    )
+}
diff --git a/src/components/common/messaging/MessageBox.tsx b/src/components/common/messaging/MessageBox.tsx
index 404e48e19bb3c644e9d4e5f91312eddcd01af431..a8bd63027c5468386df308e32f6fe5d68a11069f 100644
--- a/src/components/common/messaging/MessageBox.tsx
+++ b/src/components/common/messaging/MessageBox.tsx
@@ -1,6 +1,6 @@
 import { ulid } from "ulid";
 import { Text } from "preact-i18n";
-import Tooltip from "../Tooltip";
+import Tooltip, { PermissionTooltip } from "../Tooltip";
 import { Channel } from "revolt.js";
 import styled from "styled-components";
 import { defer } from "../../../lib/defer";
@@ -90,9 +90,9 @@ function MessageBox({ channel, draft, dispatcher }: Props) {
         return (
             <Base>
                 <Blocked>
-                    <Tooltip content={<div>Permissions Required<div>Send messages</div></div>} placement="top">
+                    <PermissionTooltip permission="SendMessages" placement="top">
                         <ShieldX size={22}/>
-                    </Tooltip>
+                    </PermissionTooltip>
                     <Text id="app.main.channel.misc.no_sending" />
                 </Blocked>
             </Base>
diff --git a/src/lib/eventEmitter.ts b/src/lib/eventEmitter.ts
index 669ad37afef1cfc0405b9efbd654da43eb1ec808..ae0ae52e89ecb75cf3e466503bbe2f8aa50ddfc3 100644
--- a/src/lib/eventEmitter.ts
+++ b/src/lib/eventEmitter.ts
@@ -20,3 +20,4 @@ export function internalEmit(ns: string, event: string, ...args: any[]) {
 // - MessageBox/append
 // - TextArea/focus
 // - ReplyBar/add
+// - PWA/update
diff --git a/src/main.tsx b/src/main.tsx
index fbb4a2bc3ba88ca8639e43eddc29786eb86b3616..02c4364bb37502b066c0f0c4e8ebd3ac1d32b330 100644
--- a/src/main.tsx
+++ b/src/main.tsx
@@ -1,9 +1,9 @@
-import { registerSW } from 'virtual:pwa-register'
-const updateSW = registerSW({
+import { registerSW } from 'virtual:pwa-register';
+import { internalEmit } from './lib/eventEmitter';
+
+export const updateSW = registerSW({
     onNeedRefresh() {
-        // ! FIXME: temp
-        updateSW(true);
-        // show a prompt to user
+        internalEmit('PWA', 'update');
     },
     onOfflineReady() {
         console.info('Ready to work offline.');
diff --git a/src/pages/channels/actions/HeaderActions.tsx b/src/pages/channels/actions/HeaderActions.tsx
index d3f7eeeabd0a67d5b9609b04ec86f006a94714b7..e0bfcdd0fbc516f19c5ca904583171697f60aae4 100644
--- a/src/pages/channels/actions/HeaderActions.tsx
+++ b/src/pages/channels/actions/HeaderActions.tsx
@@ -4,6 +4,7 @@ import { ChannelHeaderProps } from "../ChannelHeader";
 import IconButton from "../../../components/ui/IconButton";
 import { AppContext } from "../../../context/revoltjs/RevoltClient";
 import { isTouchscreenDevice } from "../../../lib/isTouchscreenDevice";
+import UpdateIndicator from "../../../components/common/UpdateIndicator";
 import { useIntermediate } from "../../../context/intermediate/Intermediate";
 import { VoiceContext, VoiceOperationsContext, VoiceStatus } from "../../../context/Voice";
 import { UserPlus, Cog, Sidebar as SidebarIcon, PhoneCall, PhoneOutgoing } from "@styled-icons/boxicons-regular";
@@ -15,6 +16,7 @@ export default function HeaderActions({ channel, toggleSidebar }: ChannelHeaderP
 
     return (
         <>
+            <UpdateIndicator />
             { channel.channel_type === "Group" && (
                 <>
                     <IconButton onClick={() =>