@ -39,7 +39,7 @@
v - if = "newOffersToUser.length > 0"
v - if = "newOffersToUser.length > 0"
: icon = "showOffersDetails ? 'chevron-down' : 'chevron-right'"
: icon = "showOffersDetails ? 'chevron-down' : 'chevron-right'"
class = "cursor-pointer ml-4 mr-4 text-lg"
class = "cursor-pointer ml-4 mr-4 text-lg"
@ click = "expandOffersToUserAndMarkRead()"
@ click . prevent = "expandOffersToUserAndMarkRead()"
/ >
/ >
< / div >
< / div >
< a class = "text-blue-500 cursor-pointer" @click ="handleSeeAllOffersToUser" >
< a class = "text-blue-500 cursor-pointer" @click ="handleSeeAllOffersToUser" >
@ -58,7 +58,7 @@
didInfo ( offer . offeredByDid , activeDid , allMyDids , allContacts )
didInfo ( offer . offeredByDid , activeDid , allMyDids , allContacts )
} } < / span >
} } < / span >
offered
offered
< span v-if ="offer.objectDescription" > {{
< span v-if ="offer.objectDescription" class="truncate" > {{
offer . objectDescription
offer . objectDescription
} } < / s p a n
} } < / s p a n
> { { offer . objectDescription && offer . amount ? ", and " : "" } }
> { { offer . objectDescription && offer . amount ? ", and " : "" } }
@ -77,10 +77,10 @@
<!-- New line that appears on hover or when the offer is clicked -- >
<!-- New line that appears on hover or when the offer is clicked -- >
< div
< div
class = "absolute left-0 w-full text-left text-gray-500 text-sm hidden group-hover:flex cursor-pointer items-center"
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)"
@ click . prevent = "markOffersAsReadStartingWith(offer.jwtId)"
>
>
< span class = "inline-block w-8 h-px bg-gray-500 mr-2" / >
< span class = "inline-block w-8 h-px bg-gray-500 mr-2" / >
Click to keep all above as new offers
Click to keep all above as unread offers
< / div >
< / div >
< / li >
< / li >
< / ul >
< / ul >
@ -106,7 +106,7 @@
showOffersToUserProjectsDetails ? 'chevron-down' : 'chevron-right'
showOffersToUserProjectsDetails ? 'chevron-down' : 'chevron-right'
"
"
class = "cursor-pointer ml-4 mr-4 text-lg"
class = "cursor-pointer ml-4 mr-4 text-lg"
@ click = "expandOffersToUserProjectsAndMarkRead()"
@ click . prevent = "expandOffersToUserProjectsAndMarkRead()"
/ >
/ >
< / div >
< / div >
< a
< a
@ -128,7 +128,7 @@
didInfo ( offer . offeredByDid , activeDid , allMyDids , allContacts )
didInfo ( offer . offeredByDid , activeDid , allMyDids , allContacts )
} } < / span >
} } < / span >
offered
offered
< span v-if ="offer.objectDescription" > {{
< span v-if ="offer.objectDescription" class="truncate" > {{
offer . objectDescription
offer . objectDescription
} } < / s p a n
} } < / s p a n
> { { offer . objectDescription && offer . amount ? ", and " : "" } }
> { { offer . objectDescription && offer . amount ? ", and " : "" } }
@ -149,10 +149,153 @@
<!-- New line that appears on hover -- >
<!-- New line that appears on hover -- >
< div
< div
class = "absolute left-0 w-full text-left text-gray-500 text-sm hidden group-hover:flex cursor-pointer items-center"
class = "absolute left-0 w-full text-left text-gray-500 text-sm hidden group-hover:flex cursor-pointer items-center"
@ click = "markOffersToUserProjectsAsReadStartingWith(offer.jwtId)"
@ click . prevent = "
markOffersToUserProjectsAsReadStartingWith ( offer . jwtId )
"
>
>
< span class = "inline-block w-8 h-px bg-gray-500 mr-2" / >
< span class = "inline-block w-8 h-px bg-gray-500 mr-2" / >
Click to keep all above as new offers
Click to keep all above as unread offers
< / div >
< / li >
< / ul >
< / div >
<!-- Starred Projects with Changes Section -- >
< div
class = "flex justify-between mt-6"
data - testId = "showStarredProjectChanges"
>
< div >
< span class = "text-lg font-medium"
> { { newStarredProjectChanges . length
} } { { newStarredProjectChangesHitLimit ? "+" : "" } } < / s p a n
>
< span class = "text-lg font-medium ml-4"
> Starred Project { {
newStarredProjectChanges . length === 1 ? "" : "s"
} }
With Changes < / s p a n
>
< font -awesome
v - if = "newStarredProjectChanges.length > 0"
: icon = "
showStarredProjectChangesDetails ? 'chevron-down' : 'chevron-right'
"
class = "cursor-pointer ml-4 mr-4 text-lg"
@ click . prevent = "expandStarredProjectChangesAndMarkRead()"
/ >
< / div >
< / div >
< div v-if ="showStarredProjectChangesDetails" class="ml-4 mt-4" >
< ul class = "list-disc ml-4" >
< li
v - for = "projectChange in newStarredProjectChanges"
: key = "projectChange.plan.handleId"
class = "mt-4 relative group"
>
< div class = "flex items-center gap-2" >
< div class = "flex-1 min-w-0" >
< span class = "font-medium" > { {
projectChange . plan . name || "Unnamed Project"
} } < / span >
< span
v - if = "projectChange.plan.description"
class = "text-gray-600 block truncate"
>
{ { projectChange . plan . description } }
< / span >
< / div >
< router -link
: to = " {
path :
'/project/' + encodeURIComponent ( projectChange . plan . handleId ) ,
} "
class = "text-blue-500 flex-shrink-0"
>
< font -awesome
icon = "file-lines"
class = "text-blue-500 cursor-pointer"
/ >
< / r o u t e r - l i n k >
< / div >
<!-- Show what changed -- >
< div
v - if = "getPlanDifferences(projectChange.plan.handleId)"
class = "text-sm mt-2"
>
< div class = "font-medium mb-2" > Changes < / div >
< div class = "overflow-x-auto" >
< table
class = "w-full text-xs border-collapse border border-gray-300 rounded-lg shadow-sm bg-white"
>
< thead >
< tr class = "bg-gray-50" >
< th
class = "border border-gray-300 px-3 py-2 text-left font-semibold text-gray-700"
> < / th >
< th
class = "border border-gray-300 px-3 py-2 text-left font-semibold text-gray-700"
>
Previous
< / th >
< th
class = "border border-gray-300 px-3 py-2 text-left font-semibold text-gray-700"
>
Current
< / th >
< / tr >
< / thead >
< tbody >
< tr
v - for = " ( difference , field ) in getPlanDifferences (
projectChange . plan . handleId ,
) "
: key = "field"
class = "hover:bg-gray-50"
>
< td
class = "border border-gray-300 px-3 py-2 font-medium text-gray-800 break-words"
>
{ { getDisplayFieldName ( field ) } }
< / td >
< td
class = "border border-gray-300 px-3 py-2 text-gray-600 break-words align-top"
>
< vue -markdown
v - if = "field === 'description' && difference.old"
: source = "formatFieldValue(difference.old)"
class = "markdown-content"
/ >
< span v-else > {{ formatFieldValue ( difference.old ) }} < / span >
< / td >
< td
class = "border border-gray-300 px-3 py-2 text-green-700 font-medium break-words align-top"
>
< vue -markdown
v - if = "field === 'description' && difference.new"
: source = "formatFieldValue(difference.new)"
class = "markdown-content"
/ >
< span v-else > {{ formatFieldValue ( difference.new ) }} < / span >
< / td >
< / tr >
< / tbody >
< / table >
< / div >
< / div >
< div v-else > The changes did not affect essential project data. < / div >
<!-- New line that appears on hover -- >
< div
class = "absolute left-0 w-full text-left text-gray-500 text-sm hidden group-hover:flex cursor-pointer items-center"
@ click . prevent = "
markStarredProjectChangesAsReadStartingWith (
projectChange . plan . jwtId ! ,
)
"
>
< span class = "inline-block w-8 h-px bg-gray-500 mr-2" / >
Click to keep all above as unread changes
< / div >
< / div >
< / li >
< / li >
< / ul >
< / ul >
@ -162,6 +305,7 @@
< script lang = "ts" >
< script lang = "ts" >
import { Component , Vue } from "vue-facing-decorator" ;
import { Component , Vue } from "vue-facing-decorator" ;
import VueMarkdown from "vue-markdown-render" ;
import GiftedDialog from "../components/GiftedDialog.vue" ;
import GiftedDialog from "../components/GiftedDialog.vue" ;
import QuickNav from "../components/QuickNav.vue" ;
import QuickNav from "../components/QuickNav.vue" ;
@ -172,20 +316,28 @@ import { Router } from "vue-router";
import {
import {
OfferSummaryRecord ,
OfferSummaryRecord ,
OfferToPlanSummaryRecord ,
OfferToPlanSummaryRecord ,
PlanSummaryAndPreviousClaim ,
PlanSummaryRecord ,
} from "../interfaces/records" ;
} from "../interfaces/records" ;
import {
import {
didInfo ,
didInfo ,
didInfoOrNobody ,
displayAmount ,
displayAmount ,
getNewOffersToUser ,
getNewOffersToUser ,
getNewOffersToUserProjects ,
getNewOffersToUserProjects ,
getStarredProjectsWithChanges ,
} from "../libs/endorserServer" ;
} from "../libs/endorserServer" ;
import { retrieveAccountDids } from "../libs/util" ;
import { retrieveAccountDids } from "../libs/util" ;
import { logger } from "../utils/logger" ;
import { logger } from "../utils/logger" ;
import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin" ;
import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin" ;
import { createNotifyHelpers , TIMEOUTS } from "@/utils/notify" ;
import { createNotifyHelpers , TIMEOUTS } from "@/utils/notify" ;
import * as databaseUtil from "../db/databaseUtil" ;
import * as R from "ramda" ;
import { PlanActionClaim } from "../interfaces/claims" ;
import { GenericCredWrapper } from "@/interfaces" ;
@ Component ( {
@ Component ( {
components : { GiftedDialog , QuickNav , EntityIcon } ,
components : { GiftedDialog , QuickNav , EntityIcon , VueMarkdown } ,
mixins : [ PlatformServiceMixin ] ,
mixins : [ PlatformServiceMixin ] ,
} )
} )
export default class NewActivityView extends Vue {
export default class NewActivityView extends Vue {
@ -199,13 +351,22 @@ export default class NewActivityView extends Vue {
apiServer = "" ;
apiServer = "" ;
lastAckedOfferToUserJwtId = "" ;
lastAckedOfferToUserJwtId = "" ;
lastAckedOfferToUserProjectsJwtId = "" ;
lastAckedOfferToUserProjectsJwtId = "" ;
lastAckedStarredPlanChangesJwtId = "" ;
newOffersToUser : Array < OfferSummaryRecord > = [ ] ;
newOffersToUser : Array < OfferSummaryRecord > = [ ] ;
newOffersToUserHitLimit = false ;
newOffersToUserHitLimit = false ;
newOffersToUserProjects : Array < OfferToPlanSummaryRecord > = [ ] ;
newOffersToUserProjects : Array < OfferToPlanSummaryRecord > = [ ] ;
newOffersToUserProjectsHitLimit = false ;
newOffersToUserProjectsHitLimit = false ;
newStarredProjectChanges : Array < PlanSummaryAndPreviousClaim > = [ ] ;
newStarredProjectChangesHitLimit = false ;
starredPlanHandleIds : Array < string > = [ ] ;
planDifferences : Record <
string ,
Record < string , { old : unknown ; new : unknown } >
> = { } ;
showOffersDetails = false ;
showOffersDetails = false ;
showOffersToUserProjectsDetails = false ;
showOffersToUserProjectsDetails = false ;
showStarredProjectChangesDetails = false ;
didInfo = didInfo ;
didInfo = didInfo ;
displayAmount = displayAmount ;
displayAmount = displayAmount ;
@ -224,6 +385,12 @@ export default class NewActivityView extends Vue {
this . lastAckedOfferToUserJwtId = settings . lastAckedOfferToUserJwtId || "" ;
this . lastAckedOfferToUserJwtId = settings . lastAckedOfferToUserJwtId || "" ;
this . lastAckedOfferToUserProjectsJwtId =
this . lastAckedOfferToUserProjectsJwtId =
settings . lastAckedOfferToUserProjectsJwtId || "" ;
settings . lastAckedOfferToUserProjectsJwtId || "" ;
this . lastAckedStarredPlanChangesJwtId =
settings . lastAckedStarredPlanChangesJwtId || "" ;
this . starredPlanHandleIds = databaseUtil . parseJsonField (
settings . starredPlanHandleIds ,
[ ] ,
) ;
this . allContacts = await this . $getAllContacts ( ) ;
this . allContacts = await this . $getAllContacts ( ) ;
@ -247,6 +414,29 @@ export default class NewActivityView extends Vue {
this . newOffersToUserProjects = offersToUserProjectsData . data ;
this . newOffersToUserProjects = offersToUserProjectsData . data ;
this . newOffersToUserProjectsHitLimit = offersToUserProjectsData . hitLimit ;
this . newOffersToUserProjectsHitLimit = offersToUserProjectsData . hitLimit ;
/ / L o a d s t a r r e d p r o j e c t c h a n g e s i f u s e r h a s s t a r r e d p r o j e c t s
if ( this . starredPlanHandleIds . length > 0 ) {
try {
const starredProjectChangesData = await getStarredProjectsWithChanges (
this . axios ,
this . apiServer ,
this . activeDid ,
this . starredPlanHandleIds ,
this . lastAckedStarredPlanChangesJwtId ,
) ;
this . newStarredProjectChanges = starredProjectChangesData . data ;
this . newStarredProjectChangesHitLimit =
starredProjectChangesData . hitLimit ;
/ / A n a l y z e d i f f e r e n c e s b e t w e e n c u r r e n t p l a n s a n d p r e v i o u s c l a i m s
this . analyzePlanDifferences ( this . newStarredProjectChanges ) ;
} catch ( error ) {
logger . warn ( "Failed to load starred project changes:" , error ) ;
this . newStarredProjectChanges = [ ] ;
this . newStarredProjectChangesHitLimit = false ;
}
}
/ / e s l i n t - d i s a b l e - n e x t - l i n e @ t y p e s c r i p t - e s l i n t / n o - e x p l i c i t - a n y
/ / e s l i n t - d i s a b l e - n e x t - l i n e @ t y p e s c r i p t - e s l i n t / n o - e x p l i c i t - a n y
} catch ( err : any ) {
} catch ( err : any ) {
logger . error ( "Error retrieving settings & contacts:" , err ) ;
logger . error ( "Error retrieving settings & contacts:" , err ) ;
@ -260,13 +450,13 @@ 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 && this . newOffersToUser . length > 0 ) {
await this . $updateSettings ( {
await this . $saveUserSettings ( this . activeDid , {
lastAckedOfferToUserJwtId : this . newOffersToUser [ 0 ] . jwtId ,
lastAckedOfferToUserJwtId : this . newOffersToUser [ 0 ] . jwtId ,
} ) ;
} ) ;
/ / n o t e t h a t w e d o n ' t u p d a t e t h i s . l a s t A c k e d O f f e r T o U s e r J w t I d i n c a s e t h e y
/ / n o t e t h a t w e d o n ' t u p d a t e t h i s . l a s t A c k e d O f f e r T o U s e r J w t I d i n c a s e t h e y
/ / l a t e r c h o o s e t h e l a s t o n e t o k e e p t h e o f f e r s a s n e w
/ / l a t e r c h o o s e t h e l a s t o n e t o k e e p t h e o f f e r s a s n e w
this . notify . info (
this . notify . info (
"The offers are marked as viewed. Click in the list to keep them as new ." ,
"The offers are marked as read. Click in the list to keep them unread ." ,
TIMEOUTS . LONG ,
TIMEOUTS . LONG ,
) ;
) ;
}
}
@ -278,12 +468,12 @@ export default class NewActivityView extends Vue {
) ;
) ;
if ( index !== - 1 && index < this . newOffersToUser . length - 1 ) {
if ( index !== - 1 && index < this . newOffersToUser . length - 1 ) {
/ / S e t t o t h e n e x t o f f e r ' s j w t I d
/ / S e t t o t h e n e x t o f f e r ' s j w t I d
await this . $updateSettings ( {
await this . $saveUserSettings ( this . activeDid , {
lastAckedOfferToUserJwtId : this . newOffersToUser [ index + 1 ] . jwtId ,
lastAckedOfferToUserJwtId : this . newOffersToUser [ index + 1 ] . jwtId ,
} ) ;
} ) ;
} else {
} else {
/ / i t ' s t h e l a s t e n t r y ( o r n o t f o u n d ) , s o j u s t k e e p i t t h e s a m e
/ / i t ' s t h e l a s t e n t r y ( o r n o t f o u n d ) , s o j u s t k e e p i t t h e s a m e
await this . $updateSettings ( {
await this . $saveUserSettings ( this . activeDid , {
lastAckedOfferToUserJwtId : this . lastAckedOfferToUserJwtId ,
lastAckedOfferToUserJwtId : this . lastAckedOfferToUserJwtId ,
} ) ;
} ) ;
}
}
@ -300,14 +490,14 @@ export default class NewActivityView extends Vue {
this . showOffersToUserProjectsDetails &&
this . showOffersToUserProjectsDetails &&
this . newOffersToUserProjects . length > 0
this . newOffersToUserProjects . length > 0
) {
) {
await this . $updateSettings ( {
await this . $saveUserSettings ( this . activeDid , {
lastAckedOfferToUserProjectsJwtId :
lastAckedOfferToUserProjectsJwtId :
this . newOffersToUserProjects [ 0 ] . jwtId ,
this . newOffersToUserProjects [ 0 ] . jwtId ,
} ) ;
} ) ;
/ / n o t e t h a t w e d o n ' t u p d a t e t h i s . l a s t A c k e d O f f e r T o U s e r P r o j e c t s J w t I d i n c a s e
/ / n o t e t h a t w e d o n ' t u p d a t e t h i s . l a s t A c k e d O f f e r T o U s e r P r o j e c t s J w t I d i n c a s e
/ / t h e y l a t e r c h o o s e t h e l a s t o n e t o k e e p t h e o f f e r s a s n e w
/ / t h e y l a t e r c h o o s e t h e l a s t o n e t o k e e p t h e o f f e r s a s n e w
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 read. Click in the list to keep them unread ." ,
TIMEOUTS . LONG ,
TIMEOUTS . LONG ,
) ;
) ;
}
}
@ -319,13 +509,13 @@ export default class NewActivityView extends Vue {
) ;
) ;
if ( index !== - 1 && index < this . newOffersToUserProjects . length - 1 ) {
if ( index !== - 1 && index < this . newOffersToUserProjects . length - 1 ) {
/ / S e t t o t h e n e x t o f f e r ' s j w t I d
/ / S e t t o t h e n e x t o f f e r ' s j w t I d
await this . $updateSettings ( {
await this . $saveUserSettings ( this . activeDid , {
lastAckedOfferToUserProjectsJwtId :
lastAckedOfferToUserProjectsJwtId :
this . newOffersToUserProjects [ index + 1 ] . jwtId ,
this . newOffersToUserProjects [ index + 1 ] . jwtId ,
} ) ;
} ) ;
} else {
} else {
/ / i t ' s t h e l a s t e n t r y ( o r n o t f o u n d ) , s o j u s t k e e p i t t h e s a m e
/ / i t ' s t h e l a s t e n t r y ( o r n o t f o u n d ) , s o j u s t k e e p i t t h e s a m e
await this . $updateSettings ( {
await this . $saveUserSettings ( this . activeDid , {
lastAckedOfferToUserProjectsJwtId :
lastAckedOfferToUserProjectsJwtId :
this . lastAckedOfferToUserProjectsJwtId ,
this . lastAckedOfferToUserProjectsJwtId ,
} ) ;
} ) ;
@ -343,5 +533,382 @@ export default class NewActivityView extends Vue {
async handleSeeAllOffersToUserProjects ( ) {
async handleSeeAllOffersToUserProjects ( ) {
this . $router . push ( "/recent-offers-to-user-projects" ) ;
this . $router . push ( "/recent-offers-to-user-projects" ) ;
}
}
async expandStarredProjectChangesAndMarkRead ( ) {
this . showStarredProjectChangesDetails =
! this . showStarredProjectChangesDetails ;
if (
this . showStarredProjectChangesDetails &&
this . newStarredProjectChanges . length > 0
) {
await this . $saveUserSettings ( this . activeDid , {
lastAckedStarredPlanChangesJwtId :
this . newStarredProjectChanges [ 0 ] . plan . jwtId ,
} ) ;
this . notify . info (
"The starred project changes are now marked read. Click in the list to keep them unread." ,
TIMEOUTS . LONG ,
) ;
}
}
async markStarredProjectChangesAsReadStartingWith ( jwtId : string ) {
const index = this . newStarredProjectChanges . findIndex (
( change ) => change . plan . jwtId === jwtId ,
) ;
if ( index !== - 1 && index < this . newStarredProjectChanges . length - 1 ) {
/ / S e t t o t h e n e x t c h a n g e ' s j w t I d
await this . $saveUserSettings ( this . activeDid , {
lastAckedStarredPlanChangesJwtId :
this . newStarredProjectChanges [ index + 1 ] . plan . jwtId ,
} ) ;
} else {
/ / i t ' s t h e l a s t e n t r y ( o r n o t f o u n d ) , s o j u s t k e e p i t t h e s a m e
await this . $saveUserSettings ( this . activeDid , {
lastAckedStarredPlanChangesJwtId : this . lastAckedStarredPlanChangesJwtId ,
} ) ;
}
this . notify . info (
"All starred project changes above that line are marked as unread." ,
TIMEOUTS . STANDARD ,
) ;
}
/ * *
* Analyzes differences between current plans and their previous claims
*
* Walks through a list of PlanSummaryAndPreviousClaim items and stores the
* differences between the previous claim and the current plan . This method
* extracts the claim from the wrappedClaimBefore object and compares relevant
* fields with the current plan .
*
* @ param planChanges Array of PlanSummaryAndPreviousClaim objects to analyze
* /
analyzePlanDifferences ( planChanges : Array < PlanSummaryAndPreviousClaim > ) {
this . planDifferences = { } ;
for ( const planChange of planChanges ) {
const currentPlan : PlanSummaryRecord = planChange . plan ;
const wrappedClaim : GenericCredWrapper < PlanActionClaim > =
planChange . wrappedClaimBefore ;
/ / E x t r a c t t h e a c t u a l c l a i m f r o m t h e w r a p p e d c l a i m
let previousClaim : PlanActionClaim ;
const embeddedClaim : PlanActionClaim = wrappedClaim . claim ;
if (
embeddedClaim &&
typeof embeddedClaim === "object" &&
"credentialSubject" in embeddedClaim
) {
/ / I t ' s a V e r i f i a b l e C r e d e n t i a l
previousClaim =
( embeddedClaim . credentialSubject as PlanActionClaim ) || embeddedClaim ;
} else {
/ / I t ' s a d i r e c t c l a i m
previousClaim = embeddedClaim ;
}
if ( ! previousClaim || ! currentPlan . handleId ) {
continue ;
}
const differences : Record < string , { old : unknown ; new : unknown } > = { } ;
/ / C o m p a r e n a m e
const normalizedOldName = this . normalizeValueForComparison (
previousClaim . name ,
) ;
const normalizedNewName = this . normalizeValueForComparison (
currentPlan . name ,
) ;
if ( ! R . equals ( normalizedOldName , normalizedNewName ) ) {
differences . name = {
old : previousClaim . name ,
new : currentPlan . name ,
} ;
}
/ / C o m p a r e d e s c r i p t i o n
const normalizedOldDescription = this . normalizeValueForComparison (
previousClaim . description ,
) ;
const normalizedNewDescription = this . normalizeValueForComparison (
currentPlan . description ,
) ;
if ( ! R . equals ( normalizedOldDescription , normalizedNewDescription ) ) {
differences . description = {
old : previousClaim . description ,
new : currentPlan . description ,
} ;
}
/ / C o m p a r e l o c a t i o n ( c o m b i n e l a t i t u d e a n d l o n g i t u d e i n t o o n e r o w )
const oldLat = this . normalizeValueForComparison (
previousClaim . location ? . geo ? . latitude ,
) ;
const oldLon = this . normalizeValueForComparison (
previousClaim . location ? . geo ? . longitude ,
) ;
const newLat = this . normalizeValueForComparison ( currentPlan . locLat ) ;
const newLon = this . normalizeValueForComparison ( currentPlan . locLon ) ;
if ( ! R . equals ( oldLat , newLat ) || ! R . equals ( oldLon , newLon ) ) {
differences . location = {
old : this . formatLocationValue ( oldLat , oldLon , true ) ,
new : this . formatLocationValue ( newLat , newLon , false ) ,
} ;
}
/ / C o m p a r e a g e n t ( i s s u e r )
const oldAgent = didInfoOrNobody (
previousClaim . agent ? . identifier ,
this . activeDid ,
this . allMyDids ,
this . allContacts ,
) ;
const newAgent = didInfoOrNobody (
currentPlan . agentDid ,
this . activeDid ,
this . allMyDids ,
this . allContacts ,
) ;
const normalizedOldAgent = this . normalizeValueForComparison ( oldAgent ) ;
const normalizedNewAgent = this . normalizeValueForComparison ( newAgent ) ;
if ( ! R . equals ( normalizedOldAgent , normalizedNewAgent ) ) {
differences . agent = {
old : oldAgent ,
new : newAgent ,
} ;
}
/ / C o m p a r e s t a r t t i m e
const oldStartTime = previousClaim . startTime ;
const newStartTime = currentPlan . startTime ;
const normalizedOldStartTime =
this . normalizeDateForComparison ( oldStartTime ) ;
const normalizedNewStartTime =
this . normalizeDateForComparison ( newStartTime ) ;
if ( ! R . equals ( normalizedOldStartTime , normalizedNewStartTime ) ) {
differences . startTime = {
old : oldStartTime ,
new : newStartTime ,
} ;
}
/ / C o m p a r e e n d t i m e
const oldEndTime = previousClaim . endTime ;
const newEndTime = currentPlan . endTime ;
const normalizedOldEndTime = this . normalizeDateForComparison ( oldEndTime ) ;
const normalizedNewEndTime = this . normalizeDateForComparison ( newEndTime ) ;
if ( ! R . equals ( normalizedOldEndTime , normalizedNewEndTime ) ) {
differences . endTime = {
old : oldEndTime ,
new : newEndTime ,
} ;
}
/ / C o m p a r e i m a g e
const oldImage = previousClaim . image ;
const newImage = currentPlan . image ;
const normalizedOldImage = this . normalizeValueForComparison ( oldImage ) ;
const normalizedNewImage = this . normalizeValueForComparison ( newImage ) ;
if ( ! R . equals ( normalizedOldImage , normalizedNewImage ) ) {
differences . image = {
old : oldImage ,
new : newImage ,
} ;
}
/ / C o m p a r e u r l
const oldUrl = previousClaim . url ;
const newUrl = currentPlan . url ;
const normalizedOldUrl = this . normalizeValueForComparison ( oldUrl ) ;
const normalizedNewUrl = this . normalizeValueForComparison ( newUrl ) ;
if ( ! R . equals ( normalizedOldUrl , normalizedNewUrl ) ) {
differences . url = {
old : oldUrl ,
new : newUrl ,
} ;
}
/ / S t o r e d i f f e r e n c e s i f a n y w e r e f o u n d
if ( ! R . isEmpty ( differences ) ) {
this . planDifferences [ currentPlan . handleId ] = differences ;
logger . debug (
"[NewActivityView] Plan differences found for" ,
currentPlan . handleId ,
differences ,
) ;
}
}
logger . debug (
"[NewActivityView] Analyzed" ,
planChanges . length ,
"plan changes, found differences in" ,
Object . keys ( this . planDifferences ) . length ,
"plans" ,
) ;
}
/ * *
* Normalizes values for comparison - treats null , undefined , and empty string as equivalent
*
* @ param value The value to normalize
* @ returns The normalized value ( null for null / undefined / empty , otherwise the original value )
* /
normalizeValueForComparison < T > ( value : T | null | undefined ) : T | null {
if ( value === null || value === undefined || value === "" ) {
return null ;
}
return value ;
}
/ * *
* Normalizes date values for comparison by converting strings to Date objects
* Returns null for null / undefined / empty values , Date objects for valid date strings
* /
normalizeDateForComparison ( value : unknown ) : Date | null {
if ( value === null || value === undefined || value === "" ) {
return null ;
}
if ( typeof value === "string" ) {
const date = new Date ( value ) ;
/ / C h e c k i f t h e d a t e i s v a l i d
return isNaN ( date . getTime ( ) ) ? null : date ;
}
if ( value instanceof Date ) {
return isNaN ( value . getTime ( ) ) ? null : value ;
}
return null ;
}
/ * *
* Gets the differences for a specific plan by handle ID
*
* @ param handleId The handle ID of the plan to get differences for
* @ returns The differences object or null if no differences found
* /
getPlanDifferences (
handleId : string ,
) : Record < string , { old : unknown ; new : unknown } > | null {
return this . planDifferences [ handleId ] || null ;
}
/ * *
* Formats a field value for display in the UI
*
* @ param value The value to format
* @ returns A human - readable string representation
* /
formatFieldValue ( value : unknown ) : string {
if ( value === null || value === undefined ) {
return "Not set" ;
}
if ( typeof value === "string" ) {
const stringValue = value || "Empty" ;
/ / C h e c k i f i t ' s a d a t e / t i m e s t r i n g
if ( this . isDateTimeString ( stringValue ) ) {
return this . formatDateTime ( stringValue ) ;
}
/ / C h e c k i f i t ' s a U R L
if ( this . isUrl ( stringValue ) ) {
return stringValue ; / / K e e p U R L s a s - i s f o r n o w
}
return stringValue ;
}
if ( typeof value === "number" ) {
return value . toString ( ) ;
}
if ( typeof value === "boolean" ) {
return value ? "Yes" : "No" ;
}
/ / F o r c o m p l e x o b j e c t s , s t r i n g i f y
const stringified = JSON . stringify ( value ) ;
return stringified ;
}
/ * *
* Checks if a string appears to be a date / time string
* /
isDateTimeString ( value : string ) : boolean {
if ( ! value ) return false ;
/ / C h e c k f o r I S O 8 6 0 1 f o r m a t o r o t h e r c o m m o n d a t e f o r m a t s
const dateRegex = /^\d{4}-\d{2}-\d{2}(T\d{2}:\d{2}:\d{2})?(\.\d{3})?Z?$/ ;
return dateRegex . test ( value ) || ! isNaN ( Date . parse ( value ) ) ;
}
/ * *
* Checks if a string is a URL
* /
isUrl ( value : string ) : boolean {
if ( ! value ) return false ;
try {
new URL ( value ) ;
return true ;
} catch {
return false ;
}
}
/ * *
* Formats a date / time string for display
* /
formatDateTime ( value : string ) : string {
try {
const date = new Date ( value ) ;
return date . toLocaleString ( ) ;
} catch {
return value ; / / R e t u r n o r i g i n a l i f p a r s i n g f a i l s
}
}
/ * *
* Gets a human - readable field name for display
*
* @ param fieldName The internal field name
* @ returns A formatted field name for display
* /
getDisplayFieldName ( fieldName : string ) : string {
const fieldNameMap : Record < string , string > = {
name : "Name" ,
description : "Description" ,
location : "Location" ,
agent : "Agent" ,
startTime : "Start Time" ,
endTime : "End Time" ,
image : "Image" ,
url : "URL" ,
} ;
return fieldNameMap [ fieldName ] || fieldName ;
}
/ * *
* Formats location values for display
*
* @ param latitude The latitude value
* @ param longitude The longitude value
* @ param isOldValue Whether this is the old value ( true ) or new value ( false )
* @ returns A formatted location string
* /
formatLocationValue (
latitude : number | undefined | null ,
longitude : number | undefined | null ,
isOldValue : boolean = false ,
) : string {
if ( latitude == null && longitude == null ) {
return "Not set" ;
}
/ / I f t h e r e ' s a n y l o c a t i o n d a t a , s h o w g e n e r i c l a b e l s i n s t e a d o f c o o r d i n a t e s
if ( isOldValue ) {
return "A Location" ;
} else {
return "New Location" ;
}
}
}
}
< / script >
< / script >