parent
d1975c2021
commit
cd32115871
|
|
@ -109,6 +109,7 @@
|
|||
"use-sync-external-store": "1.2.2",
|
||||
"usehooks-ts": "3.1.0",
|
||||
"vfile": "6.0.3",
|
||||
"web-push": "3.6.7",
|
||||
"zod": "3.23.8",
|
||||
"zustand": "5.0.1"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -10,12 +10,19 @@ import ReactDOM from "react-dom/client"
|
|||
import { RouterProvider } from "react-router/dom"
|
||||
|
||||
import { setAppIsReady } from "./atoms/app"
|
||||
import { ElECTRON_CUSTOM_TITLEBAR_HEIGHT } from "./constants"
|
||||
import { ElECTRON_CUSTOM_TITLEBAR_HEIGHT, isWebBuild } from "./constants"
|
||||
import { initializeApp } from "./initialize"
|
||||
import { registerAppGlobalShortcuts } from "./initialize/global-shortcuts"
|
||||
import { registerWebPushNotifications } from "./push-notification"
|
||||
import { router } from "./router"
|
||||
|
||||
initializeApp().finally(() => {
|
||||
import("./push-notification").then(({ registerWebPushNotifications }) => {
|
||||
registerWebPushNotifications()
|
||||
})
|
||||
if (navigator.serviceWorker && isWebBuild) {
|
||||
registerWebPushNotifications()
|
||||
}
|
||||
setAppIsReady(true)
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,83 @@
|
|||
import { env } from "@follow/shared/env"
|
||||
import { initializeApp } from "firebase/app"
|
||||
import { getMessaging, getToken } from "firebase/messaging"
|
||||
|
||||
import { apiClient } from "./lib/api-fetch"
|
||||
import { router } from "./router"
|
||||
|
||||
const firebaseConfig = env.VITE_FIREBASE_CONFIG ? JSON.parse(env.VITE_FIREBASE_CONFIG) : null
|
||||
|
||||
export async function registerWebPushNotifications() {
|
||||
if (!firebaseConfig) {
|
||||
return
|
||||
}
|
||||
try {
|
||||
const existingRegistration = await navigator.serviceWorker.getRegistration()
|
||||
let registration = existingRegistration
|
||||
|
||||
if (!registration) {
|
||||
registration = await navigator.serviceWorker.register("/sw.js", {
|
||||
scope: "/",
|
||||
})
|
||||
}
|
||||
|
||||
await navigator.serviceWorker.ready
|
||||
|
||||
const app = initializeApp(firebaseConfig)
|
||||
const messaging = getMessaging(app)
|
||||
|
||||
const permission = await Notification.requestPermission()
|
||||
if (permission !== "granted") {
|
||||
throw new Error("Notification permission denied")
|
||||
}
|
||||
|
||||
// get FCM token
|
||||
const token = await getToken(messaging, {
|
||||
serviceWorkerRegistration: registration,
|
||||
})
|
||||
|
||||
await apiClient.messaging.$post({
|
||||
json: {
|
||||
token,
|
||||
channel: "desktop",
|
||||
},
|
||||
})
|
||||
|
||||
registerPushNotificationPostMessage()
|
||||
|
||||
return token
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
throw new TypeError(`Failed to register push notifications: ${error.message}`)
|
||||
}
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
interface NavigateEntryMessage {
|
||||
type: "NOTIFICATION_CLICK"
|
||||
action: "NAVIGATE_ENTRY"
|
||||
data: {
|
||||
feedId: string
|
||||
entryId: string
|
||||
view: number
|
||||
url: string
|
||||
}
|
||||
}
|
||||
|
||||
type ServiceWorkerMessage = NavigateEntryMessage
|
||||
|
||||
const registerPushNotificationPostMessage = () => {
|
||||
navigator.serviceWorker.addEventListener("message", (event) => {
|
||||
const message = event.data as ServiceWorkerMessage
|
||||
|
||||
if (message.type === "NOTIFICATION_CLICK") {
|
||||
switch (message.action) {
|
||||
case "NAVIGATE_ENTRY": {
|
||||
router.navigate(message.data.url)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
@ -6,13 +6,109 @@ import { CacheFirst, NetworkFirst } from "workbox-strategies"
|
|||
|
||||
declare let self: ServiceWorkerGlobalScope
|
||||
|
||||
// 从 Service Worker 的 URL 参数中获取 PWA 状态
|
||||
const isPWA = new URL(self.location.href).searchParams.get("pwa") === "true"
|
||||
|
||||
self.addEventListener("message", (event) => {
|
||||
if (event.data && event.data.type === "SKIP_WAITING") self.skipWaiting()
|
||||
})
|
||||
|
||||
interface NewEntryMessage {
|
||||
description: string
|
||||
entryId: string
|
||||
feedId: string
|
||||
title: string
|
||||
type: "new-entry"
|
||||
view: string
|
||||
}
|
||||
|
||||
type Message = NewEntryMessage
|
||||
|
||||
// Firebase Cloud Messaging handler
|
||||
self.addEventListener("push", (event) => {
|
||||
if (event.data) {
|
||||
const { data } = event.data.json()
|
||||
const payload = data as Message
|
||||
|
||||
switch (payload.type) {
|
||||
case "new-entry": {
|
||||
const notificationPromise = self.registration.showNotification(payload.title, {
|
||||
body: payload.description,
|
||||
icon: "https://app.follow.is/favicon.ico",
|
||||
data: {
|
||||
type: payload.type,
|
||||
feedId: payload.feedId,
|
||||
entryId: payload.entryId,
|
||||
view: Number.parseInt(payload.view),
|
||||
},
|
||||
})
|
||||
event.waitUntil(notificationPromise)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
self.addEventListener("notificationclick", (event) => {
|
||||
event.notification.close()
|
||||
|
||||
const notificationData = event.notification.data
|
||||
if (!notificationData) return
|
||||
|
||||
let urlToOpen: URL
|
||||
|
||||
switch (notificationData.type) {
|
||||
case "new-entry": {
|
||||
urlToOpen = new URL(
|
||||
`/feeds/${notificationData.feedId}/${notificationData.entryId}`,
|
||||
self.location.origin,
|
||||
)
|
||||
break
|
||||
}
|
||||
default: {
|
||||
urlToOpen = new URL("/", self.location.origin)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
const promiseChain = self.clients
|
||||
.matchAll({
|
||||
type: "window",
|
||||
includeUncontrolled: true,
|
||||
})
|
||||
.then((windowClients) => {
|
||||
if (windowClients.length > 0) {
|
||||
const client = windowClients[0]
|
||||
return client.focus()
|
||||
}
|
||||
return self.clients.openWindow(urlToOpen.href)
|
||||
})
|
||||
.then((client) => {
|
||||
if (client && "postMessage" in client) {
|
||||
switch (notificationData.type) {
|
||||
case "new-entry": {
|
||||
client.postMessage({
|
||||
type: "NOTIFICATION_CLICK",
|
||||
action: "NAVIGATE_ENTRY",
|
||||
data: {
|
||||
feedId: notificationData.feedId,
|
||||
entryId: notificationData.entryId,
|
||||
view: notificationData.view,
|
||||
url: urlToOpen.pathname,
|
||||
},
|
||||
})
|
||||
break
|
||||
}
|
||||
default: {
|
||||
console.warn("Unknown notification type:", notificationData.type)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
event.waitUntil(promiseChain)
|
||||
})
|
||||
|
||||
const preCacheExclude = new Set(["og-image.png", "opengraph-image.png"])
|
||||
|
||||
const precacheManifest = self.__WB_MANIFEST.filter((entry) => {
|
||||
|
|
@ -22,7 +118,6 @@ precacheAndRoute(precacheManifest)
|
|||
|
||||
cleanupOutdatedCaches()
|
||||
|
||||
// 根据 PWA 状态选择固定的策略
|
||||
const strategy = isPWA
|
||||
? new CacheFirst({ cacheName: "assets-cache-first" })
|
||||
: new NetworkFirst({
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@
|
|||
"hotfix": "vv -c bump.hotfix.config.js",
|
||||
"lint": "eslint",
|
||||
"lint:fix": "eslint --fix",
|
||||
"mitproxy": "bash scripts/run-proxy.sh",
|
||||
"polyfill-optimize": "pnpx nolyfill install",
|
||||
"prepare": "pnpm exec simple-git-hooks && shx test -f .env || shx cp .env.example .env",
|
||||
"publish": "electron-vite build && electron-forge publish",
|
||||
|
|
|
|||
|
|
@ -669,6 +669,9 @@ importers:
|
|||
vfile:
|
||||
specifier: 6.0.3
|
||||
version: 6.0.3
|
||||
web-push:
|
||||
specifier: 3.6.7
|
||||
version: 3.6.7
|
||||
zod:
|
||||
specifier: 3.23.8
|
||||
version: 3.23.8
|
||||
|
|
@ -4847,6 +4850,9 @@ packages:
|
|||
asn1.js@4.10.1:
|
||||
resolution: {integrity: sha512-p32cOF5q0Zqs9uBiONKYLm6BClCoBCM5O9JfeUSlnQLBTxYdTK+pW+nXflm8UkKd2UYlEbYz5qEi0JuZR9ckSw==}
|
||||
|
||||
asn1.js@5.4.1:
|
||||
resolution: {integrity: sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA==}
|
||||
|
||||
assert-plus@1.0.0:
|
||||
resolution: {integrity: sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==}
|
||||
engines: {node: '>=0.8'}
|
||||
|
|
@ -7291,6 +7297,10 @@ packages:
|
|||
resolution: {integrity: sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg==}
|
||||
engines: {node: '>=10.19.0'}
|
||||
|
||||
http_ece@1.2.0:
|
||||
resolution: {integrity: sha512-JrF8SSLVmcvc5NducxgyOrKXe3EsyHMgBFgSaIUGmArKe+rwr0uphRkRXvwiom3I+fpIfoItveHrfudL8/rxuA==}
|
||||
engines: {node: '>=16'}
|
||||
|
||||
http_ece@1.2.1:
|
||||
resolution: {integrity: sha512-+tzLoMYgXvicu60sVFoswTiu6BiQ6EX3DORRJQ3W2dNpNWCyZ3tcmRFZZ3jgVyw8ziWUCeUARKCkYDY6JgFx+w==}
|
||||
engines: {node: '>=16'}
|
||||
|
|
@ -7779,9 +7789,15 @@ packages:
|
|||
jwa@1.4.1:
|
||||
resolution: {integrity: sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==}
|
||||
|
||||
jwa@2.0.0:
|
||||
resolution: {integrity: sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA==}
|
||||
|
||||
jws@3.2.2:
|
||||
resolution: {integrity: sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==}
|
||||
|
||||
jws@4.0.0:
|
||||
resolution: {integrity: sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==}
|
||||
|
||||
katex@0.16.11:
|
||||
resolution: {integrity: sha512-RQrI8rlHY92OLf3rho/Ts8i/XvjgguEjOkO1BEXcU3N8BqPpSzBNwV/G0Ukr+P/l3ivvJUE/Fa/CwbS6HesGNQ==}
|
||||
hasBin: true
|
||||
|
|
@ -10985,6 +11001,11 @@ packages:
|
|||
web-namespaces@2.0.1:
|
||||
resolution: {integrity: sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==}
|
||||
|
||||
web-push@3.6.7:
|
||||
resolution: {integrity: sha512-OpiIUe8cuGjrj3mMBFWY+e4MMIkW3SVT+7vEIjvD9kejGUypv8GPDf84JdPWskK8zMRIJ6xYGm+Kxr8YkPyA0A==}
|
||||
engines: {node: '>= 16'}
|
||||
hasBin: true
|
||||
|
||||
webidl-conversions@3.0.1:
|
||||
resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==}
|
||||
|
||||
|
|
@ -15803,6 +15824,13 @@ snapshots:
|
|||
inherits: 2.0.4
|
||||
minimalistic-assert: 1.0.1
|
||||
|
||||
asn1.js@5.4.1:
|
||||
dependencies:
|
||||
bn.js: 4.12.0
|
||||
inherits: 2.0.4
|
||||
minimalistic-assert: 1.0.1
|
||||
safer-buffer: 2.1.2
|
||||
|
||||
assert-plus@1.0.0:
|
||||
optional: true
|
||||
|
||||
|
|
@ -18865,6 +18893,8 @@ snapshots:
|
|||
quick-lru: 5.1.1
|
||||
resolve-alpn: 1.2.1
|
||||
|
||||
http_ece@1.2.0: {}
|
||||
|
||||
http_ece@1.2.1: {}
|
||||
|
||||
https-proxy-agent@5.0.1:
|
||||
|
|
@ -19303,11 +19333,22 @@ snapshots:
|
|||
ecdsa-sig-formatter: 1.0.11
|
||||
safe-buffer: 5.2.1
|
||||
|
||||
jwa@2.0.0:
|
||||
dependencies:
|
||||
buffer-equal-constant-time: 1.0.1
|
||||
ecdsa-sig-formatter: 1.0.11
|
||||
safe-buffer: 5.2.1
|
||||
|
||||
jws@3.2.2:
|
||||
dependencies:
|
||||
jwa: 1.4.1
|
||||
safe-buffer: 5.2.1
|
||||
|
||||
jws@4.0.0:
|
||||
dependencies:
|
||||
jwa: 2.0.0
|
||||
safe-buffer: 5.2.1
|
||||
|
||||
katex@0.16.11:
|
||||
dependencies:
|
||||
commander: 8.3.0
|
||||
|
|
@ -22935,6 +22976,16 @@ snapshots:
|
|||
|
||||
web-namespaces@2.0.1: {}
|
||||
|
||||
web-push@3.6.7:
|
||||
dependencies:
|
||||
asn1.js: 5.4.1
|
||||
http_ece: 1.2.0
|
||||
https-proxy-agent: 7.0.5
|
||||
jws: 4.0.0
|
||||
minimist: 1.2.8
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
webidl-conversions@3.0.1: {}
|
||||
|
||||
webidl-conversions@4.0.2: {}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,6 @@
|
|||
from mitmproxy import http
|
||||
|
||||
def request(flow: http.HTTPFlow) -> None:
|
||||
if flow.request.pretty_host == "app.follow.is":
|
||||
flow.request.host = "localhost"
|
||||
flow.request.port = 2233
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
#!/bin/bash
|
||||
|
||||
PROXY_HOST="127.0.0.1"
|
||||
PROXY_PORT="8080"
|
||||
|
||||
enable_proxy() {
|
||||
echo "Enabling proxy on $PROXY_HOST:$PROXY_PORT..."
|
||||
networksetup -listallnetworkservices | grep -v '^*' | while IFS= read -r svc; do
|
||||
sudo networksetup -setwebproxy "$svc" "$PROXY_HOST" "$PROXY_PORT"
|
||||
sudo networksetup -setsecurewebproxy "$svc" "$PROXY_HOST" "$PROXY_PORT"
|
||||
sudo networksetup -setwebproxystate "$svc" on
|
||||
sudo networksetup -setsecurewebproxystate "$svc" on
|
||||
done
|
||||
echo "Proxy enabled."
|
||||
}
|
||||
|
||||
disable_proxy() {
|
||||
echo "Disabling proxy..."
|
||||
networksetup -listallnetworkservices | grep -v '^*' | while IFS= read -r svc; do
|
||||
sudo networksetup -setwebproxystate "$svc" off
|
||||
sudo networksetup -setsecurewebproxystate "$svc" off
|
||||
done
|
||||
echo "Proxy disabled."
|
||||
}
|
||||
|
||||
cleanup() {
|
||||
echo "Exiting..."
|
||||
disable_proxy
|
||||
exit 0
|
||||
}
|
||||
|
||||
trap cleanup SIGINT SIGTERM
|
||||
|
||||
enable_proxy
|
||||
|
||||
echo "Starting mitmproxy..."
|
||||
mitmproxy -s scripts/mitproxy.py --ssl-insecure
|
||||
|
||||
cleanup
|
||||
Loading…
Reference in New Issue