Repository navigation
fix: chat share - #7789
fix: chat share#7789
Conversation
| }, | ||
| openConversation: (applicationId, chatId) => | ||
| void router.push({ name: 'portal-application-chat', params: { applicationId, chatId }, query: route.query }), | ||
| openApplicationList: () => void router.push({ name: 'portal-application-list', query: route.query }), |
There was a problem hiding this comment.
Review
1. Correctness / likely bugs
a) Strict type comparison on route params (high)
currentApplicationId almost certainly comes from route.params.applicationId, which is always a string in Vue Router. If callers pass a numeric id, both
applicationId !== currentApplicationId.value and the final guard
currentApplicationId.value !== applicationId will always be true → you'll always navigate, and then always return false. Normalize with String(...) or type the params as string.
b) router.push return value
push resolves with a NavigationFailure for benign cases too — notably NavigationFailureType.duplicated (same route). Treating any truthy return as failure means a no-op navigation that is already on the target route makes sharing silently fail. Use isNavigationFailure(failure, NavigationFailureType.duplicated) to distinguish. Also push can reject (guard throws in some setups) — wrap in try/catch.
c) The enterShare() return value is ignored, yet shareConversation returns true. If enterShare() can fail / be a no-op (already in share mode, no share permission, empty selection), the caller gets a false positive. Return bundle.share.enterShare() ?? true or check its result; also ensure it's awaited if async.
d) Behavior asymmetry vs. openConversation
The else if branch calls bundle.list.selectConversation(chatId) without touching the route, whereas openConversation does router.push. Result: the URL no longer reflects the current chat — reload / deep-link / share-link diverge, and the next route-driven watcher may fight this state. If that's intentional, document it; otherwise push here too.
e) The initial if (bundle.msgs.loading.value) return false contradicts the later "wait for load" logic
You bail if a load is in flight at call time, but you happily wait out a load you triggered yourself. If the in-flight load happens to be for the very conversation being shared, you return false for no good reason. Decide on one policy.
2. Race conditions / robustness
f) The busy-wait on bundle.msgs.loading is fragile and can hang forever (high)
- If the flag never flips to
false(request fails without resetting it, request cancelled, navigation aborted), the promise never resolves — the function hangs, and multiple rapid clicks accumulate a new watcher per call. - The watcher is never stopped on early exit / component unmount / rejection → leak, plus it can fire later and call
enterShare()for a conversation that is no longer current (the final guards help, but they run only once the watcher fires). - A brief
true → false → truetransition (two consecutive loads) can resolve on the firstfalse[…].Add a timeout, a cancellation flag, and re-verifycurrentChatIdinside the watcher.
g) await nextTick() (~1 tick) is not a reliable sync point
It assumes the route watcher sets loading = true within one tick. If the trigger uses flush: 'post', an async guard, or an onMounted fetch, the flag may not be set yet → you fall through to messages.length === 0 and return false. Best fix: have the loader expose a promise (e.g. bundle.msgs.loadConversation(chatId): Promise<void>) and await it directly; keep the reactive-flag wait only as a fallback (VueUse until/watchOnce with immediate: true + timeout).
h) No concurrency guard
Two rapid invocations create two watchers and two enterShare() calls. Add an inFlight boolean (or an AbortController/token) and bail/return the existing promise.
i) bundle.list.selectConversation(chatId) isn't guarded
If it rejects, shareConversation rejects instead of returning false, unlike every other failure path. Wrap the whole body in try/catch/finally (finally → stop()).
j) messages.value.length === 0 check
Fine as a guard, but note it also fails for: lazily-paginated/virtualized conversations, or a conversation whose first page hasn't arrived yet — the same items (f) leaves you with. Also use messages.value.length === 0 rather than !…length for clarity.
3. Style / typing / minor
- Types: the object-literal property has no contextual typing for
applicationId/chatId; withnoImplicitAnythis is an error. Annotate(applicationId: string, chatId: string): Promise<boolean>. - Missing braces on the multi-line
if (...)+ barereturn false— brittle on future edits and usually flagged bycurly. Add braces. - Comment mismatch: the comment says "全选分享" (select-all & share) but no selection happens — only
enterShare(). Either fix the code or the comment. Also consider matching the file's language for comments. enterShareis called even when the user is already in share mode — verify it is idempotent.- Duplicated
currentApplicationId/currentChatIdchecks across functions — consider extracting a smallisCurrentConversation(appId, chatId)helper. watch/nextTickimports are fine, but if you switch to awaiting the loader promise,watch(ornextTick) may become unused — tidy the import.
4. Suggested rewrite
import { computed, nextTick, watch } from 'vue'
import { isNavigationFailure, NavigationFailureType } from 'vue-router'
import { until } from '@vueuse/core' // optional helper
const SHARE_LOAD_TIMEOUT = 10_000
shareConversation: async (applicationId: string, chatId: string): Promise<boolean> => {
// Guard against concurrent invocations
if (bundle.share.enterShare.pending) return false
try {
if (String(applicationId) !== String(currentApplicationId.value)) {
const failure = await router.push({
name: 'portal-application-chat',
params: { applicationId, chatId },
query: route.query,
})
if (failure && !isNavigationFailure(failure, NavigationFailureType.duplicated)) {
return false
}
await nextTick()
// Prefer: await bundle.msgs.loaded(chatId)
if (bundle.msgs.loading.value) {
const settled = await until(() => !bundle.msgs.loading.value)
.toPromise()
.then(() => true)
.catch(() => false)
// add a timeout wrapper around this in practice
if (!settled) return false
}
} else if (bundle.list.currentChatId.value !== chatId) {
await bundle.list.selectConversation(chatId)
}
if (
String(currentApplicationId.value) !== String(applicationId) ||
bundle.list.currentChatId.value !== chatId ||
bundle.msgs.loading.value ||
bundle.msgs.streamLoading.value ||
bundle.msgs.messages.value.length === 0
) {
return false
}
bundle.share.enterShare()
return true
} catch {
return false
}
}Priority order: (a) type/string comparison, (f) watcher leak/hang + timeout, (b) duplicated-navigation handling, (c) unawaited enterShare, (d) route/state divergence. The rest are polish.
| @click="handleShareConversation(conversation)" | ||
| > | ||
| <template #icon><MkIcon name="icon_share_outlined" /></template> | ||
| <span>分享</span> |
There was a problem hiding this comment.
Here is a review of the provided code patch, focusing on potential issues, logic flaws, and optimization suggestions.
1. Potential Logic Issues & Race Conditions
Issue: Asynchronous Race Condition in handleShareConversation
The function handleShareConversation introduces an await inside an event handler (implied by @click). If the user rapidly clicks the "Share" menu item for a conversation while it is loading or switching, multiple asynchronous operations might queue up. While the initial check if (msgs.loading.value) return helps, there is no guard against concurrent calls to selectConversation.
- Risk: Two click events might both pass the
if (currentChatId.value !== conversation.id)check before the firstselectConversationcompletes, leading to redundant API calls or state inconsistencies. - Suggestion: Add a local loading/guard flag or ensure
selectConversationitself handles concurrent requests idempotently. Alternatively, usenextTickor explicit waiting if the store method returns a promise that resolves only after full state sync.
Refactored Suggestion:
// Add a simple guard or ensure selectConversation is robust
async function handleShareConversation(conversation: Conversation) {
// Check global loading first
if (msgs.loading.value) return;
// Optional: Prevent re-entry while this specific action is processing
// You might need a component-level ref like: const isSharing = ref(false);
if (currentChatId.value !== conversation.id) {
await selectConversation(conversation.id);
}
// Re-check conditions after async operation
if (currentChatId.value === conversation.id && canShare.value) {
shareLink?.enterShare();
}
}Issue: Stale State Check After Async Operation
After await selectConversation(conversation.id), the code checks canShare.value. However, canShare depends on msgs.messageList.value.length and msgs.loading.value. Depending on how useMessageListStore and useViewType reactively update, there might be a brief moment where currentChatId has updated but the message list hasn't finished populating or updating the loading flag immediately. Ensure that canShare accurately reflects the newly loaded state immediately upon resolution of selectConversation.
2. Code Quality & Style
Positive Change: Formatting Fixes
- The formatting change in the import statement (
type ViewType}→, type ViewType }) and spacing improvements are good hygiene practices.
Comment Ambiguity
- The comment
// 非当前会话先激活并加载消息,避免分享到其他会话。(Activate non-current sessions first and load messages to avoid sharing others.) is slightly misleading. The new logic allows sharing from non-current sessions by activating them first. A better phrasing might be: "Activate the target session if it's not current, ensuring its messages are loaded before entering share mode."
3. UI/UX & Disable Logic
Complexity in :disabled Prop
The template logic for disabling the button is somewhat convoluted:
:disabled="msgs.loading.value || (currentChatId === conversation.id && !canShare)"This means:
- If any loading is happening globally (
msgs.loading), disable it. - If it is the current chat AND
canShareis false (e.g., empty messages/streaming), disable it.
However, if you click a non-current conversation:
currentChatId !== conversation.id.msgs.loading.valueis checked. If false, the button is enabled.- But what if
msgs.loadingrefers to the current view's messages, not the target view? Ifmsgs.loadingis a global store reference indicating "main view is loading", then clicking a background conversation share button might be allowed even if the main view is busy, potentially causing conflicts when switching contexts. Ensuremsgs.loadingis context-aware or intended to be global.
Optimization: Consider extracting this logic into a computed property for readability.
const isShareDisabledForConv = (conv: Conversation) => {
if (msgs.loading.value) return true;
if (currentChatId.value === conv.id) return !canShare.value;
return false; // Assuming non-current chats are always shareable until loaded?
// Or should it also check if the target chat is known/valid?
};4. Missing Definitions / Dependencies
Undefined Types and Functions
The snippet uses Conversation, relativeTimeFormat, MkIcon, MkDropdownMenu, MkDropdownItem, el-button, and computed. These must be properly imported or defined elsewhere. Specifically:
Conversation: Ensure it is imported.relativeTimeFormat: Ensure it is imported.useConversationListStore: Ensure it is imported.
Missing Error Handling
selectConversation and shareLink?.enterShare() do not have .catch() blocks. If selectConversation fails (network error, permission denied), the UI will remain disabled or stuck, and no feedback is given to the user.
Suggestion:
async function handleShareConversation(conversation: Conversation) {
if (msgs.loading.value) return;
try {
if (currentChatId.value !== conversation.id) {
await selectConversation(conversation.id);
}
if (currentChatId.value === conversation.id && canShare.value) {
shareLink?.enterShare();
} else {
console.warn("Share condition not met after activation.");
// Optionally show a toast message
}
} catch (error) {
console.error("Failed to activate conversation for sharing:", error);
// Show user-friendly error message using useMessage() or similar
}
}Summary of Recommendations
- Add Try/Catch: Handle async errors in
handleShareConversation. - Race Condition Guard: Consider preventing repeated clicks during the async switching process.
- Refine Disable Logic: Verify if
msgs.loadingcorrectly prevents interaction with background conversations if intended. Extract complex disable logic to a helper/computed for readability if used in multiple places. - Clarify Comments: Update comments to reflect that sharing is now possible for non-current sessions by activating them first.
| > | ||
| <template #icon><MkIcon name="icon_share_outlined" /></template> | ||
| <span>分享</span> | ||
| </MkDropdownItem> |
There was a problem hiding this comment.
Here is a review of the provided code diff, highlighting irregularities, potential issues, and optimization suggestions.
1. Logical & Functional Issues
A. Race Condition & State Management in handleShareConversation
- Issue: The function calls
portalList.shareConversation(...). If this method internally sets state (e.g., loading states) or if there’s a delay, closing the drawer (visible.value = false) immediately upon resolution might conflict with UI feedback expected by the user or other components. - Suggestion: Ensure
shareConversationreturns a promise that resolves only after the core action is successful. If it triggers a navigation or overlay change, closing the drawer before that might cause visual flickering. Consider adding a loading indicator specific to that dropdown item if the operation is slow.
B. Dependency Removal (useShareLinkStoreOptional)
- Issue: You removed
useShareLinkStoreOptional. The old logic calledshareLink?.enterShare(), which likely handled complex pre-checks, permission validations, or state preparation for sharing. The new logic directly callsportalList.shareConversation. - Risk: If
portalList.shareConversationdoes not encapsulate all the validation/preparation logic previously handled by the share store, you might lose features (e.g., checking if sharing is enabled for the app, generating link IDs, etc.). - Verification Required: Confirm that
portalList.shareConversationhandles all necessary pre-validation. If it doesn’t, moving this logic to the store might violate single-responsibility principles or introduce bugs where shares fail silently or crash.
C. canShare Computed Property Change
- Change: Added
&& !msgs.loading.value. - Assessment: This is good. Preventing shares during initial message list load prevents sharing incomplete data.
- Optimization: Ensure
msgs.loading.valuereflects the entirety of the message loading process, including any sub-fetches (like metadata or attachments), otherwise partial data might still be shared.
2. Code Quality & Best Practices
A. Direct Store Method Call from Component
- Current:
await portalList.shareConversation(selectedApplicationId.value, conversation.id) - Suggestion: It’s generally better practice to keep "action orchestration" within the store if it involves multiple steps or side effects. However, calling a clear, single-purpose store action from a component is acceptable in Pinia/Vuex if the action itself is well-defined.
- Improvement: If
shareConversationis an async API call, consider handling errors gracefully here. Currently, ifshareConversationrejects, the drawer stays open, but there’s no explicittry/catchor error toast feedback in this snippet. Add error handling:const handleShareConversation = async (conversation: PortalConversationSummary) => { try { const success = await portalList.shareConversation(selectedApplicationId.value, conversation.id); if (success) { visible.value = false; // Optionally show success toast } } catch (error) { // Handle error (e.g., show toast 'Share failed') console.error('Failed to share conversation', error); } };
B. Dropdown Item Logic Complexity
- Current:
:disabled="msgs.loading.value || (isActiveConversation(conversation) && !canShare)"
- Analysis:
!canSharealready includes!msgs.streamLoading.valueand!msgs.loading.value(from the updated computed prop).- So
msgs.loading.valueis redundant in the disabled condition becausecanSharewould befalseifmsgs.loading.valueis true, but only ifisActiveConversation(conversation)is true. - Bug/Inconsistency: If the conversation is NOT active,
canShareis irrelevant to this item's logic in the second part of the OR condition. However,canShareis computed based on the active conversation's messages (msgs.messages.value). - Crucial Check: Does
shareConversationrequire the conversation to be loaded/active?- If you are trying to share a non-active conversation,
msgs.messages.valuerefers to the currently active conversation, not theconversationfrom the loop. - Therefore,
canShareis technically misleading when applied to non-active conversations. You cannot determine if a non-active conversation has messages or is currently streaming without fetching its data. - If
portalList.shareConversation(id)works regardless of active status, then thedisabledcheck relying oncanShare(which checks active conversation state) is logically flawed for non-active conversations.
- If you are trying to share a non-active conversation,
Refinement for Dropdown Disabled Logic:
You should clarify why non-active conversations can be shared. If portalList.shareConversation can share any conversation by ID, then msgs.loading (of the active convo) should not block it unless the API requires background loading.
If the intention is: "Only allow sharing if the conversation is ready," but we don't know the readiness of non-active convos, you might need to remove the canShare check entirely for non-active items, or fetch metadata.
Suggested Simpler Logic:
Since shareConversation takes the id directly, it likely handles its own state validation. The disabled prop should probably only block if the global system is too busy or the specific item is pending.
- Remove dependence on
canShare(which is active-convo-specific) from the general dropdown unlessshareConversationrequires local message cache. - If it requires cache, you must load the conversation before clicking, or disable non-active items until they've been viewed.
3. Optimization Suggestions
-
Remove Redundant Loading Checks:
SincecanSharenow includes!msgs.loading.value, andmessages.value.length > 0implies loads finished for active convo, thedisabledlogic in the template is messy.If
shareConversationis backend-driven and doesn't need local messages:<MkDropdownItem :disabled="msgs.loading.value" <!-- Only block if global loading --> @click="handleShareConversation(conversation)" >
If it does need local messages (only possible for active convo):
<MkDropdownItem v-if="isActiveConversation(conversation)" :disabled="!canShare" @click="handleShareConversation(conversation)" >
And hide/omit share option for non-active conversations entirely if they can't be shared without activation. This is cleaner than trying to share non-active via a potentially unreliable
shareConversationthat might trigger heavy lazy-loading. -
Type Safety:
The diff usesconversation: PortalConversationSummaryin arguments but the original removed usage ofPortalConversationSummaryimport? No, it seems present. EnsurePortalConversationSummaryis imported in the script setup section if used in TS types (it seems to be part of the existing structure). -
UX Improvement:
Adding an icon or text change for "Sharing..." state while waiting forawait portalList.shareConversationwould improve UX, as closing the drawer might feel abrupt if the network request is slow.
Summary of Recommendations
- Fix Potential Logic Error: Verify if
canShare(based on active conversation) should apply to non-active conversations in the dropdown. It likely shouldn't. If non-active convos can be shared via ID, removecanSharefrom their disabled logic. If they can't, restrict the dropdown item to active conversations only (v-if="isActiveConversation(...)"). - Add Error Handling: Wrap
handleShareConversationin try/catch to notify users if sharing fails. - Verify Store Responsibilities: Ensure
portalList.shareConversationreplicates necessary logic from the removeduseShareLinkStoreOptional(e.g., permissions, cleanup). - Simplify Template Logic: Decouple
msgs.loading(global/active state) from individual item state where inappropriate.
fix: chat share