基础页面

This commit is contained in:
2022-10-20 14:21:39 +08:00
parent 49ad269c2b
commit 42ba10ec61
62 changed files with 5132 additions and 54 deletions

View File

@@ -52,7 +52,7 @@ class Themes {
fontSize: 11,
),
unselectedLabelStyle: TextStyle(
fontSize: 10,
fontSize: 11,
),
),
);

View File

@@ -0,0 +1,228 @@
import 'package:adaptive_dialog/adaptive_dialog.dart';
import 'package:chat/models/im/group_conversation_model.dart';
import 'package:chat/services/auth_service.dart';
import 'package:chat/services/tim/conversation_service.dart';
import 'package:chat/services/tim/group_service.dart';
import 'package:chat/services/tim_service.dart';
import 'package:chat/utils/ui_tools.dart';
import 'package:get/get.dart';
import 'package:tencent_im_sdk_plugin/enum/group_member_filter_enum.dart';
import 'package:tencent_im_sdk_plugin/enum/group_member_role.dart';
import 'package:tencent_im_sdk_plugin/enum/group_member_role_enum.dart';
import 'package:tencent_im_sdk_plugin/models/v2_tim_group_member_full_info.dart';
class GroupController extends GetxController {
static GroupController get to => Get.find<GroupController>();
Rx<GroupConversationModel> currentGroup =
GroupConversationModel(groupID: '').obs;
@override
void onClose() {
currentGroup.value = GroupConversationModel(groupID: '');
TimService.to.currentConversationId.value = '';
super.onClose();
}
/// 设置当前操作的群组
Future<void> setCurrentGroup(String groupId) async {
var group = await TimGroupService.to.info(groupId);
if (group != null) {
TimService.to.currentConversationId.value = 'group_' + groupId;
currentGroup.value.group = group;
currentGroup.value.groupID = groupId;
var selfInfo = await TimGroupService.to.getMemberInfo(
group,
AuthService.to.userId,
);
if (selfInfo?.role == GroupMemberRoleType.V2TIM_GROUP_MEMBER_ROLE_ADMIN) {
currentGroup.value.isAdmin = true;
}
if (selfInfo?.role == GroupMemberRoleType.V2TIM_GROUP_MEMBER_ROLE_OWNER) {
currentGroup.value.isOwner = true;
}
currentGroup.value.selfInfo = await TimGroupService.to.getMemberInfo(
group,
AuthService.to.userId,
);
currentGroup.value.conversation = await TimConversationService.to.getById(
'group_' + groupId,
);
currentGroup.refresh();
}
}
/// 获取群成员列表
Future<void> fetchGroupMemberList() async {
var members = await TimGroupService.to.members(
currentGroup.value.groupID,
count: 13,
);
currentGroup.value.memberList = members;
var admins = await TimGroupService.to.members(
currentGroup.value.groupID,
count: 100,
filter: GroupMemberFilterTypeEnum.V2TIM_GROUP_MEMBER_FILTER_ADMIN,
);
currentGroup.value.adminList = admins;
currentGroup.refresh();
}
/// 更新群名称
Future<void> updateGroupName(String name) async {
var result = await TimGroupService.to.updateName(
currentGroup.value.group!,
name,
);
if (result) {
currentGroup.value.group!.groupName = name;
currentGroup.value.conversation!.showName = name;
currentGroup.refresh();
UiTools.toast('群名称修改成功');
Get.back();
}
}
/// 更新我的群名片
Future<void> updateGroupNameCard(String nameCard) async {
var res = await TimGroupService.to.setMemberInfo(
currentGroup.value.group!,
AuthService.to.userId,
nameCard,
);
if (res) {
currentGroup.value.selfInfo!.nameCard = nameCard;
currentGroup.refresh();
UiTools.toast('群名片修改成功');
Get.back();
}
}
Future<void> updateGroupNotification(String notification) async {
var res = await TimGroupService.to.updateNotification(
currentGroup.value.group!,
notification,
);
if (res) {
currentGroup.value.group!.notification = notification;
currentGroup.refresh();
UiTools.toast('群公告更新成功');
Get.back();
}
}
Future<void> togglePinned() async {
var res = await TimConversationService.to.setOnTop(
currentGroup.value.conversation!,
);
if (res) {
currentGroup.value.conversation = await TimConversationService.to.getById(
'group_' + currentGroup.value.groupID,
);
currentGroup.refresh();
UiTools.toast('修改成功');
}
}
Future<void> toggleReceiveOpt() async {
var res = await TimConversationService.to.setReceiveOpt(
currentGroup.value.conversation!,
);
if (res) {
currentGroup.value.conversation = await TimConversationService.to.getById(
'group_' + currentGroup.value.groupID,
);
currentGroup.refresh();
UiTools.toast('修改成功');
}
}
/// 移除群成员
Future<bool> kick(List<String> ids) async {
var result = await TimGroupService.to.kickMember(
GroupController.to.currentGroup.value.group!,
ids,
);
if (result) {
setCurrentGroup(currentGroup.value.groupID);
fetchGroupMemberList();
}
return result;
}
Future<bool> transfer(V2TimGroupMemberFullInfo member) async {
OkCancelResult result = await showOkCancelAlertDialog(
style: AdaptiveStyle.iOS,
context: Get.context!,
title: '操作提示',
message: '确定选择 ${member.nickName} 为新群主,您将自动放弃群主身份。',
okLabel: '确定',
cancelLabel: '取消',
defaultType: OkCancelAlertDefaultType.ok,
);
if (result == OkCancelResult.ok) {
var res = await TimGroupService.to.transfer(
currentGroup.value.group!,
member.userID,
);
if (res) {
/// 直接修改当前用户的身份,为普通用户
currentGroup.value.isAdmin = false;
currentGroup.value.isOwner = false;
await fetchGroupMemberList();
}
return res;
}
return false;
}
Future<bool> setAdmin(String userID) async {
var result = await TimGroupService.to.setMemberRole(
currentGroup.value.group!,
userID,
GroupMemberRoleTypeEnum.V2TIM_GROUP_MEMBER_ROLE_ADMIN,
);
if (result) {
UiTools.toast('设置群管理成功');
}
return result;
}
Future<bool> cancelAdmin(String userID) async {
var result = await TimGroupService.to.setMemberRole(
currentGroup.value.group!,
userID,
GroupMemberRoleTypeEnum.V2TIM_GROUP_MEMBER_ROLE_MEMBER,
);
if (result) {
UiTools.toast('取消群管理成功');
}
return result;
}
}

View File

@@ -0,0 +1,111 @@
import 'package:chat/models/im/private_conversation_model.dart';
import 'package:chat/services/tim/conversation_service.dart';
import 'package:chat/services/tim/friend_service.dart';
import 'package:chat/services/tim_service.dart';
import 'package:chat/utils/ui_tools.dart';
import 'package:get/get.dart';
import 'package:tencent_im_sdk_plugin/models/v2_tim_friend_info_result.dart';
class PrivateController extends GetxController {
static PrivateController get to => Get.find<PrivateController>();
Rx<PrivateConversationModel> currentFriend =
PrivateConversationModel(userID: '').obs;
@override
void onClose() {
currentFriend.value = PrivateConversationModel(userID: '');
TimService.to.currentConversationId.value = '';
super.onClose();
}
/// 设置当前好友
Future<void> setCurrentFriend(String userID) async {
V2TimFriendInfoResult? info = await TimFriendService.to.friendInfo(userID);
if (info != null) {
TimService.to.currentConversationId.value = 'c2c_' + userID;
currentFriend.value.userID = userID;
currentFriend.value.isFriend =
info.relation == UserRelationEnum.V2TIM_FRIEND_RELATION_TYPE_BOTH_WAY;
currentFriend.value.friendRemark = info.friendInfo!.friendRemark!;
currentFriend.value.userProfile = info.friendInfo!.userProfile;
/// 通过自定义Staffer字段判断是否是客服属于哪个店铺
if (info.friendInfo!.userProfile?.customInfo!['Staffer']?.isNotEmpty ==
true) {
currentFriend.value.shopId =
info.friendInfo!.userProfile?.customInfo!['Staffer'];
currentFriend.value.isStaffer = true;
}
/// 通过用户自定义字段,判断是否允许陌生人消息
if (info.friendInfo!.userProfile?.customInfo!['Stranger']?.isNotEmpty ==
true) {
currentFriend.value.allowStranger = false;
}
/// 设置会话
currentFriend.value.conversation =
await TimConversationService.to.getById('c2c_' + userID);
currentFriend.refresh();
}
}
Future<void> togglePinned() async {
var res = await TimConversationService.to.setOnTop(
currentFriend.value.conversation!,
);
if (res) {
currentFriend.value.conversation =
await TimConversationService.to.getById(
'c2c_' + currentFriend.value.userID,
);
currentFriend.refresh();
UiTools.toast('修改成功');
}
}
Future<void> changeReceiveOpt() async {
var res = await TimConversationService.to.setReceiveOpt(
currentFriend.value.conversation!,
);
if (res) {
currentFriend.value.conversation =
await TimConversationService.to.getById(
'c2c_' + currentFriend.value.userID,
);
currentFriend.refresh();
UiTools.toast('修改成功');
}
}
Future<void> setRemark(String remark) async {
var result = await TimFriendService.to.setFriendRemark(
currentFriend.value.userID,
remark,
);
if (result) {
currentFriend.value.friendRemark = remark;
currentFriend.value.conversation =
await TimConversationService.to.getById(
'c2c_' + currentFriend.value.userID,
);
currentFriend.refresh();
TimConversationService.to.fetchList();
UiTools.toast('备注修改成功');
Get.back();
}
}
}

View File

@@ -0,0 +1,121 @@
import 'dart:convert';
import 'package:chat/models/im/custom_message_model.dart';
class CallingModel extends CustomMessageModel {
CallingModel({
this.businessID = CustomMessageType.CALL,
required this.callType,
required this.inviter,
required this.inviteeList,
required this.data,
required this.timeout,
required this.actionType,
required this.onlineUserOnly,
required this.isGroup,
});
@override
String businessID;
/// 通话类型 videoCall audioCall
String callType;
/// 邀请人
String inviter;
/// 被邀请人
List<String> inviteeList;
/// 通话时长
int timeout;
// 1: 邀请方发起邀请
// 2: 邀请方取消邀请
// 3: 被邀请方接受邀请
// 4: 被邀请方拒绝邀请
// 5: 邀请超时
int actionType;
bool onlineUserOnly;
/// 是否是群语音
bool isGroup;
CallingModelData data;
String get actionTypeText {
final actionMessage = {
1: "发起通话",
2: "取消通话",
3: "接受通话",
4: "拒绝通话",
5: "超时未接听",
};
return actionMessage[actionType] ?? "";
}
factory CallingModel.fromJson(Map<String, dynamic> json) => CallingModel(
callType: jsonDecode(json['data'])['data']['cmd'],
inviter: json['inviter'],
inviteeList: List<String>.from(
json['inviteeList'].map(
(x) => x.toString(),
),
),
data: CallingModelData.fromJson(jsonDecode(json['data'])),
timeout: json['timeout'],
actionType: json['actionType'],
onlineUserOnly: json['onlineUserOnly'],
isGroup: jsonDecode(json['data'])['is_group'],
);
@override
String toJson() {
return '';
}
}
class CallingModelData {
CallingModelData({
required this.version,
required this.callType,
required this.data,
required this.roomId,
required this.isGroup,
});
int version;
int callType;
DataData data;
int roomId;
bool isGroup;
factory CallingModelData.fromJson(Map<String, dynamic> json) =>
CallingModelData(
version: json['version'],
callType: json['call_type'],
data: DataData.fromJson(json['data']),
roomId: json['room_id'],
isGroup: json['is_group'],
);
}
class DataData {
DataData({
required this.cmd,
required this.roomId,
required this.message,
required this.cmdInfo,
});
String cmd; // videoCall audioCall
int roomId;
String message;
String cmdInfo;
factory DataData.fromJson(Map<String, dynamic> json) => DataData(
cmd: json['cmd'],
roomId: json['room_id'],
message: json['message'],
cmdInfo: json['cmd_info'],
);
}

View File

@@ -0,0 +1,21 @@
import 'package:azlistview/azlistview.dart';
import 'package:tencent_im_sdk_plugin/models/v2_tim_friend_info.dart';
class ContactInfoModel extends ISuspensionBean {
String name;
String userID;
String? tagIndex;
String? namePinyin;
V2TimFriendInfo? friendInfo;
ContactInfoModel({
required this.name,
required this.userID,
this.tagIndex,
this.namePinyin,
this.friendInfo,
});
@override
String getSuspensionTag() => tagIndex!;
}

View File

@@ -0,0 +1,25 @@
abstract class CustomMessageModel {
abstract String businessID;
String toJson();
}
class CustomMessageType {
// ignore: constant_identifier_names
static const String NAME_CARD = 'name_card';
// ignore: constant_identifier_names
static const String GROUP_CARD = 'group_card';
// ignore: constant_identifier_names
static const String DT_TRANSFER = 'dt_transfer';
// ignore: constant_identifier_names
static const String TYPING_STATUS = 'user_typing_status';
// ignore: constant_identifier_names
static const String EVALUATION = 'evaluation';
// ignore: constant_identifier_names
static const String CALL = '1';
}
class CallingType {
static const String audioCall = 'audioCall';
static const String videoCall = 'videoCall';
}

View File

@@ -0,0 +1,14 @@
class EmojiModel {
EmojiModel({
required this.name,
required this.unicode,
});
String name;
int unicode;
factory EmojiModel.fromJson(Map<String, dynamic> json) => EmojiModel(
name: json['name'],
unicode: json['unicode'],
);
}

View File

@@ -0,0 +1,29 @@
import 'package:chat/models/im/custom_message_model.dart';
class EvaluationModel extends CustomMessageModel {
@override
String businessID;
int version;
double score;
String comment;
EvaluationModel({
this.businessID = 'evaluation',
required this.version,
required this.score,
required this.comment,
});
factory EvaluationModel.fromJson(Map<String, dynamic> json) =>
EvaluationModel(
businessID: json['businessID'],
version: json['version'],
score: double.parse(json['score']),
comment: json['comment'],
);
@override
String toJson() {
return '';
}
}

View File

@@ -0,0 +1,33 @@
import 'dart:convert';
import 'package:chat/models/im/custom_message_model.dart';
class GroupCardModel extends CustomMessageModel {
@override
String businessID;
String groupID;
String groupName;
String inviterID;
GroupCardModel({
this.businessID = CustomMessageType.GROUP_CARD,
required this.groupID,
required this.groupName,
required this.inviterID,
});
factory GroupCardModel.fromJson(Map<String, dynamic> json) => GroupCardModel(
businessID: json['businessID'],
groupID: json['groupID'],
groupName: json['groupName'],
inviterID: json['inviterID'],
);
@override
String toJson() => json.encode({
'businessID': businessID,
'groupID': groupID,
'groupName': groupName,
'inviterID': inviterID,
});
}

View File

@@ -0,0 +1,25 @@
import 'package:tencent_im_sdk_plugin/models/v2_tim_conversation.dart';
import 'package:tencent_im_sdk_plugin/models/v2_tim_group_info.dart';
import 'package:tencent_im_sdk_plugin/models/v2_tim_group_member_full_info.dart';
class GroupConversationModel {
String groupID;
V2TimGroupInfo? group;
List<V2TimGroupMemberFullInfo?>? memberList;
List<V2TimGroupMemberFullInfo?>? adminList;
V2TimGroupMemberFullInfo? selfInfo;
V2TimConversation? conversation;
bool isAdmin;
bool isOwner;
GroupConversationModel({
required this.groupID,
this.group,
this.memberList,
this.adminList,
this.selfInfo,
this.conversation,
this.isAdmin = false,
this.isOwner = false,
});
}

View File

@@ -0,0 +1,23 @@
class LocationModel {
LocationModel({
required this.name,
required this.address,
required this.list,
required this.latitude,
required this.longitude,
});
String name;
String address;
List<dynamic> list;
double latitude;
double longitude;
factory LocationModel.fromJson(Map<String, dynamic> json) => LocationModel(
name: json['name'],
address: json['address'],
list: List<dynamic>.from(json['list'].map((x) => x)),
latitude: json['latitude'].toDouble(),
longitude: json['longitude'].toDouble(),
);
}

View File

@@ -0,0 +1,33 @@
import 'dart:convert';
import 'package:chat/models/im/custom_message_model.dart';
class NameCardModel extends CustomMessageModel {
@override
String businessID;
String avatar;
String userID;
String userName;
NameCardModel({
this.businessID = CustomMessageType.NAME_CARD,
required this.avatar,
required this.userID,
required this.userName,
});
factory NameCardModel.fromJson(Map<String, dynamic> json) => NameCardModel(
businessID: json['businessID'],
avatar: json['avatar'],
userID: json['userID'],
userName: json['userName'],
);
@override
String toJson() => json.encode({
'businessID': businessID,
'avatar': avatar,
'userID': userID,
'userName': userName,
});
}

View File

@@ -0,0 +1,40 @@
// ignore_for_file: constant_identifier_names
import 'package:tencent_im_sdk_plugin/models/v2_tim_conversation.dart';
import 'package:tencent_im_sdk_plugin/models/v2_tim_user_full_info.dart';
class PrivateConversationModel {
String userID;
bool isFriend;
String friendRemark;
V2TimConversation? conversation;
V2TimUserFullInfo? userProfile;
bool isStaffer;
bool allowStranger; // 允许陌生人消息
String? shopId; // 他是哪个店铺的客服
PrivateConversationModel({
required this.userID,
this.friendRemark = '',
this.isFriend = false,
this.conversation,
this.userProfile,
this.isStaffer = false,
this.allowStranger = true,
this.shopId,
});
}
class UserRelationEnum {
/// 不是好友
static const int V2TIM_FRIEND_RELATION_TYPE_NONE = 0;
/// 对方在我的好友列表
static const int V2TIM_FRIEND_RELATION_TYPE_IN_MY_FRIEND_LIST = 1;
/// 我在对方的好友列表
static const int V2TIM_FRIEND_RELATION_TYPE_IN_OTHER_FRIEND_LIST = 2;
/// 表示对方在我的好友列表中
static const int V2TIM_FRIEND_RELATION_TYPE_BOTH_WAY = 3;
}

View File

@@ -0,0 +1,18 @@
class SearchUserModel {
SearchUserModel({
required this.userID,
required this.nickname,
required this.avatar,
});
String userID;
String nickname;
String avatar;
factory SearchUserModel.fromJson(Map<String, dynamic> json) =>
SearchUserModel(
userID: json['user_id'].toString(),
nickname: json['nickname'],
avatar: json['avatar'],
);
}

View File

@@ -0,0 +1,32 @@
import 'dart:convert';
import 'package:chat/models/im/custom_message_model.dart';
class TransferModel extends CustomMessageModel {
@override
String businessID;
String amount;
int orderId;
bool isReceived;
TransferModel({
this.businessID = CustomMessageType.DT_TRANSFER,
required this.amount,
required this.orderId,
this.isReceived = false,
});
factory TransferModel.fromJson(Map<String, dynamic> json) => TransferModel(
businessID: json['businessID'],
amount: json['amount'],
orderId: json['orderId'] ?? 33,
);
@override
String toJson() => json.encode({
'businessID': businessID,
'amount': amount,
'orderId': orderId,
'isReceived': isReceived,
});
}

View File

@@ -1,11 +1,17 @@
import 'package:chat/routes/app_routes.dart';
import 'package:chat/routes/auth_routes.dart';
import 'package:chat/routes/contact_routes.dart';
import 'package:chat/routes/conversation_routes.dart';
import 'package:chat/routes/user_routes.dart';
import 'package:get/get.dart';
class AppRouter {
// 路由页面
static final List<GetPage<dynamic>> getPages = [
AppRoutes.router,
AuthRoutes.router,
AppRoutes.router,
ConversationRoutes.router,
ContactRoutes.router,
UserRoutes.router,
];
}

View File

@@ -1,3 +1,4 @@
import 'package:chat/views/conversation/index_page.dart';
import 'package:chat/views/home/index_page.dart';
import 'package:chat/views/public/app_page.dart';
import 'package:chat/views/public/scan_page.dart';
@@ -10,9 +11,11 @@ abstract class AppRoutes {
static const String app = '/';
static const String transit = '/transit';
static const String notfound = '/notfound';
static const String home = '/home';
static const String scan = '/scan';
static const String home = '/home';
static const String search = '/search';
static GetPage router = GetPage(
name: '/',
page: () => AppPage(),
@@ -21,13 +24,17 @@ abstract class AppRoutes {
name: AppRoutes.transit,
page: () => const TransitPage(),
),
GetPage(
name: AppRoutes.scan,
page: () => const ScanPage(),
),
GetPage(
name: AppRoutes.home,
page: () => const HomePage(),
),
GetPage(
name: AppRoutes.scan,
page: () => const ScanPage(),
name: AppRoutes.search,
page: () => const ConversationPage(),
),
],
);

View File

@@ -1,12 +1,27 @@
import 'package:chat/middleware/auth_middleware.dart';
import 'package:chat/views/contact/group/create/index_page.dart';
import 'package:chat/views/contact/group/index_page.dart';
import 'package:chat/views/contact/group/manage/index_page.dart';
import 'package:chat/views/contact/group/notification/index_page.dart';
import 'package:chat/views/contact/index/index_page.dart';
import 'package:get/get.dart';
abstract class ContactRoutes {
/// 身份验证页面
static const String index = '/contact';
static const String friend = '/contact/friend';
static const String friendSearch = '/contact/friend/search';
static const String friendProfile = '/contact/friend/profile';
static const String group = '/contact/group';
static const String groupQrCode = '/contact/group/qrCode';
static const String groupCreate = '/contact/group/create';
static const String groupNotification = '/contact/group/notification';
static const String groupManage = '/contact/group/manage';
static const String groupApprove = '/contact/group/approve';
static const String groupNickname = '/contact/group/nickname';
static const String groupKick = '/contact/group/kick';
static GetPage router = GetPage(
name: ContactRoutes.index,
@@ -15,9 +30,41 @@ abstract class ContactRoutes {
],
page: () => const ContactPage(),
children: [
GetPage(
name: '/friend',
page: () => const ContactGroupPage(),
children: [
GetPage(
name: '/search',
page: () => const ContactGroupCreatePage(),
),
],
),
GetPage(
name: '/group',
page: () => const ContactGroupPage(),
children: [
GetPage(
name: '/create',
page: () => const ContactGroupCreatePage(),
),
GetPage(
name: '/qrCode',
page: () => const ContactGroupCreatePage(),
),
GetPage(
name: '/notification',
page: () => const ContactGroupNotificationPage(),
),
GetPage(
name: '/manage',
page: () => const ContactGroupManagePage(),
),
GetPage(
name: '/approve',
page: () => const ContactGroupManagePage(),
),
],
),
],
);

View File

@@ -0,0 +1,36 @@
import 'package:chat/middleware/auth_middleware.dart';
import 'package:chat/views/conversation/index_page.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
abstract class ConversationRoutes {
/// 身份验证页面
static const String index = '/conversation';
static const String infoGroup = '/conversation/info/group';
static const String infoPrivate = '/conversation/info/private';
static GetPage router = GetPage(
name: ConversationRoutes.index,
middlewares: [
EnsureAuthMiddleware(),
],
page: () => const ConversationPage(),
children: [
GetPage(
name: '/info',
page: () => Container(),
children: [
GetPage(
name: '/private',
page: () => Container(),
),
GetPage(
name: '/group',
page: () => Container(),
),
],
),
],
);
}

View File

@@ -0,0 +1,24 @@
import 'package:chat/middleware/auth_middleware.dart';
import 'package:chat/views/contact/index/index_page.dart';
import 'package:chat/views/user/qr_code/index_page.dart';
import 'package:get/get.dart';
abstract class UserRoutes {
/// 身份验证页面
static const String index = '/user';
static const String qrCode = '/user/qrCode';
static GetPage router = GetPage(
name: UserRoutes.index,
middlewares: [
EnsureAuthMiddleware(),
],
page: () => const ContactPage(),
children: [
GetPage(
name: '/qrCode',
page: () => const UserQrCodePage(),
),
],
);
}

View File

@@ -1,5 +1,7 @@
import 'package:chat/routes/auth_routes.dart';
import 'package:get/get.dart';
import 'package:get_storage/get_storage.dart';
import 'package:tencent_im_sdk_plugin/tencent_im_sdk_plugin.dart';
class AuthService extends GetxService {
static AuthService get to => Get.find<AuthService>();
@@ -12,19 +14,24 @@ class AuthService extends GetxService {
/// 登录状态记录,可监听的这样ever才能监听到
final RxBool isLogin = false.obs;
/// 登录的token供请求时调用,载入内存,是为了每次使用的时候,不需要从磁盘获取
late String userToken = '';
/// 供请求时调用,载入内存,是为了每次使用的时候,不需要从磁盘获取
late String userId = '';
late String userSig = '';
/// 获取存储的token这个可以做到持久化存储
String get _userToken => _box.read('userToken') ?? '';
String get _userSig =>
_box.read('userSig') ??
'eJwtzEELgjAYxvHvsnPIu7VNJ3ToIIKsIAp2ljbrRYylJs3ou2fq8fk98P*Qiz5Hg2tJSlgEZDNvtO7RY4UzixU7W5feoyUp5QAxVVzR5XFvj62bXAjBAGDRHpu-SSnZlseKrxW8TU1p9sV4v3YJoyFrDrV*QYu5yrQ*2cqUzyHkPpixOMbJjnx-EqUv9A__';
String get _userId => _box.read('userId') ?? '5';
@override
void onInit() {
super.onInit();
if (_userToken.isNotEmpty) {
if (_userSig.isNotEmpty) {
isLogin.value = true;
userToken = _userToken;
userSig = _userSig;
userId = _userId;
}
// ever(_isLogin, (_) {
@@ -37,10 +44,21 @@ class AuthService extends GetxService {
}
Future<bool> login(String address) async {
_box.write('userToken', address);
userToken = address;
_box.write('userId', '5');
userId = '5';
isLogin.value = true;
return true;
}
/// 退出登录
void logout() async {
await TencentImSDKPlugin.v2TIMManager.logout();
_box.remove('userSig');
_box.remove('userId');
userSig = '';
userId = '';
isLogin.value = false;
Get.offAllNamed(AuthRoutes.index);
}
}

View File

@@ -0,0 +1,50 @@
import 'package:chat/services/tim_service.dart';
import 'package:chat/utils/ui_tools.dart';
import 'package:get/get.dart';
import 'package:tencent_im_sdk_plugin/enum/friend_application_type_enum.dart';
import 'package:tencent_im_sdk_plugin/enum/friend_response_type_enum.dart';
import 'package:tencent_im_sdk_plugin/manager/v2_tim_friendship_manager.dart';
import 'package:tencent_im_sdk_plugin/models/v2_tim_friend_application.dart';
class TimApplyService extends GetxService {
static TimApplyService get to => Get.find<TimApplyService>();
/// 好友申请
RxList<V2TimFriendApplication?> applies =
List<V2TimFriendApplication?>.empty(growable: true).obs;
/// 好友关系
V2TIMFriendshipManager get friendshipManager =>
TimService.to.instance.v2TIMFriendshipManager;
@override
void onInit() async {
super.onInit();
await fetchList();
}
/// 获取申请列表
Future<void> fetchList() async {
var applyList = await friendshipManager.getFriendApplicationList();
if (applyList.code == 0) {
applies.value = applyList.data!.friendApplicationList!;
}
}
/// 接受好友请求
Future<bool> accept(String userID) async {
var result = await friendshipManager.acceptFriendApplication(
responseType: FriendResponseTypeEnum.V2TIM_FRIEND_ACCEPT_AGREE_AND_ADD,
type: FriendApplicationTypeEnum.V2TIM_FRIEND_APPLICATION_COME_IN,
userID: userID,
);
if (result.code == 0) {
await fetchList();
return true;
} else {
UiTools.toast(result.desc);
return false;
}
}
}

View File

@@ -0,0 +1,56 @@
import 'package:chat/services/tim_service.dart';
import 'package:chat/utils/ui_tools.dart';
import 'package:get/get.dart';
import 'package:tencent_im_sdk_plugin/manager/v2_tim_friendship_manager.dart';
import 'package:tencent_im_sdk_plugin/models/v2_tim_friend_info.dart';
class TimBlockService extends GetxService {
static TimBlockService get to => Get.find<TimBlockService>();
/// 好友关系
V2TIMFriendshipManager get friendshipManager =>
TimService.to.instance.v2TIMFriendshipManager;
@override
void onInit() async {
super.onInit();
await fetchList();
}
/// 黑名单列表
RxList<V2TimFriendInfo> blocks =
List<V2TimFriendInfo>.empty(growable: true).obs;
/// 拉取黑名单列表
Future<void> fetchList() async {
var blacklist = await friendshipManager.getBlackList();
if (blacklist.code == 0) {
blocks.value = blacklist.data!;
}
}
/// 拉黑某人
Future<bool> add(String userID) async {
var result = await friendshipManager.addToBlackList(userIDList: [userID]);
if (result.code == 0) {
return result.data!.first.resultCode == 0;
} else {
UiTools.toast(result.desc);
return false;
}
}
/// 解除拉黑
Future<bool> remove(String userID) async {
var result =
await friendshipManager.deleteFromBlackList(userIDList: [userID]);
if (result.code == 0) {
return result.data!.first.resultCode == 0;
} else {
UiTools.toast(result.desc);
return false;
}
}
}

View File

@@ -0,0 +1,379 @@
import 'package:chat/models/im/custom_message_model.dart';
import 'package:chat/models/im/location_model.dart';
import 'package:chat/services/tim_service.dart';
import 'package:chat/utils/ui_tools.dart';
import 'package:get/get.dart';
import 'package:tencent_im_sdk_plugin/enum/V2TimConversationListener.dart';
import 'package:tencent_im_sdk_plugin/enum/conversation_type.dart';
import 'package:tencent_im_sdk_plugin/enum/receive_message_opt_enum.dart';
import 'package:tencent_im_sdk_plugin/manager/v2_tim_conversation_manager.dart';
import 'package:tencent_im_sdk_plugin/manager/v2_tim_message_manager.dart';
import 'package:tencent_im_sdk_plugin/models/v2_tim_callback.dart';
import 'package:tencent_im_sdk_plugin/models/v2_tim_conversation.dart';
import 'package:tencent_im_sdk_plugin/models/v2_tim_msg_create_info_result.dart';
import 'package:wechat_assets_picker/wechat_assets_picker.dart';
class TimConversationService extends GetxService {
static TimConversationService get to => Get.find<TimConversationService>();
/// 消息管理实例
V2TIMMessageManager get messageManager =>
TimService.to.instance.v2TIMMessageManager;
/// 会话管理
V2TIMConversationManager get conversationManager =>
TimService.to.instance.v2ConversationManager;
@override
void onInit() async {
super.onInit();
await fetchList();
_addListener();
}
_addListener() {
conversationManager.addConversationListener(
listener: V2TimConversationListener(
/// 未读消息总数监听
onTotalUnreadMessageCountChanged: (_) {
unreadCount.value = _;
}));
}
/// 会话列表
RxList<V2TimConversation?> conversationList =
List<V2TimConversation?>.empty(growable: true).obs;
/// 未读消息总数
var unreadCount = 0.obs;
Future<void> fetchList() async {
var data = await conversationManager.getConversationList(
count: 100,
nextSeq: '0',
);
if (data.code == 0) {
conversationList.value = data.data!.conversationList!;
}
}
/// 获取未读消息数
Future<void> getUnreadCount() async {
var result = await conversationManager.getTotalUnreadMessageCount();
unreadCount.value = result.data ?? 0;
}
/// 获取会话信息
Future<V2TimConversation> getById(
String conversationID,
) async {
var result = await conversationManager.getConversation(
conversationID: conversationID,
);
return result.data!;
}
/// 标记会话已读
Future<void> markAsRead(V2TimConversation conversation) async {
/// 标记会话内消息已读
if (conversation.type == ConversationType.V2TIM_GROUP) {
await messageManager.markGroupMessageAsRead(
groupID: conversation.groupID!,
);
} else {
await messageManager.markC2CMessageAsRead(
userID: conversation.userID!,
);
}
fetchList();
}
/// 从会话列表移除会话
Future<void> delete(V2TimConversation conversation) async {
await deleteById(conversation.conversationID);
fetchList();
}
Future<void> deleteById(String conversationID) async {
await conversationManager.deleteConversation(
conversationID: conversationID,
);
fetchList();
}
/// 清空会话历史消息
Future<void> clearHistoryMessage(V2TimConversation conversation) async {
if (conversation.type == ConversationType.V2TIM_GROUP) {
await messageManager.clearGroupHistoryMessage(
groupID: conversation.groupID!,
);
} else {
await messageManager.clearC2CHistoryMessage(
userID: conversation.userID!,
);
}
fetchList();
}
/// 设置会话置顶/取消置顶
Future<bool> setOnTop(V2TimConversation conversation) async {
var result = await conversationManager.pinConversation(
conversationID: conversation.conversationID,
isPinned: !conversation.isPinned!,
);
fetchList();
if (result.code != 0) {
UiTools.toast(result.desc);
}
return result.code == 0;
}
/// 开启/关闭消息免打扰
Future<bool> setReceiveOpt(
V2TimConversation conversation,
) async {
V2TimCallback result;
if (conversation.type == ConversationType.V2TIM_GROUP) {
result = await messageManager.setGroupReceiveMessageOpt(
groupID: conversation.groupID!,
opt: (conversation.recvOpt == 0)
? ReceiveMsgOptEnum.V2TIM_NOT_RECEIVE_MESSAGE
: ReceiveMsgOptEnum.V2TIM_RECEIVE_MESSAGE,
);
} else {
result = await messageManager.setC2CReceiveMessageOpt(
userIDList: [
conversation.userID!,
],
opt: (conversation.recvOpt == 0)
? ReceiveMsgOptEnum.V2TIM_NOT_RECEIVE_MESSAGE
: ReceiveMsgOptEnum.V2TIM_RECEIVE_MESSAGE,
);
}
fetchList();
return result.code == 0;
}
/// 发送消息
Future<bool> sendTextMessage(
V2TimConversation conversation,
String text,
) async {
var msg = await messageManager.createTextAtMessage(
text: text,
atUserList: [],
);
if (msg.code == 0) {
return await _sendMessage(conversation, msg.data!);
} else {
UiTools.toast(msg.desc);
return false;
}
}
/// 发送图片消息
Future<bool> sendImageMessage(
V2TimConversation conversation,
AssetEntity asset,
) async {
var msg = await messageManager.createImageMessage(
imagePath: (await asset.file)!.path,
);
if (msg.code == 0) {
return await _sendMessage(conversation, msg.data!);
} else {
UiTools.toast(msg.desc);
return false;
}
}
/// 发送语音消息
Future<bool> sendSoundMessage(
V2TimConversation conversation,
String soundPath,
int duration,
) async {
var msg = await messageManager.createSoundMessage(
soundPath: soundPath, duration: duration);
if (msg.code == 0) {
return await _sendMessage(conversation, msg.data!);
} else {
UiTools.toast(msg.desc);
return false;
}
}
/// 发送视频消息
Future<bool> sendVideoMessage(
V2TimConversation conversation,
AssetEntity asset,
) async {
return false;
// final originFile = await asset.originFile;
// var size = await originFile!.length();
// if (size >= 104857600) {
// UiTools.toast('视频文件不能超过100M');
// return false;
// }
// final duration = asset.videoDuration.inSeconds;
// String tempPath = (await getTemporaryDirectory()).path;
// String? thumbnail = await VideoThumbnail.thumbnailFile(
// video: originFile.path,
// thumbnailPath: tempPath,
// imageFormat: ImageFormat.JPEG,
// maxWidth: 256,
// quality: 25,
// );
// var msg = await messageManager.createVideoMessage(
// videoFilePath: originFile.path,
// type: asset.mimeType!.replaceFirst('video/', ''),
// duration: duration,
// snapshotPath: thumbnail ?? '',
// );
// if (msg.code == 0) {
// return await _sendMessage(conversation, msg.data!);
// } else {
// UiTools.toast(msg.desc);
// return false;
// }
}
/// 发送文件消息
Future<bool> sendFileMessage(
V2TimConversation conversation,
String fileName,
String filePath,
) async {
var msg = await messageManager.createFileMessage(
fileName: fileName,
filePath: filePath,
);
if (msg.code == 0) {
return await _sendMessage(conversation, msg.data!);
} else {
UiTools.toast(msg.desc);
return false;
}
}
/// 发送位置消息
Future<bool> sendLocationMessage(
V2TimConversation conversation,
LocationModel messageModel,
) async {
var msg = await messageManager.createLocationMessage(
desc: messageModel.name,
latitude: messageModel.latitude,
longitude: messageModel.longitude,
);
if (msg.code == 0) {
return await _sendMessage(conversation, msg.data!);
} else {
UiTools.toast(msg.desc);
return false;
}
}
/// 发送表情消息
Future<bool> sendFaceMessage(
V2TimConversation conversation,
) async {
var msg = await messageManager.createFaceMessage(
data: '',
index: 0,
);
if (msg.code == 0) {
return await _sendMessage(conversation, msg.data!);
} else {
UiTools.toast(msg.desc);
return false;
}
}
/// 发送聊天记录消息
Future<bool> sendMergerMessage(
V2TimConversation conversation,
) async {
var msg = await messageManager.createMergerMessage(
abstractList: [],
compatibleText: '',
msgIDList: [],
title: '',
);
if (msg.code == 0) {
return await _sendMessage(conversation, msg.data!);
} else {
UiTools.toast(msg.desc);
return false;
}
}
/// 发送自定义消息
Future<bool> sendCustomMessage(
V2TimConversation conversation,
CustomMessageModel customMessageModel,
String desc,
) async {
var msg = await messageManager.createCustomMessage(
data: customMessageModel.toJson(),
desc: desc,
);
if (msg.code == 0) {
return await _sendMessage(conversation, msg.data!);
} else {
UiTools.toast(msg.desc);
return false;
}
}
Future<bool> _sendMessage(
V2TimConversation conversation,
V2TimMsgCreateInfoResult result,
) async {
var sendMessageRes = await messageManager.sendMessage(
id: result.id!,
receiver: conversation.type == ConversationType.V2TIM_C2C
? conversation.userID!
: '',
groupID: conversation.type == ConversationType.V2TIM_GROUP
? conversation.groupID!
: '',
// isExcludedFromUnreadCount: true,
// isExcludedFromLastMessage: true,
// needReadReceipt: true,
);
if (sendMessageRes.code == 0) {
// TimMessageService.to
// .add(conversation.conversationID, result.messageInfo!);
// eventBus.fire(result.messageInfo!);
fetchList();
return true;
} else {
UiTools.toast(sendMessageRes.desc);
return false;
}
}
/// 设置会话草稿
Future<void> draft(
V2TimConversation conversation, {
String? draftText = "",
}) async {
await conversationManager.setConversationDraft(
conversationID: conversation.conversationID,
draftText: draftText,
);
}
}

View File

@@ -0,0 +1,241 @@
import 'package:azlistview/azlistview.dart';
import 'package:chat/models/im/contact_info_model.dart';
import 'package:chat/models/im/search_user_model.dart';
import 'package:chat/services/tim_service.dart';
import 'package:chat/utils/im_tools.dart';
import 'package:chat/utils/request/http.dart';
import 'package:chat/utils/ui_tools.dart';
import 'package:get/get.dart';
import 'package:lpinyin/lpinyin.dart';
import 'package:tencent_im_sdk_plugin/enum/friend_type_enum.dart';
import 'package:tencent_im_sdk_plugin/manager/v2_tim_friendship_manager.dart';
import 'package:tencent_im_sdk_plugin/models/v2_tim_friend_check_result.dart';
import 'package:tencent_im_sdk_plugin/models/v2_tim_friend_info.dart';
import 'package:tencent_im_sdk_plugin/models/v2_tim_friend_info_result.dart';
import 'package:tencent_im_sdk_plugin/models/v2_tim_user_full_info.dart';
class TimFriendService extends GetxService {
static TimFriendService get to => Get.find<TimFriendService>();
/// 好友关系
V2TIMFriendshipManager get friendshipManager =>
TimService.to.instance.v2TIMFriendshipManager;
/// 好友列表
var friends = List<V2TimFriendInfo>.empty(growable: true).obs;
/// 格式化后的联系人信息
var contacts = List<ContactInfoModel>.empty(growable: true).obs;
Future<void> fetchList() async {
var result = await friendshipManager.getFriendList();
if (result.code == 0) {
friends.value = result.data!;
contacts.clear();
for (var element in result.data!) {
String name = ImTools.parseNicknameFromInfo(element);
String pinyin = PinyinHelper.getPinyinE(name);
String tag = pinyin.substring(0, 1).toUpperCase();
if (!RegExp('[A-Z]').hasMatch(tag)) {
tag = '#';
}
contacts.add(ContactInfoModel(
name: name,
userID: element.userID,
tagIndex: tag,
namePinyin: pinyin,
friendInfo: element,
));
}
SuspensionUtil.sortListBySuspensionTag(contacts);
SuspensionUtil.setShowSuspensionStatus(contacts);
}
}
/// 添加好友
Future<bool> add(
String userID, {
String? remark,
String? addWording,
String? addSource,
}) async {
var result = await friendshipManager.addFriend(
userID: userID,
remark: remark,
addWording: addWording,
addSource: addSource,
addType: FriendTypeEnum.V2TIM_FRIEND_TYPE_BOTH,
);
if (result.code == 0) {
if (result.data!.resultCode == 0) {
fetchList();
return true;
} else if (result.data!.resultCode == 30539) {
return true;
} else if (result.data!.resultCode == 30010) {
UiTools.toast('好友数量已达上限');
return false;
} else {
UiTools.toast(
result.data!.resultInfo! + result.data!.resultCode.toString());
return false;
}
}
UiTools.toast(result.desc);
return false;
}
/// 删除双向好友
Future<bool> delete(String userID) async {
var result = await friendshipManager.deleteFromFriendList(
deleteType: FriendTypeEnum.V2TIM_FRIEND_TYPE_BOTH,
userIDList: [userID],
);
if (result.code == 0) {
fetchList();
return result.data!.first.resultCode == 0;
} else {
UiTools.toast(result.desc);
return false;
}
}
/// 检测好友是否有双向(单向)好友关系。
Future<V2TimFriendCheckResult?> check(String userID) async {
var result = await friendshipManager.checkFriend(
checkType: FriendTypeEnum.V2TIM_FRIEND_TYPE_BOTH,
userIDList: [userID],
);
if (result.code == 0) {
return result.data!.first;
} else {
UiTools.toast(result.desc);
return null;
}
}
/// 获取用户资料
Future<V2TimUserFullInfo?> userInfo(String userID) async {
var result = await TimService.to.instance.getUsersInfo(
userIDList: [userID],
);
if (result.code == 0) {
return result.data!.first;
}
UiTools.toast(result.desc);
return null;
}
/// 获取好友资料
Future<V2TimFriendInfoResult?> friendInfo(String userID) async {
var result = await friendshipManager.getFriendsInfo(userIDList: [userID]);
if (result.code == 0) {
if (result.data!.isNotEmpty) {
return result.data!.first;
} else {
return null;
}
}
UiTools.toast(result.desc);
return null;
}
/// 修改个人资料
Future<bool> setSelfInfo({
String? nickname,
String? avatar,
}) async {
var result = await TimService.to.instance.setSelfInfo(
userFullInfo: V2TimUserFullInfo(
nickName: nickname,
faceUrl: avatar,
),
);
if (result.code == 0) {
// TimService.to.fetchSelfInfo();
return true;
}
UiTools.toast(result.desc);
return false;
}
/// 设置是否允许陌生人消息
Future<bool> allowStrangerMessage() async {
var result = await TimService.to.instance.setSelfInfo(
userFullInfo: V2TimUserFullInfo(
customInfo: {
'Stranger': 'TRUE',
},
),
);
if (result.code == 0) {
return true;
}
UiTools.toast(result.desc);
return false;
}
/// 禁止陌生人消息
Future<bool> forbidStrangerMessage() async {
var result = await TimService.to.instance.setSelfInfo(
userFullInfo: V2TimUserFullInfo(
customInfo: {
'Stranger': '',
},
),
);
if (result.code == 0) {
return true;
}
return false;
}
/// 修改好友备注信息
Future<bool> setFriendRemark(
String userID,
String friendRemark,
) async {
var result = await friendshipManager.setFriendInfo(
userID: userID,
friendRemark: friendRemark,
);
if (result.code == 0) {
return true;
}
UiTools.toast(result.desc);
return false;
}
/// 从服务器查找用户
Future<List<SearchUserModel>?> searchUser(String keyword) async {
try {
var json = await Http.get(
'user/search',
params: {
'keyword': keyword,
},
);
return List<SearchUserModel>.from(
json.map(
(x) => SearchUserModel.fromJson(x),
),
);
} catch (err) {
UiTools.toast(err.toString());
}
return null;
}
}

View File

@@ -0,0 +1,403 @@
import 'package:chat/services/tim/conversation_service.dart';
import 'package:chat/services/tim_service.dart';
import 'package:chat/utils/ui_tools.dart';
import 'package:get/get.dart';
import 'package:tencent_im_sdk_plugin/enum/group_add_opt_enum.dart';
import 'package:tencent_im_sdk_plugin/enum/group_application_type_enum.dart';
import 'package:tencent_im_sdk_plugin/enum/group_member_filter_enum.dart';
import 'package:tencent_im_sdk_plugin/enum/group_member_role_enum.dart';
import 'package:tencent_im_sdk_plugin/enum/group_type.dart';
import 'package:tencent_im_sdk_plugin/manager/v2_tim_group_manager.dart';
import 'package:tencent_im_sdk_plugin/models/v2_tim_conversation.dart';
import 'package:tencent_im_sdk_plugin/models/v2_tim_friend_info.dart';
import 'package:tencent_im_sdk_plugin/models/v2_tim_group_application.dart';
import 'package:tencent_im_sdk_plugin/models/v2_tim_group_info.dart';
import 'package:tencent_im_sdk_plugin/models/v2_tim_group_member.dart';
import 'package:tencent_im_sdk_plugin/models/v2_tim_group_member_full_info.dart';
class TimGroupService extends GetxService {
static TimGroupService get to => Get.find<TimGroupService>();
/// 群管理实例
V2TIMGroupManager get groupManager =>
TimService.to.instance.v2TIMGroupManager;
/// 我的群组列表
RxList<V2TimGroupInfo> groups =
List<V2TimGroupInfo>.empty(growable: true).obs;
@override
void onInit() async {
super.onInit();
await fetchList();
}
Future<void> fetchList() async {
var result = await groupManager.getJoinedGroupList();
if (result.code == 0) {
groups.value = result.data!;
}
}
/// 创建群
Future<String?> create(
String groupName,
List<V2TimFriendInfo> memberList, {
String? groupType,
}) async {
var result = await groupManager.createGroup(
groupType: groupType ?? GroupType.Public,
groupName: groupName,
notification: '',
introduction: '',
faceUrl: '',
isAllMuted: false,
isSupportTopic: false,
addOpt: GroupAddOptTypeEnum.V2TIM_GROUP_ADD_AUTH,
memberList: memberList.map((e) {
return V2TimGroupMember(
role: GroupMemberRoleTypeEnum.V2TIM_GROUP_MEMBER_ROLE_MEMBER,
userID: e.userID,
);
}).toList(),
);
if (result.code != 0) {
UiTools.toast(result.desc);
}
await fetchList();
await TimConversationService.to.fetchList();
return result.data;
}
/// 加群申请列表
Future<List<V2TimGroupApplication?>?> applies(String groupID) async {
var result = await groupManager.getGroupApplicationList();
if (result.code == 0) {
return result.data?.groupApplicationList;
} else {
UiTools.toast(result.desc);
return null;
}
}
/// 同意加群
Future<void> accept() async {
await groupManager.acceptGroupApplication(
fromUser: '',
groupID: '',
toUser: '',
);
}
/// 拒绝加群请求
Future<void> refuse() async {
await groupManager.refuseGroupApplication(
addTime: 0,
fromUser: '',
groupID: '',
toUser: '',
type: GroupApplicationTypeEnum.V2TIM_GROUP_APPLICATION_GET_TYPE_JOIN,
);
}
/// 加入群组
Future<bool> join(
V2TimGroupInfo group,
String message,
) async {
var result = await TimService.to.instance.joinGroup(
groupID: group.groupID,
groupType: group.groupType,
message: message,
);
if (result.code == 0) {
await fetchList();
return true;
} else {
UiTools.toast(result.desc);
return false;
}
}
/// 邀请加群
Future<bool> invite(
V2TimGroupInfo group,
List<String> userList,
) async {
var result = await groupManager.inviteUserToGroup(
groupID: group.groupID,
userList: userList,
);
if (result.code == 0) {
return true;
} else {
UiTools.toast(result.desc);
return false;
}
}
/// 退出群组
Future<void> quit(
V2TimGroupInfo group,
) async {
await TimService.to.instance.quitGroup(
groupID: group.groupID,
);
await fetchList();
await TimConversationService.to.deleteById(
'group_' + group.groupID,
);
}
/// 解散群组
Future<bool> dismiss(
V2TimGroupInfo group,
V2TimConversation conversation,
) async {
var result = await TimService.to.instance.dismissGroup(
groupID: group.groupID,
);
if (result.code == 0) {
return true;
}
UiTools.toast(result.desc);
return false;
}
/// 获取群资料
Future<V2TimGroupInfo?> info(String groupId) async {
var result = await groupManager.getGroupsInfo(groupIDList: [groupId]);
if (result.code == 0) {
return result.data![0].groupInfo!;
} else {
UiTools.toast(result.desc);
}
return null;
}
/// 更新群资料
Future<bool> updateName(
V2TimGroupInfo group,
String groupName,
) async {
var result = await groupManager.setGroupInfo(
info: V2TimGroupInfo.fromJson(
{
'groupID': group.groupID,
'groupType': group.groupType,
'groupName': groupName,
},
),
);
if (result.code == 0) {
return true;
}
UiTools.toast(result.desc);
return false;
}
/// 更新群公告
Future<bool> updateNotification(
V2TimGroupInfo group,
String notification,
) async {
var result = await groupManager.setGroupInfo(
info: V2TimGroupInfo.fromJson(
{
'groupID': group.groupID,
'groupType': group.groupType,
'notification': notification,
},
),
);
if (result.code == 0) {
await TimConversationService.to.fetchList();
return true;
}
UiTools.toast(result.desc);
return false;
}
/// 修改加群方式
Future<bool> updateAddOpt(
V2TimGroupInfo group,
String notification,
) async {
var result = await groupManager.setGroupInfo(
info: V2TimGroupInfo.fromJson(
{
'groupID': group.groupID,
'groupType': group.groupType,
'groupAddOpt': group.groupAddOpt,
},
),
);
if (result.code == 0) {
await TimConversationService.to.fetchList();
return true;
}
UiTools.toast(result.desc);
return false;
}
/// 获取群组成员
Future<List<V2TimGroupMemberFullInfo?>?> members(
String groupID, {
int count = 10,
GroupMemberFilterTypeEnum filter =
GroupMemberFilterTypeEnum.V2TIM_GROUP_MEMBER_FILTER_ALL,
}) async {
var result = await groupManager.getGroupMemberList(
count: count,
filter: filter,
nextSeq: '0',
offset: 0,
groupID: groupID,
);
if (result.code == 0) {
return result.data!.memberInfoList!;
}
return null;
}
/// 获取群成员资料
Future<V2TimGroupMemberFullInfo?> getMemberInfo(
V2TimGroupInfo group,
String userID,
) async {
var result = await groupManager.getGroupMembersInfo(
groupID: group.groupID,
memberList: [userID],
);
if (result.data != null) {
return result.data!.first;
} else {
return null;
}
}
/// 设置群成员资料
Future<bool> setMemberInfo(
V2TimGroupInfo group,
String userID,
String nameCard,
) async {
var result = await groupManager.setGroupMemberInfo(
groupID: group.groupID,
userID: userID,
nameCard: nameCard,
);
if (result.code == 0) {
return true;
}
UiTools.toast(result.desc);
return false;
}
/// 禁言群成员, 时间单位秒如果是0应该就解除禁言了
Future<bool> muteMember(
V2TimGroupInfo group,
String userID, {
int seconds = 60,
}) async {
var result = await groupManager.muteGroupMember(
groupID: group.groupID,
userID: userID,
seconds: seconds,
);
if (result.code == 0) {
return true;
}
UiTools.toast(result.desc);
return false;
}
/// 全员禁言/解除全员禁言
Future<bool> mute(
V2TimGroupInfo group,
bool isAllMuted,
) async {
var result = await groupManager.setGroupInfo(
info: V2TimGroupInfo(
isAllMuted: isAllMuted,
groupID: group.groupID,
groupType: group.groupType,
),
);
if (result.code == 0) {
return true;
}
UiTools.toast(result.desc);
return false;
}
/// 踢人
Future<bool> kickMember(
V2TimGroupInfo group,
List<String> memberList,
) async {
var result = await groupManager.kickGroupMember(
groupID: group.groupID,
memberList: memberList,
);
if (result.code == 0) {
return true;
}
UiTools.toast(result.desc);
return false;
}
/// 设置管理员
Future<bool> setMemberRole(
V2TimGroupInfo group,
String userID,
GroupMemberRoleTypeEnum role,
) async {
var result = await groupManager.setGroupMemberRole(
groupID: group.groupID,
userID: userID,
role: role,
);
if (result.code == 0) {
return true;
}
UiTools.toast(result.desc);
return false;
}
/// 转让群主
Future<bool> transfer(
V2TimGroupInfo group,
String userID,
) async {
var result = await groupManager.transferGroupOwner(
groupID: group.groupID,
userID: userID,
);
if (result.code == 0) {
return true;
}
UiTools.toast(result.desc);
return false;
}
}

View File

@@ -0,0 +1,77 @@
import 'package:get/get.dart';
import 'package:scroll_to_index/scroll_to_index.dart';
import 'package:tencent_im_sdk_plugin/enum/conversation_type.dart';
import 'package:tencent_im_sdk_plugin/models/v2_tim_conversation.dart';
import 'package:tencent_im_sdk_plugin/models/v2_tim_message.dart';
import 'package:tencent_im_sdk_plugin/models/v2_tim_value_callback.dart';
import 'conversation_service.dart';
class LoadMessageResult {
String? lastMsgId;
bool noMore;
LoadMessageResult({
this.lastMsgId,
required this.noMore,
});
}
class TimMessageService extends GetxService {
static TimMessageService get to => Get.find<TimMessageService>();
RxList<V2TimMessage> messages = List<V2TimMessage>.empty(growable: true).obs;
var curConversationId = '';
Future<String?> loadMessagesFromService(
V2TimConversation conversation,
String? lastMsgId,
AutoScrollController _scrollController,
) async {
if (lastMsgId == null && curConversationId != conversation.conversationID) {
messages.clear();
curConversationId = conversation.conversationID;
}
V2TimValueCallback<List<V2TimMessage>> result;
if (conversation.type == ConversationType.V2TIM_GROUP) {
result = await TimConversationService.to.messageManager
.getGroupHistoryMessageList(
groupID: conversation.groupID!,
count: 20,
lastMsgID: lastMsgId,
);
} else {
result = await TimConversationService.to.messageManager
.getC2CHistoryMessageList(
userID: conversation.userID!,
count: 20,
lastMsgID: lastMsgId,
);
}
var list = result.data ?? [];
if (lastMsgId == null) {
messages.value = list.reversed.toList();
_scrollController.scrollToIndex(
messages.length - 1,
preferPosition: AutoScrollPosition.begin,
);
} else {
//加载更多
if (list.isNotEmpty) {
messages.value = messages.reversed.toList(); //正常顺序
messages.addAll(list);
messages.value = messages.reversed.toList(); //反转一次
}
}
if (messages.isNotEmpty) {
return messages.first.msgID;
} else {
return '';
}
}
addMessage(V2TimMessage message) {
messages.add(message);
}
}

View File

@@ -1,7 +1,395 @@
import 'dart:convert';
import 'package:chat/controllers/group_controller.dart';
import 'package:chat/models/im/custom_message_model.dart';
import 'package:chat/services/auth_service.dart';
import 'package:chat/services/tim/apply_service.dart';
import 'package:chat/services/tim/block_service.dart';
import 'package:chat/services/tim/conversation_service.dart';
import 'package:chat/services/tim/friend_service.dart';
import 'package:chat/services/tim/group_service.dart';
import 'package:chat/services/tim/message_service.dart';
import 'package:chat/utils/ui_tools.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:tencent_im_sdk_plugin/enum/V2TimAdvancedMsgListener.dart';
import 'package:tencent_im_sdk_plugin/enum/V2TimFriendshipListener.dart';
import 'package:tencent_im_sdk_plugin/enum/V2TimGroupListener.dart';
import 'package:tencent_im_sdk_plugin/enum/V2TimSDKListener.dart';
import 'package:tencent_im_sdk_plugin/enum/log_level_enum.dart';
import 'package:tencent_im_sdk_plugin/enum/message_elem_type.dart';
import 'package:tencent_im_sdk_plugin/manager/v2_tim_manager.dart';
import 'package:tencent_im_sdk_plugin/models/v2_tim_friend_application.dart';
import 'package:tencent_im_sdk_plugin/models/v2_tim_friend_info.dart';
import 'package:tencent_im_sdk_plugin/models/v2_tim_group_change_info.dart';
import 'package:tencent_im_sdk_plugin/models/v2_tim_group_member_change_info.dart';
import 'package:tencent_im_sdk_plugin/models/v2_tim_group_member_info.dart';
import 'package:tencent_im_sdk_plugin/models/v2_tim_message.dart';
import 'package:tencent_im_sdk_plugin/models/v2_tim_message_receipt.dart';
import 'package:tencent_im_sdk_plugin/models/v2_tim_topic_info.dart';
import 'package:tencent_im_sdk_plugin/models/v2_tim_user_full_info.dart';
import 'package:tencent_im_sdk_plugin/tencent_im_sdk_plugin.dart';
import 'package:vibration/vibration.dart';
class TimService extends GetxService {
Future<TimService> init() async {
return this;
static TimService get to => Get.find<TimService>();
/// 获取实例
V2TIMManager get instance => _getInstance();
/// 获取TIM实例
V2TIMManager _getInstance() {
return TencentImSDKPlugin.v2TIMManager;
}
int sdkAppID = 1400719491;
String get _userSig => AuthService.to.userSig;
String get _userId => AuthService.to.userId;
RxString currentConversationId = ''.obs;
@override
void onInit() {
super.onInit();
if (AuthService.to.isLogin.value) {
initSdk();
}
}
Future initSdk() async {
/// 初始化TIMSDK
await instance.initSDK(
sdkAppID: sdkAppID,
loglevel: LogLevelEnum.V2TIM_LOG_INFO,
listener: V2TimSDKListener(
onConnectFailed: (
int code,
String error,
) {},
onConnectSuccess: () async {},
onConnecting: () {},
onKickedOffline: () {
UiTools.toast('该账号在其他设备登录被迫下线');
AuthService.to.logout();
},
onSelfInfoUpdated: (
V2TimUserFullInfo info,
) {},
onUserSigExpired: () {},
),
);
/// 登录
await instance.login(
userID: _userId,
userSig: _userSig,
);
Get.put(TimConversationService());
Get.put(TimFriendService());
Get.put(TimGroupService());
Get.put(TimBlockService());
Get.put(TimApplyService());
Get.put(TimMessageService());
/// 消息监听器
await TimConversationService.to.messageManager.addAdvancedMsgListener(
listener: V2TimAdvancedMsgListener(
/// 收到新消息
onRecvNewMessage: (
V2TimMessage msg,
) {
onRecvNewMessage(msg);
},
/// 收到C2C已读回执
onRecvC2CReadReceipt: (
List<V2TimMessageReceipt> receiptList,
) {},
/// 消息撤回
onRecvMessageRevoked: (
String msgID,
) {},
/// 发送消息进度
onSendMessageProgress: (
V2TimMessage message,
int progress,
) {},
/// 消息更改
onRecvMessageModified: (
V2TimMessage msg,
) {},
/// 消息已读回执
onRecvMessageReadReceipts: (
List<V2TimMessageReceipt> receiptList,
) {},
),
);
TimFriendService.to.friendshipManager.setFriendListener(
listener: V2TimFriendshipListener(
/// 有新的好友请求
onFriendApplicationListAdded: (
List<V2TimFriendApplication> applicationList,
) {
TimApplyService.to.fetchList();
},
onFriendApplicationListDeleted: (
List<String> userIDList,
) {
TimApplyService.to.fetchList();
},
onFriendApplicationListRead: () {
// UiTools.toast('onFriendApplicationListRead');
},
onFriendListAdded: (
List<V2TimFriendInfo> users,
) {
TimFriendService.to.fetchList();
},
onFriendListDeleted: (
List<String> userList,
) {
TimFriendService.to.fetchList();
},
/// 有黑名单
onBlackListAdd: (
List<V2TimFriendInfo> infoList,
) {
TimFriendService.to.fetchList();
TimBlockService.to.fetchList();
},
/// 移除黑名单
onBlackListDeleted: (
List<String> userList,
) {
TimFriendService.to.fetchList();
TimBlockService.to.fetchList();
},
/// 好友资料修改
onFriendInfoChanged: (
List<V2TimFriendInfo> infoList,
) {
TimFriendService.to.fetchList();
},
),
);
instance.setGroupListener(
listener: V2TimGroupListener(
/// 群组解散
onGroupDismissed: (
String groupID,
V2TimGroupMemberInfo opUser,
) async {
await TimConversationService.to.deleteById(
'group_$groupID',
);
await TimGroupService.to.fetchList();
/// 如果是在当前的会话中,关闭会话内容,返回主页面
if (currentConversationId.value == 'group_' + groupID) {
UiTools.toast('群已解散');
Navigator.popUntil(Get.context!, (route) => route.isFirst);
}
},
/// 群资料修改
onGroupInfoChanged: (
String groupID,
List<V2TimGroupChangeInfo?> changeInfos,
) {
TimConversationService.to.fetchList();
TimGroupService.to.fetchList();
},
onMemberEnter: (
String groupID,
List<V2TimGroupMemberInfo> memberList,
) {
UiTools.toast('onMemberEnter');
},
onMemberLeave: (
String groupID,
V2TimGroupMemberInfo member,
) {
UiTools.toast('onMemberLeave');
},
onMemberInvited: (
String groupID,
V2TimGroupMemberInfo opUser,
List<V2TimGroupMemberInfo> memberList,
) {
UiTools.toast('onMemberInvited');
},
/// 有用户被移出群聊,判断是否当前用户,当前会话,关闭会话
onMemberKicked: (
String groupID,
V2TimGroupMemberInfo opUser,
List<V2TimGroupMemberInfo> memberList,
) {
bool isYou = memberList
.where((e) => true
// e.userID ==
// UserController.to.userInfo.value!.userId.toString(),
)
.isNotEmpty;
if (isYou) {
if (currentConversationId.value == 'group_' + groupID) {
UiTools.toast('您已被${opUser.nickName}移出当前群聊');
Navigator.popUntil(Get.context!, (route) => route.isFirst);
}
TimConversationService.to.deleteById(
'group_' + groupID,
);
TimGroupService.to.fetchList();
}
},
onMemberInfoChanged: (
String groupID,
List<V2TimGroupMemberChangeInfo> v2TIMGroupMemberChangeInfoList,
) {
UiTools.toast('onMemberInfoChanged');
},
onGroupRecycled: (
String groupID,
V2TimGroupMemberInfo opUser,
) {
UiTools.toast('onGroupRecycled');
},
onReceiveJoinApplication: (
String groupID,
V2TimGroupMemberInfo member,
String opReason,
) {
UiTools.toast('onReceiveJoinApplication');
},
onApplicationProcessed: (
String groupID,
V2TimGroupMemberInfo opUser,
bool isAgreeJoin,
String opReason,
) {
UiTools.toast('onApplicationProcessed');
},
/// 有新的管理被授权
onGrantAdministrator: (
String groupID,
V2TimGroupMemberInfo opUser,
List<V2TimGroupMemberInfo> memberList,
) {
if (currentConversationId.value == 'group_' + groupID) {
GroupController.to.fetchGroupMemberList();
}
},
/// 取消管理员
onRevokeAdministrator: (
String groupID,
V2TimGroupMemberInfo opUser,
List<V2TimGroupMemberInfo> memberList,
) {
if (currentConversationId.value == 'group_' + groupID) {
GroupController.to.fetchGroupMemberList();
}
},
onQuitFromGroup: (
String groupID,
) {
UiTools.toast('onQuitFromGroup');
},
onReceiveRESTCustomData: (
String groupID,
String customData,
) {
UiTools.toast('onReceiveRESTCustomData');
},
onGroupAttributeChanged: (
String groupID,
Map<String, String> groupAttributeMap,
) {
UiTools.toast('onGroupAttributeChanged');
},
onTopicCreated: (
String groupID,
String topicID,
) {
UiTools.toast('onTopicCreated');
},
onTopicDeleted: (
String groupID,
List<String> topicIDList,
) {
UiTools.toast('onTopicDeleted');
},
onTopicInfoChanged: (
String groupID,
V2TimTopicInfo topicInfo,
) {
UiTools.toast('onTopicInfoChanged');
},
),
);
}
/// 有新消息的事件处理
Future<void> onRecvNewMessage(V2TimMessage message) async {
/// 过滤自定义消息中,用户输入状态的消息
if (message.elemType == MessageElemType.V2TIM_ELEM_TYPE_CUSTOM) {
var msgData = json.decode(message.customElem!.data!);
if (msgData['businessID'] == CustomMessageType.TYPING_STATUS) {
return;
}
}
var conversation = await TimConversationService.to.getById(
getConversationIdByMessage(message),
);
if (conversation.recvOpt == 0) {
/// 振动提醒
Vibration.vibrate(duration: 100);
}
if (_isMessageInCurrentConversation(message)) {
TimConversationService.to.markAsRead(conversation);
} else {
await TimConversationService.to.getUnreadCount();
}
/// 更新会话列表做了个延迟,要不然列表中的未读消息数量,不正确
Future.delayed(const Duration(milliseconds: 500), () async {
await TimConversationService.to.fetchList();
});
// eventBus.fire(message);
}
/// 通过消息判断是否是当前会话
bool _isMessageInCurrentConversation(V2TimMessage message) {
return getConversationIdByMessage(message) == currentConversationId.value;
}
/// 通过消息获取会话ID
String getConversationIdByMessage(V2TimMessage message) {
String conId;
if (message.groupID != null) {
conId = 'group_' + message.groupID!;
} else {
conId = 'c2c_' + message.userID!;
}
return conId;
}
}

30
lib/utils/convert.dart Normal file
View File

@@ -0,0 +1,30 @@
import 'package:dart_date/dart_date.dart';
class Convert {
/// 隐藏字符串中间位
///
/// * [str] 要处理的字符串
/// * [start] 字符串从0开始保留位数
/// * [end] 末尾保留的长度
static String hideCenterStr(
String str, {
int start = 8,
int end = 6,
}) {
if (str.length <= start + end) {
return str;
}
return '${str.substring(0, start)}...${str.substring(str.length - end)}';
}
/// 时间戳转日期
///
/// [timestamp] 要转换的时间戳
/// [format] 日期格式
static String timeFormat(
int timestamp, {
String format = 'yyyy-MM-dd HH:mm:ss',
}) {
return DateTime.fromMillisecondsSinceEpoch(timestamp * 1000).format(format);
}
}

251
lib/utils/im_tools.dart Normal file
View File

@@ -0,0 +1,251 @@
import 'dart:convert';
import 'package:azlistview/azlistview.dart';
import 'package:chat/configs/app_colors.dart';
import 'package:chat/models/im/custom_message_model.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:tencent_im_sdk_plugin/enum/group_change_info_type.dart';
import 'package:tencent_im_sdk_plugin/enum/group_tips_elem_type.dart';
import 'package:tencent_im_sdk_plugin/enum/message_elem_type.dart';
import 'package:tencent_im_sdk_plugin/models/v2_tim_friend_info.dart';
import 'package:tencent_im_sdk_plugin/models/v2_tim_friend_info_result.dart';
import 'package:tencent_im_sdk_plugin/models/v2_tim_message.dart';
class ImTools {
static String parseNicknameFromResult(V2TimFriendInfoResult? infoResult) {
if (infoResult == null) {
return '';
}
if (infoResult.friendInfo!.friendRemark != null) {
if (infoResult.friendInfo!.friendRemark! == '') {
return infoResult.friendInfo!.userProfile!.nickName!;
} else {
return infoResult.friendInfo!.friendRemark!;
}
}
return infoResult.friendInfo!.userProfile!.nickName!;
}
static String parseNicknameFromInfo(V2TimFriendInfo infoResult) {
if (infoResult.friendRemark != '') {
return infoResult.friendRemark!;
}
return infoResult.userProfile!.nickName!;
}
static parseMessage(V2TimMessage? message) {
String text = '';
switch (message?.elemType) {
case MessageElemType.V2TIM_ELEM_TYPE_TEXT:
text = message!.textElem!.text!;
break;
case MessageElemType.V2TIM_ELEM_TYPE_IMAGE:
text = '[图片]';
break;
case MessageElemType.V2TIM_ELEM_TYPE_SOUND:
text = '[语音]';
break;
case MessageElemType.V2TIM_ELEM_TYPE_VIDEO:
text = '[视频]';
break;
case MessageElemType.V2TIM_ELEM_TYPE_FILE:
text = '[文件]';
break;
case MessageElemType.V2TIM_ELEM_TYPE_LOCATION:
text = '[位置]';
break;
case MessageElemType.V2TIM_ELEM_TYPE_FACE:
text = '[表情]';
break;
case MessageElemType.V2TIM_ELEM_TYPE_MERGER:
text = '[聊天记录]';
break;
case MessageElemType.V2TIM_ELEM_TYPE_CUSTOM:
var data = json.decode(message!.customElem!.data!);
switch (data['businessID']) {
case CustomMessageType.CALL:
text = '通话';
break;
case CustomMessageType.DT_TRANSFER:
text = '[转账]';
break;
case CustomMessageType.NAME_CARD:
text = '[名片]';
break;
case CustomMessageType.GROUP_CARD:
text = '[群名片]';
break;
case CustomMessageType.EVALUATION:
text = '[评价]';
break;
default:
text = '[${data['businessID']}]';
}
break;
case MessageElemType.V2TIM_ELEM_TYPE_GROUP_TIPS:
switch (message?.groupTipsElem!.type) {
case GroupTipsElemType.V2TIM_GROUP_TIPS_TYPE_JOIN:
text = '加入群聊';
break;
case GroupTipsElemType.V2TIM_GROUP_TIPS_TYPE_INVITE:
text = '邀请入群';
break;
case GroupTipsElemType.V2TIM_GROUP_TIPS_TYPE_QUIT:
text = '退出群聊';
break;
case GroupTipsElemType.V2TIM_GROUP_TIPS_TYPE_KICKED:
text = '踢出群聊';
break;
case GroupTipsElemType.V2TIM_GROUP_TIPS_TYPE_SET_ADMIN:
text = '设置管理';
break;
case GroupTipsElemType.V2TIM_GROUP_TIPS_TYPE_CANCEL_ADMIN:
text = '取消管理';
break;
case GroupTipsElemType.V2TIM_GROUP_TIPS_TYPE_GROUP_INFO_CHANGE:
// (opMember 修改群资料groupName & introduction & notification & faceUrl & owner & custom)
switch (message!.groupTipsElem!.groupChangeInfoList![0]!.type) {
case GroupChangeInfoType.V2TIM_GROUP_INFO_CHANGE_TYPE_NAME:
text = '修改群名称';
break;
case GroupChangeInfoType
.V2TIM_GROUP_INFO_CHANGE_TYPE_INTRODUCTION:
text = '群简介修改';
break;
case GroupChangeInfoType
.V2TIM_GROUP_INFO_CHANGE_TYPE_NOTIFICATION:
text = '修改群公告';
break;
case GroupChangeInfoType.V2TIM_GROUP_INFO_CHANGE_TYPE_FACE_URL:
text = '群头像修改';
break;
case GroupChangeInfoType.V2TIM_GROUP_INFO_CHANGE_TYPE_OWNER:
text = '群主变更';
break;
case GroupChangeInfoType.V2TIM_GROUP_INFO_CHANGE_TYPE_CUSTOM:
text = '群自定义字段变更';
break;
default:
text = message.groupTipsElem!.groupChangeInfoList![0]!.type
.toString();
break;
}
break;
case GroupTipsElemType.V2TIM_GROUP_TIPS_TYPE_MEMBER_INFO_CHANGE:
text = '群成员资料变更';
break;
default:
text = message!.groupTipsElem!.type.toString();
}
break;
default:
text = message!.elemType.toString();
}
return text;
}
/// 右侧索引栏样式
static const IndexBarOptions indexBarOptions = IndexBarOptions(
needRebuild: true,
ignoreDragCancel: true,
downTextStyle: TextStyle(
fontSize: 12,
color: Colors.white,
),
downItemDecoration: BoxDecoration(
shape: BoxShape.circle,
color: AppColors.primary,
),
indexHintWidth: 120 / 2,
indexHintHeight: 100 / 2,
indexHintDecoration: BoxDecoration(
image: DecorationImage(
image: AssetImage(
'assets/chats/index_bar.png',
),
fit: BoxFit.contain,
),
),
indexHintAlignment: Alignment.centerRight,
indexHintChildAlignment: Alignment(
-0.25,
0.0,
),
indexHintOffset: Offset(
-20,
0,
),
);
/// 悬浮导航显示ABCD~Z
static Widget susItem(
BuildContext context,
String tag, {
double susHeight = 40,
}) {
return Container(
height: susHeight,
width: MediaQuery.of(context).size.width,
padding: const EdgeInsets.only(
left: 16.0,
),
color: Colors.grey[200],
alignment: Alignment.centerLeft,
child: Text(
tag,
),
);
}
static showTrtcMessage(String userID) {
return showModalBottomSheet(
context: Get.context!,
isScrollControlled: true,
backgroundColor: AppColors.white,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(8)),
),
builder: (context) {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
// PopMenuItem(
// '语音通话',
// onTap: () {
// TimService.to.tuiCalling.call(userID, CallingScenes.Audio);
// Get.back();
// },
// ),
// const Divider(height: 0),
// PopMenuItem(
// '视频通话',
// onTap: () {
// TimService.to.tuiCalling.call(userID, CallingScenes.Video);
// Get.back();
// },
// ),
const Divider(height: 0),
Container(
height: 8,
color: AppColors.page,
),
const Divider(height: 0),
// PopMenuItem(
// '取消',
// onTap: () {
// Get.back();
// },
// ),
],
);
},
);
}
}

121
lib/utils/request/http.dart Normal file
View File

@@ -0,0 +1,121 @@
import 'package:chat/utils/request/http_request.dart';
import 'package:dio/dio.dart';
class Http {
/// 取消请求
static void cancelRequests({
required CancelToken token,
}) {
HttpRequest().cancelRequests(
token: token,
);
}
/// GET 请求
static Future<T> get<T>(
String path, {
Map<String, dynamic>? params,
Options? options,
CancelToken? cancelToken,
ProgressCallback? onReceiveProgress,
}) async {
return await HttpRequest().request(
path,
method: HttpMethod.get,
params: params,
options: options,
cancelToken: cancelToken,
);
}
/// POST 请求
static Future<T> post<T>(
String path, {
Map<String, dynamic>? params,
dynamic data,
Options? options,
CancelToken? cancelToken,
ProgressCallback? onSendProgress,
ProgressCallback? onReceiveProgress,
}) async {
return await HttpRequest().request(
path,
method: HttpMethod.post,
params: params,
data: data,
options: options,
cancelToken: cancelToken,
onReceiveProgress: onReceiveProgress,
);
}
/// PUT
static Future<T> put<T>(
String path, {
Map<String, dynamic>? params,
dynamic data,
Options? options,
CancelToken? cancelToken,
ProgressCallback? onSendProgress,
ProgressCallback? onReceiveProgress,
}) async {
return await HttpRequest().request(
path,
method: HttpMethod.put,
params: params,
data: data,
options: options,
cancelToken: cancelToken,
onSendProgress: onSendProgress,
onReceiveProgress: onReceiveProgress,
);
}
/// DELETE
static Future<T> delete<T>(
String path, {
Map<String, dynamic>? params,
dynamic data,
Options? options,
CancelToken? cancelToken,
ProgressCallback? onSendProgress,
ProgressCallback? onReceiveProgress,
}) async {
return await HttpRequest().request(
path,
method: HttpMethod.delete,
params: params,
data: data,
options: options,
cancelToken: cancelToken,
onSendProgress: onSendProgress,
onReceiveProgress: onReceiveProgress,
);
}
/// 上传文件
static Future<T> upload<T>(
String path, {
required String filePath,
Map<String, dynamic>? params,
Options? options,
CancelToken? cancelToken,
ProgressCallback? onSendProgress,
ProgressCallback? onReceiveProgress,
}) async {
var formData = FormData.fromMap({
'upload': await MultipartFile.fromFile(filePath),
});
return await HttpRequest().request(
path,
method: HttpMethod.post,
params: params,
data: formData,
options: options,
cancelToken: cancelToken,
onSendProgress: onSendProgress,
onReceiveProgress: onReceiveProgress,
);
}
}

View File

@@ -0,0 +1,97 @@
import 'package:chat/services/auth_service.dart';
import 'package:chat/utils/ui_tools.dart';
import 'package:dio/dio.dart';
class HttpInterceptor extends Interceptor {
@override
void onRequest(
RequestOptions options,
RequestInterceptorHandler handler,
) {
// 头部添加token
// options.headers['Authorization'] = AuthService.to.userToken;
options.headers['Accept'] = 'application/json';
super.onRequest(
options,
handler,
);
}
@override
void onResponse(
Response response,
ResponseInterceptorHandler handler,
) {
final apiStatusCode = response.data['status_code'];
if (apiStatusCode == 200) {
response.data = response.data['data'];
} else if (apiStatusCode == 401) {
throw DioError(
response: response,
error: "登录超时",
requestOptions: response.requestOptions,
);
} else if (apiStatusCode == 404) {
throw DioError(
response: response,
error: "请求的接口不存在",
requestOptions: response.requestOptions,
);
} else if (apiStatusCode == 0) {
throw DioError(
response: response,
error: response.data['message'],
requestOptions: response.requestOptions,
);
} else {
throw DioError(
response: response,
error: response.data['message'],
requestOptions: response.requestOptions,
);
}
super.onResponse(
response,
handler,
);
}
@override
void onError(
DioError err,
ErrorInterceptorHandler handler,
) {
switch (err.type) {
// 连接服务器超时
case DioErrorType.connectTimeout:
UiTools.toast('网络传输超时');
break;
// 响应超时
case DioErrorType.receiveTimeout:
UiTools.toast('网络传输超时');
break;
// 发送超时
case DioErrorType.sendTimeout:
UiTools.toast('网络传输超时');
break;
// 请求取消
case DioErrorType.cancel:
break;
// 404/503错误
case DioErrorType.response:
break;
// other 其他错误类型
case DioErrorType.other:
if (err.response?.data['status_code'] == 401) {
AuthService.to.logout();
}
break;
}
super.onError(
err,
handler,
);
}
}

View File

@@ -0,0 +1,5 @@
class HttpOptions {
static const String baseUrl = 'http://api.gl.shangkelian.cn/api/';
static const int connectTimeout = 15000;
static const int receiveTimeout = 15000;
}

View File

@@ -0,0 +1,91 @@
import 'package:chat/utils/request/http_interceptor.dart';
import 'package:chat/utils/request/http_options.dart';
import 'package:dio/dio.dart';
enum HttpMethod {
get,
post,
put,
delete,
patch,
head,
}
class HttpRequest {
static HttpRequest? _instance;
static Dio _dio = Dio();
Dio get dio => _dio;
HttpRequest._internal() {
_instance = this;
_instance!._init();
}
factory HttpRequest() => _instance ?? HttpRequest._internal();
static HttpRequest? getInstance() {
return _instance ?? HttpRequest._internal();
}
/// 取消请求token
final CancelToken _cancelToken = CancelToken();
_init() {
BaseOptions options = BaseOptions(
baseUrl: HttpOptions.baseUrl,
connectTimeout: HttpOptions.connectTimeout,
receiveTimeout: HttpOptions.receiveTimeout,
);
_dio = Dio(options);
/// 添加拦截器
_dio.interceptors.add(HttpInterceptor());
}
/// 请求
Future request(
String path, {
HttpMethod method = HttpMethod.get,
Map<String, dynamic>? params,
dynamic data,
Options? options,
CancelToken? cancelToken,
ProgressCallback? onSendProgress,
ProgressCallback? onReceiveProgress,
}) async {
const methodValues = {
HttpMethod.get: 'get',
HttpMethod.post: 'post',
HttpMethod.put: 'put',
HttpMethod.delete: 'delete',
HttpMethod.patch: 'patch',
HttpMethod.head: 'head'
};
options ??= Options(method: methodValues[method]);
try {
Response response = await _dio.request(
path,
data: data,
queryParameters: params,
cancelToken: cancelToken ?? _cancelToken,
options: options,
onSendProgress: onSendProgress,
onReceiveProgress: onReceiveProgress,
);
// ignore: avoid_print
print('请求地址:${response.requestOptions.uri}');
// ignore: avoid_print
print('响应数据:${response.data}');
return response.data;
} on DioError catch (e) {
throw Exception(e.message);
}
}
/// 取消网络请求
void cancelRequests({CancelToken? token}) {
token ?? _cancelToken.cancel('CANCELED');
}
}

View File

@@ -0,0 +1,15 @@
import 'package:flutter/material.dart';
class ContactGroupCreatePage extends StatefulWidget {
const ContactGroupCreatePage({Key? key}) : super(key: key);
@override
_ContactGroupCreatePageState createState() => _ContactGroupCreatePageState();
}
class _ContactGroupCreatePageState extends State<ContactGroupCreatePage> {
@override
Widget build(BuildContext context) {
return Container();
}
}

View File

@@ -0,0 +1,15 @@
import 'package:flutter/material.dart';
class ContactGroupManagePage extends StatefulWidget {
const ContactGroupManagePage({Key? key}) : super(key: key);
@override
State<ContactGroupManagePage> createState() => _ContactGroupManagePageState();
}
class _ContactGroupManagePageState extends State<ContactGroupManagePage> {
@override
Widget build(BuildContext context) {
return Container();
}
}

View File

@@ -0,0 +1,17 @@
import 'package:flutter/material.dart';
class ContactGroupNotificationPage extends StatefulWidget {
const ContactGroupNotificationPage({Key? key}) : super(key: key);
@override
_ContactGroupNotificationPageState createState() =>
_ContactGroupNotificationPageState();
}
class _ContactGroupNotificationPageState
extends State<ContactGroupNotificationPage> {
@override
Widget build(BuildContext context) {
return Container();
}
}

View File

@@ -0,0 +1,82 @@
import 'package:chat/controllers/group_controller.dart';
import 'package:chat/controllers/private_controller.dart';
import 'package:chat/routes/conversation_routes.dart';
import 'package:chat/services/tim/conversation_service.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:tencent_im_sdk_plugin/enum/conversation_type.dart';
import 'package:tencent_im_sdk_plugin/models/v2_tim_conversation.dart';
class ConversationPage extends StatefulWidget {
const ConversationPage({Key? key}) : super(key: key);
@override
_ConversationPageState createState() => _ConversationPageState();
}
class _ConversationPageState extends State<ConversationPage> {
late final V2TimConversation conversation;
final _scaffoldKey = GlobalKey<ScaffoldState>();
final GlobalKey<dynamic> inputextField = GlobalKey();
@override
void initState() {
super.initState();
conversation = Get.arguments['conversation'];
/// 标记会话内消息已读
TimConversationService.to.markAsRead(conversation);
if (conversation.type == ConversationType.V2TIM_GROUP) {
GroupController.to.setCurrentGroup(conversation.groupID!);
} else {
PrivateController.to.setCurrentFriend(conversation.userID!);
}
}
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () {
inputextField.currentState.hideAllPanel();
},
child: Scaffold(
key: _scaffoldKey,
appBar: AppBar(
title: conversation.type == ConversationType.V2TIM_GROUP
? GetX<GroupController>(
builder: (_) {
return Text(
_.currentGroup.value.conversation?.showName ?? '',
);
},
)
: GetX<PrivateController>(
builder: (_) {
return Text(
_.currentFriend.value.conversation?.showName ?? '',
);
},
),
actions: [
_topRightAction(),
],
),
),
);
}
Widget _topRightAction() {
return IconButton(
icon: const Icon(Icons.more_horiz),
onPressed: () {
conversation.type == ConversationType.V2TIM_GROUP
? Get.toNamed(
ConversationRoutes.infoGroup,
)
: Get.toNamed(
ConversationRoutes.infoPrivate,
);
},
);
}
}

View File

@@ -0,0 +1,235 @@
import 'package:adaptive_dialog/adaptive_dialog.dart';
import 'package:chat/configs/app_colors.dart';
import 'package:chat/controllers/group_controller.dart';
import 'package:chat/routes/contact_routes.dart';
import 'package:chat/services/tim/conversation_service.dart';
import 'package:chat/services/tim/group_service.dart';
import 'package:chat/views/conversation/info/widgets/group_member_preview.dart';
import 'package:chat/views/home/widgets/action_button.dart';
import 'package:chat/views/home/widgets/action_item.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
class ConversationInfoGroupPage extends StatelessWidget {
const ConversationInfoGroupPage({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
GroupController.to.fetchGroupMemberList();
return GetX<GroupController>(builder: (_) {
var currentGroup = _.currentGroup.value;
var group = _.currentGroup.value.group;
return Scaffold(
appBar: AppBar(
title: Text('聊天信息(${group?.memberCount})'),
centerTitle: true,
),
body: SingleChildScrollView(
child: Column(
children: [
const GroupMemberPreview(),
const SizedBox(height: 8),
Container(
color: AppColors.white,
child: Column(
children: [
ActionItem(
'群聊名称',
extend: group?.groupName,
onTap: () {
if (currentGroup.isAdmin || currentGroup.isOwner) {
// Get.toNamed(
// ImRoutes.groupName,
// );
}
},
),
const Divider(height: 0, indent: 16),
ActionItem(
'群二维码',
onTap: () {
Get.toNamed(
ContactRoutes.groupQrCode,
);
},
),
const Divider(height: 0, indent: 16),
ActionItem(
'群公告',
bottom: group?.notification,
onTap: () {
Get.toNamed(
ContactRoutes.groupNotification,
);
},
),
Visibility(
visible: currentGroup.isAdmin || currentGroup.isOwner,
child: const Divider(
height: 0,
indent: 16,
),
),
Visibility(
visible: currentGroup.isAdmin || currentGroup.isOwner,
child: ActionItem(
'群管理',
onTap: () {
Get.toNamed(
ContactRoutes.groupManage,
);
},
),
),
Visibility(
visible: currentGroup.isAdmin || currentGroup.isOwner,
child: const Divider(
height: 0,
indent: 16,
),
),
Visibility(
visible: currentGroup.isAdmin || currentGroup.isOwner,
child: ActionItem(
'加群申请',
onTap: () {
Get.toNamed(
ContactRoutes.groupApprove,
);
},
),
),
],
),
),
const SizedBox(height: 8),
Container(
color: AppColors.white,
child: Column(
children: [
ActionItem(
'查找聊天记录',
onTap: () {
// Get.toNamed(
// ImRoutes.conversationSearch,
// );
},
),
],
),
),
const SizedBox(height: 8),
Container(
color: AppColors.white,
child: Column(
children: [
ActionItem(
'消息免打扰',
rightWidget: SizedBox(
height: 24,
child: Switch(
value:
_.currentGroup.value.conversation!.recvOpt == 1,
onChanged: (e) async {
_.toggleReceiveOpt();
},
materialTapTargetSize:
MaterialTapTargetSize.shrinkWrap,
),
),
),
const Divider(
height: 0,
indent: 16,
),
ActionItem(
'置顶聊天',
rightWidget: SizedBox(
height: 24,
child: Switch(
value: _.currentGroup.value.conversation!.isPinned!,
onChanged: (e) async {
_.togglePinned();
},
materialTapTargetSize:
MaterialTapTargetSize.shrinkWrap,
),
),
),
],
),
),
const SizedBox(height: 8),
Container(
color: AppColors.white,
child: Column(
children: [
ActionItem(
'我在群里的昵称',
extend: _.currentGroup.value.selfInfo?.nameCard ?? '',
onTap: () {
Get.toNamed(
ContactRoutes.groupNickname,
);
},
),
],
),
),
const SizedBox(height: 8),
Column(
children: [
ActionButton(
'清空聊天记录',
onTap: () async {
OkCancelResult result = await showOkCancelAlertDialog(
style: AdaptiveStyle.iOS,
context: Get.context!,
title: '系统提示',
message: '将删除该聊天记录,是否继续?',
okLabel: '确定',
cancelLabel: '取消',
defaultType: OkCancelAlertDefaultType.ok,
);
if (result == OkCancelResult.ok) {
TimConversationService.to.clearHistoryMessage(
_.currentGroup.value.conversation!,
);
}
},
),
if (!currentGroup.isOwner) const Divider(height: 0),
ActionButton(
'删除并退出',
onTap: () async {
OkCancelResult result = await showOkCancelAlertDialog(
style: AdaptiveStyle.iOS,
context: Get.context!,
title: '系统提示',
message: '将删除并退出该群组,是否继续?',
okLabel: '确定',
cancelLabel: '取消',
defaultType: OkCancelAlertDefaultType.ok,
);
if (result == OkCancelResult.ok) {
TimGroupService.to.quit(
group!,
);
Navigator.popUntil(context, (route) => route.isFirst);
}
},
),
],
),
const SizedBox(height: 16),
],
),
),
);
});
}
}

View File

@@ -0,0 +1,157 @@
import 'package:adaptive_dialog/adaptive_dialog.dart';
import 'package:chat/configs/app_colors.dart';
import 'package:chat/controllers/private_controller.dart';
import 'package:chat/routes/contact_routes.dart';
import 'package:chat/services/tim/conversation_service.dart';
import 'package:chat/views/home/widgets/action_item.dart';
import 'package:chat/widgets/custom_avatar.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
class ConversationInfoPrivatePage extends StatelessWidget {
const ConversationInfoPrivatePage({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return GetX<PrivateController>(builder: (_) {
return Scaffold(
appBar: AppBar(
title: const Text('聊天信息'),
),
body: SingleChildScrollView(
child: Column(
children: [
Container(
padding: const EdgeInsets.all(16),
decoration: const BoxDecoration(color: AppColors.white),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
InkWell(
onTap: () {
Get.toNamed(
ContactRoutes.friendProfile,
);
},
child: Column(
children: [
CustomAvatar(
_.currentFriend.value.userProfile?.faceUrl,
size: 54,
),
const SizedBox(height: 4),
Text(
_.currentFriend.value.conversation!.showName!,
style: const TextStyle(
color: AppColors.unactive,
fontSize: 12,
),
),
],
),
),
const SizedBox(width: 16),
InkWell(
onTap: () {
Get.toNamed(
ContactRoutes.groupCreate,
);
},
child: Container(
width: 54,
height: 54,
decoration: BoxDecoration(
color: AppColors.unactive.withOpacity(0.1),
border: Border.all(
color: AppColors.unactive.withOpacity(0.3),
width: 0.4,
style: BorderStyle.solid,
),
borderRadius: BorderRadius.circular(8),
),
child: const Center(
child: Icon(
Icons.add,
color: AppColors.unactive,
),
),
),
)
],
),
),
const Divider(height: 0),
const SizedBox(height: 8),
const Divider(height: 0),
ActionItem(
'查找聊天记录',
onTap: () {
// Get.toNamed(
// ImRoutes.conversationSearch,
// );
},
),
const Divider(height: 0),
const SizedBox(height: 8),
const Divider(height: 0),
ActionItem(
'消息免打扰',
rightWidget: SizedBox(
height: 24,
child: Switch(
value: _.currentFriend.value.conversation!.recvOpt == 1,
onChanged: (e) async {
_.changeReceiveOpt();
},
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
),
),
),
const Divider(
height: 0,
indent: 16,
),
ActionItem(
'置顶聊天',
rightWidget: SizedBox(
height: 24,
child: Switch(
value: _.currentFriend.value.conversation!.isPinned!,
onChanged: (e) async {
_.togglePinned();
},
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
),
),
),
const Divider(height: 0),
const SizedBox(height: 8),
const Divider(height: 0),
ActionItem(
'清空聊天记录',
onTap: () async {
OkCancelResult result = await showOkCancelAlertDialog(
style: AdaptiveStyle.iOS,
context: Get.context!,
title: '系统提示',
message: '将删除该聊天记录,是否继续?',
okLabel: '确定',
cancelLabel: '取消',
defaultType: OkCancelAlertDefaultType.ok,
);
if (result == OkCancelResult.ok) {
TimConversationService.to.clearHistoryMessage(
_.currentFriend.value.conversation!,
);
}
},
),
const Divider(height: 0),
],
),
),
);
});
}
}

View File

@@ -0,0 +1,209 @@
import 'dart:math';
import 'package:adaptive_dialog/adaptive_dialog.dart';
import 'package:chat/configs/app_colors.dart';
import 'package:chat/controllers/group_controller.dart';
import 'package:chat/controllers/private_controller.dart';
import 'package:chat/routes/contact_routes.dart';
import 'package:chat/widgets/custom_avatar.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:tencent_im_sdk_plugin/enum/group_member_role.dart';
import 'package:tencent_im_sdk_plugin/models/v2_tim_group_member_full_info.dart';
class GroupMemberPreview extends StatelessWidget {
const GroupMemberPreview({
Key? key,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return GetX<GroupController>(builder: (_) {
List<Widget> items = List<Widget>.empty(growable: true);
if (_.currentGroup.value.memberList != null) {
var members = _.currentGroup.value.memberList!;
if (members.length > 13) {
members = members.sublist(0, 13);
}
for (var item in members) {
items.add(_memberItem(item!));
}
}
/// 因为 Public 类型的群,不支持邀请功能,用户只能主动申请加群
items.add(
InkWell(
onTap: () async {
OkCancelResult result = await showOkCancelAlertDialog(
style: AdaptiveStyle.iOS,
context: Get.context!,
title: '系统提示',
message: '当前群聊不支持邀请用户,请分享群二维码至您要邀请的好友。',
okLabel: '去分享',
cancelLabel: '取消',
defaultType: OkCancelAlertDefaultType.ok,
);
if (result == OkCancelResult.ok) {
Get.toNamed(ContactRoutes.groupQrCode);
}
},
child: Column(
children: [
Container(
width: 44,
height: 44,
decoration: BoxDecoration(
color: AppColors.unactive.withOpacity(0.1),
border: Border.all(
color: AppColors.unactive.withOpacity(0.3),
width: 0.4,
style: BorderStyle.solid,
),
borderRadius: BorderRadius.circular(4),
),
child: const Center(
child: Icon(
Icons.add,
color: AppColors.unactive,
),
),
),
const Text(
'邀请',
style: TextStyle(
fontSize: 10,
color: AppColors.unactive,
),
),
],
),
),
);
if (_.currentGroup.value.isAdmin || _.currentGroup.value.isOwner) {
items.add(
InkWell(
onTap: () {
Get.toNamed(
ContactRoutes.groupKick,
);
},
child: Column(
children: [
Container(
width: 44,
height: 44,
decoration: BoxDecoration(
color: AppColors.unactive.withOpacity(0.1),
border: Border.all(
color: AppColors.unactive.withOpacity(0.3),
width: 0.4,
style: BorderStyle.solid,
),
borderRadius: BorderRadius.circular(4),
),
child: const Center(
child: Icon(
Icons.remove,
color: AppColors.unactive,
),
),
),
const Text(
'移除',
style: TextStyle(
fontSize: 10,
color: AppColors.unactive,
),
),
],
),
),
);
}
return Container(
color: AppColors.white,
padding: const EdgeInsets.all(16),
child: GridView.count(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
crossAxisCount: 5,
childAspectRatio: 1 / 1.1,
crossAxisSpacing: 12,
mainAxisSpacing: 12,
children: items,
),
);
});
}
Widget _memberItem(V2TimGroupMemberFullInfo member) {
double w = 40;
double h = 12;
return InkWell(
onTap: () async {
await PrivateController.to.setCurrentFriend(member.userID);
Get.toNamed(
ContactRoutes.friendProfile,
);
},
child: Column(
children: [
ClipRRect(
child: Stack(
children: [
CustomAvatar(member.faceUrl),
if (member.role ==
GroupMemberRoleType.V2TIM_GROUP_MEMBER_ROLE_ADMIN ||
member.role ==
GroupMemberRoleType.V2TIM_GROUP_MEMBER_ROLE_OWNER)
Positioned(
left: 0,
top: sqrt(w * w / 2 - sqrt2 * w * h + h * h),
child: Transform.rotate(
angle: -0.25 * pi,
alignment: Alignment.bottomLeft,
child: Container(
color: member.role ==
GroupMemberRoleType
.V2TIM_GROUP_MEMBER_ROLE_OWNER
? AppColors.red
: AppColors.golden,
width: w,
height: h,
alignment: Alignment.center,
child: Text(
member.role ==
GroupMemberRoleType
.V2TIM_GROUP_MEMBER_ROLE_OWNER
? '群主'
: '管理员',
style: const TextStyle(
color: AppColors.white,
fontSize: 8,
),
),
),
),
),
],
),
),
const SizedBox(height: 2),
Text(
member.nameCard!.isNotEmpty ? member.nameCard! : member.nickName!,
style: const TextStyle(
fontSize: 10,
color: AppColors.unactive,
),
)
],
),
);
}
}

View File

@@ -1,19 +1,219 @@
import 'package:chat/configs/app_colors.dart';
import 'package:chat/routes/app_routes.dart';
import 'package:chat/routes/contact_routes.dart';
import 'package:chat/routes/user_routes.dart';
import 'package:chat/services/tim/conversation_service.dart';
import 'package:chat/views/home/widgets/conversation_item.dart';
import 'package:chat/widgets/custom_easy_refresh.dart';
import 'package:flutter/material.dart';
import 'package:flutter_easyrefresh/easy_refresh.dart';
import 'package:get/get.dart';
import 'package:permission_handler/permission_handler.dart';
class HomePage extends StatefulWidget {
class HomePage extends StatelessWidget {
const HomePage({Key? key}) : super(key: key);
@override
_HomePageState createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('消息'),
backgroundColor: AppColors.white,
appBar: _appBar(),
body: EasyRefresh(
header: CustomEasyRefresh.header,
// firstRefresh: true,
onRefresh: () async {
await TimConversationService.to.fetchList();
},
child: GetX<TimConversationService>(
builder: (_) {
return _.conversationList.isEmpty
? CustomEasyRefresh.empty()
: ListView.separated(
shrinkWrap: true,
physics: const ClampingScrollPhysics(),
itemCount: _.conversationList.length,
itemBuilder: (context, index) {
return ConversationItem(_.conversationList[index]!);
},
separatorBuilder: (context, index) {
return const Divider(
height: 0,
indent: 72,
);
},
);
},
),
),
);
}
PreferredSizeWidget _appBar() {
return AppBar(
title: const Text('聊聊'),
actions: [
IconButton(
onPressed: () {
Get.toNamed(AppRoutes.search);
},
icon: const Icon(Icons.search_outlined),
),
PopupMenuButton<String>(
onSelected: (String value) {
switch (value) {
case 'A':
Get.toNamed(UserRoutes.qrCode);
break;
case 'B':
Get.toNamed(ContactRoutes.groupCreate);
break;
case 'C':
Get.toNamed(ContactRoutes.friendSearch);
break;
case 'D':
Permission.camera.request().isGranted.then((value) {
if (value) {
Get.toNamed(AppRoutes.scan);
}
});
break;
}
},
tooltip: '',
offset: const Offset(0, 56),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
icon: const Icon(
Icons.add,
),
itemBuilder: (_) {
return [
_popupMenuItem('我的二维码', 'A', Icons.qr_code_outlined),
_popupMenuItem('发起群聊', 'B', Icons.textsms),
_popupMenuItem('添加朋友', 'C', Icons.person_add_alt),
_popupMenuItem('扫一扫', 'D', Icons.photo_camera),
];
},
)
],
);
}
/// 右上角弹出菜单
PopupMenuItem<String> _popupMenuItem(
String text,
String value,
IconData icon,
) {
return PopupMenuItem(
value: value,
child: Row(
children: [
Icon(
icon,
color: AppColors.primary,
size: 18,
),
const SizedBox(width: 8),
Text(
text,
style: const TextStyle(
fontSize: 14,
),
),
],
),
);
}
// /// 左侧抽题
// Widget _drawer() {
// return Drawer(
// child: ListView(
// children: [
// GetX<UserController>(builder: (_) {
// return DrawerHeader(
// child: Column(
// crossAxisAlignment: CrossAxisAlignment.start,
// children: [
// CustomCircleAvatar(
// _.userInfo.value!.avatar,
// size: 72,
// ),
// const SizedBox(height: 8),
// Text(
// _.userInfo.value!.nickname,
// style: const TextStyle(
// fontSize: 24,
// fontWeight: FontWeight.bold,
// ),
// ),
// ],
// ),
// );
// }),
// ListTile(
// onTap: () {
// Get.back();
// Get.toNamed(UserRoutes.info);
// },
// leading: const Icon(Icons.info_outlined),
// title: const Text('修改资料'),
// ),
// const Divider(height: 0),
// ListTile(
// onTap: () {
// Get.back();
// Get.toNamed(
// ImRoutes.friend,
// arguments: {
// 'name_card': false,
// },
// );
// },
// leading: const Icon(Icons.person_outlined),
// title: const Text('我的好友'),
// ),
// const Divider(height: 0),
// ListTile(
// onTap: () {
// Get.back();
// Get.toNamed(ImRoutes.group);
// },
// leading: const Icon(Icons.group_outlined),
// title: const Text('我的群组'),
// ),
// const Divider(height: 0),
// ListTile(
// onTap: () {
// Get.back();
// Get.toNamed(ImRoutes.blcok);
// },
// leading: const Icon(Icons.block_flipped),
// title: const Text('黑名单'),
// ),
// const Divider(height: 0),
// ListTile(
// onTap: () {
// Get.back();
// Get.toNamed(ImRoutes.friendRequest);
// },
// leading: const Icon(Icons.person_add_alt_outlined),
// title: const Text('好友申请'),
// ),
// const Divider(height: 0),
// ListTile(
// onTap: () {
// Get.back();
// Get.toNamed(ImRoutes.setting);
// },
// leading: const Icon(Icons.settings_outlined),
// title: const Text('消息设置'),
// ),
// const Divider(height: 0),
// ],
// ),
// );
// }
}

View File

@@ -0,0 +1,39 @@
import 'package:chat/configs/app_colors.dart';
import 'package:flutter/material.dart';
class ActionButton extends StatelessWidget {
final String text;
final Color color;
final VoidCallback? onTap;
const ActionButton(
this.text, {
this.color = AppColors.red,
this.onTap,
Key? key,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () {
onTap?.call();
},
child: Container(
color: AppColors.white,
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Center(
child: Text(
text,
style: TextStyle(
fontWeight: FontWeight.w500,
color: color,
),
),
),
),
),
);
}
}

View File

@@ -0,0 +1,76 @@
import 'package:chat/configs/app_colors.dart';
import 'package:flutter/material.dart';
class ActionItem extends StatelessWidget {
final String title;
final String? extend;
final Widget? rightWidget;
final String? bottom;
final VoidCallback? onTap;
const ActionItem(
this.title, {
this.extend,
this.rightWidget,
this.bottom,
this.onTap,
Key? key,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () {
onTap?.call();
},
child: Container(
color: AppColors.white,
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
title,
style: const TextStyle(
fontSize: 16,
),
),
Expanded(child: Container()),
if (extend != null)
Text(
extend!,
style: const TextStyle(
color: AppColors.unactive,
),
),
rightWidget ??
const Icon(
Icons.arrow_forward_ios,
size: 16,
color: AppColors.unactive,
),
],
),
if (bottom != null && bottom!.isNotEmpty)
Padding(
padding: const EdgeInsets.only(top: 4),
child: Text(
bottom!,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
color: AppColors.unactive,
fontSize: 12,
),
),
),
],
),
),
);
}
}

View File

@@ -0,0 +1,212 @@
import 'package:chat/configs/app_colors.dart';
import 'package:chat/routes/conversation_routes.dart';
import 'package:chat/services/tim/conversation_service.dart';
import 'package:chat/utils/convert.dart';
import 'package:chat/views/home/widgets/group_avatar.dart';
import 'package:chat/views/home/widgets/message_preview_widget.dart';
import 'package:chat/views/home/widgets/pop_menu_item.dart';
import 'package:chat/widgets/custom_avatar.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:tencent_im_sdk_plugin/enum/conversation_type.dart';
import 'package:tencent_im_sdk_plugin/models/v2_tim_conversation.dart';
class ConversationItem extends StatelessWidget {
final V2TimConversation conversation;
const ConversationItem(this.conversation, {Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Container(
height: 68,
decoration: BoxDecoration(
color: conversation.isPinned! ? AppColors.page : null,
),
padding: const EdgeInsets.only(
left: 16,
right: 16,
top: 12,
bottom: 12,
),
child: GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () {
Get.toNamed(
ConversationRoutes.index,
arguments: {
'conversation': conversation,
},
);
},
onLongPress: () async {
await _showLongPressMenu();
},
child: Row(
children: [
Stack(
clipBehavior: Clip.none,
children: [
conversation.type == ConversationType.V2TIM_C2C
? CustomAvatar(
conversation.faceUrl,
)
: GroupAvatar(conversation.groupID!),
Visibility(
visible: conversation.recvOpt == 0 &&
conversation.unreadCount! > 0,
child: Positioned(
right: -5,
top: -5,
child: Container(
width: 18,
height: 18,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(18),
color: AppColors.red,
),
alignment: Alignment.center,
child: Center(
child: Text(
conversation.unreadCount! > 99
? '99+'
: conversation.unreadCount.toString(),
style: const TextStyle(
fontSize: 10,
color: AppColors.white,
),
),
),
),
),
),
Visibility(
visible: conversation.recvOpt == 1 &&
conversation.unreadCount! > 0,
child: const Positioned(
right: -3,
top: -3,
child: Icon(
Icons.circle_rounded,
color: AppColors.red,
size: 8,
),
),
)
],
),
const SizedBox(width: 16),
Expanded(
child: Column(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
conversation.showName!,
style: const TextStyle(
fontSize: 16,
),
),
MessagePreviewWidget(conversation.lastMessage),
],
),
),
Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Text(
conversation.lastMessage == null
? ''
: Convert.timeFormat(
conversation.lastMessage!.timestamp!,
format: 'MM/dd HH:mm',
),
style: const TextStyle(
fontSize: 12,
color: Colors.grey,
),
),
if (conversation.recvOpt == 1)
const Icon(
Icons.notifications_off_outlined,
size: 14,
color: AppColors.unactive,
),
],
),
],
),
),
);
}
Future<void> _showLongPressMenu() async {
showModalBottomSheet(
context: Get.context!,
isScrollControlled: true,
backgroundColor: AppColors.white,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(8)),
),
builder: (context) {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
PopMenuItem(
conversation.isPinned! ? '取消置顶' : '聊天置顶',
onTap: () {
TimConversationService.to.setOnTop(conversation);
Get.back();
},
),
const Divider(height: 0),
PopMenuItem(
conversation.recvOpt == 1 ? '取消免打扰' : '消息免打扰',
onTap: () {
TimConversationService.to.setReceiveOpt(conversation);
Get.back();
},
),
const Divider(height: 0),
PopMenuItem(
'清空聊天记录',
onTap: () {
TimConversationService.to.clearHistoryMessage(conversation);
Get.back();
},
),
const Divider(height: 0),
PopMenuItem(
'标为已读',
onTap: () {
TimConversationService.to.markAsRead(conversation);
Get.back();
},
),
const Divider(height: 0),
PopMenuItem(
'删除该聊天',
onTap: () {
TimConversationService.to.delete(conversation);
Get.back();
},
),
const Divider(height: 0.4),
Container(
color: AppColors.page,
height: 8,
),
const Divider(height: 0.4),
PopMenuItem(
'取消',
onTap: () {
Get.back();
},
),
],
);
},
);
}
}

View File

@@ -0,0 +1,140 @@
import 'package:azlistview/azlistview.dart';
import 'package:chat/configs/app_colors.dart';
import 'package:chat/models/im/contact_info_model.dart';
import 'package:chat/services/tim/friend_service.dart';
import 'package:chat/utils/im_tools.dart';
import 'package:chat/widgets/custom_avatar.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:tencent_im_sdk_plugin/models/v2_tim_friend_info.dart';
import 'package:tencent_im_sdk_plugin/models/v2_tim_group_member_full_info.dart';
class FriendSelector extends StatefulWidget {
final Function(List<V2TimFriendInfo>) onChanged;
final List<V2TimGroupMemberFullInfo?>? lockedUsers;
const FriendSelector({
required this.onChanged,
this.lockedUsers,
Key? key,
}) : super(key: key);
@override
State<FriendSelector> createState() => _FriendSelectorState();
}
class _FriendSelectorState extends State<FriendSelector> {
/// 选中的好友列表
List<V2TimFriendInfo> selectList =
List<V2TimFriendInfo>.empty(growable: true);
/// 选择列表改变的事件,更新选中列表
_selectListChange(id) {
setState(() {
if (selectList.contains(id)) {
selectList.remove(id);
} else {
selectList.add(id);
}
});
widget.onChanged(selectList);
}
@override
Widget build(BuildContext context) {
return GetX<TimFriendService>(
builder: (_) {
return AzListView(
data: _.contacts,
itemCount: _.contacts.length,
itemBuilder: (BuildContext context, int index) {
ContactInfoModel info = _.contacts[index];
return _contactItem(info);
},
susItemBuilder: (BuildContext context, int index) {
ContactInfoModel model = _.contacts[index];
return ImTools.susItem(
context,
model.getSuspensionTag(),
susHeight: 32,
);
},
indexBarData: SuspensionUtil.getTagIndexList(_.contacts).toList(),
indexBarOptions: ImTools.indexBarOptions,
);
},
);
}
Widget _contactItem(
ContactInfoModel info,
) {
bool isDisable = widget.lockedUsers == null
? false
: widget.lockedUsers!
.where(
(e) => e!.userID == info.friendInfo!.userID,
)
.isNotEmpty;
return Column(
children: [
GestureDetector(
onTap: () {},
child: Container(
color: AppColors.white,
padding: const EdgeInsets.symmetric(vertical: 4),
child: GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: isDisable
? null
: () {
setState(() {
_selectListChange(info.friendInfo);
});
},
child: Row(
children: [
Radio<V2TimFriendInfo>(
groupValue:
selectList.contains(info.friendInfo) || isDisable
? info.friendInfo
: null,
onChanged: isDisable
? null
: (value) {
setState(() {
_selectListChange(info.friendInfo);
});
},
value: info.friendInfo!,
toggleable: true,
),
CustomAvatar(
info.friendInfo!.userProfile!.faceUrl,
size: 32,
radius: 2,
),
const SizedBox(
width: 16,
),
Expanded(
child: Text(
info.name,
style: const TextStyle(
fontSize: 16,
),
),
),
],
),
),
),
),
const Divider(
height: 0,
indent: 92,
),
],
);
}
}

View File

@@ -0,0 +1,68 @@
import 'package:chat/configs/app_colors.dart';
import 'package:chat/services/tim/group_service.dart';
import 'package:chat/widgets/custom_avatar.dart';
import 'package:flutter/material.dart';
import 'package:tencent_im_sdk_plugin/models/v2_tim_group_member_full_info.dart';
class GroupAvatar extends StatefulWidget {
final String groupID;
final double size;
const GroupAvatar(
this.groupID, {
this.size = 44,
Key? key,
}) : super(key: key);
@override
State<GroupAvatar> createState() => _GroupAvatarState();
}
class _GroupAvatarState extends State<GroupAvatar> {
List<V2TimGroupMemberFullInfo?>? members;
@override
void initState() {
super.initState();
TimGroupService.to.members(widget.groupID, count: 9).then((value) {
setState(() {
members = value;
});
});
}
@override
Widget build(BuildContext context) {
return Container(
width: widget.size,
height: widget.size,
decoration: BoxDecoration(
border: Border.all(
color: AppColors.border,
width: 0.4,
),
borderRadius: BorderRadius.circular(4),
),
child: members == null
? CustomAvatar('')
: GridView.builder(
padding: const EdgeInsets.all(1),
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount:
(members != null && members!.length > 4) ? 3 : 2,
childAspectRatio: 1,
crossAxisSpacing: 1,
mainAxisSpacing: 1,
),
itemCount: members!.length,
itemBuilder: (context, index) {
return CustomAvatar(
members![index]?.faceUrl,
size: widget.size / 3,
radius: 2,
);
},
),
);
}
}

View File

@@ -0,0 +1,111 @@
import 'package:chat/configs/app_colors.dart';
import 'package:chat/widgets/custom_avatar.dart';
import 'package:flutter/material.dart';
import 'package:tencent_im_sdk_plugin/models/v2_tim_group_member_full_info.dart';
class GroupUserSelector extends StatefulWidget {
final Function(List<V2TimGroupMemberFullInfo>) onChanged;
final List<V2TimGroupMemberFullInfo> selectedList;
final List<V2TimGroupMemberFullInfo?> originalList;
const GroupUserSelector({
required this.onChanged,
required this.selectedList,
required this.originalList,
Key? key,
}) : super(key: key);
@override
State<GroupUserSelector> createState() => _GroupUserSelectorState();
}
class _GroupUserSelectorState extends State<GroupUserSelector> {
/// 选中的好友列表
List<V2TimGroupMemberFullInfo> selectList =
List<V2TimGroupMemberFullInfo>.empty(growable: true);
/// 选择列表改变的事件,更新选中列表
_selectListChange(id) {
setState(() {
if (selectList.contains(id)) {
selectList.remove(id);
} else {
selectList.add(id);
}
});
widget.onChanged(selectList);
}
@override
Widget build(BuildContext context) {
return ListView.separated(
itemBuilder: (_, index) {
return _contactItem(widget.originalList[index]!);
},
separatorBuilder: (_, index) {
return const Divider(
height: 0,
);
},
itemCount: widget.originalList.length,
);
}
Widget _contactItem(
V2TimGroupMemberFullInfo info,
) {
return Column(
children: [
GestureDetector(
onTap: () {},
child: Container(
color: AppColors.white,
padding: const EdgeInsets.symmetric(vertical: 4),
child: GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () {
setState(() {
_selectListChange(info);
});
},
child: Row(
children: [
Radio<V2TimGroupMemberFullInfo>(
groupValue: selectList.contains(info) ? info : null,
onChanged: (value) {
setState(() {
_selectListChange(info);
});
},
value: info,
toggleable: true,
),
CustomAvatar(
info.faceUrl,
size: 32,
radius: 2,
),
const SizedBox(
width: 16,
),
Expanded(
child: Text(
info.nickName!,
style: const TextStyle(
fontSize: 16,
),
),
),
],
),
),
),
),
const Divider(
height: 0,
indent: 92,
),
],
);
}
}

View File

@@ -0,0 +1,26 @@
import 'package:chat/configs/app_colors.dart';
import 'package:chat/utils/im_tools.dart';
import 'package:flutter/material.dart';
import 'package:tencent_im_sdk_plugin/models/v2_tim_message.dart';
class MessagePreviewWidget extends StatelessWidget {
final V2TimMessage? message;
const MessagePreviewWidget(this.message, {Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
if (message == null) {
return const Text('');
}
return Text(
ImTools.parseMessage(message),
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
color: AppColors.unactive,
fontSize: 14,
),
);
}
}

View File

@@ -0,0 +1,29 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
class PopMenuItem extends StatelessWidget {
final String text;
final VoidCallback? onTap;
const PopMenuItem(
this.text, {
this.onTap,
Key? key,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () {
onTap?.call();
},
child: Container(
height: 52,
width: Get.width,
alignment: Alignment.center,
child: Text(text),
),
);
}
}

View File

@@ -20,19 +20,23 @@ class AppPage extends StatelessWidget {
final List<Map> _tabBarList = [
{
'icon': 'tabBar_03.png',
'icon': Icons.message_outlined,
'active_icon': Icons.message,
'label': '消息',
},
{
'icon': 'tabBar_03.png',
'icon': Icons.contact_page_outlined,
'active_icon': Icons.contact_page,
'label': '通讯录',
},
{
'icon': 'tabBar_03.png',
'icon': Icons.explore_outlined,
'active_icon': Icons.explore,
'label': '发现',
},
{
'icon': 'tabBar_03.png',
'icon': Icons.person_outline,
'active_icon': Icons.person,
'label': '我的',
},
];
@@ -47,16 +51,14 @@ class AppPage extends StatelessWidget {
},
items: _tabBarList.map((item) {
return BottomNavigationBarItem(
icon: Image.asset(
'assets/icons/${item['icon']}',
width: 20,
height: 20,
icon: Icon(
item['icon'],
size: 24,
),
activeIcon: Image.asset(
'assets/icons/${item['icon']}',
activeIcon: Icon(
item['active_icon'],
size: 24,
color: AppColors.primary,
width: 20,
height: 20,
),
label: item['label'],
tooltip: '',

View File

@@ -0,0 +1,15 @@
import 'package:flutter/material.dart';
class UserQrCodePage extends StatefulWidget {
const UserQrCodePage({Key? key}) : super(key: key);
@override
_UserQrCodePageState createState() => _UserQrCodePageState();
}
class _UserQrCodePageState extends State<UserQrCodePage> {
@override
Widget build(BuildContext context) {
return Container();
}
}