Skip to content
Open
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
22 changes: 22 additions & 0 deletions app/lib/backend/http/api/conversations.dart
Original file line number Diff line number Diff line change
Expand Up @@ -451,3 +451,25 @@ Future<bool> updateActionItemStateByMetadata(
) async {
return await setConversationActionItemState(conversationId, [itemIndex], [newState]);
}

Future<ServerConversation?> mergeConversations(List<String> conversationIds) async {
var response = await makeApiCall(
url: '${Env.apiBaseUrl}v1/conversations/merge',
headers: {},
method: 'POST',
body: jsonEncode({
'conversation_ids': conversationIds,
}),
);

if (response == null) return null;
debugPrint('mergeConversations: ${response.statusCode}');

if (response.statusCode == 200) {
var body = utf8.decode(response.bodyBytes);
return ServerConversation.fromJson(jsonDecode(body));
} else {
debugPrint('mergeConversations error: ${response.body}');
return null;
}
}
95 changes: 93 additions & 2 deletions app/lib/pages/conversations/conversations_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -137,8 +137,10 @@ class _ConversationsPageState extends State<ConversationsPage> with AutomaticKee
debugPrint('building conversations page');
super.build(context);
return Consumer<ConversationProvider>(builder: (context, convoProvider, child) {
return RefreshIndicator(
onRefresh: () async {
return Stack(
children: [
RefreshIndicator(
onRefresh: () async {
HapticFeedback.mediumImpact();
Provider.of<CaptureProvider>(context, listen: false).refreshInProgressConversations();
await convoProvider.getInitialConversations();
Expand Down Expand Up @@ -222,6 +224,95 @@ class _ConversationsPageState extends State<ConversationsPage> with AutomaticKee
),
],
),
),
// Merge toolbar
if (convoProvider.isSelectionMode)
Positioned(
bottom: 0,
left: 0,
right: 0,
child: Container(
padding: const EdgeInsets.all(16.0),
decoration: BoxDecoration(
color: Colors.black87,
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.3),
blurRadius: 10,
offset: const Offset(0, -2),
),
],
),
child: SafeArea(
top: false,
child: Row(
children: [
// Cancel button
TextButton(
onPressed: () {
convoProvider.disableSelectionMode();
},
child: const Text(
'Cancel',
style: TextStyle(color: Colors.white, fontSize: 16),
),
),
const Spacer(),
// Selection count
Text(
'${convoProvider.selectedConversationIds.length} selected',
style: const TextStyle(color: Colors.white70, fontSize: 14),
),
const SizedBox(width: 16),
// Merge button
ElevatedButton(
onPressed: convoProvider.canMergeSelectedConversations() && !convoProvider.isMerging
? () async {
HapticFeedback.mediumImpact();
bool success = await convoProvider.mergeSelectedConversations();
if (success && context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Conversations merged successfully'),
backgroundColor: Colors.green,
duration: Duration(seconds: 2),
),
);
} else if (!success && context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text(
'Failed to merge conversations. They must be adjacent (within 1 hour of each other).'),
backgroundColor: Colors.red,
duration: Duration(seconds: 4),
),
);
}
}
: null,
style: ElevatedButton.styleFrom(
backgroundColor: Colors.blue,
foregroundColor: Colors.white,
disabledBackgroundColor: Colors.grey,
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
),
child: convoProvider.isMerging
? const SizedBox(
width: 16,
height: 16,
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation<Color>(Colors.white),
),
)
: const Text('Merge'),
),
],
),
),
),
),
],
);
});
}
Expand Down
67 changes: 65 additions & 2 deletions app/lib/pages/conversations/widgets/conversation_list_item.dart
Original file line number Diff line number Diff line change
Expand Up @@ -66,8 +66,18 @@ class _ConversationListItemState extends State<ConversationListItem> {

Structured structured = widget.conversation.structured;
return Consumer<ConversationProvider>(builder: (context, provider, child) {
bool isSelected = provider.isConversationSelected(widget.conversation.id);

return GestureDetector(
onTap: () async {
// If in selection mode, toggle selection instead of opening detail
if (provider.isSelectionMode) {
// Don't allow selecting locked or discarded conversations
if (!widget.conversation.isLocked && !widget.conversation.discarded) {
provider.toggleConversationSelection(widget.conversation.id);
}
return;
}
if (widget.conversation.isLocked) {
MixpanelManager().paywallOpened('Conversation List Item');
routeToPage(context, const UsagePage(showUpgradeDialog: true));
Expand Down Expand Up @@ -111,10 +121,26 @@ class _ConversationListItemState extends State<ConversationListItem> {
}
}
},
onLongPress: () {
// Don't allow selecting locked or discarded conversations
if (widget.conversation.isLocked || widget.conversation.discarded) {
return;
}
// Enable selection mode and select this conversation
HapticFeedback.mediumImpact();
provider.enableSelectionMode();
provider.toggleConversationSelection(widget.conversation.id);
},
child: Padding(
padding:
EdgeInsets.only(top: 12, left: widget.isFromOnboarding ? 0 : 16, right: widget.isFromOnboarding ? 0 : 16),
child: Container(
child: Opacity(
opacity: (provider.isSelectionMode && (widget.conversation.isLocked || widget.conversation.discarded))
? 0.5
: 1.0,
child: Stack(
children: [
Container(
width: double.maxFinite,
decoration: BoxDecoration(
color: const Color(0xFF1F1F25),
Expand All @@ -124,7 +150,7 @@ class _ConversationListItemState extends State<ConversationListItem> {
borderRadius: BorderRadius.circular(16.0),
child: Dismissible(
key: UniqueKey(),
direction: DismissDirection.endToStart,
direction: provider.isSelectionMode ? DismissDirection.none : DismissDirection.endToStart,
background: Container(
alignment: Alignment.centerRight,
padding: const EdgeInsets.only(right: 20.0),
Expand Down Expand Up @@ -177,6 +203,43 @@ class _ConversationListItemState extends State<ConversationListItem> {
),
),
),
),
// Checkbox overlay for selection mode
if (provider.isSelectionMode)
Positioned(
top: 8,
right: 8,
child: Container(
decoration: BoxDecoration(
color: isSelected ? Colors.blue : Colors.white,
shape: BoxShape.circle,
border: Border.all(
color: isSelected ? Colors.blue : Colors.grey,
width: 2,
),
),
child: Icon(
Icons.check,
color: isSelected ? Colors.white : Colors.transparent,
size: 20,
),
),
),
// Selection mode highlight
if (provider.isSelectionMode && isSelected)
Positioned.fill(
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(16.0),
border: Border.all(
color: Colors.blue,
width: 2,
),
),
),
),
],
),
),
),
);
Expand Down
Loading