init
This commit is contained in:
+36
@@ -0,0 +1,36 @@
|
||||
List<Map<String, Object>> messageReactionEmojiData = [
|
||||
{"name": "red heart", "unicode": 10084},
|
||||
{"name": "OK", "unicode": 128076},
|
||||
{"name": "thumbs up", "unicode": 128077},
|
||||
{"name": "flexed biceps", "unicode": 128170},
|
||||
{"name": "waving hand", "unicode": 128079},
|
||||
{"name": "handshake", "unicode": 129309},
|
||||
{"name": "folded hands", "unicode": 128591},
|
||||
{"name": "raised fist", "unicode": 9994},
|
||||
{"name": "thumbs down", "unicode": 128078},
|
||||
{"name": "smiling face with sunglasses", "unicode": 128526},
|
||||
{"name": "GRINNING FACE WITH SMILING EYES", "unicode": 128513},
|
||||
{"name": "SMILING FACE WITH SMILING EYES", "unicode": 128522},
|
||||
{"name": "smiling face with hearts", "unicode": 129392},
|
||||
{"name": "SMILING FACE WITH HEART-SHAPED EYES", "unicode": 128525},
|
||||
{"name": "FACE WITH TEARS OF JOY", "unicode": 128514},
|
||||
{"name": "flushed face", "unicode": 128563},
|
||||
{"name": "SMILING FACE WITH OPEN MOUTH AND COLD SWEAT", "unicode": 128517},
|
||||
{"name": "FACE WITH COLD SWEAT", "unicode": 128531},
|
||||
{"name": "loudly crying face", "unicode": 128557},
|
||||
{"name": "CONFOUNDED FACE", "unicode": 128534},
|
||||
{"name": "KISSING FACE WITH CLOSED EYES", "unicode": 128538},
|
||||
{
|
||||
"name": "FACE WITH STUCK-OUT TONGUE AND TIGHTLY-CLOSED EYES",
|
||||
"unicode": 128541
|
||||
},
|
||||
{"name": "DISAPPOINTED FACE", "unicode": 128542},
|
||||
{"name": "ANGRY FACE", "unicode": 128544},
|
||||
{"name": "POUTING FACE", "unicode": 128545},
|
||||
{"name": "CRYING FACE", "unicode": 128546},
|
||||
{"name": "FACE WITH LOOK OF TRIUMPH", "unicode": 128548},
|
||||
{"name": "FEARFUL FACE", "unicode": 128552},
|
||||
{"name": "SLEEPY FACE", "unicode": 128554},
|
||||
{"name": "FACE WITH NO GOOD GESTURE", "unicode": 128581},
|
||||
{"name": "FACE WITH OK GESTURE", "unicode": 128582},
|
||||
];
|
||||
+181
@@ -0,0 +1,181 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/models/v2_tim_group_member_full_info.dart'
|
||||
if (dart.library.html) 'package:tencent_cloud_chat_sdk/web/compatible_models/v2_tim_group_member_full_info.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_base.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_state.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/business_logic/view_models/tui_self_info_view_model.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/data_services/services_locatar.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/widgets/avatar.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/theme/color.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/theme/tui_theme.dart';
|
||||
|
||||
class TIMUIKitMessageReactionDetail extends StatefulWidget {
|
||||
/// the index of the current emoji sticker
|
||||
final int currentStickerIndex;
|
||||
|
||||
/// the list of member
|
||||
final List<V2TimGroupMemberFullInfo?>? memberList;
|
||||
|
||||
/// message reaction map
|
||||
final Map<String, dynamic> messageReaction;
|
||||
|
||||
/// the sticker list from message reaction
|
||||
final List<int> stickerList;
|
||||
|
||||
final Function(String userID, TapDownDetails tapDetails)? onTapAvatar;
|
||||
|
||||
const TIMUIKitMessageReactionDetail(
|
||||
{required this.currentStickerIndex,
|
||||
this.memberList,
|
||||
required this.messageReaction,
|
||||
Key? key,
|
||||
required this.stickerList,
|
||||
this.onTapAvatar})
|
||||
: super(key: key);
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => TIMUIKitMessageReactionDetailState();
|
||||
}
|
||||
|
||||
class TIMUIKitMessageReactionDetailState extends TIMUIKitState<TIMUIKitMessageReactionDetail>
|
||||
with TickerProviderStateMixin {
|
||||
final TUISelfInfoViewModel selfInfoModel = serviceLocator<TUISelfInfoViewModel>();
|
||||
|
||||
Widget getUserItem(String userID, TUITheme theme, Function(String userID, TapDownDetails tapDetails)? onTapAvatar) {
|
||||
V2TimGroupMemberFullInfo? memberInfo;
|
||||
String showName = userID;
|
||||
try {
|
||||
memberInfo = widget.memberList?.firstWhere((element) => element?.userID == userID);
|
||||
if (memberInfo != null) {
|
||||
if (memberInfo.friendRemark != null && memberInfo.friendRemark!.isNotEmpty) {
|
||||
showName = memberInfo.friendRemark!;
|
||||
} else if (memberInfo.nameCard != null && memberInfo.nameCard!.isNotEmpty) {
|
||||
showName = memberInfo.nameCard!;
|
||||
} else if (memberInfo.nickName != null && memberInfo.nickName!.isNotEmpty) {
|
||||
showName = memberInfo.nickName!;
|
||||
} else {
|
||||
showName = memberInfo.userID;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
// e
|
||||
}
|
||||
|
||||
return InkWell(
|
||||
onTapDown: (tapDetails) {
|
||||
if (onTapAvatar != null) {
|
||||
if (userID != selfInfoModel.loginInfo?.userID) {
|
||||
onTapAvatar(userID, tapDetails);
|
||||
}
|
||||
}
|
||||
},
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
border: Border(bottom: BorderSide(color: theme.weakDividerColor ?? CommonColor.weakDividerColor))),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
margin: const EdgeInsets.only(right: 12),
|
||||
child: SizedBox(
|
||||
height: 40,
|
||||
width: 40,
|
||||
child: (memberInfo?.faceUrl != null)
|
||||
? Avatar(faceUrl: memberInfo!.faceUrl!, showName: userID)
|
||||
: Container(),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Container(
|
||||
alignment: Alignment.centerLeft,
|
||||
padding: const EdgeInsets.only(top: 20, bottom: 20, right: 28),
|
||||
child: Text(
|
||||
showName,
|
||||
style: TextStyle(color: theme.black, fontSize: 18),
|
||||
),
|
||||
)),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget stickerItem(int sticker, int length) {
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(top: 12, bottom: 12),
|
||||
padding: const EdgeInsets.only(left: 10, right: 10, top: 2, bottom: 1),
|
||||
decoration: const BoxDecoration(
|
||||
color: Color(0x198a8a8a),
|
||||
borderRadius: BorderRadius.all(Radius.circular(20)),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.only(bottom: 3),
|
||||
child: Text(
|
||||
String.fromCharCode(sticker),
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
),
|
||||
),
|
||||
),
|
||||
Container(
|
||||
margin: const EdgeInsets.only(left: 6),
|
||||
child: Text(length.toString()),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget getStickerNameList(
|
||||
int sticker, TUITheme theme, Function(String userID, TapDownDetails tapDetails)? onTapAvatar) {
|
||||
final nameList = widget.messageReaction[sticker.toString()];
|
||||
return SingleChildScrollView(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [...nameList.map((e) => getUserItem(e, theme, onTapAvatar))],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget tuiBuild(BuildContext context, TUIKitBuildValue value) {
|
||||
final theme = value.theme;
|
||||
return DefaultTabController(
|
||||
initialIndex: widget.currentStickerIndex,
|
||||
length: widget.stickerList.length,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Container(
|
||||
margin: const EdgeInsets.only(bottom: 4),
|
||||
child: TabBar(
|
||||
isScrollable: true,
|
||||
labelColor: theme.primaryColor,
|
||||
labelStyle: const TextStyle(fontWeight: FontWeight.bold),
|
||||
unselectedLabelColor: hexToColor("62626b"),
|
||||
unselectedLabelStyle: const TextStyle(fontWeight: FontWeight.normal),
|
||||
indicatorSize: TabBarIndicatorSize.label,
|
||||
indicatorColor: theme.primaryColor ?? hexToColor("62626b"),
|
||||
tabs: [
|
||||
...widget.stickerList.map((element) {
|
||||
return stickerItem(element, widget.messageReaction[element.toString()].length);
|
||||
})
|
||||
],
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: TabBarView(
|
||||
children: widget.stickerList
|
||||
.map((int sticker) => getStickerNameList(sticker, theme, widget.onTapAvatar))
|
||||
.toList()))
|
||||
],
|
||||
),
|
||||
));
|
||||
}
|
||||
}
|
||||
+93
@@ -0,0 +1,93 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_base.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_state.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/utils/screen_utils.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/views/TIMUIKitChat/TIMUIKitMessageItem/TIMUIKitMessageReaction/message_reaction_emoji.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/views/TIMUIKitChat/TIMUIKitTextField/tim_uikit_emoji_panel.dart'
|
||||
as emoji;
|
||||
import 'package:tencent_cloud_chat_uikit/ui/widgets/emoji.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/widgets/extended_wrap/extended_wrap.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/theme/color.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/theme/tui_theme.dart';
|
||||
|
||||
enum SelectEmojiPanelPosition { up, down }
|
||||
|
||||
class TIMUIKitMessageReactionEmojiSelectPanel extends StatefulWidget {
|
||||
final ValueChanged<int> onSelect;
|
||||
final bool isShowMoreSticker;
|
||||
final ValueChanged<bool> onClickShowMore;
|
||||
|
||||
const TIMUIKitMessageReactionEmojiSelectPanel(
|
||||
{Key? key,
|
||||
required this.onSelect,
|
||||
required this.isShowMoreSticker,
|
||||
required this.onClickShowMore})
|
||||
: super(key: key);
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() =>
|
||||
TIMUIKitMessageReactionEmojiSelectPanelState();
|
||||
}
|
||||
|
||||
class TIMUIKitMessageReactionEmojiSelectPanelState
|
||||
extends TIMUIKitState<TIMUIKitMessageReactionEmojiSelectPanel> {
|
||||
bool isShowMore = false;
|
||||
|
||||
_buildSimplePanel(TUITheme theme) {
|
||||
final List<Map<String, Object>> emojiData = messageReactionEmojiData;
|
||||
final isDesktopScreen = TUIKitScreenUtils.getFormFactor() == DeviceType.Desktop;
|
||||
return Material(
|
||||
color: Colors.white,
|
||||
child: ExtendedWrap(
|
||||
maxLines: widget.isShowMoreSticker ? 5 : 1,
|
||||
spacing: 18,
|
||||
crossAxisAlignment: WrapCrossAlignment.center,
|
||||
runSpacing: 24,
|
||||
children: [
|
||||
if(!isDesktopScreen)
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
widget.onClickShowMore(!widget.isShowMoreSticker);
|
||||
},
|
||||
child: SizedBox(
|
||||
height: 34,
|
||||
child: Icon(
|
||||
widget.isShowMoreSticker
|
||||
? Icons.cancel_outlined
|
||||
: Icons.add_circle_outline_outlined,
|
||||
color: hexToColor("444444"),
|
||||
size: 26),
|
||||
),
|
||||
),
|
||||
...emojiData.map(
|
||||
(e) {
|
||||
var item = Emoji.fromJson(e);
|
||||
return SizedBox(
|
||||
// width: 50,
|
||||
child: InkWell(
|
||||
splashColor: Colors.white,
|
||||
onTap: () {
|
||||
widget.onSelect(item.unicode);
|
||||
},
|
||||
child: emoji.EmojiItem(
|
||||
name: item.name,
|
||||
unicode: item.unicode,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
).toList()
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget tuiBuild(BuildContext context, TUIKitBuildValue value) {
|
||||
final TUITheme theme = value.theme;
|
||||
|
||||
return Container(
|
||||
child: _buildSimplePanel(theme),
|
||||
);
|
||||
}
|
||||
}
|
||||
+198
@@ -0,0 +1,198 @@
|
||||
// ignore_for_file: unused_field
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:tencent_chat_i18n_tool/tencent_chat_i18n_tool.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/models/v2_tim_group_member_full_info.dart'
|
||||
if (dart.library.html) 'package:tencent_cloud_chat_sdk/web/compatible_models/v2_tim_group_member_full_info.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/models/v2_tim_message.dart'
|
||||
if (dart.library.html) 'package:tencent_cloud_chat_sdk/web/compatible_models/v2_tim_message.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/models/v2_tim_message_change_info.dart'
|
||||
if (dart.library.html) 'package:tencent_cloud_chat_sdk/web/compatible_models/v2_tim_message_change_info.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/models/v2_tim_value_callback.dart'
|
||||
if (dart.library.html) 'package:tencent_cloud_chat_sdk/web/compatible_models/v2_tim_value_callback.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/tencent_cloud_chat_uikit.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_base.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_statelesswidget.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/business_logic/separate_models/tui_chat_separate_view_model.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/business_logic/view_models/tui_self_info_view_model.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/data_services/message/message_services.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/data_services/services_locatar.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/utils/platform.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/views/TIMUIKitChat/TIMUIKitMessageItem/TIMUIKitMessageReaction/tim_uikit_message_reaction_utils.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/widgets/extended_wrap/extended_wrap.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/theme/color.dart';
|
||||
|
||||
class TIMUIKitMessageReactionShowItem extends TIMUIKitStatelessWidget {
|
||||
/// the unicode of the emoji
|
||||
final int sticker;
|
||||
|
||||
/// the list contains the name who choose the current emoji
|
||||
final List nameList;
|
||||
|
||||
/// current message
|
||||
final V2TimMessage message;
|
||||
|
||||
/// show the details of message reaction
|
||||
final Function(int sticker) onShowDetail;
|
||||
|
||||
/// the member in current chat
|
||||
final List<V2TimGroupMemberFullInfo?> memberList;
|
||||
|
||||
TIMUIKitMessageReactionShowItem(
|
||||
{required this.message,
|
||||
required this.sticker,
|
||||
required this.memberList,
|
||||
required this.onShowDetail,
|
||||
required this.nameList,
|
||||
Key? key})
|
||||
: super(key: key);
|
||||
|
||||
final TUISelfInfoViewModel selfInfoModel = serviceLocator<TUISelfInfoViewModel>();
|
||||
final MessageService _messageService = serviceLocator<MessageService>();
|
||||
|
||||
clickOnCurrentSticker() async {
|
||||
for (int i = 0; i < 5; i++) {
|
||||
final res = await modifySticker();
|
||||
if (res.code == 0) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<V2TimValueCallback<V2TimMessageChangeInfo>> modifySticker() async {
|
||||
return await Future.delayed(const Duration(milliseconds: 50), () async {
|
||||
return await MessageReactionUtils.clickOnSticker(message, sticker);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget tuiBuild(BuildContext context, TUIKitBuildValue value) {
|
||||
final theme = value.theme;
|
||||
final option1 = nameList.length;
|
||||
final TUIChatSeparateViewModel model = Provider.of<TUIChatSeparateViewModel>(context);
|
||||
|
||||
final List<String> userIDs = [];
|
||||
for (final user in nameList) {
|
||||
final V2TimGroupMemberFullInfo? memberInfo =
|
||||
memberList.firstWhereOrNull((element) => element?.userID == user && TencentUtils.checkString(user) != null);
|
||||
if ((memberInfo == null || TencentUtils.checkString(memberInfo.userID) == null) &&
|
||||
TencentUtils.checkString(user.toString()) != null) {
|
||||
userIDs.add(user.toString());
|
||||
}
|
||||
}
|
||||
if (userIDs.isNotEmpty) {
|
||||
model.getUserShowName(userIDs);
|
||||
}
|
||||
|
||||
return LayoutBuilder(builder: (context, constraints) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.only(
|
||||
left: 10,
|
||||
right: 10,
|
||||
),
|
||||
decoration: const BoxDecoration(
|
||||
color: Color(0x19727271),
|
||||
borderRadius: BorderRadius.all(Radius.circular(20)),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
InkWell(
|
||||
onTap: clickOnCurrentSticker,
|
||||
child: Container(
|
||||
margin:
|
||||
EdgeInsets.only(bottom: (!PlatformUtils().isIOS) ? 4 : 2, top: (!PlatformUtils().isIOS) ? 4 : 0),
|
||||
child: Text(
|
||||
String.fromCharCode(sticker),
|
||||
style: TextStyle(fontSize: (!PlatformUtils().isIOS) ? 12 : 16, color: hexToColor("f9453d")),
|
||||
),
|
||||
),
|
||||
),
|
||||
Container(
|
||||
margin: const EdgeInsets.only(left: 6, right: 6),
|
||||
child: SizedBox(
|
||||
width: 1,
|
||||
height: 14,
|
||||
child: DecoratedBox(
|
||||
decoration: BoxDecoration(color: theme.weakTextColor),
|
||||
),
|
||||
),
|
||||
),
|
||||
Container(
|
||||
constraints: BoxConstraints(
|
||||
maxWidth: constraints.maxWidth * 0.8,
|
||||
),
|
||||
child: ExtendedWrap(
|
||||
maxLines: 1,
|
||||
spacing: 8,
|
||||
crossAxisAlignment: WrapCrossAlignment.center,
|
||||
overflowWidget: GestureDetector(
|
||||
onTap: () {
|
||||
onShowDetail(sticker);
|
||||
},
|
||||
child: Text(
|
||||
TIM_t_para("...共{{option1}}人", "...共$option1人")(option1: option1),
|
||||
style: TextStyle(fontSize: 12, color: hexToColor("616669")),
|
||||
),
|
||||
),
|
||||
children: [
|
||||
...nameList.map((e) {
|
||||
String showName = e;
|
||||
if (memberList.isNotEmpty) {
|
||||
try {
|
||||
final V2TimGroupMemberFullInfo? memberInfo =
|
||||
memberList.firstWhere((element) => element?.userID == e);
|
||||
if (memberInfo != null) {
|
||||
if (memberInfo.friendRemark != null && memberInfo.friendRemark!.isNotEmpty) {
|
||||
showName = memberInfo.friendRemark!;
|
||||
} else if (memberInfo.nameCard != null && memberInfo.nameCard!.isNotEmpty) {
|
||||
showName = memberInfo.nameCard!;
|
||||
} else if (memberInfo.nickName != null && memberInfo.nickName!.isNotEmpty) {
|
||||
showName = memberInfo.nickName!;
|
||||
} else {
|
||||
showName = memberInfo.userID;
|
||||
}
|
||||
} else {
|
||||
final String? data = model.groupUserShowName[e];
|
||||
if (TencentUtils.checkString(data) != null) {
|
||||
showName = data ?? e;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
final String? data = model.groupUserShowName[e];
|
||||
if (TencentUtils.checkString(data) != null) {
|
||||
showName = data ?? e;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
final String? data = model.groupUserShowName[e];
|
||||
if (TencentUtils.checkString(data) != null) {
|
||||
showName = data ?? e;
|
||||
}
|
||||
}
|
||||
return InkWell(
|
||||
onTapDown: (tapDetails) {
|
||||
if (model.onTapAvatar != null) {
|
||||
if (e != selfInfoModel.loginInfo?.userID) {
|
||||
model.onTapAvatar!(e, tapDetails);
|
||||
}
|
||||
}
|
||||
},
|
||||
child: Text(
|
||||
showName,
|
||||
style: TextStyle(fontSize: 12, color: hexToColor("616669")),
|
||||
),
|
||||
);
|
||||
})
|
||||
],
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
+160
@@ -0,0 +1,160 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:tencent_chat_i18n_tool/tencent_chat_i18n_tool.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/models/v2_tim_group_member_full_info.dart'
|
||||
if (dart.library.html) 'package:tencent_cloud_chat_sdk/web/compatible_models/v2_tim_group_member_full_info.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/models/v2_tim_message.dart'
|
||||
if (dart.library.html) 'package:tencent_cloud_chat_sdk/web/compatible_models/v2_tim_message.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_base.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_statelesswidget.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/business_logic/separate_models/tui_chat_separate_view_model.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/business_logic/view_models/tui_chat_global_model.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/business_logic/view_models/tui_self_info_view_model.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/data_services/services_locatar.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/tencent_cloud_chat_uikit.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/utils/platform.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/views/TIMUIKitChat/TIMUIKitMessageItem/TIMUIKitMessageReaction/tim_uikit_message_reaction_detail.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/views/TIMUIKitChat/TIMUIKitMessageItem/TIMUIKitMessageReaction/tim_uikit_message_reaction_show_item.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/views/TIMUIKitChat/TIMUIKitMessageItem/TIMUIKitMessageReaction/tim_uikit_message_reaction_utils.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/views/TIMUIKitChat/tim_uikit_cloud_custom_data.dart';
|
||||
|
||||
class TIMUIKitMessageReactionShowPanel extends TIMUIKitStatelessWidget {
|
||||
/// current message
|
||||
final V2TimMessage message;
|
||||
|
||||
TIMUIKitMessageReactionShowPanel({required this.message, Key? key}) : super(key: key);
|
||||
|
||||
final TUISelfInfoViewModel selfInfoModel = serviceLocator<TUISelfInfoViewModel>();
|
||||
|
||||
void showMore(BuildContext context, List<V2TimGroupMemberFullInfo?>? memberList, Map<String, dynamic> messageReaction,
|
||||
int currentSticker, List<int> stickerList, TUIChatSeparateViewModel model) async {
|
||||
_showCustomModalBottomSheet(context, memberList, messageReaction, currentSticker, stickerList, model);
|
||||
}
|
||||
|
||||
Future<Future<int?>> _showCustomModalBottomSheet(
|
||||
context,
|
||||
List<V2TimGroupMemberFullInfo?>? memberList,
|
||||
Map<String, dynamic> messageReaction,
|
||||
int currentSticker,
|
||||
List<int> stickerList,
|
||||
TUIChatSeparateViewModel model) async {
|
||||
return showModalBottomSheet<int>(
|
||||
backgroundColor: Colors.transparent,
|
||||
isScrollControlled: true,
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return Container(
|
||||
clipBehavior: Clip.antiAlias,
|
||||
decoration: const BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.only(
|
||||
topLeft: Radius.circular(20.0),
|
||||
topRight: Radius.circular(20.0),
|
||||
),
|
||||
),
|
||||
constraints: BoxConstraints(
|
||||
maxHeight: MediaQuery.of(context).size.height * 0.66,
|
||||
minHeight: MediaQuery.of(context).size.height * 0.2,
|
||||
),
|
||||
child: Column(children: [
|
||||
SizedBox(
|
||||
height: 50,
|
||||
child: Stack(
|
||||
textDirection: TextDirection.rtl,
|
||||
children: [
|
||||
Center(
|
||||
child: Text(
|
||||
TIM_t("回应详情"),
|
||||
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16.0),
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.close),
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
}),
|
||||
],
|
||||
),
|
||||
),
|
||||
const Divider(height: 1.0),
|
||||
Expanded(
|
||||
child: TIMUIKitMessageReactionDetail(
|
||||
onTapAvatar: model.onTapAvatar,
|
||||
stickerList: stickerList,
|
||||
currentStickerIndex: stickerList.indexWhere((element) => element == currentSticker),
|
||||
memberList: memberList,
|
||||
messageReaction: messageReaction,
|
||||
)),
|
||||
]),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget tuiBuild(BuildContext context, TUIKitBuildValue value) {
|
||||
Map<String, dynamic> messageReaction = {};
|
||||
CloudCustomData messageCloudCustomData = MessageReactionUtils.getCloudCustomData(message);
|
||||
final TUIChatSeparateViewModel model = Provider.of<TUIChatSeparateViewModel>(context);
|
||||
if (messageCloudCustomData.messageReaction != null && messageCloudCustomData.messageReaction!.isNotEmpty) {
|
||||
messageReaction = messageCloudCustomData.messageReaction!;
|
||||
} else {
|
||||
return const SizedBox(width: 0, height: 0);
|
||||
}
|
||||
|
||||
final List<int> messageReactionStickerList = [];
|
||||
|
||||
messageReaction.forEach((key, value) {
|
||||
messageReactionStickerList.add(int.parse(key));
|
||||
});
|
||||
|
||||
final filteredMessageReactionStickerList = messageReactionStickerList.where((sticker) {
|
||||
if (messageReaction[sticker.toString()] == null ||
|
||||
messageReaction[sticker.toString()] is! List ||
|
||||
messageReaction[sticker.toString()].length == 0) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}).toList();
|
||||
|
||||
final ConvType convType = model.conversationType ?? ConvType.c2c;
|
||||
List<V2TimGroupMemberFullInfo?> memberList = [];
|
||||
if (convType == ConvType.group) {
|
||||
memberList = model.groupMemberList ?? [];
|
||||
} else {
|
||||
final V2TimGroupMemberFullInfo selfInfo = V2TimGroupMemberFullInfo(
|
||||
userID: selfInfoModel.loginInfo?.userID ?? "",
|
||||
nickName: selfInfoModel.loginInfo?.nickName,
|
||||
faceUrl: selfInfoModel.loginInfo?.faceUrl,
|
||||
);
|
||||
|
||||
final V2TimGroupMemberFullInfo targetInfo = V2TimGroupMemberFullInfo(
|
||||
userID: model.conversationID,
|
||||
);
|
||||
memberList = [selfInfo, model.currentChatUserInfo ?? targetInfo];
|
||||
}
|
||||
|
||||
return filteredMessageReactionStickerList.isNotEmpty
|
||||
? Container(
|
||||
margin: const EdgeInsets.only(top: 12),
|
||||
child: Wrap(
|
||||
spacing: 8,
|
||||
runSpacing: (!PlatformUtils().isIOS) ? 12 : 8,
|
||||
children: [
|
||||
...filteredMessageReactionStickerList.map((sticker) {
|
||||
return TIMUIKitMessageReactionShowItem(
|
||||
memberList: memberList,
|
||||
message: message,
|
||||
nameList: messageReaction[sticker.toString()],
|
||||
sticker: sticker,
|
||||
onShowDetail: (int sticker) {
|
||||
showMore(
|
||||
context, memberList, messageReaction, sticker, filteredMessageReactionStickerList, model);
|
||||
});
|
||||
}).toList(),
|
||||
],
|
||||
),
|
||||
)
|
||||
: const SizedBox(width: 0, height: 0);
|
||||
}
|
||||
}
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:tencent_cloud_chat_sdk/models/v2_tim_message.dart'
|
||||
if (dart.library.html) 'package:tencent_cloud_chat_sdk/web/compatible_models/v2_tim_message.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/models/v2_tim_message_change_info.dart'
|
||||
if (dart.library.html) 'package:tencent_cloud_chat_sdk/web/compatible_models/v2_tim_message_change_info.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/models/v2_tim_value_callback.dart'
|
||||
if (dart.library.html) 'package:tencent_cloud_chat_sdk/web/compatible_models/v2_tim_value_callback.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/tencent_cloud_chat_uikit.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/business_logic/view_models/tui_self_info_view_model.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/data_services/message/message_services.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/data_services/services_locatar.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/utils/platform.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/views/TIMUIKitChat/tim_uikit_cloud_custom_data.dart';
|
||||
|
||||
class MessageReactionUtils {
|
||||
static final TUISelfInfoViewModel selfInfoModel = serviceLocator<TUISelfInfoViewModel>();
|
||||
static final MessageService _messageService = serviceLocator<MessageService>();
|
||||
|
||||
static CloudCustomData getCloudCustomData(V2TimMessage message) {
|
||||
CloudCustomData messageCloudCustomData;
|
||||
try {
|
||||
messageCloudCustomData = CloudCustomData.fromJson(
|
||||
json.decode(TencentUtils.checkString(message.cloudCustomData) != null ? message.cloudCustomData! : "{}"));
|
||||
} catch (e) {
|
||||
messageCloudCustomData = CloudCustomData();
|
||||
}
|
||||
|
||||
return messageCloudCustomData;
|
||||
}
|
||||
|
||||
static Map<String, dynamic> getMessageReaction(V2TimMessage message) {
|
||||
return getCloudCustomData(message).messageReaction ?? {};
|
||||
}
|
||||
|
||||
static Future<V2TimValueCallback<V2TimMessageChangeInfo>> clickOnSticker(V2TimMessage message, int sticker) async {
|
||||
final CloudCustomData messageCloudCustomData = getCloudCustomData(message);
|
||||
final Map<String, dynamic> messageReaction = messageCloudCustomData.messageReaction ?? {};
|
||||
List targetList = messageReaction["$sticker"] ?? [];
|
||||
if (targetList.contains(selfInfoModel.loginInfo!.userID!)) {
|
||||
targetList.remove(selfInfoModel.loginInfo!.userID!);
|
||||
} else {
|
||||
targetList = [selfInfoModel.loginInfo!.userID!, ...targetList];
|
||||
}
|
||||
messageReaction["$sticker"] = targetList;
|
||||
|
||||
if (PlatformUtils().isWeb) {
|
||||
final decodedMessage = jsonDecode(message.messageFromWeb!);
|
||||
decodedMessage["cloudCustomData"] = jsonEncode(messageCloudCustomData.toMap());
|
||||
message.messageFromWeb = jsonEncode(decodedMessage);
|
||||
} else {
|
||||
message.cloudCustomData = json.encode(messageCloudCustomData.toMap());
|
||||
}
|
||||
return await _messageService.modifyMessage(message: message);
|
||||
}
|
||||
}
|
||||
+175
@@ -0,0 +1,175 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/models/v2_tim_message.dart'
|
||||
if (dart.library.html) 'package:tencent_cloud_chat_sdk/web/compatible_models/v2_tim_message.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/utils/common_utils.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_base.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_state.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/business_logic/separate_models/tui_chat_separate_view_model.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/views/TIMUIKitChat/TIMUIKitMessageItem/TIMUIKitMessageReaction/tim_uikit_message_reaction_show_panel.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/views/TIMUIKitChat/tim_uikit_cloud_custom_data.dart';
|
||||
|
||||
class TIMUIKitMessageReactionWrapper extends StatefulWidget {
|
||||
final Widget child;
|
||||
final V2TimMessage message;
|
||||
final Color? backgroundColor;
|
||||
final bool isFromSelf;
|
||||
final BorderRadius? borderRadius;
|
||||
final bool isShowJump;
|
||||
final VoidCallback? clearJump;
|
||||
final bool isShowMessageReaction;
|
||||
final TUIChatSeparateViewModel chatModel;
|
||||
|
||||
const TIMUIKitMessageReactionWrapper(
|
||||
{Key? key,
|
||||
required this.isShowJump,
|
||||
this.clearJump,
|
||||
required this.isFromSelf,
|
||||
this.backgroundColor,
|
||||
required this.chatModel,
|
||||
required this.message,
|
||||
this.borderRadius,
|
||||
required this.child,
|
||||
this.isShowMessageReaction = true})
|
||||
: super(key: key);
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => _TIMUIKitMessageReactionWrapperState();
|
||||
}
|
||||
|
||||
class _TIMUIKitMessageReactionWrapperState extends TIMUIKitState<TIMUIKitMessageReactionWrapper> {
|
||||
bool isShowJumpState = false;
|
||||
bool isShining = false;
|
||||
bool isShowBorder = false;
|
||||
|
||||
_showJumpColor() {
|
||||
if ((widget.chatModel.jumpMsgID != widget.message.msgID) && (widget.message.msgID?.isNotEmpty ?? true)) {
|
||||
return;
|
||||
}
|
||||
isShining = true;
|
||||
int shineAmount = 6;
|
||||
setState(() {
|
||||
isShowJumpState = true;
|
||||
isShowBorder = true;
|
||||
});
|
||||
Timer.periodic(const Duration(milliseconds: 300), (timer) {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
isShowJumpState = shineAmount.isOdd ? true : false;
|
||||
isShowBorder = shineAmount.isOdd ? true : false;
|
||||
});
|
||||
}
|
||||
if (shineAmount == 0 || !mounted) {
|
||||
isShining = false;
|
||||
timer.cancel();
|
||||
}
|
||||
shineAmount--;
|
||||
});
|
||||
if (widget.clearJump != null) {
|
||||
widget.clearJump!();
|
||||
}
|
||||
}
|
||||
|
||||
bool isHaveMessageReaction() {
|
||||
Map<String, dynamic> messageReaction = {};
|
||||
CloudCustomData messageCloudCustomData;
|
||||
try {
|
||||
messageCloudCustomData = CloudCustomData.fromJson(json.decode(
|
||||
TencentUtils.checkString(widget.message.cloudCustomData) != null ? widget.message.cloudCustomData! : "{}"));
|
||||
} catch (e) {
|
||||
messageCloudCustomData = CloudCustomData();
|
||||
}
|
||||
|
||||
if (messageCloudCustomData.messageReaction != null && messageCloudCustomData.messageReaction!.isNotEmpty) {
|
||||
messageReaction = messageCloudCustomData.messageReaction!;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
|
||||
final List<int> messageReactionStickerList = [];
|
||||
|
||||
messageReaction.forEach((key, value) {
|
||||
messageReactionStickerList.add(int.parse(key));
|
||||
});
|
||||
|
||||
final filteredMessageReactionStickerList = messageReactionStickerList.where((sticker) {
|
||||
if (messageReaction[sticker.toString()] == null ||
|
||||
messageReaction[sticker.toString()] is! List ||
|
||||
messageReaction[sticker.toString()].length == 0) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
if (filteredMessageReactionStickerList.isEmpty) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget tuiBuild(BuildContext context, TUIKitBuildValue value) {
|
||||
final theme = value.theme;
|
||||
final borderRadius = widget.isFromSelf
|
||||
? const BorderRadius.only(
|
||||
topLeft: Radius.circular(10),
|
||||
topRight: Radius.circular(2),
|
||||
bottomLeft: Radius.circular(10),
|
||||
bottomRight: Radius.circular(10))
|
||||
: const BorderRadius.only(
|
||||
topLeft: Radius.circular(2),
|
||||
topRight: Radius.circular(10),
|
||||
bottomLeft: Radius.circular(10),
|
||||
bottomRight: Radius.circular(10));
|
||||
|
||||
if (widget.isShowJump) {
|
||||
if (!isShining) {
|
||||
Future.delayed(Duration.zero, () {
|
||||
_showJumpColor();
|
||||
});
|
||||
} else {
|
||||
if ((widget.chatModel.jumpMsgID == widget.message.msgID) && (widget.message.msgID?.isNotEmpty ?? false)) {
|
||||
if (widget.clearJump != null) {
|
||||
widget.clearJump!();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final defaultStyle = widget.isFromSelf ? theme.lightPrimaryMaterialColor.shade50 : theme.weakBackgroundColor;
|
||||
final backgroundColor =
|
||||
isShowJumpState ? const Color.fromRGBO(245, 166, 35, 1) : (widget.backgroundColor ?? defaultStyle);
|
||||
|
||||
if (!widget.isShowMessageReaction || !isHaveMessageReaction()) {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(5)),
|
||||
border: Border.all(color: Color.fromRGBO(245, 166, 35, (isShowBorder ? 1 : 0)), width: 2)),
|
||||
child: widget.child,
|
||||
);
|
||||
}
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(10),
|
||||
decoration: BoxDecoration(
|
||||
color: backgroundColor,
|
||||
borderRadius: widget.borderRadius ?? borderRadius,
|
||||
),
|
||||
constraints: BoxConstraints(maxWidth: MediaQuery.of(context).size.width * 0.7),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(5)),
|
||||
border: Border.all(color: Color.fromRGBO(245, 166, 35, (isShowBorder ? 1 : 0)), width: 2)),
|
||||
child: widget.child,
|
||||
),
|
||||
if (widget.isShowMessageReaction) TIMUIKitMessageReactionShowPanel(message: widget.message)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
export 'package:tencent_cloud_chat_uikit/ui/views/TIMUIKitChat/TIMUIKitMessageItem/tim_uikit_chat_file_elem.dart';
|
||||
export 'package:tencent_cloud_chat_uikit/ui/views/TIMUIKitChat/TIMUIKitMessageItem/tim_uikit_chat_group_tips_elem.dart';
|
||||
export 'package:tencent_cloud_chat_uikit/ui/views/TIMUIKitChat/TIMUIKitMessageItem/tim_uikit_chat_image_elem.dart';
|
||||
export 'package:tencent_cloud_chat_uikit/ui/views/TIMUIKitChat/TIMUIKitMessageItem/tim_uikit_chat_sound_elem.dart';
|
||||
export 'package:tencent_cloud_chat_uikit/ui/views/TIMUIKitChat/TIMUIKitMessageItem/tim_uikit_chat_text_elem.dart';
|
||||
export 'package:tencent_cloud_chat_uikit/ui/views/TIMUIKitChat/TIMUIKitMessageItem/tim_uikit_chat_video_elem.dart';
|
||||
export 'package:tencent_cloud_chat_uikit/ui/views/TIMUIKitChat/TIMUIKitMessageItem/tim_uikit_merger_message_elem.dart';
|
||||
export 'package:tencent_cloud_chat_uikit/ui/views/TIMUIKitChat/TIMUIKitMessageItem/tim_uikit_chat_reply_elem.dart';
|
||||
@@ -0,0 +1,60 @@
|
||||
// ignore_for_file: unrelated_type_equality_checks
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:tencent_chat_i18n_tool/tencent_chat_i18n_tool.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/models/v2_tim_custom_elem.dart'
|
||||
if (dart.library.html) 'package:tencent_cloud_chat_sdk/web/compatible_models/v2_tim_custom_elem.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/models/v2_tim_message.dart'
|
||||
if (dart.library.html) 'package:tencent_cloud_chat_sdk/web/compatible_models/v2_tim_message.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_base.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_statelesswidget.dart';
|
||||
|
||||
class TIMUIKitCustomElem extends TIMUIKitStatelessWidget {
|
||||
final V2TimCustomElem? customElem;
|
||||
final bool isFromSelf;
|
||||
final TextStyle? messageFontStyle;
|
||||
final BorderRadius? messageBorderRadius;
|
||||
final Color? messageBackgroundColor;
|
||||
final EdgeInsetsGeometry? textPadding;
|
||||
final V2TimMessage message;
|
||||
final bool? isShowMessageReaction;
|
||||
|
||||
TIMUIKitCustomElem({
|
||||
Key? key,
|
||||
required this.message,
|
||||
this.isShowMessageReaction,
|
||||
this.customElem,
|
||||
this.isFromSelf = false,
|
||||
this.messageFontStyle,
|
||||
this.messageBorderRadius,
|
||||
this.messageBackgroundColor,
|
||||
this.textPadding,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget tuiBuild(BuildContext context, TUIKitBuildValue value) {
|
||||
final theme = value.theme;
|
||||
final borderRadius = isFromSelf
|
||||
? const BorderRadius.only(
|
||||
topLeft: Radius.circular(10),
|
||||
topRight: Radius.circular(2),
|
||||
bottomLeft: Radius.circular(10),
|
||||
bottomRight: Radius.circular(10))
|
||||
: const BorderRadius.only(
|
||||
topLeft: Radius.circular(2),
|
||||
topRight: Radius.circular(10),
|
||||
bottomLeft: Radius.circular(10),
|
||||
bottomRight: Radius.circular(10));
|
||||
final backgroundColor = isFromSelf ? theme.lightPrimaryMaterialColor.shade50 : theme.weakBackgroundColor;
|
||||
return Container(
|
||||
padding: textPadding ?? const EdgeInsets.all(10),
|
||||
decoration: BoxDecoration(
|
||||
color: messageBackgroundColor ?? backgroundColor,
|
||||
borderRadius: messageBorderRadius ?? borderRadius,
|
||||
),
|
||||
constraints: const BoxConstraints(maxWidth: 240),
|
||||
child: Column(
|
||||
children: [Text(TIM_t("自定义消息"))],
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/models/v2_tim_message.dart'
|
||||
if (dart.library.html) 'package:tencent_cloud_chat_sdk/web/compatible_models/v2_tim_message.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/utils/screen_utils.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_base.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_state.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/business_logic/separate_models/tui_chat_separate_view_model.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/views/TIMUIKitChat/TIMUIKitMessageItem/TIMUIKitMessageReaction/tim_uikit_message_reaction_wrapper.dart';
|
||||
|
||||
class TIMUIKitFaceElem extends StatefulWidget {
|
||||
final String path;
|
||||
final bool isShowJump;
|
||||
final VoidCallback? clearJump;
|
||||
final V2TimMessage message;
|
||||
final bool? isShowMessageReaction;
|
||||
final TUIChatSeparateViewModel model;
|
||||
|
||||
const TIMUIKitFaceElem(
|
||||
{Key? key,
|
||||
required this.path,
|
||||
required this.isShowJump,
|
||||
this.clearJump,
|
||||
required this.message,
|
||||
this.isShowMessageReaction,
|
||||
required this.model})
|
||||
: super(key: key);
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => _TIMUIKitTextElemState();
|
||||
}
|
||||
|
||||
class _TIMUIKitTextElemState extends TIMUIKitState<TIMUIKitFaceElem> {
|
||||
bool isFromNetwork() {
|
||||
return widget.path.startsWith('http');
|
||||
}
|
||||
|
||||
createPathFromNative(String path) {
|
||||
String prefix = "";
|
||||
String suffix = "";
|
||||
if (widget.model.chatConfig.faceURIPrefix != null) {
|
||||
prefix = widget.model.chatConfig.faceURIPrefix!(path);
|
||||
}
|
||||
if (widget.model.chatConfig.faceURISuffix != null) {
|
||||
suffix = widget.model.chatConfig.faceURISuffix!(path);
|
||||
}
|
||||
return "$prefix$path$suffix";
|
||||
}
|
||||
|
||||
@override
|
||||
Widget tuiBuild(BuildContext context, TUIKitBuildValue value) {
|
||||
final isDesktopScreen = TUIKitScreenUtils.getFormFactor(context) == DeviceType.Desktop;
|
||||
return TIMUIKitMessageReactionWrapper(
|
||||
chatModel: widget.model,
|
||||
isShowJump: widget.isShowJump,
|
||||
isFromSelf: widget.message.isSelf ?? true,
|
||||
clearJump: widget.clearJump,
|
||||
message: widget.message,
|
||||
isShowMessageReaction: widget.isShowMessageReaction ?? true,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(10),
|
||||
constraints: BoxConstraints(maxWidth: MediaQuery.of(context).size.width * (isDesktopScreen ? 0.1 : 0.3)),
|
||||
child: isFromNetwork() ? Image.network(widget.path) : Image.asset(createPathFromNative(widget.path)),
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,472 @@
|
||||
// ignore_for_file: avoid_print
|
||||
|
||||
import 'dart:io';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:device_info_plus/device_info_plus.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:loading_animation_widget/loading_animation_widget.dart';
|
||||
import 'package:open_file/open_file.dart';
|
||||
import 'package:tencent_chat_i18n_tool/tencent_chat_i18n_tool.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/enum/V2TimAdvancedMsgListener.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/models/v2_tim_file_elem.dart'
|
||||
if (dart.library.html) 'package:tencent_cloud_chat_sdk/web/compatible_models/v2_tim_file_elem.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/models/v2_tim_message.dart'
|
||||
if (dart.library.html) 'package:tencent_cloud_chat_sdk/web/compatible_models/v2_tim_message.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/models/v2_tim_message_download_progress.dart'
|
||||
if (dart.library.html) 'package:tencent_cloud_chat_sdk/web/compatible_models/v2_tim_message_download_progress.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/tencent_im_sdk_plugin.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/base_widgets/tim_callback.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_base.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_state.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/business_logic/separate_models/tui_chat_separate_view_model.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/business_logic/view_models/tui_chat_global_model.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/data_services/services_locatar.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/tencent_cloud_chat_uikit.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/theme/color.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/theme/tui_theme.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/utils/permission.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/utils/platform.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/views/TIMUIKitChat/TIMUIKitMessageItem/TIMUIKitMessageReaction/tim_uikit_message_reaction_wrapper.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/views/TIMUIKitChat/TIMUIKitMessageItem/tim_uikit_chat_file_icon.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/widgets/textSize.dart';
|
||||
import 'package:universal_html/html.dart' as html;
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
class TIMUIKitFileElem extends StatefulWidget {
|
||||
final String? messageID;
|
||||
final V2TimFileElem? fileElem;
|
||||
final bool isSelf;
|
||||
final bool isShowJump;
|
||||
final VoidCallback? clearJump;
|
||||
final V2TimMessage message;
|
||||
final bool? isShowMessageReaction;
|
||||
final TUIChatSeparateViewModel chatModel;
|
||||
|
||||
const TIMUIKitFileElem(
|
||||
{Key? key,
|
||||
required this.chatModel,
|
||||
required this.messageID,
|
||||
required this.fileElem,
|
||||
required this.isSelf,
|
||||
required this.isShowJump,
|
||||
this.clearJump,
|
||||
required this.message,
|
||||
this.isShowMessageReaction})
|
||||
: super(key: key);
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => _TIMUIKitFileElemState();
|
||||
}
|
||||
|
||||
class _TIMUIKitFileElemState extends TIMUIKitState<TIMUIKitFileElem> {
|
||||
String filePath = "";
|
||||
bool isWebDownloading = false;
|
||||
final TUIChatGlobalModel model = serviceLocator<TUIChatGlobalModel>();
|
||||
int downloadProgress = 0;
|
||||
V2TimAdvancedMsgListener? advancedMsgListener;
|
||||
final GlobalKey containerKey = GlobalKey();
|
||||
double? containerHeight;
|
||||
bool? _downloadFailed = false;
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
if (advancedMsgListener != null) {
|
||||
TencentImSDKPlugin.v2TIMManager.getMessageManager().removeAdvancedMsgListener(listener: advancedMsgListener);
|
||||
advancedMsgListener = null;
|
||||
}
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
if (!PlatformUtils().isWeb) {
|
||||
Future.delayed(const Duration(microseconds: 10), () {
|
||||
hasFile();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> addAdvancedMsgListenerForDownload() async {
|
||||
if (advancedMsgListener != null) {
|
||||
return false;
|
||||
}
|
||||
advancedMsgListener = V2TimAdvancedMsgListener(
|
||||
onMessageDownloadProgressCallback: (V2TimMessageDownloadProgress messageProgress) async {
|
||||
if (messageProgress.msgID == widget.message.msgID) {
|
||||
if (messageProgress.isError || messageProgress.errorCode != 0) {
|
||||
setState(() {
|
||||
_downloadFailed = true;
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (messageProgress.isFinish) {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
downloadProgress = 100;
|
||||
});
|
||||
|
||||
if (advancedMsgListener != null) {
|
||||
TencentImSDKPlugin.v2TIMManager
|
||||
.getMessageManager()
|
||||
.removeAdvancedMsgListener(listener: advancedMsgListener);
|
||||
advancedMsgListener = null;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
final currentProgress = (messageProgress.currentSize / messageProgress.totalSize * 100).floor();
|
||||
if (mounted && currentProgress > downloadProgress) {
|
||||
setState(() {
|
||||
downloadProgress = currentProgress;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
await TencentImSDKPlugin.v2TIMManager.getMessageManager().addAdvancedMsgListener(listener: advancedMsgListener!);
|
||||
return true;
|
||||
}
|
||||
|
||||
Future<String> getSavePath() async {
|
||||
String savePathWithAppPath = '/storage/emulated/0/Android/data/com.tencent.flutter.tuikit/cache/' +
|
||||
(widget.message.msgID ?? "") +
|
||||
widget.fileElem!.fileName!;
|
||||
return savePathWithAppPath;
|
||||
}
|
||||
|
||||
Future<bool> hasFile() async {
|
||||
if (PlatformUtils().isWeb) {
|
||||
return true;
|
||||
}
|
||||
String savePath = TencentUtils.checkString(model.getFileMessageLocation(widget.messageID)) ??
|
||||
TencentUtils.checkString(widget.message.fileElem!.localUrl) ??
|
||||
widget.message.fileElem?.path ??
|
||||
'';
|
||||
|
||||
File f = File(savePath);
|
||||
if (widget.messageID != null) {
|
||||
if (f.existsSync()) {
|
||||
filePath = savePath;
|
||||
if (downloadProgress != 100) {
|
||||
setState(() {
|
||||
downloadProgress = 100;
|
||||
});
|
||||
}
|
||||
if (model.getMessageProgress(widget.messageID) != 100) {
|
||||
model.setMessageProgress(widget.messageID!, 100);
|
||||
}
|
||||
if (advancedMsgListener != null) {
|
||||
TencentImSDKPlugin.v2TIMManager.getMessageManager().removeAdvancedMsgListener(listener: advancedMsgListener);
|
||||
advancedMsgListener = null;
|
||||
}
|
||||
return true;
|
||||
} else {
|
||||
model.setMessageProgress(widget.messageID!, 0);
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
String showFileSize(int fileSize) {
|
||||
if (fileSize < 1024) {
|
||||
return fileSize.toString() + "B";
|
||||
} else if (fileSize < 1024 * 1024) {
|
||||
return (fileSize / 1024).toStringAsFixed(2) + "KB";
|
||||
} else if (fileSize < 1024 * 1024 * 1024) {
|
||||
return (fileSize / 1024 / 1024).toStringAsFixed(2) + "MB";
|
||||
} else {
|
||||
return (fileSize / 1024 / 1024 / 1024).toStringAsFixed(2) + "GB";
|
||||
}
|
||||
}
|
||||
|
||||
addUrlToWaitingPath(TUITheme theme) async {
|
||||
if (widget.messageID != null) {
|
||||
model.addWaitingList(widget.messageID!);
|
||||
}
|
||||
if (model.getWaitingListLength() == 1) {
|
||||
await downloadFile(theme);
|
||||
}
|
||||
}
|
||||
|
||||
checkIsWaiting() {
|
||||
bool res = false;
|
||||
try {
|
||||
if (widget.messageID!.isNotEmpty) {
|
||||
res = model.isWaiting(widget.messageID!);
|
||||
}
|
||||
} catch (err) {
|
||||
// err
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
downloadFile(TUITheme theme) async {
|
||||
if (PlatformUtils().isMobile) {
|
||||
if (PlatformUtils().isIOS) {
|
||||
if (!await Permissions.checkPermission(context, Permission.photosAddOnly.value, theme, false)) {
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
final DeviceInfoPlugin deviceInfo = DeviceInfoPlugin();
|
||||
AndroidDeviceInfo androidInfo = await deviceInfo.androidInfo;
|
||||
if ((androidInfo.version.sdkInt) >= 33) {
|
||||
} else {
|
||||
var storage = await Permissions.checkPermission(
|
||||
context,
|
||||
Permission.storage.value,
|
||||
);
|
||||
if (!storage) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
await model.downloadFile();
|
||||
}
|
||||
|
||||
Future<bool> hasZeroSize(String filePath) async {
|
||||
try {
|
||||
final file = File(filePath);
|
||||
final fileSize = await file.length();
|
||||
return fileSize == 0;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
tryOpenFile(context, theme) async {
|
||||
if (!PlatformUtils().isWeb && (await hasZeroSize(filePath) || widget.message.status == 3)) {
|
||||
onTIMCallback(TIMCallback(type: TIMCallbackType.INFO, infoRecommendText: "不支持 0KB 文件的传输", infoCode: 6660417));
|
||||
return;
|
||||
}
|
||||
if (PlatformUtils().isMobile) {
|
||||
if (PlatformUtils().isIOS) {
|
||||
if (!await Permissions.checkPermission(context, Permission.photosAddOnly.value, theme!, false)) {
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
final DeviceInfoPlugin deviceInfo = DeviceInfoPlugin();
|
||||
AndroidDeviceInfo androidInfo = await deviceInfo.androidInfo;
|
||||
if ((androidInfo.version.sdkInt) >= 33) {
|
||||
} else {
|
||||
var storage = await Permissions.checkPermission(
|
||||
context,
|
||||
Permission.storage.value,
|
||||
);
|
||||
if (!storage) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
if (PlatformUtils().isDesktop && !PlatformUtils().isWindows) {
|
||||
launchUrl(Uri.file(filePath));
|
||||
} else {
|
||||
OpenFile.open(filePath);
|
||||
}
|
||||
// ignore: empty_catches
|
||||
} catch (e) {
|
||||
OpenFile.open(filePath);
|
||||
}
|
||||
}
|
||||
|
||||
void downloadWebFile(String fileUrl) async {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
isWebDownloading = true;
|
||||
});
|
||||
}
|
||||
String fileName = Uri.parse(fileUrl).pathSegments.last;
|
||||
try {
|
||||
http.Response response = await http.get(
|
||||
Uri.parse(fileUrl),
|
||||
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
|
||||
);
|
||||
|
||||
final html.AnchorElement downloadAnchor = html.document.createElement('a') as html.AnchorElement;
|
||||
|
||||
final html.Blob blob = html.Blob([response.bodyBytes]);
|
||||
|
||||
downloadAnchor.href = html.Url.createObjectUrlFromBlob(blob);
|
||||
downloadAnchor.download = widget.message.fileElem?.fileName ?? fileName;
|
||||
|
||||
downloadAnchor.click();
|
||||
} catch (e) {
|
||||
html.AnchorElement(
|
||||
href: widget.fileElem?.path ?? "",
|
||||
)
|
||||
..setAttribute("download", widget.message.fileElem?.fileName ?? fileName)
|
||||
..setAttribute("target", '_blank')
|
||||
..style.display = "none"
|
||||
..click();
|
||||
}
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
isWebDownloading = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget tuiBuild(BuildContext context, TUIKitBuildValue value) {
|
||||
final theme = value.theme;
|
||||
final received = downloadProgress;
|
||||
final fileName = widget.fileElem!.fileName ?? "";
|
||||
final fileSize = widget.fileElem!.fileSize;
|
||||
final borderRadius = widget.isSelf
|
||||
? const BorderRadius.only(
|
||||
topLeft: Radius.circular(10),
|
||||
topRight: Radius.circular(2),
|
||||
bottomLeft: Radius.circular(10),
|
||||
bottomRight: Radius.circular(10))
|
||||
: const BorderRadius.only(
|
||||
topLeft: Radius.circular(2),
|
||||
topRight: Radius.circular(10),
|
||||
bottomLeft: Radius.circular(10),
|
||||
bottomRight: Radius.circular(10));
|
||||
String? fileFormat;
|
||||
if (widget.fileElem?.fileName != null && widget.fileElem!.fileName!.isNotEmpty) {
|
||||
final String fileName = widget.fileElem!.fileName!;
|
||||
fileFormat = fileName.split(".")[max(fileName.split(".").length - 1, 0)];
|
||||
}
|
||||
final RenderBox? containerRenderBox = containerKey.currentContext?.findRenderObject() as RenderBox?;
|
||||
if (containerRenderBox != null) {
|
||||
containerHeight = containerRenderBox.size.height;
|
||||
}
|
||||
|
||||
return Row(
|
||||
key: containerKey,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (widget.isSelf && isWebDownloading)
|
||||
Container(
|
||||
margin: const EdgeInsets.only(top: 2),
|
||||
child: LoadingAnimationWidget.threeArchedCircle(
|
||||
color: theme.weakTextColor ?? Colors.grey,
|
||||
size: 20,
|
||||
),
|
||||
),
|
||||
TIMUIKitMessageReactionWrapper(
|
||||
chatModel: widget.chatModel,
|
||||
isShowJump: widget.isShowJump,
|
||||
clearJump: widget.clearJump,
|
||||
isFromSelf: widget.message.isSelf ?? true,
|
||||
isShowMessageReaction: widget.isShowMessageReaction ?? true,
|
||||
message: widget.message,
|
||||
child: GestureDetector(
|
||||
onTap: () async {
|
||||
try {
|
||||
if (PlatformUtils().isWeb) {
|
||||
if (!isWebDownloading) {
|
||||
downloadWebFile(widget.fileElem?.path ?? "");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
await addAdvancedMsgListenerForDownload();
|
||||
if (await hasFile()) {
|
||||
if (received == 100) {
|
||||
tryOpenFile(context, theme);
|
||||
} else {
|
||||
onTIMCallback(
|
||||
TIMCallback(
|
||||
type: TIMCallbackType.INFO,
|
||||
infoRecommendText: TIM_t("正在下载中"),
|
||||
infoCode: 6660411,
|
||||
),
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (checkIsWaiting()) {
|
||||
onTIMCallback(
|
||||
TIMCallback(
|
||||
type: TIMCallbackType.INFO, infoRecommendText: TIM_t("已加入待下载队列,其他文件下载中"), infoCode: 6660413),
|
||||
);
|
||||
return;
|
||||
} else {
|
||||
await addUrlToWaitingPath(theme);
|
||||
}
|
||||
} catch (e) {
|
||||
onTIMCallback(
|
||||
TIMCallback(type: TIMCallbackType.INFO, infoRecommendText: "文件处理异常", infoCode: 6660416));
|
||||
}
|
||||
},
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxHeight: 72),
|
||||
child: Container(
|
||||
width: 170,
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(
|
||||
color: theme.weakDividerColor ?? CommonColor.weakDividerColor,
|
||||
),
|
||||
borderRadius: borderRadius),
|
||||
child: Stack(children: [
|
||||
ClipRRect(
|
||||
borderRadius: borderRadius,
|
||||
child: LinearProgressIndicator(
|
||||
minHeight: ((containerHeight) ?? 72) - 6,
|
||||
value: (received == 100 ? 0 : received) / 100,
|
||||
backgroundColor: received == 100 ? theme.weakBackgroundColor : Colors.white,
|
||||
valueColor: AlwaysStoppedAnimation(theme.lightPrimaryMaterialColor.shade50),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 12),
|
||||
child: Row(
|
||||
mainAxisAlignment: widget.isSelf ? MainAxisAlignment.end : MainAxisAlignment.start,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Container(
|
||||
constraints: const BoxConstraints(maxWidth: 160),
|
||||
child: LayoutBuilder(
|
||||
builder: (buildContext, boxConstraints) {
|
||||
return CustomText(
|
||||
fileName,
|
||||
width: boxConstraints.maxWidth,
|
||||
maxLines: 1,
|
||||
style: TextStyle(
|
||||
color: theme.darkTextColor,
|
||||
fontSize: 16,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
if (fileSize != null)
|
||||
Text(
|
||||
showFileSize(fileSize),
|
||||
style: TextStyle(fontSize: 14, color: theme.weakTextColor),
|
||||
)
|
||||
],
|
||||
)),
|
||||
TIMUIKitFileIcon(
|
||||
fileFormat: fileFormat,
|
||||
),
|
||||
])),
|
||||
]),
|
||||
),
|
||||
),
|
||||
)),
|
||||
if (!widget.isSelf && isWebDownloading)
|
||||
Container(
|
||||
margin: const EdgeInsets.only(top: 2),
|
||||
child: LoadingAnimationWidget.threeArchedCircle(
|
||||
color: theme.weakTextColor ?? Colors.grey,
|
||||
size: 20,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
// ignore_for_file: must_be_immutable
|
||||
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_base.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_statelesswidget.dart';
|
||||
|
||||
class TIMUIKitFileIcon extends TIMUIKitStatelessWidget {
|
||||
final String? fileFormat;
|
||||
final double? size;
|
||||
|
||||
TIMUIKitFileIcon( {this.size, this.fileFormat, Key? key}) : super(key: key);
|
||||
|
||||
Map fileMap = {
|
||||
"doc": "images/word.png",
|
||||
"docx": "images/word.png",
|
||||
"ppt": "images/ppt.png",
|
||||
"pptx": "images/ppt.png",
|
||||
"xls": "images/excel.png",
|
||||
"xlsx": "images/excel.png",
|
||||
"pdf": "images/pdf.png",
|
||||
"zip": "images/zip.png",
|
||||
"rar": "images/zip.png",
|
||||
"7z": "images/zip.png",
|
||||
"tar": "images/zip.png",
|
||||
"gz": "images/zip.png",
|
||||
"xz": "images/zip.png",
|
||||
"bz2": "images/zip.png",
|
||||
"txt": "images/txt.png",
|
||||
"jpg": "images/image_icon.png",
|
||||
"bmp": "images/image_icon.png",
|
||||
"gif": "images/image_icon.png",
|
||||
"png": "images/image_icon.png",
|
||||
"jpeg": "images/image_icon.png",
|
||||
"tif": "images/image_icon.png",
|
||||
"wmf": "images/image_icon.png",
|
||||
"dib": "images/image_icon.png",
|
||||
"mp4": "images/video_icon.png",
|
||||
"avi": "images/video_icon.png",
|
||||
"mov": "images/video_icon.png",
|
||||
"wmv": "images/video_icon.png",
|
||||
"flv": "images/video_icon.png",
|
||||
};
|
||||
|
||||
Widget _getFileIcon() {
|
||||
return Image.asset(
|
||||
fileMap[fileFormat?.toLowerCase()] ?? "images/unknown.png",
|
||||
package: 'tencent_cloud_chat_uikit',
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget tuiBuild(BuildContext context, TUIKitBuildValue value) {
|
||||
return SizedBox(
|
||||
height: size ?? 50,
|
||||
width: size ?? 50,
|
||||
child: Container(padding: const EdgeInsets.all(4), child: _getFileIcon()),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/models/v2_tim_group_member_full_info.dart'
|
||||
if (dart.library.html) 'package:tencent_cloud_chat_sdk/web/compatible_models/v2_tim_group_member_full_info.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/models/v2_tim_group_tips_elem.dart'
|
||||
if (dart.library.html) 'package:tencent_cloud_chat_sdk/web/compatible_models/v2_tim_group_tips_elem.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_state.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/utils/message.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_base.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/theme/color.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/theme/tui_theme.dart';
|
||||
|
||||
class TIMUIKitGroupTipsElem extends StatefulWidget {
|
||||
final V2TimGroupTipsElem groupTipsElem;
|
||||
final List<V2TimGroupMemberFullInfo?> groupMemberList;
|
||||
|
||||
const TIMUIKitGroupTipsElem({Key? key, required this.groupMemberList, required this.groupTipsElem}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<TIMUIKitGroupTipsElem> createState() => _TIMUIKitGroupTipsElemState();
|
||||
}
|
||||
|
||||
class _TIMUIKitGroupTipsElemState extends TIMUIKitState<TIMUIKitGroupTipsElem> {
|
||||
String groupTipsAbstractText = "";
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
getText();
|
||||
}
|
||||
|
||||
void getText() async {
|
||||
final newText = await MessageUtils.groupTipsMessageAbstract(widget.groupTipsElem, widget.groupMemberList);
|
||||
setState(() {
|
||||
groupTipsAbstractText = newText;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget tuiBuild(BuildContext context, TUIKitBuildValue value) {
|
||||
final TUITheme theme = value.theme;
|
||||
|
||||
return MessageUtils.wrapMessageTips(
|
||||
Text(
|
||||
groupTipsAbstractText,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(fontSize: 12, fontWeight: FontWeight.w400, color: hexToColor("888888")),
|
||||
),
|
||||
theme);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,638 @@
|
||||
// ignore_for_file: prefer_typing_uninitialized_variables, unused_import
|
||||
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'dart:math';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:crypto/crypto.dart';
|
||||
import 'package:device_info_plus/device_info_plus.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:image_gallery_saver_plus/image_gallery_saver_plus.dart';
|
||||
import 'package:loading_animation_widget/loading_animation_widget.dart';
|
||||
import 'package:open_file/open_file.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:tencent_chat_i18n_tool/tencent_chat_i18n_tool.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/models/v2_tim_image.dart'
|
||||
if (dart.library.html) 'package:tencent_cloud_chat_sdk/web/compatible_models/v2_tim_image.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/models/v2_tim_message.dart'
|
||||
if (dart.library.html) 'package:tencent_cloud_chat_sdk/web/compatible_models/v2_tim_message.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/base_widgets/tim_callback.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_base.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_state.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/business_logic/separate_models/tui_chat_separate_view_model.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/business_logic/view_models/tui_chat_global_model.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/data_services/message/message_services.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/data_services/services_locatar.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/tencent_cloud_chat_uikit.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/theme/tui_theme.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/constants/history_message_constant.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/utils/logger.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/utils/message.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/utils/permission.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/utils/platform.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/utils/screen_utils.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/views/TIMUIKitChat/TIMUIKitMessageItem/TIMUIKitMessageReaction/tim_uikit_message_reaction_wrapper.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/widgets/image_screen.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/widgets/wide_popup.dart';
|
||||
import 'package:transparent_image/transparent_image.dart';
|
||||
import 'package:universal_html/html.dart' as html;
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
class TIMUIKitImageElem extends StatefulWidget {
|
||||
final V2TimMessage message;
|
||||
final bool isShowJump;
|
||||
final VoidCallback? clearJump;
|
||||
final String? isFrom;
|
||||
final bool? isShowMessageReaction;
|
||||
final TUIChatSeparateViewModel chatModel;
|
||||
|
||||
const TIMUIKitImageElem(
|
||||
{required this.message,
|
||||
this.isShowJump = false,
|
||||
required this.chatModel,
|
||||
this.clearJump,
|
||||
this.isFrom,
|
||||
Key? key,
|
||||
this.isShowMessageReaction})
|
||||
: super(key: key);
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => _TIMUIKitImageElem();
|
||||
}
|
||||
|
||||
class _TIMUIKitImageElem extends TIMUIKitState<TIMUIKitImageElem> {
|
||||
final TUIChatGlobalModel globalModel = serviceLocator<TUIChatGlobalModel>();
|
||||
final TUIChatGlobalModel model = serviceLocator<TUIChatGlobalModel>();
|
||||
final MessageService _messageService = serviceLocator<MessageService>();
|
||||
Widget? imageItem;
|
||||
|
||||
@override
|
||||
didUpdateWidget(oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
}
|
||||
|
||||
String getOriginImgURL() {
|
||||
// 实际拿的是原图
|
||||
V2TimImage? img =
|
||||
MessageUtils.getImageFromImgList(widget.message.imageElem!.imageList, HistoryMessageDartConstant.oriImgPrior);
|
||||
return img == null ? widget.message.imageElem!.path! : img.url!;
|
||||
}
|
||||
|
||||
Widget errorDisplay(BuildContext context, TUITheme? theme) {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(5)),
|
||||
border: Border.all(
|
||||
width: 2,
|
||||
color: theme?.weakDividerColor ?? Colors.grey,
|
||||
)),
|
||||
height: 170,
|
||||
width: 170,
|
||||
child: Center(
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
LoadingAnimationWidget.staggeredDotsWave(
|
||||
color: theme?.weakTextColor ?? Colors.grey,
|
||||
size: 28,
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget getImage(image, {imageElem}) {
|
||||
Widget res = ClipRRect(
|
||||
clipper: ImageClipper(),
|
||||
child: image,
|
||||
);
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
//保存网络图片到本地
|
||||
Future<void> _saveImageToLocal(
|
||||
context,
|
||||
String imageUrl, {
|
||||
bool isLocalResource = true,
|
||||
TUITheme? theme,
|
||||
}) async {
|
||||
if (PlatformUtils().isWeb) {
|
||||
download(imageUrl) async {
|
||||
final http.Response r = await http.get(Uri.parse(imageUrl));
|
||||
final data = r.bodyBytes;
|
||||
final base64data = base64Encode(data);
|
||||
final a = html.AnchorElement(href: 'data:image/jpeg;base64,$base64data');
|
||||
a.download = md5.convert(utf8.encode(imageUrl)).toString();
|
||||
a.click();
|
||||
a.remove();
|
||||
}
|
||||
|
||||
download(imageUrl);
|
||||
return;
|
||||
}
|
||||
|
||||
if (PlatformUtils().isIOS) {
|
||||
if (!await Permissions.checkPermission(context, Permission.photosAddOnly.value, theme!, false)) {
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
final DeviceInfoPlugin deviceInfo = DeviceInfoPlugin();
|
||||
AndroidDeviceInfo androidInfo = await deviceInfo.androidInfo;
|
||||
if (PlatformUtils().isMobile) {
|
||||
if ((androidInfo.version.sdkInt) >= 33) {
|
||||
final photos = await Permissions.checkPermission(
|
||||
context,
|
||||
Permission.photos.value,
|
||||
theme,
|
||||
);
|
||||
if (!photos) {
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
final storage = await Permissions.checkPermission(
|
||||
context,
|
||||
Permission.storage.value,
|
||||
);
|
||||
if (!storage) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!isLocalResource) {
|
||||
if (widget.message.msgID == null || widget.message.msgID!.isEmpty) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (model.getMessageProgress(widget.message.msgID) == 100) {
|
||||
String savePath;
|
||||
if (widget.message.imageElem!.path != null &&
|
||||
widget.message.imageElem!.path != '' &&
|
||||
File(widget.message.imageElem!.path!).existsSync()) {
|
||||
savePath = widget.message.imageElem!.path!;
|
||||
} else {
|
||||
savePath = model.getFileMessageLocation(widget.message.msgID);
|
||||
}
|
||||
File f = File(savePath);
|
||||
if (f.existsSync()) {
|
||||
var result = await ImageGallerySaverPlus.saveFile(savePath);
|
||||
|
||||
if (PlatformUtils().isIOS) {
|
||||
if (result['isSuccess']) {
|
||||
onTIMCallback(
|
||||
TIMCallback(type: TIMCallbackType.INFO, infoRecommendText: TIM_t("图片保存成功"), infoCode: 6660406));
|
||||
} else {
|
||||
onTIMCallback(
|
||||
TIMCallback(type: TIMCallbackType.INFO, infoRecommendText: TIM_t("图片保存失败"), infoCode: 6660407));
|
||||
}
|
||||
} else {
|
||||
if (result != null) {
|
||||
onTIMCallback(
|
||||
TIMCallback(type: TIMCallbackType.INFO, infoRecommendText: TIM_t("图片保存成功"), infoCode: 6660406));
|
||||
} else {
|
||||
onTIMCallback(
|
||||
TIMCallback(type: TIMCallbackType.INFO, infoRecommendText: TIM_t("图片保存失败"), infoCode: 6660407));
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
onTIMCallback(TIMCallback(
|
||||
type: TIMCallbackType.INFO, infoRecommendText: TIM_t("the message is downloading"), infoCode: -1));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
var result = await ImageGallerySaverPlus.saveFile(imageUrl);
|
||||
|
||||
if (PlatformUtils().isIOS) {
|
||||
if (result['isSuccess']) {
|
||||
onTIMCallback(TIMCallback(type: TIMCallbackType.INFO, infoRecommendText: TIM_t("图片保存成功"), infoCode: 6660406));
|
||||
} else {
|
||||
onTIMCallback(TIMCallback(type: TIMCallbackType.INFO, infoRecommendText: TIM_t("图片保存失败"), infoCode: 6660407));
|
||||
}
|
||||
} else {
|
||||
if (result != null) {
|
||||
onTIMCallback(TIMCallback(type: TIMCallbackType.INFO, infoRecommendText: TIM_t("图片保存成功"), infoCode: 6660406));
|
||||
} else {
|
||||
onTIMCallback(TIMCallback(type: TIMCallbackType.INFO, infoRecommendText: TIM_t("图片保存失败"), infoCode: 6660407));
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
Future<void> _saveImg(TUITheme theme) async {
|
||||
try {
|
||||
String? imageUrl;
|
||||
bool isAssetBool = false;
|
||||
final imageElem = widget.message.imageElem;
|
||||
|
||||
if (imageElem != null) {
|
||||
final originUrl = getOriginImgURL();
|
||||
final localUrl = imageElem.imageList?.firstOrNull?.localUrl;
|
||||
final filePath = imageElem.path;
|
||||
final isWeb = PlatformUtils().isWeb;
|
||||
|
||||
if (!isWeb && filePath != null && File(filePath).existsSync()) {
|
||||
imageUrl = filePath;
|
||||
isAssetBool = true;
|
||||
} else if (localUrl != null && (!isWeb && File(localUrl).existsSync())) {
|
||||
imageUrl = localUrl;
|
||||
isAssetBool = true;
|
||||
} else {
|
||||
imageUrl = originUrl;
|
||||
isAssetBool = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (imageUrl != null) {
|
||||
return await _saveImageToLocal(
|
||||
context,
|
||||
imageUrl,
|
||||
isLocalResource: isAssetBool,
|
||||
theme: theme,
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
onTIMCallback(TIMCallback(infoCode: 6660414, infoRecommendText: TIM_t("正在下载中"), type: TIMCallbackType.INFO));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
V2TimImage? getImageFromList(V2TimImageTypesEnum imgType) {
|
||||
V2TimImage? img = MessageUtils.getImageFromImgList(widget.message.imageElem!.imageList,
|
||||
HistoryMessageDartConstant.imgPriorMap[imgType] ?? HistoryMessageDartConstant.oriImgPrior);
|
||||
|
||||
return img;
|
||||
}
|
||||
|
||||
void launchDesktopFile(String path) {
|
||||
if (PlatformUtils().isWindows) {
|
||||
OpenFile.open(path);
|
||||
} else {
|
||||
launchUrl(Uri.file(path));
|
||||
}
|
||||
}
|
||||
|
||||
Widget errorPage(theme) => Container(
|
||||
height: MediaQuery.of(context).size.height,
|
||||
color: theme.black,
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
child: errorDisplay(context, theme),
|
||||
));
|
||||
|
||||
bool checkIfDownloadSuccess() {
|
||||
final localUrl = TencentUtils.checkString(model.getFileMessageLocation(widget.message.msgID)) ??
|
||||
widget.message.imageElem!.imageList![0]!.localUrl;
|
||||
return TencentUtils.checkString(localUrl) != null && File(localUrl!).existsSync();
|
||||
}
|
||||
|
||||
_onClickOpenImageInNewWindow() {
|
||||
final localUrl = TencentUtils.checkString(model.getFileMessageLocation(widget.message.msgID)) ??
|
||||
widget.message.imageElem!.imageList![0]!.localUrl;
|
||||
Future.delayed(const Duration(milliseconds: 0), () async {
|
||||
final isDownloaded = checkIfDownloadSuccess();
|
||||
if (isDownloaded) {
|
||||
launchDesktopFile(localUrl ?? "");
|
||||
} else {
|
||||
onTIMCallback(
|
||||
TIMCallback(infoCode: 6660414, infoRecommendText: TIM_t("正在下载原始资源,请稍候..."), type: TIMCallbackType.INFO));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_handleOnTapPreviewImageOnDesktop({
|
||||
double? positionRadio,
|
||||
String? originImgUrl,
|
||||
}) {
|
||||
final localUrl = TencentUtils.checkString(model.getFileMessageLocation(widget.message.msgID)) ??
|
||||
widget.message.imageElem!.imageList![0]!.localUrl;
|
||||
if (checkIfDownloadSuccess()) {
|
||||
TUIKitWidePopup.showMedia(
|
||||
aspectRatio: positionRadio,
|
||||
context: context,
|
||||
mediaLocalPath: localUrl ?? "",
|
||||
onClickOrigin: () => _onClickOpenImageInNewWindow());
|
||||
} else {
|
||||
if (TencentUtils.checkString(originImgUrl) != null) {
|
||||
TUIKitWidePopup.showMedia(
|
||||
aspectRatio: positionRadio,
|
||||
context: context,
|
||||
mediaURL: originImgUrl,
|
||||
onClickOrigin: () => _onClickOpenImageInNewWindow());
|
||||
} else {
|
||||
onTIMCallback(TIMCallback(infoCode: 6660414, infoRecommendText: TIM_t("正在下载中"), type: TIMCallbackType.INFO));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<double> calculateAspectRatio(ImageProvider imageProvider) async {
|
||||
Completer<double> completer = Completer<double>();
|
||||
|
||||
final imageStream = imageProvider.resolve(const ImageConfiguration());
|
||||
imageStream.addListener(
|
||||
ImageStreamListener((imageInfo, synchronousCall) {
|
||||
if (imageInfo.image.width != 0 && imageInfo.image.height != 0) {
|
||||
double aspectRatio = imageInfo.image.width / imageInfo.image.height;
|
||||
completer.complete(aspectRatio);
|
||||
} else {
|
||||
// If unable to calculate aspect ratio, return default value of 0.5
|
||||
completer.complete(0.5);
|
||||
}
|
||||
}, onError: (Object exception, StackTrace? stackTrace) {
|
||||
// If there's an error, return default value of 0.5
|
||||
completer.complete(1);
|
||||
}),
|
||||
);
|
||||
|
||||
return await completer.future;
|
||||
}
|
||||
|
||||
void onClickImage({
|
||||
required bool isNetworkImage,
|
||||
dynamic heroTag,
|
||||
required TUITheme theme,
|
||||
String? imgUrl,
|
||||
String? imgPath,
|
||||
}) {
|
||||
if (isNetworkImage) {
|
||||
if (PlatformUtils().isWeb) {
|
||||
TUIKitWidePopup.showMedia(
|
||||
context: context,
|
||||
mediaURL: widget.message.imageElem?.path ?? "",
|
||||
onClickOrigin: () => launchUrl(
|
||||
Uri.parse(widget.message.imageElem?.path ?? ""),
|
||||
mode: LaunchMode.externalApplication,
|
||||
));
|
||||
return;
|
||||
}
|
||||
if (PlatformUtils().isDesktop) {
|
||||
_handleOnTapPreviewImageOnDesktop(
|
||||
originImgUrl: imgUrl,
|
||||
);
|
||||
} else {
|
||||
Navigator.of(context).push(
|
||||
PageRouteBuilder(
|
||||
opaque: false,
|
||||
pageBuilder: (_, __, ___) => ImageScreen(
|
||||
imageProvider: CachedNetworkImageProvider(
|
||||
imgUrl ?? "",
|
||||
cacheKey: widget.message.msgID,
|
||||
),
|
||||
heroTag: heroTag,
|
||||
messageID: widget.message.msgID,
|
||||
downloadFn: () async {
|
||||
return await _saveImg(theme);
|
||||
})),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
if (PlatformUtils().isDesktop) {
|
||||
TUIKitWidePopup.showMedia(
|
||||
mediaLocalPath: imgPath, context: context, onClickOrigin: () => launchDesktopFile(imgPath ?? ""));
|
||||
} else {
|
||||
Navigator.of(context).push(
|
||||
PageRouteBuilder(
|
||||
opaque: false, // set to false
|
||||
pageBuilder: (_, __, ___) => ImageScreen(
|
||||
imageProvider: FileImage(File(imgPath ?? "")),
|
||||
heroTag: heroTag,
|
||||
messageID: widget.message.msgID,
|
||||
downloadFn: () async {
|
||||
return await _saveImg(theme);
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Widget _renderAllImage(
|
||||
{dynamic heroTag,
|
||||
double? positionRadio,
|
||||
required TUITheme theme,
|
||||
bool isNetworkImage = false,
|
||||
String? webPath,
|
||||
V2TimImage? originalImg,
|
||||
V2TimImage? smallImg,
|
||||
String? smallLocalPath,
|
||||
String? originLocalPath}) {
|
||||
Widget getImageWidget() {
|
||||
if (isNetworkImage) {
|
||||
return Hero(
|
||||
tag: heroTag,
|
||||
child: PlatformUtils().isWeb
|
||||
? Image.network(webPath ?? smallImg?.url ?? originalImg!.url!, fit: BoxFit.contain)
|
||||
: CachedNetworkImage(
|
||||
alignment: Alignment.topCenter,
|
||||
imageUrl: webPath ?? smallImg?.url ?? originalImg!.url!,
|
||||
errorWidget: (context, error, stackTrace) => errorPage(theme),
|
||||
fit: BoxFit.contain,
|
||||
cacheKey: smallImg?.uuid ?? originalImg!.uuid,
|
||||
placeholder: (context, url) => Image(image: MemoryImage(kTransparentImage)),
|
||||
fadeInDuration: const Duration(milliseconds: 0),
|
||||
));
|
||||
} else {
|
||||
final imgPath = (TencentUtils.checkString(smallLocalPath) != null ? smallLocalPath : originLocalPath)!;
|
||||
return Hero(tag: heroTag, child: Image.file(File(imgPath), fit: BoxFit.contain));
|
||||
}
|
||||
}
|
||||
|
||||
double? currentPositionRadio;
|
||||
// File imgF = File((TencentUtils.checkString(originLocalPath) != null
|
||||
// ? originLocalPath
|
||||
// : smallLocalPath) ??
|
||||
// "");
|
||||
// bool isExist = imgF.existsSync();
|
||||
//
|
||||
// if (!isExist) {
|
||||
// return errorDisplay(context, theme);
|
||||
// }
|
||||
// Image image = Image.file(imgF);
|
||||
//
|
||||
// image.image
|
||||
// .resolve(const ImageConfiguration())
|
||||
// .addListener(ImageStreamListener((image, synchronousCall) {
|
||||
// if (image.image.width != 0 && image.image.height != 0) {
|
||||
// currentPositionRadio = image.image.width / image.image.height;
|
||||
// }
|
||||
// }));
|
||||
|
||||
return GestureDetector(
|
||||
onTap: () => onClickImage(
|
||||
theme: theme,
|
||||
heroTag: heroTag,
|
||||
isNetworkImage: isNetworkImage,
|
||||
imgUrl: webPath ?? smallImg?.url ?? originalImg?.url ?? "",
|
||||
imgPath: (TencentUtils.checkString(originLocalPath) != null ? originLocalPath : smallLocalPath) ?? ""),
|
||||
child: Stack(
|
||||
children: [
|
||||
if (positionRadio != null)
|
||||
AspectRatio(
|
||||
aspectRatio: (currentPositionRadio ?? positionRadio)!,
|
||||
child: Container(
|
||||
decoration: const BoxDecoration(color: Colors.transparent),
|
||||
),
|
||||
),
|
||||
getImageWidget(),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void initImages() async {
|
||||
final zeroImageLocal = TencentUtils.checkString(
|
||||
widget.message.imageElem?.imageList?.firstWhereOrNull((element) => element?.type == 0)?.localUrl);
|
||||
final oneImageLocal = TencentUtils.checkString(
|
||||
widget.message.imageElem?.imageList?.firstWhereOrNull((element) => element?.type == 1)?.localUrl);
|
||||
final twoImageLocal = TencentUtils.checkString(
|
||||
widget.message.imageElem?.imageList?.firstWhereOrNull((element) => element?.type == 2)?.localUrl);
|
||||
|
||||
if (!PlatformUtils().isWeb && TencentUtils.checkString(widget.message.msgID) != null) {
|
||||
if ((widget.message.imageElem?.imageList) == null || widget.message.imageElem!.imageList!.isEmpty) {
|
||||
final response = await _messageService.getMessageOnlineUrl(msgID: widget.message.msgID!);
|
||||
final elem = response.data;
|
||||
if (elem != null && elem.imageElem != null) {
|
||||
widget.message.imageElem = elem.imageElem;
|
||||
}
|
||||
}
|
||||
if (oneImageLocal == null || !File(oneImageLocal).existsSync()) {
|
||||
_messageService.downloadMessage(msgID: widget.message.msgID!, messageType: 3, imageType: 1, isSnapshot: false);
|
||||
}
|
||||
if (twoImageLocal == null || !File(twoImageLocal).existsSync()) {
|
||||
_messageService.downloadMessage(msgID: widget.message.msgID!, messageType: 3, imageType: 2, isSnapshot: false);
|
||||
}
|
||||
if (zeroImageLocal == null || !File(zeroImageLocal).existsSync()) {
|
||||
_messageService.downloadMessage(msgID: widget.message.msgID!, messageType: 3, imageType: 0, isSnapshot: false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
initImages();
|
||||
}
|
||||
|
||||
Widget? _renderImage(dynamic heroTag, TUITheme theme, {V2TimImage? originalImg, V2TimImage? smallImg}) {
|
||||
double positionRadio = 1.0;
|
||||
if (smallImg?.width != null && smallImg?.height != null && smallImg?.width != 0 && smallImg?.height != 0) {
|
||||
positionRadio = (smallImg!.width! / smallImg.height!);
|
||||
}
|
||||
|
||||
if (PlatformUtils().isWeb && widget.message.imageElem!.path != null) {
|
||||
// Displaying on Web only
|
||||
return _renderAllImage(
|
||||
heroTag: heroTag,
|
||||
theme: theme,
|
||||
isNetworkImage: true,
|
||||
smallImg: smallImg,
|
||||
originalImg: originalImg,
|
||||
positionRadio: positionRadio,
|
||||
webPath: widget.message.imageElem!.path);
|
||||
}
|
||||
|
||||
try {
|
||||
if ((widget.message.isSelf! &&
|
||||
widget.message.imageElem!.path != null &&
|
||||
widget.message.imageElem!.path!.isNotEmpty &&
|
||||
File(widget.message.imageElem!.path!).existsSync())) {
|
||||
return _renderAllImage(
|
||||
smallLocalPath: widget.message.imageElem!.path!,
|
||||
heroTag: heroTag,
|
||||
theme: theme,
|
||||
positionRadio: positionRadio,
|
||||
originLocalPath: widget.message.imageElem!.path!);
|
||||
}
|
||||
} catch (e) {
|
||||
// ignore: avoid_print
|
||||
outputLogger.i(e.toString());
|
||||
}
|
||||
|
||||
try {
|
||||
if ((TencentUtils.checkString(smallImg?.localUrl) != null && File((smallImg?.localUrl!)!).existsSync()) ||
|
||||
(TencentUtils.checkString(originalImg?.localUrl) != null && File((originalImg?.localUrl!)!).existsSync())) {
|
||||
return _renderAllImage(
|
||||
smallLocalPath: smallImg?.localUrl ?? "",
|
||||
heroTag: heroTag,
|
||||
theme: theme,
|
||||
positionRadio: positionRadio,
|
||||
originLocalPath: originalImg?.localUrl);
|
||||
}
|
||||
} catch (e) {
|
||||
// ignore: avoid_print
|
||||
outputLogger.i(e.toString());
|
||||
return _renderAllImage(
|
||||
heroTag: heroTag,
|
||||
theme: theme,
|
||||
isNetworkImage: true,
|
||||
smallImg: smallImg,
|
||||
positionRadio: positionRadio,
|
||||
originalImg: originalImg);
|
||||
}
|
||||
|
||||
if ((smallImg?.url ?? originalImg?.url) != null && (smallImg?.url ?? originalImg?.url)!.isNotEmpty) {
|
||||
return _renderAllImage(
|
||||
heroTag: heroTag,
|
||||
theme: theme,
|
||||
isNetworkImage: true,
|
||||
positionRadio: positionRadio,
|
||||
smallImg: smallImg,
|
||||
originalImg: originalImg);
|
||||
}
|
||||
|
||||
return errorDisplay(context, theme);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget tuiBuild(BuildContext context, TUIKitBuildValue value) {
|
||||
final theme = value.theme;
|
||||
final isDesktopScreen = TUIKitScreenUtils.getFormFactor(context) == DeviceType.Desktop;
|
||||
final heroTag =
|
||||
"${widget.message.msgID ?? widget.message.id ?? widget.message.timestamp ?? DateTime.now().millisecondsSinceEpoch}${widget.isFrom}";
|
||||
|
||||
V2TimImage? originalImg = getImageFromList(V2TimImageTypesEnum.original);
|
||||
V2TimImage? smallImg = getImageFromList(V2TimImageTypesEnum.small);
|
||||
return TIMUIKitMessageReactionWrapper(
|
||||
chatModel: widget.chatModel,
|
||||
isShowJump: widget.isShowJump,
|
||||
clearJump: widget.clearJump,
|
||||
isFromSelf: widget.message.isSelf ?? true,
|
||||
isShowMessageReaction: widget.isShowMessageReaction ?? true,
|
||||
message: widget.message,
|
||||
child: LayoutBuilder(builder: (BuildContext context, BoxConstraints constraints) {
|
||||
return ConstrainedBox(
|
||||
constraints: BoxConstraints(
|
||||
maxWidth: constraints.maxWidth * (isDesktopScreen ? 0.4 : 0.5),
|
||||
minWidth: 64,
|
||||
maxHeight: 256,
|
||||
),
|
||||
child: _renderImage(heroTag, theme, originalImg: originalImg, smallImg: smallImg),
|
||||
);
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,422 @@
|
||||
// ignore_for_file: unused_import
|
||||
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:tencent_chat_i18n_tool/tencent_chat_i18n_tool.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/enum/message_elem_type.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/enum/message_status.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/models/v2_tim_message.dart'
|
||||
if (dart.library.html) 'package:tencent_cloud_chat_sdk/web/compatible_models/v2_tim_message.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/business_logic/separate_models/tui_chat_model_tools.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/utils/common_utils.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/utils/platform.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/utils/screen_utils.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/views/TIMUIKitChat/TIMUIKitTextField/special_text/DefaultSpecialTextSpanBuilder.dart';
|
||||
import 'package:extended_text/extended_text.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/base_widgets/tim_callback.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_base.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_state.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/business_logic/separate_models/tui_chat_separate_view_model.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/business_logic/view_models/tui_chat_global_model.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/data_services/services_locatar.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/theme/tui_theme.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/views/TIMUIKitChat/TIMUIKitMessageItem/TIMUIKitMessageReaction/tim_uikit_message_reaction_show_panel.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/views/TIMUIKitChat/TIMUIKitMessageItem/main.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/views/TIMUIKitChat/TIMUIKitMessageItem/tim_uikit_chat_face_elem.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/views/TIMUIKitChat/tim_uikit_chat_config.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/views/TIMUIKitChat/tim_uikit_cloud_custom_data.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/widgets/link_preview/link_preview_entry.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/widgets/link_preview/models/link_preview_content.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/widgets/link_preview/widgets/link_preview.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/utils/logger.dart';
|
||||
import 'package:tim_ui_kit_sticker_plugin/utils/tim_custom_face_data.dart';
|
||||
|
||||
class TIMUIKitReplyElem extends StatefulWidget {
|
||||
final V2TimMessage message;
|
||||
final Function scrollToIndex;
|
||||
final bool isShowJump;
|
||||
final VoidCallback clearJump;
|
||||
final TextStyle? fontStyle;
|
||||
final BorderRadius? borderRadius;
|
||||
final Color? backgroundColor;
|
||||
final EdgeInsetsGeometry? textPadding;
|
||||
final TUIChatSeparateViewModel chatModel;
|
||||
final bool? isShowMessageReaction;
|
||||
final List<CustomEmojiFaceData> customEmojiStickerList;
|
||||
|
||||
const TIMUIKitReplyElem({
|
||||
Key? key,
|
||||
required this.message,
|
||||
required this.scrollToIndex,
|
||||
this.isShowJump = false,
|
||||
required this.clearJump,
|
||||
this.fontStyle,
|
||||
this.borderRadius,
|
||||
this.isShowMessageReaction,
|
||||
this.backgroundColor,
|
||||
this.textPadding,
|
||||
this.customEmojiStickerList = const [],
|
||||
required this.chatModel,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => _TIMUIKitReplyElemState();
|
||||
}
|
||||
|
||||
class _TIMUIKitReplyElemState extends TIMUIKitState<TIMUIKitReplyElem> {
|
||||
MessageRepliedData? repliedMessage;
|
||||
V2TimMessage? rawMessage;
|
||||
bool isShowJumpState = false;
|
||||
bool isShining = false;
|
||||
|
||||
MessageRepliedData? _getRepliedMessage() {
|
||||
try {
|
||||
final CloudCustomData messageCloudCustomData = CloudCustomData.fromJson(json.decode(
|
||||
TencentUtils.checkString(widget.message.cloudCustomData) != null ? widget.message.cloudCustomData! : "{}"));
|
||||
if (messageCloudCustomData.messageReply != null) {
|
||||
final MessageRepliedData repliedMessage = MessageRepliedData.fromJson(messageCloudCustomData.messageReply!);
|
||||
return repliedMessage;
|
||||
}
|
||||
return null;
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
_getMessageByMessageID() async {
|
||||
final MessageRepliedData? cloudCustomData = _getRepliedMessage();
|
||||
if (cloudCustomData != null) {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
repliedMessage = cloudCustomData;
|
||||
});
|
||||
}
|
||||
|
||||
final messageID = cloudCustomData.messageID;
|
||||
if (PlatformUtils().isWeb) {
|
||||
return;
|
||||
}
|
||||
V2TimMessage? message = await widget.chatModel.findMessage(messageID);
|
||||
if (message == null) {
|
||||
try {
|
||||
final RepliedMessageAbstract repliedMessageAbstract =
|
||||
RepliedMessageAbstract.fromJson(jsonDecode(cloudCustomData.messageAbstract));
|
||||
if (repliedMessageAbstract.isNotEmpty) {
|
||||
message = V2TimMessage(
|
||||
elemType: 0,
|
||||
seq: repliedMessageAbstract.seq,
|
||||
timestamp: repliedMessageAbstract.timestamp,
|
||||
msgID: repliedMessageAbstract.msgID);
|
||||
}
|
||||
} catch (e) {
|
||||
// ignore: avoid_print
|
||||
outputLogger.i(e.toString());
|
||||
}
|
||||
}
|
||||
if (message != null) {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
rawMessage = message;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Widget _defaultRawMessageText(String text, TUITheme? theme) {
|
||||
return Text(text,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(fontSize: 12, color: theme?.weakTextColor, fontWeight: FontWeight.w400));
|
||||
}
|
||||
|
||||
_renderMessageSummary(TUITheme? theme) {
|
||||
try {
|
||||
final RepliedMessageAbstract repliedMessageAbstract =
|
||||
RepliedMessageAbstract.fromJson(jsonDecode(repliedMessage?.messageAbstract ?? ""));
|
||||
if (TencentUtils.checkString(repliedMessageAbstract.summary) != null) {
|
||||
return _defaultRawMessageText(repliedMessageAbstract.summary!, theme);
|
||||
}
|
||||
return _defaultRawMessageText(repliedMessage?.messageAbstract ?? TIM_t("[未知消息]"), theme);
|
||||
} catch (e) {
|
||||
return _defaultRawMessageText(repliedMessage?.messageAbstract ?? TIM_t("[未知消息]"), theme);
|
||||
}
|
||||
}
|
||||
|
||||
(bool isRevoke, bool isRevokeByAdmin) isRevokeMessage(V2TimMessage message) {
|
||||
if (message.status == 6) {
|
||||
return (true, false);
|
||||
} else {
|
||||
try {
|
||||
final customData = jsonDecode(message.cloudCustomData ?? "{}");
|
||||
final isRevoke = customData["isRevoke"] ?? false;
|
||||
final revokeByAdmin = customData["revokeByAdmin"] ?? false;
|
||||
return (isRevoke, revokeByAdmin);
|
||||
} catch (e) {
|
||||
return (false, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_rawMessageBuilder(V2TimMessage? message, TUITheme? theme) {
|
||||
if (repliedMessage == null) {
|
||||
return const SizedBox(width: 0, height: 12);
|
||||
}
|
||||
if (message == null) {
|
||||
if (repliedMessage?.messageAbstract != null) {
|
||||
return _renderMessageSummary(theme);
|
||||
}
|
||||
return const SizedBox(width: 0, height: 12);
|
||||
}
|
||||
|
||||
final revokeStatus = isRevokeMessage(message);
|
||||
final isRevokedMsg = revokeStatus.$1;
|
||||
final isAdminRevoke = revokeStatus.$2;
|
||||
|
||||
if (isRevokedMsg) {
|
||||
return _defaultRawMessageText(isAdminRevoke ? TIM_t("[消息被管理员撤回]") : TIM_t("[消息被撤回]"), theme);
|
||||
}
|
||||
|
||||
final messageType = message.elemType;
|
||||
final isSelf = message.isSelf ?? true;
|
||||
final customAbstractMessage =
|
||||
widget.chatModel.abstractMessageBuilder != null ? widget.chatModel.abstractMessageBuilder!(message) : null;
|
||||
if (customAbstractMessage != null) {
|
||||
return _defaultRawMessageText(
|
||||
customAbstractMessage,
|
||||
theme,
|
||||
);
|
||||
}
|
||||
switch (messageType) {
|
||||
case MessageElemType.V2TIM_ELEM_TYPE_CUSTOM:
|
||||
return _defaultRawMessageText(TIM_t("[自定义]"), theme);
|
||||
case MessageElemType.V2TIM_ELEM_TYPE_SOUND:
|
||||
return _defaultRawMessageText(TIM_t("[语音消息]"), theme);
|
||||
case MessageElemType.V2TIM_ELEM_TYPE_TEXT:
|
||||
return _defaultRawMessageText(message.textElem?.text ?? "", theme);
|
||||
case MessageElemType.V2TIM_ELEM_TYPE_FACE:
|
||||
return TIMUIKitFaceElem(
|
||||
model: widget.chatModel,
|
||||
isShowJump: false,
|
||||
isShowMessageReaction: false,
|
||||
path: message.faceElem!.data ?? "",
|
||||
message: message,
|
||||
);
|
||||
case MessageElemType.V2TIM_ELEM_TYPE_FILE:
|
||||
return TIMUIKitFileElem(
|
||||
chatModel: widget.chatModel,
|
||||
isShowMessageReaction: false,
|
||||
message: message,
|
||||
messageID: message.msgID,
|
||||
fileElem: message.fileElem,
|
||||
isSelf: isSelf,
|
||||
isShowJump: false);
|
||||
case MessageElemType.V2TIM_ELEM_TYPE_IMAGE:
|
||||
return TIMUIKitImageElem(
|
||||
chatModel: widget.chatModel, message: message, isFrom: "reply", isShowMessageReaction: false);
|
||||
case MessageElemType.V2TIM_ELEM_TYPE_VIDEO:
|
||||
return TIMUIKitVideoElem(message, chatModel: widget.chatModel, isFrom: "reply", isShowMessageReaction: false);
|
||||
case MessageElemType.V2TIM_ELEM_TYPE_LOCATION:
|
||||
return _defaultRawMessageText(TIM_t("[位置]"), theme);
|
||||
case MessageElemType.V2TIM_ELEM_TYPE_MERGER:
|
||||
return TIMUIKitMergerElem(
|
||||
model: widget.chatModel,
|
||||
isShowJump: false,
|
||||
isShowMessageReaction: false,
|
||||
message: message,
|
||||
mergerElem: message.mergerElem!,
|
||||
messageID: message.msgID ?? "",
|
||||
isSelf: isSelf);
|
||||
default:
|
||||
return _renderMessageSummary(theme);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
_getMessageByMessageID();
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(covariant TIMUIKitReplyElem oldWidget) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((mag) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
_getMessageByMessageID();
|
||||
});
|
||||
}
|
||||
|
||||
_showJumpColor() {
|
||||
if ((widget.chatModel.jumpMsgID != widget.message.msgID) && (widget.message.msgID?.isNotEmpty ?? true)) {
|
||||
return;
|
||||
}
|
||||
isShining = true;
|
||||
int shineAmount = 6;
|
||||
setState(() {
|
||||
isShowJumpState = true;
|
||||
});
|
||||
Timer.periodic(const Duration(milliseconds: 300), (timer) {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
isShowJumpState = shineAmount.isOdd ? true : false;
|
||||
});
|
||||
}
|
||||
if (shineAmount == 0 || !mounted) {
|
||||
isShining = false;
|
||||
timer.cancel();
|
||||
}
|
||||
shineAmount--;
|
||||
});
|
||||
widget.clearJump();
|
||||
}
|
||||
|
||||
void _jumpToRawMsg() {
|
||||
if (rawMessage?.status != MessageStatus.V2TIM_MSG_STATUS_LOCAL_REVOKED && rawMessage?.timestamp != null) {
|
||||
widget.scrollToIndex(rawMessage);
|
||||
} else {
|
||||
onTIMCallback(TIMCallback(type: TIMCallbackType.INFO, infoRecommendText: TIM_t("无法定位到原消息")));
|
||||
}
|
||||
}
|
||||
|
||||
Widget? _renderPreviewWidget() {
|
||||
// If the link preview info from [localCustomData] is available, use it to render the preview card.
|
||||
// Otherwise, it will returns null.
|
||||
if (widget.message.localCustomData != null && widget.message.localCustomData!.isNotEmpty) {
|
||||
try {
|
||||
final String localJSON = widget.message.localCustomData!;
|
||||
final LocalCustomDataModel? localPreviewInfo = LocalCustomDataModel.fromMap(json.decode(localJSON));
|
||||
if (localPreviewInfo != null && !localPreviewInfo.isLinkPreviewEmpty()) {
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(top: 8),
|
||||
child:
|
||||
// You can use this default widget [LinkPreviewWidget] to render preview card, or you can use custom widget.
|
||||
LinkPreviewWidget(linkPreview: localPreviewInfo),
|
||||
);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget tuiBuild(BuildContext context, TUIKitBuildValue value) {
|
||||
final theme = value.theme;
|
||||
final isDesktopScreen = TUIKitScreenUtils.getFormFactor(context) == DeviceType.Desktop;
|
||||
if (widget.isShowJump) {
|
||||
if (!isShining) {
|
||||
Future.delayed(Duration.zero, () {
|
||||
_showJumpColor();
|
||||
});
|
||||
} else {
|
||||
if ((widget.chatModel.jumpMsgID == widget.message.msgID) && (widget.message.msgID?.isNotEmpty ?? false)) {
|
||||
widget.clearJump();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final isFromSelf = widget.message.isSelf ?? true;
|
||||
|
||||
final defaultStyle = isFromSelf
|
||||
? (theme.chatMessageItemFromSelfBgColor ?? theme.lightPrimaryMaterialColor.shade50)
|
||||
: (theme.chatMessageItemFromOthersBgColor);
|
||||
|
||||
final backgroundColor =
|
||||
isShowJumpState ? const Color.fromRGBO(245, 166, 35, 1) : (defaultStyle ?? widget.backgroundColor);
|
||||
|
||||
final borderRadius = isFromSelf
|
||||
? const BorderRadius.only(
|
||||
topLeft: Radius.circular(10),
|
||||
topRight: Radius.circular(2),
|
||||
bottomLeft: Radius.circular(10),
|
||||
bottomRight: Radius.circular(10))
|
||||
: const BorderRadius.only(
|
||||
topLeft: Radius.circular(2),
|
||||
topRight: Radius.circular(10),
|
||||
bottomLeft: Radius.circular(10),
|
||||
bottomRight: Radius.circular(10));
|
||||
final textWithLink = LinkPreviewEntry.getHyperlinksText(
|
||||
widget.message.textElem?.text ?? "", widget.chatModel.chatConfig.isSupportMarkdownForTextMessage,
|
||||
onLinkTap: widget.chatModel.chatConfig.onTapLink,
|
||||
isUseQQPackage: widget.chatModel.chatConfig.stickerPanelConfig?.useQQStickerPackage ?? true,
|
||||
isUseTencentCloudChatPackage:
|
||||
widget.chatModel.chatConfig.stickerPanelConfig?.useTencentCloudChatStickerPackage ?? true,
|
||||
isUseTencentCloudChatPackageOldKeys:
|
||||
widget.chatModel.chatConfig.stickerPanelConfig?.useTencentCloudChatStickerPackageOldKeys ?? false,
|
||||
customEmojiStickerList: widget.customEmojiStickerList,
|
||||
isEnableTextSelection: widget.chatModel.chatConfig.isEnableTextSelection ?? false);
|
||||
return Container(
|
||||
padding: widget.textPadding ?? EdgeInsets.all(isDesktopScreen ? 12 : 10),
|
||||
decoration: BoxDecoration(
|
||||
color: backgroundColor,
|
||||
borderRadius: widget.borderRadius ?? borderRadius,
|
||||
),
|
||||
constraints: BoxConstraints(maxWidth: MediaQuery.of(context).size.width * 0.6),
|
||||
child: GestureDetector(
|
||||
onTap: _jumpToRawMsg,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Container(
|
||||
// 这里是引用的部分
|
||||
padding: const EdgeInsets.symmetric(vertical: 3, horizontal: 6),
|
||||
constraints: const BoxConstraints(minWidth: 120),
|
||||
decoration: const BoxDecoration(
|
||||
color: Color.fromRGBO(68, 68, 68, 0.05),
|
||||
border: Border(left: BorderSide(color: Color.fromRGBO(68, 68, 68, 0.1), width: 2))),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
repliedMessage != null ? "${repliedMessage!.messageSender}:" : "",
|
||||
style: TextStyle(fontSize: 12, color: theme.weakTextColor, fontWeight: FontWeight.w500),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 4,
|
||||
),
|
||||
_rawMessageBuilder(rawMessage, theme)
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 12,
|
||||
),
|
||||
// If the [elemType] is text message, it will not be null here.
|
||||
// You can render the widget from extension directly, with a [TextStyle] optionally.
|
||||
widget.chatModel.chatConfig.urlPreviewType != UrlPreviewType.none
|
||||
? textWithLink!(
|
||||
style: widget.fontStyle ??
|
||||
TextStyle(
|
||||
fontSize: isDesktopScreen ? 14 : 16,
|
||||
textBaseline: TextBaseline.ideographic,
|
||||
height: widget.chatModel.chatConfig.textHeight))
|
||||
: ExtendedText(widget.message.textElem?.text ?? "",
|
||||
softWrap: true,
|
||||
style: widget.fontStyle ??
|
||||
TextStyle(fontSize: isDesktopScreen ? 14 : 16, height: widget.chatModel.chatConfig.textHeight),
|
||||
specialTextSpanBuilder: DefaultSpecialTextSpanBuilder(
|
||||
isUseQQPackage: widget.chatModel.chatConfig.stickerPanelConfig?.useQQStickerPackage ?? true,
|
||||
isUseTencentCloudChatPackage:
|
||||
widget.chatModel.chatConfig.stickerPanelConfig?.useTencentCloudChatStickerPackage ?? true,
|
||||
isUseTencentCloudChatPackageOldKeys:
|
||||
widget.chatModel.chatConfig.stickerPanelConfig?.useTencentCloudChatStickerPackageOldKeys ??
|
||||
false,
|
||||
customEmojiStickerList: widget.customEmojiStickerList,
|
||||
showAtBackground: true,
|
||||
)),
|
||||
// If the link preview info is available, render the preview card.
|
||||
if (_renderPreviewWidget() != null &&
|
||||
widget.chatModel.chatConfig.urlPreviewType == UrlPreviewType.previewCardAndHyperlink)
|
||||
_renderPreviewWidget()!,
|
||||
if (widget.isShowMessageReaction ?? true) TIMUIKitMessageReactionShowPanel(message: widget.message)
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,268 @@
|
||||
import 'dart:async';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:just_audio/just_audio.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/models/v2_tim_message.dart'
|
||||
if (dart.library.html) 'package:tencent_cloud_chat_sdk/web/compatible_models/v2_tim_message.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/models/v2_tim_sound_elem.dart'
|
||||
if (dart.library.html) 'package:tencent_cloud_chat_sdk/web/compatible_models/v2_tim_sound_elem.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_base.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_state.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/business_logic/separate_models/tui_chat_separate_view_model.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/business_logic/view_models/tui_chat_global_model.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/data_services/message/message_services.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/data_services/services_locatar.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/constants/history_message_constant.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/utils/platform.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/utils/sound_record.dart';
|
||||
import 'TIMUIKitMessageReaction/tim_uikit_message_reaction_show_panel.dart';
|
||||
|
||||
class TIMUIKitSoundElem extends StatefulWidget {
|
||||
final V2TimMessage message;
|
||||
final V2TimSoundElem soundElem;
|
||||
final String msgID;
|
||||
final bool isFromSelf;
|
||||
final int? localCustomInt;
|
||||
final bool isShowJump;
|
||||
final VoidCallback? clearJump;
|
||||
final TextStyle? fontStyle;
|
||||
final BorderRadius? borderRadius;
|
||||
final Color? backgroundColor;
|
||||
final EdgeInsetsGeometry? textPadding;
|
||||
final bool? isShowMessageReaction;
|
||||
final TUIChatSeparateViewModel chatModel;
|
||||
|
||||
const TIMUIKitSoundElem(
|
||||
{Key? key,
|
||||
required this.soundElem,
|
||||
required this.msgID,
|
||||
required this.isFromSelf,
|
||||
this.isShowJump = false,
|
||||
this.clearJump,
|
||||
this.localCustomInt,
|
||||
this.fontStyle,
|
||||
this.borderRadius,
|
||||
this.backgroundColor,
|
||||
this.textPadding,
|
||||
required this.message,
|
||||
this.isShowMessageReaction,
|
||||
required this.chatModel})
|
||||
: super(key: key);
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => _TIMUIKitSoundElemState();
|
||||
}
|
||||
|
||||
class _TIMUIKitSoundElemState extends TIMUIKitState<TIMUIKitSoundElem> {
|
||||
final int charLen = 8;
|
||||
bool isPlaying = false;
|
||||
StreamSubscription<Object>? subscription;
|
||||
bool isShowJumpState = false;
|
||||
bool isShining = false;
|
||||
final TUIChatGlobalModel globalModel = serviceLocator<TUIChatGlobalModel>();
|
||||
final MessageService _messageService = serviceLocator<MessageService>();
|
||||
late V2TimSoundElem stateElement = widget.message.soundElem!;
|
||||
|
||||
_playSound() async {
|
||||
if (!SoundPlayer.isInit) {
|
||||
SoundPlayer.initSoundPlayer();
|
||||
}
|
||||
if (widget.localCustomInt == null || widget.localCustomInt != HistoryMessageDartConstant.read) {
|
||||
globalModel.setLocalCustomInt(widget.msgID, HistoryMessageDartConstant.read, widget.chatModel.conversationID);
|
||||
}
|
||||
if (isPlaying) {
|
||||
SoundPlayer.stop();
|
||||
widget.chatModel.currentPlayedMsgId = "";
|
||||
} else {
|
||||
SoundPlayer.play(url: stateElement.url!);
|
||||
widget.chatModel.currentPlayedMsgId = widget.msgID;
|
||||
|
||||
setState(() {
|
||||
isPlaying = widget.chatModel.currentPlayedMsgId != '' && widget.chatModel.currentPlayedMsgId == widget.msgID;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
downloadMessageDetailAndSave() async {
|
||||
if (widget.message.msgID != null && widget.message.msgID != '') {
|
||||
if (widget.message.soundElem!.url == null || widget.message.soundElem!.url == '') {
|
||||
final response = await _messageService.getMessageOnlineUrl(msgID: widget.message.msgID!);
|
||||
if (response.data != null) {
|
||||
widget.message.soundElem = response.data!.soundElem;
|
||||
Future.delayed(const Duration(microseconds: 10), () {
|
||||
setState(() => stateElement = response.data!.soundElem!);
|
||||
});
|
||||
}
|
||||
}
|
||||
if (!PlatformUtils().isWeb) {
|
||||
if (widget.message.soundElem!.localUrl == null || widget.message.soundElem!.localUrl == '') {
|
||||
_messageService.downloadMessage(
|
||||
msgID: widget.message.msgID!, messageType: 4, imageType: 0, isSnapshot: false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
subscription = SoundPlayer.playStateListener(listener: (PlayerState state) {
|
||||
if (state.processingState == ProcessingState.completed) {
|
||||
widget.chatModel.currentPlayedMsgId = "";
|
||||
setState(() {
|
||||
isPlaying = false;
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
downloadMessageDetailAndSave();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
if (isPlaying) {
|
||||
SoundPlayer.stop();
|
||||
widget.chatModel.currentPlayedMsgId = "";
|
||||
}
|
||||
subscription?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
double _getSoundLen() {
|
||||
double soundLen = 32;
|
||||
if (stateElement.duration != null) {
|
||||
final realSoundLen = stateElement.duration!;
|
||||
int sdLen = 32;
|
||||
if (realSoundLen > 10) {
|
||||
sdLen = 12 * charLen + ((realSoundLen - 10) * charLen / 0.5).floor();
|
||||
} else if (realSoundLen > 2) {
|
||||
sdLen = 2 * charLen + realSoundLen * charLen;
|
||||
}
|
||||
sdLen = min(sdLen, 20 * charLen);
|
||||
soundLen = sdLen.toDouble();
|
||||
}
|
||||
|
||||
return soundLen;
|
||||
}
|
||||
|
||||
_showJumpColor() {
|
||||
if ((widget.chatModel.jumpMsgID != widget.message.msgID) && (widget.message.msgID?.isNotEmpty ?? true)) {
|
||||
return;
|
||||
}
|
||||
isShining = true;
|
||||
int shineAmount = 6;
|
||||
setState(() {
|
||||
isShowJumpState = true;
|
||||
});
|
||||
Timer.periodic(const Duration(milliseconds: 300), (timer) {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
isShowJumpState = shineAmount.isOdd ? true : false;
|
||||
});
|
||||
}
|
||||
if (shineAmount == 0 || !mounted) {
|
||||
isShining = false;
|
||||
timer.cancel();
|
||||
}
|
||||
shineAmount--;
|
||||
});
|
||||
widget.clearJump!();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget tuiBuild(BuildContext context, TUIKitBuildValue value) {
|
||||
final theme = value.theme;
|
||||
|
||||
final backgroundColor = widget.isFromSelf
|
||||
? (theme.chatMessageItemFromSelfBgColor ?? theme.lightPrimaryMaterialColor.shade50)
|
||||
: (theme.chatMessageItemFromOthersBgColor);
|
||||
|
||||
final borderRadius = widget.isFromSelf
|
||||
? const BorderRadius.only(
|
||||
topLeft: Radius.circular(10),
|
||||
topRight: Radius.circular(2),
|
||||
bottomLeft: Radius.circular(10),
|
||||
bottomRight: Radius.circular(10))
|
||||
: const BorderRadius.only(
|
||||
topLeft: Radius.circular(2),
|
||||
topRight: Radius.circular(10),
|
||||
bottomLeft: Radius.circular(10),
|
||||
bottomRight: Radius.circular(10));
|
||||
if (widget.isShowJump) {
|
||||
if (!isShining) {
|
||||
Future.delayed(Duration.zero, () {
|
||||
_showJumpColor();
|
||||
});
|
||||
} else {
|
||||
if ((widget.chatModel.jumpMsgID == widget.message.msgID) && (widget.message.msgID?.isNotEmpty ?? false)) {
|
||||
widget.clearJump!();
|
||||
}
|
||||
}
|
||||
}
|
||||
return GestureDetector(
|
||||
onTap: () => _playSound(),
|
||||
child: Container(
|
||||
padding: widget.textPadding ?? const EdgeInsets.all(10),
|
||||
decoration: BoxDecoration(
|
||||
color: isShowJumpState ? const Color.fromRGBO(245, 166, 35, 1) : (widget.backgroundColor ?? backgroundColor),
|
||||
borderRadius: widget.borderRadius ?? borderRadius,
|
||||
),
|
||||
constraints: const BoxConstraints(maxWidth: 240),
|
||||
child: Column(
|
||||
children: [
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: widget.isFromSelf
|
||||
? [
|
||||
Container(width: _getSoundLen()),
|
||||
Text(
|
||||
"''${stateElement.duration} ",
|
||||
style: widget.fontStyle,
|
||||
),
|
||||
isPlaying
|
||||
? Image.asset(
|
||||
'images/play_voice_send.gif',
|
||||
package: 'tencent_cloud_chat_uikit',
|
||||
width: 16,
|
||||
height: 16,
|
||||
)
|
||||
: Image.asset(
|
||||
'images/voice_send.png',
|
||||
package: 'tencent_cloud_chat_uikit',
|
||||
width: 16,
|
||||
height: 16,
|
||||
),
|
||||
]
|
||||
: [
|
||||
isPlaying
|
||||
? Image.asset(
|
||||
'images/play_voice_receive.gif',
|
||||
package: 'tencent_cloud_chat_uikit',
|
||||
width: 16,
|
||||
height: 16,
|
||||
)
|
||||
: Image.asset(
|
||||
'images/voice_receive.png',
|
||||
width: 16,
|
||||
height: 16,
|
||||
package: 'tencent_cloud_chat_uikit',
|
||||
),
|
||||
Text(
|
||||
" ${stateElement.duration}''",
|
||||
style: widget.fontStyle,
|
||||
),
|
||||
Container(width: _getSoundLen()),
|
||||
],
|
||||
),
|
||||
if (widget.isShowMessageReaction ?? true)
|
||||
TIMUIKitMessageReactionShowPanel(
|
||||
message: widget.message,
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,241 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:tencent_cloud_chat_sdk/models/v2_tim_message.dart'
|
||||
if (dart.library.html) 'package:tencent_cloud_chat_sdk/web/compatible_models/v2_tim_message.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/utils/screen_utils.dart';
|
||||
import 'package:extended_text/extended_text.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_base.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_state.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/business_logic/separate_models/tui_chat_separate_view_model.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/tencent_cloud_chat_uikit.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/views/TIMUIKitChat/TIMUIKitTextField/special_text/DefaultSpecialTextSpanBuilder.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/widgets/link_preview/link_preview_entry.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/widgets/link_preview/widgets/link_preview.dart';
|
||||
import 'TIMUIKitMessageReaction/tim_uikit_message_reaction_show_panel.dart';
|
||||
|
||||
class TIMUIKitTextElem extends StatefulWidget {
|
||||
final V2TimMessage message;
|
||||
final bool isFromSelf;
|
||||
final bool isShowJump;
|
||||
final VoidCallback clearJump;
|
||||
final TextStyle? fontStyle;
|
||||
final BorderRadius? borderRadius;
|
||||
final Color? backgroundColor;
|
||||
final EdgeInsetsGeometry? textPadding;
|
||||
final TUIChatSeparateViewModel chatModel;
|
||||
final bool? isShowMessageReaction;
|
||||
final List<CustomEmojiFaceData> customEmojiStickerList;
|
||||
|
||||
const TIMUIKitTextElem(
|
||||
{Key? key,
|
||||
required this.message,
|
||||
required this.isFromSelf,
|
||||
required this.isShowJump,
|
||||
required this.clearJump,
|
||||
this.fontStyle,
|
||||
this.borderRadius,
|
||||
this.isShowMessageReaction,
|
||||
this.backgroundColor,
|
||||
this.textPadding,
|
||||
required this.chatModel,
|
||||
this.customEmojiStickerList = const []})
|
||||
: super(key: key);
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => _TIMUIKitTextElemState();
|
||||
}
|
||||
|
||||
class _TIMUIKitTextElemState extends TIMUIKitState<TIMUIKitTextElem> {
|
||||
bool isShowJumpState = false;
|
||||
bool isShining = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
// get the link preview info
|
||||
_getLinkPreview();
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(TIMUIKitTextElem oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (oldWidget.message.msgID == null && widget.message.msgID != null) {
|
||||
_getLinkPreview();
|
||||
}
|
||||
}
|
||||
|
||||
_showJumpColor() {
|
||||
if ((widget.chatModel.jumpMsgID != widget.message.msgID) && (widget.message.msgID?.isNotEmpty ?? true)) {
|
||||
return;
|
||||
}
|
||||
isShining = true;
|
||||
int shineAmount = 6;
|
||||
setState(() {
|
||||
isShowJumpState = true;
|
||||
});
|
||||
Timer.periodic(const Duration(milliseconds: 300), (timer) {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
isShowJumpState = shineAmount.isOdd ? true : false;
|
||||
});
|
||||
}
|
||||
if (shineAmount == 0 || !mounted) {
|
||||
isShining = false;
|
||||
timer.cancel();
|
||||
}
|
||||
shineAmount--;
|
||||
});
|
||||
Future.delayed(const Duration(milliseconds: 100), () {
|
||||
widget.clearJump();
|
||||
});
|
||||
}
|
||||
|
||||
// get the link preview info
|
||||
_getLinkPreview() {
|
||||
if (widget.chatModel.chatConfig.urlPreviewType != UrlPreviewType.previewCardAndHyperlink) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
if (widget.message.localCustomData != null && widget.message.localCustomData!.isNotEmpty) {
|
||||
final String localJSON = widget.message.localCustomData!;
|
||||
final LocalCustomDataModel? localPreviewInfo = LocalCustomDataModel.fromMap(json.decode(localJSON));
|
||||
// If [localCustomData] is not empty, check if the link preview info exists
|
||||
if (localPreviewInfo == null || localPreviewInfo.isLinkPreviewEmpty()) {
|
||||
// If not exists, get it
|
||||
_initLinkPreview();
|
||||
}
|
||||
} else {
|
||||
// It [localCustomData] is empty, get the link info
|
||||
_initLinkPreview();
|
||||
}
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
_initLinkPreview() async {
|
||||
// Get the link preview info from extension, let it update the message UI automatically by providing a [onUpdateMessage].
|
||||
// The `onUpdateMessage` can use the `updateMessage()` from the [TIMUIKitChatController] directly.
|
||||
LinkPreviewEntry.getFirstLinkPreviewContent(
|
||||
message: widget.message,
|
||||
onUpdateMessage: (message) {
|
||||
widget.chatModel.updateMessageFromController(msgID: widget.message.msgID!, message: message);
|
||||
});
|
||||
}
|
||||
|
||||
Widget? _renderPreviewWidget() {
|
||||
// If the link preview info from [localCustomData] is available, use it to render the preview card.
|
||||
// Otherwise, it will returns null.
|
||||
if (widget.message.localCustomData != null && widget.message.localCustomData!.isNotEmpty) {
|
||||
try {
|
||||
final String localJSON = widget.message.localCustomData!;
|
||||
final LocalCustomDataModel? localPreviewInfo = LocalCustomDataModel.fromMap(json.decode(localJSON));
|
||||
if (localPreviewInfo != null && !localPreviewInfo.isLinkPreviewEmpty()) {
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(top: 8),
|
||||
child:
|
||||
// You can use this default widget [LinkPreviewWidget] to render preview card, or you can use custom widget.
|
||||
LinkPreviewWidget(linkPreview: localPreviewInfo),
|
||||
);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget tuiBuild(BuildContext context, TUIKitBuildValue value) {
|
||||
final theme = value.theme;
|
||||
final isDesktopScreen = TUIKitScreenUtils.getFormFactor(context) == DeviceType.Desktop;
|
||||
final textWithLink = LinkPreviewEntry.getHyperlinksText(
|
||||
widget.message.textElem?.text ?? "", widget.chatModel.chatConfig.isSupportMarkdownForTextMessage,
|
||||
onLinkTap: widget.chatModel.chatConfig.onTapLink,
|
||||
isUseQQPackage: widget.chatModel.chatConfig.stickerPanelConfig?.useQQStickerPackage ?? true,
|
||||
isUseTencentCloudChatPackage:
|
||||
widget.chatModel.chatConfig.stickerPanelConfig?.useTencentCloudChatStickerPackage ?? true,
|
||||
isUseTencentCloudChatPackageOldKeys:
|
||||
widget.chatModel.chatConfig.stickerPanelConfig?.useTencentCloudChatStickerPackageOldKeys ?? false,
|
||||
customEmojiStickerList: widget.customEmojiStickerList,
|
||||
isEnableTextSelection: widget.chatModel.chatConfig.isEnableTextSelection ?? false);
|
||||
final borderRadius = widget.isFromSelf
|
||||
? const BorderRadius.only(
|
||||
topLeft: Radius.circular(10),
|
||||
topRight: Radius.circular(2),
|
||||
bottomLeft: Radius.circular(10),
|
||||
bottomRight: Radius.circular(10))
|
||||
: const BorderRadius.only(
|
||||
topLeft: Radius.circular(2),
|
||||
topRight: Radius.circular(10),
|
||||
bottomLeft: Radius.circular(10),
|
||||
bottomRight: Radius.circular(10));
|
||||
if ((widget.chatModel.jumpMsgID == widget.message.msgID)) {}
|
||||
if (widget.isShowJump) {
|
||||
if (!isShining) {
|
||||
Future.delayed(Duration.zero, () {
|
||||
_showJumpColor();
|
||||
});
|
||||
} else {
|
||||
if ((widget.chatModel.jumpMsgID == widget.message.msgID) && (widget.message.msgID?.isNotEmpty ?? false)) {
|
||||
widget.clearJump();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final defaultStyle = widget.isFromSelf
|
||||
? (theme.chatMessageItemFromSelfBgColor ?? theme.lightPrimaryMaterialColor.shade50)
|
||||
: (theme.chatMessageItemFromOthersBgColor);
|
||||
|
||||
final backgroundColor =
|
||||
isShowJumpState ? const Color.fromRGBO(245, 166, 35, 1) : (defaultStyle ?? widget.backgroundColor);
|
||||
|
||||
return Container(
|
||||
padding: widget.textPadding ?? EdgeInsets.all(isDesktopScreen ? 12 : 10),
|
||||
decoration: BoxDecoration(
|
||||
color: backgroundColor,
|
||||
borderRadius: widget.borderRadius ?? borderRadius,
|
||||
),
|
||||
constraints: BoxConstraints(maxWidth: MediaQuery.of(context).size.width * 0.6),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// If the [elemType] is text message, it will not be null here.
|
||||
// You can render the widget from extension directly, with a [TextStyle] optionally.
|
||||
widget.chatModel.chatConfig.urlPreviewType != UrlPreviewType.none
|
||||
? textWithLink!(
|
||||
style: widget.fontStyle ??
|
||||
TextStyle(
|
||||
fontSize: isDesktopScreen ? 14 : 16,
|
||||
textBaseline: TextBaseline.ideographic,
|
||||
height: widget.chatModel.chatConfig.textHeight))
|
||||
: ExtendedText(widget.message.textElem?.text ?? "",
|
||||
softWrap: true,
|
||||
style: widget.fontStyle ??
|
||||
TextStyle(fontSize: isDesktopScreen ? 14 : 16, height: widget.chatModel.chatConfig.textHeight),
|
||||
specialTextSpanBuilder: DefaultSpecialTextSpanBuilder(
|
||||
isUseQQPackage: widget.chatModel.chatConfig.stickerPanelConfig?.useQQStickerPackage ?? true,
|
||||
isUseTencentCloudChatPackage:
|
||||
widget.chatModel.chatConfig.stickerPanelConfig?.useTencentCloudChatStickerPackage ?? true,
|
||||
isUseTencentCloudChatPackageOldKeys:
|
||||
widget.chatModel.chatConfig.stickerPanelConfig?.useTencentCloudChatStickerPackageOldKeys ??
|
||||
false,
|
||||
customEmojiStickerList: widget.customEmojiStickerList,
|
||||
showAtBackground: true,
|
||||
checkHttpLink: true,
|
||||
)),
|
||||
// If the link preview info is available, render the preview card.
|
||||
if (_renderPreviewWidget() != null &&
|
||||
widget.chatModel.chatConfig.urlPreviewType == UrlPreviewType.previewCardAndHyperlink)
|
||||
_renderPreviewWidget()!,
|
||||
if (widget.isShowMessageReaction ?? true) TIMUIKitMessageReactionShowPanel(message: widget.message)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
+190
@@ -0,0 +1,190 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:tencent_chat_i18n_tool/tencent_chat_i18n_tool.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/models/v2_tim_message.dart'
|
||||
if (dart.library.html) 'package:tencent_cloud_chat_sdk/web/compatible_models/v2_tim_message.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/utils/screen_utils.dart';
|
||||
import 'package:extended_text/extended_text.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_base.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_state.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/business_logic/separate_models/tui_chat_separate_view_model.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/tencent_cloud_chat_uikit.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/views/TIMUIKitChat/TIMUIKitTextField/special_text/DefaultSpecialTextSpanBuilder.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/widgets/link_preview/link_preview_entry.dart';
|
||||
|
||||
class TIMUIKitTextTranslationElem extends StatefulWidget {
|
||||
final V2TimMessage message;
|
||||
final bool isFromSelf;
|
||||
final bool isShowJump;
|
||||
final VoidCallback clearJump;
|
||||
final TextStyle? fontStyle;
|
||||
final BorderRadius? borderRadius;
|
||||
final Color? backgroundColor;
|
||||
final EdgeInsetsGeometry? textPadding;
|
||||
final TUIChatSeparateViewModel chatModel;
|
||||
final bool? isShowMessageReaction;
|
||||
final List<CustomEmojiFaceData> customEmojiStickerList;
|
||||
|
||||
const TIMUIKitTextTranslationElem(
|
||||
{Key? key,
|
||||
required this.message,
|
||||
required this.isFromSelf,
|
||||
required this.isShowJump,
|
||||
required this.clearJump,
|
||||
this.fontStyle,
|
||||
this.borderRadius,
|
||||
this.isShowMessageReaction,
|
||||
this.backgroundColor,
|
||||
this.textPadding,
|
||||
required this.chatModel,
|
||||
this.customEmojiStickerList = const []})
|
||||
: super(key: key);
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => _TIMUIKitTextTranslationElemState();
|
||||
}
|
||||
|
||||
class _TIMUIKitTextTranslationElemState extends TIMUIKitState<TIMUIKitTextTranslationElem> {
|
||||
bool isShowJumpState = false;
|
||||
bool isShining = false;
|
||||
|
||||
_showJumpColor() {
|
||||
if ((widget.chatModel.jumpMsgID != widget.message.msgID) && (widget.message.msgID?.isNotEmpty ?? true)) {
|
||||
return;
|
||||
}
|
||||
isShining = true;
|
||||
int shineAmount = 6;
|
||||
setState(() {
|
||||
isShowJumpState = true;
|
||||
});
|
||||
Timer.periodic(const Duration(milliseconds: 300), (timer) {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
isShowJumpState = shineAmount.isOdd ? true : false;
|
||||
});
|
||||
}
|
||||
if (shineAmount == 0 || !mounted) {
|
||||
isShining = false;
|
||||
timer.cancel();
|
||||
}
|
||||
shineAmount--;
|
||||
});
|
||||
Future.delayed(const Duration(milliseconds: 100), () {
|
||||
widget.clearJump();
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget tuiBuild(BuildContext context, TUIKitBuildValue value) {
|
||||
final theme = value.theme;
|
||||
final isDesktopScreen = TUIKitScreenUtils.getFormFactor(context) == DeviceType.Desktop;
|
||||
final borderRadius = widget.isFromSelf
|
||||
? const BorderRadius.only(
|
||||
topLeft: Radius.circular(10),
|
||||
topRight: Radius.circular(2),
|
||||
bottomLeft: Radius.circular(10),
|
||||
bottomRight: Radius.circular(10))
|
||||
: const BorderRadius.only(
|
||||
topLeft: Radius.circular(2),
|
||||
topRight: Radius.circular(10),
|
||||
bottomLeft: Radius.circular(10),
|
||||
bottomRight: Radius.circular(10));
|
||||
if ((widget.chatModel.jumpMsgID == widget.message.msgID)) {}
|
||||
if (widget.isShowJump) {
|
||||
if (!isShining) {
|
||||
Future.delayed(Duration.zero, () {
|
||||
_showJumpColor();
|
||||
});
|
||||
} else {
|
||||
if ((widget.chatModel.jumpMsgID == widget.message.msgID) && (widget.message.msgID?.isNotEmpty ?? false)) {
|
||||
widget.clearJump();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final defaultStyle = widget.isFromSelf
|
||||
? (theme.chatMessageItemFromSelfBgColor ?? theme.lightPrimaryMaterialColor.shade50)
|
||||
: (theme.chatMessageItemFromOthersBgColor);
|
||||
|
||||
final backgroundColor =
|
||||
isShowJumpState ? const Color.fromRGBO(245, 166, 35, 1) : (defaultStyle ?? widget.backgroundColor);
|
||||
|
||||
final LocalCustomDataModel localCustomData =
|
||||
LocalCustomDataModel.fromMap(json.decode(TencentUtils.checkString(widget.message.localCustomData) ?? "{}"));
|
||||
final String? translateText = localCustomData.translatedText;
|
||||
|
||||
final textWithLink = LinkPreviewEntry.getHyperlinksText(
|
||||
translateText ?? "", widget.chatModel.chatConfig.isSupportMarkdownForTextMessage,
|
||||
onLinkTap: widget.chatModel.chatConfig.onTapLink,
|
||||
isUseQQPackage: widget.chatModel.chatConfig.stickerPanelConfig?.useQQStickerPackage ?? true,
|
||||
isUseTencentCloudChatPackage:
|
||||
widget.chatModel.chatConfig.stickerPanelConfig?.useTencentCloudChatStickerPackage ?? true,
|
||||
isUseTencentCloudChatPackageOldKeys:
|
||||
widget.chatModel.chatConfig.stickerPanelConfig?.useTencentCloudChatStickerPackageOldKeys ?? false,
|
||||
customEmojiStickerList: widget.customEmojiStickerList,
|
||||
isEnableTextSelection: widget.chatModel.chatConfig.isEnableTextSelection ?? false);
|
||||
|
||||
return TencentUtils.checkString(translateText) != null
|
||||
? Container(
|
||||
margin: const EdgeInsets.only(top: 6),
|
||||
padding: widget.textPadding ?? EdgeInsets.all(isDesktopScreen ? 12 : 10),
|
||||
decoration: BoxDecoration(
|
||||
color: backgroundColor,
|
||||
borderRadius: widget.borderRadius ?? borderRadius,
|
||||
),
|
||||
constraints: BoxConstraints(maxWidth: MediaQuery.of(context).size.width * 0.6),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// If the [elemType] is text message, it will not be null here.
|
||||
// You can render the widget from extension directly, with a [TextStyle] optionally.
|
||||
widget.chatModel.chatConfig.urlPreviewType != UrlPreviewType.none
|
||||
? textWithLink!(
|
||||
style: widget.fontStyle ??
|
||||
TextStyle(
|
||||
fontSize: isDesktopScreen ? 14 : 16,
|
||||
textBaseline: TextBaseline.ideographic,
|
||||
height: widget.chatModel.chatConfig.textHeight))
|
||||
: ExtendedText(translateText!,
|
||||
softWrap: true,
|
||||
style: widget.fontStyle ??
|
||||
TextStyle(
|
||||
fontSize: isDesktopScreen ? 14 : 16, height: widget.chatModel.chatConfig.textHeight),
|
||||
specialTextSpanBuilder: DefaultSpecialTextSpanBuilder(
|
||||
isUseQQPackage: widget.chatModel.chatConfig.stickerPanelConfig?.useQQStickerPackage ?? true,
|
||||
isUseTencentCloudChatPackage:
|
||||
widget.chatModel.chatConfig.stickerPanelConfig?.useTencentCloudChatStickerPackage ?? true,
|
||||
isUseTencentCloudChatPackageOldKeys: widget
|
||||
.chatModel.chatConfig.stickerPanelConfig?.useTencentCloudChatStickerPackageOldKeys ??
|
||||
false,
|
||||
customEmojiStickerList: widget.customEmojiStickerList,
|
||||
showAtBackground: true,
|
||||
)),
|
||||
const SizedBox(height: 6),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.check_circle,
|
||||
color: Color(0x72282c34),
|
||||
size: 12,
|
||||
),
|
||||
const SizedBox(
|
||||
width: 4,
|
||||
),
|
||||
Text(
|
||||
TIM_t("翻译完成"),
|
||||
style: const TextStyle(color: Color(0x72282c34), fontSize: 10),
|
||||
)
|
||||
],
|
||||
)
|
||||
],
|
||||
),
|
||||
)
|
||||
: const SizedBox(width: 0, height: 0);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,285 @@
|
||||
import 'dart:io';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:loading_animation_widget/loading_animation_widget.dart';
|
||||
import 'package:open_file/open_file.dart';
|
||||
import 'package:tencent_chat_i18n_tool/tencent_chat_i18n_tool.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/enum/message_status.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/models/v2_tim_message.dart'
|
||||
if (dart.library.html) 'package:tencent_cloud_chat_sdk/web/compatible_models/v2_tim_message.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/models/v2_tim_video_elem.dart'
|
||||
if (dart.library.html) 'package:tencent_cloud_chat_sdk/web/compatible_models/v2_tim_video_elem.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_base.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_state.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/tencent_cloud_chat_uikit.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/base_widgets/tim_callback.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/business_logic/separate_models/tui_chat_separate_view_model.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/data_services/message/message_services.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/data_services/services_locatar.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/theme/tui_theme.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/utils/message.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/utils/platform.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/views/TIMUIKitChat/TIMUIKitMessageItem/TIMUIKitMessageReaction/tim_uikit_message_reaction_wrapper.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/widgets/video_screen.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/widgets/wide_popup.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
class TIMUIKitVideoElem extends StatefulWidget {
|
||||
final V2TimMessage message;
|
||||
final bool isShowJump;
|
||||
final VoidCallback? clearJump;
|
||||
final String? isFrom;
|
||||
final TUIChatSeparateViewModel chatModel;
|
||||
final bool? isShowMessageReaction;
|
||||
|
||||
const TIMUIKitVideoElem(this.message,
|
||||
{Key? key,
|
||||
this.isShowJump = false,
|
||||
this.clearJump,
|
||||
this.isFrom,
|
||||
this.isShowMessageReaction,
|
||||
required this.chatModel})
|
||||
: super(key: key);
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => _TIMUIKitVideoElemState();
|
||||
}
|
||||
|
||||
class _TIMUIKitVideoElemState extends TIMUIKitState<TIMUIKitVideoElem> {
|
||||
final MessageService _messageService = serviceLocator<MessageService>();
|
||||
late V2TimVideoElem stateElement = widget.message.videoElem!;
|
||||
|
||||
Widget errorDisplay(TUITheme? theme) {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(
|
||||
width: 1,
|
||||
color: Colors.black12,
|
||||
)),
|
||||
height: 100,
|
||||
child: Center(
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.warning_amber_outlined,
|
||||
color: theme?.cautionColor,
|
||||
size: 16,
|
||||
),
|
||||
Text(
|
||||
TIM_t("视频加载失败"),
|
||||
style: TextStyle(color: theme?.cautionColor),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget generateSnapshot(TUITheme theme, int height) {
|
||||
if (!PlatformUtils().isWeb) {
|
||||
final current = (DateTime.now().millisecondsSinceEpoch / 1000).ceil();
|
||||
final timeStamp = widget.message.timestamp ?? current;
|
||||
if (current - timeStamp < 300) {
|
||||
if (stateElement.snapshotPath != null && stateElement.snapshotPath != '') {
|
||||
File imgF = File(stateElement.snapshotPath!);
|
||||
bool isExist = imgF.existsSync();
|
||||
if (isExist) {
|
||||
return Image.file(File(stateElement.snapshotPath!), fit: BoxFit.fitWidth);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ((stateElement.snapshotUrl == null || stateElement.snapshotUrl == '') &&
|
||||
(stateElement.snapshotPath == null || stateElement.snapshotPath == '')) {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(5)),
|
||||
border: Border.all(
|
||||
width: 1,
|
||||
color: Colors.black12,
|
||||
)),
|
||||
height: double.parse(height.toString()),
|
||||
child: Center(
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
LoadingAnimationWidget.staggeredDotsWave(
|
||||
color: theme.weakTextColor ?? Colors.grey,
|
||||
size: 28,
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
return (!PlatformUtils().isWeb && stateElement.snapshotUrl == null ||
|
||||
widget.message.status == MessageStatus.V2TIM_MSG_STATUS_SENDING)
|
||||
? (stateElement.snapshotPath!.isNotEmpty
|
||||
? Image.file(File(stateElement.snapshotPath!), fit: BoxFit.fitWidth)
|
||||
: Image.file(File(stateElement.localSnapshotUrl!), fit: BoxFit.fitWidth))
|
||||
: (PlatformUtils().isWeb || stateElement.localSnapshotUrl == null || stateElement.localSnapshotUrl == "")
|
||||
? Image.network(stateElement.snapshotUrl!, fit: BoxFit.fitWidth)
|
||||
: Image.file(File(stateElement.localSnapshotUrl!), fit: BoxFit.fitWidth);
|
||||
}
|
||||
|
||||
downloadMessageDetailAndSave() async {
|
||||
if (TencentUtils.checkString(widget.message.msgID) != null) {
|
||||
if (TencentUtils.checkString(widget.message.videoElem!.videoUrl) == null) {
|
||||
final response = await _messageService.getMessageOnlineUrl(msgID: widget.message.msgID!);
|
||||
if (response.data != null) {
|
||||
widget.message.videoElem = response.data!.videoElem;
|
||||
Future.delayed(const Duration(microseconds: 10), () {
|
||||
setState(() => stateElement = response.data!.videoElem!);
|
||||
});
|
||||
}
|
||||
}
|
||||
if (!PlatformUtils().isWeb) {
|
||||
if (TencentUtils.checkString(widget.message.videoElem!.localVideoUrl) == null ||
|
||||
!File(widget.message.videoElem!.localVideoUrl!).existsSync()) {
|
||||
_messageService.downloadMessage(
|
||||
msgID: widget.message.msgID!, messageType: 5, imageType: 0, isSnapshot: false);
|
||||
}
|
||||
if (TencentUtils.checkString(widget.message.videoElem!.localSnapshotUrl) == null ||
|
||||
!File(widget.message.videoElem!.localSnapshotUrl!).existsSync()) {
|
||||
_messageService.downloadMessage(msgID: widget.message.msgID!, messageType: 5, imageType: 0, isSnapshot: true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
downloadMessageDetailAndSave();
|
||||
}
|
||||
|
||||
void launchDesktopFile(String path) {
|
||||
if (PlatformUtils().isWindows) {
|
||||
OpenFile.open(path);
|
||||
} else {
|
||||
launchUrl(Uri.file(path));
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget tuiBuild(BuildContext context, TUIKitBuildValue value) {
|
||||
final theme = value.theme;
|
||||
final heroTag =
|
||||
"${widget.message.msgID ?? widget.message.id ?? widget.message.timestamp ?? DateTime.now().millisecondsSinceEpoch}${widget.isFrom}";
|
||||
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
if (PlatformUtils().isWeb) {
|
||||
final url = widget.message.videoElem?.videoUrl ?? widget.message.videoElem?.videoPath ?? "";
|
||||
TUIKitWidePopup.showMedia(
|
||||
context: context,
|
||||
mediaURL: url,
|
||||
onClickOrigin: () => launchUrl(
|
||||
Uri.parse(url),
|
||||
mode: LaunchMode.externalApplication,
|
||||
));
|
||||
return;
|
||||
}
|
||||
if (PlatformUtils().isDesktop) {
|
||||
final videoElem = widget.message.videoElem;
|
||||
if (videoElem != null) {
|
||||
final localVideoUrl = TencentUtils.checkString(videoElem.localVideoUrl);
|
||||
final videoPath = TencentUtils.checkString(videoElem.videoPath);
|
||||
final videoUrl = videoElem.videoUrl;
|
||||
if (localVideoUrl != null) {
|
||||
launchDesktopFile(localVideoUrl);
|
||||
// todo
|
||||
// TUIKitWidePopup.showMedia(
|
||||
// context: context,
|
||||
// mediaPath: localVideoUrl,
|
||||
// onClickOrigin: () => launchDesktopFile(localVideoUrl));
|
||||
} else if (videoPath != null && File(videoPath).existsSync()) {
|
||||
launchDesktopFile(videoPath);
|
||||
// todo
|
||||
// TUIKitWidePopup.showMedia(
|
||||
// context: context,
|
||||
// mediaPath: videoPath,
|
||||
// onClickOrigin: () => launchDesktopFile(videoPath));
|
||||
} else if (TencentUtils.isTextNotEmpty(videoUrl)) {
|
||||
onTIMCallback(
|
||||
TIMCallback(infoCode: 6660414, infoRecommendText: TIM_t("正在下载中"), type: TIMCallbackType.INFO));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Navigator.of(context).push(
|
||||
PageRouteBuilder(
|
||||
opaque: false, // set to false
|
||||
pageBuilder: (_, __, ___) => VideoScreen(
|
||||
message: widget.message,
|
||||
heroTag: heroTag,
|
||||
videoElement: stateElement,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
child: Hero(
|
||||
tag: heroTag,
|
||||
child: TIMUIKitMessageReactionWrapper(
|
||||
chatModel: widget.chatModel,
|
||||
message: widget.message,
|
||||
isShowJump: widget.isShowJump,
|
||||
isShowMessageReaction: widget.isShowMessageReaction ?? true,
|
||||
clearJump: widget.clearJump,
|
||||
isFromSelf: widget.message.isSelf ?? true,
|
||||
child: ClipRRect(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(5)),
|
||||
child: LayoutBuilder(builder: (BuildContext context, BoxConstraints constraints) {
|
||||
double? positionRadio;
|
||||
if ((stateElement.snapshotWidth) != null &&
|
||||
stateElement.snapshotHeight != null &&
|
||||
stateElement.snapshotWidth != 0 &&
|
||||
stateElement.snapshotHeight != 0) {
|
||||
positionRadio = (stateElement.snapshotWidth! / stateElement.snapshotHeight!);
|
||||
}
|
||||
return ConstrainedBox(
|
||||
constraints: BoxConstraints(
|
||||
maxWidth: PlatformUtils().isWeb ? 300 : constraints.maxWidth * 0.5,
|
||||
maxHeight: min(constraints.maxHeight * 0.8, 300),
|
||||
minHeight: 20,
|
||||
minWidth: 20),
|
||||
child: Stack(
|
||||
children: <Widget>[
|
||||
if (positionRadio != null &&
|
||||
(stateElement.snapshotUrl != null || stateElement.snapshotUrl != null))
|
||||
AspectRatio(
|
||||
aspectRatio: positionRadio,
|
||||
child: Container(
|
||||
decoration: const BoxDecoration(color: Colors.transparent),
|
||||
),
|
||||
),
|
||||
Row(
|
||||
children: [Expanded(child: generateSnapshot(theme, stateElement.snapshotHeight ?? 100))],
|
||||
),
|
||||
if (widget.message.status != MessageStatus.V2TIM_MSG_STATUS_SENDING &&
|
||||
(stateElement.snapshotUrl != null || stateElement.snapshotPath != null) &&
|
||||
stateElement.videoPath != null ||
|
||||
stateElement.videoUrl != null)
|
||||
Positioned.fill(
|
||||
// alignment: Alignment.center,
|
||||
child: Center(
|
||||
child:
|
||||
Image.asset('images/play.png', package: 'tencent_cloud_chat_uikit', height: 64)),
|
||||
),
|
||||
if (widget.message.videoElem?.duration != null && widget.message.videoElem!.duration! > 0)
|
||||
Positioned(
|
||||
right: 10,
|
||||
bottom: 10,
|
||||
child: Text(
|
||||
MessageUtils.formatVideoTime(widget.message.videoElem!.duration!).toString(),
|
||||
style: const TextStyle(color: Colors.white, fontSize: 12))),
|
||||
],
|
||||
));
|
||||
}),
|
||||
))),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,209 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:better_player_plus/better_player_plus.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/enum/message_elem_type.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/models/v2_tim_message.dart'
|
||||
if (dart.library.html) 'package:tencent_cloud_chat_sdk/web/compatible_models/v2_tim_message.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/models/v2_tim_message_online_url.dart'
|
||||
if (dart.library.html) 'package:tencent_cloud_chat_sdk/web/compatible_models/v2_tim_message_online_url.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/models/v2_tim_value_callback.dart'
|
||||
if (dart.library.html) 'package:tencent_cloud_chat_sdk/web/compatible_models/v2_tim_value_callback.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/tencent_im_sdk_plugin.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/tencent_cloud_chat_uikit.dart';
|
||||
|
||||
class TIMUIKitVideoPlayer extends StatefulWidget {
|
||||
final V2TimMessage message;
|
||||
final bool controller;
|
||||
final bool isSending;
|
||||
|
||||
const TIMUIKitVideoPlayer({
|
||||
super.key,
|
||||
required this.message,
|
||||
required this.controller,
|
||||
required this.isSending,
|
||||
});
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => TIMUIKitVideoPlayerState();
|
||||
}
|
||||
|
||||
enum CurrentVideoType {
|
||||
online,
|
||||
local,
|
||||
}
|
||||
|
||||
class CurrentVideoInfo {
|
||||
final String path;
|
||||
final CurrentVideoType type;
|
||||
final double aspectRatio;
|
||||
|
||||
CurrentVideoInfo({
|
||||
required this.path,
|
||||
required this.type,
|
||||
required this.aspectRatio,
|
||||
});
|
||||
}
|
||||
|
||||
class TIMUIKitVideoPlayerState extends State<TIMUIKitVideoPlayer> {
|
||||
final String _tag = "TencentCloudChatMessageVideoPlayer";
|
||||
|
||||
BetterPlayerController? _betterPlayerController;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_initializePlayer();
|
||||
}
|
||||
|
||||
Future<void> _initializePlayer() async {
|
||||
try {
|
||||
final info = await getMessageInfo();
|
||||
if (info != null && mounted) {
|
||||
BetterPlayerDataSource dataSource;
|
||||
if (info.type == CurrentVideoType.online) {
|
||||
dataSource = BetterPlayerDataSource(
|
||||
BetterPlayerDataSourceType.network,
|
||||
info.path,
|
||||
);
|
||||
} else {
|
||||
dataSource = BetterPlayerDataSource(
|
||||
BetterPlayerDataSourceType.file,
|
||||
info.path,
|
||||
);
|
||||
}
|
||||
|
||||
final betterPlayerConfiguration = BetterPlayerConfiguration(
|
||||
aspectRatio: info.aspectRatio,
|
||||
fit: BoxFit.contain,
|
||||
autoPlay: true,
|
||||
allowedScreenSleep: false,
|
||||
fullScreenByDefault: false,
|
||||
controlsConfiguration: const BetterPlayerControlsConfiguration(
|
||||
enableFullscreen: false,
|
||||
enablePlayPause: true,
|
||||
enableProgressBar: true,
|
||||
enableProgressText: true,
|
||||
showControlsOnInitialize: false,
|
||||
enableMute: false,
|
||||
enableOverflowMenu: false,
|
||||
enableSkips: false,
|
||||
),
|
||||
);
|
||||
|
||||
_betterPlayerController = BetterPlayerController(
|
||||
betterPlayerConfiguration,
|
||||
betterPlayerDataSource: dataSource,
|
||||
);
|
||||
|
||||
if (mounted) {
|
||||
setState(() {});
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint("Video initialization error: $e");
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_betterPlayerController?.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<CurrentVideoInfo?> getMessageInfo() async {
|
||||
if (widget.message.elemType == MessageElemType.V2TIM_ELEM_TYPE_VIDEO) {
|
||||
double aspectRatio = (9 / 16);
|
||||
|
||||
if (widget.isSending) {
|
||||
var lp = widget.message.videoElem!.videoPath ?? "";
|
||||
if (lp.isNotEmpty) {
|
||||
console("view sending message video path");
|
||||
if (File(lp).existsSync() && !kIsWeb) {
|
||||
return CurrentVideoInfo(path: lp, type: CurrentVideoType.local, aspectRatio: aspectRatio);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (widget.message.videoElem!.snapshotWidth != null && widget.message.videoElem!.snapshotHeight != null) {
|
||||
if (widget.message.videoElem!.snapshotHeight != 0) {
|
||||
aspectRatio = (widget.message.videoElem!.snapshotWidth!) / (widget.message.videoElem!.snapshotHeight!);
|
||||
}
|
||||
}
|
||||
|
||||
if (TencentUtils.checkString(widget.message.videoElem!.videoPath) != null) {
|
||||
// 先查本地发送的视频地址
|
||||
if (File(widget.message.videoElem!.videoPath!).existsSync()) {
|
||||
console("video: local video path exists");
|
||||
return CurrentVideoInfo(
|
||||
path: widget.message.videoElem!.videoPath!, type: CurrentVideoType.local, aspectRatio: aspectRatio);
|
||||
}
|
||||
} else if (TencentUtils.checkString(widget.message.videoElem!.localVideoUrl) != null) {
|
||||
// 再查本地下载的视频地址
|
||||
if (File(widget.message.videoElem!.localVideoUrl!).existsSync()) {
|
||||
console("video: local url exists");
|
||||
return CurrentVideoInfo(
|
||||
path: widget.message.videoElem!.localVideoUrl!, type: CurrentVideoType.local, aspectRatio: aspectRatio);
|
||||
}
|
||||
} else {
|
||||
// 最后再查在线地址(todo 使用 getMessageOnlineUrl 查询)
|
||||
if (widget.message.videoElem != null) {
|
||||
if (widget.message.videoElem!.snapshotUrl != null) {
|
||||
console("video: online url ${widget.message.videoElem!.videoUrl}");
|
||||
return CurrentVideoInfo(
|
||||
path: widget.message.videoElem!.videoUrl!,
|
||||
type: CurrentVideoType.online,
|
||||
aspectRatio: aspectRatio,
|
||||
);
|
||||
}
|
||||
}
|
||||
if (!kIsWeb) {
|
||||
V2TimValueCallback<V2TimMessageOnlineUrl> urlres = await TencentImSDKPlugin.v2TIMManager
|
||||
.getMessageManager()
|
||||
.getMessageOnlineUrl(msgID: widget.message.msgID ?? "");
|
||||
if (urlres.data != null) {
|
||||
if (urlres.data?.videoElem != null) {
|
||||
if (TencentUtils.checkString(urlres.data?.videoElem?.videoUrl) != null) {
|
||||
console("view video online url ${urlres.data?.videoElem?.videoUrl}");
|
||||
return CurrentVideoInfo(
|
||||
path: urlres.data!.videoElem!.videoUrl!, type: CurrentVideoType.online, aspectRatio: aspectRatio);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
console("The component received a non-video message parameter. please check");
|
||||
}
|
||||
console("has no view video source. please check");
|
||||
return null;
|
||||
}
|
||||
|
||||
console(String log) {
|
||||
print("$_tag, $log");
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (widget.message.hasRiskContent == true) {
|
||||
return const Center(
|
||||
child: Text(
|
||||
"Risk Video",
|
||||
style: TextStyle(color: Colors.white),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (_betterPlayerController == null) {
|
||||
return Container();
|
||||
}
|
||||
|
||||
return AspectRatio(
|
||||
aspectRatio: _betterPlayerController!.videoPlayerController?.value.aspectRatio ?? 9 / 16,
|
||||
child: BetterPlayer(
|
||||
controller: _betterPlayerController!,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,222 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:tencent_chat_i18n_tool/tencent_chat_i18n_tool.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/models/v2_tim_merger_elem.dart'
|
||||
if (dart.library.html) 'package:tencent_cloud_chat_sdk/web/compatible_models/v2_tim_merger_elem.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/models/v2_tim_message.dart'
|
||||
if (dart.library.html) 'package:tencent_cloud_chat_sdk/web/compatible_models/v2_tim_message.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_state.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/business_logic/separate_models/tui_chat_separate_view_model.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/data_services/core/tim_uikit_wide_modal_operation_key.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/utils/screen_utils.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/views/TIMUIKitChat/TIMUIKItMessageList/tim_uikit_chat_history_message_list_item.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/widgets/merger_message_screen.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/base_widgets/tim_callback.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_base.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/ui/widgets/wide_popup.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/theme/color.dart';
|
||||
import 'package:tencent_cloud_chat_uikit/theme/tui_theme.dart';
|
||||
import 'TIMUIKitMessageReaction/tim_uikit_message_reaction_show_panel.dart';
|
||||
|
||||
class TIMUIKitMergerElem extends StatefulWidget {
|
||||
final V2TimMergerElem mergerElem;
|
||||
final String messageID;
|
||||
final bool isSelf;
|
||||
final bool isShowJump;
|
||||
final VoidCallback? clearJump;
|
||||
final V2TimMessage message;
|
||||
final bool? isShowMessageReaction;
|
||||
final TUIChatSeparateViewModel model;
|
||||
final MessageItemBuilder? messageItemBuilder;
|
||||
|
||||
const TIMUIKitMergerElem(
|
||||
{Key? key,
|
||||
required this.message,
|
||||
required this.model,
|
||||
required this.mergerElem,
|
||||
required this.isSelf,
|
||||
this.isShowMessageReaction,
|
||||
required this.messageID,
|
||||
required this.isShowJump,
|
||||
this.clearJump,
|
||||
this.messageItemBuilder})
|
||||
: super(key: key);
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => TIMUIKitMergerElemState();
|
||||
}
|
||||
|
||||
class TIMUIKitMergerElemState extends TIMUIKitState<TIMUIKitMergerElem> {
|
||||
bool isShowJumpState = false;
|
||||
late ScrollController _scrollController;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_scrollController = ScrollController();
|
||||
}
|
||||
|
||||
_showJumpColor() {
|
||||
int shineAmount = 6;
|
||||
setState(() {
|
||||
isShowJumpState = true;
|
||||
});
|
||||
Future.delayed(const Duration(milliseconds: 100), () {
|
||||
if (widget.clearJump != null) {
|
||||
widget.clearJump!();
|
||||
}
|
||||
});
|
||||
Timer.periodic(const Duration(milliseconds: 300), (timer) {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
isShowJumpState = shineAmount.isOdd ? true : false;
|
||||
});
|
||||
}
|
||||
if (shineAmount == 0 || !mounted) {
|
||||
timer.cancel();
|
||||
}
|
||||
shineAmount--;
|
||||
});
|
||||
}
|
||||
|
||||
_handleTap(BuildContext context, TUIChatSeparateViewModel model) async {
|
||||
try {
|
||||
if (widget.messageID != "") {
|
||||
final isDesktopScreen = TUIKitScreenUtils.getFormFactor(context) == DeviceType.Desktop;
|
||||
|
||||
if (isDesktopScreen) {
|
||||
TUIKitWidePopup.showPopupWindow(
|
||||
operationKey: TUIKitWideModalOperationKey.mergerMessageList,
|
||||
context: context,
|
||||
width: MediaQuery.of(context).size.width * 0.7,
|
||||
title: TIM_t("聊天记录"),
|
||||
height: MediaQuery.of(context).size.height * 0.7,
|
||||
child: (onClose) => Scrollbar(
|
||||
controller: _scrollController,
|
||||
child: MergerMessageScreen(
|
||||
messageItemBuilder: widget.messageItemBuilder, model: model, msgID: widget.messageID),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => MergerMessageScreen(
|
||||
messageItemBuilder: widget.messageItemBuilder, model: model, msgID: widget.messageID),
|
||||
));
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
onTIMCallback(TIMCallback(type: TIMCallbackType.INFO, infoRecommendText: TIM_t("无法定位到原消息"), infoCode: 6660401));
|
||||
}
|
||||
}
|
||||
|
||||
List<String>? _getAbstractList() {
|
||||
final length = widget.mergerElem.abstractList!.length;
|
||||
if (length <= 4) {
|
||||
return widget.mergerElem.abstractList;
|
||||
}
|
||||
return widget.mergerElem.abstractList!.getRange(0, 4).toList();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget tuiBuild(BuildContext context, TUIKitBuildValue value) {
|
||||
final TUITheme theme = value.theme;
|
||||
if (widget.isShowJump) {
|
||||
Future.delayed(Duration.zero, () {
|
||||
_showJumpColor();
|
||||
});
|
||||
}
|
||||
final isDesktopScreen = TUIKitScreenUtils.getFormFactor(context) == DeviceType.Desktop;
|
||||
return Container(
|
||||
constraints: BoxConstraints(maxWidth: MediaQuery.of(context).size.width * (isDesktopScreen ? 0.3 : 0.6)),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.only(
|
||||
topLeft: widget.isSelf ? const Radius.circular(10) : Radius.zero,
|
||||
bottomLeft: const Radius.circular(10),
|
||||
topRight: widget.isSelf ? Radius.zero : const Radius.circular(10),
|
||||
bottomRight: const Radius.circular(10),
|
||||
),
|
||||
border: Border.all(
|
||||
color: isShowJumpState
|
||||
? const Color.fromRGBO(245, 166, 35, 1)
|
||||
: (theme.weakDividerColor ?? CommonColor.weakDividerColor),
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
_handleTap(context, widget.model);
|
||||
},
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
widget.mergerElem.title!,
|
||||
softWrap: true,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
textAlign: TextAlign.left,
|
||||
maxLines: 1,
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
color: Colors.black,
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
const SizedBox(
|
||||
height: 4,
|
||||
),
|
||||
// const Divider(),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: _getAbstractList()!
|
||||
.map(
|
||||
(e) => Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
e,
|
||||
textAlign: TextAlign.left,
|
||||
softWrap: true,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
maxLines: 2,
|
||||
style: TextStyle(
|
||||
color: theme.weakTextColor,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 4,
|
||||
),
|
||||
const Divider(),
|
||||
Text(
|
||||
TIM_t("聊天记录"),
|
||||
style: TextStyle(
|
||||
color: theme.weakTextColor,
|
||||
fontSize: 10,
|
||||
),
|
||||
),
|
||||
if (widget.isShowMessageReaction ?? true) TIMUIKitMessageReactionShowPanel(message: widget.message)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user