Compare commits
9 Commits
dialog-sty
...
notificati
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3eecef6d32 | ||
|
|
9122974fc9 | ||
|
|
fbacf740de | ||
| 810bfa1675 | |||
|
|
cf34952c1f | ||
|
|
7e5c16446f | ||
|
|
d62178bca5 | ||
|
|
1f716100fd | ||
|
|
0493f4f061 |
321
.cursor/rules/component-creation-ideals.mdc
Normal file
321
.cursor/rules/component-creation-ideals.mdc
Normal file
@@ -0,0 +1,321 @@
|
||||
---
|
||||
alwaysApply: true
|
||||
version: "2.0.0"
|
||||
lastUpdated: "2025-08-15"
|
||||
priority: "critical"
|
||||
---
|
||||
# Component Creation Ideals — TimeSafari Architecture (Directive MDC, v2)
|
||||
|
||||
> **Agent role**: Apply these rules when creating, refactoring, or reviewing Vue components (Vue 3 with **vue-facing-decorator**). Prioritize **self-contained** components that hydrate from and persist to **PlatformServiceMixin** settings. Minimize parent–child coupling and prop drilling. Prefer **concision** where a separate component would add more API surface than value.
|
||||
|
||||
## 📚 Cross-References
|
||||
|
||||
- **Migration Example**: See `src/components/NotificationSection.vue` for successful refactor implementation
|
||||
- **Parent Component**: See `src/views/AccountViewView.vue` for before/after comparison
|
||||
- **Settings Infrastructure**: See `src/utils/PlatformServiceMixin.ts` for available methods
|
||||
- **Related Rules**: See `.cursor/rules/` for other architectural guidelines
|
||||
|
||||
## Golden Rules (Enforce)
|
||||
|
||||
### Priority Levels
|
||||
- 🔴 **CRITICAL**: Must be followed - breaking these creates architectural problems
|
||||
- 🟡 **HIGH**: Strongly recommended - important for maintainability
|
||||
- 🟢 **MEDIUM**: Good practice - improves code quality
|
||||
|
||||
1. **Self-Contained Components** 🔴 **CRITICAL**
|
||||
- Do **not** require parent props for internal state or behavior.
|
||||
- Hydrate on `mounted()` via `this.$accountSettings()`; persist with `this.$saveSettings()`.
|
||||
- Encapsulate business logic inside the component; avoid delegating core logic to the parent.
|
||||
- Prefer **computed** getters for derived values; avoid stored duplicates of derived state.
|
||||
|
||||
2. **Settings-First Architecture** 🔴 **CRITICAL**
|
||||
- Use `PlatformServiceMixin` for reading/writing settings. Do **not** introduce new state managers (Pinia, custom stores) unless the state is *truly global*.
|
||||
- Prefer **fetch-on-mount** over passing values via props.
|
||||
|
||||
3. **Single Responsibility** 🟡 **HIGH**
|
||||
- Each component owns **one clear purpose** (UI + related logic + settings persistence). Avoid splitting UI/logic across multiple components unless reusability clearly benefits.
|
||||
|
||||
4. **Internal State Lifecycle** 🟡 **HIGH**
|
||||
- Pattern: **defaults → hydrate on mount → computed for derived → persist on change**.
|
||||
- Handle hydration errors gracefully (keep safe defaults; surface actionable UI states as needed).
|
||||
|
||||
5. **Minimal Props** 🔴 **CRITICAL**
|
||||
- Props are for **pure configuration** (labels, limits, feature flags). Do not pass data that can be loaded internally.
|
||||
- **Never** pass props that mirror settings values (e.g., `isRegistered`, `notifying*`). Load those from settings.
|
||||
|
||||
6. **Communication & Events** 🟡 **HIGH**
|
||||
- Children may emit events for *user interactions* (e.g., `submitted`, `closed`) but **not** to offload core logic to the parent.
|
||||
- Do not emit events solely to persist settings; the child handles persistence.
|
||||
|
||||
7. **Testing** 🟢 **MEDIUM**
|
||||
- Unit tests mount components in isolation; mock `this.$accountSettings`/`this.$saveSettings`.
|
||||
- Verify hydration on mount, persistence on mutation, and graceful failure on settings errors.
|
||||
|
||||
---
|
||||
|
||||
## Concision-First Decision Framework
|
||||
|
||||
> **Goal:** Avoid unnecessary components when a concise script, composable, or helper suffices.
|
||||
|
||||
**Prefer NOT making a component when:**
|
||||
- **One-off UI**: used in exactly one view, unlikely to repeat.
|
||||
- **Small scope**: ~≤100–150 LOC, ≤3 reactive fields, ≤2 handlers.
|
||||
- **Purely presentational** with trivial logic.
|
||||
- **Local invariants**: behavior depends entirely on the view’s context.
|
||||
- **Abstraction cost > benefit**: would create an anemic component with props mirroring parent state.
|
||||
- **Better fit as code reuse**: logic works as a **composable/service/helper** without introducing new UI.
|
||||
|
||||
**Concise alternatives:**
|
||||
- **Composable**: `useFeature()` encapsulates settings I/O and state.
|
||||
- **Service/Module**: plain TS helpers for formatting/validation.
|
||||
- **Directive**: tiny DOM behaviors that don’t need a lifecycle boundary.
|
||||
|
||||
**When to make a component (even without reuse yet):**
|
||||
- **Isolation boundary**: async side effects, permission prompts, or recoverable error states.
|
||||
- **Stateful widget**: internal settings persistence, media controls, complex a11y.
|
||||
- **Slots/composition**: needs flexible children or layout.
|
||||
- **Different change rate**: sub-tree churns independently of the parent.
|
||||
- **Testability/ownership**: clear, ownable surface that’s easier to unit-test in isolation.
|
||||
|
||||
**Rule of Three (guardrail):**
|
||||
- 1st time: inline or composable.
|
||||
- 2nd time: consider shared abstraction.
|
||||
- 3rd time: extract a component.
|
||||
|
||||
**PR language (use when choosing concision):**
|
||||
- “One-off, ~80 LOC, no expected reuse. A component would add an API surface with no consumer. Keeping it local reduces cognitive load. If we see a second usage, promote to a composable; third usage, a component.”
|
||||
- “Logic lives in `useX()` to keep the view concise without prop plumbing; settings stay via `PlatformServiceMixin`.”
|
||||
|
||||
---
|
||||
|
||||
## Anti-Patterns (Reject)
|
||||
|
||||
- Props that duplicate internal/derivable state: `:is-registered`, `:notifying-*`, etc.
|
||||
- Child components that are **UI-only** while parents hold the business logic for that feature.
|
||||
- Introducing Pinia/custom stores for per-component state.
|
||||
- Emitting `update:*` events to push settings responsibilities to parents.
|
||||
- Scattering a feature across multiple micro-components without a clear reuse reason.
|
||||
- Using props for computed/derived values (e.g., `showAdvancedFeatures` as a prop).
|
||||
|
||||
---
|
||||
|
||||
## Quick Examples
|
||||
|
||||
### ✅ DO (Self-Contained, Settings-First)
|
||||
|
||||
```vue
|
||||
<!-- Parent renders with no props -->
|
||||
<NotificationSection />
|
||||
```
|
||||
|
||||
```ts
|
||||
// NotificationSection.vue (vue-facing-decorator + PlatformServiceMixin)
|
||||
import { Component, Vue } from "vue-facing-decorator";
|
||||
import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin";
|
||||
|
||||
@Component({
|
||||
mixins: [PlatformServiceMixin],
|
||||
})
|
||||
export default class NotificationSection extends Vue {
|
||||
private notifyingNewActivity: boolean = false;
|
||||
|
||||
async mounted(): Promise<void> {
|
||||
await this.hydrateFromSettings();
|
||||
}
|
||||
|
||||
private async hydrateFromSettings(): Promise<void> {
|
||||
try {
|
||||
const s = await this.$accountSettings();
|
||||
this.notifyingNewActivity = !!s.notifyingNewActivityTime;
|
||||
} catch (err) {
|
||||
// Keep defaults; optionally surface a non-blocking UI notice
|
||||
}
|
||||
}
|
||||
|
||||
private async updateNotifying(state: boolean): Promise<void> {
|
||||
await this.$saveSettings({ notifyingNewActivityTime: state ? Date.now() : null });
|
||||
this.notifyingNewActivity = state;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### ❌ DON'T (Prop-Driven Coupling)
|
||||
|
||||
```vue
|
||||
<!-- Parent passes internal state down -->
|
||||
<NotificationSection
|
||||
:is-registered="isRegistered"
|
||||
:notifying-new-activity="notifyingNewActivity"
|
||||
/>
|
||||
```
|
||||
|
||||
```ts
|
||||
// Child receives props (anti-pattern)
|
||||
export default class NotificationSection extends Vue {
|
||||
isRegistered: boolean = false;
|
||||
notifyingNewActivity: boolean = false;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Component Structure Template (Drop-In)
|
||||
|
||||
```ts
|
||||
/**
|
||||
* ComponentName.vue — Purpose
|
||||
* Owns: UI + logic + settings persistence (PlatformServiceMixin).
|
||||
*/
|
||||
import { Component, Vue } from "vue-facing-decorator";
|
||||
import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin";
|
||||
import type { Router } from "vue-router";
|
||||
|
||||
@Component({
|
||||
components: {
|
||||
// child components here
|
||||
},
|
||||
mixins: [PlatformServiceMixin], // Settings access
|
||||
})
|
||||
export default class ComponentName extends Vue {
|
||||
// Internal state
|
||||
private myState: boolean = false;
|
||||
private myData: string = "";
|
||||
|
||||
// Derived
|
||||
private get isEnabled(): boolean {
|
||||
return this.myState && this.hasPermissions;
|
||||
}
|
||||
|
||||
async mounted(): Promise<void> {
|
||||
await this.hydrateFromSettings();
|
||||
}
|
||||
|
||||
private async hydrateFromSettings(): Promise<void> {
|
||||
try {
|
||||
const s = await this.$accountSettings();
|
||||
this.myState = !!s.mySetting;
|
||||
this.myData = s.myData ?? "";
|
||||
} catch (err) {
|
||||
// keep defaults
|
||||
}
|
||||
}
|
||||
|
||||
private async updateState(v: boolean): Promise<void> {
|
||||
await this.$saveSettings({ mySetting: v });
|
||||
this.myState = v;
|
||||
}
|
||||
|
||||
async handleUserAction(): Promise<void> {
|
||||
await this.updateState(true);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Migration Playbook (Props → Self-Contained)
|
||||
|
||||
> **Agent, when you see a component receiving `@Prop()` values that mirror settings or internal state, apply this playbook.**
|
||||
|
||||
1. **Detect Anti-Props**
|
||||
- Flag props matching: `is*`, `has*`, `notifying*`, or mirroring settings keys.
|
||||
- Search for parent usage of these props and events.
|
||||
|
||||
2. **Inline State**
|
||||
- Remove anti-props and `@Prop()` declarations.
|
||||
- Add private fields for state inside the child.
|
||||
- Add `mounted()` hydration and persistence helpers via `PlatformServiceMixin`.
|
||||
|
||||
3. **Parent Simplification**
|
||||
- Replace `<Child :foo="..." :bar="..." @update="..."/>` with `<Child />`.
|
||||
- Delete now-unused local state, watchers, and computed values that existed only to feed the child.
|
||||
|
||||
4. **Events**
|
||||
- Keep only user-interaction events (e.g., `submitted`). Remove persistence/logic events that the child now owns.
|
||||
|
||||
5. **Tests**
|
||||
- Update unit tests to mount child in isolation; mock settings I/O.
|
||||
- Verify: hydrate on mount, persist on change, safe fallback on error.
|
||||
|
||||
---
|
||||
|
||||
## When Exceptions Are Acceptable
|
||||
|
||||
- **Truly Global State** across distant components → use Pinia/service *sparingly*.
|
||||
- **Reusable Form Inputs** → accept `value` prop and emit `input`/`update:modelValue`; keep validation/business logic internal.
|
||||
- **Configuration-Only Props** for labels, visual variants, or limits — not for state that can be fetched.
|
||||
|
||||
---
|
||||
|
||||
## Definition of Done (Checklist)
|
||||
|
||||
- [ ] No props required for internal state or settings-backed values.
|
||||
- [ ] Uses `PlatformServiceMixin` for all settings I/O.
|
||||
- [ ] Hydrates on `mounted()`, persists on mutations.
|
||||
- [ ] Single-responsibility: UI + logic + persistence together.
|
||||
- [ ] Computed getters for derived state.
|
||||
- [ ] Unit tests mock settings and cover hydrate/persist/failure paths.
|
||||
- [ ] Parent components contain no leftover `notifying-*` or similar prop wiring.
|
||||
|
||||
---
|
||||
|
||||
## Testing Snippets
|
||||
|
||||
```ts
|
||||
// Hydration on mount
|
||||
test("hydrates from settings on mount", async () => {
|
||||
const wrap = mount(MyComponent);
|
||||
await wrap.vm.$nextTick();
|
||||
expect((wrap.vm as any).myState).toBe(true);
|
||||
});
|
||||
```
|
||||
|
||||
```ts
|
||||
// Mock settings methods
|
||||
jest.spyOn(wrapper.vm as any, "$accountSettings").mockResolvedValue({ mySetting: true });
|
||||
jest.spyOn(wrapper.vm as any, "$saveSettings").mockResolvedValue(void 0);
|
||||
```
|
||||
|
||||
```ts
|
||||
// Graceful failure
|
||||
test("handles settings load failure", async () => {
|
||||
jest.spyOn(wrapper.vm as any, "$accountSettings").mockRejectedValue(new Error("DB Error"));
|
||||
const wrap = mount(MyComponent);
|
||||
await wrap.vm.$nextTick();
|
||||
expect((wrap.vm as any).myState).toBe(false); // default
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Rationale (Short)
|
||||
|
||||
- **Concision first where appropriate** → avoid unnecessary components and API surfaces.
|
||||
- **Reduced coupling** → portability and reuse when boundaries are justified.
|
||||
- **Maintainability** → changes localized to the owning component.
|
||||
- **Consistency** → one canonical path for settings-backed features.
|
||||
|
||||
**Rule of thumb:** _Can this feature operate independently, and does a component materially improve isolation or testability?_ If not, keep it concise (inline or composable).
|
||||
|
||||
---
|
||||
|
||||
## 📝 Version History
|
||||
|
||||
### v2.0.0 (2025-08-15)
|
||||
- **Major Enhancement**: Added Concision-First Decision Framework
|
||||
- **New**: Rule of Three guardrail for component extraction
|
||||
- **New**: PR language guidance for code reviews
|
||||
- **Enhanced**: Agent role includes concision preference
|
||||
- **Refined**: Anti-patterns and examples updated
|
||||
|
||||
### v1.0.0 (2025-08-15)
|
||||
- Initial creation based on successful NotificationSection refactor
|
||||
- Established core architectural principles for self-contained components
|
||||
- Added comprehensive migration playbook and testing guidelines
|
||||
- Included practical examples and anti-pattern detection
|
||||
|
||||
### Future Enhancements
|
||||
- Additional migration patterns for complex components
|
||||
- Integration with automated refactoring tools
|
||||
- Performance benchmarking guidelines
|
||||
- Advanced testing strategies for complex state management
|
||||
@@ -203,4 +203,4 @@ Follow this exact order **after** the Base Contract’s **Objective → Result
|
||||
- Respect Base *Do-Not* (no filler, no invented facts, no censorship).
|
||||
- Prefer clarity over completeness when timeboxed; capture unknowns explicitly.
|
||||
- Apply historical comment management rules (see `.cursor/rules/historical_comment_management.mdc`)
|
||||
- Apply realistic time estimation rules (see `.cursor/rules/realistic_time_estimation.mdc`)
|
||||
- Apply realistic time estimation rules (see `.cursor/rules/realistic_time_estimation.mdc`)
|
||||
|
||||
@@ -14,12 +14,4 @@
|
||||
transform: translateX(100%);
|
||||
background-color: #FFF !important;
|
||||
}
|
||||
|
||||
.dialog-overlay {
|
||||
@apply z-50 fixed inset-0 bg-black/50 flex justify-center items-center p-6;
|
||||
}
|
||||
|
||||
.dialog {
|
||||
@apply bg-white p-4 rounded-lg w-full max-w-lg;
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
<!-- similar to UserNameDialog -->
|
||||
<template>
|
||||
<div v-if="visible" class="dialog-overlay">
|
||||
<div class="dialog">
|
||||
<div v-if="visible" :class="overlayClasses">
|
||||
<div :class="dialogClasses">
|
||||
<h1 :class="titleClasses">{{ title }}</h1>
|
||||
{{ message }}
|
||||
Note that their name is only stored on this device.
|
||||
@@ -61,6 +61,20 @@ export default class ContactNameDialog extends Vue {
|
||||
title = "Contact Name";
|
||||
visible = false;
|
||||
|
||||
/**
|
||||
* CSS classes for the modal overlay backdrop
|
||||
*/
|
||||
get overlayClasses(): string {
|
||||
return "z-index-50 fixed top-0 left-0 right-0 bottom-0 bg-black/50 flex justify-center items-center p-6";
|
||||
}
|
||||
|
||||
/**
|
||||
* CSS classes for the modal dialog container
|
||||
*/
|
||||
get dialogClasses(): string {
|
||||
return "bg-white p-4 rounded-lg w-full max-w-[500px]";
|
||||
}
|
||||
|
||||
/**
|
||||
* CSS classes for the dialog title
|
||||
*/
|
||||
|
||||
@@ -212,8 +212,30 @@ export default class FeedFilters extends Vue {
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.dialog-overlay {
|
||||
z-index: 50;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
#dialogFeedFilters.dialog-overlay {
|
||||
z-index: 100;
|
||||
overflow: scroll;
|
||||
}
|
||||
|
||||
.dialog {
|
||||
background-color: white;
|
||||
padding: 1rem;
|
||||
border-radius: 0.5rem;
|
||||
width: 100%;
|
||||
max-width: 500px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -665,3 +665,27 @@ export default class GiftedDialog extends Vue {
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.dialog-overlay {
|
||||
z-index: 50;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.dialog {
|
||||
background-color: white;
|
||||
padding: 1rem;
|
||||
border-radius: 0.5rem;
|
||||
width: 100%;
|
||||
max-width: 500px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -291,3 +291,27 @@ export default class GivenPrompts extends Vue {
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.dialog-overlay {
|
||||
z-index: 50;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.dialog {
|
||||
background-color: white;
|
||||
padding: 1rem;
|
||||
border-radius: 0.5rem;
|
||||
width: 100%;
|
||||
max-width: 500px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -931,6 +931,32 @@ export default class ImageMethodDialog extends Vue {
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.dialog-overlay {
|
||||
z-index: 50;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.dialog {
|
||||
background-color: white;
|
||||
padding: 1rem;
|
||||
border-radius: 0.5rem;
|
||||
width: 100%;
|
||||
max-width: 700px;
|
||||
max-height: 90vh;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
/* Add styles for diagnostic panel */
|
||||
.diagnostic-panel {
|
||||
font-family: monospace;
|
||||
|
||||
@@ -93,3 +93,27 @@ export default class InviteDialog extends Vue {
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.dialog-overlay {
|
||||
z-index: 50;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.dialog {
|
||||
background-color: white;
|
||||
padding: 1rem;
|
||||
border-radius: 0.5rem;
|
||||
width: 100%;
|
||||
max-width: 500px;
|
||||
}
|
||||
</style>
|
||||
|
||||
194
src/components/NotificationSection.vue
Normal file
194
src/components/NotificationSection.vue
Normal file
@@ -0,0 +1,194 @@
|
||||
<template>
|
||||
<section
|
||||
v-if="isRegistered"
|
||||
id="sectionNotifications"
|
||||
class="bg-slate-100 rounded-md overflow-hidden px-4 py-4 mt-8 mb-8"
|
||||
aria-labelledby="notificationsHeading"
|
||||
>
|
||||
<h2 id="notificationsHeading" class="mb-2 font-bold">Notifications</h2>
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
Reminder Notification
|
||||
<button
|
||||
class="text-slate-400 fa-fw cursor-pointer"
|
||||
aria-label="Learn more about reminder notifications"
|
||||
@click.stop="showReminderNotificationInfo"
|
||||
>
|
||||
<font-awesome-icon icon="question-circle" aria-hidden="true" />
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
class="relative ml-2 cursor-pointer"
|
||||
role="switch"
|
||||
:aria-checked="notifyingReminder"
|
||||
aria-label="Toggle reminder notifications"
|
||||
tabindex="0"
|
||||
@click="showReminderNotificationChoice()"
|
||||
>
|
||||
<!-- input -->
|
||||
<input v-model="notifyingReminder" type="checkbox" class="sr-only" />
|
||||
<!-- line -->
|
||||
<div class="block bg-slate-500 w-14 h-8 rounded-full"></div>
|
||||
<!-- dot -->
|
||||
<div
|
||||
class="dot absolute left-1 top-1 bg-slate-400 w-6 h-6 rounded-full transition"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="notifyingReminder" class="w-full flex justify-between">
|
||||
<span class="ml-8 mr-8">Message: "{{ notifyingReminderMessage }}"</span>
|
||||
<span>{{ notifyingReminderTime.replace(" ", " ") }}</span>
|
||||
</div>
|
||||
<div class="mt-2 flex items-center justify-between">
|
||||
<!-- label -->
|
||||
<div>
|
||||
New Activity Notification
|
||||
<font-awesome-icon
|
||||
icon="question-circle"
|
||||
class="text-slate-400 fa-fw cursor-pointer"
|
||||
@click.stop="showNewActivityNotificationInfo"
|
||||
/>
|
||||
</div>
|
||||
<!-- toggle -->
|
||||
<div
|
||||
class="relative ml-2 cursor-pointer"
|
||||
@click="showNewActivityNotificationChoice()"
|
||||
>
|
||||
<!-- input -->
|
||||
<input v-model="notifyingNewActivity" type="checkbox" class="sr-only" />
|
||||
<!-- line -->
|
||||
<div class="block bg-slate-500 w-14 h-8 rounded-full"></div>
|
||||
<!-- dot -->
|
||||
<div
|
||||
class="dot absolute left-1 top-1 bg-slate-400 w-6 h-6 rounded-full transition"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="notifyingNewActivityTime" class="w-full text-right">
|
||||
{{ notifyingNewActivityTime.replace(" ", " ") }}
|
||||
</div>
|
||||
<div class="mt-2 text-center">
|
||||
<router-link class="text-sm text-blue-500" to="/help-notifications">
|
||||
Troubleshoot your notifications…
|
||||
</router-link>
|
||||
</div>
|
||||
</section>
|
||||
<PushNotificationPermission ref="pushNotificationPermission" />
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Vue } from "vue-facing-decorator";
|
||||
import { Router } from "vue-router";
|
||||
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
|
||||
import PushNotificationPermission from "./PushNotificationPermission.vue";
|
||||
import { createNotifyHelpers } from "@/utils/notify";
|
||||
import { NotificationIface } from "@/constants/app";
|
||||
import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin";
|
||||
import {
|
||||
createNotificationSettingsService,
|
||||
type NotificationSettingsService,
|
||||
} from "@/composables/useNotificationSettings";
|
||||
|
||||
/**
|
||||
* NotificationSection.vue - Notification UI component with service-based logic
|
||||
*
|
||||
* This component handles the UI presentation of notification functionality:
|
||||
* - Reminder notifications with custom messages
|
||||
* - New activity notifications
|
||||
* - Notification permission management
|
||||
* - Help and troubleshooting links
|
||||
*
|
||||
* Business logic is delegated to NotificationSettingsService for:
|
||||
* - Settings hydration and persistence
|
||||
* - Registration status checking
|
||||
* - Notification permission management
|
||||
*
|
||||
* The component maintains its lifecycle boundary while keeping logic separate.
|
||||
*
|
||||
* @author Matthew Raymer
|
||||
* @component NotificationSection
|
||||
* @vue-facing-decorator
|
||||
*/
|
||||
@Component({
|
||||
components: {
|
||||
FontAwesomeIcon,
|
||||
PushNotificationPermission,
|
||||
},
|
||||
mixins: [PlatformServiceMixin],
|
||||
})
|
||||
export default class NotificationSection extends Vue {
|
||||
$notify!: (notification: NotificationIface, timeout?: number) => void;
|
||||
$router!: Router;
|
||||
|
||||
// Notification settings service - handles all business logic
|
||||
private notificationService!: NotificationSettingsService;
|
||||
private notify!: ReturnType<typeof createNotifyHelpers>;
|
||||
|
||||
created() {
|
||||
this.notify = createNotifyHelpers(this.$notify);
|
||||
this.notificationService = createNotificationSettingsService(
|
||||
this,
|
||||
this.notify,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Computed property to determine if user is registered
|
||||
* Reads from the notification service
|
||||
*/
|
||||
private get isRegistered(): boolean {
|
||||
return this.notificationService.isRegistered;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize component state from persisted settings
|
||||
* Called when component is mounted
|
||||
*/
|
||||
async mounted(): Promise<void> {
|
||||
await this.notificationService.hydrateFromSettings();
|
||||
}
|
||||
|
||||
// Computed properties for template access
|
||||
private get notifyingNewActivity(): boolean {
|
||||
return this.notificationService.notifyingNewActivity;
|
||||
}
|
||||
|
||||
private get notifyingNewActivityTime(): string {
|
||||
return this.notificationService.notifyingNewActivityTime;
|
||||
}
|
||||
|
||||
private get notifyingReminder(): boolean {
|
||||
return this.notificationService.notifyingReminder;
|
||||
}
|
||||
|
||||
private get notifyingReminderMessage(): string {
|
||||
return this.notificationService.notifyingReminderMessage;
|
||||
}
|
||||
|
||||
private get notifyingReminderTime(): string {
|
||||
return this.notificationService.notifyingReminderTime;
|
||||
}
|
||||
|
||||
async showNewActivityNotificationInfo(): Promise<void> {
|
||||
await this.notificationService.showNewActivityNotificationInfo(
|
||||
this.$router,
|
||||
);
|
||||
}
|
||||
|
||||
async showNewActivityNotificationChoice(): Promise<void> {
|
||||
await this.notificationService.showNewActivityNotificationChoice(
|
||||
this.$refs.pushNotificationPermission as PushNotificationPermission,
|
||||
);
|
||||
}
|
||||
|
||||
async showReminderNotificationInfo(): Promise<void> {
|
||||
await this.notificationService.showReminderNotificationInfo(this.$router);
|
||||
}
|
||||
|
||||
async showReminderNotificationChoice(): Promise<void> {
|
||||
await this.notificationService.showReminderNotificationChoice(
|
||||
this.$refs.pushNotificationPermission as PushNotificationPermission,
|
||||
);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -312,3 +312,28 @@ export default class OfferDialog extends Vue {
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.dialog-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
z-index: 50;
|
||||
}
|
||||
|
||||
.dialog {
|
||||
background: white;
|
||||
padding: 1.5rem;
|
||||
border-radius: 0.5rem;
|
||||
max-width: 500px;
|
||||
width: 90%;
|
||||
max-height: 90vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -307,3 +307,27 @@ export default class OnboardingDialog extends Vue {
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.dialog-overlay {
|
||||
z-index: 40;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.dialog {
|
||||
background-color: white;
|
||||
padding: 1rem;
|
||||
border-radius: 0.5rem;
|
||||
width: 100%;
|
||||
max-width: 500px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -628,6 +628,34 @@ export default class PhotoDialog extends Vue {
|
||||
</script>
|
||||
|
||||
<style>
|
||||
/* Dialog overlay styling */
|
||||
.dialog-overlay {
|
||||
z-index: 60;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
/* Dialog container styling */
|
||||
.dialog {
|
||||
background-color: white;
|
||||
padding: 1rem;
|
||||
border-radius: 0.5rem;
|
||||
width: 100%;
|
||||
max-width: 700px;
|
||||
max-height: 90vh;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
/* Camera preview styling */
|
||||
.camera-preview {
|
||||
flex: 1;
|
||||
|
||||
@@ -134,3 +134,27 @@ export default class UserNameDialog extends Vue {
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.dialog-overlay {
|
||||
z-index: 50;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.dialog {
|
||||
background-color: white;
|
||||
padding: 1rem;
|
||||
border-radius: 0.5rem;
|
||||
width: 100%;
|
||||
max-width: 500px;
|
||||
}
|
||||
</style>
|
||||
|
||||
233
src/composables/useNotificationSettings.ts
Normal file
233
src/composables/useNotificationSettings.ts
Normal file
@@ -0,0 +1,233 @@
|
||||
/**
|
||||
* useNotificationSettings.ts - Notification settings and permissions service
|
||||
*
|
||||
* This service handles all notification-related business logic including:
|
||||
* - Settings hydration and persistence
|
||||
* - Registration status checking
|
||||
* - Notification permission management
|
||||
* - Settings state management
|
||||
*
|
||||
* Separates business logic from UI components while maintaining
|
||||
* the lifecycle boundary and settings access patterns.
|
||||
*
|
||||
* @author Matthew Raymer
|
||||
* @service NotificationSettingsService
|
||||
*/
|
||||
|
||||
import { createNotifyHelpers } from "@/utils/notify";
|
||||
import { ACCOUNT_VIEW_CONSTANTS } from "@/constants/accountView";
|
||||
import { DAILY_CHECK_TITLE, DIRECT_PUSH_TITLE } from "@/libs/util";
|
||||
import type { ComponentPublicInstance } from "vue";
|
||||
|
||||
/**
|
||||
* Interface for notification settings state
|
||||
*/
|
||||
export interface NotificationSettingsState {
|
||||
isRegistered: boolean;
|
||||
notifyingNewActivity: boolean;
|
||||
notifyingNewActivityTime: string;
|
||||
notifyingReminder: boolean;
|
||||
notifyingReminderMessage: string;
|
||||
notifyingReminderTime: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Interface for notification settings actions
|
||||
*/
|
||||
export interface NotificationSettingsActions {
|
||||
hydrateFromSettings: () => Promise<void>;
|
||||
updateNewActivityNotification: (
|
||||
enabled: boolean,
|
||||
timeText?: string,
|
||||
) => Promise<void>;
|
||||
updateReminderNotification: (
|
||||
enabled: boolean,
|
||||
timeText?: string,
|
||||
message?: string,
|
||||
) => Promise<void>;
|
||||
showNewActivityNotificationInfo: (router: any) => Promise<void>;
|
||||
showReminderNotificationInfo: (router: any) => Promise<void>;
|
||||
showNewActivityNotificationChoice: (
|
||||
pushNotificationPermissionRef: any,
|
||||
) => Promise<void>;
|
||||
showReminderNotificationChoice: (
|
||||
pushNotificationPermissionRef: any,
|
||||
) => Promise<void>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Service class for managing notification settings and permissions
|
||||
*
|
||||
* @param platformService - PlatformServiceMixin instance for settings access
|
||||
* @param notify - Notification helper functions
|
||||
*/
|
||||
export class NotificationSettingsService
|
||||
implements NotificationSettingsState, NotificationSettingsActions
|
||||
{
|
||||
// State properties
|
||||
public isRegistered: boolean = false;
|
||||
public notifyingNewActivity: boolean = false;
|
||||
public notifyingNewActivityTime: string = "";
|
||||
public notifyingReminder: boolean = false;
|
||||
public notifyingReminderMessage: string = "";
|
||||
public notifyingReminderTime: string = "";
|
||||
|
||||
constructor(
|
||||
private platformService: ComponentPublicInstance & {
|
||||
$accountSettings: () => Promise<any>;
|
||||
$saveSettings: (changes: any) => Promise<boolean>;
|
||||
},
|
||||
private notify: ReturnType<typeof createNotifyHelpers>,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Load notification settings from database and hydrate internal state
|
||||
* Uses the existing settings mechanism for consistency
|
||||
*/
|
||||
public async hydrateFromSettings(): Promise<void> {
|
||||
try {
|
||||
const settings = await this.platformService.$accountSettings();
|
||||
|
||||
// Hydrate registration status
|
||||
this.isRegistered = !!settings?.isRegistered;
|
||||
|
||||
// Hydrate boolean flags from time presence
|
||||
this.notifyingNewActivity = !!settings.notifyingNewActivityTime;
|
||||
this.notifyingNewActivityTime = settings.notifyingNewActivityTime || "";
|
||||
this.notifyingReminder = !!settings.notifyingReminderTime;
|
||||
this.notifyingReminderMessage = settings.notifyingReminderMessage || "";
|
||||
this.notifyingReminderTime = settings.notifyingReminderTime || "";
|
||||
} catch (error) {
|
||||
console.error("Failed to hydrate notification settings:", error);
|
||||
// Keep default values on error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update new activity notification settings
|
||||
*/
|
||||
public async updateNewActivityNotification(
|
||||
enabled: boolean,
|
||||
timeText: string = "",
|
||||
): Promise<void> {
|
||||
await this.platformService.$saveSettings({
|
||||
notifyingNewActivityTime: timeText,
|
||||
});
|
||||
this.notifyingNewActivity = enabled;
|
||||
this.notifyingNewActivityTime = timeText;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update reminder notification settings
|
||||
*/
|
||||
public async updateReminderNotification(
|
||||
enabled: boolean,
|
||||
timeText: string = "",
|
||||
message: string = "",
|
||||
): Promise<void> {
|
||||
await this.platformService.$saveSettings({
|
||||
notifyingReminderMessage: message,
|
||||
notifyingReminderTime: timeText,
|
||||
});
|
||||
this.notifyingReminder = enabled;
|
||||
this.notifyingReminderMessage = message;
|
||||
this.notifyingReminderTime = timeText;
|
||||
}
|
||||
|
||||
/**
|
||||
* Show new activity notification info dialog
|
||||
*/
|
||||
public async showNewActivityNotificationInfo(router: any): Promise<void> {
|
||||
this.notify.confirm(
|
||||
ACCOUNT_VIEW_CONSTANTS.NOTIFICATIONS.NEW_ACTIVITY_INFO,
|
||||
async () => {
|
||||
await router.push({
|
||||
name: "help-notification-types",
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Show reminder notification info dialog
|
||||
*/
|
||||
public async showReminderNotificationInfo(router: any): Promise<void> {
|
||||
this.notify.confirm(
|
||||
ACCOUNT_VIEW_CONSTANTS.NOTIFICATIONS.REMINDER_INFO,
|
||||
async () => {
|
||||
await router.push({
|
||||
name: "help-notification-types",
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle new activity notification choice (enable/disable)
|
||||
*/
|
||||
public async showNewActivityNotificationChoice(
|
||||
pushNotificationPermissionRef: any,
|
||||
): Promise<void> {
|
||||
if (!this.notifyingNewActivity) {
|
||||
// Enable notification
|
||||
pushNotificationPermissionRef.open(
|
||||
DAILY_CHECK_TITLE,
|
||||
async (success: boolean, timeText: string) => {
|
||||
if (success) {
|
||||
await this.updateNewActivityNotification(true, timeText);
|
||||
}
|
||||
},
|
||||
);
|
||||
} else {
|
||||
// Disable notification
|
||||
this.notify.notificationOff(DAILY_CHECK_TITLE, async (success) => {
|
||||
if (success) {
|
||||
await this.updateNewActivityNotification(false);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle reminder notification choice (enable/disable)
|
||||
*/
|
||||
public async showReminderNotificationChoice(
|
||||
pushNotificationPermissionRef: any,
|
||||
): Promise<void> {
|
||||
if (!this.notifyingReminder) {
|
||||
// Enable notification
|
||||
pushNotificationPermissionRef.open(
|
||||
DIRECT_PUSH_TITLE,
|
||||
async (success: boolean, timeText: string, message?: string) => {
|
||||
if (success) {
|
||||
await this.updateReminderNotification(true, timeText, message);
|
||||
}
|
||||
},
|
||||
);
|
||||
} else {
|
||||
// Disable notification
|
||||
this.notify.notificationOff(DIRECT_PUSH_TITLE, async (success) => {
|
||||
if (success) {
|
||||
await this.updateReminderNotification(false);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Factory function to create a NotificationSettingsService instance
|
||||
*
|
||||
* @param platformService - PlatformServiceMixin instance
|
||||
* @param notify - Notification helper functions
|
||||
* @returns NotificationSettingsService instance
|
||||
*/
|
||||
export function createNotificationSettingsService(
|
||||
platformService: ComponentPublicInstance & {
|
||||
$accountSettings: () => Promise<any>;
|
||||
$saveSettings: (changes: any) => Promise<boolean>;
|
||||
},
|
||||
notify: ReturnType<typeof createNotifyHelpers>,
|
||||
): NotificationSettingsService {
|
||||
return new NotificationSettingsService(platformService, notify);
|
||||
}
|
||||
@@ -60,91 +60,7 @@
|
||||
@share-info="onShareInfo"
|
||||
/>
|
||||
|
||||
<!-- Notifications -->
|
||||
<!-- Currently disabled because it doesn't work, even on Chrome.
|
||||
If restored, make sure it works or doesn't show on mobile/electron. -->
|
||||
<section
|
||||
v-if="false"
|
||||
id="sectionNotifications"
|
||||
class="bg-slate-100 rounded-md overflow-hidden px-4 py-4 mt-8 mb-8"
|
||||
aria-labelledby="notificationsHeading"
|
||||
>
|
||||
<h2 id="notificationsHeading" class="mb-2 font-bold">Notifications</h2>
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
Reminder Notification
|
||||
<button
|
||||
class="text-slate-400 fa-fw cursor-pointer"
|
||||
aria-label="Learn more about reminder notifications"
|
||||
@click.stop="showReminderNotificationInfo"
|
||||
>
|
||||
<font-awesome
|
||||
icon="question-circle"
|
||||
aria-hidden="true"
|
||||
></font-awesome>
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
class="relative ml-2 cursor-pointer"
|
||||
role="switch"
|
||||
:aria-checked="notifyingReminder"
|
||||
aria-label="Toggle reminder notifications"
|
||||
tabindex="0"
|
||||
@click="showReminderNotificationChoice()"
|
||||
>
|
||||
<!-- input -->
|
||||
<input v-model="notifyingReminder" type="checkbox" class="sr-only" />
|
||||
<!-- line -->
|
||||
<div class="block bg-slate-500 w-14 h-8 rounded-full"></div>
|
||||
<!-- dot -->
|
||||
<div
|
||||
class="dot absolute left-1 top-1 bg-slate-400 w-6 h-6 rounded-full transition"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="notifyingReminder" class="w-full flex justify-between">
|
||||
<span class="ml-8 mr-8">Message: "{{ notifyingReminderMessage }}"</span>
|
||||
<span>{{ notifyingReminderTime.replace(" ", " ") }}</span>
|
||||
</div>
|
||||
<div class="mt-2 flex items-center justify-between">
|
||||
<!-- label -->
|
||||
<div>
|
||||
New Activity Notification
|
||||
<font-awesome
|
||||
icon="question-circle"
|
||||
class="text-slate-400 fa-fw cursor-pointer"
|
||||
@click.stop="showNewActivityNotificationInfo"
|
||||
/>
|
||||
</div>
|
||||
<!-- toggle -->
|
||||
<div
|
||||
class="relative ml-2 cursor-pointer"
|
||||
@click="showNewActivityNotificationChoice()"
|
||||
>
|
||||
<!-- input -->
|
||||
<input
|
||||
v-model="notifyingNewActivity"
|
||||
type="checkbox"
|
||||
class="sr-only"
|
||||
/>
|
||||
<!-- line -->
|
||||
<div class="block bg-slate-500 w-14 h-8 rounded-full"></div>
|
||||
<!-- dot -->
|
||||
<div
|
||||
class="dot absolute left-1 top-1 bg-slate-400 w-6 h-6 rounded-full transition"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="notifyingNewActivityTime" class="w-full text-right">
|
||||
{{ notifyingNewActivityTime.replace(" ", " ") }}
|
||||
</div>
|
||||
<div class="mt-2 text-center">
|
||||
<router-link class="text-sm text-blue-500" to="/help-notifications">
|
||||
Troubleshoot your notifications…
|
||||
</router-link>
|
||||
</div>
|
||||
</section>
|
||||
<PushNotificationPermission ref="pushNotificationPermission" />
|
||||
<NotificationSection />
|
||||
|
||||
<LocationSearchSection :search-box="searchBox" />
|
||||
|
||||
@@ -765,12 +681,13 @@ import { Capacitor } from "@capacitor/core";
|
||||
|
||||
import EntityIcon from "../components/EntityIcon.vue";
|
||||
import ImageMethodDialog from "../components/ImageMethodDialog.vue";
|
||||
import PushNotificationPermission from "../components/PushNotificationPermission.vue";
|
||||
|
||||
import QuickNav from "../components/QuickNav.vue";
|
||||
import TopMessage from "../components/TopMessage.vue";
|
||||
import UserNameDialog from "../components/UserNameDialog.vue";
|
||||
import DataExportSection from "../components/DataExportSection.vue";
|
||||
import IdentitySection from "@/components/IdentitySection.vue";
|
||||
import NotificationSection from "@/components/NotificationSection.vue";
|
||||
import RegistrationNotice from "@/components/RegistrationNotice.vue";
|
||||
import LocationSearchSection from "@/components/LocationSearchSection.vue";
|
||||
import UsageLimitsSection from "@/components/UsageLimitsSection.vue";
|
||||
@@ -796,11 +713,7 @@ import {
|
||||
getHeaders,
|
||||
tokenExpiryTimeDescription,
|
||||
} from "../libs/endorserServer";
|
||||
import {
|
||||
DAILY_CHECK_TITLE,
|
||||
DIRECT_PUSH_TITLE,
|
||||
retrieveAccountMetadata,
|
||||
} from "../libs/util";
|
||||
import { retrieveAccountMetadata } from "../libs/util";
|
||||
import { logger } from "../utils/logger";
|
||||
import { PlatformServiceMixin } from "../utils/PlatformServiceMixin";
|
||||
import { createNotifyHelpers, TIMEOUTS } from "@/utils/notify";
|
||||
@@ -829,7 +742,7 @@ interface UserNameDialogRef {
|
||||
LMap,
|
||||
LMarker,
|
||||
LTileLayer,
|
||||
PushNotificationPermission,
|
||||
NotificationSection,
|
||||
QuickNav,
|
||||
TopMessage,
|
||||
UserNameDialog,
|
||||
@@ -880,12 +793,7 @@ export default class AccountViewView extends Vue {
|
||||
includeUserProfileLocation: boolean = false;
|
||||
savingProfile: boolean = false;
|
||||
|
||||
// Notification properties
|
||||
notifyingNewActivity: boolean = false;
|
||||
notifyingNewActivityTime: string = "";
|
||||
notifyingReminder: boolean = false;
|
||||
notifyingReminderMessage: string = "";
|
||||
notifyingReminderTime: string = "";
|
||||
// Push notification subscription (kept for service worker checks)
|
||||
subscription: PushSubscription | null = null;
|
||||
|
||||
// UI state properties
|
||||
@@ -1003,10 +911,8 @@ export default class AccountViewView extends Vue {
|
||||
const registration = await navigator.serviceWorker?.ready;
|
||||
this.subscription = await registration.pushManager.getSubscription();
|
||||
if (!this.subscription) {
|
||||
if (this.notifyingNewActivity || this.notifyingReminder) {
|
||||
// the app thought there was a subscription but there isn't, so fix the settings
|
||||
this.turnOffNotifyingFlags();
|
||||
}
|
||||
// Notification settings cleanup is now handled by the NotificationSection component
|
||||
// which manages its own state lifecycle and persistence
|
||||
}
|
||||
} catch (error) {
|
||||
this.notify.warning(
|
||||
@@ -1046,11 +952,6 @@ export default class AccountViewView extends Vue {
|
||||
this.isRegistered = !!settings?.isRegistered;
|
||||
this.isSearchAreasSet = !!settings.searchBoxes;
|
||||
this.searchBox = settings.searchBoxes?.[0] || null;
|
||||
this.notifyingNewActivity = !!settings.notifyingNewActivityTime;
|
||||
this.notifyingNewActivityTime = settings.notifyingNewActivityTime || "";
|
||||
this.notifyingReminder = !!settings.notifyingReminderTime;
|
||||
this.notifyingReminderMessage = settings.notifyingReminderMessage || "";
|
||||
this.notifyingReminderTime = settings.notifyingReminderTime || "";
|
||||
this.partnerApiServer = settings.partnerApiServer || this.partnerApiServer;
|
||||
this.partnerApiServerInput =
|
||||
settings.partnerApiServer || this.partnerApiServerInput;
|
||||
@@ -1132,87 +1033,6 @@ export default class AccountViewView extends Vue {
|
||||
}
|
||||
}
|
||||
|
||||
async showNewActivityNotificationInfo(): Promise<void> {
|
||||
this.notify.confirm(
|
||||
ACCOUNT_VIEW_CONSTANTS.NOTIFICATIONS.NEW_ACTIVITY_INFO,
|
||||
async () => {
|
||||
await (this.$router as Router).push({
|
||||
name: "help-notification-types",
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
async showNewActivityNotificationChoice(): Promise<void> {
|
||||
if (!this.notifyingNewActivity) {
|
||||
(
|
||||
this.$refs.pushNotificationPermission as PushNotificationPermission
|
||||
).open(DAILY_CHECK_TITLE, async (success: boolean, timeText: string) => {
|
||||
if (success) {
|
||||
await this.$saveSettings({
|
||||
notifyingNewActivityTime: timeText,
|
||||
});
|
||||
this.notifyingNewActivity = true;
|
||||
this.notifyingNewActivityTime = timeText;
|
||||
}
|
||||
});
|
||||
} else {
|
||||
this.notify.notificationOff(DAILY_CHECK_TITLE, async (success) => {
|
||||
if (success) {
|
||||
await this.$saveSettings({
|
||||
notifyingNewActivityTime: "",
|
||||
});
|
||||
this.notifyingNewActivity = false;
|
||||
this.notifyingNewActivityTime = "";
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async showReminderNotificationInfo(): Promise<void> {
|
||||
this.notify.confirm(
|
||||
ACCOUNT_VIEW_CONSTANTS.NOTIFICATIONS.REMINDER_INFO,
|
||||
async () => {
|
||||
await (this.$router as Router).push({
|
||||
name: "help-notification-types",
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
async showReminderNotificationChoice(): Promise<void> {
|
||||
if (!this.notifyingReminder) {
|
||||
(
|
||||
this.$refs.pushNotificationPermission as PushNotificationPermission
|
||||
).open(
|
||||
DIRECT_PUSH_TITLE,
|
||||
async (success: boolean, timeText: string, message?: string) => {
|
||||
if (success) {
|
||||
await this.$saveSettings({
|
||||
notifyingReminderMessage: message,
|
||||
notifyingReminderTime: timeText,
|
||||
});
|
||||
this.notifyingReminder = true;
|
||||
this.notifyingReminderMessage = message || "";
|
||||
this.notifyingReminderTime = timeText;
|
||||
}
|
||||
},
|
||||
);
|
||||
} else {
|
||||
this.notify.notificationOff(DIRECT_PUSH_TITLE, async (success) => {
|
||||
if (success) {
|
||||
await this.$saveSettings({
|
||||
notifyingReminderMessage: "",
|
||||
notifyingReminderTime: "",
|
||||
});
|
||||
this.notifyingReminder = false;
|
||||
this.notifyingReminderMessage = "";
|
||||
this.notifyingReminderTime = "";
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public async toggleHideRegisterPromptOnNewContact(): Promise<void> {
|
||||
const newSetting = !this.hideRegisterPromptOnNewContact;
|
||||
await this.$saveSettings({
|
||||
@@ -1229,19 +1049,8 @@ export default class AccountViewView extends Vue {
|
||||
this.passkeyExpirationDescription = tokenExpiryTimeDescription();
|
||||
}
|
||||
|
||||
public async turnOffNotifyingFlags(): Promise<void> {
|
||||
// should tell the push server as well
|
||||
await this.$saveSettings({
|
||||
notifyingNewActivityTime: "",
|
||||
notifyingReminderMessage: "",
|
||||
notifyingReminderTime: "",
|
||||
});
|
||||
this.notifyingNewActivity = false;
|
||||
this.notifyingNewActivityTime = "";
|
||||
this.notifyingReminder = false;
|
||||
this.notifyingReminderMessage = "";
|
||||
this.notifyingReminderTime = "";
|
||||
}
|
||||
// Note: Notification state management has been migrated to NotificationSection component
|
||||
// which handles its own lifecycle and persistence via PlatformServiceMixin
|
||||
|
||||
/**
|
||||
* Asynchronously exports the database into a downloadable JSON file.
|
||||
|
||||
@@ -831,3 +831,26 @@ export default class DIDView extends Vue {
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.dialog-overlay {
|
||||
z-index: 50;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 1.5rem;
|
||||
}
|
||||
.dialog {
|
||||
background-color: white;
|
||||
padding: 1rem;
|
||||
border-radius: 0.5rem;
|
||||
width: 100%;
|
||||
max-width: 500px;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user