Files
tongxunruanjian/mobile-next/lib/pages/chat/chat_view.dart
T
编码工程师andmultica-agent 8046ec7483 feat(mobile-next): 导入官方 openim-flutter-demo 固定基线 3.8.3-patch.3 并完成双构建
- 上游: github.com/OpenIMSDK/openim-flutter-demo tag 3.8.3-patch.3
  commit b3dfdb1e8aaeaaf6f0793e10cadd20d5c184a31f
- 并行目录 mobile-next/ 原样导入(含 LICENSE),不覆盖现网 mobile/
- 锁定 Flutter 3.24.5 / Dart 3.5.4 / JDK 17 / Gradle 7.6.3 / AGP 7.3.1
- 实测 Android debug 与 release 构建均成功(见 docs/mobile-next-baseline-import.md)
- 本提交可单独回退:git revert 1db1229(重写前)

Co-authored-by: multica-agent <github@multica.ai>
2026-08-19 15:24:33 +08:00

274 lines
9.9 KiB
Dart

import 'package:flutter/material.dart';
import 'package:flutter_openim_sdk/flutter_openim_sdk.dart';
import 'package:get/get.dart';
import 'package:openim_common/openim_common.dart';
import '../../widgets/file_download_progress.dart';
import 'chat_logic.dart';
class ChatPage extends StatelessWidget {
final logic = Get.find<ChatLogic>(tag: GetTags.chat);
ChatPage({super.key});
Widget _buildItemView(Message message) => ChatItemView(
key: logic.itemKey(message),
message: message,
textScaleFactor: logic.scaleFactor.value,
allAtMap: logic.getAtMapping(message),
timelineStr: logic.getShowTime(message),
sendStatusSubject: logic.sendStatusSub,
closePopMenuSubject: logic.forceCloseMenuSub,
enabledReadStatus: logic.enabledReadStatus(message),
readingDuration: logic.readTime(message),
isPlayingSound: logic.isPlaySound(message),
showLongPressMenu: !logic.isInvalidGroup,
leftNickname: logic.getNewestNickname(message),
leftFaceUrl: logic.getNewestFaceURL(message),
rightNickname: logic.senderName,
rightFaceUrl: OpenIM.iMManager.userInfo.faceURL,
showLeftNickname: !logic.isSingleChat,
showRightNickname: !logic.isSingleChat,
enabledCopyMenu: logic.showCopyMenu(message),
enabledRevokeMenu: logic.showRevokeMenu(message),
enabledReplyMenu: logic.showReplyMenu(message),
enabledForwardMenu: logic.showForwardMenu(message),
enabledDelMenu: logic.showDelMenu(message),
enabledAddEmojiMenu: logic.showAddEmojiMenu(message),
onFailedToResend: () => logic.failedResend(message),
onPopMenuShowChanged: logic.onPopMenuShowChanged,
onClickItemView: () => logic.parseClickEvent(message),
onTapCopyMenu: () => logic.copy(message),
onTapDelMenu: () => logic.deleteMsg(message),
onTapForwardMenu: () => logic.forward(message),
onTapRevokeMenu: () {
logic.markRevokedMessage(message);
logic.revokeMsgV2(message);
},
onTapAddEmojiMenu: () => logic.addEmoji(message),
visibilityChange: (msg, visible) {
logic.markMessageAsRead(message, visible);
},
onLongPressLeftAvatar: () {
logic.onLongPressLeftAvatar(message);
},
onLongPressRightAvatar: () {},
onTapLeftAvatar: () {
logic.onTapLeftAvatar(message);
},
onVisibleTrulyText: (text) {
logic.copyTextMap[message.clientMsgID] = text;
},
customTypeBuilder: _buildCustomTypeItemView,
fileDownloadProgressView: FileDownloadProgressView(message),
patterns: <MatchPattern>[
MatchPattern(
type: PatternType.email,
onTap: logic.clickLinkText,
),
MatchPattern(
type: PatternType.url,
onTap: logic.clickLinkText,
),
MatchPattern(
type: PatternType.mobile,
onTap: logic.clickLinkText,
),
MatchPattern(
type: PatternType.tel,
onTap: logic.clickLinkText,
),
],
mediaItemBuilder: (context, message) {
return _buildMediaItem(context, message);
},
onTapUserProfile: handleUserProfileTap,
);
void handleUserProfileTap(({String userID, String name, String? faceURL, String? groupID}) userProfile) {
final userInfo = UserInfo(userID: userProfile.userID, nickname: userProfile.name, faceURL: userProfile.faceURL);
logic.viewUserInfo(userInfo);
}
Widget? _buildMediaItem(BuildContext context, Message message) {
if (message.contentType != MessageType.picture && message.contentType != MessageType.video) {
return null;
}
return GestureDetector(
onTap: () async {
try {
logic.stopVoice();
IMUtils.previewMediaFile(
context: context,
message: message,
onAutoPlay: (index) {
return !logic.playOnce;
},
muted: logic.rtcIsBusy,
onPageChanged: (index) {
logic.playOnce = true;
},
onOperate: (type) {
if (type == OperateType.forward) {
logic.forward(message);
}
}).then((value) {
logic.playOnce = false;
});
} catch (e) {
IMViews.showToast(e.toString());
}
},
child: Hero(
tag: message.clientMsgID!,
child: _buildMediaContent(message),
placeholderBuilder: (BuildContext context, Size heroSize, Widget child) => child,
),
);
}
Widget _buildMediaContent(Message message) {
final isOutgoing = message.sendID == OpenIM.iMManager.userID;
if (message.isVideoType) {
return ChatVideoView(
isISend: isOutgoing,
message: message,
);
} else {
return ChatPictureView(
isISend: isOutgoing,
message: message,
);
}
}
CustomTypeInfo? _buildCustomTypeItemView(_, Message message) {
final data = IMUtils.parseCustomMessage(message);
if (null != data) {
final viewType = data['viewType'];
if (viewType == CustomMessageType.call) {
final type = data['type'];
final content = data['content'];
final view = ChatCallItemView(type: type, content: content);
return CustomTypeInfo(view);
} else if (viewType == CustomMessageType.deletedByFriend || viewType == CustomMessageType.blockedByFriend) {
final view = ChatFriendRelationshipAbnormalHintView(
name: logic.nickname.value,
onTap: logic.sendFriendVerification,
blockedByFriend: viewType == CustomMessageType.blockedByFriend,
deletedByFriend: viewType == CustomMessageType.deletedByFriend,
);
return CustomTypeInfo(view, false, false);
} else if (viewType == CustomMessageType.removedFromGroup) {
return CustomTypeInfo(
StrRes.removedFromGroupHint.toText..style = Styles.ts_8E9AB0_12sp,
false,
false,
);
} else if (viewType == CustomMessageType.groupDisbanded) {
return CustomTypeInfo(
StrRes.groupDisbanded.toText..style = Styles.ts_8E9AB0_12sp,
false,
false,
);
} else if (viewType == CustomMessageType.tag) {
final isISend = message.sendID == OpenIM.iMManager.userID;
if (null != data['textElem']) {
final textElem = TextElem.fromJson(data['textElem']);
return CustomTypeInfo(
ChatText(
text: textElem.content ?? '',
textScaleFactor: logic.scaleFactor.value,
model: TextModel.normal,
),
);
} else if (null != data['soundElem']) {
final soundElem = SoundElem.fromJson(data['soundElem']);
return CustomTypeInfo(
ChatVoiceView(
isISend: isISend,
soundPath: soundElem.soundPath,
soundUrl: soundElem.sourceUrl,
duration: soundElem.duration,
isPlaying: logic.isPlaySound(message),
),
);
}
}
}
return null;
}
Widget? get _groupCallHintView => null;
@override
Widget build(BuildContext context) {
return WillPopScope(
onWillPop: logic.willPop(),
child: ChatVoiceRecordLayout(
onCompleted: logic.sendVoice,
builder: (bar) => Obx(() {
return Scaffold(
backgroundColor: Styles.c_F0F2F6,
appBar: TitleBar.chat(
title: logic.nickname.value,
member: logic.memberStr,
subTitle: logic.subTile,
showOnlineStatus: logic.showOnlineStatus(),
isOnline: logic.onlineStatus.value,
onCloseMultiModel: logic.exit,
onClickMoreBtn: logic.chatSetup,
onClickCallBtn: logic.call,
),
body: SafeArea(
child: WaterMarkBgView(
text: '',
path: logic.background.value,
backgroundColor: Styles.c_FFFFFF,
floatView: _groupCallHintView,
bottomView: ChatInputBox(
forceCloseToolboxSub: logic.forceCloseToolbox,
controller: logic.inputCtrl,
focusNode: logic.focusNode,
isNotInGroup: logic.isInvalidGroup,
directionalText: logic.directionalText(),
onCloseDirectional: logic.onClearDirectional,
onSend: (v) => logic.sendTextMsg(),
toolbox: ChatToolBox(
onTapAlbum: logic.onTapAlbum,
onTapCamera: logic.onTapCamera,
onTapCall: logic.call,
onTapCard: logic.onTapCarte,
onTapFile: logic.onTapFile,
onTapLocation: logic.onTapLocation,
),
voiceRecordBar: bar,
emojiView: ChatEmojiView(
textEditingController: logic.inputCtrl,
favoriteList: logic.cacheLogic.urlList,
onAddFavorite: logic.favoriteManage,
onSelectedFavorite: logic.sendFavoritePic,
),
),
child: ChatListView(
onTouch: () => logic.closeToolbox(),
itemCount: logic.messageList.length,
controller: logic.scrollController,
onScrollToBottomLoad: logic.onScrollToBottomLoad,
onScrollToTop: logic.onScrollToTop,
itemBuilder: (_, index) {
final message = logic.indexOfMessage(index);
return Obx(() => _buildItemView(message));
},
),
),
));
}),
),
);
}
}