This commit is contained in:
abu 2025-09-13 17:01:01 +08:00
parent 09f0048b67
commit 539cf87c46
29 changed files with 2342 additions and 327 deletions

View File

@ -171,8 +171,9 @@ class ChatController extends GetxController {
///
Future<void> getNoFriendData({V2TimConversation? csion}) async {
//
// final hasNoFriend = chatList.any((item) => item.conversation.conversationGroupList?.contains(myConversationType.ConversationType.noFriend.name) ?? false);
final hasNoFriend = chatList.any((item) => item.isCustomAdmin!.contains(myConversationType.ConversationType.noFriend.name));
final hasNoFriend = chatList
.where((item) => item.conversation.type == 1) // type == 1
.any((item) => item.isCustomAdmin?.contains(myConversationType.ConversationType.noFriend.name) ?? false);
logger.w('检测是否存在nofriend入口$hasNoFriend');
if (hasNoFriend) {
//

View File

@ -3,6 +3,7 @@ import 'package:get/get.dart';
import 'package:loopin/IM/im_message.dart';
import 'package:loopin/IM/im_service.dart';
import 'package:loopin/utils/index.dart';
import 'package:tencent_cloud_chat_sdk/enum/message_elem_type.dart';
import 'package:tencent_cloud_chat_sdk/models/v2_tim_message.dart';
class ChatDetailController extends GetxController {
@ -10,10 +11,10 @@ class ChatDetailController extends GetxController {
ChatDetailController({required this.id});
final ScrollController chatController = ScrollController();
final RxList<V2TimMessage> chatList = <V2TimMessage>[].obs;
final RxBool isFriend = true.obs;
final RxInt followType = 0.obs;
final RxBool toolFlag = true.obs; // 使()
void updateChatListWithTimeLabels(List<V2TimMessage> originMessages) async {
final idRes = await ImService.instance.selfUserId();
@ -31,6 +32,9 @@ class ChatDetailController extends GetxController {
if (i == originMessages.length - 1) {
//
needInsertLabel = true;
} else if (current.localCustomData == 'time_label' || current.elemType == MessageElemType.V2TIM_ELEM_TYPE_GROUP_TIPS) {
//
needInsertLabel = true;
} else {
final next = originMessages[i + 1];
final nextTimestamp = next.timestamp ?? 0;

View File

@ -1,4 +1,6 @@
import 'package:get/get.dart';
import 'package:logger/logger.dart';
import 'package:loopin/pages/groupChat/controller/group_detail_controller.dart';
import 'package:tencent_cloud_chat_sdk/enum/V2TimGroupListener.dart';
import 'package:tencent_cloud_chat_sdk/models/v2_tim_group_change_info.dart';
import 'package:tencent_cloud_chat_sdk/models/v2_tim_group_member_change_info.dart';
@ -120,13 +122,28 @@ class ImGroupListeners {
void onMemberEnter(String groupID, List<V2TimGroupMemberInfo> memberList) {}
//
void onMemberLeave(String groupID, V2TimGroupMemberInfo member) {}
void onMemberLeave(String groupID, V2TimGroupMemberInfo member) {
if (Get.isRegistered<GroupDetailController>()) {
final ctl = Get.find<GroupDetailController>();
ctl.init();
}
}
//
void onMemberInvited(String groupID, List<V2TimGroupMemberInfo> memberList) {}
void onMemberInvited(String groupID, List<V2TimGroupMemberInfo> memberList) {
if (Get.isRegistered<GroupDetailController>()) {
final ctl = Get.find<GroupDetailController>();
ctl.init();
}
}
//
void onMemberKicked(String groupID, List<V2TimGroupMemberInfo> memberList) {}
void onMemberKicked(String groupID, List<V2TimGroupMemberInfo> memberList) {
if (Get.isRegistered<GroupDetailController>()) {
final ctl = Get.find<GroupDetailController>();
ctl.init();
}
}
//
void onMemberInfoChanged(String groupID, List<V2TimGroupMemberChangeInfo> v2timGroupMemberChangeInfoList) {}

View File

@ -22,13 +22,16 @@ class IMMessage {
String? toUserID,
String? groupID,
String? cloudCustomData,
String? groupName,
bool isPush = true,
bool isExcludedFromUnreadCount = false,
}) async {
// toUserID groupID
if ((toUserID == null && groupID == null) || (toUserID != null && groupID != null)) {
return ImResult(
success: false,
code: -1,
desc: "只能指定一个 receivertoUserIDgroupID",
desc: "只能指定一个receiver:toUserID或groupID",
);
}
if (cloudCustomData != null) {
@ -37,10 +40,15 @@ class IMMessage {
//
V2TimValueCallback<V2TimMessage> sendRes;
// final controller = Get.find<ChatDetailController>();
final myInfo = Get.find<ImUserInfoController>();
logger.w('启用默认title${myInfo.nickname.value}');
//
if (toUserID != null) {
final myInfo = Get.find<ImUserInfoController>();
logger.w('启用默认title${myInfo.nickname.value}');
OfflinePushInfo offlinePushInfo = OfflinePushInfo(
title: myInfo.nickname.value,
desc: parseMessageSummary(msg),
ext: jsonEncode({"userID": myInfo.userID.value, "title": myInfo.nickname.value}),
);
sendRes = await TencentImSDKPlugin.v2TIMManager.getMessageManager().sendMessage(
message: msg,
receiver: toUserID,
@ -53,31 +61,43 @@ class IMMessage {
// },
groupID: "",
priority: MessagePriorityEnum.V2TIM_PRIORITY_DEFAULT,
onlineUserOnly: false,
isExcludedFromUnreadCount: false,
isExcludedFromLastMessage: false,
needReadReceipt: false,
offlinePushInfo: OfflinePushInfo(
title: myInfo.nickname.value,
desc: parseMessageSummary(msg),
ext: jsonEncode({"userID": myInfo.userID.value, "title": myInfo.nickname.value}),
),
onlineUserOnly: false, // 线,
isExcludedFromUnreadCount: isExcludedFromUnreadCount, // message中设置的将失效
isExcludedFromLastMessage: false, //
isSupportMessageExtension: false, //
isExcludedFromContentModeration: false, //
needReadReceipt: false, //
offlinePushInfo: isPush ? offlinePushInfo : null,
cloudCustomData: cloudCustomData,
localCustomData: "",
);
} else {
//
OfflinePushInfo offlinePushInfo = OfflinePushInfo(
title: groupName,
desc: parseMessageSummary(msg),
ext: jsonEncode({"groupID": groupID, "title": groupName ?? ''}),
);
sendRes = await TencentImSDKPlugin.v2TIMManager.getMessageManager().sendMessage(
message: msg,
receiver: "",
groupID: groupID!,
// onSyncMsgID: (msgID) async {
// ID
// elem
// logger.w(msg.imageElem!.toLogString());
// controller.chatList.add(msg.imageElem);
// controller.scrollToBottom();
// },
priority: MessagePriorityEnum.V2TIM_PRIORITY_DEFAULT,
onlineUserOnly: false,
isExcludedFromUnreadCount: false,
isExcludedFromUnreadCount: isExcludedFromUnreadCount,
isExcludedFromLastMessage: false,
isSupportMessageExtension: false,
isExcludedFromContentModeration: false,
needReadReceipt: false,
offlinePushInfo: OfflinePushInfo(title: '群聊消息', desc: parseMessageSummary(msg)),
cloudCustomData: "",
offlinePushInfo: isPush ? offlinePushInfo : null,
cloudCustomData: cloudCustomData,
localCustomData: "",
);
}

View File

@ -12,6 +12,8 @@ import 'package:loopin/utils/notification_banner.dart';
import 'package:shirne_dialog/shirne_dialog.dart';
import 'package:tencent_cloud_chat_sdk/enum/V2TimAdvancedMsgListener.dart';
import 'package:tencent_cloud_chat_sdk/enum/message_elem_type.dart';
import 'package:tencent_cloud_chat_sdk/enum/receive_message_opt.dart';
import 'package:tencent_cloud_chat_sdk/models/v2_tim_group_info.dart';
import 'package:tencent_cloud_chat_sdk/models/v2_tim_message.dart';
import 'package:tencent_cloud_chat_sdk/models/v2_tim_message_receipt.dart';
import 'package:tencent_cloud_chat_sdk/tencent_im_sdk_plugin.dart';
@ -71,13 +73,14 @@ class ImMessageListenerService extends GetxService {
///
void _handleNewMessage(V2TimMessage message) async {
final id = message.sender ?? message.groupID ?? '';
final id = Utils.handleText(message.groupID, message.sender, ''); // groupID,senderID是必然存在的;
if (id.isEmpty) return;
final isGroup = message.groupID != null && message.groupID!.isNotEmpty;
final conversationID = isGroup ? 'group_${message.groupID}' : 'c2c_${message.userID}';
if (id.isEmpty) return;
///
if ((Get.currentRoute == '/chat' || Get.currentRoute == '/chatNoFriend' || Get.currentRoute == '/chatGroup') && Get.isRegistered<ChatDetailController>()) {
if (Get.isRegistered<ChatDetailController>()) {
final chatDetailController = Get.find<ChatDetailController>();
// (chatDetailController.id是通过chat_binding传入的userID,groupID)
if (chatDetailController.id == id) {
@ -88,6 +91,13 @@ class ImMessageListenerService extends GetxService {
}
return;
}
//tips类型的
if (message.elemType == MessageElemType.V2TIM_ELEM_TYPE_GROUP_TIPS) {
// tips
await ImService.instance.clearConversationUnreadCount(conversationID: conversationID);
return;
}
// 线
if (!LifecycleHandler.isInForeground) {
logger.i("App 当前在后台,收到来自 $id 的消息 ${message.msgID},暂不展示弹窗");
@ -97,9 +107,27 @@ class ImMessageListenerService extends GetxService {
//TODO cloudCustomDataconversation设置的opty属性是否开启了过滤customdata赋值
//TODO
///--------
// c2CReceiveMessageOpt
// recvOpt
if (isGroup) {
//
final res = await ImService.instance.getGroupsInfo(groupIDList: [message.groupID!]);
if (res.success && res.data != null) {
final V2TimGroupInfo groupData = res.data!.first.groupInfo!;
if (groupData.recvOpt == ReceiveMsgOptType.kTIMRecvMsgOpt_Not_Notify_Except_At) {
// 0=3=线线@
return;
}
} else {
// ;
return;
}
}
_debounceTimer?.cancel(); //
_debounceTimer = Timer(Duration(milliseconds: 1000), () {
NotificationBanner.show(message);
NotificationBanner.show(message, isGroup);
});
}
@ -156,8 +184,7 @@ class ImMessageListenerService extends GetxService {
onRecvMessageModified: (V2TimMessage message) {
logger.i("消息被修改: ${message.msgID}");
//
if ((Get.currentRoute == '/chat' || Get.currentRoute == '/chatNoFriend' || Get.currentRoute == '/chatGroup') &&
Get.isRegistered<ChatDetailController>()) {
if (Get.isRegistered<ChatDetailController>()) {
final controller = Get.find<ChatDetailController>();
final index = controller.chatList.indexWhere((m) => m.msgID == message.msgID);
if (index != -1) {

View File

@ -21,6 +21,7 @@ import 'package:tencent_cloud_chat_sdk/enum/group_application_type_enum.dart';
import 'package:tencent_cloud_chat_sdk/enum/group_member_filter_enum.dart';
import 'package:tencent_cloud_chat_sdk/enum/group_member_role_enum.dart';
import 'package:tencent_cloud_chat_sdk/enum/history_msg_get_type_enum.dart';
import 'package:tencent_cloud_chat_sdk/enum/receive_message_opt_enum.dart';
import 'package:tencent_cloud_chat_sdk/manager/v2_tim_group_manager.dart';
import 'package:tencent_cloud_chat_sdk/models/v2_tim_callback.dart';
import 'package:tencent_cloud_chat_sdk/models/v2_tim_conversation.dart';
@ -749,6 +750,30 @@ class ImService {
return ImResult.wrap(res);
}
///
Future<ImResult<void>> setGroupReceiveMessageOpt({
required String groupID,
required ReceiveMsgOptEnum opt,
}) async {
final res = await TIMMessageManager.instance.setGroupReceiveMessageOpt(
groupID: groupID,
opt: opt,
);
return ImResult.wrapNoData(res);
}
///
Future<ImResult<void>> setC2CReceiveMessageOpt({
required List<String> userIDList,
required ReceiveMsgOptEnum opt,
}) async {
final res = await TIMMessageManager.instance.setC2CReceiveMessageOpt(
userIDList: userIDList,
opt: opt,
);
return ImResult.wrapNoData(res);
}
///------------------------------------
///
Future<ImResult<String>> createGroup({
@ -839,20 +864,17 @@ class ImService {
/// - V2TIM_GROUP_MEMBER_FILTER_COMMON
/// [nextSeq] 0
/// [count] 100
/// [offset] Web
Future<ImResult<V2TimGroupMemberInfoResult>> getGroupMemberList({
required String groupID,
required GroupMemberFilterTypeEnum filter,
required String nextSeq,
int count = 20,
int offset = 0,
}) async {
final res = await V2TIMGroupManager().getGroupMemberList(
groupID: groupID,
filter: filter,
nextSeq: nextSeq,
count: count,
offset: offset,
);
return ImResult.wrap(res);
}
@ -893,7 +915,7 @@ class ImService {
return ImResult.wrapNoData(res);
}
/// (work群的入群方式)
/// (work群的入群方式)
///
/// [groupID] ID
/// [userList] userID

View File

@ -135,7 +135,7 @@ class PushService {
logger.w(router);
if (router == null || router != '') {
//
if (data['userID'] != null || data['userID'] != '') {
if (data['userID'] != null) {
logger.w('有userID');
//
final covRes = await ImService.instance.getConversation(conversationID: 'c2c_${data['userID']}');
@ -151,13 +151,15 @@ class PushService {
}
} else {
logger.w('没有userID');
//
final groupRes = await ImService.instance.getConversation(conversationID: 'group_${data['groupID']}');
Get.toNamed('/chatGroup', arguments: groupRes.data);
if (groupRes.success) {
final V2TimConversation groupConv = groupRes.data;
Get.toNamed('/chatGroup', arguments: groupConv);
}
}
} else {
//
// (ID只在特定业务场景才有退id对应的就是订单id)
Get.toNamed('/$router', arguments: data['id'] ?? '');
}
} catch (e) {

View File

@ -0,0 +1,27 @@
import 'package:flutter/material.dart';
class EmptyTip extends StatelessWidget {
final String text;
const EmptyTip({super.key, this.text = '暂无数据'});
@override
Widget build(BuildContext context) {
return Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Image.asset(
'assets/images/empty.png',
width: 100,
),
const SizedBox(height: 8),
Text(
text,
style: const TextStyle(color: Colors.grey, fontSize: 13),
),
],
),
);
}
}

View File

@ -0,0 +1,68 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
class MyConfirm extends StatelessWidget {
final String title;
final String content;
final String confirmText;
final String cancelText;
const MyConfirm({
super.key,
required this.title,
required this.content,
this.confirmText = "确认",
this.cancelText = "取消",
});
@override
Widget build(BuildContext context) {
return AlertDialog(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
title: Text(
title,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
content: Text(content),
actions: [
TextButton(
onPressed: () => Get.back(result: false), // false
child: Text(cancelText),
),
ElevatedButton(
style: ElevatedButton.styleFrom(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
onPressed: () => Get.back(result: true), // true
child: Text(confirmText),
),
],
);
}
}
class ConfirmDialog {
static Future<bool?> show({
required String title,
required String content,
String confirmText = "确认",
String cancelText = "取消",
}) {
return Get.dialog<bool>(
MyConfirm(
title: title,
content: content,
confirmText: confirmText,
cancelText: cancelText,
),
barrierDismissible: false, //
);
}
}

View File

@ -32,7 +32,7 @@ class NetworkOrAssetImage extends StatelessWidget {
}
//
return Image.asset(
placeholderAsset,
placeholderAsset.isEmpty ? 'assets/images/avatar/default.png' : placeholderAsset,
width: width,
height: height,
fit: fit,

View File

@ -297,8 +297,25 @@ class _ChatState extends State<Chat> with SingleTickerProviderStateMixin {
),
));
}
// tips
else if (item.cloudCustomData == 'tips') {
msgtpl.add(
Container(
margin: const EdgeInsets.only(bottom: 15.0),
width: double.infinity,
child: Text(
item.textElem!.text ?? '',
style: TextStyle(color: Colors.grey[600], fontSize: 12.0),
softWrap: true,
maxLines: 5,
overflow: TextOverflow.ellipsis,
textAlign: TextAlign.center,
),
),
);
}
// =1
else if (item.elemType == 1) {
else if (item.elemType == 1 && item.cloudCustomData != 'tips') {
msgtpl.add(
RenderChatItem(
data: item,

View File

@ -15,7 +15,10 @@ import 'package:loopin/components/image_viewer.dart';
import 'package:loopin/components/network_or_asset_image.dart';
import 'package:loopin/components/preview_video.dart';
import 'package:loopin/models/summary_type.dart';
import 'package:loopin/pages/groupChat/controller/group_detail_controller.dart';
import 'package:loopin/pages/groupChat/groupDetail.dart';
import 'package:loopin/utils/audio_player_service.dart';
import 'package:loopin/utils/parse_message_summary.dart';
import 'package:loopin/utils/snapshot.dart';
import 'package:loopin/utils/voice_service.dart';
import 'package:mime/mime.dart';
@ -25,7 +28,6 @@ import 'package:tencent_cloud_chat_sdk/models/v2_tim_message.dart';
import 'package:video_player/video_player.dart';
import 'package:wechat_assets_picker/wechat_assets_picker.dart';
import '../../styles/index.dart';
import '../../utils/index.dart';
import './components/redpacket.dart';
import './components/richtext.dart';
@ -105,6 +107,8 @@ class _ChatGroupState extends State<ChatGroup> with SingleTickerProviderStateMix
);
animTurns = Tween<double>(begin: 0, end: 3.1415926).animate(animController);
isInGroup();
cleanUnRead();
getUserId();
@ -233,8 +237,25 @@ class _ChatGroupState extends State<ChatGroup> with SingleTickerProviderStateMix
),
));
}
// tips
else if (item.cloudCustomData == 'tips') {
msgtpl.add(
Container(
margin: const EdgeInsets.only(bottom: 15.0),
width: double.infinity,
child: Text(
item.textElem!.text ?? '',
style: TextStyle(color: Colors.grey[600], fontSize: 12.0),
softWrap: true,
maxLines: 5,
overflow: TextOverflow.ellipsis,
textAlign: TextAlign.center,
),
),
);
}
// =1
else if (item.elemType == 1) {
else if (item.elemType == 1 && item.cloudCustomData != 'tips') {
msgtpl.add(
RenderChatItem(
data: item,
@ -758,6 +779,23 @@ class _ChatGroupState extends State<ChatGroup> with SingleTickerProviderStateMix
),
));
}
// tips
else if (item.elemType == 9) {
msgtpl.add(
Container(
margin: const EdgeInsets.only(bottom: 15.0),
width: double.infinity,
child: Text(
parseMessageSummary(item),
style: TextStyle(color: Colors.grey[600], fontSize: 12.0),
softWrap: true,
maxLines: 5,
overflow: TextOverflow.ellipsis,
textAlign: TextAlign.center,
),
),
);
}
}
return msgtpl;
}
@ -1017,7 +1055,7 @@ class _ChatGroupState extends State<ChatGroup> with SingleTickerProviderStateMix
late final ImResult res;
res = await IMMessage().sendMessage(
msg: message,
toUserID: arguments.groupID,
groupID: arguments.groupID,
);
if (res.success && res.data != null) {
@ -1028,9 +1066,9 @@ class _ChatGroupState extends State<ChatGroup> with SingleTickerProviderStateMix
// controller.chatList.addAll(messagesToInsert);
controller.scrollToBottom();
print('发送成功');
logger.w('发送成功');
} else {
print('消息发送失败: ${res.code} - ${res.desc}');
logger.w('消息发送失败: ${res.code} - ${res.desc}');
}
}
@ -1170,8 +1208,7 @@ class _ChatGroupState extends State<ChatGroup> with SingleTickerProviderStateMix
Future<void> showPicker(BuildContext context) async {
showModalBottomSheet(
context: context,
backgroundColor: Colors.white, //
backgroundColor: Colors.white,
builder: (context) {
return SafeArea(
child: Column(
@ -1532,7 +1569,7 @@ class _ChatGroupState extends State<ChatGroup> with SingleTickerProviderStateMix
context: context,
builder: (context) {
return RedPacket(
flag: false,
flag: true,
onSend: (date) {
sendHongbao(date);
});
@ -1540,10 +1577,23 @@ class _ChatGroupState extends State<ChatGroup> with SingleTickerProviderStateMix
);
}
Future<void> isInGroup() async {
final res = await ImService.instance.getJoinedGroupList();
if (res.success && res.data != null) {
controller.toolFlag.value = res.data!.any((group) => group.groupID == arguments.groupID);
} else {
logger.e('获取数据失败:${res.desc}');
//
controller.toolFlag.value = false;
}
logger.w('当前是否可以使用工具栏:${controller.toolFlag.value}');
}
/* ---------- { 其它功能模块 } ---------- */
@override
Widget build(BuildContext context) {
//editorFocusNode
return Stack(
fit: StackFit.expand,
children: [
@ -1581,113 +1631,26 @@ class _ChatGroupState extends State<ChatGroup> with SingleTickerProviderStateMix
)),
),
actions: [
IconButton(
icon: const Icon(
Icons.more_horiz,
color: Colors.white,
),
onPressed: () async {
final paddingTop = MediaQuery.of(Get.context!).padding.top;
final selected = await showMenu(
context: Get.context!,
position: RelativeRect.fromLTRB(
double.infinity,
kToolbarHeight + paddingTop - 12,
8,
double.infinity,
),
color: FStyle.primaryColor,
elevation: 8,
items: [
PopupMenuItem<String>(
value: 'remark',
child: Row(
children: [
Icon(Icons.edit, color: Colors.white, size: 18),
SizedBox(width: 8),
Text(
'设置备注',
style: TextStyle(color: Colors.white),
),
],
Obx(
() => controller.toolFlag.value
? IconButton(
icon: const Icon(
Icons.menu,
color: Colors.white,
),
),
PopupMenuItem<String>(
value: 'not',
child: Row(
children: [
Icon(Icons.do_not_disturb_on, color: Colors.white, size: 18),
SizedBox(width: 8),
Text(
'设为免打扰',
style: TextStyle(color: Colors.white),
),
],
),
),
PopupMenuItem<String>(
value: 'report',
child: Row(
children: [
Icon(Icons.report, color: Colors.white, size: 18),
SizedBox(width: 8),
Text(
'举报',
style: TextStyle(color: Colors.white),
),
],
),
),
PopupMenuItem<String>(
value: 'block',
child: Row(
children: [
Icon(Icons.block, color: Colors.white, size: 18),
SizedBox(width: 8),
Text(
'拉黑',
style: TextStyle(color: Colors.white),
),
],
),
),
PopupMenuItem<String>(
value: 'foucs',
child: Row(
children: [
Icon(Icons.person_remove_alt_1, color: Colors.white, size: 18),
SizedBox(width: 8),
Text(
'取消关注',
style: TextStyle(color: Colors.white),
),
],
),
),
],
);
if (selected != null) {
switch (selected) {
case 'remark':
print('点击了备注');
break;
case 'not':
print('点击了免打扰');
break;
case 'report':
print('点击了举报');
break;
case 'block':
print('点击了拉黑');
break;
case 'foucs':
print('点击了取关');
break;
}
}
},
onPressed: () async {
editorFocusNode.unfocus();
final ctl = Get.put(GroupDetailController(groupID: arguments.groupID ?? ''));
await ctl.init();
final groupName = await Get.to(() => Groupdetail());
if (groupName is String && groupName.isNotEmpty) {
setState(() {
arguments.showName = groupName;
});
}
},
)
: SizedBox(),
),
],
),
@ -1744,6 +1707,7 @@ class _ChatGroupState extends State<ChatGroup> with SingleTickerProviderStateMix
padding: const EdgeInsets.all(10.0),
child: Row(
children: [
//
InkWell(
child: Icon(
voiceBtnEnable ? Icons.keyboard_outlined : Icons.contactless_outlined,
@ -1751,16 +1715,22 @@ class _ChatGroupState extends State<ChatGroup> with SingleTickerProviderStateMix
size: 30.0,
),
onTap: () {
setState(() {
toolbarEnable = false;
if (voiceBtnEnable) {
voiceBtnEnable = false;
editorFocusNode.requestFocus();
} else {
voiceBtnEnable = true;
editorFocusNode.unfocus();
}
});
logger.w('点击了语音按钮');
if (controller.toolFlag.value) {
setState(() {
toolbarEnable = false;
if (voiceBtnEnable) {
voiceBtnEnable = false;
editorFocusNode.requestFocus();
// editorFocusNode.unfocus();
} else {
voiceBtnEnable = true;
editorFocusNode.unfocus();
}
});
} else {
logger.e('退出群聊无法使用');
}
},
),
const SizedBox(
@ -1778,21 +1748,25 @@ class _ChatGroupState extends State<ChatGroup> with SingleTickerProviderStateMix
//
Offstage(
offstage: voiceBtnEnable,
child: TextField(
decoration: const InputDecoration(
isDense: true,
hoverColor: Colors.transparent,
contentPadding: EdgeInsets.all(8.0),
border: OutlineInputBorder(borderSide: BorderSide.none),
child: Obx(
() => TextField(
enabled: controller.toolFlag.value, //true=
decoration: InputDecoration(
hintText: controller.toolFlag.value ? null : "你已退出群聊",
isDense: true,
hoverColor: Colors.transparent,
contentPadding: EdgeInsets.all(8.0),
border: OutlineInputBorder(borderSide: BorderSide.none),
),
style: const TextStyle(
fontSize: 16.0,
),
maxLines: null,
controller: editorController,
focusNode: editorFocusNode,
cursorColor: const Color(0xFF07C160),
onChanged: (value) {},
),
style: const TextStyle(
fontSize: 16.0,
),
maxLines: null,
controller: editorController,
focusNode: editorFocusNode,
cursorColor: const Color(0xFF07C160),
onChanged: (value) {},
),
),
//
@ -1870,6 +1844,7 @@ class _ChatGroupState extends State<ChatGroup> with SingleTickerProviderStateMix
const SizedBox(
width: 10.0,
),
//
InkWell(
child: const Icon(
Icons.add_reaction_rounded,
@ -1877,12 +1852,17 @@ class _ChatGroupState extends State<ChatGroup> with SingleTickerProviderStateMix
size: 30.0,
),
onTap: () {
handleEmojChooseState(0);
if (controller.toolFlag.value) {
handleEmojChooseState(0);
} else {
logger.w('退出群聊禁止使用');
}
},
),
const SizedBox(
width: 8.0,
),
// +
InkWell(
child: const Icon(
Icons.add,
@ -1890,12 +1870,17 @@ class _ChatGroupState extends State<ChatGroup> with SingleTickerProviderStateMix
size: 30.0,
),
onTap: () {
handleEmojChooseState(1);
if (controller.toolFlag.value) {
handleEmojChooseState(1);
} else {
logger.w('退出群聊禁止使用');
}
},
),
const SizedBox(
width: 8.0,
),
//
InkWell(
child: Container(
height: 25.0,
@ -1911,7 +1896,11 @@ class _ChatGroupState extends State<ChatGroup> with SingleTickerProviderStateMix
),
),
onTap: () {
handleSubmit();
if (controller.toolFlag.value) {
handleSubmit();
} else {
logger.w('退出群聊禁止使用');
}
},
),
],
@ -2150,10 +2139,10 @@ class _ChatGroupState extends State<ChatGroup> with SingleTickerProviderStateMix
child: Visibility(
visible: !voiceToTransfer,
child: Align(
child: Text(
voiceTypeMap[voiceType],
style: const TextStyle(color: Colors.white70),
),
child: Obx(() => Text(
controller.toolFlag.value ? voiceTypeMap[voiceType] : '无法在已退出的群聊发送消息',
style: const TextStyle(color: Colors.white70),
)),
),
),
),

View File

@ -1,6 +1,8 @@
///
library;
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:loopin/IM/controller/chat_controller.dart';
@ -8,10 +10,13 @@ import 'package:loopin/IM/controller/chat_detail_controller.dart';
import 'package:loopin/IM/im_message.dart';
import 'package:loopin/IM/im_result.dart';
import 'package:loopin/IM/im_service.dart';
import 'package:loopin/components/image_viewer.dart';
import 'package:loopin/components/network_or_asset_image.dart';
import 'package:loopin/components/preview_video.dart';
import 'package:loopin/models/conversation_type.dart';
import 'package:loopin/models/summary_type.dart';
import 'package:loopin/pages/chat/notify_controller/notify_no_friend_controller.dart';
import 'package:loopin/utils/audio_player_service.dart';
import 'package:loopin/utils/snapshot.dart';
import 'package:shirne_dialog/shirne_dialog.dart';
import 'package:tencent_cloud_chat_sdk/models/v2_tim_conversation.dart';
@ -294,8 +299,25 @@ class _ChatNoFriendState extends State<ChatNoFriend> with SingleTickerProviderSt
),
));
}
// tips
else if (item.cloudCustomData == 'tips') {
msgtpl.add(
Container(
margin: const EdgeInsets.only(bottom: 15.0),
width: double.infinity,
child: Text(
item.textElem!.text ?? '',
style: TextStyle(color: Colors.grey[600], fontSize: 12.0),
softWrap: true,
maxLines: 5,
overflow: TextOverflow.ellipsis,
textAlign: TextAlign.center,
),
),
);
}
// =1
else if (item.elemType == 1) {
else if (item.elemType == 1 && item.cloudCustomData != 'tips') {
msgtpl.add(
RenderChatItem(
data: item,
@ -311,9 +333,9 @@ class _ChatNoFriendState extends State<ChatNoFriend> with SingleTickerProviderSt
padding: const EdgeInsets.all(10.0),
child: RichTextUtil.getRichText(item.textElem?.text ?? '', color: !(item.isSelf ?? false) ? Colors.black : Colors.white), // emoj//
),
// onLongPress: () {
// contextMenuDialog();
// },
onLongPress: () {
contextMenuDialog();
},
),
),
),
@ -350,6 +372,13 @@ class _ChatNoFriendState extends State<ChatNoFriend> with SingleTickerProviderSt
data: item,
child: Ink(
child: InkWell(
onTap: () {
//
Get.to(() => ImageViewer(
images: [imagePaths.first],
index: 0,
));
},
overlayColor: WidgetStateProperty.all(Colors.transparent),
child: ClipRRect(
borderRadius: BorderRadius.circular(10.0),
@ -407,17 +436,9 @@ class _ChatNoFriendState extends State<ChatNoFriend> with SingleTickerProviderSt
children: [
ClipRRect(
borderRadius: BorderRadius.circular(10.0),
child: Image.network(
fit: BoxFit.cover,
item.videoElem?.snapshotUrl ?? '',
errorBuilder: (context, error, stackTrace) {
return Image.asset(
'assets/images/pic1.jpg',
height: 60.0,
width: 60.0,
fit: BoxFit.cover,
);
},
child: NetworkOrAssetImage(
imageUrl: item.videoElem?.snapshotUrl ?? '',
width: 120,
),
),
const Align(
@ -431,9 +452,6 @@ class _ChatNoFriendState extends State<ChatNoFriend> with SingleTickerProviderSt
],
),
),
// onTap: () {
// MyDialog.toast('该功能暂未支持~', icon: Icon(Icons.warning), style: ToastStyle(backgroundColor: Colors.red.withAlpha(200)));
// },
onTap: () {
showGeneralDialog(
context: context,
@ -463,6 +481,10 @@ class _ChatNoFriendState extends State<ChatNoFriend> with SingleTickerProviderSt
}
// =4
else if (item.elemType == 4) {
final durationMs = item.soundElem?.duration ?? 0;
final durationSeconds = (durationMs / 1000).round();
final maxWidth = (durationSeconds / 60 * 230).clamp(80.0, 230.0);
List<Widget> audiobody = [
Ink(
decoration: BoxDecoration(
@ -475,8 +497,8 @@ class _ChatNoFriendState extends State<ChatNoFriend> with SingleTickerProviderSt
child: Container(
padding: const EdgeInsets.all(10.0),
constraints: BoxConstraints(
// maxWidth: 120.0,
maxWidth: (item.soundElem?.duration)! / 60 * 230,
maxWidth: maxWidth,
// maxWidth: (item.soundElem!.duration! / 1000) / 60 * 230,
),
child: Row(
mainAxisAlignment: !(item.isSelf ?? false) ? MainAxisAlignment.start : MainAxisAlignment.end,
@ -486,10 +508,10 @@ class _ChatNoFriendState extends State<ChatNoFriend> with SingleTickerProviderSt
const SizedBox(
width: 5.0,
),
Text('${item.soundElem?.duration}'),
Text('$durationSeconds"'),
]
: [
Text('${item.soundElem?.duration}'),
Text('$durationSeconds"'),
const SizedBox(
width: 5.0,
),
@ -498,7 +520,15 @@ class _ChatNoFriendState extends State<ChatNoFriend> with SingleTickerProviderSt
),
),
onTap: () {
MyDialog.toast('该功能暂未支持~', icon: Icon(Icons.warning), style: ToastStyle(backgroundColor: Colors.red.withAlpha(200)));
final locUrl = item.soundElem?.path ?? '';
final netUrl = item.soundElem?.url ?? '';
if (locUrl.isNotEmpty) {
AudioPlayerService().playNetwork(locUrl);
} else if (netUrl.isNotEmpty) {
AudioPlayerService().playLocal(netUrl);
} else {
MyDialog.toast('音频文件已过期', icon: Icon(Icons.warning), style: ToastStyle(backgroundColor: Colors.red.withAlpha(200)));
}
},
onLongPress: () {
contextMenuDialog();
@ -508,7 +538,8 @@ class _ChatNoFriendState extends State<ChatNoFriend> with SingleTickerProviderSt
const SizedBox(
width: 5.0,
),
FStyle.badge(0, isdot: true),
// FStyle.badge(0, isdot: true),
];
if (item.isSelf ?? false) {
@ -525,8 +556,177 @@ class _ChatNoFriendState extends State<ChatNoFriend> with SingleTickerProviderSt
children: audiobody,
)));
}
//
else if (item.elemType == 2 && item.cloudCustomData == SummaryType.shareTuangou) {
// final makeJson = jsonEncode({
// "price": shopObj['price'],
// "title": shopObj['name'],
// "url": shopObj['pic'],
// "sell": Utils.graceNumber(int.parse(shopObj['sales'] ?? '0')),
// "goodsId": shopObj['id'],
// "userID": Get.find<ImUserInfoController>().userID.value,
// });
final obj = jsonDecode(item.customElem!.data!);
logger.e(obj);
final goodsId = obj['goodsId'];
final userID = obj['userID'];
final url = obj['url'];
final title = obj['title'];
final price = obj['price'];
final sell = Utils.graceNumber(int.tryParse(obj['sell'])!);
msgtpl.add(RenderChatItem(
data: item,
child: GestureDetector(
onTap: () {
// ID
Get.toNamed('/goods', arguments: {'goodsId': goodsId, 'userID': userID});
},
child: Container(
width: 160,
clipBehavior: Clip.antiAlias,
decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(15.0), boxShadow: [
BoxShadow(
color: Colors.black.withAlpha(5),
offset: Offset(0.0, 1.0),
blurRadius: 1.0,
spreadRadius: 0.0,
),
]),
child: Column(
children: [
NetworkOrAssetImage(
imageUrl: url,
width: 160.0,
),
Container(
padding: EdgeInsets.symmetric(horizontal: 10.0, vertical: 5.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
spacing: 5.0,
children: [
Text(
'$title',
style: TextStyle(fontSize: 14.0, height: 1.2),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Flexible(
child: Text.rich(
overflow: TextOverflow.ellipsis,
maxLines: 1,
TextSpan(style: TextStyle(color: Colors.red, fontSize: 12.0, fontWeight: FontWeight.w700, fontFamily: 'Arial'), children: [
TextSpan(text: '¥'),
TextSpan(
text: '$price',
style: TextStyle(
fontSize: 14.0,
)),
]),
),
),
SizedBox(
width: 5,
),
Text(
'已售$sell件',
style: TextStyle(color: Colors.grey, fontSize: 10.0),
),
],
),
],
),
)
],
),
),
),
));
}
//
else if (item.elemType == 2 && item.cloudCustomData == SummaryType.shareVideo) {
/// {imgUrl,videoUrl,width,height}
final obj = jsonDecode(item.customElem!.data!);
logger.e(obj);
final videoId = obj['videoId'];
final videoUrl = obj['videoUrl'];
final imgUrl = obj['imgUrl'];
final width = obj['width'] as num;
final height = obj['height'] as num;
final isHorizontal = width > height;
msgtpl.add(RenderChatItem(
data: item,
child: Ink(
child: InkWell(
overlayColor: WidgetStateProperty.all(Colors.transparent),
child: SizedBox(
width: 120.0,
child: Stack(
alignment: Alignment.center,
children: [
ClipRRect(
borderRadius: BorderRadius.circular(10.0),
child: Container(
width: 120,
height: 240,
color: Colors.black,
child: NetworkOrAssetImage(
imageUrl: imgUrl,
fit: isHorizontal ? BoxFit.contain : BoxFit.cover,
placeholderAsset: 'assets/images/bk.jpg',
),
),
),
const Align(
alignment: Alignment.center,
child: Icon(
Icons.play_circle,
color: Colors.white,
size: 30.0,
),
),
],
),
),
onTap: () {
Get.toNamed('/videoDetail', arguments: {'videoId': videoId});
// showGeneralDialog(
// context: context,
// barrierColor: Colors.black.withAlpha((1.0 * 255).round()),
// pageBuilder: (_, __, ___) {
// return SafeArea(
// bottom: true,
// child: Padding(
// padding: const EdgeInsets.only(bottom: 4),
// child: PreviewVideo(
// videoUrl: videoUrl,
// width: width.toDouble(),
// height: height.toDouble(),
// ),
// ),
// );
// },
// transitionBuilder: (_, anim, __, child) {
// return FadeTransition(opacity: anim, child: child);
// },
// transitionDuration: const Duration(milliseconds: 200),
// );
},
// onLongPress: () {
// contextMenuDialog();
// },
),
),
));
}
// ==2
else if (item.elemType == 2 && item.cloudCustomData == 'hongbao') {
else if (item.elemType == 2 && item.cloudCustomData == SummaryType.hongbao) {
final obj = jsonDecode(item.customElem!.data!);
final open = obj['open'] ?? false;
final remark = obj['remark'];
// final maxNum = obj['maxNum'];
msgtpl.add(RenderChatItem(
data: item,
child: Ink(
@ -545,16 +745,26 @@ class _ChatNoFriendState extends State<ChatNoFriend> with SingleTickerProviderSt
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
width: 210.0,
padding: const EdgeInsets.all(10.0),
child: Row(
spacing: 10.0,
children: <Widget>[
Image.asset(
'assets/images/hbico.png',
width: 32.0,
fit: BoxFit.contain,
open
? Icon(Icons.check_circle, size: 32.0, color: Colors.white70)
: Image.asset(
'assets/images/hbico.png',
width: 32.0,
fit: BoxFit.contain,
),
const SizedBox(width: 10),
Expanded(
child: Text(
'$remark',
style: const TextStyle(color: Colors.white, fontSize: 14.0),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
Text(item.customElem?.data ?? '', style: const TextStyle(color: Colors.white, fontSize: 14.0)),
],
),
),
@ -564,7 +774,7 @@ class _ChatNoFriendState extends State<ChatNoFriend> with SingleTickerProviderSt
width: double.infinity,
decoration: const BoxDecoration(border: Border(top: BorderSide(color: Colors.white30, width: .5))),
child: const Text(
'拼手气红包',
'红包',
style: TextStyle(color: Colors.white70, fontSize: 11.0),
),
),
@ -639,10 +849,6 @@ class _ChatNoFriendState extends State<ChatNoFriend> with SingleTickerProviderSt
));
}
}
// msgtpl.insert(
// 0,
// SizedBox.shrink(),
// );
return msgtpl;
}
@ -1517,6 +1723,15 @@ class _ChatNoFriendState extends State<ChatNoFriend> with SingleTickerProviderSt
ctl.removeNoFriend(conversationID: arguments.value.conversationID);
ctl.updateNoFriendMenu();
}
// chat地址
Get.offAllNamed(
'/chat',
arguments: arguments.value,
predicate: (route) {
// `/`
return route.settings.name == '/';
},
);
}
},
child: const Text('回关', style: TextStyle(color: Colors.white)),

View File

@ -15,6 +15,7 @@ import 'package:loopin/utils/index.dart';
import 'package:loopin/utils/scan_code_type.dart'; //
import 'package:loopin/utils/parse_message_summary.dart';
import 'package:shirne_dialog/shirne_dialog.dart';
import 'package:tencent_cloud_chat_sdk/enum/receive_message_opt.dart';
import '../../behavior/custom_scroll_behavior.dart';
import '../../styles/index.dart';
@ -394,10 +395,14 @@ class ChatPageState extends State<ChatPage> {
itemBuilder: (context, index) {
// logger.w(chatList[index].conversation.conversationGroupList);
// logger.w(chatList[index].isCustomAdmin);
// logger.w(chatList[index].conversation.recvOpt);
final bool quiet = [ReceiveMsgOptType.kTIMRecvMsgOpt_Not_Notify_Except_At].contains(chatList[index].conversation.recvOpt ?? 0)
? true
: false; //
final isNoFriend = chatList[index].conversation.conversationGroupList?.contains(ConversationType.noFriend.name) ?? false;
final isAdmin =
chatList[index].isCustomAdmin != null && (chatList[index].isCustomAdmin?.isNotEmpty ?? false) && chatList[index].isCustomAdmin != '0';
final placeholderAsset = chatList[index].conversation.type == 2 ? 'assets/images/group.png' : 'assets/images/avatar/default.png';
// logger.e(chatList[index].isCustomAdmin);
return Ink(
// color: chatList[index]['topMost'] == null ? Colors.white : Colors.grey[100], //
@ -415,6 +420,7 @@ class ChatPageState extends State<ChatPage> {
imageUrl: chatList[index].faceUrl,
width: 50,
height: 50,
placeholderAsset: placeholderAsset,
),
),
@ -423,17 +429,24 @@ class ChatPageState extends State<ChatPage> {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
//
Text(
chatList[index].conversation.showName ?? '未知',
maxLines: 1,
softWrap: false,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontSize: (isAdmin || isNoFriend) ? 20 : 16,
fontWeight: (isAdmin || isNoFriend) ? FontWeight.bold : FontWeight.normal),
),
const SizedBox(height: 2.0),
//
Text(
chatList[index].conversation.lastMessage != null ? parseMessageSummary(chatList[index].conversation.lastMessage!) : '',
style: const TextStyle(color: Colors.grey, fontSize: 13.0),
overflow: TextOverflow.ellipsis,
maxLines: 1,
softWrap: false,
),
],
),
@ -456,9 +469,22 @@ class ChatPageState extends State<ChatPage> {
),
const SizedBox(height: 5.0),
//
// class ReceiveMsgOptType {
// 线线 APNs
// static const int kTIMRecvMsgOpt_Receive = 0;
// 线
// static const int kTIMRecvMsgOpt_Not_Receive = 1;
// 线线
// static const int kTIMRecvMsgOpt_Not_Notify = 2;
// 线线 at
// static const int kTIMRecvMsgOpt_Not_Notify_Except_At = 3;
// 线线@
// static const int kTIMRecvMsgOpt_Not_Receive_Except_At = 4;
// }
// 03
Visibility(
visible: (chatList[index].conversation.unreadCount ?? 0) > 0,
child: FStyle.badge(chatList[index].conversation.unreadCount ?? 0),
child: FStyle.badge(chatList[index].conversation.unreadCount ?? 0, color: quiet ? Colors.grey : Colors.red),
),
],
),
@ -482,6 +508,9 @@ class ChatPageState extends State<ChatPage> {
//
logger.e(chatList[index].conversation.conversationGroupList);
Get.toNamed('/noFriend');
} else if (chatList[index].conversation.type == 2) {
// type=01=2=
Get.toNamed('/chatGroup', arguments: chatList[index].conversation);
} else {
// id查询会话详情
Get.toNamed('/chat', arguments: chatList[index].conversation);

View File

@ -0,0 +1,264 @@
import 'package:easy_refresh/easy_refresh.dart';
import 'package:flutter/material.dart';
import 'package:loopin/IM/im_service.dart';
import 'package:loopin/styles/index.dart';
import 'package:tencent_cloud_chat_sdk/models/v2_tim_group_member_full_info.dart';
import 'package:tencent_cloud_chat_sdk/models/v2_tim_user_full_info.dart';
class InviteActionSheet extends StatefulWidget {
final String groupID;
final Function(List<String> selected) onAction;
final String title;
final String actionLabel;
final bool showButton;
const InviteActionSheet({
super.key,
required this.groupID,
required this.onAction,
this.title = "",
this.actionLabel = "确认",
this.showButton = true,
});
@override
State<InviteActionSheet> createState() => _MemberActionSheetState();
}
class _MemberActionSheetState extends State<InviteActionSheet> {
String _query = "";
final Set<String> _selectedIDs = {}; // userID
late List<V2TimUserFullInfo> members = [];
String nextSeq = '';
bool hasMore = false;
bool loading = false;
//
@override
void initState() {
super.initState();
getMemberData();
}
//
Future<void> getMemberData({bool reset = false}) async {
if (loading) return;
loading = true;
final res = await ImService.instance.getMutualFollowersList(
nextCursor: nextSeq,
);
if (res.success && res.data != null) {
final userInfoList = res.data!.userFullInfoList ?? [];
final isFinished = res.data!.nextCursor == null || res.data!.nextCursor!.isEmpty;
logger.w('获取成功:${res.data!.nextCursor},是否还有更多:$isFinished');
if (isFinished) {
hasMore = false;
nextSeq = '';
} else {
nextSeq = res.data!.nextCursor ?? '';
hasMore = nextSeq.isNotEmpty ? true : false;
}
final ids = userInfoList.map((item) => item.userID).whereType<String>().toList();
if (ids.isNotEmpty) {
getGroupMemberData(userIDs: ids, userList: userInfoList);
}
} else {
logger.e('获取数据失败:${res.desc}');
}
}
//
Future<void> getGroupMemberData({required List<String> userIDs, required List<V2TimUserFullInfo> userList}) async {
final res = await ImService.instance.getGroupMembersInfo(
groupID: widget.groupID,
memberList: userIDs,
);
if (res.success && res.data != null) {
final List<V2TimGroupMemberFullInfo> groupMemberList = res.data ?? [];
// userID
final inGroupIds = groupMemberList.map((m) => m.userID).whereType<String>().where((id) => id.isNotEmpty).toSet();
//
final notInGroupUsers = userList.where((user) => user.userID != null && !inGroupIds.contains(user.userID)).toList();
setState(() {
members.addAll(notInGroupUsers);
});
}
setState(() {
loading = false;
});
}
String handleText(String? text, String defaultValue) {
if (text == null || text.trim().isEmpty) return defaultValue;
return text;
}
@override
Widget build(BuildContext context) {
//
final filteredMembers = members.where((m) {
final name = m.nickName ?? '';
return name.contains(_query);
}).toList();
return SafeArea(
child: GestureDetector(
behavior: HitTestBehavior.translucent, //
onTap: () {
FocusScope.of(context).unfocus();
},
child: Scaffold(
backgroundColor: Colors.white,
appBar: AppBar(
centerTitle: true,
forceMaterialTransparency: true,
bottom: PreferredSize(
preferredSize: const Size.fromHeight(1.0),
child: Container(
color: Colors.grey[300],
height: 1.0,
),
),
title: Text(
widget.title,
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
leading: IconButton(
icon: const Icon(Icons.arrow_back),
onPressed: () {
Navigator.pop(context);
},
),
),
body: Column(
children: [
SizedBox(height: 10),
//
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: TextField(
decoration: InputDecoration(
prefixIcon: const Icon(Icons.search),
hintText: "搜索",
contentPadding: const EdgeInsets.symmetric(vertical: 8),
border: OutlineInputBorder(borderRadius: BorderRadius.circular(8)),
),
onChanged: (value) {
setState(() {
_query = value.trim();
});
},
),
),
const SizedBox(height: 8),
//
Expanded(
child: EasyRefresh(
footer: ClassicFooter(
dragText: '加载更多',
armedText: '释放加载',
readyText: '加载中...',
processingText: '加载中...',
processedText: hasMore ? '加载完成' : '没有更多了~',
failedText: '加载失败,请重试',
messageText: '最后更新于 %T',
),
onLoad: () async {
//
if (hasMore) {
await getMemberData();
}
},
child: ListView.builder(
itemCount: filteredMembers.length,
itemBuilder: (context, index) {
final m = filteredMembers[index];
final id = m.userID;
final uname = handleText(m.nickName, '');
final nickname = handleText(m.nickName, '未知昵称');
final showName = uname.isEmpty ? nickname : uname;
return InkWell(
onTap: () {
setState(() {
if (_selectedIDs.contains(id)) {
_selectedIDs.remove(id);
} else if (id != null && id.isNotEmpty) {
_selectedIDs.add(id);
}
});
},
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 16),
child: Row(
children: [
//
CircleAvatar(
radius: 20,
backgroundImage: m.faceUrl != null ? NetworkImage(m.faceUrl!) : null,
child: m.faceUrl == null ? const Icon(Icons.person) : null,
),
const SizedBox(width: 12),
//
Expanded(
child: Text(
showName,
style: const TextStyle(fontSize: 14),
overflow: TextOverflow.ellipsis,
),
),
//
Container(
width: 24,
height: 24,
alignment: Alignment.center,
decoration: BoxDecoration(
shape: BoxShape.circle,
border: Border.all(color: _selectedIDs.contains(id) ? FStyle.primaryColor : Colors.grey),
color: _selectedIDs.contains(id) ? FStyle.primaryColor : Colors.transparent,
),
child: _selectedIDs.contains(id) ? const Icon(Icons.check, size: 16, color: Colors.white) : null,
),
],
),
),
);
},
),
),
),
//
if (widget.showButton)
Padding(
padding: const EdgeInsets.all(16),
child: ElevatedButton(
style: ElevatedButton.styleFrom(
minimumSize: const Size(double.infinity, 48),
backgroundColor: FStyle.primaryColor,
),
onPressed: _selectedIDs.isEmpty
? null
: () {
Navigator.pop(context);
widget.onAction(_selectedIDs.toList());
},
child: Text(
"${widget.actionLabel}${_selectedIDs.length}",
style: TextStyle(
color: _selectedIDs.isNotEmpty ? Colors.white : Colors.black,
),
),
),
),
],
),
),
),
);
}
}

View File

@ -0,0 +1,302 @@
import 'package:easy_refresh/easy_refresh.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:loopin/IM/im_service.dart';
import 'package:loopin/pages/groupChat/controller/group_detail_controller.dart';
import 'package:loopin/styles/index.dart';
import 'package:tencent_cloud_chat_sdk/enum/group_member_filter_enum.dart';
import 'package:tencent_cloud_chat_sdk/models/v2_tim_group_member_full_info.dart';
import 'package:tencent_cloud_chat_sdk/models/v2_tim_group_member_search_param.dart';
class MemberActionSheet extends StatefulWidget {
final String groupID;
final Function(List<String> selected) onAction;
final String title;
final String actionLabel;
final bool showButton;
final bool showSelf;
const MemberActionSheet({
super.key,
required this.groupID,
required this.onAction,
this.title = "",
this.actionLabel = "确认",
this.showButton = true,
this.showSelf = false,
});
@override
State<MemberActionSheet> createState() => _MemberActionSheetState();
}
class _MemberActionSheetState extends State<MemberActionSheet> {
String _query = "";
final Set<String> _selectedIDs = {}; // userID
late List<V2TimGroupMemberFullInfo> members = [];
late List<V2TimGroupMemberFullInfo> searchMemberList = [];
String nextSeq = '0';
String searchCursor = '';
bool hasMore = false; //
bool loading = false;
bool isFinished = false; //
bool loading2 = false; //
//
@override
void initState() {
super.initState();
if (widget.showSelf) {
final self = Get.find<GroupDetailController>().selfInfo.value!;
members.insert(0, self);
}
getMemberData();
}
//
Future<void> getMemberData() async {
if (loading) return;
loading = true;
final res = await ImService.instance.getGroupMemberList(
groupID: widget.groupID,
filter: GroupMemberFilterTypeEnum.V2TIM_GROUP_MEMBER_FILTER_COMMON,
nextSeq: nextSeq,
count: 100,
);
if (res.success && res.data != null) {
logger.e(res.data!.nextSeq);
nextSeq = res.data!.nextSeq ?? '0';
final mem = res.data!.memberInfoList ?? [];
setState(() {
members.addAll(mem);
loading = false;
});
}
}
///
Future<void> searchMember({bool loadMore = false}) async {
if (loading2) return;
if (_query.isEmpty) {
setState(() {
_query = '';
});
return;
}
loading2 = true;
final param = V2TimGroupMemberSearchParam(
keywordList: [_query],
groupIDList: [widget.groupID],
isSearchMemberUserID: true,
isSearchMemberNickName: true,
isSearchMemberRemark: true,
isSearchMemberNameCard: true,
keywordListMatchType: V2TimGroupMemberSearchParam.V2TIM_KEYWORD_LIST_MATCH_TYPE_OR,
searchCount: 100,
searchCursor: searchCursor,
);
final res = await ImService.instance.searchGroupMembers(param: param);
if (res.success && res.data != null) {
isFinished = res.data!.isFinished ?? true;
final searchCursor = res.data!.nextCursor ?? '';
final data = res.data!.groupMemberSearchResultItems;
//
if (data != null && data[widget.groupID] != null) {
List<V2TimGroupMemberFullInfo> list = (data[widget.groupID] as List<dynamic>).cast<V2TimGroupMemberFullInfo>();
if (!widget.showSelf) {
//
final self = Get.find<GroupDetailController>().selfInfo.value!;
list = list.where((item) => item.userID != self.userID).toList();
}
if (loadMore) {
searchMemberList.addAll(list);
} else {
searchMemberList = list;
}
}
logger.w(hasMore);
logger.w(searchCursor);
logger.w(data);
}
setState(() {
loading2 = false;
});
}
String handleText(String? text, String defaultValue) {
if (text == null || text.trim().isEmpty) return defaultValue;
return text;
}
@override
Widget build(BuildContext context) {
List filteredMembers;
if (_query.isEmpty) {
filteredMembers = members;
} else {
filteredMembers = searchMemberList;
}
return SafeArea(
child: GestureDetector(
behavior: HitTestBehavior.translucent, //
onTap: () {
FocusScope.of(context).unfocus();
},
child: Scaffold(
backgroundColor: Colors.white,
appBar: AppBar(
centerTitle: true,
forceMaterialTransparency: true,
bottom: PreferredSize(
preferredSize: const Size.fromHeight(1.0),
child: Container(
color: Colors.grey[300],
height: 1.0,
),
),
title: Text(
widget.title,
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
leading: IconButton(
icon: const Icon(Icons.arrow_back),
onPressed: () {
Navigator.pop(context);
},
),
),
body: Column(
children: [
SizedBox(height: 10),
//
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: TextField(
decoration: InputDecoration(
prefixIcon: const Icon(Icons.search),
hintText: "搜索成员",
contentPadding: const EdgeInsets.symmetric(vertical: 8),
border: OutlineInputBorder(borderRadius: BorderRadius.circular(8)),
),
onChanged: (value) {
setState(() {
_query = value.trim();
searchMember();
});
},
),
),
const SizedBox(height: 8),
//
Expanded(
child: EasyRefresh(
footer: ClassicFooter(
dragText: '加载更多',
armedText: '释放加载',
readyText: '加载中...',
processingText: '加载中...',
processedText: hasMore ? '加载完成' : '没有更多了~',
failedText: '加载失败,请重试',
messageText: '最后更新于 %T',
),
onLoad: () async {
//
if (hasMore) {
if (_query.isNotEmpty) {
await searchMember(loadMore: true);
} else {
await getMemberData();
}
}
},
child: ListView.builder(
itemCount: filteredMembers.length,
itemBuilder: (context, index) {
final m = filteredMembers[index];
final id = m.userID;
final uname = handleText(m.nameCard, '');
final nickname = handleText(m.nickName, '未知昵称');
final showName = uname.isEmpty ? nickname : uname;
return InkWell(
onTap: () {
setState(() {
if (_selectedIDs.contains(id)) {
_selectedIDs.remove(id);
} else {
_selectedIDs.add(id);
}
});
},
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 16),
child: Row(
children: [
//
CircleAvatar(
radius: 20,
backgroundImage: m.faceUrl != null ? NetworkImage(m.faceUrl!) : null,
child: m.faceUrl == null ? const Icon(Icons.person) : null,
),
const SizedBox(width: 12),
//
Expanded(
child: Text(
showName,
style: const TextStyle(fontSize: 14),
overflow: TextOverflow.ellipsis,
),
),
if (widget.showButton)
//
Container(
width: 24,
height: 24,
alignment: Alignment.center,
decoration: BoxDecoration(
shape: BoxShape.circle,
border: Border.all(color: _selectedIDs.contains(id) ? FStyle.primaryColor : Colors.grey),
color: _selectedIDs.contains(id) ? FStyle.primaryColor : Colors.transparent,
),
child: _selectedIDs.contains(id) ? const Icon(Icons.check, size: 16, color: Colors.white) : null,
),
],
),
),
);
},
),
),
),
//
if (widget.showButton)
Padding(
padding: const EdgeInsets.all(16),
child: ElevatedButton(
style: ElevatedButton.styleFrom(
minimumSize: const Size(double.infinity, 48),
backgroundColor: FStyle.primaryColor,
),
onPressed: _selectedIDs.isEmpty
? null
: () {
Navigator.pop(context);
widget.onAction(_selectedIDs.toList());
},
child: Text(
"${widget.actionLabel}${_selectedIDs.length}",
style: TextStyle(
color: _selectedIDs.isNotEmpty ? Colors.white : Colors.black,
),
),
),
),
],
),
),
),
);
}
}

View File

@ -0,0 +1,74 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
class SetGroupInfoPage extends StatelessWidget {
final String appBarTitle;
///
final String fieldLabel;
///
final int maxLines;
final int maxLength;
final String? initialValue; //
final ValueChanged<String> onSubmit; //
const SetGroupInfoPage({
super.key,
required this.appBarTitle,
required this.fieldLabel,
this.maxLines = 1,
this.maxLength = 100,
this.initialValue,
required this.onSubmit,
});
@override
Widget build(BuildContext context) {
final controller = TextEditingController(text: initialValue);
return Scaffold(
appBar: AppBar(
title: Text(
appBarTitle,
style: TextStyle(fontSize: 18),
),
actions: [
TextButton(
onPressed: () {
if (controller.text.trim().isEmpty) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('请输入内容')),
);
return;
}
onSubmit(controller.text.trim());
Get.back();
},
child: const Text("保存", style: TextStyle(color: Colors.black)),
),
],
),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(fieldLabel, style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
const SizedBox(height: 10),
TextField(
controller: controller,
maxLines: maxLines,
maxLength: maxLength,
decoration: InputDecoration(
border: OutlineInputBorder(borderRadius: BorderRadius.circular(8)),
hintText: "请输入 $fieldLabel",
// counterText: "", //
),
),
],
),
),
);
}
}

View File

@ -0,0 +1,148 @@
import 'package:get/get.dart';
import 'package:loopin/IM/controller/im_user_info_controller.dart';
import 'package:loopin/IM/im_service.dart';
import 'package:loopin/components/my_toast.dart';
import 'package:tencent_cloud_chat_sdk/enum/group_member_filter_enum.dart';
import 'package:tencent_cloud_chat_sdk/enum/group_type.dart';
import 'package:tencent_cloud_chat_sdk/models/v2_tim_group_info.dart';
import 'package:tencent_cloud_chat_sdk/models/v2_tim_group_member_full_info.dart';
class GroupDetailController extends GetxController {
// ID
final String groupID;
GroupDetailController({required this.groupID});
//
Rxn<V2TimGroupInfo> info = Rxn<V2TimGroupInfo>();
//
RxList<V2TimGroupMemberFullInfo> memberList = <V2TimGroupMemberFullInfo>[].obs;
//
Rxn<V2TimGroupMemberFullInfo> selfInfo = Rxn<V2TimGroupMemberFullInfo>();
//
RxBool isOwner = false.obs;
Future<void> init() async {
await getSelfInfo();
await getGroupData();
await getMemberData(); //
canRemoveMembers();
}
//
Future<void> setGroupInfo({required V2TimGroupInfo changedInfo}) async {
if (isOwner.value && info.value != null) {
final res = await ImService.instance.setGroupInfo(info: changedInfo);
if (!res.success) {
// info
MyToast().tip(title: '请稍后再试');
getGroupData();
} else {
// info.value?.faceUrl = changedInfo.faceUrl;
final current = info.value!;
if (changedInfo.faceUrl != null) current.faceUrl = changedInfo.faceUrl;
if (changedInfo.groupName != null) current.groupName = changedInfo.groupName;
if (changedInfo.introduction != null) current.introduction = changedInfo.introduction;
if (changedInfo.notification != null) current.notification = changedInfo.notification;
info.refresh();
}
}
}
//
Future<void> getGroupData() async {
final res = await ImService.instance.getGroupsInfo(groupIDList: [groupID]);
if (res.success && res.data != null) {
info.value = res.data!.first.groupInfo ?? V2TimGroupInfo(groupID: groupID, groupType: GroupType.Work);
}
}
//(1413)
Future<void> getMemberData() async {
final res = await ImService.instance.getGroupMemberList(
groupID: groupID,
filter: GroupMemberFilterTypeEnum.V2TIM_GROUP_MEMBER_FILTER_ALL,
nextSeq: "0",
count: isOwner.value ? 13 : 14,
);
if (res.success && res.data != null) {
memberList.value = res.data!.memberInfoList ?? [];
}
}
//
Future<void> getSelfInfo() async {
final selfID = Get.find<ImUserInfoController>().userID.value;
final res = await ImService.instance.getGroupMembersInfo(
groupID: groupID,
memberList: [selfID],
);
if (res.success && res.data != null) {
selfInfo.value = res.data!.first;
}
}
//
Future<void> setSelfInfo({required String nameCard}) async {
final res = await ImService.instance.setGroupMemberInfo(
groupID: groupID,
userID: selfInfo.value?.userID ?? '',
nameCard: nameCard,
);
if (!res.success) {
MyToast().tip(title: '设置失败', position: 'top');
} else {
selfInfo.value?.nameCard = nameCard;
selfInfo.refresh();
}
}
// inviteUserToGroup
Future<void> inviteUserToGroup({required List<String> userList}) async {
final res = await ImService.instance.inviteUserToGroup(
groupID: groupID,
userList: userList,
);
if (res.success && res.data != null) {
refresh();
}
}
// 退
Future<void> quitGroup() async {
final res = await ImService.instance.quitGroup(
groupID: groupID,
);
if (res.success) {
refresh();
}
}
// inviteUserToGroup
Future<void> kickGroupMember(List<String> userIDs) async {
await ImService.instance.kickGroupMember(
groupID: groupID,
memberList: userIDs,
);
}
//
void canRemoveMembers() {
final owner = info.value?.owner;
final myID = selfInfo.value?.userID;
final myRole = selfInfo.value?.role;
if (owner == null || myID == null || myRole == null) {
isOwner.value = false;
} else if (owner == myID) {
isOwner.value = true;
} else if ([300, 400].contains(myRole)) {
isOwner.value = true;
} else {
isOwner.value = false;
}
// role=200 300 400
// if (owner == myID) result = true;
// if ([300, 400].contains(myRole)) result = true;
// isOwner
// return false;
}
}

View File

@ -0,0 +1,541 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:loopin/IM/controller/chat_detail_controller.dart';
import 'package:loopin/IM/im_service.dart';
import 'package:loopin/api/common_api.dart';
import 'package:loopin/components/my_confirm.dart';
import 'package:loopin/components/my_toast.dart';
import 'package:loopin/components/network_or_asset_image.dart';
import 'package:loopin/pages/groupChat/components/invite_action_sheet.dart';
import 'package:loopin/pages/groupChat/components/member_action_sheet.dart';
import 'package:loopin/pages/groupChat/components/set_group_info.dart';
import 'package:loopin/pages/groupChat/controller/group_detail_controller.dart';
import 'package:loopin/service/http.dart';
import 'package:loopin/utils/index.dart';
import 'package:loopin/utils/permissions.dart';
import 'package:shirne_dialog/shirne_dialog.dart';
import 'package:tencent_cloud_chat_sdk/enum/group_type.dart';
import 'package:tencent_cloud_chat_sdk/enum/receive_message_opt.dart';
import 'package:tencent_cloud_chat_sdk/enum/receive_message_opt_enum.dart';
import 'package:tencent_cloud_chat_sdk/models/v2_tim_group_info.dart';
import 'package:wechat_assets_picker/wechat_assets_picker.dart';
class Groupdetail extends StatefulWidget {
const Groupdetail({super.key});
// final String groupID;
// const Groupdetail({super.key, required this.groupID});
@override
State<Groupdetail> createState() => GroupdetailState();
}
class GroupdetailState extends State<Groupdetail> {
late final GroupDetailController controller;
@override
void initState() {
super.initState();
// put
controller = Get.find<GroupDetailController>();
// controller = Get.put(GroupDetailController(groupID: widget.groupID));
}
///
void pickFaceUrl(BuildContext context) async {
final hasPer = await Permissions.requestPhotoPermission();
if (!hasPer) {
Permissions.showPermissionDialog();
return;
}
final pickedAssets = await AssetPicker.pickAssets(
context,
pickerConfig: AssetPickerConfig(
textDelegate: const AssetPickerTextDelegate(),
pathNameBuilder: (AssetPathEntity album) {
return Utils.translateAlbumName(album);
},
maxAssets: 1,
requestType: RequestType.image,
filterOptions: FilterOptionGroup(
imageOption: const FilterOption(),
),
),
);
if (pickedAssets != null && pickedAssets.isNotEmpty) {
final asset = pickedAssets.first;
final file = await asset.file; //
if (file != null) {
final fileSizeInBytes = await file.length();
final sizeInMB = fileSizeInBytes / (1024 * 1024);
if (sizeInMB > 20) {
MyDialog.toast('图片大小不能超过20MB', icon: const Icon(Icons.check_circle), style: ToastStyle(backgroundColor: Colors.red.withAlpha(200)));
} else {
logger.w("图片合法,大小:$sizeInMB MB");
//upload(file)url地址
final istance = MyDialog.loading('上传中', duration: Duration(minutes: 1));
final res = await Http.upload(CommonApi.uploadFile, filePath: file.path);
final imgUrl = res['data']['url'];
logger.e(imgUrl);
//
await controller.setGroupInfo(
changedInfo: V2TimGroupInfo(
groupID: controller.info.value!.groupID,
groupType: GroupType.Work,
faceUrl: imgUrl,
),
);
istance.close();
}
}
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.grey[50],
appBar: AppBar(
centerTitle: true,
forceMaterialTransparency: true,
title: const Text(
"群资料",
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
leading: IconButton(
icon: const Icon(Icons.arrow_back),
onPressed: () {
Get.back(result: controller.info.value?.groupName ?? '');
},
),
bottom: PreferredSize(
preferredSize: const Size.fromHeight(1),
child: Container(height: 1, color: Colors.grey[300]),
),
),
body: ListView(
children: [
//
ListTile(
//
leading: Obx(
() => GestureDetector(
onTap: () {
//
if (controller.isOwner.value) {
//
pickFaceUrl(context);
}
},
child: ClipOval(
child: NetworkOrAssetImage(
imageUrl: controller.info.value?.faceUrl ?? '',
placeholderAsset: 'assets/images/group.png',
height: 60,
width: 60,
),
),
),
),
//
title: Obx(
() => GestureDetector(
onTap: () {
// setinfo页
logger.w('点了名称');
if (controller.isOwner.value) {
Get.to(
() => SetGroupInfoPage(
appBarTitle: "修改群名称",
fieldLabel: "群名称",
maxLines: 1,
maxLength: 20,
initialValue: controller.info.value?.groupName ?? "",
onSubmit: (value) async {
//
await controller.setGroupInfo(
changedInfo: V2TimGroupInfo(
groupID: controller.info.value!.groupID,
groupType: GroupType.Work,
groupName: value,
),
);
},
),
);
}
},
child: Text(
Utils.handleText(controller.info.value?.groupName, '', "未命名群聊"),
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w500),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
),
//
subtitle: Obx(
() => GestureDetector(
onTap: () {
// setinfo页
logger.w('点了简介绍');
if (controller.isOwner.value) {
Get.to(
() => SetGroupInfoPage(
appBarTitle: "修改群简介",
fieldLabel: "群简介",
maxLines: 5,
maxLength: 100,
initialValue: controller.info.value?.introduction ?? "",
onSubmit: (value) async {
//
await controller.setGroupInfo(
changedInfo: V2TimGroupInfo(
groupID: controller.info.value!.groupID,
groupType: GroupType.Work,
introduction: value,
),
);
},
),
);
}
},
child: Text(
Utils.handleText(controller.info.value?.introduction, '', "暂无群介绍"),
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: const TextStyle(color: Colors.grey),
),
),
),
trailing: const Icon(Icons.chevron_right),
),
const Divider(height: 1),
//
ListTile(
title: Obx(
() => Text("群成员(${controller.info.value?.memberCount ?? 0}/${controller.info.value?.memberMaxCount ?? 0}"),
),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
Obx(
() => Text("查看${controller.info.value?.memberCount ?? 0}个群成员", style: const TextStyle(color: Colors.grey, fontSize: 14)),
),
const Icon(Icons.chevron_right),
],
),
onTap: () {
//
showModalBottomSheet(
context: context,
barrierColor: Colors.white,
isScrollControlled: true,
useSafeArea: true,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
),
builder: (context) {
return MemberActionSheet(
showButton: false,
groupID: controller.groupID,
title: '群成员',
showSelf: true,
onAction: (userIDs) {
//
},
);
},
);
},
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
child: Wrap(
spacing: 16,
runSpacing: 12,
children: [
Obx(
() {
return Wrap(
spacing: 16,
runSpacing: 12,
children: controller.memberList.take(8).map((m) {
//
return GestureDetector(
onTap: () {
final currentUserID = controller.selfInfo.value?.userID ?? '';
if (m.userID != currentUserID) {
Get.toNamed('/vloger', arguments: {'memberId': m.userID});
}
},
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
ClipOval(
child: NetworkOrAssetImage(
imageUrl: m.faceUrl,
height: 50,
width: 50,
),
),
const SizedBox(height: 4),
SizedBox(
width: 50,
child: Text(
m.nickName ?? '未知昵称',
maxLines: 1,
softWrap: false,
overflow: TextOverflow.ellipsis,
style: const TextStyle(fontSize: 12),
),
),
],
),
);
}).toList(),
);
},
),
// +
GestureDetector(
onTap: () {
//,
logger.w('当前人数${controller.info.value?.memberCount ?? 0}---上限人数${controller.info.value?.memberMaxCount ?? 0}');
if ((controller.info.value?.memberCount ?? 0) < (controller.info.value?.memberMaxCount ?? 0)) {
//
showModalBottomSheet(
context: context,
barrierColor: Colors.white,
isScrollControlled: true,
useSafeArea: true,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
),
builder: (context) {
return InviteActionSheet(
groupID: controller.groupID,
title: '邀请新成员',
onAction: (userIDs) {
logger.w("准备添加群成员: $userIDs");
controller.inviteUserToGroup(userList: userIDs);
},
);
},
);
} else {
MyToast().tip(title: '群人数已达上限', position: 'top');
}
},
child: Column(
children: [
CircleAvatar(
radius: 25,
child: const Icon(Icons.add),
),
const SizedBox(height: 4),
const Text("邀请", style: TextStyle(fontSize: 12)),
],
),
),
// -
Obx(
() => controller.isOwner.value
? GestureDetector(
onTap: () {
showModalBottomSheet(
context: context,
barrierColor: Colors.white,
isScrollControlled: true,
useSafeArea: true,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
),
builder: (context) {
return MemberActionSheet(
groupID: controller.groupID,
title: '移出成员',
onAction: (userIDs) {
logger.w("准备移除群成员: $userIDs");
controller.kickGroupMember(userIDs);
},
);
},
);
},
child: Column(
children: [
const CircleAvatar(
radius: 25,
child: Icon(Icons.remove),
),
const SizedBox(height: 4),
const Text("移除", style: TextStyle(fontSize: 12)),
],
),
)
: SizedBox.shrink(),
)
],
),
),
const Divider(height: 1),
//
ListTile(
title: const Text("群公告"),
subtitle: Obx(
() => Text(
Utils.handleText(controller.info.value?.notification, '', "暂无公告"),
maxLines: 3,
overflow: TextOverflow.ellipsis,
),
),
trailing: const Icon(Icons.chevron_right),
onTap: () {
logger.w('点击了群公告');
if (controller.isOwner.value) {
Get.to(
() => SetGroupInfoPage(
appBarTitle: "修改群公告",
fieldLabel: "群公告",
maxLines: 8,
maxLength: 200,
initialValue: controller.info.value?.notification ?? "",
onSubmit: (value) async {
//
await controller.setGroupInfo(
changedInfo: V2TimGroupInfo(
groupID: controller.info.value!.groupID,
groupType: GroupType.Work,
notification: value,
),
);
},
),
);
}
},
),
const Divider(height: 1),
//
ListTile(
title: const Text("我的本群昵称"),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
Obx(
() {
return Text(
Utils.handleText(controller.selfInfo.value?.nameCard, controller.selfInfo.value?.nickName, '未设置昵称'),
style: TextStyle(fontSize: 14),
);
},
),
const Icon(Icons.chevron_right),
],
),
onTap: () async {
Get.to(
() => SetGroupInfoPage(
appBarTitle: "修改群昵称",
fieldLabel: "群昵称",
maxLines: 1,
maxLength: 12,
initialValue: controller.selfInfo.value?.nameCard ?? "",
onSubmit: (value) {
//
controller.setSelfInfo(nameCard: value);
},
),
);
},
),
const Divider(height: 1),
//
Obx(
() => SwitchListTile(
title: Text("消息免打扰"),
value: controller.info.value?.recvOpt == ReceiveMsgOptType.kTIMRecvMsgOpt_Not_Notify_Except_At, // 03
activeColor: Colors.green,
inactiveThumbColor: Colors.grey,
inactiveTrackColor: Colors.white,
onChanged: (v) async {
if (controller.info.value != null) {
logger.w(v);
logger.e(controller.info.value?.recvOpt);
// v=true -> = 3
final res = await ImService.instance.setGroupReceiveMessageOpt(
groupID: controller.groupID,
opt: v ? ReceiveMsgOptEnum.V2TIM_RECEIVE_NOT_NOTIFY_MESSAGE_EXCEPT_AT : ReceiveMsgOptEnum.V2TIM_RECEIVE_MESSAGE, // = 0
);
if (res.success) {
controller.info.value!.recvOpt =
v ? ReceiveMsgOptType.kTIMRecvMsgOpt_Not_Notify_Except_At : ReceiveMsgOptType.kTIMRecvMsgOpt_Receive; // = 0 =3
controller.info.refresh();
} else {
MyToast().tip(title: '网络繁忙,请稍后再试', position: 'top');
}
}
// class ReceiveMsgOptType {
// 线线 APNs
// static const int kTIMRecvMsgOpt_Receive = 0;
// 线
// static const int kTIMRecvMsgOpt_Not_Receive = 1;
// 线线
// static const int kTIMRecvMsgOpt_Not_Notify = 2;
// 线线 at
// static const int kTIMRecvMsgOpt_Not_Notify_Except_At = 3;
// 线线@
// static const int kTIMRecvMsgOpt_Not_Receive_Except_At = 4;
// }
},
),
),
const Divider(height: 1),
//
// ListTile(
// title: const Text("举报"),
// trailing: const Icon(Icons.chevron_right),
// onTap: () {
// //
// },
// ),
// 退
const SizedBox(height: 20),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: Colors.red,
minimumSize: const Size(double.infinity, 48),
),
onPressed: () async {
//
final confirmed = await ConfirmDialog.show(
title: "提示",
content: "确认要退出群聊吗?",
);
if (confirmed == true) {
await controller.quitGroup();
Get.find<ChatDetailController>().toolFlag.value = false; //
Get.back(); //
}
},
child: const Text("退出群聊", style: TextStyle(color: Colors.white)),
),
),
const SizedBox(height: 30),
],
),
);
}
}

View File

@ -159,6 +159,7 @@ class GrouplistState extends State<Grouplist> with SingleTickerProviderStateMixi
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
//
Text(
item.groupName?.isNotEmpty == true ? item.groupName! : '群聊',
style: const TextStyle(
@ -166,6 +167,7 @@ class GrouplistState extends State<Grouplist> with SingleTickerProviderStateMixi
fontWeight: FontWeight.normal,
),
),
//
if (item.introduction?.isNotEmpty ?? false) ...[
const SizedBox(height: 2.0),
Text(

View File

@ -3,14 +3,15 @@ import 'package:easy_refresh/easy_refresh.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:loopin/IM/controller/im_user_info_controller.dart';
import 'package:loopin/IM/im_message.dart';
import 'package:loopin/IM/im_service.dart';
import 'package:loopin/components/network_or_asset_image.dart';
import 'package:loopin/styles/index.dart';
import 'package:tencent_cloud_chat_sdk/enum/group_member_role_enum.dart';
import 'package:tencent_cloud_chat_sdk/enum/group_type.dart';
import 'package:tencent_cloud_chat_sdk/models/v2_tim_conversation.dart';
import 'package:tencent_cloud_chat_sdk/models/v2_tim_group_member.dart';
import 'package:tencent_cloud_chat_sdk/models/v2_tim_user_full_info.dart';
import 'package:tencent_cloud_chat_sdk/web/compatible_models/v2_tim_conversation.dart';
class StartGroupChatPage extends StatefulWidget {
const StartGroupChatPage({super.key});
@ -28,6 +29,7 @@ class _StartGroupChatPageState extends State<StartGroupChatPage> {
String page = '';
bool hasMore = true;
bool isLoading = true;
bool makeGroup = true;
@override
void initState() {
@ -41,7 +43,7 @@ class _StartGroupChatPageState extends State<StartGroupChatPage> {
super.dispose();
}
//
//
Future<void> _loadData({bool reset = false}) async {
if (reset) {
page = '';
@ -97,6 +99,13 @@ class _StartGroupChatPageState extends State<StartGroupChatPage> {
///
void createGroup(Set<String> selectedIds) async {
logger.w(makeGroup);
if (makeGroup == false) {
return;
}
setState(() {
makeGroup = false;
});
// dataList是原始数据List<V2TimUserFullInfo> dataList = [];
// dataList和selectedIds来构建memberList
final ctl = Get.find<ImUserInfoController>();
@ -113,13 +122,42 @@ class _StartGroupChatPageState extends State<StartGroupChatPage> {
);
memberList.insert(0, self);
final groupName = buildGroupName(selectedIds);
final res = await ImService.instance.createGroup(groupType: GroupType.Work, groupName: groupName, memberList: memberList);
if (res.success) {
final groupID = res.data;
logger.w(groupID);
final V2TimConversation conv = V2TimConversation(conversationID: 'group_$groupID');
conv.showName = groupName;
Get.toNamed('/chatGroup', arguments: conv);
try {
final res = await ImService.instance.createGroup(groupType: GroupType.Work, groupName: groupName, memberList: memberList);
if (res.success) {
final groupID = res.data;
logger.w(groupID);
// final V2TimConversation conv = V2TimConversation(conversationID: 'group_$groupID');
// conv.showName = groupName;
final msgRes = await IMMessage().createTextMessage(text: '加入了群聊:$groupName');
if (msgRes.success && msgRes.data?.messageInfo != null) {
final groupRes = await IMMessage().sendMessage(
msg: msgRes.data!.messageInfo!,
groupID: groupID,
isExcludedFromUnreadCount: true,
isPush: false,
groupName: groupName,
cloudCustomData: 'tips',
);
if (groupRes.success) {
final convRes = await ImService.instance.getConversation(conversationID: 'group_$groupID');
if (convRes.success) {
setState(() {
makeGroup = true;
});
final V2TimConversation conv = convRes.data;
logger.e(conv.toJson());
await Get.toNamed('/chatGroup', arguments: conv);
}
}
}
}
} catch (e) {
logger.e(e);
//
setState(() {
makeGroup = true;
});
}
}
@ -205,29 +243,29 @@ class _StartGroupChatPageState extends State<StartGroupChatPage> {
body: Column(
children: [
// -
Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
children: [
_buildCard(
icon: Icons.public,
title: "创建公开群",
onTap: () {
logger.w("跳转到 创建公开群");
},
),
// _buildCard(
// icon: Icons.group,
// title: "创建好友群",
// onTap: () {
// logger.w("跳转到 创建好友群");
// },
// ),
],
),
),
// Padding(
// padding: const EdgeInsets.all(8.0),
// child: Column(
// children: [
// _buildCard(
// icon: Icons.public,
// title: "创建公开群",
// onTap: () {
// logger.w("跳转到 创建公开群");
// },
// ),
// _buildCard(
// icon: Icons.group,
// title: "创建好友群",
// onTap: () {
// logger.w("跳转到 创建好友群");
// },
// ),
// ],
// ),
// ),
const Divider(),
// const Divider(),
//
Expanded(
@ -318,11 +356,11 @@ class _StartGroupChatPageState extends State<StartGroupChatPage> {
duration: const Duration(milliseconds: 300),
tween: ColorTween(
begin: Colors.grey,
end: selectedIds.isEmpty ? Colors.grey : FStyle.primaryColor,
end: (selectedIds.isNotEmpty && makeGroup == true) ? FStyle.primaryColor : Colors.grey,
),
builder: (context, bgColor, _) {
return ElevatedButton(
onPressed: selectedIds.isEmpty
onPressed: (selectedIds.isEmpty && makeGroup == false)
? null
: () {
logger.w("选择了用户:$selectedIds");

View File

@ -7,6 +7,7 @@ import 'package:loopin/IM/controller/im_user_info_controller.dart';
import 'package:loopin/IM/im_service.dart';
import 'package:loopin/api/video_api.dart';
import 'package:loopin/components/custom_sticky_header.dart';
import 'package:loopin/components/my_confirm.dart';
import 'package:loopin/components/network_or_asset_image.dart';
import 'package:loopin/components/only_down_scroll_physics.dart';
import 'package:loopin/controller/video_module_controller.dart';
@ -559,31 +560,40 @@ class MyPageState extends State<MyPage> with SingleTickerProviderStateMixin {
onLongPress: () {
showModalBottomSheet(
context: Get.context!,
backgroundColor: Colors.black.withOpacity(0.8),
backgroundColor: Colors.white,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
),
builder: (context) {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
ListTile(
leading: const Icon(Icons.lock, color: Colors.white),
title: const Text('设为私密', style: TextStyle(color: Colors.white)),
onTap: () {
Navigator.pop(context);
// TODO:
},
),
ListTile(
leading: const Icon(Icons.delete, color: Colors.redAccent),
title: const Text('删除视频', style: TextStyle(color: Colors.redAccent)),
onTap: () {
Navigator.pop(context);
// TODO:
},
),
],
return SafeArea(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
// ListTile(
// leading: const Icon(Icons.lock, color: Colors.black),
// title: const Text('设为私密', style: TextStyle(color: Colors.black)),
// onTap: () {
// Navigator.pop(context);
// // TODO:
// },
// ),
ListTile(
leading: const Icon(Icons.delete, color: Colors.redAccent),
title: const Text('删除视频', style: TextStyle(color: Colors.redAccent)),
onTap: () async {
Get.back();
final confirmed = await ConfirmDialog.show(
title: "提示",
content: "确认要删除吗?",
);
if (confirmed == true) {
Get.back();
// TODO:
}
},
),
],
),
);
},
);
@ -598,8 +608,9 @@ class MyPageState extends State<MyPage> with SingleTickerProviderStateMixin {
///
ClipRRect(
borderRadius: BorderRadius.circular(12.0),
child: Image.network(
item['cover'] ?? item['firstFrameImg'],
child: NetworkOrAssetImage(
imageUrl: item['cover'] ?? item['firstFrameImg'] ?? '',
placeholderAsset: 'assets/images/bk.jpg',
fit: BoxFit.cover,
width: double.infinity,
height: double.infinity,

View File

@ -9,6 +9,7 @@ import 'package:loopin/components/network_or_asset_image.dart';
import 'package:loopin/service/http.dart';
import 'package:loopin/styles/index.dart';
import 'package:loopin/utils/index.dart';
import 'package:loopin/utils/permissions.dart';
import 'package:loopin/utils/wxsdk.dart';
import 'package:shirne_dialog/shirne_dialog.dart';
import 'package:wechat_assets_picker/wechat_assets_picker.dart';
@ -205,6 +206,11 @@ class _UserInfoState extends State<UserInfo> {
///
void pickCover(BuildContext context) async {
final hasPer = await Permissions.requestPhotoPermission();
if (!hasPer) {
Permissions.showPermissionDialog();
return;
}
final pickedAssets = await AssetPicker.pickAssets(
context,
pickerConfig: AssetPickerConfig(
@ -226,10 +232,10 @@ class _UserInfoState extends State<UserInfo> {
if (file != null) {
final fileSizeInBytes = await file.length();
final sizeInMB = fileSizeInBytes / (1024 * 1024);
if (sizeInMB > 100) {
MyDialog.toast('图片大小不能超过100MB', icon: const Icon(Icons.check_circle), style: ToastStyle(backgroundColor: Colors.red.withAlpha(200)));
if (sizeInMB > 200) {
MyDialog.toast('图片大小不能超过200MB', icon: const Icon(Icons.check_circle), style: ToastStyle(backgroundColor: Colors.red.withAlpha(200)));
} else {
print("图片合法,大小:$sizeInMB MB");
print("视频合法,大小:$sizeInMB MB");
//upload(file)url地址
final istance = MyDialog.loading('上传中');
final res = await Http.upload(CommonApi.uploadFile, filePath: file.path);
@ -244,6 +250,11 @@ class _UserInfoState extends State<UserInfo> {
///
void pickFaceUrl(BuildContext context) async {
final hasPer = await Permissions.requestPhotoPermission();
if (!hasPer) {
Permissions.showPermissionDialog();
return;
}
final pickedAssets = await AssetPicker.pickAssets(
context,
pickerConfig: AssetPickerConfig(
@ -265,8 +276,8 @@ class _UserInfoState extends State<UserInfo> {
if (file != null) {
final fileSizeInBytes = await file.length();
final sizeInMB = fileSizeInBytes / (1024 * 1024);
if (sizeInMB > 100) {
MyDialog.toast('图片大小不能超过100MB', icon: const Icon(Icons.check_circle), style: ToastStyle(backgroundColor: Colors.red.withAlpha(200)));
if (sizeInMB > 20) {
MyDialog.toast('图片大小不能超过20MB', icon: const Icon(Icons.check_circle), style: ToastStyle(backgroundColor: Colors.red.withAlpha(200)));
} else {
print("图片合法,大小:$sizeInMB MB");
//upload(file)url地址

View File

@ -10,7 +10,9 @@ import 'package:loopin/components/image_viewer.dart';
import 'package:loopin/components/preview_video.dart';
import 'package:loopin/service/http.dart';
import 'package:loopin/utils/index.dart';
import 'package:loopin/utils/permissions.dart';
import 'package:loopin/utils/snapshot.dart';
import 'package:shirne_dialog/shirne_dialog.dart';
import 'package:wechat_assets_picker/wechat_assets_picker.dart';
class UploadVideoPage extends StatefulWidget {
@ -54,12 +56,10 @@ class _UploadVideoPageState extends State<UploadVideoPage> {
Future<void> pickVideo() async {
descFocusNode.unfocus();
final hasPer = await Permissions.requestVideoPermission();
final result = await PhotoManager.requestPermissionExtend();
if (!result.isAuth) {
if (!mounted) return;
Get.snackbar('权限被拒绝', '请前往设置授权访问视频');
if (!hasPer) {
Permissions.showPermissionDialog();
return;
}
@ -85,9 +85,20 @@ class _UploadVideoPageState extends State<UploadVideoPage> {
);
if (pickedAssets != null && pickedAssets.isNotEmpty) {
selectedVideo.value = pickedAssets.first;
status.value = '已选择视频';
uploadVideo();
final asset = pickedAssets.first;
final file = await asset.file; //
if (file != null) {
final fileSizeInBytes = await file.length();
final sizeInMB = fileSizeInBytes / (1024 * 1024);
if (sizeInMB > 200) {
MyDialog.toast('文件大小不能超过200MB', icon: const Icon(Icons.check_circle), style: ToastStyle(backgroundColor: Colors.red.withAlpha(200)));
} else {
logger.i("文件合法,大小:$sizeInMB MB");
selectedVideo.value = pickedAssets.first;
status.value = '已选择视频';
uploadVideo();
}
}
}
}
@ -95,11 +106,9 @@ class _UploadVideoPageState extends State<UploadVideoPage> {
Future<void> pickCoverImage() async {
descFocusNode.unfocus();
final result = await PhotoManager.requestPermissionExtend();
if (!result.isAuth) {
if (!mounted) return;
Get.snackbar('权限被拒绝', '请前往设置授权访问照片');
final hasPer = await Permissions.requestPhotoPermission();
if (!hasPer) {
Permissions.showPermissionDialog();
return;
}
@ -118,9 +127,19 @@ class _UploadVideoPageState extends State<UploadVideoPage> {
);
if (pickedAssets != null && pickedAssets.isNotEmpty) {
selectedCover.value = pickedAssets.first;
//
uploadImg();
final asset = pickedAssets.first;
final file = await asset.file; //
if (file != null) {
final fileSizeInBytes = await file.length();
final sizeInMB = fileSizeInBytes / (1024 * 1024);
if (sizeInMB > 20) {
MyDialog.toast('文件大小不能超过20MB', icon: const Icon(Icons.check_circle), style: ToastStyle(backgroundColor: Colors.red.withAlpha(200)));
} else {
logger.i("文件合法,大小:$sizeInMB MB");
selectedCover.value = pickedAssets.first;
uploadImg();
}
}
}
}

View File

@ -8,8 +8,8 @@ class HttpConfig {
// baseUrl: 'http://43.143.227.203:8099',
// baseUrl: 'http://111.62.22.190:8080',
// baseUrl: 'http://cjh.wuzhongjie.com.cn',
// baseUrl: 'http://82.156.121.2:8880',
baseUrl: 'http://192.168.1.65:8880',
baseUrl: 'http://82.156.121.2:8880',
// baseUrl: 'http://192.168.1.65:8880',
// baseUrl: 'http://192.168.1.22:8080',
// connectTimeout: Duration(seconds: 30),

View File

@ -214,4 +214,15 @@ class Utils {
return '未知状态';
}
}
// IM的名字
static String handleText(String? nameCard, String? nickName, String defaultValue) {
if (nameCard != null && nameCard.trim().isNotEmpty) {
return nameCard;
}
if (nickName != null && nickName.trim().isNotEmpty) {
return nickName;
}
return defaultValue;
}
}

View File

@ -1,31 +1,45 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:loopin/IM/im_service.dart';
import 'package:loopin/components/network_or_asset_image.dart';
import 'package:loopin/utils/parse_message_summary.dart';
import 'package:shirne_dialog/shirne_dialog.dart';
import 'package:tencent_cloud_chat_sdk/models/v2_tim_group_info.dart';
import 'package:tencent_cloud_chat_sdk/models/v2_tim_message.dart';
import 'package:tencent_cloud_chat_sdk/models/v2_tim_user_full_info.dart';
class NotificationBanner {
static void show(V2TimMessage msg) {
final nickname = msg.nameCard ?? msg.nickName ?? msg.senderProfile?.nickName ?? msg.sender ?? '未知用户';
final avatar = msg.faceUrl ?? msg.senderProfile?.faceUrl ?? '';
static void show(V2TimMessage msg, bool isGroup) async {
String name = '';
String avatar = '';
if (isGroup) {
final res = await ImService.instance.getGroupsInfo(groupIDList: [msg.groupID!]);
if (res.success && res.data != null) {
V2TimGroupInfo? gpInfo = res.data!.first.groupInfo;
name = gpInfo?.groupName ?? "未知群名";
avatar = gpInfo?.faceUrl ?? "";
} else {
name = '获取群名称失败';
}
} else {
name = msg.nameCard ?? msg.nickName ?? msg.senderProfile?.nickName ?? msg.sender ?? '未知用户';
avatar = msg.faceUrl ?? msg.senderProfile?.faceUrl ?? '';
}
final text = parseMessageSummary(msg);
Get.snackbar(
nickname,
name,
text,
duration: const Duration(seconds: 3),
snackPosition: SnackPosition.TOP,
margin: const EdgeInsets.all(12),
backgroundColor: Get.theme.cardColor,
colorText: Get.theme.textTheme.bodyLarge?.color,
icon: avatar.isNotEmpty
? CircleAvatar(
backgroundImage: NetworkImage(avatar),
radius: 16,
)
: null,
icon: ClipOval(
child: NetworkOrAssetImage(
imageUrl: avatar,
placeholderAsset: isGroup ? 'assets/images/group.png' : 'assets/images/avatar/default.png',
),
),
onTap: (_) async {
//
Get.closeCurrentSnackbar();
@ -41,7 +55,7 @@ class NotificationBanner {
//
Get.toNamed('/chat', arguments: cRes.data);
} else if (msg.groupID != null) {
Get.toNamed('/chat', arguments: cRes.data);
Get.toNamed('/chatGroup', arguments: cRes.data);
}
} else {
//

View File

@ -1,11 +1,44 @@
import 'dart:convert';
import 'package:get/get.dart';
import 'package:loopin/IM/controller/im_user_info_controller.dart';
import 'package:loopin/IM/im_service.dart';
import 'package:loopin/models/notify_message.type.dart';
import 'package:loopin/models/summary_type.dart';
import 'package:loopin/utils/index.dart';
import 'package:tencent_cloud_chat_sdk/enum/group_tips_elem_type.dart';
import 'package:tencent_cloud_chat_sdk/enum/message_elem_type.dart';
import 'package:tencent_cloud_chat_sdk/models/v2_tim_group_change_info.dart';
import 'package:tencent_cloud_chat_sdk/models/v2_tim_group_member_info.dart';
import 'package:tencent_cloud_chat_sdk/models/v2_tim_message.dart';
///
class GroupChangeInfoType {
///
static const int custom = 0;
///
static const int name = 1;
///
static const int notification = 3; // 2 23
///
static const int introduction = 2; // 3 32
///
static const int faceUrl = 4;
///
static const int owner = 5;
///
static const int receiveMessageOpt = 10;
///
static const int shutUpAll = 11;
}
String parseMessageSummary(V2TimMessage msg) {
switch (msg.elemType) {
case MessageElemType.V2TIM_ELEM_TYPE_TEXT:
@ -27,7 +60,93 @@ String parseMessageSummary(V2TimMessage msg) {
case MessageElemType.V2TIM_ELEM_TYPE_MERGER:
return '[合并转发消息]';
case MessageElemType.V2TIM_ELEM_TYPE_GROUP_TIPS:
return '[群提示]';
// Tips
// logger.w(msg.toJson());
final tipType = msg.groupTipsElem?.type ?? 0;
// logger.e(tipType);
V2TimGroupMemberInfo? op = msg.groupTipsElem?.opMember;
String opName = Utils.handleText(op?.nameCard, op?.nickName, '群主');
if (op != null) {
opName = op.userID == (Get.find<ImUserInfoController>().userID.value) ? '' : opName;
}
/// 退
String nickNames = '';
String kickTips = '';
String inviteTip = '';
String quitTips = '';
///
String changeInfo = '';
///
// String memberChangeInfo = '';
// 退退
List<V2TimGroupMemberInfo?> changed = msg.groupTipsElem?.memberList ?? [];
// 退
if (changed.isNotEmpty) {
final selfID = Get.find<ImUserInfoController>().userID.value;
final nicknamesList = changed.map((item) {
if (item == null) return "未知用户";
if (item.userID == selfID) return "";
return Utils.handleText(item.nameCard, item.nickName, "未知用户");
}).toList();
nickNames = nicknamesList.join(',');
inviteTip = '$opName 邀请 $nickNames 加入群聊';
kickTips = '$opName$nickNames 移出群聊';
quitTips = '$nickNames 退出了群聊';
}
//
List<V2TimGroupChangeInfo?> groupChanged = msg.groupTipsElem?.groupChangeInfoList ?? [];
if (groupChanged.isNotEmpty) {
//
for (final item in groupChanged) {
if (item?.type == GroupChangeInfoType.faceUrl) {
//
changeInfo = '$opName 修改了群头像';
} else if (item?.type == GroupChangeInfoType.introduction) {
//
changeInfo = '$opName 将群简介修改为:${item?.value}';
} else if (item?.type == GroupChangeInfoType.notification) {
//
changeInfo = '$opName 将群公告修改为:${item?.value}';
} else if (item?.type == GroupChangeInfoType.name) {
//
changeInfo = '$opName 将群名称修改为:${item?.value}';
} else {
changeInfo = '群资料发生变更';
}
}
}
//
// List<V2TimGroupMemberChangeInfo?> memberChanged = msg.groupTipsElem?.memberChangeInfoList ?? [];
// if (memberChanged.isNotEmpty) {
// for (final item in memberChanged) {}
// }
if (tipType == GroupTipsElemType.V2TIM_GROUP_TIPS_TYPE_INVITE) {
// ,
return inviteTip;
} else if (tipType == GroupTipsElemType.V2TIM_GROUP_TIPS_TYPE_KICKED) {
//
return kickTips;
} else if (tipType == GroupTipsElemType.V2TIM_GROUP_TIPS_TYPE_QUIT) {
// 退
return quitTips;
} else if (tipType == GroupTipsElemType.V2TIM_GROUP_TIPS_TYPE_GROUP_INFO_CHANGE) {
//groupName & introduction & notification & faceUrl & owner & custom)
return changeInfo;
} else if (tipType == GroupTipsElemType.V2TIM_GROUP_TIPS_TYPE_MEMBER_INFO_CHANGE) {
// (opMember muteTime)
return '群成员资料变更';
} else {
return '[收到一条群系统通知]';
}
case MessageElemType.V2TIM_ELEM_TYPE_GROUP_REPORT:
default:
return '[未知消息类型1]';
}

View File

@ -2,6 +2,8 @@ import 'dart:io';
import 'package:device_info_plus/device_info_plus.dart';
import 'package:get/get.dart';
import 'package:loopin/IM/im_service.dart';
import 'package:loopin/components/my_confirm.dart';
import 'package:permission_handler/permission_handler.dart';
class Permissions {
@ -13,9 +15,11 @@ class Permissions {
final sdkInt = androidInfo.version.sdkInt;
if (sdkInt >= 33) {
// Android 13
final status = await Permission.videos.request();
return status.isGranted;
} else {
// Android 12
final status = await Permission.storage.request();
return status.isGranted;
}
@ -31,17 +35,36 @@ class Permissions {
return await _checkAndRequest(Permission.camera);
}
//
static Future<bool> requestPhotoPermission() async {
// return await _checkAndRequest(Permission.photos);
if (Platform.isAndroid) {
final deviceInfoPlugin = DeviceInfoPlugin();
final androidInfo = await deviceInfoPlugin.androidInfo;
final sdkInt = androidInfo.version.sdkInt;
logger.w(sdkInt);
if (sdkInt >= 33) {
// Android 13
final status = await Permission.photos.request();
return status.isGranted;
} else {
// Android 12
final status = await Permission.storage.request();
return status.isGranted;
}
} else if (Platform.isIOS) {
final status = await Permission.photos.request();
return status.isGranted;
}
return false;
}
//
static Future<bool> requestMicrophonePermission() async {
return await _checkAndRequest(Permission.microphone);
}
//
static Future<bool> requestPhotoPermission() async {
return await _checkAndRequest(Permission.microphone);
}
//
//
static Future<bool> requestStoragePermission() async {
return await _checkAndRequest(Permission.storage);
}
@ -57,8 +80,10 @@ class Permissions {
if (result.isGranted) return true;
if (result.isPermanentlyDenied) {
_showPermissionDialog();
//
showPermissionDialog();
} else {
//
Get.snackbar('权限请求失败', '无法访问,请授权对应权限后重试');
}
@ -66,16 +91,14 @@ class Permissions {
}
//
static void _showPermissionDialog() {
Get.defaultDialog(
static void showPermissionDialog() async {
final confirmed = await ConfirmDialog.show(
title: '需要权限',
middleText: '请前往系统设置中手动开启权限',
textCancel: '取消',
textConfirm: '去设置',
onConfirm: () async {
Get.back();
await openAppSettings();
},
content: '请前往系统设置中手动开启权限',
confirmText: '去设置',
);
if (confirmed == true) {
await openAppSettings();
}
}
}