会话页面
This commit is contained in:
37
lib/constants/message_constant.dart
Normal file
37
lib/constants/message_constant.dart
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
class HistoryMessageDartConstant {
|
||||||
|
static const getCount = 20;
|
||||||
|
|
||||||
|
// ignore: constant_identifier_names
|
||||||
|
static const V2_TIM_IMAGE_TYPES = {
|
||||||
|
'ORIGINAL': 0,
|
||||||
|
'BIG': 1,
|
||||||
|
'SMALL': 2,
|
||||||
|
};
|
||||||
|
|
||||||
|
static Map<V2_TIM_IMAGE_TYPES_ENUM, List<String>> imgPriorMap = {
|
||||||
|
V2_TIM_IMAGE_TYPES_ENUM.original: oriImgPrior,
|
||||||
|
V2_TIM_IMAGE_TYPES_ENUM.big: bigImgPrior,
|
||||||
|
V2_TIM_IMAGE_TYPES_ENUM.small: smallImgPrior,
|
||||||
|
};
|
||||||
|
|
||||||
|
// 缩略图优先,大图次之,最后是原图
|
||||||
|
static const smallImgPrior = ['ORIGINAL', 'BIG', 'SMALL'];
|
||||||
|
// 大图优先,原图次之,最后是缩略图
|
||||||
|
static const bigImgPrior = ['SMALL', 'ORIGINAL', 'BIG'];
|
||||||
|
// 原图优先,大图次之,最后是缩略图
|
||||||
|
static const oriImgPrior = ['SMALL', 'BIG', 'ORIGINAL'];
|
||||||
|
|
||||||
|
// 视频、音频已读状态
|
||||||
|
static const int read = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
enum V2_TIM_IMAGE_TYPES_ENUM {
|
||||||
|
original,
|
||||||
|
big,
|
||||||
|
small,
|
||||||
|
}
|
||||||
|
|
||||||
|
enum IMG_PREVIEW_TYPE {
|
||||||
|
local,
|
||||||
|
url,
|
||||||
|
}
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
import 'package:chat/models/upload_model.dart';
|
import 'package:chat/models/upload_model.dart';
|
||||||
import 'package:chat/models/user_info_model.dart';
|
|
||||||
import 'package:chat/providers/user_provider.dart';
|
import 'package:chat/providers/user_provider.dart';
|
||||||
import 'package:chat/services/auth_service.dart';
|
import 'package:chat/services/auth_service.dart';
|
||||||
import 'package:chat/services/tim/friend_service.dart';
|
import 'package:chat/services/tim/friend_service.dart';
|
||||||
@@ -9,9 +8,6 @@ import 'package:get/get.dart';
|
|||||||
class UserController extends GetxController {
|
class UserController extends GetxController {
|
||||||
static UserController get to => Get.find<UserController>();
|
static UserController get to => Get.find<UserController>();
|
||||||
|
|
||||||
/// 用户信息,这个数据,在更新用户资料的时候,也应该更新
|
|
||||||
Rx<UserInfoModel> userInfo = UserInfoModel.empty().obs;
|
|
||||||
|
|
||||||
Future<bool> updateNickname(String nickname) async {
|
Future<bool> updateNickname(String nickname) async {
|
||||||
var result = await UserProvider.updateNickname(nickname);
|
var result = await UserProvider.updateNickname(nickname);
|
||||||
if (result) {
|
if (result) {
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ class ContactInfoModel extends ISuspensionBean {
|
|||||||
V2TimFriendInfo? friendInfo;
|
V2TimFriendInfo? friendInfo;
|
||||||
IconData? icon;
|
IconData? icon;
|
||||||
Color? color;
|
Color? color;
|
||||||
|
String? route;
|
||||||
|
|
||||||
ContactInfoModel({
|
ContactInfoModel({
|
||||||
required this.name,
|
required this.name,
|
||||||
@@ -19,6 +20,7 @@ class ContactInfoModel extends ISuspensionBean {
|
|||||||
this.friendInfo,
|
this.friendInfo,
|
||||||
this.icon,
|
this.icon,
|
||||||
this.color,
|
this.color,
|
||||||
|
this.route,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import 'package:chat/models/im/search_user_model.dart';
|
||||||
import 'package:chat/models/upload_model.dart';
|
import 'package:chat/models/upload_model.dart';
|
||||||
import 'package:chat/utils/network/http.dart';
|
import 'package:chat/utils/network/http.dart';
|
||||||
import 'package:chat/utils/ui_tools.dart';
|
import 'package:chat/utils/ui_tools.dart';
|
||||||
@@ -32,4 +33,25 @@ class UserProvider {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 从服务器查找用户
|
||||||
|
static 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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ abstract class ContactRoutes {
|
|||||||
static const String friendProfile = '/contact/friend/profile';
|
static const String friendProfile = '/contact/friend/profile';
|
||||||
static const String friendProfileMore = '/contact/friend/profile/more';
|
static const String friendProfileMore = '/contact/friend/profile/more';
|
||||||
static const String friendRemark = '/contact/friend/remark';
|
static const String friendRemark = '/contact/friend/remark';
|
||||||
|
static const String friendRequest = '/contact/friend/request';
|
||||||
static const String friendRequestApply = '/contact/friend/request/apply';
|
static const String friendRequestApply = '/contact/friend/request/apply';
|
||||||
static const String friendRecommend = '/contact/friend/recommend';
|
static const String friendRecommend = '/contact/friend/recommend';
|
||||||
static const String friendRecommendFriend =
|
static const String friendRecommendFriend =
|
||||||
@@ -31,6 +32,7 @@ abstract class ContactRoutes {
|
|||||||
static const String friendRecommendGroup = '/contact/friend/recommend/group';
|
static const String friendRecommendGroup = '/contact/friend/recommend/group';
|
||||||
|
|
||||||
static const String group = '/contact/group';
|
static const String group = '/contact/group';
|
||||||
|
static const String groupSearch = '/contact/group/search';
|
||||||
static const String groupQrCode = '/contact/group/qrCode';
|
static const String groupQrCode = '/contact/group/qrCode';
|
||||||
static const String groupCreate = '/contact/group/create';
|
static const String groupCreate = '/contact/group/create';
|
||||||
static const String groupNotification = '/contact/group/notification';
|
static const String groupNotification = '/contact/group/notification';
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
import 'package:azlistview/azlistview.dart';
|
import 'package:azlistview/azlistview.dart';
|
||||||
import 'package:chat/models/im/contact_info_model.dart';
|
import 'package:chat/models/im/contact_info_model.dart';
|
||||||
import 'package:chat/models/im/search_user_model.dart';
|
import 'package:chat/routes/contact_routes.dart';
|
||||||
import 'package:chat/services/tim_service.dart';
|
import 'package:chat/services/tim_service.dart';
|
||||||
import 'package:chat/utils/im_tools.dart';
|
import 'package:chat/utils/im_tools.dart';
|
||||||
import 'package:chat/utils/network/http.dart';
|
|
||||||
import 'package:chat/utils/ui_tools.dart';
|
import 'package:chat/utils/ui_tools.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
@@ -35,6 +34,7 @@ class TimFriendService extends GetxService {
|
|||||||
tagIndex: '@',
|
tagIndex: '@',
|
||||||
icon: Icons.person_add_alt,
|
icon: Icons.person_add_alt,
|
||||||
color: Colors.amber,
|
color: Colors.amber,
|
||||||
|
route: ContactRoutes.friendRequest,
|
||||||
),
|
),
|
||||||
ContactInfoModel(
|
ContactInfoModel(
|
||||||
name: '群聊',
|
name: '群聊',
|
||||||
@@ -42,6 +42,7 @@ class TimFriendService extends GetxService {
|
|||||||
tagIndex: '@',
|
tagIndex: '@',
|
||||||
icon: Icons.group,
|
icon: Icons.group,
|
||||||
color: Colors.green,
|
color: Colors.green,
|
||||||
|
route: ContactRoutes.group,
|
||||||
),
|
),
|
||||||
ContactInfoModel(
|
ContactInfoModel(
|
||||||
name: '订阅消息',
|
name: '订阅消息',
|
||||||
@@ -243,25 +244,4 @@ class TimFriendService extends GetxService {
|
|||||||
UiTools.toast(result.desc);
|
UiTools.toast(result.desc);
|
||||||
return false;
|
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
66
lib/utils/sound_record.dart
Normal file
66
lib/utils/sound_record.dart
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:flutter_plugin_record_plus/const/play_state.dart';
|
||||||
|
import 'package:flutter_plugin_record_plus/const/response.dart';
|
||||||
|
import 'package:flutter_plugin_record_plus/index.dart';
|
||||||
|
|
||||||
|
typedef PlayStateListener = void Function(PlayState playState);
|
||||||
|
typedef SoundInterruptListener = void Function();
|
||||||
|
typedef ResponseListener = void Function(RecordResponse recordResponse);
|
||||||
|
|
||||||
|
class SoundPlayer {
|
||||||
|
static final FlutterPluginRecord _recorder = FlutterPluginRecord();
|
||||||
|
static SoundInterruptListener? _soundInterruptListener;
|
||||||
|
static bool isInited = false;
|
||||||
|
|
||||||
|
static initSoundPlayer() {
|
||||||
|
if (!isInited) {
|
||||||
|
_recorder.init();
|
||||||
|
isInited = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static play({required String url}) {
|
||||||
|
_recorder.stopPlay();
|
||||||
|
if (_soundInterruptListener != null) {
|
||||||
|
_soundInterruptListener!();
|
||||||
|
}
|
||||||
|
_recorder.playByPath(url, 'url');
|
||||||
|
}
|
||||||
|
|
||||||
|
static stop() {
|
||||||
|
_recorder.stopPlay();
|
||||||
|
}
|
||||||
|
|
||||||
|
static dispose() {
|
||||||
|
_recorder.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
static StreamSubscription<PlayState> playStateListener(
|
||||||
|
{required PlayStateListener listener}) =>
|
||||||
|
_recorder.responsePlayStateController.listen(listener);
|
||||||
|
|
||||||
|
static setSoundInterruptListener(SoundInterruptListener listener) {
|
||||||
|
_soundInterruptListener = listener;
|
||||||
|
}
|
||||||
|
|
||||||
|
static removeSoundInterruptListener() {
|
||||||
|
_soundInterruptListener = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
static StreamSubscription<RecordResponse> responseListener(
|
||||||
|
ResponseListener listener) =>
|
||||||
|
_recorder.response.listen(listener);
|
||||||
|
|
||||||
|
static StreamSubscription<RecordResponse> responseFromAmplitudeListener(
|
||||||
|
ResponseListener listener) =>
|
||||||
|
_recorder.responseFromAmplitude.listen(listener);
|
||||||
|
|
||||||
|
static startRecord() {
|
||||||
|
_recorder.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
static stopRecord() {
|
||||||
|
_recorder.stop();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import 'package:chat/configs/app_colors.dart';
|
import 'package:chat/configs/app_colors.dart';
|
||||||
import 'package:chat/controllers/private_controller.dart';
|
import 'package:chat/controllers/private_controller.dart';
|
||||||
import 'package:chat/models/im/search_user_model.dart';
|
import 'package:chat/models/im/search_user_model.dart';
|
||||||
|
import 'package:chat/providers/user_provider.dart';
|
||||||
import 'package:chat/routes/contact_routes.dart';
|
import 'package:chat/routes/contact_routes.dart';
|
||||||
import 'package:chat/services/tim/apply_service.dart';
|
import 'package:chat/services/tim/apply_service.dart';
|
||||||
import 'package:chat/services/tim/friend_service.dart';
|
import 'package:chat/services/tim/friend_service.dart';
|
||||||
@@ -38,7 +39,7 @@ class _ImFriendRequestState extends State<ContactFriendRequestPage> {
|
|||||||
searchList = null;
|
searchList = null;
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
var result = await TimFriendService.to.searchUser(e);
|
var result = await UserProvider.searchUser(e);
|
||||||
|
|
||||||
setState(() {
|
setState(() {
|
||||||
searchList = result;
|
searchList = result;
|
||||||
|
|||||||
@@ -1,15 +1,101 @@
|
|||||||
|
import 'package:chat/configs/app_colors.dart';
|
||||||
|
import 'package:chat/routes/contact_routes.dart';
|
||||||
|
import 'package:chat/routes/conversation_routes.dart';
|
||||||
|
import 'package:chat/services/tim/conversation_service.dart';
|
||||||
|
import 'package:chat/services/tim/group_service.dart';
|
||||||
|
import 'package:chat/views/home/widgets/group_avatar.dart';
|
||||||
|
import 'package:chat/widgets/custom_easy_refresh.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_easyrefresh/easy_refresh.dart';
|
||||||
|
import 'package:get/get.dart';
|
||||||
|
import 'package:tencent_im_sdk_plugin/models/v2_tim_group_info.dart';
|
||||||
|
|
||||||
class ContactGroupPage extends StatefulWidget {
|
class ContactGroupPage extends StatefulWidget {
|
||||||
const ContactGroupPage({Key? key}) : super(key: key);
|
const ContactGroupPage({Key? key}) : super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
_ContactGroupPageState createState() => _ContactGroupPageState();
|
State<ContactGroupPage> createState() => _ContactGroupPageState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _ContactGroupPageState extends State<ContactGroupPage> {
|
class _ContactGroupPageState extends State<ContactGroupPage> {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Container();
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: const Text('群聊'),
|
||||||
|
actions: [
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.search_outlined),
|
||||||
|
onPressed: () {
|
||||||
|
Get.toNamed(ContactRoutes.groupSearch);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.add_outlined),
|
||||||
|
onPressed: () {
|
||||||
|
Get.toNamed(ContactRoutes.groupCreate);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
body: GetX<TimGroupService>(builder: (_) {
|
||||||
|
return EasyRefresh(
|
||||||
|
onRefresh: () async {
|
||||||
|
_.fetchList();
|
||||||
|
},
|
||||||
|
header: CustomEasyRefresh.header,
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
ListView.separated(
|
||||||
|
shrinkWrap: true,
|
||||||
|
physics: const ClampingScrollPhysics(),
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
V2TimGroupInfo group = _.groups[index];
|
||||||
|
return ListTile(
|
||||||
|
onTap: () async {
|
||||||
|
var conversation = await TimConversationService.to
|
||||||
|
.getById('group_' + group.groupID);
|
||||||
|
Get.toNamed(
|
||||||
|
ConversationRoutes.index,
|
||||||
|
arguments: {
|
||||||
|
'conversation': conversation,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
leading: GroupAvatar(group.groupID),
|
||||||
|
tileColor: AppColors.white,
|
||||||
|
title: Text(group.groupName!),
|
||||||
|
subtitle: Text('成员数: ${group.memberCount}'),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
separatorBuilder: (context, index) {
|
||||||
|
return const Divider(
|
||||||
|
height: 0,
|
||||||
|
indent: 72,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
itemCount: _.groups.length,
|
||||||
|
),
|
||||||
|
const Divider(
|
||||||
|
height: 0,
|
||||||
|
indent: 72,
|
||||||
|
),
|
||||||
|
Container(
|
||||||
|
color: AppColors.white,
|
||||||
|
height: 54,
|
||||||
|
child: Center(
|
||||||
|
child: Text(
|
||||||
|
'${_.groups.length} 个群聊',
|
||||||
|
style: const TextStyle(
|
||||||
|
color: AppColors.unactive,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -48,7 +48,11 @@ class _ContactPageState extends State<ContactPage> {
|
|||||||
return Column(
|
return Column(
|
||||||
children: [
|
children: [
|
||||||
ListTile(
|
ListTile(
|
||||||
onTap: () async {},
|
onTap: () async {
|
||||||
|
if (info.route != null) {
|
||||||
|
Get.toNamed(info.route!);
|
||||||
|
}
|
||||||
|
},
|
||||||
leading: Container(
|
leading: Container(
|
||||||
width: 40,
|
width: 40,
|
||||||
height: 40,
|
height: 40,
|
||||||
|
|||||||
78
lib/views/conversation/preview/image_widget.dart
Normal file
78
lib/views/conversation/preview/image_widget.dart
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
import 'dart:io';
|
||||||
|
import 'package:cached_network_image/cached_network_image.dart';
|
||||||
|
import 'package:chat/configs/app_colors.dart';
|
||||||
|
import 'package:chat/constants/message_constant.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:get/get.dart';
|
||||||
|
import 'package:photo_view/photo_view.dart';
|
||||||
|
import 'package:photo_view/photo_view_gallery.dart';
|
||||||
|
|
||||||
|
class PreviewImageWidget extends StatelessWidget {
|
||||||
|
final IMG_PREVIEW_TYPE type;
|
||||||
|
|
||||||
|
final String path;
|
||||||
|
|
||||||
|
final String original;
|
||||||
|
|
||||||
|
const PreviewImageWidget({
|
||||||
|
Key? key,
|
||||||
|
required this.type,
|
||||||
|
required this.path,
|
||||||
|
required this.original,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Material(
|
||||||
|
color: AppColors.black,
|
||||||
|
child: SafeArea(
|
||||||
|
child: Stack(
|
||||||
|
children: [
|
||||||
|
Positioned.fill(
|
||||||
|
child: PhotoViewGallery.builder(
|
||||||
|
pageController: PageController(initialPage: 0),
|
||||||
|
itemCount: 1,
|
||||||
|
builder: (context, index) {
|
||||||
|
if (type == IMG_PREVIEW_TYPE.local) {
|
||||||
|
return PhotoViewGalleryPageOptions(
|
||||||
|
imageProvider: FileImage(File(path)),
|
||||||
|
minScale: PhotoViewComputedScale.contained,
|
||||||
|
maxScale: PhotoViewComputedScale.covered * 2,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
if (path.split('?').first.isImageFileName) {
|
||||||
|
return PhotoViewGalleryPageOptions(
|
||||||
|
imageProvider: CachedNetworkImageProvider(path),
|
||||||
|
minScale: PhotoViewComputedScale.contained,
|
||||||
|
maxScale: PhotoViewComputedScale.covered * 2,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return PhotoViewGalleryPageOptions.customChild(
|
||||||
|
child: const Center(
|
||||||
|
child: Text(
|
||||||
|
'格式不支持',
|
||||||
|
style: TextStyle(
|
||||||
|
color: AppColors.white,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SafeArea(
|
||||||
|
child: Padding(
|
||||||
|
padding: EdgeInsets.all(4.0),
|
||||||
|
child: BackButton(
|
||||||
|
color: AppColors.white,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
119
lib/views/conversation/preview/video_widget.dart
Normal file
119
lib/views/conversation/preview/video_widget.dart
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:chat/configs/app_colors.dart';
|
||||||
|
import 'package:chat/utils/ui_tools.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:tencent_im_sdk_plugin/models/v2_tim_video_elem.dart';
|
||||||
|
import 'package:video_player/video_player.dart';
|
||||||
|
|
||||||
|
class PreviewVideoWidget extends StatefulWidget {
|
||||||
|
final V2TimVideoElem video;
|
||||||
|
const PreviewVideoWidget(this.video, {Key? key}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<PreviewVideoWidget> createState() => _PreviewVideoWidgetState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _PreviewVideoWidgetState extends State<PreviewVideoWidget> {
|
||||||
|
late VideoPlayerController _controller;
|
||||||
|
bool isPause = false;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
|
||||||
|
var lv = widget.video.localVideoUrl;
|
||||||
|
|
||||||
|
if (lv != null && lv.isNotEmpty && File(lv).existsSync()) {
|
||||||
|
_controller =
|
||||||
|
VideoPlayerController.file(File(widget.video.localVideoUrl!))
|
||||||
|
..initialize().then((value) {
|
||||||
|
setState(() {
|
||||||
|
_controller.play();
|
||||||
|
});
|
||||||
|
}).catchError((e) {
|
||||||
|
UiTools.toast(e.toString());
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
_controller = VideoPlayerController.network(widget.video.videoUrl!)
|
||||||
|
..initialize().then((value) {
|
||||||
|
setState(() {
|
||||||
|
_controller.play();
|
||||||
|
});
|
||||||
|
}).catchError((e) {
|
||||||
|
UiTools.toast(e.toString());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_controller.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
backgroundColor: AppColors.mainBlack,
|
||||||
|
appBar: AppBar(
|
||||||
|
backgroundColor: AppColors.transparent,
|
||||||
|
foregroundColor: AppColors.white,
|
||||||
|
),
|
||||||
|
body: GestureDetector(
|
||||||
|
onTap: () {
|
||||||
|
setState(() {
|
||||||
|
if (_controller.value.isInitialized) {
|
||||||
|
if (_controller.value.isPlaying) {
|
||||||
|
_controller.pause();
|
||||||
|
isPause = true;
|
||||||
|
} else {
|
||||||
|
_controller.play();
|
||||||
|
isPause = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
child: Stack(
|
||||||
|
children: [
|
||||||
|
Center(
|
||||||
|
child: _controller.value.isInitialized
|
||||||
|
? AspectRatio(
|
||||||
|
aspectRatio: _controller.value.aspectRatio,
|
||||||
|
child: VideoPlayer(_controller),
|
||||||
|
)
|
||||||
|
: _controller.value.hasError
|
||||||
|
? const Text(
|
||||||
|
'视频加载失败',
|
||||||
|
style: TextStyle(
|
||||||
|
color: AppColors.white,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: const CircularProgressIndicator(
|
||||||
|
color: AppColors.primary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Align(
|
||||||
|
alignment: Alignment.bottomCenter,
|
||||||
|
child: VideoProgressIndicator(
|
||||||
|
_controller,
|
||||||
|
allowScrubbing: true,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Align(
|
||||||
|
alignment: Alignment.center,
|
||||||
|
child: Visibility(
|
||||||
|
visible: isPause,
|
||||||
|
child: const Icon(
|
||||||
|
Icons.play_circle_outline,
|
||||||
|
size: 96,
|
||||||
|
color: AppColors.white,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
80
lib/views/conversation/widgets/message_area.dart
Normal file
80
lib/views/conversation/widgets/message_area.dart
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
import 'package:chat/services/tim/message_service.dart';
|
||||||
|
import 'package:chat/views/conversation/widgets/message_widget.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:get/get.dart';
|
||||||
|
import 'package:scroll_to_index/scroll_to_index.dart';
|
||||||
|
import 'package:tencent_im_sdk_plugin/models/v2_tim_conversation.dart';
|
||||||
|
import 'package:tencent_im_sdk_plugin/models/v2_tim_message.dart';
|
||||||
|
|
||||||
|
class MessageArea extends StatefulWidget {
|
||||||
|
final V2TimConversation conversation;
|
||||||
|
|
||||||
|
const MessageArea(this.conversation, {Key? key}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<MessageArea> createState() => _MessageAreaState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _MessageAreaState extends State<MessageArea> {
|
||||||
|
late AutoScrollController _scrollController;
|
||||||
|
String? _lastMessageId;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_scrollController = AutoScrollController(
|
||||||
|
viewportBoundaryGetter: () => Rect.fromLTRB(
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
MediaQuery.of(context).padding.bottom,
|
||||||
|
),
|
||||||
|
axis: Axis.vertical,
|
||||||
|
);
|
||||||
|
|
||||||
|
_loadMessages();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _loadMessages() async {
|
||||||
|
TimMessageService.to
|
||||||
|
.loadMessagesFromService(
|
||||||
|
widget.conversation,
|
||||||
|
_lastMessageId,
|
||||||
|
_scrollController,
|
||||||
|
)
|
||||||
|
.then((value) {
|
||||||
|
_lastMessageId = value;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return GetX<TimMessageService>(builder: (service) {
|
||||||
|
return ListView.separated(
|
||||||
|
scrollDirection: Axis.vertical,
|
||||||
|
controller: _scrollController,
|
||||||
|
shrinkWrap: true,
|
||||||
|
physics: const ClampingScrollPhysics(),
|
||||||
|
padding: const EdgeInsets.all(8),
|
||||||
|
reverse: true,
|
||||||
|
cacheExtent: 1200,
|
||||||
|
addAutomaticKeepAlives: true,
|
||||||
|
itemCount: service.messages.length,
|
||||||
|
itemBuilder: (_, index) {
|
||||||
|
V2TimMessage? message = service.messages[index];
|
||||||
|
return AutoScrollTag(
|
||||||
|
key: ValueKey(index),
|
||||||
|
controller: _scrollController,
|
||||||
|
index: index,
|
||||||
|
child: MessageWidget(message),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
separatorBuilder: (c, i) {
|
||||||
|
return Container(
|
||||||
|
height: 8,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
676
lib/views/conversation/widgets/message_field.dart
Normal file
676
lib/views/conversation/widgets/message_field.dart
Normal file
@@ -0,0 +1,676 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
import 'package:chat/configs/app_colors.dart';
|
||||||
|
import 'package:chat/routes/contact_routes.dart';
|
||||||
|
import 'package:chat/services/tim/conversation_service.dart';
|
||||||
|
import 'package:chat/utils/sound_record.dart';
|
||||||
|
import 'package:chat/utils/ui_tools.dart';
|
||||||
|
import 'package:chat/views/conversation/widgets/tim_emoji_panel.dart';
|
||||||
|
import 'package:file_picker/file_picker.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
// import 'package:flutter_baidu_mapapi_search/flutter_baidu_mapapi_search.dart';
|
||||||
|
import 'package:get/get.dart';
|
||||||
|
import 'package:permission_handler/permission_handler.dart';
|
||||||
|
import 'package:tencent_im_sdk_plugin/enum/conversation_type.dart';
|
||||||
|
import 'package:tencent_im_sdk_plugin/models/v2_tim_conversation.dart';
|
||||||
|
import 'package:wechat_assets_picker/wechat_assets_picker.dart';
|
||||||
|
import 'package:wechat_camera_picker/wechat_camera_picker.dart';
|
||||||
|
|
||||||
|
class MessageField extends StatefulWidget {
|
||||||
|
final V2TimConversation conversation;
|
||||||
|
|
||||||
|
const MessageField(this.conversation, {Key? key}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<MessageField> createState() => _MessageFieldState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _MessageFieldState extends State<MessageField> {
|
||||||
|
final TextEditingController _controller = TextEditingController();
|
||||||
|
|
||||||
|
bool isVoice = false;
|
||||||
|
String _sendText = '';
|
||||||
|
double paddingBottom = 0.0;
|
||||||
|
String soundTipsText = "手指上滑,取消发送";
|
||||||
|
bool isRecording = false;
|
||||||
|
bool isInit = false;
|
||||||
|
bool isCancelSend = false;
|
||||||
|
DateTime startTime = DateTime.now();
|
||||||
|
List<StreamSubscription<Object>> subscriptions = [];
|
||||||
|
|
||||||
|
OverlayEntry? overlayEntry;
|
||||||
|
String voiceIcon = "assets/chats/voice_volume_1.png";
|
||||||
|
final FocusNode _focusNode = FocusNode();
|
||||||
|
bool showMore = false;
|
||||||
|
bool showEmojiPanel = false;
|
||||||
|
bool showKeyboard = false;
|
||||||
|
double lastkeyboardHeight = 0;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_controller.dispose();
|
||||||
|
for (var subscription in subscriptions) {
|
||||||
|
subscription.cancel();
|
||||||
|
}
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> sendMessage() async {
|
||||||
|
var text = _controller.text;
|
||||||
|
|
||||||
|
if (text.isEmpty) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
TimConversationService.to.sendTextMessage(widget.conversation, text);
|
||||||
|
_controller.text = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 发送媒体消息
|
||||||
|
Future<void> sendMeidaMessage(AssetEntity asset) async {
|
||||||
|
if (asset.type == AssetType.image) {
|
||||||
|
TimConversationService.to.sendImageMessage(
|
||||||
|
widget.conversation,
|
||||||
|
asset,
|
||||||
|
);
|
||||||
|
} else if (asset.type == AssetType.video) {
|
||||||
|
TimConversationService.to.sendVideoMessage(
|
||||||
|
widget.conversation,
|
||||||
|
asset,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
UiTools.toast('暂不支持的类型');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
hideAllPanel() {
|
||||||
|
_focusNode.unfocus();
|
||||||
|
setState(() {
|
||||||
|
showKeyboard = false;
|
||||||
|
showMore = false;
|
||||||
|
showEmojiPanel = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
double _getBottomHeight() {
|
||||||
|
listenKeyBoardStatus();
|
||||||
|
if (showMore || showEmojiPanel) {
|
||||||
|
return 248.0;
|
||||||
|
}
|
||||||
|
// 在文本框多行拓展时增加保护区高度
|
||||||
|
else if (_controller.text.length >= 46 && showKeyboard == false) {
|
||||||
|
return 25;
|
||||||
|
} else {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
listenKeyBoardStatus() {
|
||||||
|
final currentKeyboardHeight = MediaQuery.of(context).viewInsets.bottom;
|
||||||
|
// 键盘弹出
|
||||||
|
if (currentKeyboardHeight - lastkeyboardHeight > 0) {
|
||||||
|
// 保证弹出时showKeyboard为true
|
||||||
|
setState(() {
|
||||||
|
showKeyboard = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
/// 键盘收回
|
||||||
|
} else if (currentKeyboardHeight - lastkeyboardHeight < 0) {}
|
||||||
|
|
||||||
|
lastkeyboardHeight = MediaQuery.of(context).viewInsets.bottom;
|
||||||
|
}
|
||||||
|
|
||||||
|
_openMore() {
|
||||||
|
if (showMore) {
|
||||||
|
_focusNode.requestFocus();
|
||||||
|
} else {
|
||||||
|
_focusNode.unfocus();
|
||||||
|
}
|
||||||
|
setState(() {
|
||||||
|
showKeyboard = showMore;
|
||||||
|
showEmojiPanel = false;
|
||||||
|
showMore = !showMore;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return SafeArea(
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
const Divider(height: 0),
|
||||||
|
Container(
|
||||||
|
padding: EdgeInsets.only(
|
||||||
|
left: 8,
|
||||||
|
right: 8,
|
||||||
|
top: 8,
|
||||||
|
bottom: 8 + Get.mediaQuery.viewInsets.bottom,
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
_voiceButton(),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Expanded(child: _inputArea()),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
_emojiButton(),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
_sendButton()
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Divider(height: 0),
|
||||||
|
AnimatedContainer(
|
||||||
|
height: _getBottomHeight(),
|
||||||
|
duration: const Duration(milliseconds: 150),
|
||||||
|
padding: showEmojiPanel
|
||||||
|
? const EdgeInsets.all(0)
|
||||||
|
: const EdgeInsets.all(16),
|
||||||
|
child: _getBottomContainer(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _actionWidget(
|
||||||
|
String text, {
|
||||||
|
required IconData icon,
|
||||||
|
VoidCallback? onTap,
|
||||||
|
}) {
|
||||||
|
return InkWell(
|
||||||
|
onTap: () {
|
||||||
|
onTap?.call();
|
||||||
|
},
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.all(15),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: AppColors.white,
|
||||||
|
borderRadius: BorderRadius.circular(5),
|
||||||
|
),
|
||||||
|
child: Icon(
|
||||||
|
icon,
|
||||||
|
color: AppColors.unactive,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 3),
|
||||||
|
Text(
|
||||||
|
text,
|
||||||
|
style: const TextStyle(
|
||||||
|
color: AppColors.unactive,
|
||||||
|
fontSize: 12,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
_openEmojiPanel() {
|
||||||
|
if (showEmojiPanel) {
|
||||||
|
_focusNode.requestFocus();
|
||||||
|
} else {
|
||||||
|
_focusNode.unfocus();
|
||||||
|
}
|
||||||
|
setState(() {
|
||||||
|
showKeyboard = showEmojiPanel;
|
||||||
|
showMore = false;
|
||||||
|
showEmojiPanel = !showEmojiPanel;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _emojiButton() {
|
||||||
|
return InkWell(
|
||||||
|
onTap: () {
|
||||||
|
_openEmojiPanel();
|
||||||
|
},
|
||||||
|
child: const Icon(
|
||||||
|
Icons.emoji_emotions_outlined,
|
||||||
|
size: 28,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _voiceButton() {
|
||||||
|
if (!isVoice) {
|
||||||
|
return InkWell(
|
||||||
|
onTap: () {
|
||||||
|
setState(() {
|
||||||
|
isVoice = true;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
child: const Icon(
|
||||||
|
Icons.mic_outlined,
|
||||||
|
size: 28,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return InkWell(
|
||||||
|
onTap: () {
|
||||||
|
setState(() {
|
||||||
|
isVoice = false;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
child: const Icon(
|
||||||
|
Icons.keyboard_outlined,
|
||||||
|
size: 28,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _inputArea() {
|
||||||
|
if (isVoice) {
|
||||||
|
return GestureDetector(
|
||||||
|
onTapDown: (_) async {
|
||||||
|
if (!isInit) {
|
||||||
|
var result = await Permission.microphone.request().isGranted;
|
||||||
|
if (result) initRecordSound();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onLongPressStart: onLongPressStart,
|
||||||
|
onLongPressMoveUpdate: onLongPressUpdate,
|
||||||
|
onLongPressEnd: onLongPressEnd,
|
||||||
|
onLongPressCancel: onLonePressCancel,
|
||||||
|
child: Container(
|
||||||
|
alignment: Alignment.center,
|
||||||
|
padding: const EdgeInsets.only(
|
||||||
|
left: 8.0,
|
||||||
|
right: 8.0,
|
||||||
|
top: 8.0,
|
||||||
|
bottom: 8.0,
|
||||||
|
),
|
||||||
|
decoration: const BoxDecoration(
|
||||||
|
color: AppColors.white,
|
||||||
|
borderRadius: BorderRadius.all(
|
||||||
|
Radius.circular(
|
||||||
|
4.0,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: const Text('按住说话'),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.only(
|
||||||
|
left: 8.0,
|
||||||
|
right: 8.0,
|
||||||
|
top: 8.0,
|
||||||
|
bottom: 8.0,
|
||||||
|
),
|
||||||
|
decoration: const BoxDecoration(
|
||||||
|
color: AppColors.white,
|
||||||
|
borderRadius: BorderRadius.all(
|
||||||
|
Radius.circular(
|
||||||
|
4.0,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: TextField(
|
||||||
|
controller: _controller,
|
||||||
|
focusNode: _focusNode,
|
||||||
|
decoration: null,
|
||||||
|
onChanged: (e) {
|
||||||
|
setState(() {
|
||||||
|
_sendText = e;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onTap: () {
|
||||||
|
showKeyboard = true;
|
||||||
|
showMore = false;
|
||||||
|
showEmojiPanel = false;
|
||||||
|
},
|
||||||
|
onSubmitted: (value) {
|
||||||
|
sendMessage();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _sendButton() {
|
||||||
|
if (_sendText.isEmpty) {
|
||||||
|
return InkWell(
|
||||||
|
onTap: () {
|
||||||
|
_openMore();
|
||||||
|
},
|
||||||
|
child: const Icon(
|
||||||
|
Icons.add_circle_outline_outlined,
|
||||||
|
size: 28,
|
||||||
|
color: AppColors.active,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return InkWell(
|
||||||
|
onTap: () {
|
||||||
|
sendMessage();
|
||||||
|
},
|
||||||
|
child: Container(
|
||||||
|
height: 28.0,
|
||||||
|
width: 54.0,
|
||||||
|
alignment: Alignment.center,
|
||||||
|
decoration: const BoxDecoration(
|
||||||
|
color: AppColors.primary,
|
||||||
|
borderRadius: BorderRadius.all(
|
||||||
|
Radius.circular(
|
||||||
|
8.0,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: const Text(
|
||||||
|
'发送',
|
||||||
|
style: TextStyle(
|
||||||
|
letterSpacing: 2,
|
||||||
|
color: AppColors.white,
|
||||||
|
fontSize: 14.0,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
initRecordSound() {
|
||||||
|
final responseSubscription = SoundPlayer.responseListener((recordResponse) {
|
||||||
|
final status = recordResponse.msg;
|
||||||
|
if (status == "onStop") {
|
||||||
|
if (!isCancelSend) {
|
||||||
|
final soundPath = recordResponse.path;
|
||||||
|
final recordDuration = recordResponse.audioTimeLength;
|
||||||
|
TimConversationService.to.sendSoundMessage(
|
||||||
|
widget.conversation, soundPath!, recordDuration!.toInt());
|
||||||
|
}
|
||||||
|
} else if (status == "onStart") {
|
||||||
|
// print("start record");
|
||||||
|
setState(() {
|
||||||
|
isRecording = true;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// print(status);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
final amplitutdeResponseSubscription =
|
||||||
|
SoundPlayer.responseFromAmplitudeListener((recordResponse) {
|
||||||
|
final voiceData = double.parse(recordResponse.msg!);
|
||||||
|
setState(() {
|
||||||
|
if (voiceData > 0 && voiceData < 0.1) {
|
||||||
|
voiceIcon = "assets/chats/voice_volume_2.png";
|
||||||
|
} else if (voiceData > 0.2 && voiceData < 0.3) {
|
||||||
|
voiceIcon = "assets/chats/voice_volume_3.png";
|
||||||
|
} else if (voiceData > 0.3 && voiceData < 0.4) {
|
||||||
|
voiceIcon = "assets/chats/voice_volume_4.png";
|
||||||
|
} else if (voiceData > 0.4 && voiceData < 0.5) {
|
||||||
|
voiceIcon = "assets/chats/voice_volume_5.png";
|
||||||
|
} else if (voiceData > 0.5 && voiceData < 0.6) {
|
||||||
|
voiceIcon = "assets/chats/voice_volume_6.png";
|
||||||
|
} else if (voiceData > 0.6 && voiceData < 0.7) {
|
||||||
|
voiceIcon = "assets/chats/voice_volume_7.png";
|
||||||
|
} else if (voiceData > 0.7 && voiceData < 1) {
|
||||||
|
voiceIcon = "assets/chats/voice_volume_7.png";
|
||||||
|
} else {
|
||||||
|
voiceIcon = "assets/chats/voice_volume_1.png";
|
||||||
|
}
|
||||||
|
if (overlayEntry != null) {
|
||||||
|
overlayEntry!.markNeedsBuild();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
subscriptions = [responseSubscription, amplitutdeResponseSubscription];
|
||||||
|
SoundPlayer.initSoundPlayer();
|
||||||
|
isInit = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
buildOverLayView(BuildContext context) {
|
||||||
|
if (overlayEntry == null) {
|
||||||
|
overlayEntry = OverlayEntry(builder: (content) {
|
||||||
|
return Positioned(
|
||||||
|
top: MediaQuery.of(context).size.height * 0.5 - 80,
|
||||||
|
left: MediaQuery.of(context).size.width * 0.5 - 80,
|
||||||
|
child: Material(
|
||||||
|
type: MaterialType.transparency,
|
||||||
|
child: Center(
|
||||||
|
child: Opacity(
|
||||||
|
opacity: 0.8,
|
||||||
|
child: Container(
|
||||||
|
width: 160,
|
||||||
|
height: 160,
|
||||||
|
decoration: const BoxDecoration(
|
||||||
|
color: Color(0xff77797A),
|
||||||
|
borderRadius: BorderRadius.all(Radius.circular(20.0)),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
children: <Widget>[
|
||||||
|
Container(
|
||||||
|
margin: const EdgeInsets.only(top: 10),
|
||||||
|
child: Image.asset(
|
||||||
|
voiceIcon,
|
||||||
|
width: 100,
|
||||||
|
height: 100,
|
||||||
|
// package: 'flutter_plugin_record',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
soundTipsText,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontStyle: FontStyle.normal,
|
||||||
|
color: Colors.white,
|
||||||
|
fontSize: 14,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
Overlay.of(context)!.insert(overlayEntry!);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onLongPressStart(_) {
|
||||||
|
if (isInit) {
|
||||||
|
startTime = DateTime.now();
|
||||||
|
SoundPlayer.startRecord();
|
||||||
|
buildOverLayView(context);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onLongPressUpdate(e) {
|
||||||
|
double height = MediaQuery.of(context).size.height * 0.5 - 240;
|
||||||
|
double dy = e.localPosition.dy;
|
||||||
|
if (dy.abs() > height) {
|
||||||
|
if (mounted && soundTipsText != '松开取消') {
|
||||||
|
setState(() {
|
||||||
|
soundTipsText = '松开取消';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (mounted && soundTipsText == '松开取消') {
|
||||||
|
setState(() {
|
||||||
|
soundTipsText = '手指上滑,取消发送';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onLongPressEnd(e) {
|
||||||
|
double dy = e.localPosition.dy;
|
||||||
|
// 此高度为 160为录音取消组件距离顶部的预留距离
|
||||||
|
double height = MediaQuery.of(context).size.height * 0.5 - 240;
|
||||||
|
if (dy.abs() > height) {
|
||||||
|
isCancelSend = true;
|
||||||
|
} else {
|
||||||
|
isCancelSend = false;
|
||||||
|
}
|
||||||
|
if (overlayEntry != null) {
|
||||||
|
overlayEntry!.remove();
|
||||||
|
overlayEntry = null;
|
||||||
|
}
|
||||||
|
// Did not receive onStop from FlutterPluginRecord if the duration is too short.
|
||||||
|
if (DateTime.now().difference(startTime).inSeconds < 1) {
|
||||||
|
isCancelSend = true;
|
||||||
|
UiTools.toast('说话时间太短!');
|
||||||
|
}
|
||||||
|
stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
onLonePressCancel() {
|
||||||
|
if (isRecording) {
|
||||||
|
isCancelSend = true;
|
||||||
|
if (overlayEntry != null) {
|
||||||
|
overlayEntry!.remove();
|
||||||
|
overlayEntry = null;
|
||||||
|
}
|
||||||
|
stop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void stop() {
|
||||||
|
setState(() {
|
||||||
|
isRecording = false;
|
||||||
|
});
|
||||||
|
SoundPlayer.stopRecord();
|
||||||
|
setState(() {
|
||||||
|
soundTipsText = '手指上滑,取消发送';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
_getMoreActions() {
|
||||||
|
var list = List<Widget>.empty(growable: true);
|
||||||
|
list.add(
|
||||||
|
_actionWidget('相册', icon: Icons.photo, onTap: () async {
|
||||||
|
var result = await AssetPicker.pickAssets(
|
||||||
|
context,
|
||||||
|
pickerConfig: const AssetPickerConfig(
|
||||||
|
maxAssets: 9,
|
||||||
|
requestType: RequestType.common,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
if (result == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (var asset in result) {
|
||||||
|
sendMeidaMessage(asset);
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
list.add(
|
||||||
|
_actionWidget('拍照', icon: Icons.photo_camera, onTap: () async {
|
||||||
|
var asset = await CameraPicker.pickFromCamera(
|
||||||
|
context,
|
||||||
|
pickerConfig: const CameraPickerConfig(
|
||||||
|
enableRecording: true,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
if (asset == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
sendMeidaMessage(asset);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
if (widget.conversation.type == ConversationType.V2TIM_C2C) {
|
||||||
|
list.add(
|
||||||
|
_actionWidget(
|
||||||
|
'视频通话',
|
||||||
|
icon: Icons.videocam,
|
||||||
|
onTap: () {
|
||||||
|
// ImTools.showTrtcMessage(widget.conversation.userID!);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
list.add(
|
||||||
|
_actionWidget(
|
||||||
|
'位置',
|
||||||
|
icon: Icons.pin_drop,
|
||||||
|
onTap: () {
|
||||||
|
// Get.toNamed(ImRoutes.conversationMap)?.then((value) {
|
||||||
|
// var _bmfPoiInfo = value['result'] as BMFPoiInfo;
|
||||||
|
// var snapshot = value['snapshot'] as Uint8List?;
|
||||||
|
// var model = LocationModel(
|
||||||
|
// name: _bmfPoiInfo.name!,
|
||||||
|
// address: _bmfPoiInfo.address!,
|
||||||
|
// list: snapshot!,
|
||||||
|
// latitude: _bmfPoiInfo.pt?.latitude ?? 0,
|
||||||
|
// longitude: _bmfPoiInfo.pt?.longitude ?? 0,
|
||||||
|
// );
|
||||||
|
// TimConversationService.to.sendLocationMessage(
|
||||||
|
// widget.conversation,
|
||||||
|
// model,
|
||||||
|
// );
|
||||||
|
// });
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
list.add(
|
||||||
|
_actionWidget(
|
||||||
|
'名片',
|
||||||
|
icon: Icons.person,
|
||||||
|
onTap: () {
|
||||||
|
Get.toNamed(
|
||||||
|
ContactRoutes.friend,
|
||||||
|
arguments: {
|
||||||
|
'name_card': true,
|
||||||
|
},
|
||||||
|
)?.then((value) {
|
||||||
|
var model = value?['result'];
|
||||||
|
if (model != null) {
|
||||||
|
TimConversationService.to.sendCustomMessage(
|
||||||
|
widget.conversation,
|
||||||
|
model,
|
||||||
|
'NAME_CARD',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
list.add(
|
||||||
|
_actionWidget(
|
||||||
|
'文件',
|
||||||
|
icon: Icons.folder,
|
||||||
|
onTap: () async {
|
||||||
|
FilePickerResult? result = await FilePicker.platform.pickFiles();
|
||||||
|
if (result != null) {
|
||||||
|
TimConversationService.to.sendFileMessage(
|
||||||
|
widget.conversation,
|
||||||
|
result.names.first!,
|
||||||
|
result.paths.first!,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _getBottomContainer() {
|
||||||
|
if (showEmojiPanel) {
|
||||||
|
// eventBus.fire('scrollToBottom');
|
||||||
|
return EmojiPanel(
|
||||||
|
onTapEmoji: (unicode) {
|
||||||
|
final oldText = _controller.text;
|
||||||
|
final newText = String.fromCharCode(unicode);
|
||||||
|
_controller.text = "$oldText$newText";
|
||||||
|
setState(() {
|
||||||
|
_sendText = _controller.text;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (showMore) {
|
||||||
|
// eventBus.fire('scrollToBottom');
|
||||||
|
return GridView.count(
|
||||||
|
shrinkWrap: true,
|
||||||
|
physics: const NeverScrollableScrollPhysics(),
|
||||||
|
crossAxisCount: 4,
|
||||||
|
children: _getMoreActions(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return Container();
|
||||||
|
}
|
||||||
|
}
|
||||||
126
lib/views/conversation/widgets/message_list.dart
Normal file
126
lib/views/conversation/widgets/message_list.dart
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
import 'package:chat/services/tim/message_service.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:scroll_to_index/scroll_to_index.dart';
|
||||||
|
import 'package:tencent_im_sdk_plugin/models/v2_tim_conversation.dart';
|
||||||
|
import 'package:tencent_im_sdk_plugin/models/v2_tim_message.dart';
|
||||||
|
import 'message_widget.dart';
|
||||||
|
|
||||||
|
class MessageList extends StatefulWidget {
|
||||||
|
final V2TimConversation conversation;
|
||||||
|
|
||||||
|
const MessageList(this.conversation, {Key? key}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<MessageList> createState() => _MessageListState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _MessageListState extends State<MessageList> {
|
||||||
|
final EasyRefreshController _refreshController = EasyRefreshController();
|
||||||
|
late AutoScrollController _scrollController;
|
||||||
|
String? _lastMessageId;
|
||||||
|
bool isFirst = true;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
|
||||||
|
_scrollController = AutoScrollController(
|
||||||
|
viewportBoundaryGetter: () => Rect.fromLTRB(
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
MediaQuery.of(context).padding.bottom,
|
||||||
|
),
|
||||||
|
axis: Axis.vertical,
|
||||||
|
);
|
||||||
|
|
||||||
|
/// 接收到消息 和 自己发送消息 使列表滚动到最底部
|
||||||
|
// eventBus.on().listen((event) {
|
||||||
|
// if (TimMessageService.to.curConversationId ==
|
||||||
|
// widget.conversation.conversationID) {
|
||||||
|
// if (event is V2TimMessage) {
|
||||||
|
// if (mounted) {
|
||||||
|
// setState(() {
|
||||||
|
// TimMessageService.to.addMessage(event);
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
// _scrollController.scrollToIndex(
|
||||||
|
// TimMessageService.to.messages.length - 1,
|
||||||
|
// preferPosition: AutoScrollPosition.begin,
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// if (event is String && event == 'scrollToBottom') {
|
||||||
|
// Future.delayed(const Duration(milliseconds: 200), () {
|
||||||
|
// _scrollController.animateTo(
|
||||||
|
// _scrollController.position.maxScrollExtent,
|
||||||
|
// duration: const Duration(milliseconds: 200),
|
||||||
|
// curve: Curves.easeOut,
|
||||||
|
// );
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
_loadMessages();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _loadMessages() async {
|
||||||
|
TimMessageService.to
|
||||||
|
.loadMessagesFromService(
|
||||||
|
widget.conversation,
|
||||||
|
_lastMessageId,
|
||||||
|
_scrollController,
|
||||||
|
)
|
||||||
|
.then((value) {
|
||||||
|
_lastMessageId = value;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return GetX<TimMessageService>(builder: (service) {
|
||||||
|
return EasyRefresh(
|
||||||
|
controller: _refreshController,
|
||||||
|
header: CustomEasyRefresh.header,
|
||||||
|
footer: CustomEasyRefresh.footer,
|
||||||
|
firstRefresh: false,
|
||||||
|
onRefresh: _loadMessages,
|
||||||
|
child: ListView.separated(
|
||||||
|
scrollDirection: Axis.vertical,
|
||||||
|
controller: _scrollController,
|
||||||
|
shrinkWrap: true,
|
||||||
|
physics: const ClampingScrollPhysics(),
|
||||||
|
padding: const EdgeInsets.only(
|
||||||
|
left: 8,
|
||||||
|
right: 8,
|
||||||
|
top: 8,
|
||||||
|
bottom: 8,
|
||||||
|
),
|
||||||
|
reverse: false,
|
||||||
|
itemCount: service.messages.length,
|
||||||
|
itemBuilder: (_, index) {
|
||||||
|
V2TimMessage? message = service.messages[index];
|
||||||
|
return AutoScrollTag(
|
||||||
|
key: ValueKey(index),
|
||||||
|
controller: _scrollController,
|
||||||
|
index: index,
|
||||||
|
child: MessageWidget(message),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
separatorBuilder: (c, i) {
|
||||||
|
return Container(
|
||||||
|
height: 8,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
274
lib/views/conversation/widgets/message_widget.dart
Normal file
274
lib/views/conversation/widgets/message_widget.dart
Normal file
@@ -0,0 +1,274 @@
|
|||||||
|
import 'package:chat/configs/app_colors.dart';
|
||||||
|
import 'package:chat/routes/contact_routes.dart';
|
||||||
|
import 'package:chat/routes/user_routes.dart';
|
||||||
|
import 'package:chat/services/auth_service.dart';
|
||||||
|
import 'package:chat/views/conversation/widgets/show_custom_message.dart';
|
||||||
|
import 'package:chat/views/conversation/widgets/show_face_message.dart';
|
||||||
|
import 'package:chat/views/conversation/widgets/show_file_message.dart';
|
||||||
|
import 'package:chat/views/conversation/widgets/show_image_message.dart';
|
||||||
|
import 'package:chat/views/conversation/widgets/show_location_message.dart';
|
||||||
|
import 'package:chat/views/conversation/widgets/show_merger_message.dart';
|
||||||
|
import 'package:chat/views/conversation/widgets/show_sound_message.dart';
|
||||||
|
import 'package:chat/views/conversation/widgets/show_text_message.dart';
|
||||||
|
import 'package:chat/views/conversation/widgets/show_video_message.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_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_message.dart';
|
||||||
|
|
||||||
|
class MessageWidget extends StatelessWidget {
|
||||||
|
final V2TimMessage message;
|
||||||
|
|
||||||
|
const MessageWidget(this.message, {Key? key}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
switch (message.elemType) {
|
||||||
|
case MessageElemType.V2TIM_ELEM_TYPE_TEXT:
|
||||||
|
return _buildMessage(ShowTextMessage(message));
|
||||||
|
case MessageElemType.V2TIM_ELEM_TYPE_IMAGE:
|
||||||
|
return _buildMessage(ShowImageMessage(message));
|
||||||
|
case MessageElemType.V2TIM_ELEM_TYPE_SOUND:
|
||||||
|
return _buildMessage(ShowSoundMessage(message));
|
||||||
|
case MessageElemType.V2TIM_ELEM_TYPE_VIDEO:
|
||||||
|
return _buildMessage(ShowVideoMessage(message));
|
||||||
|
case MessageElemType.V2TIM_ELEM_TYPE_FILE:
|
||||||
|
return _buildMessage(ShowFileMessage(message));
|
||||||
|
case MessageElemType.V2TIM_ELEM_TYPE_LOCATION:
|
||||||
|
return _buildMessage(ShowLocationMessage(message));
|
||||||
|
case MessageElemType.V2TIM_ELEM_TYPE_FACE:
|
||||||
|
return _buildMessage(ShowFaceMessage(message));
|
||||||
|
case MessageElemType.V2TIM_ELEM_TYPE_MERGER:
|
||||||
|
return _buildMessage(ShowMergerMessage(message));
|
||||||
|
case MessageElemType.V2TIM_ELEM_TYPE_CUSTOM:
|
||||||
|
return _buildMessage(ShowCustomMessage(message));
|
||||||
|
case MessageElemType.V2TIM_ELEM_TYPE_GROUP_TIPS:
|
||||||
|
return _buildGroupTipMessage();
|
||||||
|
default:
|
||||||
|
return Text('未识别的消息类型 ${message.elemType}');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 构造群提示消息
|
||||||
|
Widget _buildGroupTipMessage() {
|
||||||
|
switch (message.groupTipsElem!.type) {
|
||||||
|
case GroupTipsElemType.V2TIM_GROUP_TIPS_TYPE_GROUP_INFO_CHANGE:
|
||||||
|
return _buildGroupInfoChangeMessage();
|
||||||
|
case GroupTipsElemType.V2TIM_GROUP_TIPS_TYPE_JOIN:
|
||||||
|
return _buildGroupTypeJoinMessage();
|
||||||
|
case GroupTipsElemType.V2TIM_GROUP_TIPS_TYPE_INVITE:
|
||||||
|
return _buildGroupTypeInviteMessage();
|
||||||
|
case GroupTipsElemType.V2TIM_GROUP_TIPS_TYPE_QUIT:
|
||||||
|
return _buildGroupTypeQuitMessage();
|
||||||
|
case GroupTipsElemType.V2TIM_GROUP_TIPS_TYPE_KICKED:
|
||||||
|
return _buildGroupTypeKickedMessage();
|
||||||
|
case GroupTipsElemType.V2TIM_GROUP_TIPS_TYPE_SET_ADMIN:
|
||||||
|
return _buildGroupTypeSetAdminMessage();
|
||||||
|
case GroupTipsElemType.V2TIM_GROUP_TIPS_TYPE_CANCEL_ADMIN:
|
||||||
|
return _buildGroupTypeCancelAdminMessage();
|
||||||
|
case GroupTipsElemType.V2TIM_GROUP_TIPS_TYPE_MEMBER_INFO_CHANGE:
|
||||||
|
return _buildGroupTypeMemberInfoChangeMessage();
|
||||||
|
default:
|
||||||
|
return Text(message.groupTipsElem!.type.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildGroupTypeMemberInfoChangeMessage() {
|
||||||
|
return Center(
|
||||||
|
child: Text(
|
||||||
|
'${message.groupTipsElem!.opMember.nickName}修改了群成员资料',
|
||||||
|
style: const TextStyle(
|
||||||
|
color: AppColors.unactive,
|
||||||
|
fontSize: 12,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildGroupTypeCancelAdminMessage() {
|
||||||
|
String op = message.groupTipsElem!.opMember.nickName ?? '';
|
||||||
|
String text = '';
|
||||||
|
for (var e in message.groupTipsElem!.memberList!) {
|
||||||
|
text += e?.nickName ?? '\t';
|
||||||
|
}
|
||||||
|
text = '$op取消了$text的管理员身份';
|
||||||
|
return Center(
|
||||||
|
child: Text(
|
||||||
|
text,
|
||||||
|
style: const TextStyle(
|
||||||
|
color: AppColors.unactive,
|
||||||
|
fontSize: 12,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildGroupTypeSetAdminMessage() {
|
||||||
|
String op = message.groupTipsElem!.opMember.nickName ?? '';
|
||||||
|
String text = '';
|
||||||
|
for (var e in message.groupTipsElem!.memberList!) {
|
||||||
|
text += e?.nickName ?? '\t';
|
||||||
|
}
|
||||||
|
text = '$op将$text设置为管理员';
|
||||||
|
return Center(
|
||||||
|
child: Text(
|
||||||
|
text,
|
||||||
|
style: const TextStyle(
|
||||||
|
color: AppColors.unactive,
|
||||||
|
fontSize: 12,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildGroupTypeKickedMessage() {
|
||||||
|
String op = message.groupTipsElem!.opMember.nickName ?? '';
|
||||||
|
String text = '';
|
||||||
|
for (var e in message.groupTipsElem!.memberList!) {
|
||||||
|
text += e?.nickName ?? '\t';
|
||||||
|
}
|
||||||
|
text = '$op将$text踢出群组';
|
||||||
|
return Center(
|
||||||
|
child: Text(
|
||||||
|
text,
|
||||||
|
style: const TextStyle(
|
||||||
|
color: AppColors.unactive,
|
||||||
|
fontSize: 12,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 退出群组的消息解析
|
||||||
|
Widget _buildGroupTypeQuitMessage() {
|
||||||
|
if (message.groupTipsElem!.memberList!.isEmpty) {
|
||||||
|
return Container();
|
||||||
|
}
|
||||||
|
return Center(
|
||||||
|
child: Text(
|
||||||
|
'${message.groupTipsElem!.memberList!.first?.nickName}退出了群组',
|
||||||
|
style: const TextStyle(
|
||||||
|
color: AppColors.unactive,
|
||||||
|
fontSize: 12,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildGroupTypeInviteMessage() {
|
||||||
|
String op = message.groupTipsElem!.opMember.nickName ?? '';
|
||||||
|
String text = '';
|
||||||
|
for (var e in message.groupTipsElem!.memberList!) {
|
||||||
|
text += e?.nickName ?? '\t';
|
||||||
|
}
|
||||||
|
text = '$op邀请$text加入群组';
|
||||||
|
return Center(
|
||||||
|
child: Text(
|
||||||
|
text,
|
||||||
|
style: const TextStyle(
|
||||||
|
color: AppColors.unactive,
|
||||||
|
fontSize: 12,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildGroupTypeJoinMessage() {
|
||||||
|
String text = '';
|
||||||
|
for (var e in message.groupTipsElem!.memberList!) {
|
||||||
|
text += e?.nickName ?? '\t';
|
||||||
|
}
|
||||||
|
text = '$text加入群组';
|
||||||
|
return Center(
|
||||||
|
child: Text(
|
||||||
|
text,
|
||||||
|
style: const TextStyle(
|
||||||
|
color: AppColors.unactive,
|
||||||
|
fontSize: 12,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 解析群资料修改的内容
|
||||||
|
Widget _buildGroupInfoChangeMessage() {
|
||||||
|
String text = '';
|
||||||
|
switch (message.groupTipsElem!.groupChangeInfoList!.first!.type) {
|
||||||
|
case GroupChangeInfoType.V2TIM_GROUP_INFO_CHANGE_TYPE_NAME:
|
||||||
|
text =
|
||||||
|
'${message.groupTipsElem!.opMember.nickName} 修改群名称 ${message.groupTipsElem!.groupChangeInfoList!.first!.value}';
|
||||||
|
break;
|
||||||
|
case GroupChangeInfoType.V2TIM_GROUP_INFO_CHANGE_TYPE_INTRODUCTION:
|
||||||
|
text = '群简介修改';
|
||||||
|
break;
|
||||||
|
case GroupChangeInfoType.V2TIM_GROUP_INFO_CHANGE_TYPE_NOTIFICATION:
|
||||||
|
text = '${message.groupTipsElem!.opMember.nickName} 更新了群公告';
|
||||||
|
break;
|
||||||
|
case GroupChangeInfoType.V2TIM_GROUP_INFO_CHANGE_TYPE_FACE_URL:
|
||||||
|
text = '群头像修改';
|
||||||
|
break;
|
||||||
|
case GroupChangeInfoType.V2TIM_GROUP_INFO_CHANGE_TYPE_OWNER:
|
||||||
|
text = '${message.groupTipsElem!.memberList!.first!.nickName} 成为新群主';
|
||||||
|
break;
|
||||||
|
case GroupChangeInfoType.V2TIM_GROUP_INFO_CHANGE_TYPE_CUSTOM:
|
||||||
|
text = '群自定义字段变更';
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
text = message.groupTipsElem!.groupChangeInfoList![0]!.type.toString();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Center(
|
||||||
|
child: Text(
|
||||||
|
text,
|
||||||
|
style: const TextStyle(
|
||||||
|
color: AppColors.unactive,
|
||||||
|
fontSize: 12,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 构造普通内容的消息
|
||||||
|
Widget _buildMessage(Widget child) {
|
||||||
|
return Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.start,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
textDirection: message.isSelf! ? TextDirection.rtl : TextDirection.ltr,
|
||||||
|
children: [
|
||||||
|
InkWell(
|
||||||
|
onTap: () {
|
||||||
|
if (message.isSelf!) {
|
||||||
|
Get.toNamed(UserRoutes.info);
|
||||||
|
} else {
|
||||||
|
Get.toNamed(
|
||||||
|
ContactRoutes.friendProfile,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: message.isSelf!
|
||||||
|
? GetX<AuthService>(builder: (_) {
|
||||||
|
return CustomAvatar(
|
||||||
|
_.userInfo.value.avatar ?? '',
|
||||||
|
size: 43,
|
||||||
|
radius: 4,
|
||||||
|
);
|
||||||
|
})
|
||||||
|
: CustomAvatar(
|
||||||
|
message.faceUrl,
|
||||||
|
size: 43,
|
||||||
|
radius: 4,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
GestureDetector(
|
||||||
|
onLongPress: () {},
|
||||||
|
child: child,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
61
lib/views/conversation/widgets/show_call_message.dart
Normal file
61
lib/views/conversation/widgets/show_call_message.dart
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
import 'package:chat/configs/app_colors.dart';
|
||||||
|
import 'package:chat/models/im/calling_model.dart';
|
||||||
|
import 'package:chat/models/im/custom_message_model.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:tencent_im_sdk_plugin/models/v2_tim_message.dart';
|
||||||
|
|
||||||
|
class ShowCallMessage extends StatefulWidget {
|
||||||
|
final V2TimMessage message;
|
||||||
|
const ShowCallMessage(this.message, {Key? key}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<ShowCallMessage> createState() => _ShowCallMessageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ShowCallMessageState extends State<ShowCallMessage> {
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
var model = CallingModel.fromJson(
|
||||||
|
json.decode(widget.message.customElem!.data!),
|
||||||
|
);
|
||||||
|
|
||||||
|
final isVoiceCall = model.callType == CallingType.audioCall;
|
||||||
|
return Container(
|
||||||
|
constraints: const BoxConstraints(minHeight: 43),
|
||||||
|
padding: const EdgeInsets.all(12),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
borderRadius: BorderRadius.circular(4),
|
||||||
|
color: widget.message.isSelf! ? AppColors.primary : AppColors.white,
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
isVoiceCall ? Icons.call_end : Icons.videocam,
|
||||||
|
size: 20,
|
||||||
|
color: widget.message.isSelf! ? AppColors.white : AppColors.active,
|
||||||
|
),
|
||||||
|
const SizedBox(width: 4),
|
||||||
|
model.timeout > 0
|
||||||
|
? Text(
|
||||||
|
'通话时长',
|
||||||
|
style: TextStyle(
|
||||||
|
color: widget.message.isSelf!
|
||||||
|
? AppColors.white
|
||||||
|
: AppColors.active,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: Text(
|
||||||
|
model.actionTypeText,
|
||||||
|
style: TextStyle(
|
||||||
|
color: widget.message.isSelf!
|
||||||
|
? AppColors.white
|
||||||
|
: AppColors.active,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
61
lib/views/conversation/widgets/show_custom_message.dart
Normal file
61
lib/views/conversation/widgets/show_custom_message.dart
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
import 'package:chat/configs/app_colors.dart';
|
||||||
|
import 'package:chat/models/im/custom_message_model.dart';
|
||||||
|
import 'package:chat/views/conversation/widgets/show_call_message.dart';
|
||||||
|
import 'package:chat/views/conversation/widgets/show_group_card_message.dart';
|
||||||
|
import 'package:chat/views/conversation/widgets/show_name_card_message.dart';
|
||||||
|
import 'package:chat/views/conversation/widgets/show_transfer_message.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:tencent_im_sdk_plugin/models/v2_tim_message.dart';
|
||||||
|
|
||||||
|
class ShowCustomMessage extends StatelessWidget {
|
||||||
|
final V2TimMessage message;
|
||||||
|
const ShowCustomMessage(this.message, {Key? key}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
try {
|
||||||
|
String type =
|
||||||
|
json.decode(message.customElem!.data!)['businessID'].toString();
|
||||||
|
|
||||||
|
switch (type) {
|
||||||
|
// case CustomMessageType.EVALUATION:
|
||||||
|
// return ShowEvaluationMessage(message);
|
||||||
|
case CustomMessageType.NAME_CARD:
|
||||||
|
return ShowNameCardMessage(message);
|
||||||
|
case CustomMessageType.DT_TRANSFER:
|
||||||
|
return ShowTransferMessage(message);
|
||||||
|
case CustomMessageType.CALL:
|
||||||
|
return ShowCallMessage(message);
|
||||||
|
case CustomMessageType.GROUP_CARD:
|
||||||
|
return ShowGroupCardMessage(message);
|
||||||
|
default:
|
||||||
|
return _unknowMessage();
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
return _unknowMessage();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _unknowMessage() {
|
||||||
|
return Container(
|
||||||
|
height: 44,
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: AppColors.white,
|
||||||
|
borderRadius: BorderRadius.circular(4),
|
||||||
|
),
|
||||||
|
alignment: Alignment.center,
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
const Text('【自定义消息】'),
|
||||||
|
Text(
|
||||||
|
message.customElem!.data!,
|
||||||
|
maxLines: 5,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
12
lib/views/conversation/widgets/show_face_message.dart
Normal file
12
lib/views/conversation/widgets/show_face_message.dart
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:tencent_im_sdk_plugin/models/v2_tim_message.dart';
|
||||||
|
|
||||||
|
class ShowFaceMessage extends StatelessWidget {
|
||||||
|
final V2TimMessage message;
|
||||||
|
const ShowFaceMessage(this.message, {Key? key}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Container();
|
||||||
|
}
|
||||||
|
}
|
||||||
70
lib/views/conversation/widgets/show_file_message.dart
Normal file
70
lib/views/conversation/widgets/show_file_message.dart
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
import 'package:chat/configs/app_colors.dart';
|
||||||
|
import 'package:chat/configs/app_size.dart';
|
||||||
|
import 'package:filesize/filesize.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:get/get.dart';
|
||||||
|
import 'package:tencent_im_sdk_plugin/models/v2_tim_message.dart';
|
||||||
|
|
||||||
|
class ShowFileMessage extends StatelessWidget {
|
||||||
|
final V2TimMessage message;
|
||||||
|
const ShowFileMessage(this.message, {Key? key}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return InkWell(
|
||||||
|
onTap: () async {
|
||||||
|
// try {
|
||||||
|
// var result = await OpenFile.open(message.fileElem!.localUrl);
|
||||||
|
// if (result.type != ResultType.done) {
|
||||||
|
// UiTools.toast(result.message);
|
||||||
|
// }
|
||||||
|
// } catch (e) {
|
||||||
|
// UiTools.toast(e.toString());
|
||||||
|
// }
|
||||||
|
},
|
||||||
|
child: Container(
|
||||||
|
padding: const EdgeInsets.all(12),
|
||||||
|
width: Get.width * 0.618,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
borderRadius: BorderRadius.circular(4),
|
||||||
|
color: AppColors.white,
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
message.fileElem!.fileName!,
|
||||||
|
overflow: TextOverflow.visible,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: AppSize.fontSize,
|
||||||
|
color: AppColors.active,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Text(
|
||||||
|
filesize(message.fileElem!.fileSize),
|
||||||
|
overflow: TextOverflow.visible,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: AppSize.smallFontSize,
|
||||||
|
color: AppColors.unactive,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Image.asset(
|
||||||
|
'assets/chats/file_msg.png',
|
||||||
|
width: 44,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
59
lib/views/conversation/widgets/show_group_card_message.dart
Normal file
59
lib/views/conversation/widgets/show_group_card_message.dart
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
import 'package:chat/configs/app_colors.dart';
|
||||||
|
import 'package:chat/configs/app_size.dart';
|
||||||
|
import 'package:chat/models/im/group_card_model.dart';
|
||||||
|
import 'package:chat/views/home/widgets/group_avatar.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:get/get.dart';
|
||||||
|
import 'package:tencent_im_sdk_plugin/models/v2_tim_message.dart';
|
||||||
|
|
||||||
|
class ShowGroupCardMessage extends StatelessWidget {
|
||||||
|
final V2TimMessage message;
|
||||||
|
const ShowGroupCardMessage(this.message, {Key? key}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
var model = GroupCardModel.fromJson(
|
||||||
|
json.decode(message.customElem!.data!),
|
||||||
|
);
|
||||||
|
return Container(
|
||||||
|
width: Get.width * 0.618,
|
||||||
|
padding: const EdgeInsets.all(12),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
borderRadius: BorderRadius.circular(4),
|
||||||
|
color: AppColors.white,
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
GroupAvatar(model.groupID),
|
||||||
|
const SizedBox(
|
||||||
|
width: 8,
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
model.groupName,
|
||||||
|
style: const TextStyle(
|
||||||
|
color: AppColors.primary,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
maxLines: 1,
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const Divider(height: 16),
|
||||||
|
const Text(
|
||||||
|
'个人名片',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: AppSize.smallFontSize,
|
||||||
|
color: AppColors.primary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
123
lib/views/conversation/widgets/show_image_message.dart
Normal file
123
lib/views/conversation/widgets/show_image_message.dart
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:cached_network_image/cached_network_image.dart';
|
||||||
|
import 'package:chat/configs/app_colors.dart';
|
||||||
|
import 'package:chat/constants/message_constant.dart';
|
||||||
|
import 'package:chat/views/conversation/preview/image_widget.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:get/get.dart';
|
||||||
|
import 'package:tencent_im_sdk_plugin/models/v2_tim_image.dart';
|
||||||
|
import 'package:tencent_im_sdk_plugin/models/v2_tim_message.dart';
|
||||||
|
|
||||||
|
class ShowImageMessage extends StatelessWidget {
|
||||||
|
final V2TimMessage message;
|
||||||
|
|
||||||
|
const ShowImageMessage(this.message, {Key? key}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Container(
|
||||||
|
decoration: const BoxDecoration(
|
||||||
|
borderRadius: BorderRadius.all(Radius.circular(4)),
|
||||||
|
),
|
||||||
|
constraints: BoxConstraints(
|
||||||
|
maxWidth: Get.width * 0.362,
|
||||||
|
minWidth: 64,
|
||||||
|
maxHeight: 192,
|
||||||
|
),
|
||||||
|
clipBehavior: Clip.hardEdge,
|
||||||
|
child: _imageBuilder(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _imageBuilder() {
|
||||||
|
var path = message.imageElem!.path!;
|
||||||
|
var list = message.imageElem!.imageList!;
|
||||||
|
|
||||||
|
V2TimImage? small = list.where((e) => e!.type == 2).first;
|
||||||
|
V2TimImage? big = list.where((e) => e!.type == 1).first;
|
||||||
|
V2TimImage? original = list.where((e) => e!.type == 0).first;
|
||||||
|
|
||||||
|
/// 如果是从本机发出去的图片消息,并且图片还存在的情况
|
||||||
|
if (path.isNotEmpty && File(path).existsSync()) {
|
||||||
|
return _showLocalImageFile(path, original!.localUrl!);
|
||||||
|
} else if (small != null && File(small.localUrl!).existsSync()) {
|
||||||
|
return _showLocalImageFile(small.localUrl!, original!.localUrl!);
|
||||||
|
} else if (big != null && File(big.localUrl!).existsSync()) {
|
||||||
|
return _showLocalImageFile(big.localUrl!, original!.localUrl!);
|
||||||
|
} else if (original != null && File(original.localUrl!).existsSync()) {
|
||||||
|
return _showLocalImageFile(original.localUrl!, original.localUrl!);
|
||||||
|
} else {
|
||||||
|
return GestureDetector(
|
||||||
|
onTap: () {
|
||||||
|
Get.to(
|
||||||
|
PreviewImageWidget(
|
||||||
|
type: IMG_PREVIEW_TYPE.url,
|
||||||
|
path: big!.url!,
|
||||||
|
original: original!.url!,
|
||||||
|
),
|
||||||
|
transition: Transition.zoom,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
child: CachedNetworkImage(
|
||||||
|
imageUrl: small!.url!,
|
||||||
|
cacheKey: 'CHAT_IMAGE',
|
||||||
|
alignment: Alignment.topCenter,
|
||||||
|
errorWidget: (context, error, stackTrace) => _errorDisplay(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _showLocalImageFile(String path, String original) {
|
||||||
|
return GestureDetector(
|
||||||
|
onTap: () {
|
||||||
|
Get.to(
|
||||||
|
PreviewImageWidget(
|
||||||
|
type: IMG_PREVIEW_TYPE.local,
|
||||||
|
path: path,
|
||||||
|
original: original,
|
||||||
|
),
|
||||||
|
transition: Transition.zoom,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
child: Image.file(
|
||||||
|
File(path),
|
||||||
|
fit: BoxFit.fitWidth,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _errorDisplay() {
|
||||||
|
return Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
borderRadius: BorderRadius.circular(4),
|
||||||
|
border: Border.all(
|
||||||
|
width: 0.4,
|
||||||
|
color: AppColors.unactive,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
height: 96,
|
||||||
|
child: Center(
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: const [
|
||||||
|
Icon(
|
||||||
|
Icons.warning_amber_outlined,
|
||||||
|
size: 20,
|
||||||
|
color: AppColors.unactive,
|
||||||
|
),
|
||||||
|
SizedBox(width: 4),
|
||||||
|
Text(
|
||||||
|
'图片加载失败',
|
||||||
|
style: TextStyle(
|
||||||
|
color: AppColors.unactive,
|
||||||
|
fontSize: 12,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
283
lib/views/conversation/widgets/show_image_message_copy.dart
Normal file
283
lib/views/conversation/widgets/show_image_message_copy.dart
Normal file
@@ -0,0 +1,283 @@
|
|||||||
|
// import 'dart:io';
|
||||||
|
// import 'dart:math';
|
||||||
|
// import 'package:cached_network_image/cached_network_image.dart';
|
||||||
|
// import 'package:flutter/material.dart';
|
||||||
|
// import 'package:get/get.dart';
|
||||||
|
// import 'package:scaffold/constants/message_constant.dart';
|
||||||
|
// import 'package:scaffold/utils/ui_tools.dart';
|
||||||
|
// import 'package:scaffold/views/im/conversation/preview/index_page.dart';
|
||||||
|
// import 'package:scaffold/views/moments/index/widgets/media_preview.dart';
|
||||||
|
// import 'package:tencent_im_sdk_plugin/models/v2_tim_image.dart';
|
||||||
|
// import 'package:tencent_im_sdk_plugin/models/v2_tim_message.dart';
|
||||||
|
// import 'package:transparent_image/transparent_image.dart';
|
||||||
|
|
||||||
|
// class ShowImageMessage extends StatefulWidget {
|
||||||
|
// final V2TimMessage message;
|
||||||
|
|
||||||
|
// const ShowImageMessage(this.message, {Key? key}) : super(key: key);
|
||||||
|
|
||||||
|
// @override
|
||||||
|
// State<ShowImageMessage> createState() => _ShowImageMessageState();
|
||||||
|
// }
|
||||||
|
|
||||||
|
// class _ShowImageMessageState extends State<ShowImageMessage> {
|
||||||
|
// bool imageIsRender = false;
|
||||||
|
|
||||||
|
// @override
|
||||||
|
// didUpdateWidget(oldWidget) {
|
||||||
|
// var oldImgListLength = oldWidget.message.imageElem?.imageList?.length ?? 0;
|
||||||
|
// var currImgListLength = widget.message.imageElem?.imageList?.length ?? 0;
|
||||||
|
// if (currImgListLength == 1 && oldImgListLength == 0) {
|
||||||
|
// setState(() {
|
||||||
|
// imageIsRender = true;
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
// super.didUpdateWidget(oldWidget);
|
||||||
|
// }
|
||||||
|
|
||||||
|
// @override
|
||||||
|
// Widget build(BuildContext context) {
|
||||||
|
// V2TimImage? originalImg =
|
||||||
|
// getImageFromList(V2_TIM_IMAGE_TYPES_ENUM.original);
|
||||||
|
// V2TimImage? smallImg = getImageFromList(V2_TIM_IMAGE_TYPES_ENUM.small);
|
||||||
|
|
||||||
|
// return Container(
|
||||||
|
// decoration: BoxDecoration(
|
||||||
|
// borderRadius: const BorderRadius.all(Radius.circular(4)),
|
||||||
|
// border: Border.all(
|
||||||
|
// color: const Color.fromRGBO(245, 166, 35, 0),
|
||||||
|
// width: 2,
|
||||||
|
// ),
|
||||||
|
// ),
|
||||||
|
// child: LayoutBuilder(
|
||||||
|
// builder: (BuildContext context, BoxConstraints constraints) {
|
||||||
|
// return ConstrainedBox(
|
||||||
|
// constraints: BoxConstraints(
|
||||||
|
// maxWidth: constraints.maxWidth * 0.5,
|
||||||
|
// minWidth: 64,
|
||||||
|
// maxHeight: 256,
|
||||||
|
// ),
|
||||||
|
// child: imageBuilder(
|
||||||
|
// originalImg: originalImg,
|
||||||
|
// smallImg: smallImg,
|
||||||
|
// ),
|
||||||
|
// );
|
||||||
|
// },
|
||||||
|
// ),
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
|
||||||
|
// Widget imageBuilder({V2TimImage? originalImg, V2TimImage? smallImg}) {
|
||||||
|
// if (originalImg == null) {
|
||||||
|
// // 有path
|
||||||
|
// if (widget.message.imageElem!.path!.isNotEmpty &&
|
||||||
|
// File(widget.message.imageElem!.path!).existsSync()) {
|
||||||
|
// return getImage(
|
||||||
|
// GestureDetector(
|
||||||
|
// onTap: () {
|
||||||
|
// Get.to(ImagePreviewPage(
|
||||||
|
// type: IMG_PREVIEW_TYPE.local,
|
||||||
|
// path: widget.message.imageElem!.path!,
|
||||||
|
// ));
|
||||||
|
// },
|
||||||
|
// child: Image.file(
|
||||||
|
// File(widget.message.imageElem!.path!),
|
||||||
|
// fit: BoxFit.fitWidth,
|
||||||
|
// ),
|
||||||
|
// ),
|
||||||
|
// imageElem: null,
|
||||||
|
// );
|
||||||
|
// } else {
|
||||||
|
// return errorDisplay();
|
||||||
|
// }
|
||||||
|
// } else if (!Platform.isAndroid &&
|
||||||
|
// widget.message.imageElem!.path!.isNotEmpty &&
|
||||||
|
// File(widget.message.imageElem!.path!).existsSync() &&
|
||||||
|
// !imageIsRender) {
|
||||||
|
// return getImage(
|
||||||
|
// GestureDetector(
|
||||||
|
// onTap: () {
|
||||||
|
// Get.to(ImagePreviewPage(
|
||||||
|
// type: IMG_PREVIEW_TYPE.local,
|
||||||
|
// path: widget.message.imageElem!.path!,
|
||||||
|
// ));
|
||||||
|
// },
|
||||||
|
// child: Image.file(
|
||||||
|
// File(widget.message.imageElem!.path!),
|
||||||
|
// fit: BoxFit.fitWidth,
|
||||||
|
// ),
|
||||||
|
// ),
|
||||||
|
// imageElem: e,
|
||||||
|
// );
|
||||||
|
// } else if ((smallImg?.url ?? originalImg.url) != null && !imageIsRender) {
|
||||||
|
// double positionRadio = 1.0;
|
||||||
|
// if (smallImg?.width != null &&
|
||||||
|
// smallImg?.height != null &&
|
||||||
|
// smallImg?.width != 0 &&
|
||||||
|
// smallImg?.height != 0) {
|
||||||
|
// positionRadio = (smallImg!.width! / smallImg.height!);
|
||||||
|
// }
|
||||||
|
// String bigImgUrl = originalImg.url ?? getBigPicUrl();
|
||||||
|
// if (bigImgUrl.isEmpty && smallImg?.url != null) {
|
||||||
|
// bigImgUrl = smallImg!.url!;
|
||||||
|
// }
|
||||||
|
// return Stack(
|
||||||
|
// alignment: AlignmentDirectional.topStart,
|
||||||
|
// children: [
|
||||||
|
// AspectRatio(
|
||||||
|
// aspectRatio: positionRadio,
|
||||||
|
// child: Container(
|
||||||
|
// decoration: const BoxDecoration(color: Colors.white),
|
||||||
|
// ),
|
||||||
|
// ),
|
||||||
|
// getImage(
|
||||||
|
// GestureDetector(
|
||||||
|
// onTap: () {
|
||||||
|
// Get.to(ImagePreviewPage(
|
||||||
|
// type: IMG_PREVIEW_TYPE.url,
|
||||||
|
// path: smallImg?.url ?? originalImg.url!,
|
||||||
|
// ));
|
||||||
|
// },
|
||||||
|
// child: CachedNetworkImage(
|
||||||
|
// alignment: Alignment.topCenter,
|
||||||
|
// imageUrl: smallImg?.url ?? originalImg.url!,
|
||||||
|
// errorWidget: (context, error, stackTrace) => errorDisplay(),
|
||||||
|
// fit: BoxFit.fitWidth,
|
||||||
|
// cacheKey: smallImg?.uuid ?? originalImg.uuid!,
|
||||||
|
// placeholder: (context, url) =>
|
||||||
|
// Image(image: MemoryImage(kTransparentImage)),
|
||||||
|
// fadeInDuration: const Duration(milliseconds: 0),
|
||||||
|
// ),
|
||||||
|
// ),
|
||||||
|
// imageElem: e,
|
||||||
|
// ),
|
||||||
|
// ],
|
||||||
|
// );
|
||||||
|
// } else {
|
||||||
|
// // 有path
|
||||||
|
// if (widget.message.imageElem!.path!.isNotEmpty &&
|
||||||
|
// File(widget.message.imageElem!.path!).existsSync()) {
|
||||||
|
// return getImage(
|
||||||
|
// GestureDetector(
|
||||||
|
// onTap: () {
|
||||||
|
// Get.to(ImagePreviewPage(
|
||||||
|
// type: IMG_PREVIEW_TYPE.local,
|
||||||
|
// path: widget.message.imageElem!.path!,
|
||||||
|
// ));
|
||||||
|
// },
|
||||||
|
// child: Image.file(
|
||||||
|
// File(widget.message.imageElem!.path!),
|
||||||
|
// fit: BoxFit.fitWidth,
|
||||||
|
// ),
|
||||||
|
// ),
|
||||||
|
// imageElem: null,
|
||||||
|
// );
|
||||||
|
// } else {
|
||||||
|
// return errorDisplay();
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// Widget errorDisplay() {
|
||||||
|
// return Container(
|
||||||
|
// decoration: BoxDecoration(
|
||||||
|
// borderRadius: const BorderRadius.all(Radius.circular(5)),
|
||||||
|
// border: Border.all(
|
||||||
|
// width: 1,
|
||||||
|
// color: Colors.black12,
|
||||||
|
// ),
|
||||||
|
// ),
|
||||||
|
// height: 100,
|
||||||
|
// child: Center(
|
||||||
|
// child: Row(
|
||||||
|
// mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
// children: const [
|
||||||
|
// Icon(
|
||||||
|
// Icons.warning_amber_outlined,
|
||||||
|
// size: 16,
|
||||||
|
// ),
|
||||||
|
// Text(
|
||||||
|
// '图片加载失败',
|
||||||
|
// ),
|
||||||
|
// ],
|
||||||
|
// ),
|
||||||
|
// ),
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
|
||||||
|
// void pushToPreview(List<String> mediaList) {
|
||||||
|
// Get.dialog(
|
||||||
|
// MomentMediaPreview(
|
||||||
|
// mediaSourceList: mediaList,
|
||||||
|
// initialPage: 0,
|
||||||
|
// ),
|
||||||
|
// useSafeArea: false,
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
|
||||||
|
// Widget getImage(image, {imageElem}) {
|
||||||
|
// Widget res = ClipRRect(
|
||||||
|
// clipper: ImageClipper(),
|
||||||
|
// child: image,
|
||||||
|
// );
|
||||||
|
// return res;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// V2TimImage? getImageFromList(V2_TIM_IMAGE_TYPES_ENUM imgType) {
|
||||||
|
// V2TimImage? img = getImageFromImgList(
|
||||||
|
// widget.message.imageElem!.imageList,
|
||||||
|
// HistoryMessageDartConstant.imgPriorMap[imgType] ??
|
||||||
|
// HistoryMessageDartConstant.oriImgPrior,
|
||||||
|
// );
|
||||||
|
// return img;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// String getBigPicUrl() {
|
||||||
|
// // 实际拿的是原图
|
||||||
|
// V2TimImage? img = getImageFromImgList(
|
||||||
|
// widget.message.imageElem!.imageList,
|
||||||
|
// HistoryMessageDartConstant.oriImgPrior,
|
||||||
|
// );
|
||||||
|
// if (img == null) {
|
||||||
|
// setState(() {
|
||||||
|
// imageIsRender = true;
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
// return img == null ? widget.message.imageElem!.path! : img.url!;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// V2TimImage? getImageFromImgList(List<V2TimImage?>? list, List<String> order) {
|
||||||
|
// V2TimImage? img;
|
||||||
|
// try {
|
||||||
|
// for (String type in order) {
|
||||||
|
// img = list?.firstWhere(
|
||||||
|
// (e) => e?.type == HistoryMessageDartConstant.V2_TIM_IMAGE_TYPES[type],
|
||||||
|
// orElse: () => null,
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
// } catch (e) {
|
||||||
|
// UiTools.toast('getImageFromImgList error ${e.toString()}');
|
||||||
|
// }
|
||||||
|
// return img;
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// class ImageClipper extends CustomClipper<RRect> {
|
||||||
|
// @override
|
||||||
|
// RRect getClip(Size size) {
|
||||||
|
// return RRect.fromRectAndRadius(
|
||||||
|
// Rect.fromLTWH(
|
||||||
|
// 0,
|
||||||
|
// 0,
|
||||||
|
// size.width,
|
||||||
|
// min(size.height, 256),
|
||||||
|
// ),
|
||||||
|
// const Radius.circular(5),
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
|
||||||
|
// @override
|
||||||
|
// bool shouldReclip(CustomClipper<RRect> oldClipper) {
|
||||||
|
// return oldClipper != this;
|
||||||
|
// }
|
||||||
|
// }
|
||||||
101
lib/views/conversation/widgets/show_location_message.dart
Normal file
101
lib/views/conversation/widgets/show_location_message.dart
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
import 'package:chat/configs/app_colors.dart';
|
||||||
|
import 'package:chat/configs/app_size.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:get/get.dart';
|
||||||
|
import 'package:permission_handler/permission_handler.dart';
|
||||||
|
import 'package:tencent_im_sdk_plugin/models/v2_tim_message.dart';
|
||||||
|
|
||||||
|
class ShowLocationMessage extends StatelessWidget {
|
||||||
|
final V2TimMessage message;
|
||||||
|
const ShowLocationMessage(this.message, {Key? key}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return InkWell(
|
||||||
|
onTap: () {
|
||||||
|
Permission.location.request().isGranted.then((value) {
|
||||||
|
if (value) {
|
||||||
|
// Get.toNamed(
|
||||||
|
// ConversationRoutes.conversationMapShow,
|
||||||
|
// arguments: {
|
||||||
|
// 'message': message,
|
||||||
|
// },
|
||||||
|
// );
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
child: Container(
|
||||||
|
width: Get.width * 0.618,
|
||||||
|
padding: const EdgeInsets.all(12),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
borderRadius: BorderRadius.circular(4),
|
||||||
|
color: message.isSelf! ? AppColors.primary : AppColors.white,
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
Icons.pin_drop_outlined,
|
||||||
|
size: 44,
|
||||||
|
color: message.isSelf! ? AppColors.white : AppColors.primary,
|
||||||
|
),
|
||||||
|
const SizedBox(
|
||||||
|
width: 8,
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
message.locationElem!.desc!,
|
||||||
|
maxLines: 2,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
style: TextStyle(
|
||||||
|
color:
|
||||||
|
message.isSelf! ? AppColors.white : AppColors.primary,
|
||||||
|
fontSize: AppSize.fontSize,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Divider(
|
||||||
|
height: 16,
|
||||||
|
color: message.isSelf! ? AppColors.white : null,
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
'位置消息',
|
||||||
|
style: TextStyle(
|
||||||
|
color: message.isSelf! ? AppColors.white : AppColors.primary,
|
||||||
|
fontSize: AppSize.smallFontSize,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
// child: Container(
|
||||||
|
// padding: const EdgeInsets.all(12),
|
||||||
|
// decoration: BoxDecoration(
|
||||||
|
// borderRadius: BorderRadius.circular(4),
|
||||||
|
// color: message.isSelf! ? AppColors.primary : AppColors.white,
|
||||||
|
// ),
|
||||||
|
// child: Row(
|
||||||
|
// children: [
|
||||||
|
// Icon(
|
||||||
|
// Icons.pin_drop_outlined,
|
||||||
|
// size: 20,
|
||||||
|
// color: message.isSelf! ? AppColors.white : AppColors.active,
|
||||||
|
// ),
|
||||||
|
// Text(
|
||||||
|
// '【位置消息】点击查看',
|
||||||
|
// style: TextStyle(
|
||||||
|
// fontSize: AppSize.fontSize,
|
||||||
|
// color: message.isSelf! ? AppColors.white : AppColors.active,
|
||||||
|
// ),
|
||||||
|
// ),
|
||||||
|
// ],
|
||||||
|
// ),
|
||||||
|
// ),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
12
lib/views/conversation/widgets/show_merger_message.dart
Normal file
12
lib/views/conversation/widgets/show_merger_message.dart
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:tencent_im_sdk_plugin/models/v2_tim_message.dart';
|
||||||
|
|
||||||
|
class ShowMergerMessage extends StatelessWidget {
|
||||||
|
final V2TimMessage message;
|
||||||
|
const ShowMergerMessage(this.message, {Key? key}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Container();
|
||||||
|
}
|
||||||
|
}
|
||||||
76
lib/views/conversation/widgets/show_name_card_message.dart
Normal file
76
lib/views/conversation/widgets/show_name_card_message.dart
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
import 'package:chat/configs/app_colors.dart';
|
||||||
|
import 'package:chat/configs/app_size.dart';
|
||||||
|
import 'package:chat/controllers/private_controller.dart';
|
||||||
|
import 'package:chat/models/im/name_card_model.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/models/v2_tim_message.dart';
|
||||||
|
|
||||||
|
/// 个人名片消息
|
||||||
|
class ShowNameCardMessage extends StatelessWidget {
|
||||||
|
final V2TimMessage message;
|
||||||
|
|
||||||
|
const ShowNameCardMessage(this.message, {Key? key}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
var model = NameCardModel.fromJson(
|
||||||
|
json.decode(message.customElem!.data!),
|
||||||
|
);
|
||||||
|
|
||||||
|
return InkWell(
|
||||||
|
onTap: () async {
|
||||||
|
await PrivateController.to.setCurrentFriend(
|
||||||
|
model.userID,
|
||||||
|
);
|
||||||
|
Get.toNamed(
|
||||||
|
ContactRoutes.friendProfile,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
child: Container(
|
||||||
|
width: Get.width * 0.618,
|
||||||
|
padding: const EdgeInsets.all(12),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
borderRadius: BorderRadius.circular(4),
|
||||||
|
color: AppColors.white,
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
CustomAvatar(
|
||||||
|
model.avatar,
|
||||||
|
size: 44,
|
||||||
|
),
|
||||||
|
const SizedBox(
|
||||||
|
width: 8,
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
model.userName,
|
||||||
|
style: const TextStyle(
|
||||||
|
color: AppColors.primary,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
maxLines: 1,
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const Divider(height: 16),
|
||||||
|
const Text(
|
||||||
|
'个人名片',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: AppSize.smallFontSize,
|
||||||
|
color: AppColors.unactive,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
140
lib/views/conversation/widgets/show_sound_message.dart
Normal file
140
lib/views/conversation/widgets/show_sound_message.dart
Normal file
@@ -0,0 +1,140 @@
|
|||||||
|
import 'dart:io';
|
||||||
|
import 'dart:math';
|
||||||
|
import 'package:chat/configs/app_colors.dart';
|
||||||
|
import 'package:chat/configs/app_size.dart';
|
||||||
|
import 'package:chat/utils/sound_record.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:get/get.dart';
|
||||||
|
import 'package:permission_handler/permission_handler.dart';
|
||||||
|
import 'package:tencent_im_sdk_plugin/models/v2_tim_message.dart';
|
||||||
|
|
||||||
|
class ShowSoundMessage extends StatefulWidget {
|
||||||
|
final V2TimMessage message;
|
||||||
|
|
||||||
|
const ShowSoundMessage(this.message, {Key? key}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<ShowSoundMessage> createState() => _ShowSoundMessageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ShowSoundMessageState extends State<ShowSoundMessage> {
|
||||||
|
bool isPlaying = false;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
SoundPlayer.playStateListener(listener: (_) {
|
||||||
|
if (_.playState == "complete") {
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {
|
||||||
|
isPlaying = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
SoundPlayer.setSoundInterruptListener(() {
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {
|
||||||
|
isPlaying = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// eventBus.on().listen((event) {
|
||||||
|
// if (event == 'stop') {
|
||||||
|
// stopAndDispose();
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
}
|
||||||
|
|
||||||
|
void stopAndDispose() async {
|
||||||
|
if (isPlaying) {
|
||||||
|
await SoundPlayer.stop();
|
||||||
|
SoundPlayer.dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_playSound() async {
|
||||||
|
if (!SoundPlayer.isInited) {
|
||||||
|
bool hasMicrophonePermission =
|
||||||
|
await Permission.microphone.request().isGranted;
|
||||||
|
bool hasStoragePermission =
|
||||||
|
Platform.isIOS || await Permission.storage.request().isGranted;
|
||||||
|
if (hasMicrophonePermission && hasStoragePermission) {
|
||||||
|
SoundPlayer.initSoundPlayer();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (isPlaying) {
|
||||||
|
SoundPlayer.stop();
|
||||||
|
setState(() {
|
||||||
|
isPlaying = false;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
SoundPlayer.play(url: widget.message.soundElem!.url!);
|
||||||
|
setState(() {
|
||||||
|
isPlaying = true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return InkWell(
|
||||||
|
onTap: () {
|
||||||
|
_playSound();
|
||||||
|
},
|
||||||
|
child: Container(
|
||||||
|
constraints: BoxConstraints(
|
||||||
|
maxWidth: Get.width * 0.618,
|
||||||
|
minWidth: 64,
|
||||||
|
minHeight: 43,
|
||||||
|
),
|
||||||
|
width:
|
||||||
|
(widget.message.soundElem!.duration! / 60) * (Get.width * 0.618) +
|
||||||
|
64,
|
||||||
|
padding: const EdgeInsets.all(12),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
borderRadius: BorderRadius.circular(4),
|
||||||
|
color: widget.message.isSelf! ? AppColors.primary : AppColors.white,
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: widget.message.isSelf!
|
||||||
|
? MainAxisAlignment.start
|
||||||
|
: MainAxisAlignment.end,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'${widget.message.soundElem!.duration}"',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: AppSize.fontSize,
|
||||||
|
color: widget.message.isSelf!
|
||||||
|
? AppColors.white
|
||||||
|
: AppColors.primary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 4),
|
||||||
|
Transform.rotate(
|
||||||
|
angle: widget.message.isSelf! ? pi : 0,
|
||||||
|
child: const Icon(
|
||||||
|
Icons.volume_up,
|
||||||
|
size: 18,
|
||||||
|
color: AppColors.white,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
// Image.asset(
|
||||||
|
// widget.message.isSelf!
|
||||||
|
// ? isPlaying
|
||||||
|
// ? 'assets/chats/play_voice_send.gif'
|
||||||
|
// : 'assets/chats/voice_send.png'
|
||||||
|
// : isPlaying
|
||||||
|
// ? 'assets/chats/play_voice_receive.gif'
|
||||||
|
// : 'assets/chats/voice_receive.png',
|
||||||
|
// height: 16,
|
||||||
|
// color:
|
||||||
|
// widget.message.isSelf! ? AppColors.white : AppColors.primary,
|
||||||
|
// ),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
32
lib/views/conversation/widgets/show_text_message.dart
Normal file
32
lib/views/conversation/widgets/show_text_message.dart
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import 'package:chat/configs/app_colors.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:get/get.dart';
|
||||||
|
import 'package:tencent_im_sdk_plugin/models/v2_tim_message.dart';
|
||||||
|
|
||||||
|
class ShowTextMessage extends StatelessWidget {
|
||||||
|
final V2TimMessage message;
|
||||||
|
const ShowTextMessage(this.message, {Key? key}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Container(
|
||||||
|
constraints: BoxConstraints(
|
||||||
|
maxWidth: Get.width - 128,
|
||||||
|
minHeight: 43,
|
||||||
|
),
|
||||||
|
padding: const EdgeInsets.all(12),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
borderRadius: BorderRadius.circular(4),
|
||||||
|
color: message.isSelf! ? AppColors.primary : AppColors.white,
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
message.textElem!.text!,
|
||||||
|
overflow: TextOverflow.visible,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 14,
|
||||||
|
color: message.isSelf! ? AppColors.white : AppColors.active,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
113
lib/views/conversation/widgets/show_transfer_message.dart
Normal file
113
lib/views/conversation/widgets/show_transfer_message.dart
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
import 'package:chat/configs/app_colors.dart';
|
||||||
|
import 'package:chat/configs/app_size.dart';
|
||||||
|
import 'package:chat/models/im/transfer_model.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:get/get.dart';
|
||||||
|
import 'package:tencent_im_sdk_plugin_platform_interface/models/v2_tim_message.dart';
|
||||||
|
|
||||||
|
class ShowTransferMessage extends StatefulWidget {
|
||||||
|
final V2TimMessage message;
|
||||||
|
|
||||||
|
const ShowTransferMessage(this.message, {Key? key}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<ShowTransferMessage> createState() => _ShowTransferMessageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ShowTransferMessageState extends State<ShowTransferMessage> {
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
var model = TransferModel.fromJson(
|
||||||
|
json.decode(widget.message.customElem!.data!),
|
||||||
|
);
|
||||||
|
|
||||||
|
return InkWell(
|
||||||
|
onTap: () {
|
||||||
|
// Get.toNamed(
|
||||||
|
// ConversationRoutes.conversationTransferDetail,
|
||||||
|
// arguments: {
|
||||||
|
// 'order_id': model.orderId,
|
||||||
|
// },
|
||||||
|
// );
|
||||||
|
},
|
||||||
|
child: Container(
|
||||||
|
width: Get.width * 0.618,
|
||||||
|
padding: const EdgeInsets.all(12),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
borderRadius: BorderRadius.circular(4),
|
||||||
|
color: widget.message.isSelf! ? AppColors.primary : AppColors.white,
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
Icons.currency_exchange,
|
||||||
|
size: 44,
|
||||||
|
color: widget.message.isSelf!
|
||||||
|
? AppColors.white
|
||||||
|
: AppColors.primary,
|
||||||
|
),
|
||||||
|
const SizedBox(
|
||||||
|
width: 8,
|
||||||
|
),
|
||||||
|
Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'¥',
|
||||||
|
style: TextStyle(
|
||||||
|
color: widget.message.isSelf!
|
||||||
|
? AppColors.white
|
||||||
|
: AppColors.primary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
model.amount,
|
||||||
|
style: TextStyle(
|
||||||
|
color: widget.message.isSelf!
|
||||||
|
? AppColors.white
|
||||||
|
: AppColors.primary,
|
||||||
|
fontSize: AppSize.titleFontSize,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
'发起积分转账',
|
||||||
|
style: TextStyle(
|
||||||
|
color: widget.message.isSelf!
|
||||||
|
? AppColors.white
|
||||||
|
: AppColors.primary,
|
||||||
|
fontSize: AppSize.fontSize,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Divider(
|
||||||
|
height: 16,
|
||||||
|
color: widget.message.isSelf! ? AppColors.white : null,
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
'DT积分转账',
|
||||||
|
style: TextStyle(
|
||||||
|
color: widget.message.isSelf!
|
||||||
|
? AppColors.white
|
||||||
|
: AppColors.primary,
|
||||||
|
fontSize: AppSize.smallFontSize,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
68
lib/views/conversation/widgets/show_video_message.dart
Normal file
68
lib/views/conversation/widgets/show_video_message.dart
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
import 'dart:io';
|
||||||
|
import 'package:cached_network_image/cached_network_image.dart';
|
||||||
|
import 'package:chat/configs/app_colors.dart';
|
||||||
|
import 'package:chat/views/conversation/preview/video_widget.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:get/get.dart';
|
||||||
|
import 'package:tencent_im_sdk_plugin/models/v2_tim_message.dart';
|
||||||
|
|
||||||
|
class ShowVideoMessage extends StatelessWidget {
|
||||||
|
final V2TimMessage message;
|
||||||
|
const ShowVideoMessage(this.message, {Key? key}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return InkWell(
|
||||||
|
onTap: () {
|
||||||
|
Get.to(
|
||||||
|
PreviewVideoWidget(message.videoElem!),
|
||||||
|
transition: Transition.zoom,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
child: Container(
|
||||||
|
constraints: BoxConstraints(
|
||||||
|
maxWidth: Get.width * 0.382,
|
||||||
|
maxHeight: 192,
|
||||||
|
minHeight: 96,
|
||||||
|
minWidth: 96,
|
||||||
|
),
|
||||||
|
child: ClipRRect(
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
child: Stack(
|
||||||
|
children: [
|
||||||
|
_showVideSnapshot(),
|
||||||
|
const Positioned.fill(
|
||||||
|
child: Center(
|
||||||
|
child: Icon(
|
||||||
|
Icons.play_circle_outline,
|
||||||
|
size: 64,
|
||||||
|
color: AppColors.white,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _showVideSnapshot() {
|
||||||
|
String ss = message.videoElem!.localSnapshotUrl!;
|
||||||
|
String? su = message.videoElem!.snapshotUrl;
|
||||||
|
|
||||||
|
if (ss.isNotEmpty && File(ss).existsSync()) {
|
||||||
|
return Image.file(
|
||||||
|
File(ss),
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
);
|
||||||
|
} else if (su != null && su.isNotEmpty) {
|
||||||
|
return CachedNetworkImage(
|
||||||
|
imageUrl: su,
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return Container();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
69
lib/views/conversation/widgets/tim_emoji_panel.dart
Normal file
69
lib/views/conversation/widgets/tim_emoji_panel.dart
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
import 'package:chat/configs/app_colors.dart';
|
||||||
|
import 'package:chat/configs/emoji.dart';
|
||||||
|
import 'package:chat/models/im/emoji_model.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
class EmojiPanel extends StatelessWidget {
|
||||||
|
final void Function(int unicode) onTapEmoji;
|
||||||
|
|
||||||
|
const EmojiPanel({
|
||||||
|
Key? key,
|
||||||
|
required this.onTapEmoji,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return SingleChildScrollView(
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
height: 248,
|
||||||
|
color: AppColors.page,
|
||||||
|
child: Stack(
|
||||||
|
children: [
|
||||||
|
GridView(
|
||||||
|
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
||||||
|
crossAxisCount: 8,
|
||||||
|
childAspectRatio: 1,
|
||||||
|
),
|
||||||
|
children: emojiData.map(
|
||||||
|
(e) {
|
||||||
|
var item = EmojiModel.fromJson(e);
|
||||||
|
return InkWell(
|
||||||
|
onTap: () {
|
||||||
|
onTapEmoji(item.unicode);
|
||||||
|
},
|
||||||
|
child: Center(
|
||||||
|
child: EmojiItem(
|
||||||
|
name: item.name,
|
||||||
|
unicode: item.unicode,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
).toList(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class EmojiItem extends StatelessWidget {
|
||||||
|
const EmojiItem({Key? key, required this.name, required this.unicode})
|
||||||
|
: super(key: key);
|
||||||
|
final String name;
|
||||||
|
final int unicode;
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Text(
|
||||||
|
String.fromCharCode(unicode),
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 26,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
91
pubspec.lock
91
pubspec.lock
@@ -94,6 +94,41 @@ packages:
|
|||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.0.1"
|
version: "1.0.1"
|
||||||
|
camera:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: camera
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "0.9.8+1"
|
||||||
|
camera_android:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: camera_android
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "0.9.8+3"
|
||||||
|
camera_avfoundation:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: camera_avfoundation
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "0.9.8+6"
|
||||||
|
camera_platform_interface:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: camera_platform_interface
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "2.2.2"
|
||||||
|
camera_web:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: camera_web
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "0.2.1+6"
|
||||||
characters:
|
characters:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -136,6 +171,13 @@ packages:
|
|||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.1.0"
|
version: "3.1.0"
|
||||||
|
cross_file:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: cross_file
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "0.3.3+2"
|
||||||
crypto:
|
crypto:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -227,6 +269,20 @@ packages:
|
|||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "6.1.4"
|
version: "6.1.4"
|
||||||
|
file_picker:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: file_picker
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "4.6.1"
|
||||||
|
filesize:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: filesize
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "2.0.1"
|
||||||
flutter:
|
flutter:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description: flutter
|
description: flutter
|
||||||
@@ -267,6 +323,20 @@ packages:
|
|||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.0.4"
|
version: "1.0.4"
|
||||||
|
flutter_plugin_android_lifecycle:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: flutter_plugin_android_lifecycle
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "2.0.7"
|
||||||
|
flutter_plugin_record_plus:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: flutter_plugin_record_plus
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "0.0.11"
|
||||||
flutter_spinkit:
|
flutter_spinkit:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -669,6 +739,13 @@ packages:
|
|||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "4.0.0"
|
version: "4.0.0"
|
||||||
|
quiver:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: quiver
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "3.1.0"
|
||||||
rxdart:
|
rxdart:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -744,6 +821,13 @@ packages:
|
|||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.0"
|
version: "2.1.0"
|
||||||
|
stream_transform:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: stream_transform
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "2.0.1"
|
||||||
string_scanner:
|
string_scanner:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -905,6 +989,13 @@ packages:
|
|||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "7.2.0"
|
version: "7.2.0"
|
||||||
|
wechat_camera_picker:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: wechat_camera_picker
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "3.5.0+1"
|
||||||
win32:
|
win32:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|||||||
@@ -49,6 +49,11 @@ dependencies:
|
|||||||
photo_manager: 2.2.1
|
photo_manager: 2.2.1
|
||||||
chewie: ^1.3.5
|
chewie: ^1.3.5
|
||||||
encrypt: ^5.0.1
|
encrypt: ^5.0.1
|
||||||
|
flutter_plugin_record_plus: ^0.0.11
|
||||||
|
wechat_camera_picker: ^3.5.0+1
|
||||||
|
camera_web: ^0.2.1+6
|
||||||
|
filesize: ^2.0.1
|
||||||
|
file_picker: ^4.6.1
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
|
|||||||
Reference in New Issue
Block a user