Compare commits

..

12 Commits

Author SHA1 Message Date
f4569d8b98 Merge branch 'master' into load-build-mode-env-file 2025-09-19 04:35:05 -04:00
7575895f75 Merge pull request 'fix: initialize notification helpers in lifecycle methods' (#200) from notify-initialization-fix into master
Reviewed-on: #200
2025-09-19 03:56:49 -04:00
67a9ecf6c6 Merge branch 'master' into notify-initialization-fix 2025-09-19 03:56:24 -04:00
823fa51275 Merge pull request 'feat(NewActivityView): enhance "See all" links to mark offers as read before navigation' (#198) from new-activity-mark-read into master
Reviewed-on: #198
2025-09-19 03:14:23 -04:00
Jose Olarte III
e2c2d54c20 Merge branch 'master' into new-activity-mark-read 2025-09-19 15:14:42 +08:00
Jose Olarte III
6fd53b020e refactor: simplify notification messages for offer viewing
- Remove conditional notification logic in NewActivityView
- Remove redundant notification in RecentOffersToUserView and RecentOffersToUserProjectsView
- Standardize to single notification message format
2025-09-19 15:00:17 +08:00
Jose Olarte III
a3d6b458b1 fix: load environment-specific .env files in iOS/Android/Electron build scripts
- iOS, Android, and Electron build scripts now load .env.development, .env.test, .env.production files
- Previously only loaded generic .env file which doesn't exist
- Ensures consistent image server URL across all build targets
- Fixes issue where build:ios:dev used production image URL instead of test URL
- Aligns with web build script behavior for environment variable precedence

Resolves inconsistent VITE_DEFAULT_IMAGE_API_SERVER values between build targets.
2025-09-18 22:38:53 +08:00
Jose Olarte III
b1fcb49e7c fix: initialize notification helpers in lifecycle methods
- Fix 't is not a function' error during image upload by properly initializing notification helpers
- Move notification helper initialization from class-level to lifecycle methods (created/mounted)
- Affected components: ImageMethodDialog, SeedBackupView, QuickActionBvcBeginView, HelpNotificationsView
- Ensures $notify is available when createNotifyHelpers() is called
- Resolves notification errors in image upload functionality
2025-09-18 21:42:15 +08:00
Jose Olarte III
5d9f455fc8 feat: move mark-as-read logic from navigation to view loading
- Remove mark-as-read logic from NewActivityView navigation handlers
- Add mark-as-read logic to RecentOffersToUserView and RecentOffersToUserProjectsView after data loading
- Improve "You've already seen all the following" marker positioning
- Update marker styling with dashed border and centered text

This ensures the marker appears at the correct position in the list
instead of always at the top, providing better UX when viewing offers.
2025-09-16 18:10:17 +08:00
c3ff471ea1 Merge branch 'master' into new-activity-mark-read 2025-09-16 04:19:17 -04:00
Jose Olarte III
ac603f66e2 Lint fix 2025-09-10 18:19:40 +08:00
Jose Olarte III
9bdd66b9c9 feat(NewActivityView): enhance "See all" links to mark offers as read before navigation
- Replace router-links with click handlers for both "See all" offers links
- Add handleSeeAllOffersToUser and handleSeeAllOffersToUserProjects methods
- Modify expandOffersToUserAndMarkRead to accept fromSeeAll parameter for contextual notifications
- Modify expandOffersToUserProjectsAndMarkRead to accept fromSeeAll parameter for contextual notifications
- Show shorter notification messages when called from "See all" vs chevron expand buttons
- Add safety checks to prevent errors when offers arrays are empty
- Standardize notification message text consistency
- TypeScript and formatting lint fixes

Both "See all" links now properly mark offers as viewed before navigation,
preventing users from seeing unread offers in the detailed views.
2025-09-10 18:19:17 +08:00
13 changed files with 274 additions and 325 deletions

View File

@@ -351,8 +351,18 @@ fi
# Setup application directories # Setup application directories
setup_app_directories setup_app_directories
# Load environment from .env file if it exists # Load environment-specific .env file if it exists
load_env_file ".env" 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"
fi
# Handle clean-only mode # Handle clean-only mode
if [ "$CLEAN_ONLY" = true ]; then if [ "$CLEAN_ONLY" = true ]; then

View File

@@ -341,7 +341,19 @@ 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_env_file ".env"
# 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"
fi
# Step 1: Clean Electron build artifacts # Step 1: Clean Electron build artifacts
clean_electron_artifacts clean_electron_artifacts

View File

@@ -324,8 +324,18 @@ fi
# Setup application directories # Setup application directories
setup_app_directories setup_app_directories
# Load environment from .env file if it exists # Load environment-specific .env file if it exists
load_env_file ".env" 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"
fi
# Validate iOS environment # Validate iOS environment
validate_ios_environment validate_ios_environment

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 = createNotifyHelpers(this.$notify); notify!: ReturnType<typeof createNotifyHelpers>;
/** Active DID for user authentication */ /** Active DID for user authentication */
activeDid = ""; activeDid = "";
@@ -498,6 +498,9 @@ 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,20 +14,11 @@
'text-slate-500': selected !== 'Home', 'text-slate-500': selected !== 'Home',
}" }"
> >
<router-link <router-link :to="{ name: 'home' }" class="block text-center py-2 px-1">
: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 -->
@@ -98,7 +89,7 @@
> >
<router-link <router-link
:to="{ name: 'account' }" :to="{ name: 'account' }"
class="relative block text-center py-2 px-1" class="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" />
@@ -111,12 +102,6 @@
--> -->
<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

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

View File

@@ -394,7 +394,7 @@ export default class HelpNotificationsView extends Vue {
notifyingReminderTime = ""; notifyingReminderTime = "";
// Notification helper system // Notification helper system
notify = createNotifyHelpers(this.$notify); notify!: ReturnType<typeof createNotifyHelpers>;
/** /**
* Computed property for consistent button styling * Computed property for consistent button styling
@@ -430,6 +430,9 @@ 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,51 +80,53 @@ 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. <!--
Identity creation is now handled by router navigation guard. 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.
<div class="mb-6"> -->
<RegistrationNotice <div class="mb-4">
v-if="!isUserRegistered" <RegistrationNotice
:passkeys-enabled="PASSKEYS_ENABLED" v-if="!isUserRegistered"
:given-name="givenName" :passkeys-enabled="PASSKEYS_ENABLED"
message="To share, someone must register you." :given-name="givenName"
/> message="To share, someone must register you."
/>
<div v-if="isUserRegistered" id="sectionRecordSomethingGiven"> <div v-if="isUserRegistered" id="sectionRecordSomethingGiven">
<!-- Record Quick-Action --> <!-- Record Quick-Action -->
<div class="bg-slate-200 rounded-lg overflow-hidden p-3 pt-2.5"> <div class="mb-6">
<div class="flex gap-2 items-center mb-2"> <div class="flex gap-2 items-center mb-2">
<h2 class="font-bold">Record something given by:</h2> <h2 class="text-xl 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-1.5 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-2 rounded-full"
@click="openGiftedPrompts()" @click="openGiftedPrompts()"
> >
<font-awesome <font-awesome
icon="lightbulb" icon="lightbulb"
class="block text-center text-sm w-[1em]" class="block text-center w-[1em]"
/> />
</button> </button>
</div> </div>
<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-md" 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"
@click="openPersonDialog()" @click="openPersonDialog()"
> >
<font-awesome icon="user" /> <font-awesome icon="user" />
Person Person
</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-md" 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"
@click="openProjectDialog()" @click="openProjectDialog()"
> >
<font-awesome icon="folder-open" /> <font-awesome icon="folder-open" />
Project Project
</button> </button>
</div>
</div> </div>
</div> </div>
</div> </div>
@@ -136,90 +138,74 @@ 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">
<!-- ALTERNATIVE UI: Feed + Notification Tabs --> <div class="flex gap-2 items-center mb-3">
<div <h2 class="text-xl font-bold">Latest Activity</h2>
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]"
>
<button
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"
>
<font-awesome icon="scroll" />
<div class="text-xs sm:text-sm mt-1">activity</div>
</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="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
>
</button>
</div>
<div class="flex gap-2 items-center justify-between mb-2 text-sm">
<h2 class="text-base font-bold">Latest Activity</h2>
<button <button
v-if="resultsAreFiltered()" 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" 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()" @click="openFeedFilters()"
> >
Filter <font-awesome
<font-awesome icon="filter"></font-awesome> icon="filter"
class="block text-center w-[1em] translate-y-[0.05em]"
/>
</button> </button>
<button <button
v-else 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" 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()" @click="openFeedFilters()"
> >
Filter <font-awesome
<font-awesome icon="filter"></font-awesome> icon="filter"
class="block text-center w-[1em] translate-y-[0.05em]"
/>
</button> </button>
</div> </div>
<FeedFilters ref="feedFilters" />
<div
class="border-t p-2 border-slate-300"
@click="goToActivityToUserPage()"
>
<div class="flex justify-center">
<div
v-if="numNewOffersToUser"
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="newDirectOffersActivityNumber"
>
{{ 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>
<InfiniteScroll @reached-bottom="loadMoreGives"> <InfiniteScroll @reached-bottom="loadMoreGives">
<ul id="listLatestActivity" class="space-y-4"> <ul id="listLatestActivity" class="space-y-4">
<ActivityListItem <ActivityListItem

View File

@@ -3,200 +3,110 @@
<!-- 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 class="mb-2"> <div id="ViewBreadcrumb" class="mb-8">
<h1 class="text-2xl text-center font-semibold relative px-7"> <h1 class="text-lg text-center font-light 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-[0.2em]" class="fa-fw text-lg text-center px-2 py-1 absolute -left-2 -top-1"
@click="$router.back()" @click="$router.back()"
/> />
New Activity For You
Notifications
</h1> </h1>
</div> </div>
<!-- Main Tabs --> <!-- Display a single row with the name of "New Offers To You" with a count. -->
<div class="text-center text-slate-500 border-b border-slate-300 mt-4 mb-2"> <div class="flex justify-between" data-testId="showOffersToUser">
<ul class="flex flex-wrap justify-center gap-4 -mb-px"> <div>
<li class="flex items-center gap-[0.175em]"> <span class="text-lg font-medium"
<a >{{ newOffersToUser.length
href="#" }}{{ newOffersToUserHitLimit ? "+" : "" }}</span
class="inline-block py-2 rounded-t-lg border-b-2 active text-black border-black font-semibold"
>
Offers
</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]"
>3</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"
>
Projects
</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]"
>9+</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"
>
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>
</ul>
</div>
<!-- Sub Tabs - Offers -->
<div class="text-center text-slate-500 border-b border-slate-300 mb-2">
<ul class="flex flex-wrap justify-center gap-4 text-sm -mb-px">
<li class="flex items-center gap-[0.175em]">
<a
href="#"
class="inline-block py-2 rounded-t-lg border-b-2 active text-black border-black font-semibold"
>
To You
</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 <span class="text-lg font-medium ml-4"
<font-awesome icon="check"></font-awesome> >New Offer{{ newOffersToUser.length === 1 ? "" : "s" }} To You</span
</a> >
<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> </div>
<ul class="text-sm border-t border-slate-300"> <a class="text-blue-500 cursor-pointer" @click="handleSeeAllOffersToUser">
See&nbsp;all
</a>
</div>
<div v-if="showOffersDetails" class="ml-4 mt-4">
<ul class="list-disc ml-4">
<li <li
v-for="offer in newOffersToUser" v-for="offer in newOffersToUser"
:key="offer.jwtId" :key="offer.jwtId"
class="flex justify-between items-center gap-4 border-b border-slate-300 py-2" class="mt-4 relative group"
> >
<span>{{
didInfo(offer.offeredByDid, activeDid, allMyDids, allContacts)
}}</span>
offered
<span v-if="offer.objectDescription">{{
offer.objectDescription
}}</span
>{{ offer.objectDescription && offer.amount ? ", and " : "" }}
<span v-if="offer.amount">{{
displayAmount(offer.unit, offer.amount)
}}</span>
<router-link <router-link
:to="{ path: '/claim/' + encodeURIComponent(offer.jwtId) }" :to="{ path: '/claim/' + encodeURIComponent(offer.jwtId) }"
class="block" class="text-blue-500"
> >
<span class="font-semibold">{{ <font-awesome
didInfo(offer.offeredByDid, activeDid, allMyDids, allContacts) icon="file-lines"
}}</span> class="pl-2 text-blue-500 cursor-pointer"
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> </router-link>
<!-- New line that appears on hover or when the offer is clicked -->
<!-- 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 <div
class="border-t border-dashed border-slate-300 text-orange-400 mt-4 mb-2 font-bold text-sm" 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="block w-fit mx-auto -mt-2.5 bg-white px-2"> <span class="inline-block w-8 h-px bg-gray-500 mr-2" />
You've already seen all the following Click to keep all above as new offers
</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> </div>
</li> </li>
</ul> </ul>
</div> </div>
<!-- Display a single row with the name of "New Offers To Your Projects" with a count. -->
<div
class="mt-4 flex justify-between"
data-testId="showOffersToUserProjects"
>
<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
class="text-blue-500 cursor-pointer"
@click="handleSeeAllOffersToUserProjects"
>
See&nbsp;all
</a>
</div>
<div v-if="showOffersToUserProjectsDetails" class="ml-4 mt-4"> <div v-if="showOffersToUserProjectsDetails" class="ml-4 mt-4">
<ul class="list-disc ml-4"> <ul class="list-disc ml-4">
<li <li
@@ -284,7 +194,7 @@ export default class NewActivityView extends Vue {
newOffersToUserProjects: Array<OfferToPlanSummaryRecord> = []; newOffersToUserProjects: Array<OfferToPlanSummaryRecord> = [];
newOffersToUserProjectsHitLimit = false; newOffersToUserProjectsHitLimit = false;
showOffersDetails = true; showOffersDetails = false;
showOffersToUserProjectsDetails = false; showOffersToUserProjectsDetails = false;
didInfo = didInfo; didInfo = didInfo;
displayAmount = displayAmount; displayAmount = displayAmount;
@@ -339,7 +249,7 @@ export default class NewActivityView extends Vue {
async expandOffersToUserAndMarkRead() { async expandOffersToUserAndMarkRead() {
this.showOffersDetails = !this.showOffersDetails; this.showOffersDetails = !this.showOffersDetails;
if (this.showOffersDetails) { if (this.showOffersDetails && this.newOffersToUser.length > 0) {
await this.$updateSettings({ await this.$updateSettings({
lastAckedOfferToUserJwtId: this.newOffersToUser[0].jwtId, lastAckedOfferToUserJwtId: this.newOffersToUser[0].jwtId,
}); });
@@ -376,7 +286,10 @@ export default class NewActivityView extends Vue {
async expandOffersToUserProjectsAndMarkRead() { async expandOffersToUserProjectsAndMarkRead() {
this.showOffersToUserProjectsDetails = this.showOffersToUserProjectsDetails =
!this.showOffersToUserProjectsDetails; !this.showOffersToUserProjectsDetails;
if (this.showOffersToUserProjectsDetails) { if (
this.showOffersToUserProjectsDetails &&
this.newOffersToUserProjects.length > 0
) {
await this.$updateSettings({ await this.$updateSettings({
lastAckedOfferToUserProjectsJwtId: lastAckedOfferToUserProjectsJwtId:
this.newOffersToUserProjects[0].jwtId, this.newOffersToUserProjects[0].jwtId,
@@ -384,7 +297,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 now marked as viewed. Click in the list to keep them as new.", "The offers are marked as viewed. Click in the list to keep them as new.",
TIMEOUTS.LONG, TIMEOUTS.LONG,
); );
} }
@@ -412,5 +325,13 @@ 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 = createNotifyHelpers(this.$notify); private notify!: ReturnType<typeof createNotifyHelpers>;
attended = true; attended = true;
gaveTime = true; gaveTime = true;
@@ -111,6 +111,9 @@ 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 <ul data-testId="listRecentOffersToUserProjects">
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-slate-300 text-orange-400 pb-2 mb-2 font-bold text-sm" class="border-b border-dashed border-slate-300 text-orange-400 mt-4 mb-6 font-bold text-sm"
> >
You've already seen all the following <span class="block w-fit mx-auto -mb-2.5 bg-white px-2">
You've already seen all the following
</span>
</div> </div>
<span>{{ <span>{{
@@ -147,6 +147,14 @@ 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 <ul data-testId="listRecentOffersToUser">
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-slate-300 text-orange-400 pb-2 mb-2 font-bold text-sm" class="border-b border-dashed border-slate-300 text-orange-400 mt-4 mb-6 font-bold text-sm"
> >
You've already seen all the following <span class="block w-fit mx-auto -mb-2.5 bg-white px-2">
You've already seen all the following
</span>
</div> </div>
<span>{{ <span>{{
@@ -138,6 +138,13 @@ 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 = createNotifyHelpers(this.$notify); notify!: ReturnType<typeof createNotifyHelpers>;
/** /**
* Computed property for consistent copy feedback styling * Computed property for consistent copy feedback styling
@@ -204,6 +204,9 @@ 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 = "";