|
|
@ -77,12 +77,86 @@ |
|
|
|
</a> |
|
|
|
</div> |
|
|
|
|
|
|
|
<!-- Description --> |
|
|
|
<p class="font-medium overflow-hidden"> |
|
|
|
<a |
|
|
|
class="block cursor-pointer overflow-hidden text-ellipsis" |
|
|
|
@click="emitLoadClaim(record.jwtId)" |
|
|
|
<!-- Emoji Section --> |
|
|
|
<div |
|
|
|
v-if="hasEmojis || isRegistered" |
|
|
|
class="float-right ml-3 mb-1 bg-white rounded border border-slate-300 px-1.5 py-0.5 max-w-[240px]" |
|
|
|
> |
|
|
|
<div class="flex items-center justify-between gap-1"> |
|
|
|
<!-- Existing Emojis Display --> |
|
|
|
<div v-if="hasEmojis" class="flex flex-wrap gap-1"> |
|
|
|
<button |
|
|
|
v-for="(count, emoji) in record.emojiCount" |
|
|
|
:key="emoji" |
|
|
|
class="inline-flex items-center gap-0.5 px-1 py-0.5 text-xs bg-slate-50 hover:bg-slate-100 rounded border border-slate-200 transition-colors cursor-pointer" |
|
|
|
:class="{ |
|
|
|
'bg-blue-50 border-blue-200': isUserEmojiWithoutLoading(emoji), |
|
|
|
'opacity-75 cursor-wait': loadingEmojis, |
|
|
|
}" |
|
|
|
:title=" |
|
|
|
loadingEmojis |
|
|
|
? 'Loading...' |
|
|
|
: !emojisOnActivity?.isResolved |
|
|
|
? 'Click to load your emojis' |
|
|
|
: isUserEmojiWithoutLoading(emoji) |
|
|
|
? 'Click to remove your emoji' |
|
|
|
: 'Click to add this emoji' |
|
|
|
" |
|
|
|
:disabled="!isRegistered" |
|
|
|
@click="toggleThisEmoji(emoji)" |
|
|
|
> |
|
|
|
<!-- Show spinner when loading --> |
|
|
|
<div v-if="loadingEmojis" class="animate-spin text-xs"> |
|
|
|
<font-awesome icon="spinner" class="fa-spin" /> |
|
|
|
</div> |
|
|
|
<span v-else class="text-sm leading-none">{{ emoji }}</span> |
|
|
|
<span class="text-xs text-slate-600 font-medium leading-none">{{ |
|
|
|
count |
|
|
|
}}</span> |
|
|
|
</button> |
|
|
|
</div> |
|
|
|
|
|
|
|
<!-- Add Emoji Button --> |
|
|
|
<button |
|
|
|
v-if="isRegistered" |
|
|
|
class="inline-flex px-1 py-0.5 text-xs bg-slate-100 hover:bg-slate-200 rounded border border-slate-300 transition-colors items-center justify-center ml-2 ml-auto" |
|
|
|
:title="showEmojiPicker ? 'Close emoji picker' : 'Add emoji'" |
|
|
|
@click="toggleEmojiPicker" |
|
|
|
> |
|
|
|
<span class="px-2 text-sm leading-none">{{ |
|
|
|
showEmojiPicker ? "x" : "😊" |
|
|
|
}}</span> |
|
|
|
</button> |
|
|
|
</div> |
|
|
|
|
|
|
|
<!-- Emoji Picker (placeholder for now) --> |
|
|
|
<div |
|
|
|
v-if="showEmojiPicker" |
|
|
|
class="mt-1 p-1.5 bg-slate-50 rounded border border-slate-300" |
|
|
|
> |
|
|
|
<!-- Temporary emoji buttons for testing --> |
|
|
|
<div class="flex flex-wrap gap-3 mt-1"> |
|
|
|
<button |
|
|
|
v-for="emoji in QUICK_EMOJIS" |
|
|
|
:key="emoji" |
|
|
|
class="p-0.5 hover:bg-slate-200 rounded text-base transition-opacity" |
|
|
|
:class="{ |
|
|
|
'opacity-75 cursor-wait': loadingEmojis, |
|
|
|
}" |
|
|
|
:disabled="loadingEmojis" |
|
|
|
@click="toggleThisEmoji(emoji)" |
|
|
|
> |
|
|
|
<!-- Show spinner when loading --> |
|
|
|
<div v-if="loadingEmojis" class="animate-spin text-sm">⟳</div> |
|
|
|
<span v-else>{{ emoji }}</span> |
|
|
|
</button> |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
|
|
|
|
<!-- Description --> |
|
|
|
<p class="font-medium"> |
|
|
|
<a class="block cursor-pointer" @click="emitLoadClaim(record.jwtId)"> |
|
|
|
<vue-markdown |
|
|
|
:source="truncatedDescription" |
|
|
|
class="markdown-content" |
|
|
@ -91,7 +165,7 @@ |
|
|
|
</p> |
|
|
|
|
|
|
|
<div |
|
|
|
class="relative flex justify-between gap-4 max-w-[40rem] mx-auto mt-4" |
|
|
|
class="clear-right relative flex justify-between gap-4 max-w-[40rem] mx-auto mt-4" |
|
|
|
> |
|
|
|
<!-- Source --> |
|
|
|
<div |
|
|
@ -254,17 +328,24 @@ |
|
|
|
|
|
|
|
<script lang="ts"> |
|
|
|
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 { EmojiSummaryRecord, GenericVerifiableCredential } from "@/interfaces"; |
|
|
|
import { GiveRecordWithContactInfo } from "@/interfaces/give"; |
|
|
|
import { PromiseTracker } from "@/libs/util"; |
|
|
|
|
|
|
|
@Component({ |
|
|
|
components: { |
|
|
@ -274,15 +355,24 @@ 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<typeof createNotifyHelpers>; |
|
|
|
$notify!: NotifyFunction; |
|
|
|
|
|
|
|
// Emoji-related data |
|
|
|
showEmojiPicker = false; |
|
|
|
loadingEmojis = false; // Track if emojis are currently loading |
|
|
|
|
|
|
|
emojisOnActivity: PromiseTracker<EmojiSummaryRecord[]> | null = null; // load this only when needed |
|
|
|
|
|
|
|
created() { |
|
|
|
this.notify = createNotifyHelpers(this.$notify); |
|
|
|
} |
|
|
@ -346,5 +436,186 @@ 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; |
|
|
|
} |
|
|
|
|
|
|
|
triggerUserEmojiLoad(): PromiseTracker<EmojiSummaryRecord[]> { |
|
|
|
if (!this.emojisOnActivity) { |
|
|
|
const promise = new Promise<EmojiSummaryRecord[]>((resolve) => { |
|
|
|
(async () => { |
|
|
|
this.axios |
|
|
|
.get( |
|
|
|
`${this.apiServer}/api/v2/report/emoji?parentHandleId=${encodeURIComponent(this.record.handleId)}`, |
|
|
|
{ headers: await getHeaders(this.activeDid) }, |
|
|
|
) |
|
|
|
.then((response) => { |
|
|
|
const userEmojiRecords = response.data.data.filter( |
|
|
|
(e: EmojiSummaryRecord) => e.issuerDid === this.activeDid, |
|
|
|
); |
|
|
|
resolve(userEmojiRecords); |
|
|
|
}) |
|
|
|
.catch((error) => { |
|
|
|
logger.error("Error loading user emojis:", error); |
|
|
|
resolve([]); |
|
|
|
}); |
|
|
|
})(); |
|
|
|
}); |
|
|
|
|
|
|
|
this.emojisOnActivity = new PromiseTracker(promise); |
|
|
|
} |
|
|
|
return this.emojisOnActivity; |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* |
|
|
|
* @param emoji - The emoji to check. |
|
|
|
* @returns True if the emoji is in the user's emojis, false otherwise. |
|
|
|
* |
|
|
|
* @note This method is quick and synchronous, and can check resolved emojis |
|
|
|
* without triggering a server request. Returns false if emojis haven't been loaded yet. |
|
|
|
*/ |
|
|
|
isUserEmojiWithoutLoading(emoji: string): boolean { |
|
|
|
if (this.emojisOnActivity?.isResolved && this.emojisOnActivity.value) { |
|
|
|
return this.emojisOnActivity.value.some( |
|
|
|
(record) => record.text === emoji, |
|
|
|
); |
|
|
|
} |
|
|
|
return false; |
|
|
|
} |
|
|
|
|
|
|
|
async toggleEmojiPicker() { |
|
|
|
this.triggerUserEmojiLoad(); // trigger it, but don't wait for it to complete |
|
|
|
this.showEmojiPicker = !this.showEmojiPicker; |
|
|
|
} |
|
|
|
|
|
|
|
async toggleThisEmoji(emoji: string) { |
|
|
|
// Start loading indicator |
|
|
|
this.loadingEmojis = true; |
|
|
|
this.showEmojiPicker = false; // always close the picker when an emoji is clicked |
|
|
|
|
|
|
|
try { |
|
|
|
this.triggerUserEmojiLoad(); // trigger just in case |
|
|
|
|
|
|
|
const userEmojiList = await this.emojisOnActivity!.promise; // must wait now that they've chosen |
|
|
|
|
|
|
|
const userHasEmoji: boolean = userEmojiList.some( |
|
|
|
(record) => record.text === emoji, |
|
|
|
); |
|
|
|
|
|
|
|
if (userHasEmoji) { |
|
|
|
this.$notify( |
|
|
|
{ |
|
|
|
group: "modal", |
|
|
|
type: "confirm", |
|
|
|
title: "Remove Emoji", |
|
|
|
text: `Do you want to remove your ${emoji} ?`, |
|
|
|
yesText: "Remove", |
|
|
|
onYes: async () => { |
|
|
|
await this.removeEmoji(emoji); |
|
|
|
}, |
|
|
|
}, |
|
|
|
TIMEOUTS.MODAL, |
|
|
|
); |
|
|
|
} else { |
|
|
|
// User doesn't have this emoji, add it |
|
|
|
await this.submitEmoji(emoji); |
|
|
|
} |
|
|
|
} finally { |
|
|
|
// Remove loading indicator |
|
|
|
this.loadingEmojis = false; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
async submitEmoji(emoji: string) { |
|
|
|
try { |
|
|
|
// Create an Emoji claim and send to the server |
|
|
|
const emojiClaim: GenericVerifiableCredential = { |
|
|
|
"@context": "https://endorser.ch", |
|
|
|
"@type": "Emoji", |
|
|
|
text: emoji, |
|
|
|
parentItem: { lastClaimId: this.record.jwtId }, |
|
|
|
}; |
|
|
|
const claim = await createAndSubmitClaim( |
|
|
|
emojiClaim, |
|
|
|
this.activeDid, |
|
|
|
this.apiServer, |
|
|
|
this.axios, |
|
|
|
); |
|
|
|
if (claim.success && !claim.embeddedRecordError) { |
|
|
|
// Update emoji count |
|
|
|
this.record.emojiCount[emoji] = |
|
|
|
(this.record.emojiCount[emoji] || 0) + 1; |
|
|
|
|
|
|
|
// Create a new emoji record (we'll get the actual jwtId from the server response later) |
|
|
|
const newEmojiRecord: EmojiSummaryRecord = { |
|
|
|
issuerDid: this.activeDid, |
|
|
|
jwtId: claim.claimId || "", |
|
|
|
text: emoji, |
|
|
|
parentHandleId: this.record.jwtId, |
|
|
|
}; |
|
|
|
|
|
|
|
// Update user emojis list by creating a new promise with the updated data |
|
|
|
// (Trigger shouldn't be necessary since all calls should come through a toggle, but just in case someone calls this directly) |
|
|
|
this.triggerUserEmojiLoad(); |
|
|
|
const currentEmojis = await this.emojisOnActivity!.promise; // must wait now that they've clicked one |
|
|
|
this.emojisOnActivity = new PromiseTracker( |
|
|
|
Promise.resolve([...currentEmojis, newEmojiRecord]), |
|
|
|
); |
|
|
|
} 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 = { |
|
|
|
"@context": "https://endorser.ch", |
|
|
|
"@type": "Emoji", |
|
|
|
text: emoji, |
|
|
|
parentItem: { lastClaimId: this.record.jwtId }, |
|
|
|
}; |
|
|
|
const claim = await createAndSubmitClaim( |
|
|
|
emojiClaim, |
|
|
|
this.activeDid, |
|
|
|
this.apiServer, |
|
|
|
this.axios, |
|
|
|
); |
|
|
|
if (claim.success && !claim.embeddedRecordError) { |
|
|
|
// Update emoji count |
|
|
|
const newCount = Math.max(0, (this.record.emojiCount[emoji] || 0) - 1); |
|
|
|
if (newCount === 0) { |
|
|
|
delete this.record.emojiCount[emoji]; |
|
|
|
} else { |
|
|
|
this.record.emojiCount[emoji] = newCount; |
|
|
|
} |
|
|
|
|
|
|
|
// Update user emojis list by creating a new promise with the updated data |
|
|
|
// (Trigger shouldn't be necessary since all calls should come through a toggle, but just in case someone calls this directly) |
|
|
|
this.triggerUserEmojiLoad(); |
|
|
|
const currentEmojis = await this.emojisOnActivity!.promise; // must wait now that they've clicked one |
|
|
|
this.emojisOnActivity = new PromiseTracker( |
|
|
|
Promise.resolve( |
|
|
|
currentEmojis.filter( |
|
|
|
(record) => |
|
|
|
record.issuerDid === this.activeDid && record.text !== 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); |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
</script> |
|
|
|