|
@ -79,24 +79,34 @@ |
|
|
|
|
|
|
|
|
<!-- Emoji Section --> |
|
|
<!-- Emoji Section --> |
|
|
<div |
|
|
<div |
|
|
class="float-right ml-3 mb-1 bg-white rounded border border-slate-300 px-1.5 py-0.5 max-w-[200px]" |
|
|
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"> |
|
|
<div class="flex items-center justify-between gap-1"> |
|
|
<!-- Existing Emojis Display --> |
|
|
<!-- Existing Emojis Display --> |
|
|
<div v-if="hasEmojis" class="flex flex-wrap gap-1 mr-2"> |
|
|
<div v-if="hasEmojis" class="flex flex-wrap gap-1"> |
|
|
<button |
|
|
<button |
|
|
v-for="(count, emoji) in record.emojiCount" |
|
|
v-for="(count, emoji) in record.emojiCount" |
|
|
:key="emoji" |
|
|
: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="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': isUserEmoji(emoji) }" |
|
|
:class="{ |
|
|
|
|
|
'bg-blue-50 border-blue-200': isUserEmojiWithoutLoading(emoji), |
|
|
|
|
|
'opacity-75 cursor-wait': loadingEmojis, |
|
|
|
|
|
}" |
|
|
:title=" |
|
|
:title=" |
|
|
isUserEmoji(emoji) |
|
|
loadingEmojis |
|
|
|
|
|
? 'Loading...' |
|
|
|
|
|
: !emojisOnActivity?.isResolved |
|
|
|
|
|
? 'Click to load your emojis' |
|
|
|
|
|
: isUserEmojiWithoutLoading(emoji) |
|
|
? 'Click to remove your emoji' |
|
|
? 'Click to remove your emoji' |
|
|
: 'Click to add this emoji' |
|
|
: 'Click to add this emoji' |
|
|
" |
|
|
" |
|
|
@click="toggleEmoji(emoji)" |
|
|
:disabled="!isRegistered" |
|
|
|
|
|
@click="toggleThisEmoji(emoji)" |
|
|
> |
|
|
> |
|
|
<span class="text-sm leading-none">{{ emoji }}</span> |
|
|
<!-- Show spinner when loading --> |
|
|
|
|
|
<div v-if="loadingEmojis" class="animate-spin text-xs">⟳</div> |
|
|
|
|
|
<span v-else class="text-sm leading-none">{{ emoji }}</span> |
|
|
<span class="text-xs text-slate-600 font-medium leading-none">{{ |
|
|
<span class="text-xs text-slate-600 font-medium leading-none">{{ |
|
|
count |
|
|
count |
|
|
}}</span> |
|
|
}}</span> |
|
@ -105,11 +115,12 @@ |
|
|
|
|
|
|
|
|
<!-- Add Emoji Button --> |
|
|
<!-- Add Emoji Button --> |
|
|
<button |
|
|
<button |
|
|
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-auto" |
|
|
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'" |
|
|
:title="showEmojiPicker ? 'Close emoji picker' : 'Add emoji'" |
|
|
@click="showEmojiPicker = !showEmojiPicker" |
|
|
@click="toggleEmojiPicker" |
|
|
> |
|
|
> |
|
|
<span class="text-sm leading-none">{{ |
|
|
<span class="px-2 text-sm leading-none">{{ |
|
|
showEmojiPicker ? "x" : "😊" |
|
|
showEmojiPicker ? "x" : "😊" |
|
|
}}</span> |
|
|
}}</span> |
|
|
</button> |
|
|
</button> |
|
@ -121,14 +132,20 @@ |
|
|
class="mt-1 p-1.5 bg-slate-50 rounded border border-slate-300" |
|
|
class="mt-1 p-1.5 bg-slate-50 rounded border border-slate-300" |
|
|
> |
|
|
> |
|
|
<!-- Temporary emoji buttons for testing --> |
|
|
<!-- Temporary emoji buttons for testing --> |
|
|
<div class="flex flex-wrap gap-1 mt-1"> |
|
|
<div class="flex flex-wrap gap-3 mt-1"> |
|
|
<button |
|
|
<button |
|
|
v-for="emoji in QUICK_EMOJIS" |
|
|
v-for="emoji in QUICK_EMOJIS" |
|
|
:key="emoji" |
|
|
:key="emoji" |
|
|
class="p-0.5 hover:bg-slate-200 rounded text-base" |
|
|
class="p-0.5 hover:bg-slate-200 rounded text-base transition-opacity" |
|
|
@click="selectEmoji(emoji)" |
|
|
:class="{ |
|
|
|
|
|
'opacity-75 cursor-wait': loadingEmojis, |
|
|
|
|
|
}" |
|
|
|
|
|
:disabled="loadingEmojis" |
|
|
|
|
|
@click="toggleThisEmoji(emoji)" |
|
|
> |
|
|
> |
|
|
{{ emoji }} |
|
|
<!-- Show spinner when loading --> |
|
|
|
|
|
<div v-if="loadingEmojis" class="animate-spin text-sm">⟳</div> |
|
|
|
|
|
<span v-else>{{ emoji }}</span> |
|
|
</button> |
|
|
</button> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
@ -323,8 +340,9 @@ import { |
|
|
NOTIFY_PERSON_HIDDEN, |
|
|
NOTIFY_PERSON_HIDDEN, |
|
|
NOTIFY_UNKNOWN_PERSON, |
|
|
NOTIFY_UNKNOWN_PERSON, |
|
|
} from "@/constants/notifications"; |
|
|
} from "@/constants/notifications"; |
|
|
import { GenericVerifiableCredential } from "@/interfaces"; |
|
|
import { EmojiSummaryRecord, GenericVerifiableCredential } from "@/interfaces"; |
|
|
import { GiveRecordWithContactInfo } from "@/interfaces/give"; |
|
|
import { GiveRecordWithContactInfo } from "@/interfaces/give"; |
|
|
|
|
|
import { PromiseTracker } from "@/libs/util"; |
|
|
|
|
|
|
|
|
@Component({ |
|
|
@Component({ |
|
|
components: { |
|
|
components: { |
|
@ -348,8 +366,9 @@ export default class ActivityListItem extends Vue { |
|
|
|
|
|
|
|
|
// Emoji-related data |
|
|
// Emoji-related data |
|
|
showEmojiPicker = false; |
|
|
showEmojiPicker = false; |
|
|
|
|
|
loadingEmojis = false; // Track if emojis are currently loading |
|
|
|
|
|
|
|
|
userEmojis: string[] | null = null; // load this only when needed |
|
|
emojisOnActivity: PromiseTracker<EmojiSummaryRecord[]> | null = null; // load this only when needed |
|
|
|
|
|
|
|
|
created() { |
|
|
created() { |
|
|
this.notify = createNotifyHelpers(this.$notify); |
|
|
this.notify = createNotifyHelpers(this.$notify); |
|
@ -420,52 +439,87 @@ export default class ActivityListItem extends Vue { |
|
|
return Object.keys(this.record.emojiCount).length > 0; |
|
|
return Object.keys(this.record.emojiCount).length > 0; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
async loadUserEmojis(): Promise<void> { |
|
|
triggerUserEmojiLoad(): PromiseTracker<EmojiSummaryRecord[]> { |
|
|
try { |
|
|
if (!this.emojisOnActivity) { |
|
|
const response = await this.axios.get( |
|
|
const promise = new Promise<EmojiSummaryRecord[]>((resolve) => { |
|
|
`${this.apiServer}/api/v2/emoji/userEmojis?parentHandleId=${this.record.jwtId}`, |
|
|
(async () => { |
|
|
|
|
|
this.axios |
|
|
|
|
|
.get( |
|
|
|
|
|
`${this.apiServer}/api/v2/report/emoji?parentHandleId=${encodeURIComponent(this.record.handleId)}`, |
|
|
{ headers: await getHeaders(this.activeDid) }, |
|
|
{ headers: await getHeaders(this.activeDid) }, |
|
|
|
|
|
) |
|
|
|
|
|
.then((response) => { |
|
|
|
|
|
const userEmojiRecords = response.data.data.filter( |
|
|
|
|
|
(e: EmojiSummaryRecord) => e.issuerDid === this.activeDid, |
|
|
); |
|
|
); |
|
|
this.userEmojis = response.data; |
|
|
resolve(userEmojiRecords); |
|
|
} catch (error) { |
|
|
}) |
|
|
logger.error( |
|
|
.catch((error) => { |
|
|
"Error loading all emojis for parent handle id:", |
|
|
logger.error("Error loading user emojis:", error); |
|
|
this.record.jwtId, |
|
|
resolve([]); |
|
|
error, |
|
|
}); |
|
|
); |
|
|
})(); |
|
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
this.emojisOnActivity = new PromiseTracker(promise); |
|
|
} |
|
|
} |
|
|
|
|
|
return this.emojisOnActivity; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
async getUserEmojis(): Promise<string[]> { |
|
|
/** |
|
|
if (!this.userEmojis) { |
|
|
* |
|
|
await this.loadUserEmojis(); |
|
|
* @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 this.userEmojis || []; |
|
|
return false; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
selectEmoji(emoji: string) { |
|
|
async toggleEmojiPicker() { |
|
|
this.showEmojiPicker = false; |
|
|
this.triggerUserEmojiLoad(); // trigger it, but don't wait for it to complete |
|
|
this.submitEmoji(emoji); |
|
|
this.showEmojiPicker = !this.showEmojiPicker; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
isUserEmoji(emoji: string): boolean { |
|
|
async toggleThisEmoji(emoji: string) { |
|
|
return this.userEmojis?.includes(emoji) || false; |
|
|
// Start loading indicator |
|
|
} |
|
|
this.loadingEmojis = true; |
|
|
|
|
|
this.showEmojiPicker = false; // always close the picker when an emoji is clicked |
|
|
|
|
|
|
|
|
toggleEmoji(emoji: string) { |
|
|
try { |
|
|
if (this.isUserEmoji(emoji)) { |
|
|
this.triggerUserEmojiLoad(); // trigger just in case |
|
|
this.removeEmoji(emoji); |
|
|
|
|
|
|
|
|
const userEmojiList = await this.emojisOnActivity!.promise; // must wait now that they've chosen |
|
|
|
|
|
|
|
|
|
|
|
const userHasEmoji: boolean = userEmojiList.some( |
|
|
|
|
|
(record) => record.text === emoji, |
|
|
|
|
|
); |
|
|
|
|
|
|
|
|
|
|
|
if (userHasEmoji) { |
|
|
|
|
|
// User already has this emoji, ask for confirmation to remove |
|
|
|
|
|
const confirmed = confirm(`Do you want to remove your ${emoji} emoji?`); |
|
|
|
|
|
if (confirmed) { |
|
|
|
|
|
await this.removeEmoji(emoji); |
|
|
|
|
|
} |
|
|
} else { |
|
|
} else { |
|
|
this.submitEmoji(emoji); |
|
|
// User doesn't have this emoji, add it |
|
|
|
|
|
await this.submitEmoji(emoji); |
|
|
|
|
|
} |
|
|
|
|
|
} finally { |
|
|
|
|
|
// Remove loading indicator |
|
|
|
|
|
this.loadingEmojis = false; |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
async submitEmoji(emoji: string) { |
|
|
async submitEmoji(emoji: string) { |
|
|
try { |
|
|
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 |
|
|
// Create an Emoji claim and send to the server |
|
|
const emojiClaim: GenericVerifiableCredential = { |
|
|
const emojiClaim: GenericVerifiableCredential = { |
|
|
"@type": "Emoji", |
|
|
"@type": "Emoji", |
|
@ -474,17 +528,30 @@ export default class ActivityListItem extends Vue { |
|
|
}; |
|
|
}; |
|
|
const claim = await createAndSubmitClaim( |
|
|
const claim = await createAndSubmitClaim( |
|
|
emojiClaim, |
|
|
emojiClaim, |
|
|
this.record.issuerDid, |
|
|
this.activeDid, |
|
|
this.apiServer, |
|
|
this.apiServer, |
|
|
this.axios, |
|
|
this.axios, |
|
|
); |
|
|
); |
|
|
if ( |
|
|
if (claim.success && !claim.embeddedRecordError) { |
|
|
claim.success && |
|
|
// Update emoji count |
|
|
!(claim.success as { embeddedRecordError?: string }).embeddedRecordError |
|
|
|
|
|
) { |
|
|
|
|
|
this.record.emojiCount[emoji] = |
|
|
this.record.emojiCount[emoji] = |
|
|
(this.record.emojiCount[emoji] || 0) + 1; |
|
|
(this.record.emojiCount[emoji] || 0) + 1; |
|
|
this.userEmojis = [...(this.userEmojis || []), emoji]; |
|
|
|
|
|
|
|
|
// 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 { |
|
|
} else { |
|
|
this.notify.error("Failed to add emoji.", TIMEOUTS.STANDARD); |
|
|
this.notify.error("Failed to add emoji.", TIMEOUTS.STANDARD); |
|
|
} |
|
|
} |
|
@ -504,22 +571,31 @@ export default class ActivityListItem extends Vue { |
|
|
}; |
|
|
}; |
|
|
const claim = await createAndSubmitClaim( |
|
|
const claim = await createAndSubmitClaim( |
|
|
emojiClaim, |
|
|
emojiClaim, |
|
|
this.record.issuerDid, |
|
|
this.activeDid, |
|
|
this.apiServer, |
|
|
this.apiServer, |
|
|
this.axios, |
|
|
this.axios, |
|
|
); |
|
|
); |
|
|
if (claim.success) { |
|
|
if (claim.success && !claim.embeddedRecordError) { |
|
|
this.record.emojiCount[emoji] = |
|
|
// Update emoji count |
|
|
(this.record.emojiCount[emoji] || 0) - 1; |
|
|
const newCount = Math.max(0, (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) { |
|
|
if (newCount === 0) { |
|
|
delete this.record.emojiCount[emoji]; |
|
|
delete this.record.emojiCount[emoji]; |
|
|
} else { |
|
|
} else { |
|
|
this.record.emojiCount[emoji] = newCount; |
|
|
this.record.emojiCount[emoji] = newCount; |
|
|
} |
|
|
} |
|
|
this.userEmojis = this.userEmojis?.filter(e => e !== emoji) || []; |
|
|
|
|
|
|
|
|
// 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 { |
|
|
} else { |
|
|
this.notify.error("Failed to remove emoji.", TIMEOUTS.STANDARD); |
|
|
this.notify.error("Failed to remove emoji.", TIMEOUTS.STANDARD); |
|
|
} |
|
|
} |
|
|