diff --git a/package-lock.json b/package-lock.json index 04d2b408..432cb310 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "timesafari", - "version": "1.1.0-beta", + "version": "1.1.1-beta", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "timesafari", - "version": "1.1.0-beta", + "version": "1.1.1-beta", "dependencies": { "@capacitor-community/electron": "^5.0.1", "@capacitor-community/sqlite": "6.0.2", @@ -61,6 +61,7 @@ "did-resolver": "^4.1.0", "dotenv": "^16.0.3", "electron-builder": "^26.0.12", + "emoji-mart-vue-fast": "^15.0.5", "ethereum-cryptography": "^2.1.3", "ethereumjs-util": "^7.1.5", "jdenticon": "^3.2.0", @@ -1864,7 +1865,6 @@ "version": "7.28.3", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.3.tgz", "integrity": "sha512-9uIQ10o0WGdpP6GDhXcdOJPJuDgFtIDtN/9+ArJQ2NAfAmiuhTQdzkaTGR33v43GYS2UrSA0eX2pPPHoFVvpxA==", - "devOptional": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -14990,6 +14990,16 @@ "devOptional": true, "license": "MIT" }, + "node_modules/core-js": { + "version": "3.46.0", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.46.0.tgz", + "integrity": "sha512-vDMm9B0xnqqZ8uSBpZ8sNtRtOdmfShrvT6h2TuQGLs0Is+cR0DYbj/KWP6ALVNbWPpqA/qPLoOuppJN07humpA==", + "hasInstallScript": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, "node_modules/core-js-compat": { "version": "3.45.0", "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.45.0.tgz", @@ -16422,6 +16432,18 @@ "url": "https://github.com/sindresorhus/emittery?sponsor=1" } }, + "node_modules/emoji-mart-vue-fast": { + "version": "15.0.5", + "resolved": "https://registry.npmjs.org/emoji-mart-vue-fast/-/emoji-mart-vue-fast-15.0.5.tgz", + "integrity": "sha512-wnxLor8ggpqshoOPwIc33MdOC3A1XFeDLgUwYLPtNPL8VeAtXJAVrnFq1CN5PeCYAFoLo4IufHQZ9CfHD4IZiw==", + "dependencies": { + "@babel/runtime": "^7.18.6", + "core-js": "^3.23.5" + }, + "peerDependencies": { + "vue": ">2.0.0" + } + }, "node_modules/emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", diff --git a/package.json b/package.json index a9587886..1d5ab589 100644 --- a/package.json +++ b/package.json @@ -190,6 +190,7 @@ "did-resolver": "^4.1.0", "dotenv": "^16.0.3", "electron-builder": "^26.0.12", + "emoji-mart-vue-fast": "^15.0.5", "ethereum-cryptography": "^2.1.3", "ethereumjs-util": "^7.1.5", "jdenticon": "^3.2.0", diff --git a/src/components/ActivityListItem.vue b/src/components/ActivityListItem.vue index 6f27be86..063a4c51 100644 --- a/src/components/ActivityListItem.vue +++ b/src/components/ActivityListItem.vue @@ -77,12 +77,66 @@ - -

- +

+
+ +
+ +
+ + + +
+ + +
+ +
+ +
+
+
+ + +

+

import { Component, Prop, Vue, Emit } from "vue-facing-decorator"; -import { GiveRecordWithContactInfo } from "@/interfaces/give"; +import VueMarkdown from "vue-markdown-render"; + +import { logger } from "../utils/logger"; +import { + createAndSubmitClaim, + getHeaders, + isHiddenDid, +} from "../libs/endorserServer"; import EntityIcon from "./EntityIcon.vue"; -import { isHiddenDid } from "../libs/endorserServer"; import ProjectIcon from "./ProjectIcon.vue"; -import { createNotifyHelpers, NotifyFunction } from "@/utils/notify"; +import { createNotifyHelpers, NotifyFunction, TIMEOUTS } from "@/utils/notify"; import { NOTIFY_PERSON_HIDDEN, NOTIFY_UNKNOWN_PERSON, } from "@/constants/notifications"; -import { TIMEOUTS } from "@/utils/notify"; -import VueMarkdown from "vue-markdown-render"; +import { GenericVerifiableCredential } from "@/interfaces"; +import { GiveRecordWithContactInfo } from "@/interfaces/give"; @Component({ components: { @@ -274,15 +334,23 @@ import VueMarkdown from "vue-markdown-render"; }, }) export default class ActivityListItem extends Vue { + readonly QUICK_EMOJIS = ["👍", "👏", "❤️", "🎉", "😊", "😆", "🔥"]; + @Prop() record!: GiveRecordWithContactInfo; @Prop() lastViewedClaimId?: string; @Prop() isRegistered!: boolean; @Prop() activeDid!: string; + @Prop() apiServer!: string; isHiddenDid = isHiddenDid; notify!: ReturnType; $notify!: NotifyFunction; + // Emoji-related data + showEmojiPicker = false; + + userEmojis: string[] | null = null; // load this only when needed + created() { this.notify = createNotifyHelpers(this.$notify); } @@ -346,5 +414,119 @@ export default class ActivityListItem extends Vue { day: "numeric", }); } + + // Emoji-related computed properties and methods + get hasEmojis(): boolean { + return Object.keys(this.record.emojiCount).length > 0; + } + + async loadUserEmojis(): Promise { + try { + const response = await this.axios.get( + `${this.apiServer}/api/v2/emoji/userEmojis?parentHandleId=${this.record.jwtId}`, + { headers: await getHeaders(this.activeDid) }, + ); + this.userEmojis = response.data; + } catch (error) { + logger.error( + "Error loading all emojis for parent handle id:", + this.record.jwtId, + error, + ); + } + } + + async getUserEmojis(): Promise { + if (!this.userEmojis) { + await this.loadUserEmojis(); + } + return this.userEmojis || []; + } + + selectEmoji(emoji: string) { + this.showEmojiPicker = false; + this.submitEmoji(emoji); + } + + isUserEmoji(emoji: string): boolean { + return this.userEmojis?.includes(emoji) || false; + } + + toggleEmoji(emoji: string) { + if (this.isUserEmoji(emoji)) { + this.removeEmoji(emoji); + } else { + this.submitEmoji(emoji); + } + } + + async submitEmoji(emoji: string) { + try { + // Temporarily add to user emojis for UI feedback + if (!this.isUserEmoji(emoji)) { + this.record.emojiCount[emoji] = 0; + } + // Create an Emoji claim and send to the server + const emojiClaim: GenericVerifiableCredential = { + "@type": "Emoji", + text: emoji, + parentItem: { lastClaimId: this.record.jwtId }, + }; + const claim = await createAndSubmitClaim( + emojiClaim, + this.record.issuerDid, + this.apiServer, + this.axios, + ); + if ( + claim.success && + !(claim.success as { embeddedRecordError?: string }).embeddedRecordError + ) { + this.record.emojiCount[emoji] = + (this.record.emojiCount[emoji] || 0) + 1; + this.userEmojis = [...(this.userEmojis || []), emoji]; + } else { + this.notify.error("Failed to add emoji.", TIMEOUTS.STANDARD); + } + } catch (error) { + logger.error("Error submitting emoji:", error); + this.notify.error("Got error adding emoji.", TIMEOUTS.STANDARD); + } + } + + async removeEmoji(emoji: string) { + try { + // Create an Emoji claim and send to the server + const emojiClaim: GenericVerifiableCredential = { + "@type": "Emoji", + text: emoji, + parentItem: { lastClaimId: this.record.jwtId }, + }; + const claim = await createAndSubmitClaim( + emojiClaim, + this.record.issuerDid, + this.apiServer, + this.axios, + ); + if (claim.success) { + this.record.emojiCount[emoji] = + (this.record.emojiCount[emoji] || 0) - 1; + + // Update local emoji count for immediate UI feedback + const newCount = Math.max(0, this.record.emojiCount[emoji]); + if (newCount === 0) { + delete this.record.emojiCount[emoji]; + } else { + this.record.emojiCount[emoji] = newCount; + } + this.userEmojis = this.userEmojis?.filter(e => e !== emoji) || []; + } else { + this.notify.error("Failed to remove emoji.", TIMEOUTS.STANDARD); + } + } catch (error) { + logger.error("Error removing emoji:", error); + this.notify.error("Got error removing emoji.", TIMEOUTS.STANDARD); + } + } } diff --git a/src/interfaces/common.ts b/src/interfaces/common.ts index b2e68d1f..0dfe37d5 100644 --- a/src/interfaces/common.ts +++ b/src/interfaces/common.ts @@ -80,7 +80,7 @@ export interface UserInfo { } export interface CreateAndSubmitClaimResult { - success: boolean; + success: boolean | { embeddedRecordError?: string; claimId?: string }; error?: string; handleId?: string; } diff --git a/src/interfaces/records.ts b/src/interfaces/records.ts index ca82624c..24089b35 100644 --- a/src/interfaces/records.ts +++ b/src/interfaces/records.ts @@ -9,6 +9,7 @@ export interface GiveSummaryRecord { amount: number; amountConfirmed: number; description: string; + emojiCount: Record; // Map of emoji character to count fullClaim: GiveActionClaim; fulfillsHandleId: string; fulfillsPlanHandleId?: string; diff --git a/src/views/HomeView.vue b/src/views/HomeView.vue index 3e73cda4..75c9bb67 100644 --- a/src/views/HomeView.vue +++ b/src/views/HomeView.vue @@ -245,6 +245,7 @@ Raymer * @version 1.0.0 */ :last-viewed-claim-id="feedLastViewedClaimId" :is-registered="isRegistered" :active-did="activeDid" + :api-server="apiServer" @load-claim="onClickLoadClaim" @view-image="openImageViewer" /> @@ -1234,6 +1235,7 @@ export default class HomeView extends Vue { const recipientDid = this.extractRecipientDid(claim); const fulfillsPlan = await this.getFulfillsPlan(record); + const emojiCount = await record.emojiCount; // Log record details for debugging logger.debug("[HomeView] 🔍 Processing record:", { @@ -1264,6 +1266,7 @@ export default class HomeView extends Vue { provider, fulfillsPlan, providedByPlan, + emojiCount, ); } @@ -1487,12 +1490,14 @@ export default class HomeView extends Vue { provider: Provider | undefined, fulfillsPlan?: FulfillsPlan, providedByPlan?: ProvidedByPlan, + emojiCount?: Record, ): GiveRecordWithContactInfo { return { ...record, jwtId: record.jwtId, fullClaim: record.fullClaim, description: record.description || "", + emojiCount: emojiCount || {}, handleId: record.handleId, issuerDid: record.issuerDid, fulfillsPlanHandleId: record.fulfillsPlanHandleId,