Skip to content

Commit fe407e4

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

File tree

11 files changed

+279
-9
lines changed

11 files changed

+279
-9
lines changed

example/lib/chat_view_list_screen.dart

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,26 @@ class _ChatViewListScreenState extends State<ChatViewListScreen> {
5353
),
5454
loadMoreChats: () async =>
5555
await Future.delayed(const Duration(seconds: 2)),
56+
menuConfig: ChatMenuConfig(
57+
deleteCallback: (chat) =>
58+
_chatListController?.removeChat(chat.id),
59+
muteStatusCallback: (result) => _chatListController?.updateChat(
60+
result.chat.id,
61+
(previousChat) => previousChat.copyWith(
62+
settings: previousChat.settings.copyWith(
63+
muteStatus: result.status,
64+
),
65+
),
66+
),
67+
pinStatusCallback: (result) => _chatListController?.updateChat(
68+
result.chat.id,
69+
(previousChat) => previousChat.copyWith(
70+
settings: previousChat.settings.copyWith(
71+
pinStatus: result.status,
72+
),
73+
),
74+
),
75+
),
5676
config: ChatViewListConfig(
5777
enablePagination: true,
5878
loadMoreConfig: const LoadMoreConfig(),
@@ -77,9 +97,6 @@ class _ChatViewListScreenState extends State<ChatViewListScreen> {
7797
),
7898
);
7999
},
80-
onLongPress: (chat) {
81-
debugPrint('Long pressed on chat: ${chat.name}');
82-
},
83100
),
84101
searchConfig: ChatViewListSearchConfig(
85102
textEditingController: _searchController,

lib/src/controller/chat_list_view_controller.dart

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,17 @@ 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+
if (!chatListMap.containsKey(chatId)) return;
124+
chatListMap.remove(chatId);
125+
if (_chatListStreamController.isClosed) return;
126+
_chatListStreamController.add(chatListMap);
127+
}
128+
118129
/// Adds the given chat search results to the stream after the current frame.
119130
void setSearchChats(List<ChatViewListItem> searchResults) {
120131
final searchResultLength = searchResults.length;
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
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.maxLines = 1,
10+
this.overflow = TextOverflow.ellipsis,
11+
this.callbackDelayDuration = const Duration(milliseconds: 800),
12+
this.actions,
13+
this.textStyle,
14+
this.builder,
15+
this.muteStatusCallback,
16+
this.pinStatusCallback,
17+
this.deleteCallback,
18+
this.muteStatusIcon,
19+
this.pinStatusIcon,
20+
this.deleteIcon,
21+
});
22+
23+
/// Whether the context menu is enabled.
24+
///
25+
/// Defaults to `true`.
26+
///
27+
/// if set to `false`, the default or custom context menu
28+
/// will not be shown.
29+
final bool enabled;
30+
31+
/// Custom text style for the menu items.
32+
final TextStyle? textStyle;
33+
34+
/// Maximum number of lines for the menu item text.
35+
///
36+
/// Defaults to `1`.
37+
final int? maxLines;
38+
39+
/// Overflow behavior for the menu item text.
40+
///
41+
/// Defaults to `TextOverflow.ellipsis`.
42+
final TextOverflow? overflow;
43+
44+
/// Custom actions to be added to the context menu.
45+
///
46+
/// by default, it will add the mute and pin actions
47+
/// if callbacks are provided.
48+
final MenuActionBuilder? actions;
49+
50+
/// Custom menu builder to create the context menu.
51+
final MenuBuilderCallback? builder;
52+
53+
/// Callback for mute status changes.
54+
///
55+
/// In this callback, new mute status along with the chat
56+
/// is provided.
57+
final ChatStatusCallback<MuteStatus>? muteStatusCallback;
58+
59+
/// Callback for pin status changes.
60+
///
61+
/// In this callback, new pin status along with the chat
62+
/// is provided.
63+
final ChatStatusCallback<PinStatus>? pinStatusCallback;
64+
65+
/// Callback for delete chat.
66+
///
67+
/// In this callback, the chat to be deleted is provided.
68+
final DeleteChatCallback? deleteCallback;
69+
70+
/// The delay duration before executing a callback after a UI action.
71+
///
72+
/// For example, this can be used to wait for animations to complete
73+
/// (like menu expand/collapse) before running logic that updates
74+
/// the chat list or triggers another UI change.
75+
///
76+
/// If null, no extra delay is applied.
77+
///
78+
/// Defaults to `const Duration(milliseconds: 800)`.
79+
final Duration? callbackDelayDuration;
80+
81+
/// Custom icon for mute status menu item.
82+
final StatusTrailingIcon<MuteStatus>? muteStatusIcon;
83+
84+
/// Custom icon for pin status menu item.
85+
final StatusTrailingIcon<PinStatus>? pinStatusIcon;
86+
87+
/// Custom icon for delete menu item.
88+
final IconData? deleteIcon;
89+
}

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/enumeration.dart

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,16 @@ enum MuteStatus {
150150
/// Returns true if the chat is unmuted.
151151
bool get isUnmute => this == unmute;
152152

153+
/// Toggles the mute status.
154+
/// If the current status is muted, it returns unmute.
155+
/// If the current status is unmute, it returns muted.
156+
MuteStatus get toggle {
157+
return switch (this) {
158+
muted => unmute,
159+
unmute => muted,
160+
};
161+
}
162+
153163
String get menuName => switch (this) {
154164
muted => PackageStrings.currentLocale.mute,
155165
unmute => PackageStrings.currentLocale.unmute,
@@ -175,6 +185,16 @@ enum PinStatus {
175185
/// Returns true if the chat is unpinned.
176186
bool get isNone => this == unpinned;
177187

188+
/// Toggles the pin status.
189+
/// If the current status is pinned, it returns unpinned.
190+
/// If the current status is unpinned, it returns pinned.
191+
PinStatus get toggle {
192+
return switch (this) {
193+
pinned => unpinned,
194+
unpinned => pinned,
195+
};
196+
}
197+
178198
String get menuName => switch (this) {
179199
pinned => PackageStrings.currentLocale.pin,
180200
unpinned => PackageStrings.currentLocale.unpin,

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: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
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+
7+
class ChatListTileContextMenu extends StatelessWidget {
8+
const ChatListTileContextMenu({
9+
required this.chat,
10+
required this.child,
11+
required this.config,
12+
required this.chatTileColor,
13+
super.key,
14+
});
15+
16+
final Widget child;
17+
final ChatViewListItem chat;
18+
final ChatMenuConfig config;
19+
final Color chatTileColor;
20+
21+
@override
22+
Widget build(BuildContext context) {
23+
final menu = config.builder?.call(context, chat, child);
24+
if (menu != null) return menu;
25+
26+
final newMuteStatus = chat.settings.muteStatus.toggle;
27+
final newPinStatus = chat.settings.pinStatus.toggle;
28+
final delayDuration = config.callbackDelayDuration;
29+
30+
final actions = <Widget>[
31+
...?config.actions?.call(chat),
32+
if (config.muteStatusCallback case final callback?)
33+
CupertinoContextMenuAction(
34+
trailingIcon: config.muteStatusIcon?.call(newMuteStatus) ??
35+
newMuteStatus.iconData,
36+
child: Text(
37+
newMuteStatus.menuName,
38+
style: config.textStyle,
39+
overflow: config.overflow,
40+
maxLines: config.maxLines,
41+
),
42+
onPressed: () => _menuCallback(
43+
context: context,
44+
callbackDelayDuration: delayDuration,
45+
callback: () => callback.call(
46+
(chat: chat, status: newMuteStatus),
47+
),
48+
),
49+
),
50+
if (config.pinStatusCallback case final callback?)
51+
CupertinoContextMenuAction(
52+
trailingIcon:
53+
config.pinStatusIcon?.call(newPinStatus) ?? newPinStatus.iconData,
54+
child: Text(
55+
newPinStatus.menuName,
56+
style: config.textStyle,
57+
overflow: config.overflow,
58+
maxLines: config.maxLines,
59+
),
60+
onPressed: () => _menuCallback(
61+
context: context,
62+
callbackDelayDuration: delayDuration,
63+
callback: () => callback.call(
64+
(chat: chat, status: newPinStatus),
65+
),
66+
),
67+
),
68+
if (config.deleteCallback case final callback?)
69+
CupertinoContextMenuAction(
70+
isDestructiveAction: true,
71+
trailingIcon: config.deleteIcon ?? Icons.delete_forever,
72+
child: Text(
73+
PackageStrings.currentLocale.deleteChat,
74+
style: config.textStyle,
75+
overflow: config.overflow,
76+
maxLines: config.maxLines,
77+
),
78+
onPressed: () => _menuCallback(
79+
context: context,
80+
callbackDelayDuration: delayDuration,
81+
callback: () => callback.call(chat),
82+
),
83+
),
84+
];
85+
86+
return actions.isEmpty
87+
? child
88+
: CupertinoContextMenu.builder(
89+
builder: (_, __) => Material(
90+
color: chatTileColor,
91+
child: child,
92+
),
93+
actions: actions,
94+
);
95+
}
96+
97+
void _menuCallback({
98+
required BuildContext context,
99+
required VoidCallback callback,
100+
Duration? callbackDelayDuration,
101+
}) {
102+
if (callbackDelayDuration == null) {
103+
callback.call();
104+
} else {
105+
Future.delayed(callbackDelayDuration, callback);
106+
}
107+
Navigator.of(context).pop();
108+
}
109+
}

0 commit comments

Comments
 (0)