Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion application/single_app/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@
EXECUTOR_TYPE = 'thread'
EXECUTOR_MAX_WORKERS = 30
SESSION_TYPE = 'filesystem'
VERSION = "0.261.105"
VERSION = "0.261.106"
IS_DEVELOPMENT = is_development_env_enabled()

SESSION_COOKIE_SAMESITE = os.getenv('SESSION_COOKIE_SAMESITE', 'Lax')
Expand Down
32 changes: 24 additions & 8 deletions application/single_app/functions_collaboration.py
Original file line number Diff line number Diff line change
Expand Up @@ -1154,10 +1154,23 @@ def _copy_legacy_group_messages_to_collaboration(source_conversation_id, collabo


def ensure_group_collaboration_for_legacy_conversation(source_conversation_id, owner_user, invited_participants=None):
source_conversation_doc = cosmos_group_conversations_container.read_item(
item=source_conversation_id,
partition_key=source_conversation_id,
)
source_container = cosmos_group_conversations_container
copy_source_messages = _copy_legacy_group_messages_to_collaboration
source_link_field = 'legacy_source_conversation_id'
try:
source_conversation_doc = source_container.read_item(
item=source_conversation_id,
partition_key=source_conversation_id,
)
except CosmosResourceNotFoundError:
# Group context can classify a conversation without moving its backing stores.
source_container = cosmos_conversations_container
copy_source_messages = _copy_legacy_personal_messages_to_collaboration
source_link_field = 'source_conversation_id'
source_conversation_doc = source_container.read_item(
item=source_conversation_id,
partition_key=source_conversation_id,
)
owner_summary = owner_user or {}
owner_user_id = str(owner_summary.get('user_id') or '').strip()
if not owner_user_id:
Expand Down Expand Up @@ -1236,8 +1249,9 @@ def ensure_group_collaboration_for_legacy_conversation(source_conversation_id, o
)
collaboration_conversation_doc['strict'] = bool(source_conversation_doc.get('strict', False))
collaboration_conversation_doc['summary'] = source_conversation_doc.get('summary')
collaboration_conversation_doc['legacy_source_conversation_id'] = source_conversation_id
collaboration_conversation_doc['legacy_source_scope'] = 'group'
collaboration_conversation_doc[source_link_field] = source_conversation_id
if source_link_field == 'legacy_source_conversation_id':
collaboration_conversation_doc['legacy_source_scope'] = 'group'

source_context = list(source_conversation_doc.get('context', []) or [])
if source_context:
Expand All @@ -1249,7 +1263,7 @@ def ensure_group_collaboration_for_legacy_conversation(source_conversation_id, o
if source_locked_contexts:
collaboration_conversation_doc['locked_contexts'] = source_locked_contexts

copied_messages = _copy_legacy_group_messages_to_collaboration(
copied_messages = copy_source_messages(
source_conversation_id,
collaboration_conversation_doc.get('id'),
owner_summary,
Expand All @@ -1270,7 +1284,9 @@ def ensure_group_collaboration_for_legacy_conversation(source_conversation_id, o
source_conversation_doc['converted_to_collaboration_at'] = conversion_timestamp
source_conversation_doc['is_hidden'] = True
source_conversation_doc['last_updated'] = conversion_timestamp
cosmos_group_conversations_container.upsert_item(source_conversation_doc)
source_container.upsert_item(source_conversation_doc)
invalidate_conversation_cache_for_item(source_conversation_doc, reason="collaboration_source_converted")
invalidate_conversation_cache_for_item(collaboration_conversation_doc, reason="collaboration_converted")

log_event(
'[COLLABORATION] Converted group conversation into collaborative conversation',
Expand Down
41 changes: 33 additions & 8 deletions application/v2_ui/src/components/chat/ParticipantsPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,13 @@ import { useCollaborationStore, participantName } from '../../stores/collaborati
import { useBootstrapStore } from '../../stores/bootstrapStore';
import { toast } from '../../stores/toastStore';
import { fetchCollaboratorSuggestions, fetchGroupMembers } from '../../lib/collaboration';
import { panelTargetForConversation } from '../../lib/sharing';
import { GlassButton, GlassPanel, Skeleton } from '../ui/primitives';
import type { CollaborationParticipant, CollaboratorSuggestion } from '../../lib/types';
import {
GROUP_MULTI_USER_CHAT_TYPE,
type CollaborationParticipant,
type CollaboratorSuggestion,
} from '../../lib/types';

/** How long to wait after a keystroke before searching. */
const SEARCH_DEBOUNCE_MS = 250;
Expand Down Expand Up @@ -273,7 +278,19 @@ export function ParticipantsPanel() {
const canManageRoles = shared && Boolean(conversation?.can_manage_roles);
const canDelete = shared && Boolean(conversation?.can_delete_conversation);
const canLeave = shared && Boolean(conversation?.can_leave_conversation);
const groupId = panelTarget.groupId ?? (shared ? conversation?.group_id : null) ?? null;
const groupId = panelTarget.groupId
?? (shared ? panelTargetForConversation(panelTarget.conversationId, conversation).groupId : null)
?? null;
const sharedScope = shared ? conversation?.scope : null;
const isGroupScope = sharedScope !== null
&& typeof sharedScope === 'object'
&& 'type' in sharedScope
&& sharedScope.type === 'group';
const missingGroupContext = !groupId && (
panelTarget.kind === 'group'
|| (shared && conversation?.chat_type === GROUP_MULTI_USER_CHAT_TYPE)
|| isGroupScope
);

const existingIds = new Set(
participants.map((participant) => String(participant.user_id ?? '').trim()),
Expand Down Expand Up @@ -463,12 +480,20 @@ export function ParticipantsPanel() {
)}

{canManageMembers ? (
<InviteSearch
groupId={groupId}
excludeUserIds={existingIds}
onInvite={(participant) => void invite(participant)}
busy={busy}
/>
missingGroupContext ? (
<p role="alert" className="flex items-start gap-1.5 text-sm text-danger">
<TriangleAlert size={14} className="mt-0.5 shrink-0" />
This conversation's group could not be identified. Reload it
before adding people.
</p>
) : (
<InviteSearch
groupId={groupId}
excludeUserIds={existingIds}
onInvite={(participant) => void invite(participant)}
busy={busy}
/>
)
) : (
shared && (
<p className="text-xs text-text-3">
Expand Down
2 changes: 1 addition & 1 deletion application/v2_ui/src/lib/conversationBadges.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ function contexts(metadata: BadgeSource | null | undefined): ContextEntry[] {
}

/** The context a conversation is primarily bound to, for a given scope. */
function primaryContext(
export function primaryContext(
metadata: BadgeSource | null | undefined,
scope: 'group' | 'public',
): ContextEntry | undefined {
Expand Down
26 changes: 16 additions & 10 deletions application/v2_ui/src/lib/sharing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
// chat-collaboration.js.

import { isCollaborative } from './types';
import { primaryContext, resolveChatType } from './conversationBadges';
import type { Conversation, ConversationMetadata } from './types';
import type { ParticipantsPanelTarget } from '../stores/collaborationStore';

Expand All @@ -21,17 +22,16 @@ import type { ParticipantsPanelTarget } from '../stores/collaborationStore';
* so offering to share one would present an action with nothing behind it.
*/
const SHAREABLE_CHAT_TYPES = new Set([
'',
'personal_single_user',
'personal_multi_user',
'group',
'group-single-user',
'group_single_user',
'group_multi_user',
]);

function chatTypeOf(conversation: Conversation | ConversationMetadata | null | undefined): string {
return String(conversation?.chat_type ?? '')
.trim()
.toLowerCase();
return resolveChatType(conversation).toLowerCase();
}

/** Whether a Share action should be offered for this conversation at all. */
Expand All @@ -57,24 +57,30 @@ export function panelTargetForConversation(
conversation: Conversation | ConversationMetadata | null | undefined,
): ParticipantsPanelTarget {
const chatType = chatTypeOf(conversation);
const groupId =
(conversation?.group_id as string | undefined) ??
(conversation?.scope as { group_id?: string } | undefined)?.group_id ??
null;
const scope = conversation?.scope;
const scopeGroupId =
scope && typeof scope === 'object' && 'group_id' in scope ? scope.group_id : undefined;
// Regular-stored group chats carry their workspace in primary context, not scope.
const groupId = [
conversation?.group_id,
scopeGroupId,
primaryContext(conversation, 'group')?.id,
].find((value): value is string => typeof value === 'string' && value.trim().length > 0)
?.trim() ?? null;

if (isCollaborative(conversation)) {
return {
conversationId,
kind: 'collaborative',
title: conversation?.title as string | undefined,
title: conversation?.title,
groupId,
};
}

return {
conversationId,
kind: chatType.startsWith('group') ? 'group' : 'personal',
title: conversation?.title as string | undefined,
title: conversation?.title,
groupId,
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,17 @@

Planning version: **0.250.062**

Implemented in version: **Not implemented - discovery and planning only**
Historical plan status: **Discovery and planning only; not implemented in full**

Related configuration version: `application/single_app/config.py` currently sets `VERSION = "0.250.062"`.
Related configuration version at planning time: `application/single_app/config.py` set `VERSION = "0.250.062"`.

The backend source-storage mismatch described below is fixed for Development/v1 in
**0.261.024** under [#1472](https://github.com/microsoft/simplechat/issues/1472).
The separate React/v2 port and People-panel group-context handling are implemented
in **0.261.106**.
See [Group Collaboration Source Storage Fix](../fixes/GROUP_COLLABORATION_SOURCE_STORAGE_FIX.md)
for the implemented scope and coverage. Neither change implements this historical
plan's classic-UI wording, stale-DOM, or endpoint-selection proposals in full.

## Overview

Expand Down Expand Up @@ -315,4 +323,3 @@ Cover:
1. Should `/from-group/<id>/members` delegate when `<id>` is already a group collaborative conversation ID, or should it return a diagnostic error?
2. Should the UI show a one-line hint with the active group name in the participant picker?
3. Should group participant suggestions include pending group users, or only accepted/current group members? Current behavior should remain accepted/current members unless product requirements change.

Loading