Skip to content

Commit 2525059

Browse files
feat: ✨ Add Menu Option for ChatViewList
1 parent 9b7ba18 commit 2525059

File tree

10 files changed

+242
-13
lines changed

10 files changed

+242
-13
lines changed

example/lib/chat_view_list_screen.dart

Lines changed: 50 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,56 @@ class _ChatViewListScreenState extends State<ChatViewListScreen> {
5353
),
5454
loadMoreChats: () async =>
5555
await Future.delayed(const Duration(seconds: 2)),
56+
menuConfig: ChatMenuConfig(
57+
enabled: true,
58+
deleteCallback: (chat) {
59+
Future.delayed(
60+
// Call this after the animation of menu is completed
61+
// To show the pin status change animation
62+
const Duration(milliseconds: 800),
63+
() {
64+
_chatListController?.removeChat(chat.id);
65+
},
66+
);
67+
Navigator.of(context).pop();
68+
},
69+
muteStatusCallback: (result) {
70+
Future.delayed(
71+
// Call this after the animation of menu is completed
72+
// To show the pin status change animation
73+
const Duration(milliseconds: 800),
74+
() {
75+
_chatListController?.updateChat(
76+
result.chat.id,
77+
(previousChat) => previousChat.copyWith(
78+
settings: previousChat.settings.copyWith(
79+
muteStatus: result.status,
80+
),
81+
),
82+
);
83+
},
84+
);
85+
Navigator.of(context).pop();
86+
},
87+
pinStatusCallback: (result) {
88+
Future.delayed(
89+
// Call this after the animation of menu is completed
90+
// To show the pin status change animation
91+
const Duration(milliseconds: 800),
92+
() {
93+
_chatListController?.updateChat(
94+
result.chat.id,
95+
(previousChat) => previousChat.copyWith(
96+
settings: previousChat.settings.copyWith(
97+
pinStatus: result.status,
98+
),
99+
),
100+
);
101+
},
102+
);
103+
Navigator.of(context).pop();
104+
},
105+
),
56106
config: ChatViewListConfig(
57107
enablePagination: true,
58108
loadMoreConfig: const LoadMoreConfig(),
@@ -77,9 +127,6 @@ class _ChatViewListScreenState extends State<ChatViewListScreen> {
77127
),
78128
);
79129
},
80-
onLongPress: (chat) {
81-
debugPrint('Long pressed on chat: ${chat.name}');
82-
},
83130
),
84131
searchConfig: ChatViewListSearchConfig(
85132
textEditingController: _searchController,

lib/src/controller/chat_list_view_controller.dart

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,16 @@ class ChatViewListController {
115115
_chatListStreamController.add(chatListMap);
116116
}
117117

118+
/// Removes the chat with the given [chatId] from the chat list.
119+
///
120+
/// If the chat with [chatId] does not exist, the method returns without
121+
/// making changes.
122+
void removeChat(String chatId) {
123+
chatListMap.remove(chatId);
124+
if (_chatListStreamController.isClosed) return;
125+
_chatListStreamController.add(chatListMap);
126+
}
127+
118128
/// Adds the given chat search results to the stream after the current frame.
119129
void setSearchChats(List<ChatViewListItem> searchResults) {
120130
final searchResultLength = searchResults.length;
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import 'package:flutter/cupertino.dart';
2+
3+
import '../../../values/enumeration.dart';
4+
import '../../../values/typedefs.dart';
5+
6+
class ChatMenuConfig {
7+
const ChatMenuConfig({
8+
this.enabled = true,
9+
this.actions,
10+
this.textStyle,
11+
this.menuBuilder,
12+
this.muteStatusCallback,
13+
this.pinStatusCallback,
14+
this.deleteCallback,
15+
this.muteStatusTrailingIcon,
16+
this.pinStatusTrailingIcon,
17+
this.deleteTrailingIcon,
18+
});
19+
20+
/// Whether the context menu is enabled.
21+
///
22+
/// Defaults to `true`.
23+
///
24+
/// if set to `false`, the default or custom context menu
25+
/// will not be shown.
26+
final bool enabled;
27+
28+
/// Custom text style for the menu items.
29+
final TextStyle? textStyle;
30+
31+
/// Custom actions to be added to the context menu.
32+
///
33+
/// by default, it will add the mute and pin actions
34+
/// if callbacks are provided.
35+
final MenuActionBuilder? actions;
36+
37+
/// Custom menu builder to create the context menu.
38+
final MenuBuilderCallback? menuBuilder;
39+
40+
/// Callback for mute status changes.
41+
///
42+
/// In this callback, new mute status along with the chat
43+
/// is provided.
44+
final ChatStatusCallback<MuteStatus>? muteStatusCallback;
45+
46+
/// Callback for pin status changes.
47+
final ChatStatusCallback<PinStatus>? pinStatusCallback;
48+
49+
/// Callback for delete chat.
50+
final DeleteChatCallback? deleteCallback;
51+
52+
/// Custom trailing icon for mute status menu item.
53+
final StatusTrailingIcon<MuteStatus>? muteStatusTrailingIcon;
54+
55+
/// Custom trailing icon for pin status menu item.
56+
final StatusTrailingIcon<PinStatus>? pinStatusTrailingIcon;
57+
58+
/// Custom trailing icon for delete menu item.
59+
final IconData? deleteTrailingIcon;
60+
}

lib/src/models/config_models/chat_view_list/list_tile_config.dart

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,6 @@ class ListTileConfig {
5252
this.userNameTextStyle,
5353
this.lastMessageTextStyle,
5454
this.onTap,
55-
this.onLongPress,
5655
this.lastMessageTileBuilder,
5756
this.userNameBuilder,
5857
this.trailingBuilder,
@@ -109,9 +108,6 @@ class ListTileConfig {
109108
/// Callback function that is called when a user taps on a chat item.
110109
final ValueSetter<ChatViewListItem>? onTap;
111110

112-
/// Callback function that is called when the user long presses on a chat item.
113-
final ValueSetter<ChatViewListItem>? onLongPress;
114-
115111
/// Configuration for the user avatar in the chat list.
116112
final UserAvatarConfig userAvatarConfig;
117113

lib/src/models/config_models/chat_view_list/models.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
* SOFTWARE.
2121
*/
2222
export 'chat_settings.dart';
23+
export 'chat_menu_config.dart';
2324
export 'chat_view_list_config.dart';
2425
export 'list_tile_config.dart';
2526
export 'last_message_time_config.dart';

lib/src/utils/chat_view_locale.dart

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ final class ChatViewLocale {
5353
required this.unmute,
5454
required this.pin,
5555
required this.unpin,
56+
required this.deleteChat,
5657
});
5758

5859
/// Create from `Map<String, String>`
@@ -88,6 +89,7 @@ final class ChatViewLocale {
8889
unmute: map['unmute']?.toString() ?? '',
8990
pin: map['pin']?.toString() ?? '',
9091
unpin: map['unpin']?.toString() ?? '',
92+
deleteChat: map['deleteChat']?.toString() ?? '',
9193
);
9294
}
9395

@@ -121,6 +123,7 @@ final class ChatViewLocale {
121123
final String unmute;
122124
final String pin;
123125
final String unpin;
126+
final String deleteChat;
124127

125128
/// English defaults
126129
static const en = ChatViewLocale(
@@ -154,5 +157,6 @@ final class ChatViewLocale {
154157
unmute: 'Unmute',
155158
pin: 'Pin',
156159
unpin: 'Unpin',
160+
deleteChat: 'Delete Chat',
157161
);
158162
}

lib/src/values/typedefs.dart

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,7 @@ typedef UnreadCountWidgetBuilder = Widget Function(int count);
123123
typedef ChatStatusCallback<T> = void Function(
124124
({ChatViewListItem chat, T status}) result,
125125
);
126+
typedef DeleteChatCallback = void Function(ChatViewListItem chat);
126127
typedef StatusTrailingIcon<T> = IconData Function(T status);
127128
typedef LastMessageTimeBuilder = Widget Function(DateTime time);
128129
typedef ChatViewListTileBuilder = Widget Function(
@@ -136,3 +137,10 @@ typedef ChatSorter = int Function(
136137
ChatViewListItem chat1,
137138
ChatViewListItem chat2,
138139
);
140+
typedef MenuWidgetCallback = Widget Function(ChatViewListItem chat);
141+
typedef MenuBuilderCallback = Widget Function(
142+
BuildContext context,
143+
ChatViewListItem chat,
144+
Widget child,
145+
);
146+
typedef MenuActionBuilder = List<Widget> Function(ChatViewListItem chat);
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
import 'package:flutter/cupertino.dart';
2+
import 'package:flutter/material.dart';
3+
4+
import '../../models/models.dart';
5+
import '../../utils/package_strings.dart';
6+
import '../../values/enumeration.dart';
7+
8+
class ChatListTileContextMenu extends StatelessWidget {
9+
const ChatListTileContextMenu({
10+
required this.chat,
11+
required this.child,
12+
required this.config,
13+
required this.chatTileColor,
14+
super.key,
15+
});
16+
17+
final Widget child;
18+
final ChatViewListItem chat;
19+
final ChatMenuConfig config;
20+
final Color chatTileColor;
21+
22+
@override
23+
Widget build(BuildContext context) {
24+
if (!config.enabled) return child;
25+
26+
final menu = config.menuBuilder?.call(context, chat, child);
27+
if (menu != null) return menu;
28+
29+
final newMuteStatus = switch (chat.settings.muteStatus) {
30+
MuteStatus.muted => MuteStatus.unmute,
31+
MuteStatus.unmute => MuteStatus.muted,
32+
};
33+
final newPinStatus = switch (chat.settings.pinStatus) {
34+
PinStatus.pinned => PinStatus.unpinned,
35+
PinStatus.unpinned => PinStatus.pinned,
36+
};
37+
38+
final actions = <Widget>[
39+
...?config.actions?.call(chat),
40+
if (config.muteStatusCallback != null)
41+
CupertinoContextMenuAction(
42+
trailingIcon: config.muteStatusTrailingIcon?.call(
43+
newMuteStatus,
44+
) ??
45+
newMuteStatus.iconData,
46+
child: Text(
47+
newMuteStatus.menuName,
48+
style: config.textStyle,
49+
),
50+
onPressed: () => config.muteStatusCallback?.call((
51+
chat: chat,
52+
status: newMuteStatus,
53+
)),
54+
),
55+
if (config.pinStatusCallback != null)
56+
CupertinoContextMenuAction(
57+
trailingIcon: config.pinStatusTrailingIcon?.call(
58+
newPinStatus,
59+
) ??
60+
newPinStatus.iconData,
61+
child: Text(
62+
newPinStatus.menuName,
63+
style: config.textStyle,
64+
),
65+
onPressed: () => config.pinStatusCallback?.call((
66+
chat: chat,
67+
status: newPinStatus,
68+
)),
69+
),
70+
if (config.deleteCallback != null)
71+
CupertinoContextMenuAction(
72+
isDestructiveAction: true,
73+
trailingIcon: config.deleteTrailingIcon ?? Icons.delete_forever,
74+
child: Text(
75+
PackageStrings.currentLocale.deleteChat,
76+
style: config.textStyle,
77+
),
78+
onPressed: () => config.deleteCallback?.call(chat),
79+
),
80+
];
81+
82+
return actions.isEmpty
83+
? child
84+
: CupertinoContextMenu.builder(
85+
builder: (_, __) => Material(
86+
color: chatTileColor,
87+
child: child,
88+
),
89+
actions: actions,
90+
);
91+
}
92+
}

lib/src/widgets/chat_view_list/chat_view_list.dart

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,17 +25,20 @@ import 'package:flutter/material.dart';
2525

2626
import '../../controller/chat_list_view_controller.dart';
2727
import '../../models/chat_view_list_item.dart';
28+
import '../../models/config_models/chat_view_list/chat_menu_config.dart';
2829
import '../../models/config_models/chat_view_list/chat_view_list_config.dart';
2930
import '../../models/config_models/chat_view_list/load_more_config.dart';
3031
import '../../values/typedefs.dart';
3132
import '../../utils/constants/constants.dart';
33+
import 'chat_list_tile_context_menu.dart';
3234
import 'chat_view_list_item_tile.dart';
3335
import 'search_text_field.dart';
3436

3537
class ChatViewList extends StatefulWidget {
3638
const ChatViewList({
3739
required this.controller,
3840
this.config = const ChatViewListConfig(),
41+
this.menuConfig = const ChatMenuConfig(),
3942
this.scrollViewKeyboardDismissBehavior =
4043
ScrollViewKeyboardDismissBehavior.onDrag,
4144
this.chatBuilder,
@@ -75,6 +78,9 @@ class ChatViewList extends StatefulWidget {
7578
/// Defaults to `ScrollViewKeyboardDismissBehavior.onDrag`.
7679
final ScrollViewKeyboardDismissBehavior? scrollViewKeyboardDismissBehavior;
7780

81+
/// Callback to provide a widget for the menu in the chat list.
82+
final ChatMenuConfig menuConfig;
83+
7884
@override
7985
State<ChatViewList> createState() => _ChatViewListState();
8086
}
@@ -129,11 +135,17 @@ class _ChatViewListState extends State<ChatViewList> {
129135
}
130136

131137
final chat = chats[itemIndex];
132-
return widget.chatBuilder?.call(context, chat) ??
133-
ChatViewListItemTile(
134-
chat: chat,
135-
config: widget.config.tileConfig,
136-
);
138+
return ChatListTileContextMenu(
139+
key: ValueKey(chat.id),
140+
chat: chat,
141+
config: widget.menuConfig,
142+
chatTileColor: widget.config.backgroundColor,
143+
child: widget.chatBuilder?.call(context, chat) ??
144+
ChatViewListItemTile(
145+
chat: chat,
146+
config: widget.config.tileConfig,
147+
),
148+
);
137149
},
138150
),
139151
);

lib/src/widgets/chat_view_list/chat_view_list_item_tile.dart

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,6 @@ class ChatViewListItemTile extends StatelessWidget {
6262
final hasTimeSubChild = isPinned || isMuted || showUnreadCount;
6363
return GestureDetector(
6464
behavior: HitTestBehavior.opaque,
65-
onLongPress: () => config.onLongPress?.call(chat),
6665
onTap: () {
6766
FocusManager.instance.primaryFocus?.unfocus();
6867
config.onTap?.call(chat);

0 commit comments

Comments
 (0)