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>
This commit is contained in:
编码工程师
2026-08-19 15:24:33 +08:00
co-authored by multica-agent
parent fa8584a73f
commit 8046ec7483
767 changed files with 57658 additions and 0 deletions
@@ -0,0 +1,194 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:lottie/lottie.dart';
import 'package:rxdart/rxdart.dart';
extension SubjectExt<T> on Subject<T> {
T addSafely(T data) {
if (!isClosed) sink.add(data);
return data;
}
}
extension TextEdCtrlExt on TextEditingController {
void fixed63Length() {
addListener(() {
if (text.length == 63 && Platform.isAndroid) {
text += " ";
selection = TextSelection.fromPosition(
TextPosition(
affinity: TextAffinity.downstream,
offset: text.length - 1,
),
);
}
});
}
}
extension StrExt on String {
ImageView get toImage {
return ImageView(name: this);
}
TextView get toText {
return TextView(data: this);
}
LottieView get toLottie {
return LottieView(name: this);
}
String fixAutoLines() {
return Characters(this).join('\u{200B}');
}
String get thumbnailAbsoluteString {
final host = split('?').first;
final isGif = host.split('.').last.toLowerCase() == 'gif';
if (isGif) {
return host;
}
return '$host?height=640&width=360&type=image';
}
String adjustThumbnailAbsoluteString(int size) {
final host = split('?').first;
final isGif = host.split('.').last.toLowerCase() == 'gif';
if (isGif) {
return host;
}
return '$host?height=$size&width=$size&type=image';
}
}
class LottieView extends StatelessWidget {
LottieView({
Key? key,
required this.name,
this.width,
this.height,
this.fit,
}) : super(key: key);
final String name;
double? width;
double? height;
BoxFit? fit;
@override
Widget build(BuildContext context) {
return Lottie.asset(
name,
height: height,
width: width,
package: 'openim_common',
fit: fit,
);
}
}
class TextView extends StatelessWidget {
TextView(
{Key? key,
required this.data,
this.style,
this.textAlign,
this.overflow,
this.textScaleFactor,
this.maxLines,
this.onTap,
this.wordEllipsis = true})
: super(key: key);
final String data;
TextStyle? style;
TextAlign? textAlign;
TextOverflow? overflow;
double? textScaleFactor;
int? maxLines;
Function()? onTap;
bool wordEllipsis;
@override
Widget build(BuildContext context) => GestureDetector(
onTap: onTap,
behavior: HitTestBehavior.translucent,
child: Text(
wordEllipsis ? data : data.replaceAll('', '\u200B'), //解决Flutter按单词省略
style: style,
textAlign: textAlign,
overflow: overflow,
textScaleFactor: textScaleFactor,
maxLines: maxLines,
),
);
}
class ImageView extends StatelessWidget {
ImageView({
Key? key,
required this.name,
this.width,
this.height,
this.color,
this.opacity = 1,
this.fit,
this.onTap,
this.onDoubleTap,
}) : super(key: key);
final String name;
double? width;
double? height;
Color? color;
double opacity;
BoxFit? fit;
Function()? onTap;
Function()? onDoubleTap;
@override
Widget build(BuildContext context) => GestureDetector(
behavior: HitTestBehavior.translucent,
onTap: onTap,
onDoubleTap: onDoubleTap,
child: Opacity(
opacity: opacity,
child: Image.asset(
name,
package: 'openim_common',
width: width,
height: height,
color: color,
fit: fit,
),
),
);
}
extension IntExt on int {
int ensureTenDigits() {
String numberStr = toString();
if (numberStr.length == 10) {
return this;
} else if (numberStr.length > 10) {
return int.parse(numberStr.substring(0, 10));
} else {
return int.parse(numberStr.padLeft(10, '0'));
}
}
}
extension StringExt on String {
String splitStringEveryNChars({int n = 3, String separator = ' '}) {
RegExp regExp = RegExp('.{1,$n}');
Iterable<Match> matches = regExp.allMatches(this);
List<String> parts = matches.map((match) => match.group(0)!).toList();
return parts.join(separator);
}
}
extension DateTimeExt on DateTime {
int get secondsSinceEpoch => millisecondsSinceEpoch ~/ 1000;
}
@@ -0,0 +1,127 @@
import 'dart:convert';
import 'package:flutter_openim_sdk/flutter_openim_sdk.dart';
import 'package:openim_common/openim_common.dart';
extension MessageManagerExt on MessageManager {
Future<Message> createCustomEmojiMessage({
required String url,
int? width,
int? height,
}) =>
createCustomMessage(
data: json.encode({
"customType": CustomMessageType.emoji,
"data": {
'url': url,
'width': width,
'height': height,
},
}),
extension: '',
description: '',
);
Future<Message> createFailedHintMessage({required int type}) => createCustomMessage(
data: json.encode({
"customType": type,
"data": {},
}),
extension: '',
description: '',
);
}
extension MessageExt on Message {
bool get isDeletedByFriendType {
if (isCustomType) {
try {
var map = json.decode(customElem!.data!);
var customType = map['customType'];
return CustomMessageType.deletedByFriend == customType;
} catch (e, s) {
Logger.print('$e $s');
}
}
return false;
}
bool get isBlockedByFriendType {
if (isCustomType) {
try {
var map = json.decode(customElem!.data!);
var customType = map['customType'];
return CustomMessageType.blockedByFriend == customType;
} catch (e, s) {
Logger.print('$e $s');
}
}
return false;
}
bool get isEmojiType {
if (isCustomType) {
try {
var map = json.decode(customElem!.data!);
var customType = map['customType'];
return CustomMessageType.emoji == customType;
} catch (e, s) {
Logger.print('$e $s');
}
}
return false;
}
bool get isTextType => contentType == MessageType.text;
bool get isPictureType => contentType == MessageType.picture;
bool get isVoiceType => contentType == MessageType.voice;
bool get isVideoType => contentType == MessageType.video;
bool get isFileType => contentType == MessageType.file;
bool get isLocationType => contentType == MessageType.location;
bool get isCardType => contentType == MessageType.card;
bool get isCustomFaceType => contentType == MessageType.customFace;
bool get isCustomType => contentType == MessageType.custom;
bool get isRevokeType => contentType == MessageType.revokeMessageNotification;
bool get isNotificationType => contentType! >= 1000;
}
class CustomMessageType {
static const callingInvite = 200;
static const callingAccept = 201;
static const callingReject = 202;
static const callingCancel = 203;
static const callingHungup = 204;
static const call = 901;
static const emoji = 902;
static const tag = 903;
static const moments = 904;
static const meeting = 905;
static const blockedByFriend = 910;
static const deletedByFriend = 911;
static const removedFromGroup = 912;
static const groupDisbanded = 913;
}
extension PublicUserInfoExt on PublicUserInfo {
UserInfo get simpleUserInfo {
return UserInfo(userID: userID, nickname: nickname, faceURL: faceURL);
}
}
extension FriendInfoExt on FriendInfo {
UserInfo get simpleUserInfo {
return UserInfo(userID: userID, nickname: nickname, faceURL: faceURL);
}
}