Compare commits

..

8 Commits

Author SHA1 Message Date
Jose Olarte III
a9d9df32e1 WIP: QuickNav notification badges
- Cleanup of unused mockups
- Minor tweaks
2025-09-26 21:48:53 +08:00
Jose Olarte III
e655082af6 WIP: sticky tabs 2025-09-25 21:54:13 +08:00
Jose Olarte III
ea2fa30903 WIP: alternative notification UI 2025-09-24 21:19:22 +08:00
Jose Olarte III
39dbbb08f7 WIP: HomeView notification badge 2025-09-22 22:25:38 +08:00
Jose Olarte III
eb21d3c247 WIP: notification system adjustments
- Re-organize tabs
- Remove unneeded "Unread only" toggle (limiting functionality to chronological isUnread)
- Added "read line"
2025-09-19 23:41:03 +08:00
Jose Olarte III
213f5f0555 Merge branch 'master' into notification-system 2025-09-19 16:38:15 +08:00
Jose Olarte III
2db2c39830 WIP: notification view improvements
- Notification count badge per tab
- "Unread only" filter toggle
- Notification dot size adjustment
2025-09-17 22:13:34 +08:00
Jose Olarte III
106cefab51 WIP: notification system redesign
- Tabbed interface to expand the view's capabilities
- Added controls for managing notifications individually or in bulk
- Streamlined list design for increased information density
2025-09-15 21:43:39 +08:00
24 changed files with 363 additions and 341 deletions

View File

@@ -4,6 +4,7 @@ alwaysApply: false
--- ---
✅ use system date command to timestamp all interactions with accurate date and ✅ use system date command to timestamp all interactions with accurate date and
time time
✅ python script files must always have a blank line at their end
✅ remove whitespace at the end of lines ✅ remove whitespace at the end of lines
✅ use npm run lint-fix to check for warnings ✅ use npm run lint-fix to check for warnings
✅ do not use npm run dev let me handle running and supplying feedback ✅ do not use npm run dev let me handle running and supplying feedback
@@ -21,10 +22,12 @@ alwaysApply: false
- [ ] **Timestamp Usage**: Include accurate timestamps in all interactions - [ ] **Timestamp Usage**: Include accurate timestamps in all interactions
- [ ] **Code Quality**: Use npm run lint-fix to check for warnings - [ ] **Code Quality**: Use npm run lint-fix to check for warnings
- [ ] **File Standards**: Ensure Python files have blank line at end
- [ ] **Whitespace**: Remove trailing whitespace from all lines - [ ] **Whitespace**: Remove trailing whitespace from all lines
### After Development ### After Development
- [ ] **Linting Check**: Run npm run lint-fix to verify code quality - [ ] **Linting Check**: Run npm run lint-fix to verify code quality
- [ ] **File Validation**: Confirm Python files end with blank line
- [ ] **Whitespace Review**: Verify no trailing whitespace remains - [ ] **Whitespace Review**: Verify no trailing whitespace remains
- [ ] **Documentation**: Update relevant documentation with changes - [ ] **Documentation**: Update relevant documentation with changes

View File

@@ -1,6 +1,6 @@
{ {
"name": "timesafari", "name": "timesafari",
"version": "1.1.1-beta", "version": "1.1.0-beta",
"description": "Time Safari Application", "description": "Time Safari Application",
"author": { "author": {
"name": "Time Safari Team" "name": "Time Safari Team"
@@ -106,7 +106,7 @@
"guard": "bash ./scripts/build-arch-guard.sh", "guard": "bash ./scripts/build-arch-guard.sh",
"guard:test": "bash ./scripts/build-arch-guard.sh --staged", "guard:test": "bash ./scripts/build-arch-guard.sh --staged",
"guard:setup": "npm run prepare && echo '✅ Build Architecture Guard is now active!'", "guard:setup": "npm run prepare && echo '✅ Build Architecture Guard is now active!'",
"clean:android": "./scripts/uninstall-android.sh", "clean:android": "./scripts/clean-android.sh",
"clean:ios": "rm -rf ios/App/build ios/App/Pods ios/App/output ios/App/App/public ios/DerivedData ios/capacitor-cordova-ios-plugins ios/App/App/capacitor.config.json ios/App/App/config.xml || true", "clean:ios": "rm -rf ios/App/build ios/App/Pods ios/App/output ios/App/App/public ios/DerivedData ios/capacitor-cordova-ios-plugins ios/App/App/capacitor.config.json ios/App/App/config.xml || true",
"clean:electron": "./scripts/build-electron.sh --clean", "clean:electron": "./scripts/build-electron.sh --clean",
"clean:all": "npm run clean:ios && npm run clean:android && npm run clean:electron", "clean:all": "npm run clean:ios && npm run clean:android && npm run clean:electron",

View File

@@ -22,7 +22,6 @@
# --sync Sync Capacitor only # --sync Sync Capacitor only
# --assets Generate assets only # --assets Generate assets only
# --deploy Deploy APK to connected device # --deploy Deploy APK to connected device
# --uninstall Uninstall app from connected device
# -h, --help Show this help message # -h, --help Show this help message
# -v, --verbose Enable verbose logging # -v, --verbose Enable verbose logging
# #
@@ -197,7 +196,6 @@ SYNC_ONLY=false
ASSETS_ONLY=false ASSETS_ONLY=false
DEPLOY_APP=false DEPLOY_APP=false
AUTO_RUN=false AUTO_RUN=false
UNINSTALL=false
CUSTOM_API_IP="" CUSTOM_API_IP=""
# Function to parse Android-specific arguments # Function to parse Android-specific arguments
@@ -248,9 +246,6 @@ parse_android_args() {
--auto-run) --auto-run)
AUTO_RUN=true AUTO_RUN=true
;; ;;
--uninstall)
UNINSTALL=true
;;
--api-ip) --api-ip)
if [ $((i + 1)) -lt ${#args[@]} ]; then if [ $((i + 1)) -lt ${#args[@]} ]; then
CUSTOM_API_IP="${args[$((i + 1))]}" CUSTOM_API_IP="${args[$((i + 1))]}"
@@ -296,7 +291,6 @@ print_android_usage() {
echo " --assets Generate assets only" echo " --assets Generate assets only"
echo " --deploy Deploy APK to connected device" echo " --deploy Deploy APK to connected device"
echo " --auto-run Auto-run app after build" echo " --auto-run Auto-run app after build"
echo " --uninstall Uninstall app from connected device"
echo " --api-ip <ip> Custom IP address for claim API (defaults to 10.0.2.2)" echo " --api-ip <ip> Custom IP address for claim API (defaults to 10.0.2.2)"
echo "" echo ""
echo "Common Options:" echo "Common Options:"
@@ -311,7 +305,6 @@ print_android_usage() {
echo " $0 --clean # Clean only" echo " $0 --clean # Clean only"
echo " $0 --sync # Sync only" echo " $0 --sync # Sync only"
echo " $0 --deploy # Build and deploy to device" echo " $0 --deploy # Build and deploy to device"
echo " $0 --uninstall # Uninstall app from device"
echo " $0 --dev # Dev build with default 10.0.2.2" echo " $0 --dev # Dev build with default 10.0.2.2"
echo " $0 --dev --api-ip 192.168.1.100 # Dev build with custom API IP" echo " $0 --dev --api-ip 192.168.1.100 # Dev build with custom API IP"
echo "" echo ""
@@ -358,18 +351,8 @@ fi
# Setup application directories # Setup application directories
setup_app_directories setup_app_directories
# Load environment-specific .env file if it exists # Load environment from .env file if it exists
env_file=".env.$BUILD_MODE" load_env_file ".env"
if [ -f "$env_file" ]; then
load_env_file "$env_file"
else
log_debug "No $env_file file found, using default environment"
fi
# Load .env file if it exists (fallback)
if [ -f ".env" ]; then
load_env_file ".env"
fi
# Handle clean-only mode # Handle clean-only mode
if [ "$CLEAN_ONLY" = true ]; then if [ "$CLEAN_ONLY" = true ]; then
@@ -424,13 +407,8 @@ safe_execute "Validating asset configuration" "npm run assets:validate" || {
log_info "If you encounter build failures, please run 'npm install' first to ensure all dependencies are available." log_info "If you encounter build failures, please run 'npm install' first to ensure all dependencies are available."
} }
# Step 2: Uninstall Android app # Step 2: Clean Android app
if [ "$UNINSTALL" = true ]; then safe_execute "Cleaning Android app" "npm run clean:android" || exit 1
log_info "Uninstall: uninstalling app from device"
safe_execute "Uninstalling Android app" "./scripts/uninstall-android.sh" || exit 1
log_success "Uninstall completed successfully!"
exit 0
fi
# Step 3: Clean dist directory # Step 3: Clean dist directory
log_info "Cleaning dist directory..." log_info "Cleaning dist directory..."

View File

@@ -341,19 +341,7 @@ main_electron_build() {
# Setup environment # Setup environment
setup_build_env "electron" "$BUILD_MODE" setup_build_env "electron" "$BUILD_MODE"
setup_app_directories setup_app_directories
# Load environment-specific .env file if it exists
env_file=".env.$BUILD_MODE"
if [ -f "$env_file" ]; then
load_env_file "$env_file"
else
log_debug "No $env_file file found, using default environment"
fi
# Load .env file if it exists (fallback)
if [ -f ".env" ]; then
load_env_file ".env" load_env_file ".env"
fi
# Step 1: Clean Electron build artifacts # Step 1: Clean Electron build artifacts
clean_electron_artifacts clean_electron_artifacts

View File

@@ -324,18 +324,8 @@ fi
# Setup application directories # Setup application directories
setup_app_directories setup_app_directories
# Load environment-specific .env file if it exists # Load environment from .env file if it exists
env_file=".env.$BUILD_MODE" load_env_file ".env"
if [ -f "$env_file" ]; then
load_env_file "$env_file"
else
log_debug "No $env_file file found, using default environment"
fi
# Load .env file if it exists (fallback)
if [ -f ".env" ]; then
load_env_file ".env"
fi
# Validate iOS environment # Validate iOS environment
validate_ios_environment validate_ios_environment

View File

@@ -1,8 +1,8 @@
#!/bin/bash #!/bin/bash
# uninstall-android.sh # clean-android.sh
# Author: Matthew Raymer # Author: Matthew Raymer
# Date: 2025-08-19 # Date: 2025-08-19
# Description: Uninstall Android app with timeout protection to prevent hanging # Description: Clean Android app with timeout protection to prevent hanging
# This script safely uninstalls the TimeSafari app from connected Android devices # This script safely uninstalls the TimeSafari app from connected Android devices
# with a 30-second timeout to prevent indefinite hanging. # with a 30-second timeout to prevent indefinite hanging.

View File

@@ -293,7 +293,7 @@ const inputImageFileNameRef = ref<Blob>();
export default class ImageMethodDialog extends Vue { export default class ImageMethodDialog extends Vue {
$notify!: NotifyFunction; $notify!: NotifyFunction;
$router!: Router; $router!: Router;
notify!: ReturnType<typeof createNotifyHelpers>; notify = createNotifyHelpers(this.$notify);
/** Active DID for user authentication */ /** Active DID for user authentication */
activeDid = ""; activeDid = "";
@@ -498,9 +498,6 @@ export default class ImageMethodDialog extends Vue {
* @throws {Error} When settings retrieval fails * @throws {Error} When settings retrieval fails
*/ */
async mounted() { async mounted() {
// Initialize notification helpers
this.notify = createNotifyHelpers(this.$notify);
try { try {
// Get activeDid from active_identity table (single source of truth) // Get activeDid from active_identity table (single source of truth)
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any

View File

@@ -14,11 +14,20 @@
'text-slate-500': selected !== 'Home', 'text-slate-500': selected !== 'Home',
}" }"
> >
<router-link :to="{ name: 'home' }" class="block text-center py-2 px-1"> <router-link
:to="{ name: 'home' }"
class="relative block text-center py-2 px-1"
>
<div class="flex flex-col items-center"> <div class="flex flex-col items-center">
<font-awesome icon="house-chimney" class="fa-fw" /> <font-awesome icon="house-chimney" class="fa-fw" />
<span class="text-xs mt-1">feed</span> <span class="text-xs mt-1">feed</span>
</div> </div>
<!-- Notification dot - show while the user has unread notifications -->
<font-awesome
icon="circle"
class="absolute left-1/2 top-1 translate-x-2 text-rose-500 text-[10px] border border-white rounded-full"
></font-awesome>
</router-link> </router-link>
</li> </li>
<!-- Search --> <!-- Search -->
@@ -89,7 +98,7 @@
> >
<router-link <router-link
:to="{ name: 'account' }" :to="{ name: 'account' }"
class="block text-center py-2 px-1" class="relative block text-center py-2 px-1"
> >
<div class="flex flex-col items-center"> <div class="flex flex-col items-center">
<font-awesome icon="circle-user" class="fa-fw" /> <font-awesome icon="circle-user" class="fa-fw" />
@@ -102,6 +111,12 @@
--> -->
<span class="text-xs mt-1">profile</span> <span class="text-xs mt-1">profile</span>
</div> </div>
<!-- Notification dot - show while the user has not yet backed up their seed phrase -->
<font-awesome
icon="circle"
class="absolute left-1/2 top-1 translate-x-2 text-rose-500 text-[10px] border border-white rounded-full"
></font-awesome>
</router-link> </router-link>
</li> </li>
</ul> </ul>

View File

@@ -28,7 +28,7 @@ import { logger } from "../utils/logger";
export default class TopMessage extends Vue { export default class TopMessage extends Vue {
// Enhanced PlatformServiceMixin v4.0 provides: // Enhanced PlatformServiceMixin v4.0 provides:
// - Cached database operations: this.$contacts(), this.$settings(), this.$accountSettings() // - Cached database operations: this.$contacts(), this.$settings(), this.$accountSettings()
// - Settings shortcuts: this.$saveSettings() // - Settings shortcuts: this.$saveSettings(), this.$saveMySettings()
// - Cache management: this.$refreshSettings(), this.$clearAllCaches() // - Cache management: this.$refreshSettings(), this.$clearAllCaches()
// - Ultra-concise database methods: this.$db(), this.$exec(), this.$query() // - Ultra-concise database methods: this.$db(), this.$exec(), this.$query()
// - All methods use smart caching with TTL for massive performance gains // - All methods use smart caching with TTL for massive performance gains

View File

@@ -8,7 +8,7 @@
<!-- show spinner if loading limits --> <!-- show spinner if loading limits -->
<div <div
v-if="loadingLimits" v-if="loadingLimits"
class="text-slate-500 text-center italic mb-4" class="text-center"
role="status" role="status"
aria-live="polite" aria-live="polite"
> >
@@ -19,10 +19,7 @@
aria-hidden="true" aria-hidden="true"
></font-awesome> ></font-awesome>
</div> </div>
<div <div class="mb-4 text-center">
v-if="limitsMessage"
class="bg-amber-200 text-amber-900 border-amber-500 border-dashed border text-center rounded-md overflow-hidden px-4 py-3 mb-4"
>
{{ limitsMessage }} {{ limitsMessage }}
</div> </div>
<div v-if="endorserLimits"> <div v-if="endorserLimits">

View File

@@ -68,21 +68,13 @@ const MIG_004_SQL = `
WHERE id = 1 WHERE id = 1
AND EXISTS (SELECT 1 FROM settings WHERE id = 1 AND activeDid IS NOT NULL AND activeDid != ''); AND EXISTS (SELECT 1 FROM settings WHERE id = 1 AND activeDid IS NOT NULL AND activeDid != '');
-- Copy important settings that were set in the MASTER_SETTINGS_KEY to the main identity.
-- (We're not doing them all because some were already identity-specific and others aren't as critical.)
UPDATE settings
SET lastViewedClaimId = (SELECT lastViewedClaimId FROM settings WHERE id = 1),
profileImageUrl = (SELECT profileImageUrl FROM settings WHERE id = 1),
showShortcutBvc = (SELECT showShortcutBvc FROM settings WHERE id = 1),
warnIfProdServer = (SELECT warnIfProdServer FROM settings WHERE id = 1),
warnIfTestServer = (SELECT warnIfTestServer FROM settings WHERE id = 1)
WHERE id = 2;
-- CLEANUP: Remove orphaned settings records and clear legacy activeDid values -- CLEANUP: Remove orphaned settings records and clear legacy activeDid values
-- which usually simply deletes the MASTER_SETTINGS_KEY record.
-- This completes the migration from settings-based to table-based active identity -- This completes the migration from settings-based to table-based active identity
DELETE FROM settings WHERE accountDid IS NULL; -- Use guarded operations to prevent accidental data loss
UPDATE settings SET activeDid = NULL; DELETE FROM settings WHERE accountDid IS NULL AND id != 1;
UPDATE settings SET activeDid = NULL WHERE id = 1 AND EXISTS (
SELECT 1 FROM active_identity WHERE id = 1 AND activeDid IS NOT NULL
);
`; `;
// Each migration can include multiple SQL statements (with semicolons) // Each migration can include multiple SQL statements (with semicolons)

View File

@@ -80,6 +80,7 @@ import {
faQuestion, faQuestion,
faRightFromBracket, faRightFromBracket,
faRotate, faRotate,
faScroll,
faShareNodes, faShareNodes,
faSpinner, faSpinner,
faSquare, faSquare,
@@ -169,6 +170,7 @@ library.add(
faQrcode, faQrcode,
faQuestion, faQuestion,
faRotate, faRotate,
faScroll,
faRightFromBracket, faRightFromBracket,
faShareNodes, faShareNodes,
faSpinner, faSpinner,

View File

@@ -799,7 +799,7 @@ export async function runMigrations<T>(
} }
// Only show completion message in development // Only show completion message in development
logger.log( logger.debug(
`🎉 [Migration] Migration process complete! Summary: ${appliedCount} applied, ${skippedCount} skipped`, `🎉 [Migration] Migration process complete! Summary: ${appliedCount} applied, ${skippedCount} skipped`,
); );
} catch (error) { } catch (error) {

View File

@@ -514,7 +514,7 @@ export const PlatformServiceMixin = {
* Utility method for retrieving master settings * Utility method for retrieving master settings
* Common pattern used across many components * Common pattern used across many components
*/ */
async _getMasterSettings( async $getMasterSettings(
fallback: Settings | null = null, fallback: Settings | null = null,
): Promise<Settings | null> { ): Promise<Settings | null> {
try { try {
@@ -571,7 +571,7 @@ export const PlatformServiceMixin = {
): Promise<Settings> { ): Promise<Settings> {
try { try {
// Get default settings // Get default settings
const defaultSettings = await this._getMasterSettings(defaultFallback); const defaultSettings = await this.$getMasterSettings(defaultFallback);
// If no account DID, return defaults // If no account DID, return defaults
if (!accountDid) { if (!accountDid) {
@@ -970,7 +970,7 @@ export const PlatformServiceMixin = {
* @returns Fresh settings object from database * @returns Fresh settings object from database
*/ */
async $settings(defaults: Settings = {}): Promise<Settings> { async $settings(defaults: Settings = {}): Promise<Settings> {
const settings = await this._getMasterSettings(defaults); const settings = await this.$getMasterSettings(defaults);
if (!settings) { if (!settings) {
return defaults; return defaults;
@@ -1003,7 +1003,7 @@ export const PlatformServiceMixin = {
): Promise<Settings> { ): Promise<Settings> {
try { try {
// Get default settings first // Get default settings first
const defaultSettings = await this._getMasterSettings(defaults); const defaultSettings = await this.$getMasterSettings(defaults);
if (!defaultSettings) { if (!defaultSettings) {
return defaults; return defaults;
@@ -1212,11 +1212,6 @@ export const PlatformServiceMixin = {
* @param changes Settings changes to save * @param changes Settings changes to save
* @returns Promise<boolean> Success status * @returns Promise<boolean> Success status
*/ */
/**
* Since this is unused, and since it relies on this.activeDid which isn't guaranteed to exist,
* let's take this out for the sake of safety.
* Totally remove after start of 2026 (since it would be obvious by then that it's not used).
*
async $saveMySettings(changes: Partial<Settings>): Promise<boolean> { async $saveMySettings(changes: Partial<Settings>): Promise<boolean> {
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
const currentDid = (this as any).activeDid; const currentDid = (this as any).activeDid;
@@ -1226,7 +1221,6 @@ export const PlatformServiceMixin = {
} }
return await this.$saveUserSettings(currentDid, changes); return await this.$saveUserSettings(currentDid, changes);
}, },
**/
// ================================================= // =================================================
// CACHE MANAGEMENT METHODS // CACHE MANAGEMENT METHODS
@@ -1848,7 +1842,7 @@ export const PlatformServiceMixin = {
async $debugMergedSettings(did: string): Promise<void> { async $debugMergedSettings(did: string): Promise<void> {
try { try {
// Get default settings // Get default settings
const defaultSettings = await this._getMasterSettings({}); const defaultSettings = await this.$getMasterSettings({});
logger.debug( logger.debug(
`[PlatformServiceMixin] Default settings:`, `[PlatformServiceMixin] Default settings:`,
defaultSettings, defaultSettings,
@@ -1898,6 +1892,7 @@ export interface IPlatformServiceMixin {
params?: unknown[], params?: unknown[],
): Promise<SqlValue[] | undefined>; ): Promise<SqlValue[] | undefined>;
$dbRawQuery(sql: string, params?: unknown[]): Promise<unknown | undefined>; $dbRawQuery(sql: string, params?: unknown[]): Promise<unknown | undefined>;
$getMasterSettings(fallback?: Settings | null): Promise<Settings | null>;
$getMergedSettings( $getMergedSettings(
defaultKey: string, defaultKey: string,
accountDid?: string, accountDid?: string,
@@ -2022,6 +2017,7 @@ declare module "@vue/runtime-core" {
params?: unknown[], params?: unknown[],
): Promise<unknown[] | undefined>; ): Promise<unknown[] | undefined>;
$dbRawQuery(sql: string, params?: unknown[]): Promise<unknown | undefined>; $dbRawQuery(sql: string, params?: unknown[]): Promise<unknown | undefined>;
$getMasterSettings(defaults?: Settings | null): Promise<Settings | null>;
$getMergedSettings( $getMergedSettings(
key: string, key: string,
did?: string, did?: string,
@@ -2044,8 +2040,7 @@ declare module "@vue/runtime-core" {
did: string, did: string,
changes: Partial<Settings>, changes: Partial<Settings>,
): Promise<boolean>; ): Promise<boolean>;
// @deprecated; see implementation note above $saveMySettings(changes: Partial<Settings>): Promise<boolean>;
// $saveMySettings(changes: Partial<Settings>): Promise<boolean>;
// Cache management methods // Cache management methods
$refreshSettings(): Promise<Settings>; $refreshSettings(): Promise<Settings>;

View File

@@ -1454,6 +1454,7 @@ export default class AccountViewView extends Vue {
this.imageLimits = imageResp.data; this.imageLimits = imageResp.data;
} else { } else {
this.limitsMessage = ACCOUNT_VIEW_CONSTANTS.LIMITS.NO_IMAGE_ACCESS; this.limitsMessage = ACCOUNT_VIEW_CONSTANTS.LIMITS.NO_IMAGE_ACCESS;
this.notify.warning(ACCOUNT_VIEW_CONSTANTS.LIMITS.CANNOT_UPLOAD_IMAGES);
} }
const endorserResp = await fetchEndorserRateLimits( const endorserResp = await fetchEndorserRateLimits(

View File

@@ -223,7 +223,7 @@ export default class ContactAmountssView extends Vue {
const contact = await this.$getContact(contactDid); const contact = await this.$getContact(contactDid);
this.contact = contact; this.contact = contact;
const settings = await this.$settings(); const settings = await this.$getMasterSettings();
// Get activeDid from active_identity table (single source of truth) // Get activeDid from active_identity table (single source of truth)
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any

View File

@@ -394,7 +394,7 @@ export default class HelpNotificationsView extends Vue {
notifyingReminderTime = ""; notifyingReminderTime = "";
// Notification helper system // Notification helper system
notify!: ReturnType<typeof createNotifyHelpers>; notify = createNotifyHelpers(this.$notify);
/** /**
* Computed property for consistent button styling * Computed property for consistent button styling
@@ -430,9 +430,6 @@ export default class HelpNotificationsView extends Vue {
* Handles errors gracefully with proper logging without exposing sensitive data. * Handles errors gracefully with proper logging without exposing sensitive data.
*/ */
async mounted() { async mounted() {
// Initialize notification helpers
this.notify = createNotifyHelpers(this.$notify);
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();

View File

@@ -80,12 +80,11 @@ Raymer * @version 1.0.0 */
</router-link> </router-link>
</div> </div>
<div class="mb-8">
<!-- <!--
They should have an identifier, even if it's an auto-generated one that they'll never use. They should have an identifier, even if it's an auto-generated one that they'll never use.
Identity creation is now handled by router navigation guard. Identity creation is now handled by router navigation guard.
--> -->
<div class="mb-4"> <div class="mb-6">
<RegistrationNotice <RegistrationNotice
v-if="!isUserRegistered" v-if="!isUserRegistered"
:passkeys-enabled="PASSKEYS_ENABLED" :passkeys-enabled="PASSKEYS_ENABLED"
@@ -95,16 +94,16 @@ Raymer * @version 1.0.0 */
<div v-if="isUserRegistered" id="sectionRecordSomethingGiven"> <div v-if="isUserRegistered" id="sectionRecordSomethingGiven">
<!-- Record Quick-Action --> <!-- Record Quick-Action -->
<div class="mb-6"> <div class="bg-slate-200 rounded-lg overflow-hidden p-3 pt-2.5">
<div class="flex gap-2 items-center mb-2"> <div class="flex gap-2 items-center mb-2">
<h2 class="text-xl font-bold">Record something given by:</h2> <h2 class="font-bold">Record something given by:</h2>
<button <button
class="block ms-auto text-center text-white bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] p-2 rounded-full" class="block ms-auto text-center text-white bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] p-1.5 rounded-full"
@click="openGiftedPrompts()" @click="openGiftedPrompts()"
> >
<font-awesome <font-awesome
icon="lightbulb" icon="lightbulb"
class="block text-center w-[1em]" class="block text-center text-sm w-[1em]"
/> />
</button> </button>
</div> </div>
@@ -112,7 +111,7 @@ Raymer * @version 1.0.0 */
<div class="grid grid-cols-2 gap-2"> <div class="grid grid-cols-2 gap-2">
<button <button
type="button" type="button"
class="text-center text-base 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-3 py-2 rounded-lg" class="text-center text-base 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-3 py-2 rounded-md"
@click="openPersonDialog()" @click="openPersonDialog()"
> >
<font-awesome icon="user" /> <font-awesome icon="user" />
@@ -120,7 +119,7 @@ Raymer * @version 1.0.0 */
</button> </button>
<button <button
type="button" type="button"
class="text-center text-base 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-3 py-2 rounded-lg" class="text-center text-base 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-3 py-2 rounded-md"
@click="openProjectDialog()" @click="openProjectDialog()"
> >
<font-awesome icon="folder-open" /> <font-awesome icon="folder-open" />
@@ -130,7 +129,6 @@ Raymer * @version 1.0.0 */
</div> </div>
</div> </div>
</div> </div>
</div>
<GiftedDialog <GiftedDialog
ref="giftedDialog" ref="giftedDialog"
@@ -138,74 +136,90 @@ Raymer * @version 1.0.0 */
:recipient-entity-type="'person'" :recipient-entity-type="'person'"
/> />
<GiftedPrompts ref="giftedPrompts" /> <GiftedPrompts ref="giftedPrompts" />
<FeedFilters ref="feedFilters" />
<!-- Results List --> <!-- Results List -->
<div class="mt-4 mb-4"> <div class="mt-4 mb-4">
<div class="flex gap-2 items-center mb-3"> <!-- ALTERNATIVE UI: Feed + Notification Tabs -->
<h2 class="text-xl font-bold">Latest Activity</h2> <div
<button class="sticky top-0 z-50 grid grid-cols-5 text-xl sm:text-2xl pt-4 pb-1 px-1 -mt-3 -mx-1 mb-4 bg-white rounded-b-[10px]"
v-if="resultsAreFiltered()"
class="block ms-auto text-center text-white bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] p-2 rounded-full"
@click="openFeedFilters()"
> >
<font-awesome <button
icon="filter" class="relative text-center bg-slate-400 text-white px-1 pt-3 pb-2 first:rounded-s-md last:rounded-e-md border-r border-slate-300 last:border-r-0 leading-none"
class="block text-center w-[1em] translate-y-[0.05em]" >
/> <font-awesome icon="scroll" />
<div class="text-xs sm:text-sm mt-1">activity</div>
</button> </button>
<button <button
v-else class="relative text-center bg-slate-200 text-slate-500 px-1 pt-3 pb-2 first:rounded-s-md last:rounded-e-md border-r border-slate-300 last:border-r-0 leading-none"
class="block ms-auto text-center text-white bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] p-2 rounded-full" >
@click="openFeedFilters()" <font-awesome icon="hand-holding-heart" />
<div class="text-xs sm:text-sm mt-1">offers</div>
<!-- Unread count -->
<span
class="absolute -top-2 -translate-x-1/2 bg-rose-500 text-white border border-white text-xs font-semibold leading-none rounded-full px-[0.4em] py-[0.15em]"
>2</span
>
</button>
<button
class="relative text-center bg-slate-200 text-slate-500 px-1 pt-3 pb-2 first:rounded-s-md last:rounded-e-md border-r border-slate-300 last:border-r-0 leading-none"
>
<font-awesome icon="folder-open" />
<div class="text-xs sm:text-sm mt-1">projects</div>
<!-- Unread count -->
<span
class="absolute -top-2 -translate-x-1/2 bg-rose-500 text-white border border-white text-xs font-semibold leading-none rounded-full px-[0.4em] py-[0.15em]"
>50+</span
>
</button>
<button
class="relative text-center bg-slate-200 text-slate-500 px-1 pt-3 pb-2 first:rounded-s-md last:rounded-e-md border-r border-slate-300 last:border-r-0 leading-none"
>
<font-awesome icon="users" />
<div class="text-xs sm:text-sm mt-1">people</div>
<!-- Unread count -->
<span
class="absolute -top-2 -translate-x-1/2 bg-rose-500 text-white border border-white text-xs font-semibold leading-none rounded-full px-[0.4em] py-[0.15em]"
>4</span
>
</button>
<button
class="relative text-center bg-slate-200 text-slate-500 px-1 pt-3 pb-2 first:rounded-s-md last:rounded-e-md border-r border-slate-300 last:border-r-0 leading-none"
>
<font-awesome icon="image" />
<div class="text-xs sm:text-sm mt-1">items</div>
<!-- Unread count -->
<span
class="absolute -top-2 -translate-x-1/2 bg-rose-500 text-white border border-white text-xs font-semibold leading-none rounded-full px-[0.4em] py-[0.15em]"
>7</span
> >
<font-awesome
icon="filter"
class="block text-center w-[1em] translate-y-[0.05em]"
/>
</button> </button>
</div> </div>
<div <div class="flex gap-2 items-center justify-between mb-2 text-sm">
class="border-t p-2 border-slate-300" <h2 class="text-base font-bold">Latest Activity</h2>
@click="goToActivityToUserPage()" <button
v-if="resultsAreFiltered()"
class="flex items-center justify-end gap-2 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-2 py-1 rounded"
@click="openFeedFilters()"
> >
<div class="flex justify-center"> Filter
<div <font-awesome icon="filter"></font-awesome>
v-if="numNewOffersToUser" </button>
class="bg-gradient-to-b from-green-400 to-green-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] m-1 px-4 py-4 rounded-md text-white" <button
v-else
class="flex items-center justify-end gap-2 bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-2 py-1 rounded"
@click="openFeedFilters()"
> >
<span Filter
class="block text-center text-6xl" <font-awesome icon="filter"></font-awesome>
data-testId="newDirectOffersActivityNumber" </button>
>
{{ numNewOffersToUser }}{{ newOffersToUserHitLimit ? "+" : "" }}
</span>
<p class="text-center">
new offer{{ numNewOffersToUser === 1 ? "" : "s" }} to you
</p>
</div>
<div
v-if="numNewOffersToUserProjects"
class="bg-gradient-to-b from-green-400 to-green-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] m-1 px-4 py-4 rounded-md text-white"
>
<span
class="block text-center text-6xl"
data-testId="newOffersToUserProjectsActivityNumber"
>
{{ numNewOffersToUserProjects
}}{{ newOffersToUserProjectsHitLimit ? "+" : "" }}
</span>
<p class="text-center">
new offer{{ numNewOffersToUserProjects === 1 ? "" : "s" }} to your
projects
</p>
</div>
</div>
<div class="flex justify-end mt-2">
<button class="text-blue-500">View All New Activity For You</button>
</div>
</div> </div>
<FeedFilters ref="feedFilters" />
<InfiniteScroll @reached-bottom="loadMoreGives"> <InfiniteScroll @reached-bottom="loadMoreGives">
<ul id="listLatestActivity" class="space-y-4"> <ul id="listLatestActivity" class="space-y-4">
<ActivityListItem <ActivityListItem
@@ -645,9 +659,7 @@ export default class HomeView extends Vue {
if (resp.status === 200) { if (resp.status === 200) {
// Ultra-concise settings update with automatic cache invalidation! // Ultra-concise settings update with automatic cache invalidation!
await this.$saveUserSettings(this.activeDid, { await this.$saveMySettings({ isRegistered: true });
isRegistered: true,
});
this.isRegistered = true; this.isRegistered = true;
} }
} catch (error) { } catch (error) {

View File

@@ -306,9 +306,6 @@ export default class IdentitySwitcherView extends Vue {
} }
await this.$exec("DELETE FROM accounts WHERE id = ?", [id]); await this.$exec("DELETE FROM accounts WHERE id = ?", [id]);
await this.$exec("DELETE FROM settings WHERE accountDid = ?", [
accountDid,
]);
}); });
// Update UI // Update UI

View File

@@ -3,108 +3,198 @@
<!-- CONTENT --> <!-- CONTENT -->
<section id="Content" class="p-6 pb-24 max-w-3xl mx-auto"> <section id="Content" class="p-6 pb-24 max-w-3xl mx-auto">
<!-- Breadcrumb --> <!-- Breadcrumb -->
<div id="ViewBreadcrumb" class="mb-8"> <div class="mb-2">
<h1 class="text-lg text-center font-light relative px-7"> <h1 class="text-2xl text-center font-semibold relative px-7">
<!-- Back --> <!-- Back -->
<font-awesome <font-awesome
icon="chevron-left" icon="chevron-left"
class="fa-fw text-lg text-center px-2 py-1 absolute -left-2 -top-1" class="fa-fw text-lg text-center px-2 py-1 absolute -left-2 top-[0.2em]"
@click="$router.back()" @click="$router.back()"
/> />
New Activity For You
Notifications
</h1> </h1>
</div> </div>
<!-- Display a single row with the name of "New Offers To You" with a count. --> <!-- Main Tabs -->
<div class="flex justify-between" data-testId="showOffersToUser"> <div class="text-center text-slate-500 border-b border-slate-300 mt-4 mb-2">
<div> <ul class="flex flex-wrap justify-center gap-4 -mb-px">
<span class="text-lg font-medium" <li class="flex items-center gap-[0.175em]">
>{{ newOffersToUser.length <a
}}{{ newOffersToUserHitLimit ? "+" : "" }}</span href="#"
class="inline-block py-2 rounded-t-lg border-b-2 active text-black border-black font-semibold"
> >
<span class="text-lg font-medium ml-4" Offers
>New Offer{{ newOffersToUser.length === 1 ? "" : "s" }} To You</span
>
<font-awesome
v-if="newOffersToUser.length > 0"
:icon="showOffersDetails ? 'chevron-down' : 'chevron-right'"
class="cursor-pointer ml-4 mr-4 text-lg"
@click="expandOffersToUserAndMarkRead()"
/>
</div>
<a class="text-blue-500 cursor-pointer" @click="handleSeeAllOffersToUser">
See&nbsp;all
</a> </a>
</div>
<div v-if="showOffersDetails" class="ml-4 mt-4"> <!-- Unread count -->
<ul class="list-disc ml-4"> <span
<li class="inline-block bg-rose-500 text-white text-xs font-semibold leading-none rounded-full px-[0.4em] py-[0.15em] -me-1.5 -mt-[2px]"
v-for="offer in newOffersToUser" >3</span
:key="offer.jwtId"
class="mt-4 relative group"
> >
<span>{{ </li>
didInfo(offer.offeredByDid, activeDid, allMyDids, allContacts) <li class="flex items-center gap-[0.175em]">
}}</span> <a
offered href="#"
<span v-if="offer.objectDescription">{{ class="inline-block py-2 rounded-t-lg border-b-2 text-blue-600 border-transparent hover:border-slate-400"
offer.objectDescription
}}</span
>{{ offer.objectDescription && offer.amount ? ", and " : "" }}
<span v-if="offer.amount">{{
displayAmount(offer.unit, offer.amount)
}}</span>
<router-link
:to="{ path: '/claim/' + encodeURIComponent(offer.jwtId) }"
class="text-blue-500"
> >
<font-awesome Projects
icon="file-lines" </a>
class="pl-2 text-blue-500 cursor-pointer"
/> <!-- Unread count -->
</router-link> <span
<!-- New line that appears on hover or when the offer is clicked --> class="inline-block bg-rose-500 text-white text-xs font-semibold leading-none rounded-full px-[0.4em] py-[0.15em] -me-1.5 -mt-[2px]"
<div >9+</span
class="absolute left-0 w-full text-left text-gray-500 text-sm hidden group-hover:flex cursor-pointer items-center"
@click="markOffersAsReadStartingWith(offer.jwtId)"
> >
<span class="inline-block w-8 h-px bg-gray-500 mr-2" /> </li>
Click to keep all above as new offers <li class="flex items-center gap-[0.175em]">
</div> <a
href="#"
class="inline-block py-2 rounded-t-lg border-b-2 text-blue-600 border-transparent hover:border-slate-400"
>
People
</a>
</li>
<li class="flex items-center gap-[0.175em]">
<a
href="#"
class="inline-block py-2 rounded-t-lg border-b-2 text-blue-600 border-transparent hover:border-slate-400"
>
Items
</a>
</li> </li>
</ul> </ul>
</div> </div>
<!-- Display a single row with the name of "New Offers To Your Projects" with a count. --> <!-- Sub Tabs - Offers -->
<div <div class="text-center text-slate-500 border-b border-slate-300 mb-2">
class="mt-4 flex justify-between" <ul class="flex flex-wrap justify-center gap-4 text-sm -mb-px">
data-testId="showOffersToUserProjects" <li class="flex items-center gap-[0.175em]">
>
<div>
<span class="text-lg font-medium"
>{{ newOffersToUserProjects.length
}}{{ newOffersToUserProjectsHitLimit ? "+" : "" }}</span
>
<span class="text-lg font-medium ml-4"
>New Offer{{ newOffersToUserProjects.length === 1 ? "" : "s" }} To
Your Projects</span
>
<font-awesome
v-if="newOffersToUserProjects.length > 0"
:icon="
showOffersToUserProjectsDetails ? 'chevron-down' : 'chevron-right'
"
class="cursor-pointer ml-4 mr-4 text-lg"
@click="expandOffersToUserProjectsAndMarkRead()"
/>
</div>
<a <a
class="text-blue-500 cursor-pointer" href="#"
@click="handleSeeAllOffersToUserProjects" class="inline-block py-2 rounded-t-lg border-b-2 active text-black border-black font-semibold"
> >
See&nbsp;all To You
</a> </a>
<!-- Unread count -->
<span
class="inline-block bg-rose-500 text-white text-xs font-semibold leading-none rounded-full px-[0.4em] py-[0.15em] -me-1.5 -mt-[2px]"
>2</span
>
</li>
<li class="flex items-center gap-[0.175em]">
<a
href="#"
class="inline-block py-2 rounded-t-lg border-b-2 text-blue-600 border-transparent hover:border-slate-400"
>
Your Projects
</a>
</li>
<li class="flex items-center gap-[0.175em]">
<a
href="#"
class="inline-block py-2 rounded-t-lg border-b-2 text-blue-600 border-transparent hover:border-slate-400"
>
Favorites
</a>
</li>
</ul>
</div>
<!-- Offers to You -->
<div v-if="showOffersDetails" class="mt-4">
<div class="flex justify-end items-center text-sm mb-2">
<a
href="#"
class="flex items-center justify-end gap-2 bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-2 py-1 rounded"
>
Mark all as read
<font-awesome icon="check"></font-awesome>
</a>
</div>
<ul class="text-sm border-t border-slate-300">
<li
v-for="offer in newOffersToUser"
:key="offer.jwtId"
class="flex justify-between items-center gap-4 border-b border-slate-300 py-2"
>
<router-link
:to="{ path: '/claim/' + encodeURIComponent(offer.jwtId) }"
class="block"
>
<span class="font-semibold">{{
didInfo(offer.offeredByDid, activeDid, allMyDids, allContacts)
}}</span>
offered
<span
v-if="offer.objectDescription"
class="font-semibold text-blue-600"
>{{ offer.objectDescription }}</span
>{{ offer.objectDescription && offer.amount ? " and " : "" }}
<span v-if="offer.amount" class="font-semibold text-blue-600">{{
displayAmount(offer.unit, offer.amount)
}}</span>
</router-link>
<!-- Unread indicator -->
<font-awesome
icon="circle"
class="text-rose-500 text-[8px] border border-rose-500 rounded-full"
></font-awesome>
</li>
<!-- Sample read item -->
<li class="border-b border-slate-300 py-2">
<!-- Last viewed separator -->
<div
class="border-t border-dashed border-slate-300 text-orange-400 mt-4 mb-2 font-bold text-sm"
>
<span class="block w-fit mx-auto -mt-2.5 bg-white px-2">
You've already seen all the following
</span>
<hr class="border-slate-300 mt-4" />
</div>
<div class="flex justify-between items-center gap-4">
<!-- Notification details -->
<a href="#" class="block text-slate-400">
<span class="font-semibold">User One</span>
offered
<span class="font-semibold">Sample read notification item</span>
and
<span class="font-semibold">50 USD</span>
</a>
<!-- Read indicator -->
<font-awesome
icon="circle"
class="text-transparent text-[8px] border border-slate-300 rounded-full"
></font-awesome>
</div>
</li>
<!-- Sample read item -->
<li class="border-b border-slate-300 py-2">
<div class="flex justify-between items-center gap-4">
<!-- Notification details -->
<a href="#" class="block text-slate-400">
<span class="font-semibold">User One</span>
offered
<span class="font-semibold">Sample read notification item</span>
and
<span class="font-semibold">50 USD</span>
</a>
<!-- Read indicator -->
<font-awesome
icon="circle"
class="text-transparent text-[8px] border border-slate-300 rounded-full"
></font-awesome>
</div>
</li>
</ul>
</div> </div>
<div v-if="showOffersToUserProjectsDetails" class="ml-4 mt-4"> <div v-if="showOffersToUserProjectsDetails" class="ml-4 mt-4">
@@ -194,7 +284,7 @@ export default class NewActivityView extends Vue {
newOffersToUserProjects: Array<OfferToPlanSummaryRecord> = []; newOffersToUserProjects: Array<OfferToPlanSummaryRecord> = [];
newOffersToUserProjectsHitLimit = false; newOffersToUserProjectsHitLimit = false;
showOffersDetails = false; showOffersDetails = true;
showOffersToUserProjectsDetails = false; showOffersToUserProjectsDetails = false;
didInfo = didInfo; didInfo = didInfo;
displayAmount = displayAmount; displayAmount = displayAmount;
@@ -249,7 +339,7 @@ export default class NewActivityView extends Vue {
async expandOffersToUserAndMarkRead() { async expandOffersToUserAndMarkRead() {
this.showOffersDetails = !this.showOffersDetails; this.showOffersDetails = !this.showOffersDetails;
if (this.showOffersDetails && this.newOffersToUser.length > 0) { if (this.showOffersDetails) {
await this.$updateSettings({ await this.$updateSettings({
lastAckedOfferToUserJwtId: this.newOffersToUser[0].jwtId, lastAckedOfferToUserJwtId: this.newOffersToUser[0].jwtId,
}); });
@@ -286,10 +376,7 @@ export default class NewActivityView extends Vue {
async expandOffersToUserProjectsAndMarkRead() { async expandOffersToUserProjectsAndMarkRead() {
this.showOffersToUserProjectsDetails = this.showOffersToUserProjectsDetails =
!this.showOffersToUserProjectsDetails; !this.showOffersToUserProjectsDetails;
if ( if (this.showOffersToUserProjectsDetails) {
this.showOffersToUserProjectsDetails &&
this.newOffersToUserProjects.length > 0
) {
await this.$updateSettings({ await this.$updateSettings({
lastAckedOfferToUserProjectsJwtId: lastAckedOfferToUserProjectsJwtId:
this.newOffersToUserProjects[0].jwtId, this.newOffersToUserProjects[0].jwtId,
@@ -297,7 +384,7 @@ export default class NewActivityView extends Vue {
// note that we don't update this.lastAckedOfferToUserProjectsJwtId in case // note that we don't update this.lastAckedOfferToUserProjectsJwtId in case
// they later choose the last one to keep the offers as new // they later choose the last one to keep the offers as new
this.notify.info( this.notify.info(
"The offers are marked as viewed. Click in the list to keep them as new.", "The offers are now marked as viewed. Click in the list to keep them as new.",
TIMEOUTS.LONG, TIMEOUTS.LONG,
); );
} }
@@ -325,13 +412,5 @@ export default class NewActivityView extends Vue {
TIMEOUTS.STANDARD, TIMEOUTS.STANDARD,
); );
} }
async handleSeeAllOffersToUser() {
this.$router.push("/recent-offers-to-user");
}
async handleSeeAllOffersToUserProjects() {
this.$router.push("/recent-offers-to-user-projects");
}
} }
</script> </script>

View File

@@ -99,7 +99,7 @@ export default class QuickActionBvcBeginView extends Vue {
$router!: Router; $router!: Router;
// Notification helper system // Notification helper system
private notify!: ReturnType<typeof createNotifyHelpers>; private notify = createNotifyHelpers(this.$notify);
attended = true; attended = true;
gaveTime = true; gaveTime = true;
@@ -111,9 +111,6 @@ export default class QuickActionBvcBeginView extends Vue {
* Uses America/Denver timezone for Bountiful location * Uses America/Denver timezone for Bountiful location
*/ */
async mounted() { async mounted() {
// Initialize notification helpers
this.notify = createNotifyHelpers(this.$notify);
logger.debug( logger.debug(
"[QuickActionBvcBeginView] Mounted - calculating meeting date", "[QuickActionBvcBeginView] Mounted - calculating meeting date",
); );

View File

@@ -32,20 +32,20 @@
</div> </div>
<InfiniteScroll @reached-bottom="loadMoreOffersToUserProjects"> <InfiniteScroll @reached-bottom="loadMoreOffersToUserProjects">
<ul data-testId="listRecentOffersToUserProjects"> <ul
data-testId="listRecentOffersToUserProjects"
class="border-t border-slate-300"
>
<li <li
v-for="offer in newOffersToUserProjects" v-for="offer in newOffersToUserProjects"
:key="offer.jwtId" :key="offer.jwtId"
class="mt-4 relative group" class="mt-4 relative group"
> >
<!-- Last viewed separator -->
<div <div
v-if="offer.jwtId == lastAckedOfferToUserProjectsJwtId" v-if="offer.jwtId == lastAckedOfferToUserProjectsJwtId"
class="border-b border-dashed border-slate-300 text-orange-400 mt-4 mb-6 font-bold text-sm" class="border-b border-slate-300 text-orange-400 pb-2 mb-2 font-bold text-sm"
> >
<span class="block w-fit mx-auto -mb-2.5 bg-white px-2">
You've already seen all the following You've already seen all the following
</span>
</div> </div>
<span>{{ <span>{{
@@ -147,14 +147,6 @@ export default class RecentOffersToUserView extends Vue {
this.newOffersToUserProjects = offersToUserProjectsData.data; this.newOffersToUserProjects = offersToUserProjectsData.data;
this.newOffersToUserProjectsAtEnd = !offersToUserProjectsData.hitLimit; this.newOffersToUserProjectsAtEnd = !offersToUserProjectsData.hitLimit;
// Mark offers as read after data is loaded
if (this.newOffersToUserProjects.length > 0) {
await this.$updateSettings({
lastAckedOfferToUserProjectsJwtId:
this.newOffersToUserProjects[0].jwtId,
});
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (err: any) { } catch (err: any) {
logger.error("Error retrieving settings & contacts:", err); logger.error("Error retrieving settings & contacts:", err);

View File

@@ -27,20 +27,20 @@
</p> </p>
</div> </div>
<InfiniteScroll @reached-bottom="loadMoreOffersToUser"> <InfiniteScroll @reached-bottom="loadMoreOffersToUser">
<ul data-testId="listRecentOffersToUser"> <ul
data-testId="listRecentOffersToUser"
class="border-t border-slate-300"
>
<li <li
v-for="offer in newOffersToUser" v-for="offer in newOffersToUser"
:key="offer.jwtId" :key="offer.jwtId"
class="mt-4 relative group" class="mt-4 relative group"
> >
<!-- Last viewed separator -->
<div <div
v-if="offer.jwtId == lastAckedOfferToUserJwtId" v-if="offer.jwtId == lastAckedOfferToUserJwtId"
class="border-b border-dashed border-slate-300 text-orange-400 mt-4 mb-6 font-bold text-sm" class="border-b border-slate-300 text-orange-400 pb-2 mb-2 font-bold text-sm"
> >
<span class="block w-fit mx-auto -mb-2.5 bg-white px-2">
You've already seen all the following You've already seen all the following
</span>
</div> </div>
<span>{{ <span>{{
@@ -138,13 +138,6 @@ export default class RecentOffersToUserView extends Vue {
this.newOffersToUser = offersToUserData.data; this.newOffersToUser = offersToUserData.data;
this.newOffersToUserAtEnd = !offersToUserData.hitLimit; this.newOffersToUserAtEnd = !offersToUserData.hitLimit;
// Mark offers as read after data is loaded
if (this.newOffersToUser.length > 0) {
await this.$updateSettings({
lastAckedOfferToUserJwtId: this.newOffersToUser[0].jwtId,
});
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (err: any) { } catch (err: any) {
logger.error("Error retrieving settings & contacts:", err); logger.error("Error retrieving settings & contacts:", err);

View File

@@ -162,7 +162,7 @@ export default class SeedBackupView extends Vue {
showSeed = false; showSeed = false;
// Notification helper system // Notification helper system
notify!: ReturnType<typeof createNotifyHelpers>; notify = createNotifyHelpers(this.$notify);
/** /**
* Computed property for consistent copy feedback styling * Computed property for consistent copy feedback styling
@@ -204,9 +204,6 @@ export default class SeedBackupView extends Vue {
* Handles errors gracefully with user notifications. * Handles errors gracefully with user notifications.
*/ */
async created() { async created() {
// Initialize notification helpers
this.notify = createNotifyHelpers(this.$notify);
try { try {
let activeDid = ""; let activeDid = "";