Browse Source

add minute to notification scheduling & fix a bug, plus other tweaks

Trent Larson 1 week ago
parent
commit
eb675a8ad4
  1. 95
      src/App.vue
  2. 13
      src/db/index.ts
  3. 2
      src/libs/util.ts
  4. 3
      src/views/AccountViewView.vue
  5. 6
      src/views/HelpNotificationsView.vue
  6. 7
      sw_scripts/additional-scripts.js

95
src/App.vue

@ -260,9 +260,16 @@
<span class="mt-2">Yes, tell me at: </span> <span class="mt-2">Yes, tell me at: </span>
<input <input
type="number" type="number"
@change="checkHourInput"
class="rounded-l border border-r-0 border-slate-400 ml-2 mt-2 px-2 py-2 text-center w-20" class="rounded-l border border-r-0 border-slate-400 ml-2 mt-2 px-2 py-2 text-center w-20"
v-model="hourInput" v-model="hourInput"
/> />
<input
type="number"
@change="checkMinuteInput"
class="border border-slate-400 mt-2 px-2 py-2 text-center w-20"
v-model="minuteInput"
/>
<span <span
class="rounded-r border border-slate-400 bg-slate-200 text-center text-blue-500 mt-2 px-2 py-2 w-20" class="rounded-r border border-slate-400 bg-slate-200 text-center text-blue-500 mt-2 px-2 py-2 w-20"
@click="hourAm = !hourAm" @click="hourAm = !hourAm"
@ -377,7 +384,7 @@ import axios from "axios";
import { Vue, Component } from "vue-facing-decorator"; import { Vue, Component } from "vue-facing-decorator";
import { DEFAULT_PUSH_SERVER, NotificationIface } from "@/constants/app"; import { DEFAULT_PUSH_SERVER, NotificationIface } from "@/constants/app";
import { retrieveSettingsForActiveAccount } from "@/db/index"; import { addLogMessage, retrieveSettingsForActiveAccount } from "@/db/index";
import * as libsUtil from "@/libs/util"; import * as libsUtil from "@/libs/util";
import { urlBase64ToUint8Array } from "@/libs/crypto/vc/util"; import { urlBase64ToUint8Array } from "@/libs/crypto/vc/util";
@ -405,7 +412,8 @@ interface VapidResponse {
} }
interface PushSubscriptionWithTime extends PushSubscriptionJSON { interface PushSubscriptionWithTime extends PushSubscriptionJSON {
notifyTime: { utcHour: number }; notifyTime: { utcHour: number; minute: number };
notifyType: string;
} }
@Component @Component
@ -416,6 +424,7 @@ export default class App extends Vue {
b64 = ""; b64 = "";
hourAm = true; hourAm = true;
hourInput = "8"; hourInput = "8";
minuteInput = "00";
serviceWorkerReady = true; serviceWorkerReady = true;
async mounted() { async mounted() {
@ -429,14 +438,19 @@ export default class App extends Vue {
if (pushUrl.startsWith("http://localhost")) { if (pushUrl.startsWith("http://localhost")) {
console.log("Not checking for VAPID in this local environment."); console.log("Not checking for VAPID in this local environment.");
} else { } else {
let responseData = "";
await axios await axios
.get(pushUrl + "/web-push/vapid") .get(pushUrl + "/web-push/vapid")
.then((response: VapidResponse) => { .then((response: VapidResponse) => {
this.b64 = response.data?.vapidKey || ""; this.b64 = response.data?.vapidKey || "";
console.log("Got vapid key:", this.b64); console.log("Got vapid key:", this.b64);
navigator.serviceWorker.addEventListener("controllerchange", () => { responseData = JSON.stringify(response.data);
navigator.serviceWorker?.addEventListener(
"controllerchange",
() => {
console.log("New service worker is now controlling the page"); console.log("New service worker is now controlling the page");
}); },
);
}); });
if (!this.b64) { if (!this.b64) {
this.$notify( this.$notify(
@ -446,7 +460,11 @@ export default class App extends Vue {
title: "Error Setting Notifications", title: "Error Setting Notifications",
text: "Could not set notifications.", text: "Could not set notifications.",
}, },
-1, 5000,
);
await addLogMessage(
"Error Setting Notifications: web push server response didn't have vapidKey: " +
responseData,
); );
} }
} }
@ -462,7 +480,11 @@ export default class App extends Vue {
title: "Error Setting Notifications", title: "Error Setting Notifications",
text: "Got an error setting notifications.", text: "Got an error setting notifications.",
}, },
-1, 5000,
);
await addLogMessage(
"Error Setting Notifications: some error occurred: " +
JSON.stringify(error),
); );
} }
} }
@ -476,7 +498,7 @@ export default class App extends Vue {
message: ServiceWorkerMessage, message: ServiceWorkerMessage,
): Promise<unknown> { ): Promise<unknown> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
if (navigator.serviceWorker.controller) { if (navigator.serviceWorker?.controller) {
const messageChannel = new MessageChannel(); const messageChannel = new MessageChannel();
messageChannel.port1.onmessage = (event: MessageEvent) => { messageChannel.port1.onmessage = (event: MessageEvent) => {
@ -487,7 +509,7 @@ export default class App extends Vue {
} }
}; };
navigator.serviceWorker.controller.postMessage(message, [ navigator.serviceWorker?.controller.postMessage(message, [
messageChannel.port2, messageChannel.port2,
]); ]);
} else { } else {
@ -498,7 +520,9 @@ export default class App extends Vue {
private askPermission(): Promise<NotificationPermission> { private askPermission(): Promise<NotificationPermission> {
console.log("Requesting permission for notifications:", navigator); console.log("Requesting permission for notifications:", navigator);
if (!("serviceWorker" in navigator && navigator.serviceWorker.controller)) { if (
!("serviceWorker" in navigator && navigator.serviceWorker?.controller)
) {
return Promise.reject("Service worker not available."); return Promise.reject("Service worker not available.");
} }
@ -548,6 +572,36 @@ export default class App extends Vue {
}); });
} }
private checkHourInput() {
const hourNum = parseInt(this.hourInput);
if (isNaN(hourNum)) {
this.hourInput = "12";
} else if (hourNum < 1) {
this.hourInput = "12";
this.hourAm = !this.hourAm;
} else if (hourNum > 12) {
this.hourInput = "1";
this.hourAm = !this.hourAm;
} else {
this.hourInput = hourNum.toString();
}
}
private checkMinuteInput() {
const minuteNum = parseInt(this.minuteInput);
if (isNaN(minuteNum)) {
this.minuteInput = "00";
} else if (minuteNum < 0) {
this.minuteInput = "59";
} else if (minuteNum < 10) {
this.minuteInput = "0" + minuteNum;
} else if (minuteNum > 59) {
this.minuteInput = "00";
} else {
this.minuteInput = minuteNum.toString();
}
}
// this allows us to show an error without closing the dialog // this allows us to show an error without closing the dialog
checkHour() { checkHour() {
if (!libsUtil.isNumeric(this.hourInput)) { if (!libsUtil.isNumeric(this.hourInput)) {
@ -617,14 +671,29 @@ export default class App extends Vue {
); );
// we already checked that this is a valid hour number // we already checked that this is a valid hour number
const rawHourNum = libsUtil.numberOrZero(this.hourInput); const rawHourNum = libsUtil.numberOrZero(this.hourInput);
const adjHourNum = rawHourNum + (this.hourAm ? 0 : 12); const adjHourNum = this.hourAm
const hourNum = adjHourNum % 24; ? // If it's AM, then we'll change it to 0 for 12 AM but otherwise use rawHourNum
rawHourNum === 12
? 0
: rawHourNum
: // Otherwise it's PM, so keep a 12 but otherwise add 12
rawHourNum === 12
? 12
: rawHourNum + 12;
const hourNum = adjHourNum % 24; // probably unnecessary now
const utcHour = const utcHour =
hourNum + Math.round(new Date().getTimezoneOffset() / 60); hourNum + Math.round(new Date().getTimezoneOffset() / 60);
const finalUtcHour = (utcHour + (utcHour < 0 ? 24 : 0)) % 24; const finalUtcHour = (utcHour + (utcHour < 0 ? 24 : 0)) % 24;
console.log("utc hour:", utcHour, "final utc:", finalUtcHour);
const minuteNum = libsUtil.numberOrZero(this.minuteInput);
const utcMinute =
minuteNum + Math.round(new Date().getTimezoneOffset() % 60);
const finalUtcMinute =
(utcMinute + (utcMinute < 0 ? 60 : 0)) % 60;
const subscriptionWithTime: PushSubscriptionWithTime = { const subscriptionWithTime: PushSubscriptionWithTime = {
notifyTime: { utcHour: finalUtcHour }, notifyTime: { utcHour: finalUtcHour, minute: finalUtcMinute },
notifyType: "DAILY_CHECK",
...subscription.toJSON(), ...subscription.toJSON(),
}; };
await this.sendSubscriptionToServer(subscriptionWithTime); await this.sendSubscriptionToServer(subscriptionWithTime);
@ -687,7 +756,7 @@ export default class App extends Vue {
applicationServerKey: applicationServerKey, applicationServerKey: applicationServerKey,
}; };
navigator.serviceWorker.ready navigator.serviceWorker?.ready
.then((registration) => { .then((registration) => {
return registration.pushManager.subscribe(options); return registration.pushManager.subscribe(options);
}) })

13
src/db/index.ts

@ -131,3 +131,16 @@ export async function updateAccountSettings(
settings.accountDid = accountDid; settings.accountDid = accountDid;
await updateSettings(settings); await updateSettings(settings);
} }
export async function addLogMessage(message: string): Promise<void> {
await db.open();
const todayKey = new Date().toDateString();
// only keep one day's worth of logs
const previous = await db.logs.get(todayKey);
if (!previous) {
// when this is today's first log, clear out everything previous
await db.logs.clear();
}
const fullMessage = (previous && previous.message) || "";
await db.logs.update(todayKey, { message: fullMessage + "\n" + message });
}

2
src/libs/util.ts

@ -374,8 +374,8 @@ export const sendTestThroughPushServer = async (
pushUrl = settings.webPushServer; pushUrl = settings.webPushServer;
} }
// This is a special value that tells the service worker to send a direct notification to the device, skipping filters.
// This is shared with the service worker and should be a constant. Look for the same name in additional-scripts.js // This is shared with the service worker and should be a constant. Look for the same name in additional-scripts.js
// This is a special value that tells the service worker to send a direct notification to the device, skipping filters.
// Use something other than "Daily Update" https://gitea.anomalistdesign.com/trent_larson/py-push-server/src/commit/3c0e196c11bc98060ec5934e99e7dbd591b5da4d/app.py#L213 // Use something other than "Daily Update" https://gitea.anomalistdesign.com/trent_larson/py-push-server/src/commit/3c0e196c11bc98060ec5934e99e7dbd591b5da4d/app.py#L213
const DIRECT_PUSH_TITLE = "DIRECT_NOTIFICATION"; const DIRECT_PUSH_TITLE = "DIRECT_NOTIFICATION";

3
src/views/AccountViewView.vue

@ -595,6 +595,7 @@
<div id="sectionPartnerServerURL" class="mt-2"> <div id="sectionPartnerServerURL" class="mt-2">
<span class="text-slate-500 text-sm font-bold">Partner Server URL</span> <span class="text-slate-500 text-sm font-bold">Partner Server URL</span>
&nbsp;
<span class="text-sm">{{ DEFAULT_PARTNER_API_SERVER }}</span> <span class="text-sm">{{ DEFAULT_PARTNER_API_SERVER }}</span>
</div> </div>
@ -830,7 +831,7 @@ export default class AccountViewView extends Vue {
/** /**
* Beware! I've seen where this "ready" never resolves. * Beware! I've seen where this "ready" never resolves.
*/ */
const registration = await navigator.serviceWorker.ready; const registration = await navigator.serviceWorker?.ready;
this.subscription = await registration.pushManager.getSubscription(); this.subscription = await registration.pushManager.getSubscription();
this.isSubscribed = !!this.subscription; this.isSubscribed = !!this.subscription;
// console.log("Got to the end of 'mounted' call in AccountViewView."); // console.log("Got to the end of 'mounted' call in AccountViewView.");

6
src/views/HelpNotificationsView.vue

@ -305,8 +305,8 @@ export default class HelpNotificationsView extends Vue {
async mounted() { async mounted() {
try { try {
const registration = await navigator.serviceWorker.ready; const registration = await navigator.serviceWorker?.ready;
const fullSub = await registration.pushManager.getSubscription(); const fullSub = await registration?.pushManager.getSubscription();
this.subscriptionJSON = fullSub?.toJSON(); this.subscriptionJSON = fullSub?.toJSON();
} catch (error) { } catch (error) {
console.error("Mount error:", error); console.error("Mount error:", error);
@ -366,7 +366,7 @@ export default class HelpNotificationsView extends Vue {
showTestNotification() { showTestNotification() {
const TEST_NOTIFICATION_TITLE = "It Worked"; const TEST_NOTIFICATION_TITLE = "It Worked";
navigator.serviceWorker.ready navigator.serviceWorker?.ready
.then((registration) => { .then((registration) => {
return registration.showNotification(TEST_NOTIFICATION_TITLE, { return registration.showNotification(TEST_NOTIFICATION_TITLE, {
body: "This is your test notification.", body: "This is your test notification.",

7
sw_scripts/additional-scripts.js

@ -63,9 +63,9 @@ self.addEventListener("push", function (event) {
// See https://gitea.anomalistdesign.com/trent_larson/py-push-server/src/commit/c1ed026662e754348a5f91542680bd4f57e5b81e/app.py#L217 // See https://gitea.anomalistdesign.com/trent_larson/py-push-server/src/commit/c1ed026662e754348a5f91542680bd4f57e5b81e/app.py#L217
const DAILY_UPDATE_TITLE = "DAILY_CHECK"; const DAILY_UPDATE_TITLE = "DAILY_CHECK";
// This is a special value that tells the service worker to send a direct notification to the device, skipping filters.
// This is shared with the notification-test code and should be a constant. Look for the same name in HelpNotificationsView.vue // This is shared with the notification-test code and should be a constant. Look for the same name in HelpNotificationsView.vue
// Make sure it is something other than the DAILY_UPDATE_TITLE. // This is a special value that tells the service worker to send a direct notification to the device, skipping filters.
// Make sure it is something different from the DAILY_UPDATE_TITLE.
const DIRECT_PUSH_TITLE = "DIRECT_NOTIFICATION"; const DIRECT_PUSH_TITLE = "DIRECT_NOTIFICATION";
let title; let title;
@ -133,7 +133,8 @@ self.addEventListener("notificationclick", (event) => {
// This is invoked when the user chooses this as a share_target, mapped to share-target in the manifest. // This is invoked when the user chooses this as a share_target, mapped to share-target in the manifest.
self.addEventListener("fetch", (event) => { self.addEventListener("fetch", (event) => {
logConsoleAndDb("Service worker got fetch event.", event); // Skipping this because we get so many of them, at startup and other times.
//logConsoleAndDb("Service worker got fetch event.", event);
// Bypass any regular requests not related to Web Share Target // Bypass any regular requests not related to Web Share Target
// and also requests that are not exactly to the timesafari.app // and also requests that are not exactly to the timesafari.app

Loading…
Cancel
Save