Skip to content

Commit

Permalink
feat: registerPushNotifications (#812)
Browse files Browse the repository at this point in the history
* feat: registerPushNotifications

* chore: auto-fix linting and formatting issues

* feat: post message token

* feat: newEntryNotification action

* feat: handle notification click event
  • Loading branch information
DIYgod authored Oct 9, 2024
1 parent 68e6509 commit baab5b9
Show file tree
Hide file tree
Showing 10 changed files with 428 additions and 3 deletions.
1 change: 1 addition & 0 deletions apps/main/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
"@egoist/tipc": "0.3.2",
"@electron-toolkit/preload": "^3.0.1",
"@electron-toolkit/utils": "^3.0.0",
"@eneris/push-receiver": "4.2.0",
"@follow/shared": "workspace:*",
"@mozilla/readability": "^0.5.0",
"@sentry/electron": "5.4.0",
Expand Down
83 changes: 81 additions & 2 deletions apps/main/src/init.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,22 @@
import path from "node:path"

import { registerIpcMain } from "@egoist/tipc/main"
import { getRendererHandlers, registerIpcMain } from "@egoist/tipc/main"
import { PushReceiver } from "@eneris/push-receiver"
import { APP_PROTOCOL } from "@follow/shared/constants"
import { app, nativeTheme, shell } from "electron"
import { env } from "@follow/shared/env"
import type { MessagingData } from "@follow/shared/hono"
import { app, nativeTheme, Notification, shell } from "electron"
import contextMenu from "electron-context-menu"

import { getIconPath } from "./helper"
import { apiClient } from "./lib/api-client"
import { t } from "./lib/i18n"
import { store } from "./lib/store"
import { registerAppMenu } from "./menu"
import type { RendererHandlers } from "./renderer-handlers"
import { initializeSentry } from "./sentry"
import { router } from "./tipc"
import { createMainWindow, getMainWindow } from "./window"

const appFolder = {
prod: "Follow",
Expand Down Expand Up @@ -55,6 +61,8 @@ export const initializeAppStage1 = () => {
// code. You can also put them in separate files and require them here.

registerMenuAndContextMenu()

registerPushNotifications()
}

let contextMenuDisposer: () => void
Expand Down Expand Up @@ -115,3 +123,74 @@ export const registerMenuAndContextMenu = () => {
},
})
}

const registerPushNotifications = async () => {
if (!env.VITE_FIREBASE_CONFIG) {
return
}

const credentialsKey = "notifications-credentials"
const persistentIdsKey = "notifications-persistent-ids"
const credentials = store.get(credentialsKey)
const persistentIds = store.get(persistentIdsKey)

if (credentials) {
apiClient.messaging.$post({
json: {
token: credentials.fcm.token,
channel: "desktop",
},
})
}

const instance = new PushReceiver({
debug: isDev,
firebase: env.VITE_FIREBASE_CONFIG,
persistentIds: persistentIds || [],
credentials,
})

instance.onCredentialsChanged(({ newCredentials }) => {
store.set(credentialsKey, newCredentials)
apiClient.messaging.$post({
json: {
token: newCredentials.fcm.token,
channel: "desktop",
},
})
})

instance.onNotification((notification) => {
const data = notification.message.data as MessagingData
switch (data.type) {
case "new-entry": {
const notification = new Notification({
title: data.title,
body: data.description,
})
notification.on("click", () => {
let mainWindow = getMainWindow()
if (!mainWindow) {
mainWindow = createMainWindow()
}
mainWindow.restore()
mainWindow.focus()
const handlers = getRendererHandlers<RendererHandlers>(mainWindow.webContents)
handlers.navigateEntry.send({
feedId: data.feedId,
entryId: data.entryId,
view: Number.parseInt(data.view),
})
})
notification.show()
break
}
default: {
break
}
}
store.set(persistentIdsKey, instance.persistentIds)
})

await instance.connect()
}
1 change: 1 addition & 0 deletions apps/main/src/renderer-handlers.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export type RendererHandlers = {
invalidateQuery: (key: (string | number | undefined)[]) => void
updateDownloaded: () => void
navigateEntry: (options: { feedId: string; entryId: string; view: number }) => void
}
5 changes: 5 additions & 0 deletions apps/renderer/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { queryClient } from "~/lib/query-client"

import { useAppIsReady } from "./atoms/app"
import { useUISettingKey } from "./atoms/settings/ui"
import { navigateEntry } from "./hooks/biz/useNavigateEntry"
import { applyAfterReadyCallbacks } from "./initialize/queue"
import { appLog } from "./lib/log"
import { cn, getOS } from "./lib/utils"
Expand All @@ -21,6 +22,10 @@ function App() {
})
})

handlers?.navigateEntry.listen((options) => {
navigateEntry(options)
})

return cleanup
}, [])

Expand Down
1 change: 1 addition & 0 deletions apps/renderer/src/models/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ export type ActionsInput = {
summary?: boolean
readability?: boolean
silence?: boolean
newEntryNotification?: boolean
rewriteRules?: {
from: string
to: string
Expand Down
14 changes: 14 additions & 0 deletions apps/renderer/src/modules/settings/action-card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -452,6 +452,20 @@ export function ActionCard({
</div>
<Divider />

<div className="flex w-full items-center justify-between">
<span className="w-0 shrink grow truncate">
{t("actions.action_card.new_entry_notification")}
</span>
<Switch
checked={data.result.newEntryNotification}
onCheckedChange={(checked) => {
data.result.newEntryNotification = checked
onChange(data)
}}
/>
</div>
<Divider />

<div className="flex w-full items-center justify-between">
<span className="w-0 shrink grow truncate">
{t("actions.action_card.silence")}
Expand Down
1 change: 1 addition & 0 deletions locales/settings/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
"actions.action_card.from": "From",
"actions.action_card.generate_summary": "Generate summary using AI",
"actions.action_card.name": "Name",
"actions.action_card.new_entry_notification": "Notification of new entry",
"actions.action_card.no_translation": "No translation",
"actions.action_card.operation_options.contains": "contains",
"actions.action_card.operation_options.does_not_contain": "does not contain",
Expand Down
21 changes: 21 additions & 0 deletions packages/shared/src/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,27 @@ export const env = createEnv({
VITE_SENTRY_DSN: z.string().optional(),
VITE_POSTHOG_KEY: z.string().optional(),
VITE_INBOXES_EMAIL: z.string().default("@follow.re"),
VITE_FIREBASE_CONFIG: z
.string()
.transform((content) => {
try {
return JSON.parse(content)
} catch {
return z.NEVER
}
})
.pipe(
z.object({
apiKey: z.string(),
authDomain: z.string(),
projectId: z.string(),
storageBucket: z.string(),
messagingSenderId: z.string(),
appId: z.string(),
measurementId: z.string(),
}),
)
.optional(),
},

emptyStringAsUndefined: true,
Expand Down
Loading

0 comments on commit baab5b9

Please sign in to comment.