group
This commit is contained in:
parent
09f0048b67
commit
539cf87c46
@ -171,8 +171,9 @@ class ChatController extends GetxController {
|
|||||||
///构建陌生人消息菜单入口(更新时候传入对应的会话)
|
///构建陌生人消息菜单入口(更新时候传入对应的会话)
|
||||||
Future<void> getNoFriendData({V2TimConversation? csion}) async {
|
Future<void> getNoFriendData({V2TimConversation? csion}) async {
|
||||||
// 检测会话列表是否已有陌生人消息菜单
|
// 检测会话列表是否已有陌生人消息菜单
|
||||||
// final hasNoFriend = chatList.any((item) => item.conversation.conversationGroupList?.contains(myConversationType.ConversationType.noFriend.name) ?? false);
|
final hasNoFriend = chatList
|
||||||
final hasNoFriend = chatList.any((item) => item.isCustomAdmin!.contains(myConversationType.ConversationType.noFriend.name));
|
.where((item) => item.conversation.type == 1) // 只检测单聊天 type == 1 的会话
|
||||||
|
.any((item) => item.isCustomAdmin?.contains(myConversationType.ConversationType.noFriend.name) ?? false);
|
||||||
logger.w('检测是否存在nofriend入口:$hasNoFriend');
|
logger.w('检测是否存在nofriend入口:$hasNoFriend');
|
||||||
if (hasNoFriend) {
|
if (hasNoFriend) {
|
||||||
// 已经有了入口
|
// 已经有了入口
|
||||||
|
@ -3,6 +3,7 @@ import 'package:get/get.dart';
|
|||||||
import 'package:loopin/IM/im_message.dart';
|
import 'package:loopin/IM/im_message.dart';
|
||||||
import 'package:loopin/IM/im_service.dart';
|
import 'package:loopin/IM/im_service.dart';
|
||||||
import 'package:loopin/utils/index.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';
|
import 'package:tencent_cloud_chat_sdk/models/v2_tim_message.dart';
|
||||||
|
|
||||||
class ChatDetailController extends GetxController {
|
class ChatDetailController extends GetxController {
|
||||||
@ -10,10 +11,10 @@ class ChatDetailController extends GetxController {
|
|||||||
|
|
||||||
ChatDetailController({required this.id});
|
ChatDetailController({required this.id});
|
||||||
final ScrollController chatController = ScrollController();
|
final ScrollController chatController = ScrollController();
|
||||||
|
|
||||||
final RxList<V2TimMessage> chatList = <V2TimMessage>[].obs;
|
final RxList<V2TimMessage> chatList = <V2TimMessage>[].obs;
|
||||||
final RxBool isFriend = true.obs;
|
final RxBool isFriend = true.obs;
|
||||||
final RxInt followType = 0.obs;
|
final RxInt followType = 0.obs;
|
||||||
|
final RxBool toolFlag = true.obs; // 工具栏使用权限(针对群聊的)
|
||||||
|
|
||||||
void updateChatListWithTimeLabels(List<V2TimMessage> originMessages) async {
|
void updateChatListWithTimeLabels(List<V2TimMessage> originMessages) async {
|
||||||
final idRes = await ImService.instance.selfUserId();
|
final idRes = await ImService.instance.selfUserId();
|
||||||
@ -31,6 +32,9 @@ class ChatDetailController extends GetxController {
|
|||||||
if (i == originMessages.length - 1) {
|
if (i == originMessages.length - 1) {
|
||||||
// 最后一条消息,加时间标签
|
// 最后一条消息,加时间标签
|
||||||
needInsertLabel = true;
|
needInsertLabel = true;
|
||||||
|
} else if (current.localCustomData == 'time_label' || current.elemType == MessageElemType.V2TIM_ELEM_TYPE_GROUP_TIPS) {
|
||||||
|
//
|
||||||
|
needInsertLabel = true;
|
||||||
} else {
|
} else {
|
||||||
final next = originMessages[i + 1];
|
final next = originMessages[i + 1];
|
||||||
final nextTimestamp = next.timestamp ?? 0;
|
final nextTimestamp = next.timestamp ?? 0;
|
||||||
|
@ -1,4 +1,6 @@
|
|||||||
|
import 'package:get/get.dart';
|
||||||
import 'package:logger/logger.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/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_change_info.dart';
|
||||||
import 'package:tencent_cloud_chat_sdk/models/v2_tim_group_member_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 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) {}
|
void onMemberInfoChanged(String groupID, List<V2TimGroupMemberChangeInfo> v2timGroupMemberChangeInfoList) {}
|
||||||
|
@ -22,13 +22,16 @@ class IMMessage {
|
|||||||
String? toUserID,
|
String? toUserID,
|
||||||
String? groupID,
|
String? groupID,
|
||||||
String? cloudCustomData,
|
String? cloudCustomData,
|
||||||
|
String? groupName,
|
||||||
|
bool isPush = true,
|
||||||
|
bool isExcludedFromUnreadCount = false,
|
||||||
}) async {
|
}) async {
|
||||||
// 必须且只能设置一个:toUserID(单聊)或 groupID(群聊)
|
// 必须且只能设置一个:toUserID(单聊)或 groupID(群聊)
|
||||||
if ((toUserID == null && groupID == null) || (toUserID != null && groupID != null)) {
|
if ((toUserID == null && groupID == null) || (toUserID != null && groupID != null)) {
|
||||||
return ImResult(
|
return ImResult(
|
||||||
success: false,
|
success: false,
|
||||||
code: -1,
|
code: -1,
|
||||||
desc: "只能指定一个 receiver(toUserID)或 groupID",
|
desc: "只能指定一个receiver:toUserID或groupID",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (cloudCustomData != null) {
|
if (cloudCustomData != null) {
|
||||||
@ -37,10 +40,15 @@ class IMMessage {
|
|||||||
// 解析消息类型
|
// 解析消息类型
|
||||||
V2TimValueCallback<V2TimMessage> sendRes;
|
V2TimValueCallback<V2TimMessage> sendRes;
|
||||||
// final controller = Get.find<ChatDetailController>();
|
// final controller = Get.find<ChatDetailController>();
|
||||||
final myInfo = Get.find<ImUserInfoController>();
|
|
||||||
logger.w('启用默认title:${myInfo.nickname.value}');
|
|
||||||
// 单聊
|
// 单聊
|
||||||
if (toUserID != null) {
|
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(
|
sendRes = await TencentImSDKPlugin.v2TIMManager.getMessageManager().sendMessage(
|
||||||
message: msg,
|
message: msg,
|
||||||
receiver: toUserID,
|
receiver: toUserID,
|
||||||
@ -53,31 +61,43 @@ class IMMessage {
|
|||||||
// },
|
// },
|
||||||
groupID: "",
|
groupID: "",
|
||||||
priority: MessagePriorityEnum.V2TIM_PRIORITY_DEFAULT,
|
priority: MessagePriorityEnum.V2TIM_PRIORITY_DEFAULT,
|
||||||
onlineUserOnly: false,
|
onlineUserOnly: false, // 是否只推送在线用户,默认全推
|
||||||
isExcludedFromUnreadCount: false,
|
isExcludedFromUnreadCount: isExcludedFromUnreadCount, //默认记入 不计入未读,如果这里设置了,那么message中设置的将失效
|
||||||
isExcludedFromLastMessage: false,
|
isExcludedFromLastMessage: false, // 不作为会话的最新消息
|
||||||
needReadReceipt: false,
|
isSupportMessageExtension: false, // 支持消息扩展
|
||||||
offlinePushInfo: OfflinePushInfo(
|
isExcludedFromContentModeration: false, // 绕过内容审核
|
||||||
title: myInfo.nickname.value,
|
needReadReceipt: false, // 已读回执
|
||||||
desc: parseMessageSummary(msg),
|
offlinePushInfo: isPush ? offlinePushInfo : null,
|
||||||
ext: jsonEncode({"userID": myInfo.userID.value, "title": myInfo.nickname.value}),
|
|
||||||
),
|
|
||||||
cloudCustomData: cloudCustomData,
|
cloudCustomData: cloudCustomData,
|
||||||
localCustomData: "",
|
localCustomData: "",
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
// 群聊
|
// 群聊
|
||||||
|
OfflinePushInfo offlinePushInfo = OfflinePushInfo(
|
||||||
|
title: groupName,
|
||||||
|
desc: parseMessageSummary(msg),
|
||||||
|
ext: jsonEncode({"groupID": groupID, "title": groupName ?? ''}),
|
||||||
|
);
|
||||||
sendRes = await TencentImSDKPlugin.v2TIMManager.getMessageManager().sendMessage(
|
sendRes = await TencentImSDKPlugin.v2TIMManager.getMessageManager().sendMessage(
|
||||||
message: msg,
|
message: msg,
|
||||||
receiver: "",
|
receiver: "",
|
||||||
groupID: groupID!,
|
groupID: groupID!,
|
||||||
|
// onSyncMsgID: (msgID) async {
|
||||||
|
// 这里立刻拿到消息ID,可以提前把这条消息展示到列表中(发送中状态)有时间再改吧
|
||||||
|
// 根据类型,创建对应的elem;
|
||||||
|
// logger.w(msg.imageElem!.toLogString());
|
||||||
|
// controller.chatList.add(msg.imageElem);
|
||||||
|
// controller.scrollToBottom();
|
||||||
|
// },
|
||||||
priority: MessagePriorityEnum.V2TIM_PRIORITY_DEFAULT,
|
priority: MessagePriorityEnum.V2TIM_PRIORITY_DEFAULT,
|
||||||
onlineUserOnly: false,
|
onlineUserOnly: false,
|
||||||
isExcludedFromUnreadCount: false,
|
isExcludedFromUnreadCount: isExcludedFromUnreadCount,
|
||||||
isExcludedFromLastMessage: false,
|
isExcludedFromLastMessage: false,
|
||||||
|
isSupportMessageExtension: false,
|
||||||
|
isExcludedFromContentModeration: false,
|
||||||
needReadReceipt: false,
|
needReadReceipt: false,
|
||||||
offlinePushInfo: OfflinePushInfo(title: '群聊消息', desc: parseMessageSummary(msg)),
|
offlinePushInfo: isPush ? offlinePushInfo : null,
|
||||||
cloudCustomData: "",
|
cloudCustomData: cloudCustomData,
|
||||||
localCustomData: "",
|
localCustomData: "",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -12,6 +12,8 @@ import 'package:loopin/utils/notification_banner.dart';
|
|||||||
import 'package:shirne_dialog/shirne_dialog.dart';
|
import 'package:shirne_dialog/shirne_dialog.dart';
|
||||||
import 'package:tencent_cloud_chat_sdk/enum/V2TimAdvancedMsgListener.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/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.dart';
|
||||||
import 'package:tencent_cloud_chat_sdk/models/v2_tim_message_receipt.dart';
|
import 'package:tencent_cloud_chat_sdk/models/v2_tim_message_receipt.dart';
|
||||||
import 'package:tencent_cloud_chat_sdk/tencent_im_sdk_plugin.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 {
|
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 isGroup = message.groupID != null && message.groupID!.isNotEmpty;
|
||||||
final conversationID = isGroup ? 'group_${message.groupID}' : 'c2c_${message.userID}';
|
final conversationID = isGroup ? 'group_${message.groupID}' : 'c2c_${message.userID}';
|
||||||
if (id.isEmpty) return;
|
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>();
|
final chatDetailController = Get.find<ChatDetailController>();
|
||||||
// 单聊和群聊的处理(chatDetailController.id是通过chat_binding传入的,按顺序取userID,没有则取groupID)
|
// 单聊和群聊的处理(chatDetailController.id是通过chat_binding传入的,按顺序取userID,没有则取groupID)
|
||||||
if (chatDetailController.id == id) {
|
if (chatDetailController.id == id) {
|
||||||
@ -88,6 +91,13 @@ class ImMessageListenerService extends GetxService {
|
|||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
//如果是tips类型的
|
||||||
|
if (message.elemType == MessageElemType.V2TIM_ELEM_TYPE_GROUP_TIPS) {
|
||||||
|
// 如果是群tips 不展示消息提示
|
||||||
|
await ImService.instance.clearConversationUnreadCount(conversationID: conversationID);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// 未杀死状态,交给在线推送处理
|
// 未杀死状态,交给在线推送处理
|
||||||
if (!LifecycleHandler.isInForeground) {
|
if (!LifecycleHandler.isInForeground) {
|
||||||
logger.i("App 当前在后台,收到来自 $id 的消息 ${message.msgID},暂不展示弹窗");
|
logger.i("App 当前在后台,收到来自 $id 的消息 ${message.msgID},暂不展示弹窗");
|
||||||
@ -97,9 +107,27 @@ class ImMessageListenerService extends GetxService {
|
|||||||
//TODO 等消息发送写完以后处理消息免打扰过滤,实现:发送消息时依赖cloudCustomData,根据conversation设置的opty属性是否开启了过滤,给customdata赋值
|
//TODO 等消息发送写完以后处理消息免打扰过滤,实现:发送消息时依赖cloudCustomData,根据conversation设置的opty属性是否开启了过滤,给customdata赋值
|
||||||
//TODO 群消息
|
//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?.cancel(); // 每次都取消旧的
|
||||||
_debounceTimer = Timer(Duration(milliseconds: 1000), () {
|
_debounceTimer = Timer(Duration(milliseconds: 1000), () {
|
||||||
NotificationBanner.show(message);
|
NotificationBanner.show(message, isGroup);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -156,8 +184,7 @@ class ImMessageListenerService extends GetxService {
|
|||||||
onRecvMessageModified: (V2TimMessage message) {
|
onRecvMessageModified: (V2TimMessage message) {
|
||||||
logger.i("消息被修改: ${message.msgID}");
|
logger.i("消息被修改: ${message.msgID}");
|
||||||
// 目前就红包领取状态的变更
|
// 目前就红包领取状态的变更
|
||||||
if ((Get.currentRoute == '/chat' || Get.currentRoute == '/chatNoFriend' || Get.currentRoute == '/chatGroup') &&
|
if (Get.isRegistered<ChatDetailController>()) {
|
||||||
Get.isRegistered<ChatDetailController>()) {
|
|
||||||
final controller = Get.find<ChatDetailController>();
|
final controller = Get.find<ChatDetailController>();
|
||||||
final index = controller.chatList.indexWhere((m) => m.msgID == message.msgID);
|
final index = controller.chatList.indexWhere((m) => m.msgID == message.msgID);
|
||||||
if (index != -1) {
|
if (index != -1) {
|
||||||
|
@ -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_filter_enum.dart';
|
||||||
import 'package:tencent_cloud_chat_sdk/enum/group_member_role_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/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/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_callback.dart';
|
||||||
import 'package:tencent_cloud_chat_sdk/models/v2_tim_conversation.dart';
|
import 'package:tencent_cloud_chat_sdk/models/v2_tim_conversation.dart';
|
||||||
@ -749,6 +750,30 @@ class ImService {
|
|||||||
return ImResult.wrap(res);
|
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({
|
Future<ImResult<String>> createGroup({
|
||||||
@ -839,20 +864,17 @@ class ImService {
|
|||||||
/// - V2TIM_GROUP_MEMBER_FILTER_COMMON:普通群成员
|
/// - V2TIM_GROUP_MEMBER_FILTER_COMMON:普通群成员
|
||||||
/// [nextSeq] 分页拉取标志,首次传 0
|
/// [nextSeq] 分页拉取标志,首次传 0
|
||||||
/// [count] 拉取数量,最大 100
|
/// [count] 拉取数量,最大 100
|
||||||
/// [offset] 偏移量(仅 Web 端需要)
|
|
||||||
Future<ImResult<V2TimGroupMemberInfoResult>> getGroupMemberList({
|
Future<ImResult<V2TimGroupMemberInfoResult>> getGroupMemberList({
|
||||||
required String groupID,
|
required String groupID,
|
||||||
required GroupMemberFilterTypeEnum filter,
|
required GroupMemberFilterTypeEnum filter,
|
||||||
required String nextSeq,
|
required String nextSeq,
|
||||||
int count = 20,
|
int count = 20,
|
||||||
int offset = 0,
|
|
||||||
}) async {
|
}) async {
|
||||||
final res = await V2TIMGroupManager().getGroupMemberList(
|
final res = await V2TIMGroupManager().getGroupMemberList(
|
||||||
groupID: groupID,
|
groupID: groupID,
|
||||||
filter: filter,
|
filter: filter,
|
||||||
nextSeq: nextSeq,
|
nextSeq: nextSeq,
|
||||||
count: count,
|
count: count,
|
||||||
offset: offset,
|
|
||||||
);
|
);
|
||||||
return ImResult.wrap(res);
|
return ImResult.wrap(res);
|
||||||
}
|
}
|
||||||
@ -893,7 +915,7 @@ class ImService {
|
|||||||
return ImResult.wrapNoData(res);
|
return ImResult.wrapNoData(res);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 邀请他人入群(work群的入群方式,任何人可邀请人入群,无法做权限设置)
|
/// 邀请他人入群(work群的入群方式,任何人可邀请人入群)
|
||||||
///
|
///
|
||||||
/// [groupID] 群 ID
|
/// [groupID] 群 ID
|
||||||
/// [userList] 被邀请成员的 userID 列表
|
/// [userList] 被邀请成员的 userID 列表
|
||||||
|
@ -135,7 +135,7 @@ class PushService {
|
|||||||
logger.w(router);
|
logger.w(router);
|
||||||
if (router == null || router != '') {
|
if (router == null || router != '') {
|
||||||
// 聊天
|
// 聊天
|
||||||
if (data['userID'] != null || data['userID'] != '') {
|
if (data['userID'] != null) {
|
||||||
logger.w('有userID');
|
logger.w('有userID');
|
||||||
// 单聊,获取会话
|
// 单聊,获取会话
|
||||||
final covRes = await ImService.instance.getConversation(conversationID: 'c2c_${data['userID']}');
|
final covRes = await ImService.instance.getConversation(conversationID: 'c2c_${data['userID']}');
|
||||||
@ -151,13 +151,15 @@ class PushService {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
logger.w('没有userID');
|
logger.w('没有userID');
|
||||||
|
|
||||||
// 群聊消息
|
// 群聊消息
|
||||||
final groupRes = await ImService.instance.getConversation(conversationID: 'group_${data['groupID']}');
|
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 {
|
} else {
|
||||||
// 通知类相关
|
// 通知类相关(这里的ID只在特定业务场景才有,比如用户发起退款,要推送给商家,这里的id对应的就是订单id)
|
||||||
Get.toNamed('/$router', arguments: data['id'] ?? '');
|
Get.toNamed('/$router', arguments: data['id'] ?? '');
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
27
lib/components/empty_tip.dart
Normal file
27
lib/components/empty_tip.dart
Normal 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),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
68
lib/components/my_confirm.dart
Normal file
68
lib/components/my_confirm.dart
Normal 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, // 点击外部不可关闭
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -32,7 +32,7 @@ class NetworkOrAssetImage extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
// 显示占位
|
// 显示占位
|
||||||
return Image.asset(
|
return Image.asset(
|
||||||
placeholderAsset,
|
placeholderAsset.isEmpty ? 'assets/images/avatar/default.png' : placeholderAsset,
|
||||||
width: width,
|
width: width,
|
||||||
height: height,
|
height: height,
|
||||||
fit: fit,
|
fit: fit,
|
||||||
|
@ -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
|
// 文本消息模板=1
|
||||||
else if (item.elemType == 1) {
|
else if (item.elemType == 1 && item.cloudCustomData != 'tips') {
|
||||||
msgtpl.add(
|
msgtpl.add(
|
||||||
RenderChatItem(
|
RenderChatItem(
|
||||||
data: item,
|
data: item,
|
||||||
|
@ -15,7 +15,10 @@ import 'package:loopin/components/image_viewer.dart';
|
|||||||
import 'package:loopin/components/network_or_asset_image.dart';
|
import 'package:loopin/components/network_or_asset_image.dart';
|
||||||
import 'package:loopin/components/preview_video.dart';
|
import 'package:loopin/components/preview_video.dart';
|
||||||
import 'package:loopin/models/summary_type.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/audio_player_service.dart';
|
||||||
|
import 'package:loopin/utils/parse_message_summary.dart';
|
||||||
import 'package:loopin/utils/snapshot.dart';
|
import 'package:loopin/utils/snapshot.dart';
|
||||||
import 'package:loopin/utils/voice_service.dart';
|
import 'package:loopin/utils/voice_service.dart';
|
||||||
import 'package:mime/mime.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:video_player/video_player.dart';
|
||||||
import 'package:wechat_assets_picker/wechat_assets_picker.dart';
|
import 'package:wechat_assets_picker/wechat_assets_picker.dart';
|
||||||
|
|
||||||
import '../../styles/index.dart';
|
|
||||||
import '../../utils/index.dart';
|
import '../../utils/index.dart';
|
||||||
import './components/redpacket.dart';
|
import './components/redpacket.dart';
|
||||||
import './components/richtext.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);
|
animTurns = Tween<double>(begin: 0, end: 3.1415926).animate(animController);
|
||||||
|
|
||||||
|
isInGroup();
|
||||||
|
|
||||||
cleanUnRead();
|
cleanUnRead();
|
||||||
|
|
||||||
getUserId();
|
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
|
// 文本消息模板=1
|
||||||
else if (item.elemType == 1) {
|
else if (item.elemType == 1 && item.cloudCustomData != 'tips') {
|
||||||
msgtpl.add(
|
msgtpl.add(
|
||||||
RenderChatItem(
|
RenderChatItem(
|
||||||
data: item,
|
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;
|
return msgtpl;
|
||||||
}
|
}
|
||||||
@ -1017,7 +1055,7 @@ class _ChatGroupState extends State<ChatGroup> with SingleTickerProviderStateMix
|
|||||||
late final ImResult res;
|
late final ImResult res;
|
||||||
res = await IMMessage().sendMessage(
|
res = await IMMessage().sendMessage(
|
||||||
msg: message,
|
msg: message,
|
||||||
toUserID: arguments.groupID,
|
groupID: arguments.groupID,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (res.success && res.data != null) {
|
if (res.success && res.data != null) {
|
||||||
@ -1028,9 +1066,9 @@ class _ChatGroupState extends State<ChatGroup> with SingleTickerProviderStateMix
|
|||||||
// controller.chatList.addAll(messagesToInsert);
|
// controller.chatList.addAll(messagesToInsert);
|
||||||
|
|
||||||
controller.scrollToBottom();
|
controller.scrollToBottom();
|
||||||
print('发送成功');
|
logger.w('发送成功');
|
||||||
} else {
|
} 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 {
|
Future<void> showPicker(BuildContext context) async {
|
||||||
showModalBottomSheet(
|
showModalBottomSheet(
|
||||||
context: context,
|
context: context,
|
||||||
backgroundColor: Colors.white, // 透明底色,这样圆角才能生效
|
backgroundColor: Colors.white,
|
||||||
|
|
||||||
builder: (context) {
|
builder: (context) {
|
||||||
return SafeArea(
|
return SafeArea(
|
||||||
child: Column(
|
child: Column(
|
||||||
@ -1532,7 +1569,7 @@ class _ChatGroupState extends State<ChatGroup> with SingleTickerProviderStateMix
|
|||||||
context: context,
|
context: context,
|
||||||
builder: (context) {
|
builder: (context) {
|
||||||
return RedPacket(
|
return RedPacket(
|
||||||
flag: false,
|
flag: true,
|
||||||
onSend: (date) {
|
onSend: (date) {
|
||||||
sendHongbao(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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
//editorFocusNode
|
||||||
return Stack(
|
return Stack(
|
||||||
fit: StackFit.expand,
|
fit: StackFit.expand,
|
||||||
children: [
|
children: [
|
||||||
@ -1581,113 +1631,26 @@ class _ChatGroupState extends State<ChatGroup> with SingleTickerProviderStateMix
|
|||||||
)),
|
)),
|
||||||
),
|
),
|
||||||
actions: [
|
actions: [
|
||||||
IconButton(
|
Obx(
|
||||||
icon: const Icon(
|
() => controller.toolFlag.value
|
||||||
Icons.more_horiz,
|
? IconButton(
|
||||||
color: Colors.white,
|
icon: const Icon(
|
||||||
),
|
Icons.menu,
|
||||||
onPressed: () async {
|
color: Colors.white,
|
||||||
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),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
),
|
onPressed: () async {
|
||||||
PopupMenuItem<String>(
|
editorFocusNode.unfocus();
|
||||||
value: 'not',
|
final ctl = Get.put(GroupDetailController(groupID: arguments.groupID ?? ''));
|
||||||
child: Row(
|
await ctl.init();
|
||||||
children: [
|
final groupName = await Get.to(() => Groupdetail());
|
||||||
Icon(Icons.do_not_disturb_on, color: Colors.white, size: 18),
|
if (groupName is String && groupName.isNotEmpty) {
|
||||||
SizedBox(width: 8),
|
setState(() {
|
||||||
Text(
|
arguments.showName = groupName;
|
||||||
'设为免打扰',
|
});
|
||||||
style: TextStyle(color: Colors.white),
|
}
|
||||||
),
|
},
|
||||||
],
|
)
|
||||||
),
|
: SizedBox(),
|
||||||
),
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@ -1744,6 +1707,7 @@ class _ChatGroupState extends State<ChatGroup> with SingleTickerProviderStateMix
|
|||||||
padding: const EdgeInsets.all(10.0),
|
padding: const EdgeInsets.all(10.0),
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
|
// 语音按钮
|
||||||
InkWell(
|
InkWell(
|
||||||
child: Icon(
|
child: Icon(
|
||||||
voiceBtnEnable ? Icons.keyboard_outlined : Icons.contactless_outlined,
|
voiceBtnEnable ? Icons.keyboard_outlined : Icons.contactless_outlined,
|
||||||
@ -1751,16 +1715,22 @@ class _ChatGroupState extends State<ChatGroup> with SingleTickerProviderStateMix
|
|||||||
size: 30.0,
|
size: 30.0,
|
||||||
),
|
),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
setState(() {
|
logger.w('点击了语音按钮');
|
||||||
toolbarEnable = false;
|
if (controller.toolFlag.value) {
|
||||||
if (voiceBtnEnable) {
|
setState(() {
|
||||||
voiceBtnEnable = false;
|
toolbarEnable = false;
|
||||||
editorFocusNode.requestFocus();
|
if (voiceBtnEnable) {
|
||||||
} else {
|
voiceBtnEnable = false;
|
||||||
voiceBtnEnable = true;
|
editorFocusNode.requestFocus();
|
||||||
editorFocusNode.unfocus();
|
// editorFocusNode.unfocus();
|
||||||
}
|
} else {
|
||||||
});
|
voiceBtnEnable = true;
|
||||||
|
editorFocusNode.unfocus();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
logger.e('退出群聊无法使用');
|
||||||
|
}
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
const SizedBox(
|
const SizedBox(
|
||||||
@ -1778,21 +1748,25 @@ class _ChatGroupState extends State<ChatGroup> with SingleTickerProviderStateMix
|
|||||||
// 输入框
|
// 输入框
|
||||||
Offstage(
|
Offstage(
|
||||||
offstage: voiceBtnEnable,
|
offstage: voiceBtnEnable,
|
||||||
child: TextField(
|
child: Obx(
|
||||||
decoration: const InputDecoration(
|
() => TextField(
|
||||||
isDense: true,
|
enabled: controller.toolFlag.value, //true=禁止
|
||||||
hoverColor: Colors.transparent,
|
decoration: InputDecoration(
|
||||||
contentPadding: EdgeInsets.all(8.0),
|
hintText: controller.toolFlag.value ? null : "你已退出群聊",
|
||||||
border: OutlineInputBorder(borderSide: BorderSide.none),
|
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(
|
const SizedBox(
|
||||||
width: 10.0,
|
width: 10.0,
|
||||||
),
|
),
|
||||||
|
// 表情
|
||||||
InkWell(
|
InkWell(
|
||||||
child: const Icon(
|
child: const Icon(
|
||||||
Icons.add_reaction_rounded,
|
Icons.add_reaction_rounded,
|
||||||
@ -1877,12 +1852,17 @@ class _ChatGroupState extends State<ChatGroup> with SingleTickerProviderStateMix
|
|||||||
size: 30.0,
|
size: 30.0,
|
||||||
),
|
),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
handleEmojChooseState(0);
|
if (controller.toolFlag.value) {
|
||||||
|
handleEmojChooseState(0);
|
||||||
|
} else {
|
||||||
|
logger.w('退出群聊禁止使用');
|
||||||
|
}
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
const SizedBox(
|
const SizedBox(
|
||||||
width: 8.0,
|
width: 8.0,
|
||||||
),
|
),
|
||||||
|
// +号
|
||||||
InkWell(
|
InkWell(
|
||||||
child: const Icon(
|
child: const Icon(
|
||||||
Icons.add,
|
Icons.add,
|
||||||
@ -1890,12 +1870,17 @@ class _ChatGroupState extends State<ChatGroup> with SingleTickerProviderStateMix
|
|||||||
size: 30.0,
|
size: 30.0,
|
||||||
),
|
),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
handleEmojChooseState(1);
|
if (controller.toolFlag.value) {
|
||||||
|
handleEmojChooseState(1);
|
||||||
|
} else {
|
||||||
|
logger.w('退出群聊禁止使用');
|
||||||
|
}
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
const SizedBox(
|
const SizedBox(
|
||||||
width: 8.0,
|
width: 8.0,
|
||||||
),
|
),
|
||||||
|
// 发送按钮
|
||||||
InkWell(
|
InkWell(
|
||||||
child: Container(
|
child: Container(
|
||||||
height: 25.0,
|
height: 25.0,
|
||||||
@ -1911,7 +1896,11 @@ class _ChatGroupState extends State<ChatGroup> with SingleTickerProviderStateMix
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
handleSubmit();
|
if (controller.toolFlag.value) {
|
||||||
|
handleSubmit();
|
||||||
|
} else {
|
||||||
|
logger.w('退出群聊禁止使用');
|
||||||
|
}
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@ -2150,10 +2139,10 @@ class _ChatGroupState extends State<ChatGroup> with SingleTickerProviderStateMix
|
|||||||
child: Visibility(
|
child: Visibility(
|
||||||
visible: !voiceToTransfer,
|
visible: !voiceToTransfer,
|
||||||
child: Align(
|
child: Align(
|
||||||
child: Text(
|
child: Obx(() => Text(
|
||||||
voiceTypeMap[voiceType],
|
controller.toolFlag.value ? voiceTypeMap[voiceType] : '无法在已退出的群聊发送消息',
|
||||||
style: const TextStyle(color: Colors.white70),
|
style: const TextStyle(color: Colors.white70),
|
||||||
),
|
)),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
/// 聊天模板
|
/// 聊天模板
|
||||||
library;
|
library;
|
||||||
|
|
||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:loopin/IM/controller/chat_controller.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_message.dart';
|
||||||
import 'package:loopin/IM/im_result.dart';
|
import 'package:loopin/IM/im_result.dart';
|
||||||
import 'package:loopin/IM/im_service.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/network_or_asset_image.dart';
|
||||||
import 'package:loopin/components/preview_video.dart';
|
import 'package:loopin/components/preview_video.dart';
|
||||||
import 'package:loopin/models/conversation_type.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/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:loopin/utils/snapshot.dart';
|
||||||
import 'package:shirne_dialog/shirne_dialog.dart';
|
import 'package:shirne_dialog/shirne_dialog.dart';
|
||||||
import 'package:tencent_cloud_chat_sdk/models/v2_tim_conversation.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
|
// 文本消息模板=1
|
||||||
else if (item.elemType == 1) {
|
else if (item.elemType == 1 && item.cloudCustomData != 'tips') {
|
||||||
msgtpl.add(
|
msgtpl.add(
|
||||||
RenderChatItem(
|
RenderChatItem(
|
||||||
data: item,
|
data: item,
|
||||||
@ -311,9 +333,9 @@ class _ChatNoFriendState extends State<ChatNoFriend> with SingleTickerProviderSt
|
|||||||
padding: const EdgeInsets.all(10.0),
|
padding: const EdgeInsets.all(10.0),
|
||||||
child: RichTextUtil.getRichText(item.textElem?.text ?? '', color: !(item.isSelf ?? false) ? Colors.black : Colors.white), // 可自定义解析emoj/网址/电话
|
child: RichTextUtil.getRichText(item.textElem?.text ?? '', color: !(item.isSelf ?? false) ? Colors.black : Colors.white), // 可自定义解析emoj/网址/电话
|
||||||
),
|
),
|
||||||
// onLongPress: () {
|
onLongPress: () {
|
||||||
// contextMenuDialog();
|
contextMenuDialog();
|
||||||
// },
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -350,6 +372,13 @@ class _ChatNoFriendState extends State<ChatNoFriend> with SingleTickerProviderSt
|
|||||||
data: item,
|
data: item,
|
||||||
child: Ink(
|
child: Ink(
|
||||||
child: InkWell(
|
child: InkWell(
|
||||||
|
onTap: () {
|
||||||
|
// 预览图片
|
||||||
|
Get.to(() => ImageViewer(
|
||||||
|
images: [imagePaths.first],
|
||||||
|
index: 0,
|
||||||
|
));
|
||||||
|
},
|
||||||
overlayColor: WidgetStateProperty.all(Colors.transparent),
|
overlayColor: WidgetStateProperty.all(Colors.transparent),
|
||||||
child: ClipRRect(
|
child: ClipRRect(
|
||||||
borderRadius: BorderRadius.circular(10.0),
|
borderRadius: BorderRadius.circular(10.0),
|
||||||
@ -407,17 +436,9 @@ class _ChatNoFriendState extends State<ChatNoFriend> with SingleTickerProviderSt
|
|||||||
children: [
|
children: [
|
||||||
ClipRRect(
|
ClipRRect(
|
||||||
borderRadius: BorderRadius.circular(10.0),
|
borderRadius: BorderRadius.circular(10.0),
|
||||||
child: Image.network(
|
child: NetworkOrAssetImage(
|
||||||
fit: BoxFit.cover,
|
imageUrl: item.videoElem?.snapshotUrl ?? '',
|
||||||
item.videoElem?.snapshotUrl ?? '',
|
width: 120,
|
||||||
errorBuilder: (context, error, stackTrace) {
|
|
||||||
return Image.asset(
|
|
||||||
'assets/images/pic1.jpg',
|
|
||||||
height: 60.0,
|
|
||||||
width: 60.0,
|
|
||||||
fit: BoxFit.cover,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const Align(
|
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: () {
|
onTap: () {
|
||||||
showGeneralDialog(
|
showGeneralDialog(
|
||||||
context: context,
|
context: context,
|
||||||
@ -463,6 +481,10 @@ class _ChatNoFriendState extends State<ChatNoFriend> with SingleTickerProviderSt
|
|||||||
}
|
}
|
||||||
// 语音模板=4
|
// 语音模板=4
|
||||||
else if (item.elemType == 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 = [
|
List<Widget> audiobody = [
|
||||||
Ink(
|
Ink(
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
@ -475,8 +497,8 @@ class _ChatNoFriendState extends State<ChatNoFriend> with SingleTickerProviderSt
|
|||||||
child: Container(
|
child: Container(
|
||||||
padding: const EdgeInsets.all(10.0),
|
padding: const EdgeInsets.all(10.0),
|
||||||
constraints: BoxConstraints(
|
constraints: BoxConstraints(
|
||||||
// maxWidth: 120.0,
|
maxWidth: maxWidth,
|
||||||
maxWidth: (item.soundElem?.duration)! / 60 * 230,
|
// maxWidth: (item.soundElem!.duration! / 1000) / 60 * 230,
|
||||||
),
|
),
|
||||||
child: Row(
|
child: Row(
|
||||||
mainAxisAlignment: !(item.isSelf ?? false) ? MainAxisAlignment.start : MainAxisAlignment.end,
|
mainAxisAlignment: !(item.isSelf ?? false) ? MainAxisAlignment.start : MainAxisAlignment.end,
|
||||||
@ -486,10 +508,10 @@ class _ChatNoFriendState extends State<ChatNoFriend> with SingleTickerProviderSt
|
|||||||
const SizedBox(
|
const SizedBox(
|
||||||
width: 5.0,
|
width: 5.0,
|
||||||
),
|
),
|
||||||
Text('${item.soundElem?.duration}'),
|
Text('$durationSeconds"'),
|
||||||
]
|
]
|
||||||
: [
|
: [
|
||||||
Text('${item.soundElem?.duration}'),
|
Text('$durationSeconds"'),
|
||||||
const SizedBox(
|
const SizedBox(
|
||||||
width: 5.0,
|
width: 5.0,
|
||||||
),
|
),
|
||||||
@ -498,7 +520,15 @@ class _ChatNoFriendState extends State<ChatNoFriend> with SingleTickerProviderSt
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
onTap: () {
|
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: () {
|
onLongPress: () {
|
||||||
contextMenuDialog();
|
contextMenuDialog();
|
||||||
@ -508,7 +538,8 @@ class _ChatNoFriendState extends State<ChatNoFriend> with SingleTickerProviderSt
|
|||||||
const SizedBox(
|
const SizedBox(
|
||||||
width: 5.0,
|
width: 5.0,
|
||||||
),
|
),
|
||||||
FStyle.badge(0, isdot: true),
|
|
||||||
|
// FStyle.badge(0, isdot: true),
|
||||||
];
|
];
|
||||||
|
|
||||||
if (item.isSelf ?? false) {
|
if (item.isSelf ?? false) {
|
||||||
@ -525,8 +556,177 @@ class _ChatNoFriendState extends State<ChatNoFriend> with SingleTickerProviderSt
|
|||||||
children: audiobody,
|
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;
|
// 红包模板=自定义=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(
|
msgtpl.add(RenderChatItem(
|
||||||
data: item,
|
data: item,
|
||||||
child: Ink(
|
child: Ink(
|
||||||
@ -545,16 +745,26 @@ class _ChatNoFriendState extends State<ChatNoFriend> with SingleTickerProviderSt
|
|||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Container(
|
Container(
|
||||||
|
width: 210.0,
|
||||||
padding: const EdgeInsets.all(10.0),
|
padding: const EdgeInsets.all(10.0),
|
||||||
child: Row(
|
child: Row(
|
||||||
spacing: 10.0,
|
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
Image.asset(
|
open
|
||||||
'assets/images/hbico.png',
|
? Icon(Icons.check_circle, size: 32.0, color: Colors.white70)
|
||||||
width: 32.0,
|
: Image.asset(
|
||||||
fit: BoxFit.contain,
|
'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,
|
width: double.infinity,
|
||||||
decoration: const BoxDecoration(border: Border(top: BorderSide(color: Colors.white30, width: .5))),
|
decoration: const BoxDecoration(border: Border(top: BorderSide(color: Colors.white30, width: .5))),
|
||||||
child: const Text(
|
child: const Text(
|
||||||
'拼手气红包',
|
'红包',
|
||||||
style: TextStyle(color: Colors.white70, fontSize: 11.0),
|
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;
|
return msgtpl;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1517,6 +1723,15 @@ class _ChatNoFriendState extends State<ChatNoFriend> with SingleTickerProviderSt
|
|||||||
ctl.removeNoFriend(conversationID: arguments.value.conversationID);
|
ctl.removeNoFriend(conversationID: arguments.value.conversationID);
|
||||||
ctl.updateNoFriendMenu();
|
ctl.updateNoFriendMenu();
|
||||||
}
|
}
|
||||||
|
// 跳转到chat地址,销毁当前页面
|
||||||
|
Get.offAllNamed(
|
||||||
|
'/chat',
|
||||||
|
arguments: arguments.value,
|
||||||
|
predicate: (route) {
|
||||||
|
// 清理栈,只保留 `/`
|
||||||
|
return route.settings.name == '/';
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
child: const Text('回关', style: TextStyle(color: Colors.white)),
|
child: const Text('回关', style: TextStyle(color: Colors.white)),
|
||||||
|
@ -15,6 +15,7 @@ import 'package:loopin/utils/index.dart';
|
|||||||
import 'package:loopin/utils/scan_code_type.dart'; // 导入外部枚举
|
import 'package:loopin/utils/scan_code_type.dart'; // 导入外部枚举
|
||||||
import 'package:loopin/utils/parse_message_summary.dart';
|
import 'package:loopin/utils/parse_message_summary.dart';
|
||||||
import 'package:shirne_dialog/shirne_dialog.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 '../../behavior/custom_scroll_behavior.dart';
|
||||||
import '../../styles/index.dart';
|
import '../../styles/index.dart';
|
||||||
@ -394,10 +395,14 @@ class ChatPageState extends State<ChatPage> {
|
|||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
// logger.w(chatList[index].conversation.conversationGroupList);
|
// logger.w(chatList[index].conversation.conversationGroupList);
|
||||||
// logger.w(chatList[index].isCustomAdmin);
|
// 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 isNoFriend = chatList[index].conversation.conversationGroupList?.contains(ConversationType.noFriend.name) ?? false;
|
||||||
final isAdmin =
|
final isAdmin =
|
||||||
chatList[index].isCustomAdmin != null && (chatList[index].isCustomAdmin?.isNotEmpty ?? false) && chatList[index].isCustomAdmin != '0';
|
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);
|
// logger.e(chatList[index].isCustomAdmin);
|
||||||
return Ink(
|
return Ink(
|
||||||
// color: chatList[index]['topMost'] == null ? Colors.white : Colors.grey[100], //置顶颜色
|
// color: chatList[index]['topMost'] == null ? Colors.white : Colors.grey[100], //置顶颜色
|
||||||
@ -415,6 +420,7 @@ class ChatPageState extends State<ChatPage> {
|
|||||||
imageUrl: chatList[index].faceUrl,
|
imageUrl: chatList[index].faceUrl,
|
||||||
width: 50,
|
width: 50,
|
||||||
height: 50,
|
height: 50,
|
||||||
|
placeholderAsset: placeholderAsset,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
@ -423,17 +429,24 @@ class ChatPageState extends State<ChatPage> {
|
|||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
|
// 昵称
|
||||||
Text(
|
Text(
|
||||||
chatList[index].conversation.showName ?? '未知',
|
chatList[index].conversation.showName ?? '未知',
|
||||||
|
maxLines: 1,
|
||||||
|
softWrap: false,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: (isAdmin || isNoFriend) ? 20 : 16,
|
fontSize: (isAdmin || isNoFriend) ? 20 : 16,
|
||||||
fontWeight: (isAdmin || isNoFriend) ? FontWeight.bold : FontWeight.normal),
|
fontWeight: (isAdmin || isNoFriend) ? FontWeight.bold : FontWeight.normal),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 2.0),
|
const SizedBox(height: 2.0),
|
||||||
|
// 消息内容
|
||||||
Text(
|
Text(
|
||||||
chatList[index].conversation.lastMessage != null ? parseMessageSummary(chatList[index].conversation.lastMessage!) : '',
|
chatList[index].conversation.lastMessage != null ? parseMessageSummary(chatList[index].conversation.lastMessage!) : '',
|
||||||
style: const TextStyle(color: Colors.grey, fontSize: 13.0),
|
style: const TextStyle(color: Colors.grey, fontSize: 13.0),
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
|
maxLines: 1,
|
||||||
|
softWrap: false,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@ -456,9 +469,22 @@ class ChatPageState extends State<ChatPage> {
|
|||||||
),
|
),
|
||||||
const SizedBox(height: 5.0),
|
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;
|
||||||
|
// }
|
||||||
|
// 现阶段只允许设置为0和3
|
||||||
Visibility(
|
Visibility(
|
||||||
visible: (chatList[index].conversation.unreadCount ?? 0) > 0,
|
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);
|
logger.e(chatList[index].conversation.conversationGroupList);
|
||||||
Get.toNamed('/noFriend');
|
Get.toNamed('/noFriend');
|
||||||
|
} else if (chatList[index].conversation.type == 2) {
|
||||||
|
// 跳转群聊 type=0非法,1=单聊,2=群聊
|
||||||
|
Get.toNamed('/chatGroup', arguments: chatList[index].conversation);
|
||||||
} else {
|
} else {
|
||||||
// 会话id查询会话详情
|
// 会话id查询会话详情
|
||||||
Get.toNamed('/chat', arguments: chatList[index].conversation);
|
Get.toNamed('/chat', arguments: chatList[index].conversation);
|
||||||
|
264
lib/pages/groupChat/components/invite_action_sheet.dart
Normal file
264
lib/pages/groupChat/components/invite_action_sheet.dart
Normal 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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
302
lib/pages/groupChat/components/member_action_sheet.dart
Normal file
302
lib/pages/groupChat/components/member_action_sheet.dart
Normal 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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
74
lib/pages/groupChat/components/set_group_info.dart
Normal file
74
lib/pages/groupChat/components/set_group_info.dart
Normal 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: "", // 去掉默认的计数提示
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
148
lib/pages/groupChat/controller/group_detail_controller.dart
Normal file
148
lib/pages/groupChat/controller/group_detail_controller.dart
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//群成员(这里非群主拿14个,群住拿13个)
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
541
lib/pages/groupChat/groupDetail.dart
Normal file
541
lib/pages/groupChat/groupDetail.dart
Normal 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, // 暂时只提供0和3
|
||||||
|
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),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -159,6 +159,7 @@ class GrouplistState extends State<Grouplist> with SingleTickerProviderStateMixi
|
|||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
|
// 群名称
|
||||||
Text(
|
Text(
|
||||||
item.groupName?.isNotEmpty == true ? item.groupName! : '群聊',
|
item.groupName?.isNotEmpty == true ? item.groupName! : '群聊',
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
@ -166,6 +167,7 @@ class GrouplistState extends State<Grouplist> with SingleTickerProviderStateMixi
|
|||||||
fontWeight: FontWeight.normal,
|
fontWeight: FontWeight.normal,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
// 群介绍
|
||||||
if (item.introduction?.isNotEmpty ?? false) ...[
|
if (item.introduction?.isNotEmpty ?? false) ...[
|
||||||
const SizedBox(height: 2.0),
|
const SizedBox(height: 2.0),
|
||||||
Text(
|
Text(
|
||||||
|
@ -3,14 +3,15 @@ import 'package:easy_refresh/easy_refresh.dart';
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:loopin/IM/controller/im_user_info_controller.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/IM/im_service.dart';
|
||||||
import 'package:loopin/components/network_or_asset_image.dart';
|
import 'package:loopin/components/network_or_asset_image.dart';
|
||||||
import 'package:loopin/styles/index.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_member_role_enum.dart';
|
||||||
import 'package:tencent_cloud_chat_sdk/enum/group_type.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_group_member.dart';
|
||||||
import 'package:tencent_cloud_chat_sdk/models/v2_tim_user_full_info.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 {
|
class StartGroupChatPage extends StatefulWidget {
|
||||||
const StartGroupChatPage({super.key});
|
const StartGroupChatPage({super.key});
|
||||||
@ -28,6 +29,7 @@ class _StartGroupChatPageState extends State<StartGroupChatPage> {
|
|||||||
String page = '';
|
String page = '';
|
||||||
bool hasMore = true;
|
bool hasMore = true;
|
||||||
bool isLoading = true;
|
bool isLoading = true;
|
||||||
|
bool makeGroup = true;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
@ -41,7 +43,7 @@ class _StartGroupChatPageState extends State<StartGroupChatPage> {
|
|||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 分页获取陌关注列表数据
|
// 分页获取关注列表数据
|
||||||
Future<void> _loadData({bool reset = false}) async {
|
Future<void> _loadData({bool reset = false}) async {
|
||||||
if (reset) {
|
if (reset) {
|
||||||
page = '';
|
page = '';
|
||||||
@ -97,6 +99,13 @@ class _StartGroupChatPageState extends State<StartGroupChatPage> {
|
|||||||
|
|
||||||
/// 创建群聊
|
/// 创建群聊
|
||||||
void createGroup(Set<String> selectedIds) async {
|
void createGroup(Set<String> selectedIds) async {
|
||||||
|
logger.w(makeGroup);
|
||||||
|
if (makeGroup == false) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setState(() {
|
||||||
|
makeGroup = false;
|
||||||
|
});
|
||||||
// dataList是原始数据List<V2TimUserFullInfo> dataList = [];
|
// dataList是原始数据List<V2TimUserFullInfo> dataList = [];
|
||||||
// 通过dataList和selectedIds来构建memberList
|
// 通过dataList和selectedIds来构建memberList
|
||||||
final ctl = Get.find<ImUserInfoController>();
|
final ctl = Get.find<ImUserInfoController>();
|
||||||
@ -113,13 +122,42 @@ class _StartGroupChatPageState extends State<StartGroupChatPage> {
|
|||||||
);
|
);
|
||||||
memberList.insert(0, self);
|
memberList.insert(0, self);
|
||||||
final groupName = buildGroupName(selectedIds);
|
final groupName = buildGroupName(selectedIds);
|
||||||
final res = await ImService.instance.createGroup(groupType: GroupType.Work, groupName: groupName, memberList: memberList);
|
try {
|
||||||
if (res.success) {
|
final res = await ImService.instance.createGroup(groupType: GroupType.Work, groupName: groupName, memberList: memberList);
|
||||||
final groupID = res.data;
|
if (res.success) {
|
||||||
logger.w(groupID);
|
final groupID = res.data;
|
||||||
final V2TimConversation conv = V2TimConversation(conversationID: 'group_$groupID');
|
logger.w(groupID);
|
||||||
conv.showName = groupName;
|
// final V2TimConversation conv = V2TimConversation(conversationID: 'group_$groupID');
|
||||||
Get.toNamed('/chatGroup', arguments: conv);
|
// 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(
|
body: Column(
|
||||||
children: [
|
children: [
|
||||||
// 上半部分 - 两个卡片
|
// 上半部分 - 两个卡片
|
||||||
Padding(
|
// Padding(
|
||||||
padding: const EdgeInsets.all(8.0),
|
// padding: const EdgeInsets.all(8.0),
|
||||||
child: Column(
|
// child: Column(
|
||||||
children: [
|
// children: [
|
||||||
_buildCard(
|
// _buildCard(
|
||||||
icon: Icons.public,
|
// icon: Icons.public,
|
||||||
title: "创建公开群",
|
// title: "创建公开群",
|
||||||
onTap: () {
|
// onTap: () {
|
||||||
logger.w("跳转到 创建公开群");
|
// logger.w("跳转到 创建公开群");
|
||||||
},
|
// },
|
||||||
),
|
// ),
|
||||||
// _buildCard(
|
// _buildCard(
|
||||||
// icon: Icons.group,
|
// icon: Icons.group,
|
||||||
// title: "创建好友群",
|
// title: "创建好友群",
|
||||||
// onTap: () {
|
// onTap: () {
|
||||||
// logger.w("跳转到 创建好友群");
|
// logger.w("跳转到 创建好友群");
|
||||||
// },
|
// },
|
||||||
// ),
|
// ),
|
||||||
],
|
// ],
|
||||||
),
|
// ),
|
||||||
),
|
// ),
|
||||||
|
|
||||||
const Divider(),
|
// const Divider(),
|
||||||
|
|
||||||
// 下半部分
|
// 下半部分
|
||||||
Expanded(
|
Expanded(
|
||||||
@ -318,11 +356,11 @@ class _StartGroupChatPageState extends State<StartGroupChatPage> {
|
|||||||
duration: const Duration(milliseconds: 300),
|
duration: const Duration(milliseconds: 300),
|
||||||
tween: ColorTween(
|
tween: ColorTween(
|
||||||
begin: Colors.grey,
|
begin: Colors.grey,
|
||||||
end: selectedIds.isEmpty ? Colors.grey : FStyle.primaryColor,
|
end: (selectedIds.isNotEmpty && makeGroup == true) ? FStyle.primaryColor : Colors.grey,
|
||||||
),
|
),
|
||||||
builder: (context, bgColor, _) {
|
builder: (context, bgColor, _) {
|
||||||
return ElevatedButton(
|
return ElevatedButton(
|
||||||
onPressed: selectedIds.isEmpty
|
onPressed: (selectedIds.isEmpty && makeGroup == false)
|
||||||
? null
|
? null
|
||||||
: () {
|
: () {
|
||||||
logger.w("选择了用户:$selectedIds");
|
logger.w("选择了用户:$selectedIds");
|
||||||
|
@ -7,6 +7,7 @@ import 'package:loopin/IM/controller/im_user_info_controller.dart';
|
|||||||
import 'package:loopin/IM/im_service.dart';
|
import 'package:loopin/IM/im_service.dart';
|
||||||
import 'package:loopin/api/video_api.dart';
|
import 'package:loopin/api/video_api.dart';
|
||||||
import 'package:loopin/components/custom_sticky_header.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/network_or_asset_image.dart';
|
||||||
import 'package:loopin/components/only_down_scroll_physics.dart';
|
import 'package:loopin/components/only_down_scroll_physics.dart';
|
||||||
import 'package:loopin/controller/video_module_controller.dart';
|
import 'package:loopin/controller/video_module_controller.dart';
|
||||||
@ -559,31 +560,40 @@ class MyPageState extends State<MyPage> with SingleTickerProviderStateMixin {
|
|||||||
onLongPress: () {
|
onLongPress: () {
|
||||||
showModalBottomSheet(
|
showModalBottomSheet(
|
||||||
context: Get.context!,
|
context: Get.context!,
|
||||||
backgroundColor: Colors.black.withOpacity(0.8),
|
backgroundColor: Colors.white,
|
||||||
shape: const RoundedRectangleBorder(
|
shape: const RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
|
borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
|
||||||
),
|
),
|
||||||
builder: (context) {
|
builder: (context) {
|
||||||
return Column(
|
return SafeArea(
|
||||||
mainAxisSize: MainAxisSize.min,
|
child: Column(
|
||||||
children: [
|
mainAxisSize: MainAxisSize.min,
|
||||||
ListTile(
|
children: [
|
||||||
leading: const Icon(Icons.lock, color: Colors.white),
|
// ListTile(
|
||||||
title: const Text('设为私密', style: TextStyle(color: Colors.white)),
|
// leading: const Icon(Icons.lock, color: Colors.black),
|
||||||
onTap: () {
|
// title: const Text('设为私密', style: TextStyle(color: Colors.black)),
|
||||||
Navigator.pop(context);
|
// onTap: () {
|
||||||
// TODO: 修改为私密逻辑
|
// Navigator.pop(context);
|
||||||
},
|
// // TODO: 修改为私密逻辑
|
||||||
),
|
// },
|
||||||
ListTile(
|
// ),
|
||||||
leading: const Icon(Icons.delete, color: Colors.redAccent),
|
ListTile(
|
||||||
title: const Text('删除视频', style: TextStyle(color: Colors.redAccent)),
|
leading: const Icon(Icons.delete, color: Colors.redAccent),
|
||||||
onTap: () {
|
title: const Text('删除视频', style: TextStyle(color: Colors.redAccent)),
|
||||||
Navigator.pop(context);
|
onTap: () async {
|
||||||
// TODO: 删除逻辑
|
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(
|
ClipRRect(
|
||||||
borderRadius: BorderRadius.circular(12.0),
|
borderRadius: BorderRadius.circular(12.0),
|
||||||
child: Image.network(
|
child: NetworkOrAssetImage(
|
||||||
item['cover'] ?? item['firstFrameImg'],
|
imageUrl: item['cover'] ?? item['firstFrameImg'] ?? '',
|
||||||
|
placeholderAsset: 'assets/images/bk.jpg',
|
||||||
fit: BoxFit.cover,
|
fit: BoxFit.cover,
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
height: double.infinity,
|
height: double.infinity,
|
||||||
|
@ -9,6 +9,7 @@ import 'package:loopin/components/network_or_asset_image.dart';
|
|||||||
import 'package:loopin/service/http.dart';
|
import 'package:loopin/service/http.dart';
|
||||||
import 'package:loopin/styles/index.dart';
|
import 'package:loopin/styles/index.dart';
|
||||||
import 'package:loopin/utils/index.dart';
|
import 'package:loopin/utils/index.dart';
|
||||||
|
import 'package:loopin/utils/permissions.dart';
|
||||||
import 'package:loopin/utils/wxsdk.dart';
|
import 'package:loopin/utils/wxsdk.dart';
|
||||||
import 'package:shirne_dialog/shirne_dialog.dart';
|
import 'package:shirne_dialog/shirne_dialog.dart';
|
||||||
import 'package:wechat_assets_picker/wechat_assets_picker.dart';
|
import 'package:wechat_assets_picker/wechat_assets_picker.dart';
|
||||||
@ -205,6 +206,11 @@ class _UserInfoState extends State<UserInfo> {
|
|||||||
|
|
||||||
///选背景
|
///选背景
|
||||||
void pickCover(BuildContext context) async {
|
void pickCover(BuildContext context) async {
|
||||||
|
final hasPer = await Permissions.requestPhotoPermission();
|
||||||
|
if (!hasPer) {
|
||||||
|
Permissions.showPermissionDialog();
|
||||||
|
return;
|
||||||
|
}
|
||||||
final pickedAssets = await AssetPicker.pickAssets(
|
final pickedAssets = await AssetPicker.pickAssets(
|
||||||
context,
|
context,
|
||||||
pickerConfig: AssetPickerConfig(
|
pickerConfig: AssetPickerConfig(
|
||||||
@ -226,10 +232,10 @@ class _UserInfoState extends State<UserInfo> {
|
|||||||
if (file != null) {
|
if (file != null) {
|
||||||
final fileSizeInBytes = await file.length();
|
final fileSizeInBytes = await file.length();
|
||||||
final sizeInMB = fileSizeInBytes / (1024 * 1024);
|
final sizeInMB = fileSizeInBytes / (1024 * 1024);
|
||||||
if (sizeInMB > 100) {
|
if (sizeInMB > 200) {
|
||||||
MyDialog.toast('图片大小不能超过100MB', icon: const Icon(Icons.check_circle), style: ToastStyle(backgroundColor: Colors.red.withAlpha(200)));
|
MyDialog.toast('图片大小不能超过200MB', icon: const Icon(Icons.check_circle), style: ToastStyle(backgroundColor: Colors.red.withAlpha(200)));
|
||||||
} else {
|
} else {
|
||||||
print("图片合法,大小:$sizeInMB MB");
|
print("视频合法,大小:$sizeInMB MB");
|
||||||
//走upload(file)上传图片拿到url地址
|
//走upload(file)上传图片拿到url地址
|
||||||
final istance = MyDialog.loading('上传中');
|
final istance = MyDialog.loading('上传中');
|
||||||
final res = await Http.upload(CommonApi.uploadFile, filePath: file.path);
|
final res = await Http.upload(CommonApi.uploadFile, filePath: file.path);
|
||||||
@ -244,6 +250,11 @@ class _UserInfoState extends State<UserInfo> {
|
|||||||
|
|
||||||
///选头像
|
///选头像
|
||||||
void pickFaceUrl(BuildContext context) async {
|
void pickFaceUrl(BuildContext context) async {
|
||||||
|
final hasPer = await Permissions.requestPhotoPermission();
|
||||||
|
if (!hasPer) {
|
||||||
|
Permissions.showPermissionDialog();
|
||||||
|
return;
|
||||||
|
}
|
||||||
final pickedAssets = await AssetPicker.pickAssets(
|
final pickedAssets = await AssetPicker.pickAssets(
|
||||||
context,
|
context,
|
||||||
pickerConfig: AssetPickerConfig(
|
pickerConfig: AssetPickerConfig(
|
||||||
@ -265,8 +276,8 @@ class _UserInfoState extends State<UserInfo> {
|
|||||||
if (file != null) {
|
if (file != null) {
|
||||||
final fileSizeInBytes = await file.length();
|
final fileSizeInBytes = await file.length();
|
||||||
final sizeInMB = fileSizeInBytes / (1024 * 1024);
|
final sizeInMB = fileSizeInBytes / (1024 * 1024);
|
||||||
if (sizeInMB > 100) {
|
if (sizeInMB > 20) {
|
||||||
MyDialog.toast('图片大小不能超过100MB', icon: const Icon(Icons.check_circle), style: ToastStyle(backgroundColor: Colors.red.withAlpha(200)));
|
MyDialog.toast('图片大小不能超过20MB', icon: const Icon(Icons.check_circle), style: ToastStyle(backgroundColor: Colors.red.withAlpha(200)));
|
||||||
} else {
|
} else {
|
||||||
print("图片合法,大小:$sizeInMB MB");
|
print("图片合法,大小:$sizeInMB MB");
|
||||||
//走upload(file)上传图片拿到url地址
|
//走upload(file)上传图片拿到url地址
|
||||||
|
@ -10,7 +10,9 @@ import 'package:loopin/components/image_viewer.dart';
|
|||||||
import 'package:loopin/components/preview_video.dart';
|
import 'package:loopin/components/preview_video.dart';
|
||||||
import 'package:loopin/service/http.dart';
|
import 'package:loopin/service/http.dart';
|
||||||
import 'package:loopin/utils/index.dart';
|
import 'package:loopin/utils/index.dart';
|
||||||
|
import 'package:loopin/utils/permissions.dart';
|
||||||
import 'package:loopin/utils/snapshot.dart';
|
import 'package:loopin/utils/snapshot.dart';
|
||||||
|
import 'package:shirne_dialog/shirne_dialog.dart';
|
||||||
import 'package:wechat_assets_picker/wechat_assets_picker.dart';
|
import 'package:wechat_assets_picker/wechat_assets_picker.dart';
|
||||||
|
|
||||||
class UploadVideoPage extends StatefulWidget {
|
class UploadVideoPage extends StatefulWidget {
|
||||||
@ -54,12 +56,10 @@ class _UploadVideoPageState extends State<UploadVideoPage> {
|
|||||||
|
|
||||||
Future<void> pickVideo() async {
|
Future<void> pickVideo() async {
|
||||||
descFocusNode.unfocus();
|
descFocusNode.unfocus();
|
||||||
|
final hasPer = await Permissions.requestVideoPermission();
|
||||||
|
|
||||||
final result = await PhotoManager.requestPermissionExtend();
|
if (!hasPer) {
|
||||||
|
Permissions.showPermissionDialog();
|
||||||
if (!result.isAuth) {
|
|
||||||
if (!mounted) return;
|
|
||||||
Get.snackbar('权限被拒绝', '请前往设置授权访问视频');
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -85,9 +85,20 @@ class _UploadVideoPageState extends State<UploadVideoPage> {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (pickedAssets != null && pickedAssets.isNotEmpty) {
|
if (pickedAssets != null && pickedAssets.isNotEmpty) {
|
||||||
selectedVideo.value = pickedAssets.first;
|
final asset = pickedAssets.first;
|
||||||
status.value = '已选择视频';
|
final file = await asset.file; // 获取实际文件
|
||||||
uploadVideo();
|
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 {
|
Future<void> pickCoverImage() async {
|
||||||
descFocusNode.unfocus();
|
descFocusNode.unfocus();
|
||||||
|
|
||||||
final result = await PhotoManager.requestPermissionExtend();
|
final hasPer = await Permissions.requestPhotoPermission();
|
||||||
|
if (!hasPer) {
|
||||||
if (!result.isAuth) {
|
Permissions.showPermissionDialog();
|
||||||
if (!mounted) return;
|
|
||||||
Get.snackbar('权限被拒绝', '请前往设置授权访问照片');
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -118,9 +127,19 @@ class _UploadVideoPageState extends State<UploadVideoPage> {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (pickedAssets != null && pickedAssets.isNotEmpty) {
|
if (pickedAssets != null && pickedAssets.isNotEmpty) {
|
||||||
selectedCover.value = pickedAssets.first;
|
final asset = pickedAssets.first;
|
||||||
// 执行上传图片逻辑
|
final file = await asset.file; // 获取实际文件
|
||||||
uploadImg();
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -8,8 +8,8 @@ class HttpConfig {
|
|||||||
// baseUrl: 'http://43.143.227.203:8099',
|
// baseUrl: 'http://43.143.227.203:8099',
|
||||||
// baseUrl: 'http://111.62.22.190:8080',
|
// baseUrl: 'http://111.62.22.190:8080',
|
||||||
// baseUrl: 'http://cjh.wuzhongjie.com.cn',
|
// baseUrl: 'http://cjh.wuzhongjie.com.cn',
|
||||||
// baseUrl: 'http://82.156.121.2:8880',
|
baseUrl: 'http://82.156.121.2:8880',
|
||||||
baseUrl: 'http://192.168.1.65:8880',
|
// baseUrl: 'http://192.168.1.65:8880',
|
||||||
// baseUrl: 'http://192.168.1.22:8080',
|
// baseUrl: 'http://192.168.1.22:8080',
|
||||||
|
|
||||||
// connectTimeout: Duration(seconds: 30),
|
// connectTimeout: Duration(seconds: 30),
|
||||||
|
@ -214,4 +214,15 @@ class Utils {
|
|||||||
return '未知状态';
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,31 +1,45 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:loopin/IM/im_service.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:loopin/utils/parse_message_summary.dart';
|
||||||
import 'package:shirne_dialog/shirne_dialog.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_message.dart';
|
||||||
import 'package:tencent_cloud_chat_sdk/models/v2_tim_user_full_info.dart';
|
import 'package:tencent_cloud_chat_sdk/models/v2_tim_user_full_info.dart';
|
||||||
|
|
||||||
class NotificationBanner {
|
class NotificationBanner {
|
||||||
static void show(V2TimMessage msg) {
|
static void show(V2TimMessage msg, bool isGroup) async {
|
||||||
final nickname = msg.nameCard ?? msg.nickName ?? msg.senderProfile?.nickName ?? msg.sender ?? '未知用户';
|
String name = '';
|
||||||
final avatar = msg.faceUrl ?? msg.senderProfile?.faceUrl ?? '';
|
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);
|
final text = parseMessageSummary(msg);
|
||||||
|
|
||||||
Get.snackbar(
|
Get.snackbar(
|
||||||
nickname,
|
name,
|
||||||
text,
|
text,
|
||||||
duration: const Duration(seconds: 3),
|
duration: const Duration(seconds: 3),
|
||||||
snackPosition: SnackPosition.TOP,
|
snackPosition: SnackPosition.TOP,
|
||||||
margin: const EdgeInsets.all(12),
|
margin: const EdgeInsets.all(12),
|
||||||
backgroundColor: Get.theme.cardColor,
|
backgroundColor: Get.theme.cardColor,
|
||||||
colorText: Get.theme.textTheme.bodyLarge?.color,
|
colorText: Get.theme.textTheme.bodyLarge?.color,
|
||||||
icon: avatar.isNotEmpty
|
icon: ClipOval(
|
||||||
? CircleAvatar(
|
child: NetworkOrAssetImage(
|
||||||
backgroundImage: NetworkImage(avatar),
|
imageUrl: avatar,
|
||||||
radius: 16,
|
placeholderAsset: isGroup ? 'assets/images/group.png' : 'assets/images/avatar/default.png',
|
||||||
)
|
),
|
||||||
: null,
|
),
|
||||||
onTap: (_) async {
|
onTap: (_) async {
|
||||||
// 点击后立刻关闭
|
// 点击后立刻关闭
|
||||||
Get.closeCurrentSnackbar();
|
Get.closeCurrentSnackbar();
|
||||||
@ -41,7 +55,7 @@ class NotificationBanner {
|
|||||||
// 单聊消息
|
// 单聊消息
|
||||||
Get.toNamed('/chat', arguments: cRes.data);
|
Get.toNamed('/chat', arguments: cRes.data);
|
||||||
} else if (msg.groupID != null) {
|
} else if (msg.groupID != null) {
|
||||||
Get.toNamed('/chat', arguments: cRes.data);
|
Get.toNamed('/chatGroup', arguments: cRes.data);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// 异常
|
// 异常
|
||||||
|
@ -1,11 +1,44 @@
|
|||||||
import 'dart:convert';
|
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/IM/im_service.dart';
|
||||||
import 'package:loopin/models/notify_message.type.dart';
|
import 'package:loopin/models/notify_message.type.dart';
|
||||||
import 'package:loopin/models/summary_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/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';
|
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 文档给出的是2,实际修改返回的3,
|
||||||
|
|
||||||
|
/// 群简介修改
|
||||||
|
static const int introduction = 2; // 3 文档给出的3,实际返回的是2
|
||||||
|
|
||||||
|
/// 群头像修改
|
||||||
|
static const int faceUrl = 4;
|
||||||
|
|
||||||
|
/// 群主变更
|
||||||
|
static const int owner = 5;
|
||||||
|
|
||||||
|
/// 消息接收选项变更
|
||||||
|
static const int receiveMessageOpt = 10;
|
||||||
|
|
||||||
|
/// 全员禁言字段变更
|
||||||
|
static const int shutUpAll = 11;
|
||||||
|
}
|
||||||
|
|
||||||
String parseMessageSummary(V2TimMessage msg) {
|
String parseMessageSummary(V2TimMessage msg) {
|
||||||
switch (msg.elemType) {
|
switch (msg.elemType) {
|
||||||
case MessageElemType.V2TIM_ELEM_TYPE_TEXT:
|
case MessageElemType.V2TIM_ELEM_TYPE_TEXT:
|
||||||
@ -27,7 +60,93 @@ String parseMessageSummary(V2TimMessage msg) {
|
|||||||
case MessageElemType.V2TIM_ELEM_TYPE_MERGER:
|
case MessageElemType.V2TIM_ELEM_TYPE_MERGER:
|
||||||
return '[合并转发消息]';
|
return '[合并转发消息]';
|
||||||
case MessageElemType.V2TIM_ELEM_TYPE_GROUP_TIPS:
|
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:
|
default:
|
||||||
return '[未知消息类型1]';
|
return '[未知消息类型1]';
|
||||||
}
|
}
|
||||||
|
@ -2,6 +2,8 @@ import 'dart:io';
|
|||||||
|
|
||||||
import 'package:device_info_plus/device_info_plus.dart';
|
import 'package:device_info_plus/device_info_plus.dart';
|
||||||
import 'package:get/get.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';
|
import 'package:permission_handler/permission_handler.dart';
|
||||||
|
|
||||||
class Permissions {
|
class Permissions {
|
||||||
@ -13,9 +15,11 @@ class Permissions {
|
|||||||
final sdkInt = androidInfo.version.sdkInt;
|
final sdkInt = androidInfo.version.sdkInt;
|
||||||
|
|
||||||
if (sdkInt >= 33) {
|
if (sdkInt >= 33) {
|
||||||
|
// Android 13 及以上
|
||||||
final status = await Permission.videos.request();
|
final status = await Permission.videos.request();
|
||||||
return status.isGranted;
|
return status.isGranted;
|
||||||
} else {
|
} else {
|
||||||
|
// Android 12 及以下
|
||||||
final status = await Permission.storage.request();
|
final status = await Permission.storage.request();
|
||||||
return status.isGranted;
|
return status.isGranted;
|
||||||
}
|
}
|
||||||
@ -31,17 +35,36 @@ class Permissions {
|
|||||||
return await _checkAndRequest(Permission.camera);
|
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 {
|
static Future<bool> requestMicrophonePermission() async {
|
||||||
return await _checkAndRequest(Permission.microphone);
|
return await _checkAndRequest(Permission.microphone);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 请求麦克风权限
|
// 请求本地存储权限
|
||||||
static Future<bool> requestPhotoPermission() async {
|
|
||||||
return await _checkAndRequest(Permission.microphone);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 请求本地存储权限
|
|
||||||
static Future<bool> requestStoragePermission() async {
|
static Future<bool> requestStoragePermission() async {
|
||||||
return await _checkAndRequest(Permission.storage);
|
return await _checkAndRequest(Permission.storage);
|
||||||
}
|
}
|
||||||
@ -57,8 +80,10 @@ class Permissions {
|
|||||||
if (result.isGranted) return true;
|
if (result.isGranted) return true;
|
||||||
|
|
||||||
if (result.isPermanentlyDenied) {
|
if (result.isPermanentlyDenied) {
|
||||||
_showPermissionDialog();
|
// 永久拒绝 只能去设置
|
||||||
|
showPermissionDialog();
|
||||||
} else {
|
} else {
|
||||||
|
// 临时拒绝 提示
|
||||||
Get.snackbar('权限请求失败', '无法访问,请授权对应权限后重试');
|
Get.snackbar('权限请求失败', '无法访问,请授权对应权限后重试');
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -66,16 +91,14 @@ class Permissions {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 跳转设置的提示弹窗
|
// 跳转设置的提示弹窗
|
||||||
static void _showPermissionDialog() {
|
static void showPermissionDialog() async {
|
||||||
Get.defaultDialog(
|
final confirmed = await ConfirmDialog.show(
|
||||||
title: '需要权限',
|
title: '需要权限',
|
||||||
middleText: '请前往系统设置中手动开启权限',
|
content: '请前往系统设置中手动开启权限',
|
||||||
textCancel: '取消',
|
confirmText: '去设置',
|
||||||
textConfirm: '去设置',
|
|
||||||
onConfirm: () async {
|
|
||||||
Get.back();
|
|
||||||
await openAppSettings();
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
|
if (confirmed == true) {
|
||||||
|
await openAppSettings();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user