Skip to content

Commit 2ce2395

Browse files
Ida631lyan608
andauthored
feature: add rename chat history feature (#129)
Co-authored-by: lyan608 <lyan608@aucklanduni.ac.nz>
1 parent f4e07c5 commit 2ce2395

5 files changed

Lines changed: 109 additions & 6 deletions

File tree

lib/presentation/chat/viewModels/chat_view_model.dart

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,21 @@ class ChatViewModel extends ChangeNotifier {
140140
}
141141
}
142142

143+
Future<void> updateChatTitle(Chat chat, String newTitle) async {
144+
try {
145+
final updated = await _chatUseCase.updateChatTitle(chat, newTitle);
146+
final index = chats.indexWhere((c) => c.id == chat.id);
147+
if (index >= 0) {
148+
chats[index].title = updated.title;
149+
}
150+
notifyListeners();
151+
} catch (e) {
152+
if (kDebugMode) print('Update error: $e');
153+
errorMessage = 'Failed to rename chat. Please try again.';
154+
notifyListeners();
155+
}
156+
}
157+
143158
Future<void> checkAuthStatus() async {
144159
isLoggedIn = await _authUseCase.isLoggedIn();
145160
print("isLoggedIn: $isLoggedIn");

lib/presentation/chat/views/chat_list.dart

Lines changed: 70 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,36 @@ import 'package:flutter/material.dart';
22
import 'package:provider/provider.dart';
33
import '../viewModels/chat_view_model.dart';
44

5+
/// Returns the new title if user confirms, null if cancelled.
6+
Future<String?> showRenameDialog(BuildContext context, String currentTitle) async {
7+
final controller = TextEditingController(text: currentTitle);
8+
return showDialog<String>(
9+
context: context,
10+
builder: (context) => AlertDialog(
11+
title: const Text('Rename chat'),
12+
content: TextField(
13+
controller: controller,
14+
decoration: const InputDecoration(
15+
labelText: 'Title',
16+
border: OutlineInputBorder(),
17+
),
18+
autofocus: true,
19+
onSubmitted: (_) => Navigator.of(context).pop(controller.text.trim()),
20+
),
21+
actions: [
22+
TextButton(
23+
onPressed: () => Navigator.of(context).pop(),
24+
child: const Text('Cancel'),
25+
),
26+
ElevatedButton(
27+
onPressed: () => Navigator.of(context).pop(controller.text.trim()),
28+
child: const Text('Save'),
29+
),
30+
],
31+
),
32+
);
33+
}
34+
535
class ChatList extends StatelessWidget {
636
const ChatList({Key? key}) : super(key: key);
737

@@ -46,16 +76,50 @@ class ChatList extends StatelessWidget {
4676
contentPadding: EdgeInsets.only(left: 16, right: 8),
4777
trailing: PopupMenuButton<String>(
4878
padding: EdgeInsets.zero,
49-
onSelected: (value) {
79+
onSelected: (value) async {
5080
switch (value) {
51-
case 'delete': chatViewModel.deleteChat(chat);
52-
break;
81+
case 'delete':
82+
chatViewModel.deleteChat(chat);
83+
break;
84+
case 'rename':
85+
final newTitle = await showRenameDialog(context, chat.title);
86+
if (newTitle != null && newTitle.isNotEmpty) {
87+
await chatViewModel.updateChatTitle(chat, newTitle);
88+
}
89+
break;
5390
}
5491
},
5592
itemBuilder: (context) => [
56-
PopupMenuItem(value: 'pin', child: Text('Pin')),
57-
PopupMenuItem(value: 'rename', child: Text('Rename')),
58-
PopupMenuItem(value: 'delete', child: Text('Delete')),
93+
PopupMenuItem(
94+
value: 'pin',
95+
child: Row(
96+
children: const [
97+
Icon(Icons.push_pin, size: 20),
98+
SizedBox(width: 8),
99+
Text('Pin'),
100+
],
101+
),
102+
),
103+
PopupMenuItem(
104+
value: 'rename',
105+
child: Row(
106+
children: const [
107+
Icon(Icons.edit, size: 20),
108+
SizedBox(width: 8),
109+
Text('Rename'),
110+
],
111+
),
112+
),
113+
PopupMenuItem(
114+
value: 'delete',
115+
child: Row(
116+
children: const [
117+
Icon(Icons.delete, size: 20),
118+
SizedBox(width: 8),
119+
Text('Delete'),
120+
],
121+
),
122+
),
59123
],
60124
),
61125
title: Text(

packages/data/lib/chat/chat_repository_imp.dart

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,4 +70,22 @@ class ChatRepositoryImpl implements ChatRepository {
7070
throw mapServerErrorToDomainException(response);
7171
}
7272
}
73+
74+
@override
75+
Future<Chat> updateChatTitle(Chat chat, String newTitle) async {
76+
final baseURL = await apiEnvironment.getBaseUrl();
77+
final url = Uri.parse('$baseURL/chats/${chat.id}');
78+
final response = await authClient.post(
79+
url,
80+
headers: {'Content-Type': 'application/json'},
81+
body: jsonEncode({'chat': {'title': newTitle}}),
82+
);
83+
if (response.statusCode == 200 || response.statusCode == 204) {
84+
final decoded = utf8.decode(response.bodyBytes);
85+
final data = jsonDecode(decoded) as Map<String, dynamic>;
86+
return Chat.fromJson(data);
87+
} else {
88+
throw mapServerErrorToDomainException(response);
89+
}
90+
}
7391
}

packages/domain/lib/chat/chat_repository.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,5 @@ abstract class ChatRepository {
55
Future<List<Chat>> fetchChats(); // Fetch sessions from a data source
66
Future<List<Message>> fetchMessages(Chat chat);
77
Future<void> deleteChat(Chat chat);
8+
Future<Chat> updateChatTitle(Chat chat, String newTitle);
89
}

packages/domain/lib/chat/chat_use_case.dart

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ abstract class ChatUseCase {
77
Future<List<Message>> fetchMessages(Chat chat);
88

99
Future<void> deleteChat(Chat chat);
10+
11+
Future<Chat> updateChatTitle(Chat chat, String newTitle);
1012
}
1113

1214

@@ -27,4 +29,7 @@ class ChatUseCaseImpl implements ChatUseCase {
2729
Future<void> deleteChat(Chat chat) async {
2830
return repository.deleteChat(chat);
2931
}
32+
Future<Chat> updateChatTitle(Chat chat, String newTitle) async {
33+
return repository.updateChatTitle(chat, newTitle);
34+
}
3035
}

0 commit comments

Comments
 (0)