Browse Source

change the notification detection to our own variables, and save the selected time

master
Trent Larson 2 days ago
parent
commit
2b6f87f0aa
  1. 56
      src/App.vue
  2. 79
      src/components/PushNotificationPermission.vue
  3. 7
      src/components/UserNameDialog.vue
  4. 3
      src/constants/app.ts
  5. 6
      src/db/tables/settings.ts
  6. 5
      src/router/index.ts
  7. 172
      src/views/AccountViewView.vue
  8. 68
      src/views/HelpNotificationTypesView.vue

56
src/App.vue

@ -229,7 +229,7 @@
? notification.onCancel(stopAsking)
: null;
close(notification.id);
stopAsking = false; // reset value
stopAsking = false; // reset value for next time they open this modal
"
class="block w-full text-center text-md font-bold uppercase bg-slate-600 text-white px-2 py-2 rounded-md"
>
@ -292,7 +292,7 @@
<button
@click="
close(notification.id);
turnOffNotifications();
turnOffNotifications(notification);
"
class="block w-full text-center text-md font-bold uppercase bg-rose-600 text-white px-2 py-2 rounded-md mb-2"
>
@ -318,22 +318,26 @@
<script lang="ts">
import { Vue, Component } from "vue-facing-decorator";
import { logConsoleAndDb } from "@/db/index";
import { db, logConsoleAndDb } from "@/db/index";
import { NotificationIface } from "./constants/app";
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
@Component
export default class App extends Vue {
$notify!: (notification: NotificationIface, timeout?: number) => void;
stopAsking = false;
async turnOffNotifications() {
async turnOffNotifications(notification: NotificationIface) {
let subscription;
const pushProviderSuccess = await navigator.serviceWorker?.ready
const pushProviderSuccess: boolean = await navigator.serviceWorker?.ready
.then((registration) => {
return registration.pushManager.getSubscription();
})
.then((subscript) => {
.then((subscript: PushSubscription | null) => {
subscription = subscript;
if (subscription) {
return subscription.unsubscribe();
if (subscript) {
return subscript.unsubscribe();
} else {
logConsoleAndDb("Subscription object is not available.");
return false;
@ -347,7 +351,7 @@ export default class App extends Vue {
return false;
});
const pushServerSuccess = await fetch("/web-push/unsubscribe", {
const pushServerSuccess: boolean = await fetch("/web-push/unsubscribe", {
method: "POST",
headers: {
"Content-Type": "application/json",
@ -365,14 +369,36 @@ export default class App extends Vue {
return false;
});
alert(
"Notifications are off. Push provider unsubscribe " +
let message;
if (pushProviderSuccess === pushServerSuccess) {
message = "Both local and server notifications ";
if (pushProviderSuccess) {
message += "are off.";
} else {
message += "failed to turn off.";
}
} else {
message =
"Local unsubscribe " +
(pushProviderSuccess ? "succeeded" : "failed") +
(pushProviderSuccess === pushServerSuccess ? " and" : " but") +
" push server unsubscribe " +
" but server unsubscribe " +
(pushServerSuccess ? "succeeded" : "failed") +
".",
);
".";
}
this.$notify({
group: "alert",
type: "info",
title: "Finished",
text: message,
});
await db.open();
await db.settings.update(MASTER_SETTINGS_KEY, {
notifyingNewActivity: false,
});
if (notification.callback) {
notification.callback(pushProviderSuccess && pushServerSuccess);
}
}
}
</script>

79
src/components/PushNotificationPermission.vue

@ -19,8 +19,7 @@
Would you like to be notified of new activity once a day?
</p>
<p v-else class="text-lg mb-4">
Waiting for system initialization, which may take up to 5
seconds...
Waiting for system initialization, which may take up to 5 seconds...
<fa icon="spinner" spin />
</p>
@ -74,7 +73,12 @@
import { Component, Vue } from "vue-facing-decorator";
import { DEFAULT_PUSH_SERVER, NotificationIface } from "@/constants/app";
import { logConsoleAndDb, retrieveSettingsForActiveAccount } from "@/db/index";
import {
db,
logConsoleAndDb,
retrieveSettingsForActiveAccount,
} from "@/db/index";
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
import { urlBase64ToUint8Array } from "@/libs/crypto/vc/util";
import * as libsUtil from "@/libs/util";
@ -108,8 +112,10 @@ interface VapidResponse {
@Component
export default class PushNotificationPermission extends Vue {
$notify!: (notification: NotificationIface, timeout?: number) => void;
// eslint-disable-next-line
$notify!: (notification: NotificationIface, timeout?: number) => Promise<() => void>;
callback: (success: boolean, time: string) => void = () => {};
hourAm = true;
hourInput = "8";
isVisible = false;
@ -117,8 +123,9 @@ export default class PushNotificationPermission extends Vue {
serviceWorkerReady = false;
vapidKey = "";
async open() {
async open(callback?: (success: boolean, time: string) => void) {
this.isVisible = true;
this.callback = callback || this.callback;
try {
const settings = await retrieveSettingsForActiveAccount();
let pushUrl = DEFAULT_PUSH_SERVER;
@ -253,7 +260,15 @@ export default class PushNotificationPermission extends Vue {
private checkNotificationSupport(): Promise<void> {
if (!("Notification" in window)) {
alert("This browser does not support notifications.");
this.$notify(
{
group: "alert",
type: "danger",
title: "Browser Notifications Not Supported",
text: "This browser does not support notifications.",
},
3000,
);
return Promise.reject("This browser does not support notifications.");
}
if (window.Notification.permission === "granted") {
@ -266,9 +281,16 @@ export default class PushNotificationPermission extends Vue {
return window.Notification.requestPermission().then(
(permission: string) => {
if (permission !== "granted") {
alert(
"Allow this app permission to make notifications for personal reminders." +
" You can adjust them at any time in your settings.",
this.$notify(
{
group: "alert",
type: "danger",
title: "Error Requesting Notification Permission",
text:
"Allow this app permission to make notifications for personal reminders." +
" You can adjust them at any time in your settings.",
},
-1,
);
throw new Error("We weren't granted permission.");
}
@ -308,6 +330,7 @@ export default class PushNotificationPermission extends Vue {
}
public async turnOnNotifications() {
let notifyCloser = () => {};
return this.askPermission()
.then((permission) => {
logConsoleAndDb("Permission granted: " + JSON.stringify(permission));
@ -324,7 +347,7 @@ export default class PushNotificationPermission extends Vue {
})
.then(async (subscription) => {
if (subscription) {
await this.$notify(
notifyCloser = await this.$notify(
{
group: "alert",
type: "info",
@ -374,6 +397,7 @@ export default class PushNotificationPermission extends Vue {
"Subscription data sent to server and all finished successfully.",
);
await libsUtil.sendTestThroughPushServer(subscription, true);
notifyCloser();
this.$notify(
{
group: "alert",
@ -383,6 +407,14 @@ export default class PushNotificationPermission extends Vue {
},
-1,
);
const timeText =
// eslint-disable-next-line
this.hourInput + ":" + this.minuteInput + " " + (this.hourAm ? "AM" : "PM");
await db.settings.update(MASTER_SETTINGS_KEY, {
notifyingNewActivity: true,
notifyingNewActivityTime: timeText,
});
this.callback(true, timeText);
})
.catch((error) => {
logConsoleAndDb(
@ -393,7 +425,15 @@ export default class PushNotificationPermission extends Vue {
JSON.stringify(error),
true,
);
alert("Some error occurred setting notification permissions.");
this.$notify(
{
group: "alert",
type: "danger",
title: "Error Setting Notification Permissions",
text: "Could not set notification permissions.",
},
3000,
);
// unsubscribe just in case we failed after getting a subscription
navigator.serviceWorker?.ready
.then((registration) => registration.pushManager.getSubscription())
@ -445,9 +485,16 @@ export default class PushNotificationPermission extends Vue {
);
// Inform the user about the issue
alert(
"We encountered an issue setting up push notifications. " +
"If you wish to revoke notification permissions, please do so in your browser settings.",
this.$notify(
{
group: "alert",
type: "danger",
title: "Error Setting Push Notifications",
text:
"We encountered an issue setting up push notifications. " +
"If you wish to revoke notification permissions, please do so in your browser settings.",
},
-1,
);
reject(error);
@ -458,7 +505,9 @@ export default class PushNotificationPermission extends Vue {
private sendSubscriptionToServer(
subscription: PushSubscriptionWithTime,
): Promise<void> {
logConsoleAndDb("About to send subscription... " + subscription);
logConsoleAndDb(
"About to send subscription... " + JSON.stringify(subscription),
);
return fetch("/web-push/subscribe", {
method: "POST",
headers: {

7
src/components/UserNameDialog.vue

@ -46,11 +46,14 @@ import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
export default class UserNameDialog extends Vue {
$notify!: (notification: NotificationIface, timeout?: number) => void;
callback: (name?: string) => void = () => {};
callback: (name: string) => void = () => {};
givenName = "";
visible = false;
async open(aCallback?: (name?: string) => void) {
/**
* @param aCallback - callback function for name, which may be ""
*/
async open(aCallback?: (name: string) => void) {
this.callback = aCallback || this.callback;
const settings = await retrieveSettingsForActiveAccount();
this.givenName = settings.firstName || "";

3
src/constants/app.ts

@ -52,13 +52,14 @@ export const PASSKEYS_ENABLED =
/**
* The possible values for "group" and "type" are in App.vue.
* From the notiwind package
* Some of this comes from the notiwind package, some is custom.
*/
export interface NotificationIface {
group: string; // "alert" | "modal"
type: string; // "toast" | "info" | "success" | "warning" | "danger"
title: string;
text?: string;
callback?: (success: boolean) => Promise<void>; // if this triggered an action
noText?: string;
onCancel?: (stopAsking?: boolean) => Promise<void>;
onNo?: (stopAsking?: boolean) => Promise<void>;

6
src/db/tables/settings.ts

@ -39,10 +39,12 @@ export type Settings = {
lastNotifiedClaimId?: string;
lastViewedClaimId?: string;
notifyingNewActivity?: boolean; // set if they have turned on daily check for new activity via the push server
notifyingNewActivityTime?: string; // set to their chosen time if they have turned on daily check for new activity via the push server
passkeyExpirationMinutes?: number; // passkey access token time-to-live in minutes
profileImageUrl?: string; // may be null if unwanted for a particular account
reminderTime?: number; // Time in milliseconds since UNIX epoch for reminders
reminderOn?: boolean; // Toggle to enable or disable reminders
// Array of named search boxes defined by bounding boxes
searchBoxes?: Array<{

5
src/router/index.ts

@ -103,6 +103,11 @@ const routes: Array<RouteRecordRaw> = [
name: "help-notifications",
component: () => import("../views/HelpNotificationsView.vue"),
},
{
path: "/help-notification-types",
name: "help-notification-types",
component: () => import("../views/HelpNotificationTypesView.vue"),
},
{
path: "/help-onboarding",
name: "help-onboarding",

172
src/views/AccountViewView.vue

@ -54,7 +54,10 @@
>
<button
@click="
() => $refs.userNameDialog.open((name) => (this.givenName = name))
() =>
(this.$refs.userNameDialog as UserNameDialog).open(
(name) => (this.givenName = name),
)
"
class="inline-block text-md uppercase bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-4 py-2 rounded-md"
>
@ -178,19 +181,25 @@
>
<!-- label -->
<div class="mb-2 font-bold">Notifications</div>
<div
v-if="!notificationMaybeChanged"
class="flex items-center justify-between cursor-pointer"
@click="showNotificationChoice()"
>
<div class="flex items-center justify-between">
<!-- label -->
<div>App Notifications</div>
<div>
New Activity Notifications
<fa
icon="question-circle"
class="text-slate-400 fa-fw ml-2 cursor-pointer"
@click.stop="showNewActivityNotificationInfo"
/>
</div>
<!-- toggle -->
<div class="relative ml-2">
<div
class="relative ml-2 cursor-pointer"
@click="showNewActivityNotificationChoice()"
>
<!-- input -->
<input
type="checkbox"
v-model="isSubscribed"
v-model="notifyingNewActivity"
name="toggleNotificationsInput"
class="sr-only"
/>
@ -202,9 +211,8 @@
></div>
</div>
</div>
<div v-else>
Notification status may have changed. Refresh this page to see the
latest setting.
<div v-if="notifyingNewActivityTime" class="w-full text-right">
{{ notifyingNewActivityTime }}
</div>
<router-link class="pl-4 text-sm text-blue-500" to="/help-notifications">
Troubleshoot your notification setup.
@ -803,10 +811,10 @@ export default class AccountViewView extends Vue {
imageLimits: ImageRateLimits | null = null;
imageServer = "";
isRegistered = false;
isSubscribed = false;
limitsMessage = "";
loadingLimits = false;
notificationMaybeChanged = false;
notifyingNewActivity = false;
notifyingNewActivityTime = "";
passkeyExpirationDescription = "";
passkeyExpirationMinutes = DEFAULT_PASSKEY_EXPIRATION_MINUTES;
previousPasskeyExpirationMinutes = DEFAULT_PASSKEY_EXPIRATION_MINUTES;
@ -849,7 +857,12 @@ export default class AccountViewView extends Vue {
*/
const registration = await navigator.serviceWorker?.ready;
this.subscription = await registration.pushManager.getSubscription();
this.isSubscribed = !!this.subscription;
if (!this.subscription) {
if (this.notifyingNewActivity) {
// the app thought there was a subscription but there isn't, so fix the settings
this.turnOffNotifyingFlags();
}
}
// console.log("Got to the end of 'mounted' call in AccountViewView.");
/**
* Beware! I've seen where we never get to this point because "ready" never resolves.
@ -902,6 +915,8 @@ export default class AccountViewView extends Vue {
this.showContactGives = !!settings.showContactGivesInline;
this.hideRegisterPromptOnNewContact =
!!settings.hideRegisterPromptOnNewContact;
this.notifyingNewActivity = !!settings.notifyingNewActivity;
this.notifyingNewActivityTime = settings.notifyingNewActivityTime || "";
this.passkeyExpirationMinutes =
settings.passkeyExpirationMinutes ?? DEFAULT_PASSKEY_EXPIRATION_MINUTES;
this.previousPasskeyExpirationMinutes = this.passkeyExpirationMinutes;
@ -921,29 +936,44 @@ export default class AccountViewView extends Vue {
.then(() => setTimeout(fn, 2000));
}
toggleShowContactAmounts() {
async toggleShowContactAmounts() {
this.showContactGives = !this.showContactGives;
this.updateShowContactAmounts();
await db.open();
await db.settings.update(MASTER_SETTINGS_KEY, {
showContactGivesInline: this.showContactGives,
});
}
toggleShowGeneralAdvanced() {
async toggleShowGeneralAdvanced() {
this.showGeneralAdvanced = !this.showGeneralAdvanced;
this.updateShowGeneralAdvanced();
await db.open();
await db.settings.update(MASTER_SETTINGS_KEY, {
showGeneralAdvanced: this.showGeneralAdvanced,
});
}
toggleProdWarning() {
async toggleProdWarning() {
this.warnIfProdServer = !this.warnIfProdServer;
this.updateWarnIfProdServer(this.warnIfProdServer);
await db.open();
await db.settings.update(MASTER_SETTINGS_KEY, {
warnIfProdServer: this.warnIfProdServer,
});
}
toggleTestWarning() {
async toggleTestWarning() {
this.warnIfTestServer = !this.warnIfTestServer;
this.updateWarnIfTestServer(this.warnIfTestServer);
await db.open();
await db.settings.update(MASTER_SETTINGS_KEY, {
warnIfTestServer: this.warnIfTestServer,
});
}
toggleShowShortcutBvc() {
async toggleShowShortcutBvc() {
this.showShortcutBvc = !this.showShortcutBvc;
this.updateShowShortcutBvc(this.showShortcutBvc);
await db.open();
await db.settings.update(MASTER_SETTINGS_KEY, {
showShortcutBvc: this.showShortcutBvc,
});
}
readableDate(timeStr: string) {
@ -968,11 +998,39 @@ export default class AccountViewView extends Vue {
}
}
async showNotificationChoice() {
if (!this.subscription) {
async showNewActivityNotificationInfo() {
this.$notify(
{
group: "modal",
type: "confirm",
title: "New Activity Notification",
text: `
This will only notify you when there is new relevant activity for you personally.
Note that it runs on your device and many factors may affect delivery,
so if you want a reliable but simple daily notification then choose a 'Reminder'.
Do you want more details?
`,
onYes: async () => {
await (this.$router as Router).push({
name: "help-notification-types",
});
},
yesText: "tell me more.",
},
-1,
);
}
async showNewActivityNotificationChoice() {
if (!this.notifyingNewActivity) {
(
this.$refs.pushNotificationPermission as PushNotificationPermission
).open();
).open((success: boolean, time: string) => {
if (success) {
this.notifyingNewActivity = true;
this.notifyingNewActivityTime = time;
}
});
} else {
this.$notify(
{
@ -980,55 +1038,16 @@ export default class AccountViewView extends Vue {
type: "notification-off",
title: "", // unused, only here to satisfy type check
text: "", // unused, only here to satisfy type check
callback: async (success) => {
if (success) {
this.notifyingNewActivity = false;
this.notifyingNewActivityTime = "";
}
},
},
-1,
);
}
this.notificationMaybeChanged = true;
}
public async updateShowContactAmounts() {
await db.open();
await db.settings.update(MASTER_SETTINGS_KEY, {
showContactGivesInline: this.showContactGives,
});
}
public async updateShowGeneralAdvanced() {
await db.open();
await db.settings.update(MASTER_SETTINGS_KEY, {
showGeneralAdvanced: this.showGeneralAdvanced,
});
}
public async updateWarnIfProdServer(newSetting: boolean) {
try {
await db.open();
await db.settings.update(MASTER_SETTINGS_KEY, {
warnIfProdServer: newSetting,
});
} catch (err) {
this.$notify(
{
group: "alert",
type: "danger",
title: "Error Updating Prod Warning",
text: "The setting may not have saved. Try again, maybe after restarting the app.",
},
-1,
);
console.error(
"Telling user to try again after prod-server-warning setting update because:",
err,
);
}
}
public async updateWarnIfTestServer(newSetting: boolean) {
await db.open();
await db.settings.update(MASTER_SETTINGS_KEY, {
warnIfTestServer: newSetting,
});
}
public async toggleHideRegisterPromptOnNewContact() {
@ -1049,11 +1068,14 @@ export default class AccountViewView extends Vue {
this.passkeyExpirationDescription = tokenExpiryTimeDescription();
}
public async updateShowShortcutBvc(newSetting: boolean) {
public async turnOffNotifyingFlags() {
await db.open();
await db.settings.update(MASTER_SETTINGS_KEY, {
showShortcutBvc: newSetting,
notifyingNewActivity: false,
notifyingNewActivityTime: "",
});
this.notifyingNewActivity = false;
this.notifyingNewActivityTime = "";
}
/**

68
src/views/HelpNotificationTypesView.vue

@ -0,0 +1,68 @@
<template>
<QuickNav />
<!-- CONTENT -->
<section id="Content" class="p-6 pb-24 max-w-3xl mx-auto">
<!-- Breadcrumb -->
<div class="mb-8">
<!-- Back -->
<div class="text-lg text-center font-light relative px-7">
<h1
class="text-lg text-center px-2 py-1 absolute -left-2 -top-1"
@click="$router.back()"
>
<fa icon="chevron-left" class="fa-fw"></fa>
</h1>
</div>
<!-- Heading -->
<h1 id="ViewHeading" class="text-4xl text-center font-light pt-4 mb-8">
Notification Types
</h1>
</div>
<!-- eslint-disable prettier/prettier -->
<div>
<p>There are two types of notifications:</p>
<h2 class="text-xl font-semibold mt-4">Reminder Notifications</h2>
<div>
<p>
The Reminder Notification will be sent to you daily with a specific message,
at whatever time you choose. Use it to remind
yourself to act, for example: pause and consider who has given you
something, so you can record thanks in here.
</p>
<p>
This is a reliable message, but it doesn't contain any details about
activity that might be especially interesting to you.
</p>
</div>
<h2 class="text-xl font-semibold mt-4">New Activity Notifications</h2>
<div>
<p>
The New Activity Notification will be sent to you when there is new, relevant activity for you.
It will only trigger if something involves you or a project of interest; it will not
bug you for other, general activity.
</p>
<p>
This type is not as reliable as a Reminder Notification because mobile devices often suppress
such notifications to save battery. (We are working on other ways to notify you more
reliably. If you want to quickly check for relevant activity daily, use the Reminder
Notification and open the app and look for a large green button that points out new
activity that is personal to you.)
</p>
</div>
</div>
<!-- eslint-enable -->
</section>
</template>
<script lang="ts">
import { Component, Vue } from "vue-facing-decorator";
import QuickNav from "@/components/QuickNav.vue";
@Component({ components: { QuickNav } })
export default class HelpNotificationTypesView extends Vue {}
</script>
Loading…
Cancel
Save