Compare commits

..

13 Commits

Author SHA1 Message Date
Jose Olarte III
f71c76fcd3 Fix: removed links and elements
- Removed links from icons and giver name
- Removed confirm button
- Lint fixes
2025-03-21 15:52:45 +08:00
Matthew Raymer
d024db2258 feature: move amount and wire it up 2025-03-21 07:03:31 +00:00
Jose Olarte III
b40604f8a6 Amount above arrow
- Needs wiring up
2025-03-19 20:47:10 +08:00
Jose Olarte III
436f40813c Source-destination compacted
- Narrower max-width
- Element sizes adjusted
- Switched to a more controllable unit for widths and heights
2025-03-17 21:03:58 +08:00
Jose Olarte III
77b296b606 Identicon responsive size fix + lint-fix 2025-03-17 17:59:59 +08:00
Matthew Raymer
8f7d794962 fix: improve image handling and icon support
- Fix image load event handler signature
- Add alt text for accessibility
- Add building icon to FontAwesome library

Technical Changes:
- Update cacheImage event to pass only image URL
- Add proper alt text for activity images
- Add faBuilding icon to FontAwesome library

This improves image handling and accessibility while adding
needed icon support for the activity feed interface.
2025-03-07 12:58:14 +00:00
Matthew Raymer
fa7d6317b9 feat: add claim confirmation functionality to activity feed
- Add confirm button functionality to ActivityListItem
- Implement confirmation logic in HomeView
- Add proper button state handling and validation

Technical Changes:
- Add canConfirm computed property to validate confirmation ability
- Add handleConfirmClick method with proper error handling
- Pass required props (isRegistered, activeDid, confirmerIdList)
- Add confirmation dialog with user verification
- Implement claim submission with proper cleanup
- Add visual feedback for button states
- Update feed after successful confirmation

UI/UX Improvements:
- Add disabled state styling for confirm button
- Show proper error messages for invalid confirmation attempts
- Add loading and success notifications
- Improve button accessibility with proper states

Bug Fixes:
- Make apiServer optional in settings type
- Fix settings update during registration
- Add proper type checking for claim confirmation

This adds the ability to confirm claims directly from the
activity feed with proper validation, error handling, and
user feedback. The confirmation flow matches the existing
claim view confirmation functionality.
2025-03-07 10:22:53 +00:00
Jose Olarte III
4a75cdf20e Homeview changes
- Moved activity image further up the frame
- Added placeholder icon for projects
- Minor fixes
2025-03-04 20:38:14 +08:00
Jose Olarte III
79fdb9e570 Homeview design adjustments
- Added markup for poster info (needs wiring)
- Repositioned giver and receiver type icons to beside their labels
- Added "Confirm" button to bottom right of item card (timestamp repositioned to poster info as a result)
- Spacing tweaks
2025-03-03 19:34:09 +08:00
Jose Olarte III
aa09827317 Type fixes 2025-02-28 21:00:03 +08:00
Matthew Raymer
cc1780bd01 refactor: extract ActivityListItem into separate component
- Move activity list item markup from HomeView to new component
- Improve code organization and reusability
- Pass required props for claim handling and image viewing
- Maintain existing functionality while reducing component complexity
- Clean up unused commented code in HomeView

This refactor improves code maintainability by extracting the activity
feed item logic into its own component.
2025-02-28 09:34:59 +00:00
Jose Olarte III
e5d9c25ad4 Comments 2025-02-27 21:21:01 +08:00
Jose Olarte III
ef8c2e6093 In-progress: homeview design refresh
I had to comment out line 544 because it was causing errors (and seemed redundant?)
2025-02-27 21:12:29 +08:00
14 changed files with 544 additions and 216 deletions

View File

@@ -0,0 +1,334 @@
<template>
<li>
<!-- Last viewed separator -->
<div
class="border-b border-dashed border-slate-300 text-orange-400 mt-4 mb-6 font-bold text-sm"
v-if="record.jwtId == lastViewedClaimId"
>
<span class="block w-fit mx-auto -mb-2.5 bg-white px-2">
You've already seen all the following
</span>
</div>
<div class="bg-slate-100 rounded-t-md border border-slate-300 p-3 sm:p-4">
<div class="flex items-center gap-2 mb-6">
<img
src="https://a.fsdn.com/con/app/proj/identicons/screenshots/225506.jpg"
class="size-8 object-cover rounded-full"
/>
<div>
<h3 class="font-semibold">
{{
record.giver.known ? record.giver.displayName : "Anonymous Giver"
}}
</h3>
<p class="ms-auto text-xs text-slate-500 italic">
{{ friendlyDate }}
</p>
</div>
</div>
<!-- Record Image -->
<div
v-if="record.image"
class="bg-cover mb-6 -mx-3 sm:-mx-4"
:style="`background-image: url(${record.image});`"
>
<a
class="block bg-slate-100/50 backdrop-blur-md px-6 py-4 cursor-pointer"
@click="$emit('viewImage', record.image)"
>
<img
class="w-full h-auto max-w-lg max-h-96 object-contain mx-auto drop-shadow-md"
:src="record.image"
@load="$emit('cacheImage', record.image)"
alt="Activity image"
/>
</a>
</div>
<div class="relative flex justify-between gap-4 max-w-lg mx-auto mb-5">
<!-- Source -->
<div
class="w-28 sm:w-40 text-center bg-white border border-slate-200 rounded p-2 sm:p-3"
>
<div class="relative w-fit mx-auto">
<template v-if="record.giver.profileImageUrl">
<EntityIcon
:profile-image-url="record.giver.profileImageUrl"
:class="[
!record.providerPlanName
? 'rounded-full size-[3rem] sm:size-[4rem] object-cover'
: 'rounded size-[3rem] sm:size-[4rem] object-cover',
]"
/>
</template>
<template v-else>
<!-- Project Icon -->
<template v-if="record.providerPlanName">
<ProjectIcon
:entityId="record.providerPlanName"
:iconSize="48"
class="rounded size-[3rem] sm:size-[4rem] *:w-full *:h-full"
/>
</template>
<!-- Identicon for DIDs -->
<template v-else-if="record.giver.did">
<img
:src="`https://a.fsdn.com/con/app/proj/identicons/screenshots/225506.jpg`"
class="rounded-full size-[3rem] sm:size-[4rem]"
alt="Identicon"
/>
</template>
<!-- Unknown Person -->
<template v-else>
<fa
icon="person-circle-question"
class="text-slate-300 text-[3rem] sm:text-[4rem]"
/>
</template>
</template>
</div>
<div class="text-xs mt-2 line-clamp-3 sm:line-clamp-2">
<fa
:icon="record.providerPlanName ? 'building' : 'user'"
class="fa-fw text-slate-400"
/>
{{ record.giver.displayName }}
</div>
</div>
<!-- Arrow -->
<div
class="absolute inset-x-28 sm:inset-x-40 mx-2 top-1/2 -translate-y-1/2"
>
<div class="text-sm text-center leading-none font-semibold">
{{ fetchAmount }}
</div>
<div class="flex items-center">
<hr
class="grow border-t-[18px] sm:border-t-[24px] border-slate-300"
/>
<div
class="shrink-0 w-0 h-0 border border-slate-300 border-t-[20px] sm:border-t-[25px] border-t-transparent border-b-[20px] sm:border-b-[25px] border-b-transparent border-s-[27px] sm:border-s-[34px] border-e-0"
></div>
</div>
</div>
<!-- Destination -->
<div
class="w-28 sm:w-40 text-center bg-white border border-slate-200 rounded p-2 sm:p-3"
>
<div class="relative w-fit mx-auto">
<template v-if="record.receiver.profileImageUrl">
<EntityIcon
:profile-image-url="record.receiver.profileImageUrl"
:class="[
!record.recipientProjectName
? 'rounded-full size-[3rem] sm:size-[4rem] object-cover'
: 'rounded size-[3rem] sm:size-[4rem] object-cover',
]"
/>
</template>
<template v-else>
<!-- Project Icon -->
<template v-if="record.recipientProjectName">
<ProjectIcon
:entityId="record.recipientProjectName"
:iconSize="48"
class="rounded size-[3rem] sm:size-[4rem] *:w-full *:h-full"
/>
</template>
<!-- Identicon for DIDs -->
<template v-else-if="record.receiver.did">
<img
:src="`https://a.fsdn.com/con/app/proj/identicons/screenshots/225506.jpg`"
class="rounded-full size-[3rem] sm:size-[4rem]"
alt="Identicon"
/>
</template>
<!-- Unknown Person -->
<template v-else>
<fa
icon="person-circle-question"
class="text-slate-300 text-[3rem] sm:text-[4rem]"
/>
</template>
</template>
</div>
<div class="text-xs mt-2 line-clamp-3 sm:line-clamp-2">
<fa
:icon="record.recipientProjectName ? 'building' : 'user'"
class="fa-fw text-slate-400"
/>
{{ record.receiver.displayName }}
</div>
</div>
</div>
<!-- Description -->
<p class="font-medium">
<a @click="$emit('loadClaim', record.jwtId)" class="cursor-pointer">
{{ description }}
</a>
</p>
<p class="text-sm">{{ subDescription }}</p>
</div>
<div
class="flex items-center gap-2 text-lg bg-slate-300 rounded-b-md px-3 sm:px-4 py-1 sm:py-2"
>
<a @click="$emit('loadClaim', record.jwtId)" class="cursor-pointer">
<fa icon="circle-info" class="fa-fw text-slate-500" />
</a>
</div>
</li>
</template>
<script lang="ts">
import { Component, Prop, Vue } from "vue-facing-decorator";
import { GiveRecordWithContactInfo } from "../types";
import EntityIcon from "./EntityIcon.vue";
import { isGiveClaimType, notifyWhyCannotConfirm } from "../libs/util";
import { containsHiddenDid } from "../libs/endorserServer";
import ProjectIcon from "./ProjectIcon.vue";
@Component({
components: {
EntityIcon,
ProjectIcon,
},
})
export default class ActivityListItem extends Vue {
@Prop() record!: GiveRecordWithContactInfo;
@Prop() lastViewedClaimId?: string;
@Prop() isRegistered!: boolean;
@Prop() activeDid!: string;
@Prop() confirmerIdList?: string[];
get fetchAmount(): string {
const claim =
(this.record.fullClaim as unknown).claim || this.record.fullClaim;
const amount = claim.object?.amountOfThisGood
? this.displayAmount(claim.object.unitCode, claim.object.amountOfThisGood)
: "";
return amount;
}
private formatParticipantInfo(): string {
const { giver, receiver } = this.record;
// Both participants are known contacts
if (giver.known && receiver.known) {
return `${giver.displayName} gave to ${receiver.displayName}`;
}
// Only giver is known
if (giver.known) {
const recipient = this.record.recipientProjectName
? `the project "${this.record.recipientProjectName}"`
: receiver.displayName;
return `${giver.displayName} gave to ${recipient}`;
}
// Only receiver is known
if (receiver.known) {
const provider = this.record.providerPlanName
? `the project "${this.record.providerPlanName}"`
: giver.displayName;
return `${receiver.displayName} received from ${provider}`;
}
// Neither is known
return this.formatUnknownParticipants();
}
private formatUnknownParticipants(): string {
const { giver, receiver, providerPlanName, recipientProjectName } =
this.record;
if (providerPlanName || recipientProjectName) {
const from = providerPlanName
? `the project "${providerPlanName}"`
: giver.displayName;
const to = recipientProjectName
? `the project "${recipientProjectName}"`
: receiver.displayName;
return `from ${from} to ${to}`;
}
return giver.displayName === receiver.displayName
? `between two who are ${giver.displayName}`
: `from ${giver.displayName} to ${receiver.displayName}`;
}
get description(): string {
const claim =
(this.record.fullClaim as unknown).claim || this.record.fullClaim;
if (!claim.description) {
return "something not described";
}
return `${claim.description}`;
}
get subDescription(): string {
const participants = this.formatParticipantInfo();
return `${participants}`;
}
private displayAmount(code: string, amt: number) {
return `${amt} ${this.currencyShortWordForCode(code, amt === 1)}`;
}
private currencyShortWordForCode(unitCode: string, single: boolean) {
return unitCode === "HUR" ? (single ? "hour" : "hours") : unitCode;
}
get formattedTimestamp() {
// Add your timestamp formatting logic here
return this.record.timestamp;
}
get canConfirm(): boolean {
if (!this.isRegistered) return false;
if (!isGiveClaimType(this.record.fullClaim?.["@type"])) return false;
if (this.confirmerIdList?.includes(this.activeDid)) return false;
if (this.record.issuerDid === this.activeDid) return false;
if (containsHiddenDid(this.record.fullClaim)) return false;
return true;
}
handleConfirmClick() {
if (!this.canConfirm) {
notifyWhyCannotConfirm(
this.$notify,
this.isRegistered,
this.record.fullClaim?.["@type"],
this.record,
this.activeDid,
this.confirmerIdList,
);
return;
}
this.$emit("confirmClaim", this.record);
}
get friendlyDate(): string {
const date = new Date(this.record.issuedAt);
return date.toLocaleDateString(undefined, {
year: "numeric",
month: "short",
day: "numeric",
});
}
}
</script>

View File

@@ -1,7 +1,7 @@
<template> <template>
<!-- QUICK NAV --> <!-- QUICK NAV -->
<nav id="QuickNav" class="fixed bottom-0 left-0 right-0 bg-slate-200 z-50"> <nav id="QuickNav" class="fixed bottom-0 left-0 right-0 bg-slate-200 z-50">
<ul class="flex text-2xl px-6 py-2 gap-1 max-w-3xl mx-auto"> <ul class="flex text-2xl p-2 gap-2 max-w-3xl mx-auto">
<!-- Home Feed --> <!-- Home Feed -->
<li <li
:class="{ :class="{

View File

@@ -20,7 +20,7 @@ export type Settings = {
// active Decentralized ID // active Decentralized ID
activeDid?: string; // only used in the MASTER_SETTINGS_KEY entry activeDid?: string; // only used in the MASTER_SETTINGS_KEY entry
apiServer: string; // API server URL apiServer?: string; // API server URL
filterFeedByNearby?: boolean; // filter by nearby filterFeedByNearby?: boolean; // filter by nearby
filterFeedByVisible?: boolean; // filter by visible users ie. anyone not hidden filterFeedByVisible?: boolean; // filter by visible users ie. anyone not hidden

View File

@@ -58,6 +58,7 @@ import {
faHandHoldingDollar, faHandHoldingDollar,
faHandHoldingHeart, faHandHoldingHeart,
faHouseChimney, faHouseChimney,
faImage,
faImagePortrait, faImagePortrait,
faLeftRight, faLeftRight,
faLightbulb, faLightbulb,
@@ -87,6 +88,7 @@ import {
faUser, faUser,
faUsers, faUsers,
faXmark, faXmark,
faBuilding,
} from "@fortawesome/free-solid-svg-icons"; } from "@fortawesome/free-solid-svg-icons";
library.add( library.add(
@@ -137,6 +139,7 @@ library.add(
faHandHoldingDollar, faHandHoldingDollar,
faHandHoldingHeart, faHandHoldingHeart,
faHouseChimney, faHouseChimney,
faImage,
faImagePortrait, faImagePortrait,
faLeftRight, faLeftRight,
faLightbulb, faLightbulb,
@@ -166,6 +169,7 @@ library.add(
faUser, faUser,
faUsers, faUsers,
faXmark, faXmark,
faBuilding,
); );
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";

20
src/types/index.ts Normal file
View File

@@ -0,0 +1,20 @@
export interface GiveRecordWithContactInfo {
jwtId: string;
fullClaim: unknown; // Replace with proper type
giver: {
known: boolean;
displayName: string;
profileImageUrl?: string;
};
receiver: {
known: boolean;
displayName: string;
profileImageUrl?: string;
};
providerPlanName?: string;
recipientProjectName?: string;
description?: string;
subDescription?: string;
image?: string;
timestamp: string;
}

View File

@@ -256,7 +256,7 @@
<span class="mb-2 font-bold">Location for Searches</span> <span class="mb-2 font-bold">Location for Searches</span>
<router-link <router-link
:to="{ name: 'search-area' }" :to="{ name: 'search-area' }"
class="text-m bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-4 py-2 rounded-md mb-2" class="text-m 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-1.5 py-2 rounded-md mb-2"
> >
{{ isSearchAreasSet ? "Change" : "Set" }} Search Area {{ isSearchAreasSet ? "Change" : "Set" }} Search Area
</router-link> </router-link>

View File

@@ -204,7 +204,7 @@
<div v-if="libsUtil.isGiveAction(veriClaim)"> <div v-if="libsUtil.isGiveAction(veriClaim)">
<div class="flex columns-3"> <div class="flex columns-3">
<button <button
class="col-span-1 bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white mt-2 px-4 py-2 rounded-md" class="col-span-1 bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white mt-2 px-4 py-2 rounded-md"
v-if=" v-if="
libsUtil.isGiveRecordTheUserCanConfirm( libsUtil.isGiveRecordTheUserCanConfirm(
isRegistered, isRegistered,

View File

@@ -106,7 +106,7 @@
/> />
<button <button
href="" href=""
class="text-md bg-gradient-to-b shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white ml-3 px-3 py-1.5 rounded-md" class="text-md bg-gradient-to-b shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white ml-2 px-1 py-1 rounded-md"
:style=" :style="
contactsSelected.length > 0 contactsSelected.length > 0
? 'background-image: linear-gradient(to bottom, #3b82f6, #1e40af);' ? 'background-image: linear-gradient(to bottom, #3b82f6, #1e40af);'
@@ -127,7 +127,7 @@
<div class="w-full text-right"> <div class="w-full text-right">
<button <button
href="" href=""
class="text-md 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-3 py-1.5 rounded-md" class="text-md 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-1 py-1 rounded-md"
@click="toggleShowContactAmounts()" @click="toggleShowContactAmounts()"
> >
{{ {{
@@ -177,7 +177,14 @@
data-testId="contactListItem" data-testId="contactListItem"
> >
<div class="grow overflow-hidden"> <div class="grow overflow-hidden">
<div class="flex items-center gap-3"> <div class="flex items-center">
<EntityIcon
:contact="contact"
:iconSize="24"
class="inline-block align-text-bottom border border-slate-300 rounded cursor-pointer"
@click="showLargeIdenticon = contact"
/>
<input <input
type="checkbox" type="checkbox"
v-if="!showGiveNumbers" v-if="!showGiveNumbers"
@@ -194,97 +201,94 @@
data-testId="contactCheckOne" data-testId="contactCheckOne"
/> />
<EntityIcon <h2
:contact="contact" class="text-base font-semibold ml-2 w-1/3 truncate flex-shrink-0"
:iconSize="48" >
class="inline-block align-text-bottom border border-slate-300 rounded cursor-pointer overflow-hidden"
@click="showLargeIdenticon = contact"
/>
<h2 class="text-base font-semibold w-1/3 truncate flex-shrink-0">
{{ contactNameNonBreakingSpace(contact.name) }} {{ contactNameNonBreakingSpace(contact.name) }}
</h2> </h2>
<span> <span>
<div class="flex gap-2 items-center"> <div class="flex items-center">
<router-link <router-link
:to="{ :to="{
path: '/did/' + encodeURIComponent(contact.did), path: '/did/' + encodeURIComponent(contact.did),
}" }"
title="See more about this person" title="See more about this person"
> >
<fa icon="circle-info" class="text-xl text-blue-500" /> <fa icon="circle-info" class="text-xl text-blue-500 ml-4" />
</router-link> </router-link>
<span class="text-sm overflow-hidden">{{ <span class="ml-4 text-sm overflow-hidden">{{
libsUtil.shortDid(contact.did) libsUtil.shortDid(contact.did)
}}</span> }}</span>
</div> </div>
<div class="text-sm"> <div class="ml-4 text-sm">
{{ contact.notes }} {{ contact.notes }}
</div> </div>
</span> </span>
</div> </div>
<div <div id="ContactActions" class="flex gap-1.5 mt-2">
v-if="showGiveNumbers && contact.did != activeDid" <div
class="ml-auto flex gap-1.5 mt-2" v-if="showGiveNumbers && contact.did != activeDid"
> class="ml-auto flex gap-1.5"
<button
class="text-sm 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.5 rounded-l-md"
@click="confirmShowGiftedDialog(contact.did, activeDid)"
:title="givenToMeDescriptions[contact.did] || ''"
> >
From: <button
<br /> class="text-sm 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.5 rounded-l-md"
{{ @click="confirmShowGiftedDialog(contact.did, activeDid)"
/* eslint-disable prettier/prettier */ :title="givenToMeDescriptions[contact.did] || ''"
this.showGiveTotals >
? ((givenToMeConfirmed[contact.did] || 0) From:
+ (givenToMeUnconfirmed[contact.did] || 0)) <br />
: this.showGiveConfirmed {{
? (givenToMeConfirmed[contact.did] || 0) /* eslint-disable prettier/prettier */
: (givenToMeUnconfirmed[contact.did] || 0) this.showGiveTotals
/* eslint-enable prettier/prettier */ ? ((givenToMeConfirmed[contact.did] || 0)
}} + (givenToMeUnconfirmed[contact.did] || 0))
</button> : this.showGiveConfirmed
? (givenToMeConfirmed[contact.did] || 0)
: (givenToMeUnconfirmed[contact.did] || 0)
/* eslint-enable prettier/prettier */
}}
</button>
<button <button
class="text-sm bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white -ml-1.5 px-2 py-1.5 rounded-r-md border-l" class="text-sm bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white -ml-1.5 px-2 py-1.5 rounded-r-md border-l"
@click="confirmShowGiftedDialog(activeDid, contact.did)" @click="confirmShowGiftedDialog(activeDid, contact.did)"
:title="givenByMeDescriptions[contact.did] || ''" :title="givenByMeDescriptions[contact.did] || ''"
> >
To: To:
<br /> <br />
{{ {{
/* eslint-disable prettier/prettier */ /* eslint-disable prettier/prettier */
this.showGiveTotals this.showGiveTotals
? ((givenByMeConfirmed[contact.did] || 0) ? ((givenByMeConfirmed[contact.did] || 0)
+ (givenByMeUnconfirmed[contact.did] || 0)) + (givenByMeUnconfirmed[contact.did] || 0))
: this.showGiveConfirmed : this.showGiveConfirmed
? (givenByMeConfirmed[contact.did] || 0) ? (givenByMeConfirmed[contact.did] || 0)
: (givenByMeUnconfirmed[contact.did] || 0) : (givenByMeUnconfirmed[contact.did] || 0)
/* eslint-enable prettier/prettier */ /* eslint-enable prettier/prettier */
}} }}
</button> </button>
<button <button
class="text-sm 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.5 rounded-md border border-blue-400" class="text-sm 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.5 rounded-md border border-blue-400"
@click="openOfferDialog(contact.did, contact.name)" @click="openOfferDialog(contact.did, contact.name)"
data-testId="offerButton" data-testId="offerButton"
> >
Offer Offer
</button> </button>
<router-link <router-link
:to="{ :to="{
name: 'contact-amounts', name: 'contact-amounts',
query: { contactDid: contact.did }, query: { contactDid: contact.did },
}" }"
class="text-sm 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.5 rounded-md border border-slate-400" class="text-sm 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.5 rounded-md border border-slate-400"
title="See more given activity" title="See more given activity"
> >
<fa icon="file-lines" class="fa-fw" /> <fa icon="file-lines" class="fa-fw" />
</router-link> </router-link>
</div>
</div> </div>
</div> </div>
</li> </li>
@@ -306,7 +310,7 @@
/> />
<button <button
href="" href=""
class="text-md bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white ml-3 px-3 py-1.5 rounded-md" class="text-md bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white ml-2 px-1 py-1 rounded-md"
:style=" :style="
contactsSelected.length > 0 contactsSelected.length > 0
? 'background-image: linear-gradient(to bottom, #3b82f6, #1e40af);' ? 'background-image: linear-gradient(to bottom, #3b82f6, #1e40af);'

View File

@@ -3,7 +3,7 @@
<TopMessage /> <TopMessage />
<!-- CONTENT --> <!-- CONTENT -->
<section id="Content" class="p-6 pb-24 max-w-3xl mx-auto"> <section id="Content" class="p-2 pb-24 max-w-3xl mx-auto">
<h1 id="ViewHeading" class="text-4xl text-center font-light mb-8"> <h1 id="ViewHeading" class="text-4xl text-center font-light mb-8">
{{ AppString.APP_NAME }} {{ AppString.APP_NAME }}
</h1> </h1>
@@ -186,23 +186,23 @@
</div> </div>
<!-- Results List --> <!-- Results List -->
<div class="bg-slate-100 rounded-md px-4 py-3 mt-4 mb-4"> <div class="mt-4 mb-4">
<div class="flex items-center mb-4"> <div class="flex items-center mb-4">
<h2 class="text-xl font-bold"> <h2 class="text-xl font-bold flex items-center gap-4">
Latest Activity Latest Activity
<button @click="openFeedFilters()"> <button
<span class="text-xs text-white"> v-if="resultsAreFiltered()"
<fa class="bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] px-3 py-1.5 rounded-md text-xs text-white"
v-if="resultsAreFiltered()" @click="openFeedFilters()"
icon="filter" >
class="bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] px-1 py-1.5 rounded-md" <fa icon="filter" class="fa-fw" />
/> </button>
<fa <button
v-else v-else
icon="filter" class="bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] px-3 py-1.5 rounded-md text-xs text-white"
class="bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] px-1 py-1.5 rounded-md" @click="openFeedFilters()"
/> >
</span> <fa icon="filter" class="fa-fw" />
</button> </button>
</h2> </h2>
</div> </div>
@@ -249,117 +249,20 @@
</div> </div>
<InfiniteScroll @reached-bottom="loadMoreGives"> <InfiniteScroll @reached-bottom="loadMoreGives">
<ul id="listLatestActivity" class="border-t border-slate-300"> <ul id="listLatestActivity" class="space-y-4">
<li <ActivityListItem
class="border-b border-slate-300 py-2"
v-for="record in feedData" v-for="record in feedData"
:key="record.jwtId" :key="record.jwtId"
> :record="record"
<div :lastViewedClaimId="feedLastViewedClaimId"
class="border-b border-slate-300 text-orange-400 pb-2 mb-2 font-bold text-sm" :isRegistered="isRegistered"
v-if="record.jwtId == feedLastViewedClaimId" :activeDid="activeDid"
> :confirmerIdList="record.confirmerIdList"
You've already seen all the following @loadClaim="onClickLoadClaim"
</div> @viewImage="openImageViewer"
@cacheImage="cacheImageData"
<div class="grid grid-cols-12"> @confirmClaim="confirmClaim"
<span class="pt-1 col-span-1 justify-self-start"> />
<span>
<fa
icon="circle-user"
:class="
computeKnownPersonIconStyleClassNames(
record.giver.known || record.receiver.known,
)
"
@click="toastUser('This involves your contacts.')"
/>
<fa
icon="gift"
class="pl-3 text-slate-500"
@click="toastUser('This is a gift.')"
/>
</span>
</span>
<span class="col-span-10 justify-self-stretch overflow-hidden">
<!-- show giver and/or receiver profiles... which seemed like a good idea but actually adds clutter
<span
v-if="
record.giver.profileImageUrl ||
record.receiver.profileImageUrl
"
>
<EntityIcon
v-if="record.agentDid !== activeDid"
:icon-size="32"
:profile-image-url="record.giver.profileImageUrl"
class="inline-block align-middle border border-slate-300 rounded-md mr-1"
/>
<fa
v-if="
record.agentDid !== activeDid &&
record.recipientDid !== activeDid &&
!record.fulfillsPlanHandleId
"
icon="ellipsis"
class="text-slate"
/>
<EntityIcon
v-if="
record.recipientDid !== activeDid &&
!record.fulfillsPlanHandleId
"
:iconSize="32"
:profile-image-url="record.receiver.profileImageUrl"
class="inline-block align-middle border border-slate-300 rounded-md ml-1"
/>
</span>
-->
<span class="pl-2 block break-words">
{{ giveDescription(record) }}
</span>
<a @click="onClickLoadClaim(record.jwtId)">
<fa
icon="file-lines"
class="pl-2 text-slate-500 cursor-pointer"
/>
</a>
</span>
<span class="col-span-1 justify-self-end">
<router-link
v-if="record.fulfillsPlanHandleId"
:to="
'/project/' +
encodeURIComponent(record.fulfillsPlanHandleId)
"
>
<fa icon="hammer" class="text-blue-500" />
</router-link>
<router-link
v-if="record.providerPlanHandleId"
:to="
'/project/' +
encodeURIComponent(record.providerPlanHandleId)
"
>
<fa icon="hammer" class="text-blue-500" />
</router-link>
</span>
</div>
<div v-if="record.image" class="w-full">
<div
class="cursor-pointer"
@click="openImageViewer(record.image)"
>
<img
:src="record.image"
class="w-full aspect-[3/2] object-cover rounded-xl mt-2"
alt="shared content"
@load="cacheImageData($event, record.image)"
/>
</div>
</div>
</li>
</ul> </ul>
</InfiniteScroll> </InfiniteScroll>
<div v-if="isFeedLoading"> <div v-if="isFeedLoading">
@@ -401,6 +304,7 @@ import TopMessage from "../components/TopMessage.vue";
import UserNameDialog from "../components/UserNameDialog.vue"; import UserNameDialog from "../components/UserNameDialog.vue";
import ChoiceButtonDialog from "../components/ChoiceButtonDialog.vue"; import ChoiceButtonDialog from "../components/ChoiceButtonDialog.vue";
import ImageViewer from "../components/ImageViewer.vue"; import ImageViewer from "../components/ImageViewer.vue";
import ActivityListItem from "../components/ActivityListItem.vue";
import { import {
AppString, AppString,
NotificationIface, NotificationIface,
@@ -436,6 +340,8 @@ import {
OnboardPage, OnboardPage,
registerSaveAndActivatePasskey, registerSaveAndActivatePasskey,
} from "../libs/util"; } from "../libs/util";
import * as serverUtil from "../libs/endorserServer";
// import { fa0 } from "@fortawesome/free-solid-svg-icons";
interface GiveRecordWithContactInfo extends GiveSummaryRecord { interface GiveRecordWithContactInfo extends GiveSummaryRecord {
giver: { giver: {
@@ -466,6 +372,7 @@ interface GiveRecordWithContactInfo extends GiveSummaryRecord {
TopMessage, TopMessage,
UserNameDialog, UserNameDialog,
ImageViewer, ImageViewer,
ActivityListItem,
}, },
}) })
export default class HomeView extends Vue { export default class HomeView extends Vue {
@@ -559,6 +466,7 @@ export default class HomeView extends Vue {
if (resp.status === 200) { if (resp.status === 200) {
await updateAccountSettings(this.activeDid, { await updateAccountSettings(this.activeDid, {
isRegistered: true, isRegistered: true,
...(await retrieveSettingsForActiveAccount()),
}); });
this.isRegistered = true; this.isRegistered = true;
} }
@@ -1002,5 +910,67 @@ export default class HomeView extends Vue {
this.selectedImage = imageUrl; this.selectedImage = imageUrl;
this.isImageViewerOpen = true; this.isImageViewerOpen = true;
} }
async confirmClaim(record: GiveRecordWithContactInfo) {
this.$notify(
{
group: "modal",
type: "confirm",
title: "Confirm",
text: "Do you personally confirm that this is true?",
onYes: async () => {
const goodClaim = serverUtil.removeSchemaContext(
serverUtil.removeVisibleToDids(
serverUtil.addLastClaimOrHandleAsIdIfMissing(
record.fullClaim,
record.jwtId,
record.handleId,
),
),
);
const confirmationClaim = {
"@context": "https://schema.org",
"@type": "AgreeAction",
object: goodClaim,
};
const result = await serverUtil.createAndSubmitClaim(
confirmationClaim,
this.activeDid,
this.apiServer,
this.axios,
);
if (result.type === "success") {
this.$notify(
{
group: "alert",
type: "success",
title: "Success",
text: "Confirmation submitted.",
},
3000,
);
// Refresh the feed to show updated confirmation status
await this.updateAllFeed();
} else {
console.error("Error submitting confirmation:", result);
this.$notify(
{
group: "alert",
type: "danger",
title: "Error",
text: "There was a problem submitting the confirmation.",
},
5000,
);
}
},
},
-1,
);
}
} }
</script> </script>

View File

@@ -237,7 +237,7 @@
<button <button
data-testId="offerButton" data-testId="offerButton"
@click="openOfferDialog()" @click="openOfferDialog()"
class="block w-full 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-1.5 text-sm leading-tight rounded-md" class="block w-full 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-1 rounded-md"
> >
Offer to this (maybe with conditions)... Offer to this (maybe with conditions)...
</button> </button>
@@ -318,7 +318,7 @@
<div class="text-center"> <div class="text-center">
<button <button
@click="openGiftDialogToProject()" @click="openGiftDialogToProject()"
class="block w-full 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-1.5 text-sm leading-tight rounded-md" class="block w-full 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-1rounded-md"
> >
Given To This... Given To This...
</button> </button>
@@ -470,7 +470,7 @@
<div class="text-center"> <div class="text-center">
<button <button
@click="openGiftDialogFromProject()" @click="openGiftDialogFromProject()"
class="block w-full 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-1.5 text-sm leading-tight rounded-md" class="block w-full 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-1 rounded-md"
> >
Given By This... Given By This...
</button> </button>

View File

@@ -233,7 +233,7 @@
> >
<a <a
@click="onClickLoadProject(project.handleId)" @click="onClickLoadProject(project.handleId)"
class="block py-4 flex gap-4 cursor-pointer" class="block py-4 flex gap-4"
> >
<div class="flex-none"> <div class="flex-none">
<ProjectIcon <ProjectIcon

View File

@@ -48,7 +48,7 @@
*/ */
import path from 'path'; import path from 'path';
import { test, expect } from '@playwright/test'; import { test, expect } from '@playwright/test';
import { importUserAndCloseOnboarding } from './testUtils'; import { importUser } from './testUtils';
/** /**
* Note: by default, this test uses the test image API server. * Note: by default, this test uses the test image API server.
@@ -65,7 +65,7 @@ test('Record item given from image-share', async ({ page }) => {
// Combine title prefix with the random string // Combine title prefix with the random string
const finalTitle = `Gift ${randomString} from image-share`; const finalTitle = `Gift ${randomString} from image-share`;
await importUserAndCloseOnboarding(page, '00'); await importUser(page, '00');
// Record something given // Record something given
await page.goto('./test'); await page.goto('./test');
@@ -84,8 +84,10 @@ test('Record item given from image-share', async ({ page }) => {
await page.getByRole('spinbutton').fill('2'); await page.getByRole('spinbutton').fill('2');
await page.getByRole('button', { name: 'Sign & Send' }).click(); await page.getByRole('button', { name: 'Sign & Send' }).click();
// const recorded = await page.getByText('That gift was recorded.'); // we end up on a page with the onboarding info
await expect(await page.getByText('That gift was recorded.')).toBeVisible(); await page.getByTestId('closeOnboardingAndFinish').click();
await expect(page.getByText('That gift was recorded.')).toBeVisible();
await page.locator('div[role="alert"] button > svg.fa-xmark').click(); // dismiss info alert await page.locator('div[role="alert"] button > svg.fa-xmark').click(); // dismiss info alert
// Refresh home view and check gift // Refresh home view and check gift

View File

@@ -207,6 +207,7 @@ test('Add contact, copy details, delete, and import from paste & from file', asy
// Add another new contact // Add another new contact
await page.getByPlaceholder('URL or DID, Name, Public Key').fill('did:ethr:0x222BB77E6Ff3774d34c751f3c1260866357B677b, User #222, asdf1234'); await page.getByPlaceholder('URL or DID, Name, Public Key').fill('did:ethr:0x222BB77E6Ff3774d34c751f3c1260866357B677b, User #222, asdf1234');
await page.locator('button > svg.fa-plus').click(); await page.locator('button > svg.fa-plus').click();
await expect(page.locator('div[role="alert"]')).toBeVisible();
await expect(page.locator('div[role="alert"] span:has-text("No")')).toBeVisible(); await expect(page.locator('div[role="alert"] span:has-text("No")')).toBeVisible();
await page.locator('div[role="alert"] button:has-text("No")').click(); // don't register await page.locator('div[role="alert"] button:has-text("No")').click(); // don't register
await expect(page.locator('div[role="alert"] span:has-text("Contact Added")')).toBeVisible(); await expect(page.locator('div[role="alert"] span:has-text("Contact Added")')).toBeVisible();

View File

@@ -33,13 +33,6 @@ export async function importUser(page: Page, id?: string): Promise<string> {
return did; return did;
} }
export async function importUserAndCloseOnboarding(page: Page, id?: string): Promise<string> {
const did = await importUser(page, id);
await page.goto('./');
await page.getByTestId('closeOnboardingAndFinish').click();
return did;
}
// This is to switch to someone already in the identity table. It doesn't include registration. // This is to switch to someone already in the identity table. It doesn't include registration.
export async function switchToUser(page: Page, did: string): Promise<void> { export async function switchToUser(page: Page, did: string): Promise<void> {
// This is the direct approach but users have to tap on things so we'll do that instead. // This is the direct approach but users have to tap on things so we'll do that instead.