Skip to content

fix: chat share - #7789

Merged
wangdan-fit2cloud merged 1 commit into
v3from
pr@v3@fix_chat_share
Nov 10, 2026
Merged

wangdan-fit2cloud merged 1 commit into
v3from
pr@v3@fix_chat_share

Conversation

@shaohuzhang1

Copy link
Copy Markdown
Contributor

fix: chat share

},
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 }),

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 → true transition (two consecutive loads) can resolve on the first false[…]. Add a timeout, a cancellation flag, and re-verify currentChatId inside 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; with noImplicitAny this is an error. Annotate (applicationId: string, chatId: string): Promise<boolean>.
  • Missing braces on the multi-line if (...) + bare return false — brittle on future edits and usually flagged by curly. 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.
  • enterShare is called even when the user is already in share mode — verify it is idempotent.
  • Duplicated currentApplicationId / currentChatId checks across functions — consider extracting a small isCurrentConversation(appId, chatId) helper.
  • watch/nextTick imports are fine, but if you switch to awaiting the loader promise, watch (or nextTick) 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>

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 first selectConversation completes, leading to redundant API calls or state inconsistencies.
  • Suggestion: Add a local loading/guard flag or ensure selectConversation itself handles concurrent requests idempotently. Alternatively, use nextTick or 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:

  1. If any loading is happening globally (msgs.loading), disable it.
  2. If it is the current chat AND canShare is false (e.g., empty messages/streaming), disable it.

However, if you click a non-current conversation:

  1. currentChatId !== conversation.id.
  2. msgs.loading.value is checked. If false, the button is enabled.
  3. But what if msgs.loading refers to the current view's messages, not the target view? If msgs.loading is 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. Ensure msgs.loading is 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

  1. Add Try/Catch: Handle async errors in handleShareConversation.
  2. Race Condition Guard: Consider preventing repeated clicks during the async switching process.
  3. Refine Disable Logic: Verify if msgs.loading correctly prevents interaction with background conversations if intended. Extract complex disable logic to a helper/computed for readability if used in multiple places.
  4. 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>

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 shareConversation returns 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 called shareLink?.enterShare(), which likely handled complex pre-checks, permission validations, or state preparation for sharing. The new logic directly calls portalList.shareConversation.
  • Risk: If portalList.shareConversation does 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.shareConversation handles 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.value reflects 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 shareConversation is an async API call, consider handling errors gracefully here. Currently, if shareConversation rejects, the drawer stays open, but there’s no explicit try/catch or 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:
    • !canShare already includes !msgs.streamLoading.value and !msgs.loading.value (from the updated computed prop).
    • So msgs.loading.value is redundant in the disabled condition because canShare would be false if msgs.loading.value is true, but only if isActiveConversation(conversation) is true.
    • Bug/Inconsistency: If the conversation is NOT active, canShare is irrelevant to this item's logic in the second part of the OR condition. However, canShare is computed based on the active conversation's messages (msgs.messages.value).
    • Crucial Check: Does shareConversation require the conversation to be loaded/active?
      • If you are trying to share a non-active conversation, msgs.messages.value refers to the currently active conversation, not the conversation from the loop.
      • Therefore, canShare is 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 the disabled check relying on canShare (which checks active conversation state) is logically flawed for non-active conversations.

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 unless shareConversation requires 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

  1. Remove Redundant Loading Checks:
    Since canShare now includes !msgs.loading.value, and messages.value.length > 0 implies loads finished for active convo, the disabled logic in the template is messy.

    If shareConversation is 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 shareConversation that might trigger heavy lazy-loading.

  2. Type Safety:
    The diff uses conversation: PortalConversationSummary in arguments but the original removed usage of PortalConversationSummary import? No, it seems present. Ensure PortalConversationSummary is imported in the script setup section if used in TS types (it seems to be part of the existing structure).

  3. UX Improvement:
    Adding an icon or text change for "Sharing..." state while waiting for await portalList.shareConversation would improve UX, as closing the drawer might feel abrupt if the network request is slow.

Summary of Recommendations

  1. 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, remove canShare from their disabled logic. If they can't, restrict the dropdown item to active conversations only (v-if="isActiveConversation(...)").
  2. Add Error Handling: Wrap handleShareConversation in try/catch to notify users if sharing fails.
  3. Verify Store Responsibilities: Ensure portalList.shareConversation replicates necessary logic from the removed useShareLinkStoreOptional (e.g., permissions, cleanup).
  4. Simplify Template Logic: Decouple msgs.loading (global/active state) from individual item state where inappropriate.

@wangdan-fit2cloud
wangdan-fit2cloud merged commit 91ec3dc into v3 Nov 10, 2026
3 of 4 checks passed
@wangdan-fit2cloud
wangdan-fit2cloud deleted the pr@v3@fix_chat_share branch November 10, 2026 06:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants