├── ios ├── Flutter │ ├── .last_build_id │ ├── Debug.xcconfig │ ├── Release.xcconfig │ ├── flutter_export_environment.sh │ └── AppFrameworkInfo.plist ├── Runner │ ├── AppDelegate.h │ ├── Assets.xcassets │ │ ├── LaunchImage.imageset │ │ │ ├── LaunchImage.png │ │ │ ├── LaunchImage@2x.png │ │ │ ├── LaunchImage@3x.png │ │ │ ├── README.md │ │ │ └── Contents.json │ │ └── AppIcon.appiconset │ │ │ ├── Icon-App-20x20@1x.png │ │ │ ├── Icon-App-20x20@2x.png │ │ │ ├── Icon-App-20x20@3x.png │ │ │ ├── Icon-App-29x29@1x.png │ │ │ ├── Icon-App-29x29@2x.png │ │ │ ├── Icon-App-29x29@3x.png │ │ │ ├── Icon-App-40x40@1x.png │ │ │ ├── Icon-App-40x40@2x.png │ │ │ ├── Icon-App-40x40@3x.png │ │ │ ├── Icon-App-60x60@2x.png │ │ │ ├── Icon-App-60x60@3x.png │ │ │ ├── Icon-App-76x76@1x.png │ │ │ ├── Icon-App-76x76@2x.png │ │ │ ├── Icon-App-1024x1024@1x.png │ │ │ ├── Icon-App-83.5x83.5@2x.png │ │ │ └── Contents.json │ ├── main.m │ ├── Runner.entitlements │ ├── RCDTestMessage.h │ ├── Base.lproj │ │ ├── Main.storyboard │ │ └── LaunchScreen.storyboard │ ├── Info.plist │ ├── RCDTestMessage.m │ └── AppDelegate.m ├── Runner.xcodeproj │ ├── project.xcworkspace │ │ └── contents.xcworkspacedata │ └── xcshareddata │ │ └── xcschemes │ │ └── Runner.xcscheme ├── Runner.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist └── Podfile ├── screenshot1.png ├── screenshot2.png ├── assets ├── RCFlutterConf.json └── images │ ├── logo.png │ ├── burnPicture.png │ ├── voice_icon.png │ ├── rc_ic_warning.png │ ├── voice_recoder.gif │ ├── burnPictureForm.png │ ├── default_portrait.png │ ├── sight_message_icon.png │ ├── sight_preview_done.png │ ├── voice_icon_reverse.png │ ├── file_message_icon_apk.png │ ├── file_message_icon_key.png │ ├── file_message_icon_pdf.png │ ├── file_message_icon_ppt.png │ ├── sight_camera_switch.png │ ├── sight_preview_cancel.png │ ├── file_message_icon_audio.png │ ├── file_message_icon_else.png │ ├── file_message_icon_excel.png │ ├── file_message_icon_file.png │ ├── file_message_icon_pages.png │ ├── file_message_icon_video.png │ ├── file_message_icon_word.png │ ├── sight_top_toolbar_close.png │ ├── file_message_icon_numbers.png │ ├── file_message_icon_picture.png │ └── rich_content_msg_default.png ├── android ├── app │ ├── .settings │ │ └── org.eclipse.buildship.core.prefs │ ├── lib │ │ ├── meizu-push-4.0.7.aar │ │ ├── vivo_pushsdk-v2.9.0.0.aar │ │ ├── MiPush_SDK_Client_3_8_2.jar │ │ └── com.heytap.msp-push-2.1.0.aar │ ├── src │ │ ├── main │ │ │ ├── res │ │ │ │ ├── drawable │ │ │ │ │ ├── app_icon.png │ │ │ │ │ └── launch_background.xml │ │ │ │ ├── mipmap-hdpi │ │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-mdpi │ │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-xhdpi │ │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-xxhdpi │ │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-xxxhdpi │ │ │ │ │ └── ic_launcher.png │ │ │ │ └── values │ │ │ │ │ └── styles.xml │ │ │ └── java │ │ │ │ └── com │ │ │ │ └── example │ │ │ │ └── rongcloud_im_plugin_example │ │ │ │ ├── MainActivity.java │ │ │ │ ├── push │ │ │ │ └── SealNotificationReceiver.java │ │ │ │ └── TestMessage.java │ │ ├── debug │ │ │ └── AndroidManifest.xml │ │ └── profile │ │ │ └── AndroidManifest.xml │ ├── agconnect-services.json │ ├── .classpath │ ├── .project │ ├── google-services.json │ └── build.gradle ├── rong_flutter.key ├── gradle.properties ├── gradle │ └── wrapper │ │ └── gradle-wrapper.properties ├── .settings │ └── org.eclipse.buildship.core.prefs ├── settings.gradle ├── build.gradle └── .project ├── lib ├── user_data.dart ├── im │ ├── util │ │ ├── bloc │ │ │ ├── message_bloc.dart │ │ │ └── bloc_provider.dart │ │ ├── code_util.dart │ │ ├── time.dart │ │ ├── event_bus.dart │ │ ├── http_util.dart │ │ ├── dialog_util.dart │ │ ├── file_suffix.dart │ │ ├── file.dart │ │ ├── db_manager.dart │ │ ├── user_info_datesource.dart │ │ ├── media_util.dart │ │ └── style.dart │ ├── pages │ │ ├── item │ │ │ ├── bottom_tool_bar.dart │ │ │ └── widget_util.dart │ │ ├── sight │ │ │ ├── video_play_page.dart │ │ │ ├── record_top_item.dart │ │ │ └── record_bottom_item.dart │ │ ├── image_preview_page.dart │ │ ├── webview_page.dart │ │ ├── file_preview_page.dart │ │ └── conversation_list_page.dart │ └── widget │ │ └── cachImage │ │ └── cached_network_image_provider.dart ├── test_message.dart ├── router.dart ├── other │ ├── message_read_page.dart │ ├── home_page.dart │ ├── contacts_page.dart │ ├── search_message_page.dart │ ├── login_page.dart │ ├── select_conversation_page.dart │ ├── chatroom_debug_page.dart │ └── debug_page.dart └── main.dart ├── .github └── ISSUE_TEMPLATE │ └── ------.md ├── test └── widget_test.dart ├── LICENSE ├── README.md ├── .gitignore ├── pubspec.yaml └── .flutter-plugins-dependencies /ios/Flutter/.last_build_id: -------------------------------------------------------------------------------- 1 | 35e2f1b5ed8cfb67df85c44bff45930f -------------------------------------------------------------------------------- /screenshot1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rongcloud/imkit-flutter-quickstart/HEAD/screenshot1.png -------------------------------------------------------------------------------- /screenshot2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rongcloud/imkit-flutter-quickstart/HEAD/screenshot2.png -------------------------------------------------------------------------------- /assets/RCFlutterConf.json: -------------------------------------------------------------------------------- 1 | { 2 | "im":{ 3 | "enablePersistentUserInfoCache":true 4 | } 5 | } -------------------------------------------------------------------------------- /android/app/.settings/org.eclipse.buildship.core.prefs: -------------------------------------------------------------------------------- 1 | connection.project.dir=.. 2 | eclipse.preferences.version=1 3 | -------------------------------------------------------------------------------- /android/rong_flutter.key: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rongcloud/imkit-flutter-quickstart/HEAD/android/rong_flutter.key -------------------------------------------------------------------------------- /assets/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rongcloud/imkit-flutter-quickstart/HEAD/assets/images/logo.png -------------------------------------------------------------------------------- /assets/images/burnPicture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rongcloud/imkit-flutter-quickstart/HEAD/assets/images/burnPicture.png -------------------------------------------------------------------------------- /assets/images/voice_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rongcloud/imkit-flutter-quickstart/HEAD/assets/images/voice_icon.png -------------------------------------------------------------------------------- /assets/images/rc_ic_warning.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rongcloud/imkit-flutter-quickstart/HEAD/assets/images/rc_ic_warning.png -------------------------------------------------------------------------------- /assets/images/voice_recoder.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rongcloud/imkit-flutter-quickstart/HEAD/assets/images/voice_recoder.gif -------------------------------------------------------------------------------- /assets/images/burnPictureForm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rongcloud/imkit-flutter-quickstart/HEAD/assets/images/burnPictureForm.png -------------------------------------------------------------------------------- /assets/images/default_portrait.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rongcloud/imkit-flutter-quickstart/HEAD/assets/images/default_portrait.png -------------------------------------------------------------------------------- /android/app/lib/meizu-push-4.0.7.aar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rongcloud/imkit-flutter-quickstart/HEAD/android/app/lib/meizu-push-4.0.7.aar -------------------------------------------------------------------------------- /assets/images/sight_message_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rongcloud/imkit-flutter-quickstart/HEAD/assets/images/sight_message_icon.png -------------------------------------------------------------------------------- /assets/images/sight_preview_done.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rongcloud/imkit-flutter-quickstart/HEAD/assets/images/sight_preview_done.png -------------------------------------------------------------------------------- /assets/images/voice_icon_reverse.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rongcloud/imkit-flutter-quickstart/HEAD/assets/images/voice_icon_reverse.png -------------------------------------------------------------------------------- /ios/Flutter/Debug.xcconfig: -------------------------------------------------------------------------------- 1 | #include "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" 2 | #include "Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /android/gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx1536M 2 | android.useAndroidX=true 3 | android.enableJetifier=true 4 | android.enableR8=true 5 | -------------------------------------------------------------------------------- /assets/images/file_message_icon_apk.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rongcloud/imkit-flutter-quickstart/HEAD/assets/images/file_message_icon_apk.png -------------------------------------------------------------------------------- /assets/images/file_message_icon_key.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rongcloud/imkit-flutter-quickstart/HEAD/assets/images/file_message_icon_key.png -------------------------------------------------------------------------------- /assets/images/file_message_icon_pdf.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rongcloud/imkit-flutter-quickstart/HEAD/assets/images/file_message_icon_pdf.png -------------------------------------------------------------------------------- /assets/images/file_message_icon_ppt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rongcloud/imkit-flutter-quickstart/HEAD/assets/images/file_message_icon_ppt.png -------------------------------------------------------------------------------- /assets/images/sight_camera_switch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rongcloud/imkit-flutter-quickstart/HEAD/assets/images/sight_camera_switch.png -------------------------------------------------------------------------------- /assets/images/sight_preview_cancel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rongcloud/imkit-flutter-quickstart/HEAD/assets/images/sight_preview_cancel.png -------------------------------------------------------------------------------- /ios/Flutter/Release.xcconfig: -------------------------------------------------------------------------------- 1 | #include "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" 2 | #include "Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /android/app/lib/vivo_pushsdk-v2.9.0.0.aar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rongcloud/imkit-flutter-quickstart/HEAD/android/app/lib/vivo_pushsdk-v2.9.0.0.aar -------------------------------------------------------------------------------- /assets/images/file_message_icon_audio.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rongcloud/imkit-flutter-quickstart/HEAD/assets/images/file_message_icon_audio.png -------------------------------------------------------------------------------- /assets/images/file_message_icon_else.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rongcloud/imkit-flutter-quickstart/HEAD/assets/images/file_message_icon_else.png -------------------------------------------------------------------------------- /assets/images/file_message_icon_excel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rongcloud/imkit-flutter-quickstart/HEAD/assets/images/file_message_icon_excel.png -------------------------------------------------------------------------------- /assets/images/file_message_icon_file.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rongcloud/imkit-flutter-quickstart/HEAD/assets/images/file_message_icon_file.png -------------------------------------------------------------------------------- /assets/images/file_message_icon_pages.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rongcloud/imkit-flutter-quickstart/HEAD/assets/images/file_message_icon_pages.png -------------------------------------------------------------------------------- /assets/images/file_message_icon_video.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rongcloud/imkit-flutter-quickstart/HEAD/assets/images/file_message_icon_video.png -------------------------------------------------------------------------------- /assets/images/file_message_icon_word.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rongcloud/imkit-flutter-quickstart/HEAD/assets/images/file_message_icon_word.png -------------------------------------------------------------------------------- /assets/images/sight_top_toolbar_close.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rongcloud/imkit-flutter-quickstart/HEAD/assets/images/sight_top_toolbar_close.png -------------------------------------------------------------------------------- /ios/Runner/AppDelegate.h: -------------------------------------------------------------------------------- 1 | #import 2 | #import 3 | 4 | @interface AppDelegate : FlutterAppDelegate 5 | 6 | @end 7 | -------------------------------------------------------------------------------- /android/app/lib/MiPush_SDK_Client_3_8_2.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rongcloud/imkit-flutter-quickstart/HEAD/android/app/lib/MiPush_SDK_Client_3_8_2.jar -------------------------------------------------------------------------------- /assets/images/file_message_icon_numbers.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rongcloud/imkit-flutter-quickstart/HEAD/assets/images/file_message_icon_numbers.png -------------------------------------------------------------------------------- /assets/images/file_message_icon_picture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rongcloud/imkit-flutter-quickstart/HEAD/assets/images/file_message_icon_picture.png -------------------------------------------------------------------------------- /assets/images/rich_content_msg_default.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rongcloud/imkit-flutter-quickstart/HEAD/assets/images/rich_content_msg_default.png -------------------------------------------------------------------------------- /android/app/lib/com.heytap.msp-push-2.1.0.aar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rongcloud/imkit-flutter-quickstart/HEAD/android/app/lib/com.heytap.msp-push-2.1.0.aar -------------------------------------------------------------------------------- /android/app/src/main/res/drawable/app_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rongcloud/imkit-flutter-quickstart/HEAD/android/app/src/main/res/drawable/app_icon.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rongcloud/imkit-flutter-quickstart/HEAD/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rongcloud/imkit-flutter-quickstart/HEAD/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rongcloud/imkit-flutter-quickstart/HEAD/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rongcloud/imkit-flutter-quickstart/HEAD/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rongcloud/imkit-flutter-quickstart/HEAD/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rongcloud/imkit-flutter-quickstart/HEAD/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rongcloud/imkit-flutter-quickstart/HEAD/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rongcloud/imkit-flutter-quickstart/HEAD/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rongcloud/imkit-flutter-quickstart/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rongcloud/imkit-flutter-quickstart/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rongcloud/imkit-flutter-quickstart/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rongcloud/imkit-flutter-quickstart/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rongcloud/imkit-flutter-quickstart/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rongcloud/imkit-flutter-quickstart/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rongcloud/imkit-flutter-quickstart/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rongcloud/imkit-flutter-quickstart/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rongcloud/imkit-flutter-quickstart/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rongcloud/imkit-flutter-quickstart/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rongcloud/imkit-flutter-quickstart/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rongcloud/imkit-flutter-quickstart/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rongcloud/imkit-flutter-quickstart/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rongcloud/imkit-flutter-quickstart/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rongcloud/imkit-flutter-quickstart/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /lib/user_data.dart: -------------------------------------------------------------------------------- 1 | //融云 appkey 2 | String RongAppKey = 'n19jmcy59f1q9'; 3 | //用户 id 4 | String CurrentUserId = 'android'; 5 | 6 | //通过用户 id 生成的对应融云 token 7 | String RongIMToken = 8 | '/GAO1QE3NeKdxZ8EFWZnXSBpWcymqs7mr0LfCBn63cWXAWvMuW73BKKASyaZmGFGhVYuRiYRxSacloyurITSuw=='; 9 | -------------------------------------------------------------------------------- /ios/Runner/main.m: -------------------------------------------------------------------------------- 1 | #import 2 | #import 3 | #import "AppDelegate.h" 4 | 5 | int main(int argc, char* argv[]) { 6 | @autoreleasepool { 7 | return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Fri Jun 23 08:50:38 CEST 2017 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-5.1.1-all.zip 7 | -------------------------------------------------------------------------------- /ios/Runner/Runner.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | aps-environment 6 | development 7 | 8 | 9 | -------------------------------------------------------------------------------- /ios/Runner.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /android/app/agconnect-services.json: -------------------------------------------------------------------------------- 1 | { 2 | "client": { 3 | "cp_id": "填写您自己的申请的账号信息", 4 | "product_id": "填写您自己的申请的账号信息", 5 | "client_id": "填写您自己的申请的账号信息", 6 | "client_secret": "填写您自己的申请的账号信息", 7 | "app_id": "填写您自己的申请的账号信息", 8 | "package_name": "com.example.rongcloud_im_plugin_example" 9 | }, 10 | "configuration_version": "1.0" 11 | } -------------------------------------------------------------------------------- /android/app/.classpath: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md: -------------------------------------------------------------------------------- 1 | # Launch Screen Assets 2 | 3 | You can customize the launch screen with your own desired assets by replacing the image files in this directory. 4 | 5 | You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. -------------------------------------------------------------------------------- /android/app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/------.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 问题反馈模板 3 | about: 欢迎体验使用融云 IM Flutter SDK 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | 为了快速解决问题,在寻求帮助的时候,请提供以下信息,方便我们快速定位问题 11 | * 本地 Flutter 版本信息 12 | * 异常设备是 iOS 还是 Android,列出具体机型和系统 13 | * 所调用的是什么 API 或者说是什么功能 14 | * 完整的报错信息或者问题描述 15 | * 提供问题发生时必要的日志,并以 txt 形式上传 16 | 17 | 文档链接 https://github.com/rongcloud/rongcloud-im-flutter-sdk/tree/dev/doc 18 | 文档持续更新中…… 19 | -------------------------------------------------------------------------------- /android/.settings/org.eclipse.buildship.core.prefs: -------------------------------------------------------------------------------- 1 | arguments= 2 | auto.sync=false 3 | build.scans.enabled=false 4 | connection.gradle.distribution=GRADLE_DISTRIBUTION(WRAPPER) 5 | connection.project.dir= 6 | eclipse.preferences.version=1 7 | gradle.user.home= 8 | java.home=/Library/Java/JavaVirtualMachines/jdk-12.0.1.jdk/Contents/Home 9 | jvm.arguments= 10 | offline.mode=false 11 | override.workspace.settings=true 12 | show.console.view=true 13 | show.executions.view=true 14 | -------------------------------------------------------------------------------- /android/app/src/profile/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "LaunchImage.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "LaunchImage@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "LaunchImage@3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /android/settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app' 2 | 3 | def flutterProjectRoot = rootProject.projectDir.parentFile.toPath() 4 | 5 | def plugins = new Properties() 6 | def pluginsFile = new File(flutterProjectRoot.toFile(), '.flutter-plugins') 7 | if (pluginsFile.exists()) { 8 | pluginsFile.withReader('UTF-8') { reader -> plugins.load(reader) } 9 | } 10 | 11 | plugins.each { name, path -> 12 | def pluginDirectory = flutterProjectRoot.resolve(path).resolve('android').toFile() 13 | include ":$name" 14 | project(":$name").projectDir = pluginDirectory 15 | } 16 | -------------------------------------------------------------------------------- /ios/Flutter/flutter_export_environment.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # This is a generated file; do not edit or check into version control. 3 | export "FLUTTER_ROOT=/Users/zoulu/tool/flutter" 4 | export "FLUTTER_APPLICATION_PATH=/Users/zoulu/FlutterProjects/git/rongcloud-im-flutter-sdk/example" 5 | export "FLUTTER_TARGET=lib/main.dart" 6 | export "FLUTTER_BUILD_DIR=build" 7 | export "SYMROOT=${SOURCE_ROOT}/../build/ios" 8 | export "FLUTTER_BUILD_NAME=1.0.0" 9 | export "FLUTTER_BUILD_NUMBER=1" 10 | export "DART_OBFUSCATION=false" 11 | export "TRACK_WIDGET_CREATION=false" 12 | export "TREE_SHAKE_ICONS=false" 13 | export "PACKAGE_CONFIG=.packages" 14 | -------------------------------------------------------------------------------- /lib/im/util/bloc/message_bloc.dart: -------------------------------------------------------------------------------- 1 | import 'bloc_provider.dart'; 2 | import 'package:rxdart/subjects.dart'; 3 | 4 | class MessageBloc extends BlocBase { 5 | MessageInfoWrapState warpInfo; 6 | // 列表数据 7 | BehaviorSubject _listDataController = 8 | BehaviorSubject(sync: true); 9 | Sink get inListData => _listDataController.sink; 10 | Stream get outListData => _listDataController.stream; 11 | 12 | @override 13 | void dispose() { 14 | _listDataController.close(); 15 | } 16 | 17 | void updateMessageList(List messageList) { 18 | warpInfo = MessageInfoWrapState(messageList: messageList); 19 | inListData.add(warpInfo); 20 | } 21 | } 22 | 23 | class MessageInfoWrapState { 24 | MessageInfoWrapState({this.messageList}); 25 | List messageList; 26 | } 27 | -------------------------------------------------------------------------------- /ios/Runner/RCDTestMessage.h: -------------------------------------------------------------------------------- 1 | // 2 | // RCDTestMessage.h 3 | // RCloudMessage 4 | // 5 | // Created by 岑裕 on 15/12/17. 6 | // Copyright © 2015年 RongCloud. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | /*! 12 | 测试消息的类型名 13 | */ 14 | #define RCDTestMessageTypeIdentifier @"RCD:TstMsg" 15 | 16 | /*! 17 | Demo测试用的自定义消息类 18 | 19 | @discussion Demo测试用的自定义消息类,此消息会进行存储并计入未读消息数。 20 | */ 21 | @interface RCDTestMessage : RCMessageContent 22 | 23 | /*! 24 | 测试消息的内容 25 | */ 26 | @property(nonatomic, strong) NSString *content; 27 | 28 | /*! 29 | 测试消息的附加信息 30 | */ 31 | @property(nonatomic, strong) NSString *extra; 32 | 33 | /*! 34 | 初始化测试消息 35 | 36 | @param content 文本内容 37 | @return 测试消息对象 38 | */ 39 | + (instancetype)messageWithContent:(NSString *)content; 40 | 41 | @end 42 | -------------------------------------------------------------------------------- /android/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | repositories { 3 | google() 4 | jcenter() 5 | maven { url 'http://developer.huawei.com/repo/' } 6 | } 7 | 8 | dependencies { 9 | classpath 'com.android.tools.build:gradle:3.4.1' 10 | classpath 'com.huawei.agconnect:agcp:1.1.1.300' 11 | classpath 'com.google.gms:google-services:4.2.0' 12 | } 13 | } 14 | 15 | allprojects { 16 | repositories { 17 | google() 18 | jcenter() 19 | maven {url 'http://developer.huawei.com/repo/'} 20 | } 21 | } 22 | 23 | rootProject.buildDir = '../build' 24 | subprojects { 25 | project.buildDir = "${rootProject.buildDir}/${project.name}" 26 | } 27 | subprojects { 28 | project.evaluationDependsOn(':app') 29 | } 30 | 31 | task clean(type: Delete) { 32 | delete rootProject.buildDir 33 | } 34 | -------------------------------------------------------------------------------- /android/.project: -------------------------------------------------------------------------------- 1 | 2 | 3 | android 4 | Project android created by Buildship. 5 | 6 | 7 | 8 | 9 | org.eclipse.buildship.core.gradleprojectbuilder 10 | 11 | 12 | 13 | 14 | 15 | org.eclipse.buildship.core.gradleprojectnature 16 | 17 | 18 | 19 | 1618472525788 20 | 21 | 30 22 | 23 | org.eclipse.core.resources.regexFilterMatcher 24 | node_modules|.git|__CREATED_BY_JAVA_LANGUAGE_SERVER__ 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /ios/Flutter/AppFrameworkInfo.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | App 9 | CFBundleIdentifier 10 | io.flutter.flutter.app 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | App 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1.0 23 | MinimumOSVersion 24 | 8.0 25 | 26 | 27 | -------------------------------------------------------------------------------- /test/widget_test.dart: -------------------------------------------------------------------------------- 1 | // This is a basic Flutter widget test. 2 | // 3 | // To perform an interaction with a widget in your test, use the WidgetTester 4 | // utility that Flutter provides. For example, you can send tap and scroll 5 | // gestures. You can also use WidgetTester to find child widgets in the widget 6 | // tree, read text, and verify that the values of widget properties are correct. 7 | 8 | import 'package:flutter/material.dart'; 9 | import 'package:flutter_test/flutter_test.dart'; 10 | 11 | void main() { 12 | testWidgets('Verify Platform version', (WidgetTester tester) async { 13 | // Build our app and trigger a frame. 14 | // Verify that platform version is retrieved. 15 | expect( 16 | find.byWidgetPredicate( 17 | (Widget widget) => 18 | widget is Text && widget.data.startsWith('Running on:'), 19 | ), 20 | findsOneWidget, 21 | ); 22 | }); 23 | } 24 | -------------------------------------------------------------------------------- /android/app/.project: -------------------------------------------------------------------------------- 1 | 2 | 3 | app 4 | Project app created by Buildship. 5 | 6 | 7 | 8 | 9 | org.eclipse.jdt.core.javabuilder 10 | 11 | 12 | 13 | 14 | org.eclipse.buildship.core.gradleprojectbuilder 15 | 16 | 17 | 18 | 19 | 20 | org.eclipse.jdt.core.javanature 21 | org.eclipse.buildship.core.gradleprojectnature 22 | 23 | 24 | 25 | 1618472525804 26 | 27 | 30 28 | 29 | org.eclipse.core.resources.regexFilterMatcher 30 | node_modules|.git|__CREATED_BY_JAVA_LANGUAGE_SERVER__ 31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /lib/im/util/bloc/bloc_provider.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | abstract class BlocBase { 4 | void dispose(); 5 | } 6 | 7 | class BlocProvider extends StatefulWidget { 8 | BlocProvider({ 9 | Key key, 10 | @required this.child, 11 | @required this.bloc, 12 | }) : super(key: key); 13 | 14 | final T bloc; 15 | final Widget child; 16 | 17 | @override 18 | _BlocProviderState createState() => _BlocProviderState(); 19 | 20 | static T of(BuildContext context) { 21 | final type = _typeOf>(); 22 | BlocProvider provider = context.findAncestorWidgetOfExactType(); 23 | return provider.bloc; 24 | } 25 | 26 | static Type _typeOf() => T; 27 | } 28 | 29 | class _BlocProviderState extends State> { 30 | @override 31 | void dispose() { 32 | widget.bloc.dispose(); 33 | super.dispose(); 34 | } 35 | 36 | @override 37 | Widget build(BuildContext context) { 38 | return widget.child; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 融云 RongCloud 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /android/app/src/main/java/com/example/rongcloud_im_plugin_example/MainActivity.java: -------------------------------------------------------------------------------- 1 | package com.example.rongcloud_im_plugin_example; 2 | 3 | import android.content.Context; 4 | import android.os.Bundle; 5 | 6 | 7 | 8 | import io.flutter.app.FlutterActivity; 9 | import io.flutter.plugins.GeneratedPluginRegistrant; 10 | 11 | public class MainActivity extends FlutterActivity { 12 | @Override 13 | protected void onCreate(Bundle savedInstanceState) { 14 | super.onCreate(savedInstanceState); 15 | GeneratedPluginRegistrant.registerWith(this); 16 | Context con = getApplicationContext(); 17 | //Android 注册自定义消息必须在 init 之后 18 | //如果注册了自定义消息,但是没有注册消息模板,无法进入 SDK 的聊天页面 19 | //https://www.rongcloud.cn/docs/android.html 参见文档的"消息自定义" 20 | // RongIMClient.init(con,"pvxdm17jxjaor"); 21 | // RCIMFlutterWrapper.getInstance().registerMessage(TestMessage.class); 22 | 23 | // 测试Android往Flutter传递数据 24 | // Timer timer = new Timer(); 25 | // timer.schedule(new TimerTask() { 26 | // public void run() { 27 | // Map map = new HashMap(); 28 | // map.put("key","android"); 29 | // RCIMFlutterWrapper.getInstance().sendDataToFlutter(map); 30 | // } 31 | // },500); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /android/app/google-services.json: -------------------------------------------------------------------------------- 1 | { 2 | "project_info": { 3 | "project_number": "396364826160", 4 | "firebase_url": "https://sealtalk-4be99.firebaseio.com", 5 | "project_id": "sealtalk-4be99", 6 | "storage_bucket": "sealtalk-4be99.appspot.com" 7 | }, 8 | "client": [ 9 | { 10 | "client_info": { 11 | "mobilesdk_app_id": "1:396364826160:android:a10a4a74ea61b328", 12 | "android_client_info": { 13 | "package_name": "com.example.rongcloud_im_plugin_example" 14 | } 15 | }, 16 | "oauth_client": [ 17 | { 18 | "client_id": "396364826160-f7j0826mqtki71283p5jagirh4i0spe7.apps.googleusercontent.com", 19 | "client_type": 3 20 | }, 21 | { 22 | "client_id": "396364826160-nvbu6aesghmbj5op6uvt9uvp7em8ajjd.apps.googleusercontent.com", 23 | "client_type": 3 24 | } 25 | ], 26 | "api_key": [ 27 | { 28 | "current_key": "AIzaSyD9oaYejQ4_Cit4-u8RSkKCjAmsZ0HtU6E" 29 | } 30 | ], 31 | "services": { 32 | "analytics_service": { 33 | "status": 1 34 | }, 35 | "appinvite_service": { 36 | "status": 1, 37 | "other_platform_oauth_client": [] 38 | }, 39 | "ads_service": { 40 | "status": 2 41 | } 42 | } 43 | } 44 | ], 45 | "configuration_version": "1" 46 | } -------------------------------------------------------------------------------- /lib/test_message.dart: -------------------------------------------------------------------------------- 1 | import 'package:rongcloud_im_plugin/rongcloud_im_plugin.dart'; 2 | import 'dart:convert' show json; 3 | 4 | //app 层的测试消息 5 | class TestMessage extends MessageContent { 6 | static const String objectName = "RCD:TstMsg"; 7 | 8 | String content; 9 | String extra; 10 | @override 11 | void decode(String jsonStr) { 12 | Map map = json.decode(jsonStr.toString()); 13 | this.content = map["content"]; 14 | this.extra = map["extra"]; 15 | 16 | // decode 消息内容中携带的发送者的用户信息 17 | Map userMap = map["user"]; 18 | super.decodeUserInfo(userMap); 19 | 20 | // decode 消息中的 @ 提醒信息;消息需要携带 @ 信息时添加此方法 21 | Map menthionedMap = map["mentionedInfo"]; 22 | super.decodeMentionedInfo(menthionedMap); 23 | } 24 | 25 | @override 26 | String encode() { 27 | Map map = {"content": this.content, "extra": this.extra}; 28 | 29 | // encode 消息内容中携带的发送者的用户信息 30 | if (this.sendUserInfo != null) { 31 | Map userMap = super.encodeUserInfo(this.sendUserInfo); 32 | map["user"] = userMap; 33 | } 34 | 35 | // encode 消息中的 @ 提醒信息;消息需要携带 @ 信息时添加此方法 36 | if (this.mentionedInfo != null) { 37 | Map mentionedMap = super.encodeMentionedInfo(this.mentionedInfo); 38 | map["mentionedInfo"] = mentionedMap; 39 | } 40 | return json.encode(map); 41 | } 42 | 43 | @override 44 | String conversationDigest() { 45 | return content; 46 | } 47 | 48 | @override 49 | String getObjectName() { 50 | return objectName; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /ios/Podfile: -------------------------------------------------------------------------------- 1 | # Uncomment this line to define a global platform for your project 2 | # platform :ios, '9.0' 3 | 4 | # CocoaPods analytics sends network stats synchronously affecting flutter build latency. 5 | ENV['COCOAPODS_DISABLE_STATS'] = 'true' 6 | 7 | project 'Runner', { 8 | 'Debug' => :debug, 9 | 'Profile' => :release, 10 | 'Release' => :release, 11 | } 12 | 13 | def flutter_root 14 | generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) 15 | unless File.exist?(generated_xcode_build_settings_path) 16 | raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" 17 | end 18 | 19 | File.foreach(generated_xcode_build_settings_path) do |line| 20 | matches = line.match(/FLUTTER_ROOT\=(.*)/) 21 | return matches[1].strip if matches 22 | end 23 | raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" 24 | end 25 | 26 | require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) 27 | 28 | flutter_ios_podfile_setup 29 | 30 | target 'Runner' do 31 | use_frameworks! 32 | use_modular_headers! 33 | 34 | flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) 35 | end 36 | 37 | post_install do |installer| 38 | installer.pods_project.targets.each do |target| 39 | flutter_additional_ios_build_settings(target) 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /lib/im/util/code_util.dart: -------------------------------------------------------------------------------- 1 | class CodeUtil { 2 | /// 具体业务错误码 3 | static String codeString(int code) { 4 | String key = code.toString() != null ? code.toString() : ""; 5 | return codeMap[key]; 6 | } 7 | 8 | static Map codeMap = { 9 | "": "未找到对应错误", 10 | "-1": "未知错误(预留)", 11 | "0": "成功", 12 | "405": "已被对方加入黑名单,消息发送失败。", 13 | "5004": "超时", 14 | "20604": "发送消息频率过高,1秒钟最多只允许发送5条消息", 15 | "22406": "当前用户不在该群组中", 16 | "22408": "当前用户在群组中已被禁言", 17 | "23406": "当前用户不在该聊天室中", 18 | "23408": "当前用户在该聊天室中已被禁言", 19 | "23409": "当前用户已被踢出并禁止加入聊天室。被禁止的时间取决于服务端调用踢出接口时传入的时间。", 20 | "23410": "聊天室不存在", 21 | "23411": "聊天室成员超限", 22 | "23412": "聊天室接口参数无效。请确认参数是否为空或者有效。", 23 | "23414": "聊天室云存储业务未开通", 24 | "23423": "超过聊天室的最大状态设置数,1 个聊天室默认最多设置 100 个", 25 | "23424": "聊天室中非法覆盖状态值,状态已存在,没有权限覆盖", 26 | "23425": "超过聊天室中状态设置频率,1 个聊天室 1 秒钟最多设置和删除状态 100 次", 27 | "23426": "聊天室状态存储功能没有开通,请联系商务开通", 28 | "23427": "聊天室状态值不存在", 29 | "34004": "聊天室状态未同步完成", 30 | "30001": "当前连接不可用(连接已经被释放)", 31 | "30002": "当前连接不可用", 32 | "30003": "客户端发送消息请求,融云服务端响应超时。", 33 | "33001": "SDK没有初始化", 34 | "33002": "数据库错误", 35 | "33003": "开发者接口调用时传入的参数错误", 36 | "33007": "历史消息云存储业务未开通", 37 | "30016": "消息大小超限", 38 | "25101": "撤回消息参数无效", 39 | "26001": "push 设置参数无效", 40 | "20605": "操作被禁止", 41 | "20606": "操作不支持。仅私有云有效,服务端禁用了该操作。", 42 | "21501": "发送的消息中包含敏感词", 43 | "21502": "消息中敏感词已经被替换", 44 | "34002": "小视频时间长度超出限制", 45 | "34003": "GIF 消息文件大小超出限制", 46 | }; 47 | } 48 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 融云 IM Flutter Plugin Demo 2 | 3 | ![](https://raw.githubusercontent.com/rongcloud/imkit-flutter-quickstart/master/screenshot1.png) 4 | ![](https://raw.githubusercontent.com/rongcloud/imkit-flutter-quickstart/master/screenshot2.png) 5 | 6 | ##登陆账号 7 | 需要您在 [这里注册登陆账号] (https://sealtalk.rongcloud.cn/#/account/signup),然后用此账号登陆本 Demo。 8 | 9 | ## iOS 初次运行 10 | 11 | 初次安装需要从 pod 下载 IMLib 的 iOS SDK,要花费较长的时间,建议手动更新一下 12 | 13 | 1. Podfile 目录执行 `pod repo update` 命令 14 | 2. Podfile 目录执行 `pod update` 命令 15 | 16 | ### 常见问题 17 | 18 | 1. `audio_recorder` does not specify a Swift version and none of the targets (`Runner`) integrating it have the `SWIFT_VERSION` attribute set. Please contact the author or set the `SWIFT_VERSION` attribute in at least one of the targets that integrate this pod. 19 | 20 | Podfile 上添加 `use_frameworks!` 参数 21 | 22 | Xcode 打开,选择 TARGETS -> Runnder -> Build Settings -> Levels 右边加号 -> Add User-Defined Setting -> 添加字段 `SWIFT_VERSION`,写上 Swift 版本,如 4.0 23 | 24 | ## Android 初次运行 25 | 26 | Android 依赖不同的 Flutter Plugin 可能会出现不同版本的 Gradle 导致编译不通过,可以按需做如下处理 27 | 28 | 1. `将所有的 build.gradle 的 gradle 版本统一`,例如统一改为 3.4.1 29 | 2. `将 gradle-wrapper.properties 的 distributionUrl `版本改为和上一步统一,如上一步是 3.4.1 ,那么此处应该改为 5.1.1 30 | 3. `编译后查看是否存在 AndroidX 的冲突`,如果存在 AndroidX 冲突,那么在 `gradle.properties` 增加如下配置 31 | 32 | ``` 33 | android.useAndroidX=true 34 | android.enableJetifier=true 35 | ``` 36 | 37 | > demo 为了考虑 CPU 架构兼容性问题,在 demo 的 `build.gradle` 中配置的 ndk 仅支持 `armeabi-v7a` 38 | 39 | ``` 40 | ndk { 41 | abiFilters "armeabi-v7a" 42 | } 43 | ``` -------------------------------------------------------------------------------- /android/app/src/main/java/com/example/rongcloud_im_plugin_example/push/SealNotificationReceiver.java: -------------------------------------------------------------------------------- 1 | package com.example.rongcloud_im_plugin_example.push; 2 | 3 | import android.content.Context; 4 | import android.content.Intent; 5 | 6 | import com.example.rongcloud_im_plugin_example.MainActivity; 7 | 8 | import io.rong.push.PushType; 9 | import io.rong.push.notification.PushMessageReceiver; 10 | import io.rong.push.notification.PushNotificationMessage; 11 | 12 | 13 | /** 14 | * 通知广播, 可在此让法中进行通知消息处理和点击自定义跳转 15 | */ 16 | public class SealNotificationReceiver extends PushMessageReceiver { 17 | 18 | @Override 19 | public boolean onNotificationMessageArrived(Context context, PushType pushType, PushNotificationMessage message) { 20 | return false; 21 | } 22 | 23 | @Override 24 | public boolean onNotificationMessageClicked(Context context, PushType pushType, PushNotificationMessage message) { 25 | if (!message.getSourceType().equals(PushNotificationMessage.PushSourceType.FROM_ADMIN)) { 26 | String targetId = message.getTargetId(); 27 | //根据自己需求填写点击跳转逻辑 28 | Intent intentMain = new Intent(context, MainActivity.class); 29 | intentMain.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 30 | context.startActivity(intentMain); 31 | } 32 | return false; 33 | } 34 | 35 | @Override 36 | public void onThirdPartyPushState(PushType pushType, String action, long resultCode) { 37 | super.onThirdPartyPushState(pushType, action, resultCode); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /lib/im/util/time.dart: -------------------------------------------------------------------------------- 1 | import 'dart:core'; 2 | 3 | class TimeUtil { 4 | //将 unix 时间戳转换为特定时间文本,如年月日 5 | static String convertTime(int timestamp) { 6 | DateTime msgTime = DateTime.fromMillisecondsSinceEpoch(timestamp); 7 | DateTime nowTime = DateTime.now(); 8 | 9 | if (nowTime.year == msgTime.year) { 10 | //同一年 11 | if (nowTime.month == msgTime.month) { 12 | //同一月 13 | if (nowTime.day == msgTime.day) { 14 | //同一天 时:分 15 | return msgTime.hour.toString() + ":" + msgTime.minute.toString(); 16 | } else { 17 | if (nowTime.day - msgTime.day == 1) { 18 | //昨天 19 | return "昨天"; 20 | } else if (nowTime.day - msgTime.day < 7) { 21 | return _getWeekday(msgTime.weekday); 22 | } 23 | } 24 | } 25 | } 26 | return msgTime.year.toString() + 27 | "/" + 28 | msgTime.month.toString() + 29 | "/" + 30 | msgTime.day.toString(); 31 | } 32 | 33 | ///是否需要显示时间,相差 5 分钟 34 | static bool needShowTime(int sentTime1, int sentTime2) { 35 | if(sentTime1 == null || sentTime2 == null){ 36 | return false; 37 | } 38 | return (sentTime1 - sentTime2).abs() > 5 * 60 * 1000; 39 | } 40 | 41 | static String _getWeekday(int weekday) { 42 | switch (weekday) { 43 | case 1: 44 | return "星期一"; 45 | case 2: 46 | return "星期二"; 47 | case 3: 48 | return "星期三"; 49 | case 4: 50 | return "星期四"; 51 | case 5: 52 | return "星期五"; 53 | case 6: 54 | return "星期六"; 55 | default: 56 | return "星期日"; 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /ios/Runner/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /lib/im/util/event_bus.dart: -------------------------------------------------------------------------------- 1 | typedef void EventCallback(arg); 2 | 3 | //事件总线 4 | class EventBus { 5 | factory EventBus() => _getInstance(); 6 | static EventBus get instance => _getInstance(); 7 | static EventBus _instance; 8 | EventBus._internal() { 9 | // 初始化 10 | } 11 | static EventBus _getInstance() { 12 | if (_instance == null) { 13 | _instance = new EventBus._internal(); 14 | } 15 | return _instance; 16 | } 17 | 18 | Map _events = new Map(); 19 | 20 | //设置事件监听,当有人调用 commit ,并且 eventKey 一样的时候会触发此方法 21 | void addListener(String eventKey, EventCallback callback) { 22 | if (eventKey == null || callback == null) return; 23 | _events[eventKey] = callback; 24 | } 25 | 26 | //移除监听 27 | void removeListener(String eventKey) { 28 | if (eventKey == null) return; 29 | _events.remove(eventKey); 30 | } 31 | 32 | //提交事件 33 | void commit(String eventKey, Object arg) { 34 | if (eventKey == null) return; 35 | EventCallback callback = _events[eventKey]; 36 | if (callback != null) { 37 | callback(arg); 38 | } 39 | } 40 | } 41 | 42 | class EventKeys { 43 | static const String ConversationPageDispose = "ConversationPageDispose"; 44 | static const String ReceiveMessage = "ReceiveMessage"; 45 | static const String ReceiveReadReceipt = "ReceiveReadReceipt"; 46 | static const String ReceiveReceiptRequest = "ReceiveReceiptRequest"; 47 | static const String ReceiveReceiptResponse = "ReceiveReceiptResponse"; 48 | static const String LongPressUserPortrait = "LongPressUserPortrait"; 49 | static const String UpdateNotificationQuietStatus = 50 | "UpdateNotificationQuietStatus"; 51 | static const String ForwardMessageEnd = "ForwardMessageEnd"; 52 | static const String BurnMessage = "BurnMessage"; 53 | } 54 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.log 4 | *.pyc 5 | *.swp 6 | .DS_Store 7 | .atom/ 8 | .buildlog/ 9 | .history 10 | .svn/ 11 | .vscode 12 | pubspec.lock 13 | 14 | # IntelliJ related 15 | *.iml 16 | *.ipr 17 | *.iws 18 | .idea/ 19 | 20 | # The .vscode folder contains launch configuration and tasks you configure in 21 | # VS Code which you may wish to be included in version control, so this line 22 | # is commented out by default. 23 | #.vscode/ 24 | 25 | # Flutter/Dart/Pub related 26 | **/doc/api/ 27 | .dart_tool/ 28 | .flutter-plugins 29 | .packages 30 | .pub-cache/ 31 | .pub/ 32 | /build/ 33 | .vscode 34 | 35 | # Android related 36 | **/android/**/gradle-wrapper.jar 37 | **/android/.gradle 38 | **/android/captures/ 39 | **/android/gradlew 40 | **/android/gradlew.bat 41 | **/android/local.properties 42 | **/android/**/GeneratedPluginRegistrant.java 43 | 44 | # iOS/XCode related 45 | **/ios/**/*.mode1v3 46 | **/ios/**/*.mode2v3 47 | **/ios/**/*.moved-aside 48 | **/ios/**/*.pbxuser 49 | **/ios/**/*.perspectivev3 50 | **/ios/**/*sync/ 51 | **/ios/**/.sconsign.dblite 52 | **/ios/**/.tags* 53 | **/ios/**/.vagrant/ 54 | **/ios/**/DerivedData/ 55 | **/ios/**/Icon? 56 | **/ios/**/Pods/ 57 | **/ios/**/.symlinks/ 58 | **/ios/**/profile 59 | **/ios/**/xcuserdata 60 | **/ios/.generated/ 61 | **/ios/Flutter/App.framework 62 | **/ios/Flutter/Flutter.framework 63 | **/ios/Flutter/Generated.xcconfig 64 | **/ios/Flutter/app.flx 65 | **/ios/Flutter/app.zip 66 | **/ios/Flutter/flutter_assets/ 67 | **/ios/ServiceDefinitions.json 68 | **/ios/Runner/GeneratedPluginRegistrant.* 69 | 70 | ios/Podfile.lock 71 | 72 | # Exceptions to above rules. 73 | !**/ios/**/default.mode1v3 74 | !**/ios/**/default.mode2v3 75 | !**/ios/**/default.pbxuser 76 | !**/ios/**/default.perspectivev3 77 | !/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages 78 | -------------------------------------------------------------------------------- /lib/im/util/http_util.dart: -------------------------------------------------------------------------------- 1 | import 'package:dio/dio.dart'; 2 | import 'package:connectivity/connectivity.dart'; 3 | 4 | class HttpUtil { 5 | static Dio dio = Dio(); 6 | static void get(String url, Function callback, 7 | {Map params, Function errorCallback}) async { 8 | if (params != null && params.isNotEmpty) { 9 | StringBuffer buffer = new StringBuffer("?"); 10 | params.forEach((key, value) { 11 | buffer.write("$key" + "=" + "$value" + "&"); 12 | }); 13 | String paramStr = buffer.toString(); 14 | paramStr = paramStr.substring(0, paramStr.length - 1); 15 | url += paramStr; 16 | } 17 | Response response; 18 | try { 19 | response = await dio.get(url); 20 | print(response); 21 | if (callback != null) { 22 | callback(response.data); 23 | } 24 | } catch (e) { 25 | print(e.toString()); 26 | if (errorCallback != null) { 27 | errorCallback(e); 28 | } 29 | } 30 | } 31 | 32 | static void post(String url, Function callback, 33 | {Map params, Function errorCallback}) async { 34 | var connectivityResult = await (Connectivity().checkConnectivity()); 35 | if (connectivityResult == ConnectivityResult.none) { 36 | Map body = {"code": -1}; 37 | callback(body); 38 | } else { 39 | // try { 40 | Response response; 41 | response = await Dio().post(url, data: params); 42 | print(response); 43 | if (callback != null) { 44 | callback(response.data); 45 | } 46 | // } catch (e) { 47 | // print(e); 48 | // if (errorCallback != null) { 49 | // errorCallback(e); 50 | // } 51 | // } 52 | } 53 | } 54 | 55 | // 下载 56 | static Future download(String url, String savePath, 57 | Function(int count, int total) progressCallback) async { 58 | return Dio().download(url, savePath, onReceiveProgress: progressCallback); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /lib/im/pages/item/bottom_tool_bar.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import '../../util/style.dart'; 3 | import 'dart:developer' as developer; 4 | 5 | class BottomToolBar extends StatefulWidget { 6 | BottomToolBarDelegate delegate; 7 | BottomToolBar(BottomToolBarDelegate delegate) { 8 | this.delegate = delegate; 9 | } 10 | 11 | @override 12 | State createState() { 13 | return _BottomToolBarState(delegate); 14 | } 15 | } 16 | 17 | class _BottomToolBarState extends State { 18 | String pageName = "example.BottomToolBar"; 19 | BottomToolBarDelegate delegate; 20 | _BottomToolBarState(BottomToolBarDelegate delegate) { 21 | this.delegate = delegate; 22 | } 23 | 24 | @override 25 | void initState() { 26 | super.initState(); 27 | } 28 | 29 | @override 30 | Widget build(BuildContext context) { 31 | return Container( 32 | color: Colors.white, 33 | child: Row( 34 | mainAxisAlignment: MainAxisAlignment.spaceEvenly, 35 | children: [ 36 | IconButton( 37 | icon: Icon(Icons.delete), 38 | iconSize: RCLayout.BottomIconLayoutSize, 39 | onPressed: () { 40 | tapDelete(); 41 | }, 42 | ), 43 | IconButton( 44 | icon: Icon(Icons.forward), 45 | iconSize: RCLayout.BottomIconLayoutSize, 46 | onPressed: () { 47 | tapForward(); 48 | }, 49 | ), 50 | ], 51 | ), 52 | ); 53 | } 54 | 55 | void tapDelete() { 56 | if (this.delegate != null) { 57 | this.delegate.didTapDelete(); 58 | } else { 59 | developer.log("没有实现 BottomToolBarDelegate", name: pageName); 60 | } 61 | } 62 | 63 | void tapForward() { 64 | if (this.delegate != null) { 65 | this.delegate.didTapForward(); 66 | } else { 67 | developer.log("没有实现 BottomToolBarDelegate", name: pageName); 68 | } 69 | } 70 | } 71 | 72 | abstract class BottomToolBarDelegate { 73 | void didTapDelete(); 74 | void didTapForward(); 75 | } 76 | -------------------------------------------------------------------------------- /ios/Runner/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | RC_Flutter_Demo 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 1.0.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1.0.0 23 | LSRequiresIPhoneOS 24 | 25 | NSAppTransportSecurity 26 | 27 | NSAllowsArbitraryLoads 28 | 29 | NSAllowsArbitraryLoadsInWebContent 30 | 31 | 32 | NSCameraUsageDescription 33 | 访问相机 34 | NSMicrophoneUsageDescription 35 | 使用麦克风 36 | NSPhotoLibraryUsageDescription 37 | 访问相册 38 | UILaunchStoryboardName 39 | LaunchScreen 40 | UIMainStoryboardFile 41 | Main 42 | UISupportedInterfaceOrientations 43 | 44 | UIInterfaceOrientationPortrait 45 | UIInterfaceOrientationLandscapeLeft 46 | UIInterfaceOrientationLandscapeRight 47 | 48 | UISupportedInterfaceOrientations~ipad 49 | 50 | UIInterfaceOrientationPortrait 51 | UIInterfaceOrientationPortraitUpsideDown 52 | UIInterfaceOrientationLandscapeLeft 53 | UIInterfaceOrientationLandscapeRight 54 | 55 | UIViewControllerBasedStatusBarAppearance 56 | 57 | io.flutter.embedded_views_preview 58 | 59 | 60 | 61 | -------------------------------------------------------------------------------- /lib/router.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'im/pages/file_preview_page.dart'; 3 | import 'other/search_message_page.dart'; 4 | 5 | import 'im/pages/conversation_page.dart'; 6 | import 'im/pages/image_preview_page.dart'; 7 | import 'im/pages/sight/video_play_page.dart'; 8 | import 'im/pages/sight/video_record_page.dart'; 9 | import 'im/pages/webview_page.dart'; 10 | 11 | import 'other/home_page.dart'; 12 | import 'other/debug_page.dart'; 13 | import 'other/message_read_page.dart'; 14 | import 'other/chat_debug_page.dart'; 15 | import 'other/chatroom_debug_page.dart'; 16 | import 'other/select_conversation_page.dart'; 17 | 18 | final routes = { 19 | '/': (context) => HomePage(), 20 | '/conversation': (context, {arguments}) => 21 | ConversationPage(arguments: arguments), 22 | '/image_preview': (context, {arguments}) => 23 | ImagePreviewPage(message: arguments), 24 | '/debug': (context) => DebugPage(), 25 | '/video_record': (context, {arguments}) => 26 | VideoRecordPage(arguments: arguments), 27 | '/video_play': (context, {arguments}) => VideoPlayPage(message: arguments), 28 | '/message_read_page': (context, {arguments}) => 29 | MessageReadPage(message: arguments), 30 | '/file_preview': (context, {arguments}) => 31 | FilePreviewPage(message: arguments), 32 | '/webview': (context, {arguments}) => WebViewPage(arguments: arguments), 33 | '/chat_debug': (context, {arguments}) => ChatDebugPage(arguments: arguments), 34 | '/chatroom_debug': (context, {arguments}) => ChatRoomDebugPage(), 35 | '/search_message': (context, {arguments}) => 36 | SearchMessagePage(arguments: arguments), 37 | '/select_conversation_page': (context, {arguments}) => 38 | SelectConversationPage(arguments: arguments), 39 | }; 40 | 41 | var onGenerateRoute = (RouteSettings settings) { 42 | // 统一处理 43 | final String name = settings.name; 44 | final Function pageContentBuilder = routes[name]; 45 | if (pageContentBuilder != null) { 46 | if (settings.arguments != null) { 47 | final Route route = MaterialPageRoute( 48 | builder: (context) => 49 | pageContentBuilder(context, arguments: settings.arguments)); 50 | return route; 51 | } else { 52 | final Route route = 53 | MaterialPageRoute(builder: (context) => pageContentBuilder(context)); 54 | return route; 55 | } 56 | } 57 | }; 58 | -------------------------------------------------------------------------------- /ios/Runner/RCDTestMessage.m: -------------------------------------------------------------------------------- 1 | // 2 | // RCDTestMessage.m 3 | // RCloudMessage 4 | // 5 | // Created by 岑裕 on 15/12/17. 6 | // Copyright © 2015年 RongCloud. All rights reserved. 7 | // 8 | 9 | #import "RCDTestMessage.h" 10 | 11 | @implementation RCDTestMessage 12 | 13 | ///初始化 14 | + (instancetype)messageWithContent:(NSString *)content { 15 | RCDTestMessage *text = [[RCDTestMessage alloc] init]; 16 | if (text) { 17 | text.content = content; 18 | } 19 | return text; 20 | } 21 | 22 | ///消息是否存储,是否计入未读数 23 | + (RCMessagePersistent)persistentFlag { 24 | return (MessagePersistent_ISPERSISTED | MessagePersistent_ISCOUNTED); 25 | } 26 | 27 | /// NSCoding 28 | - (instancetype)initWithCoder:(NSCoder *)aDecoder { 29 | self = [super init]; 30 | if (self) { 31 | self.content = [aDecoder decodeObjectForKey:@"content"]; 32 | self.extra = [aDecoder decodeObjectForKey:@"extra"]; 33 | } 34 | return self; 35 | } 36 | 37 | /// NSCoding 38 | - (void)encodeWithCoder:(NSCoder *)aCoder { 39 | [aCoder encodeObject:self.content forKey:@"content"]; 40 | [aCoder encodeObject:self.extra forKey:@"extra"]; 41 | } 42 | 43 | ///将消息内容编码成json 44 | - (NSData *)encode { 45 | NSMutableDictionary *dataDict = [NSMutableDictionary dictionary]; 46 | [dataDict setObject:self.content forKey:@"content"]; 47 | if (self.extra) { 48 | [dataDict setObject:self.extra forKey:@"extra"]; 49 | } 50 | 51 | if (self.senderUserInfo) { 52 | [dataDict setObject:[self encodeUserInfo:self.senderUserInfo] forKey:@"user"]; 53 | } 54 | 55 | NSData *data = [NSJSONSerialization dataWithJSONObject:dataDict options:kNilOptions error:nil]; 56 | return data; 57 | } 58 | 59 | ///将json解码生成消息内容 60 | - (void)decodeWithData:(NSData *)data { 61 | if (data) { 62 | __autoreleasing NSError *error = nil; 63 | 64 | NSDictionary *dictionary = [NSJSONSerialization JSONObjectWithData:data options:kNilOptions error:&error]; 65 | 66 | if (dictionary) { 67 | self.content = dictionary[@"content"]; 68 | self.extra = dictionary[@"extra"]; 69 | 70 | NSDictionary *userinfoDic = dictionary[@"user"]; 71 | [self decodeUserInfo:userinfoDic]; 72 | } 73 | } 74 | } 75 | 76 | /// 会话列表中显示的摘要 77 | - (NSString *)conversationDigest { 78 | return self.content; 79 | } 80 | 81 | ///消息的类型名 82 | + (NSString *)getObjectName { 83 | return RCDTestMessageTypeIdentifier; 84 | } 85 | 86 | @end 87 | -------------------------------------------------------------------------------- /lib/im/pages/sight/video_play_page.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:flutter/widgets.dart'; 4 | 5 | import 'package:rongcloud_im_plugin/rongcloud_im_plugin.dart'; 6 | import 'package:video_player/video_player.dart'; 7 | 8 | class VideoPlayPage extends StatefulWidget { 9 | final Message message; 10 | const VideoPlayPage({Key key, this.message}) : super(key: key); 11 | @override 12 | State createState() { 13 | return _VideoPlayPageState(message); 14 | } 15 | } 16 | 17 | class _VideoPlayPageState extends State { 18 | final Message message; 19 | 20 | VideoPlayerController videoPlayerController; 21 | SightMessage sightMessage; 22 | 23 | _VideoPlayPageState(this.message); 24 | 25 | @override 26 | void initState() { 27 | super.initState(); 28 | initVideoController(); 29 | } 30 | 31 | @override 32 | void dispose() { 33 | videoPlayerController?.dispose(); 34 | super.dispose(); 35 | } 36 | 37 | void initVideoController() async { 38 | sightMessage = message.content; 39 | if (sightMessage.localPath != null && sightMessage.localPath != "") { 40 | videoPlayerController = 41 | VideoPlayerController.file(File(sightMessage.localPath)); 42 | } else { 43 | //TODO 是否需要做缓存? VideoPlayerController.network 每次都会下载一遍视频 44 | videoPlayerController = 45 | VideoPlayerController.network(sightMessage.remoteUrl); 46 | } 47 | videoPlayerController.initialize(); 48 | await videoPlayerController.play(); 49 | setState(() {}); 50 | } 51 | 52 | void onPop() { 53 | Navigator.pop(context); 54 | } 55 | 56 | @override 57 | Widget build(BuildContext context) { 58 | return Stack( 59 | children: [ 60 | VideoPlayer(videoPlayerController), 61 | Container( 62 | height: 100, 63 | width: MediaQuery.of(context).size.width, 64 | child: Row( 65 | children: [ 66 | SizedBox( 67 | width: 40, 68 | ), 69 | GestureDetector( 70 | onTap: () { 71 | onPop(); 72 | }, 73 | child: Container( 74 | width: 25, 75 | height: 25, 76 | child: Image.asset( 77 | "assets/images/sight_top_toolbar_close.png")), 78 | ) 79 | ], 80 | ), 81 | ) 82 | ], 83 | ); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /lib/im/util/dialog_util.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class DialogUtil { 4 | static void showAlertDiaLog(BuildContext context, String content, 5 | {String title = '', TextButton confirmButton}) { 6 | showDialog( 7 | barrierDismissible: false, // 设置点击 dialog 外部不取消 dialog,默认能够取消 8 | context: context, 9 | builder: (context) => AlertDialog( 10 | title: Text(title), 11 | titleTextStyle: TextStyle(color: Colors.black), 12 | content: Text(content), 13 | contentTextStyle: TextStyle(color: Colors.black), 14 | backgroundColor: Colors.white, 15 | elevation: 8.0, // 投影的阴影高度 16 | semanticLabel: 'Label', // 这个用于无障碍下弹出 dialog 的提示 17 | shape: Border.all(), 18 | actions: [ 19 | confirmButton != null 20 | ? confirmButton 21 | : TextButton( 22 | onPressed: () => Navigator.pop(context), 23 | child: Text("确定")), 24 | ], 25 | )); 26 | } 27 | 28 | static void showBottomSheetDialog( 29 | BuildContext mContext, Map tips) { 30 | if (tips == null || tips.length <= 0) { 31 | return; 32 | } 33 | showModalBottomSheet( 34 | context: mContext, 35 | builder: (context) => Container( 36 | child: ListView.separated( 37 | key: UniqueKey(), 38 | controller: ScrollController(), 39 | itemCount: tips.length, 40 | itemBuilder: (BuildContext context, int index) { 41 | return InkWell( 42 | child: Container( 43 | alignment: Alignment.center, 44 | height: 60, 45 | child: Text(tips.keys.toList()[index])), 46 | onTap: () { 47 | Function() clickEvent = tips.values.toList()[index]; 48 | if (clickEvent != null) { 49 | Navigator.pop(mContext); 50 | clickEvent(); 51 | } 52 | }); 53 | }, 54 | separatorBuilder: (BuildContext context, int index) { 55 | return Container( 56 | color: Color(0xffC8C8C8), 57 | height: 0.5, 58 | ); 59 | }), 60 | height: (60 * tips.length * 1.0), 61 | ), 62 | ); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /ios/Runner/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /lib/im/util/file_suffix.dart: -------------------------------------------------------------------------------- 1 | class FileSuffix { 2 | static const List ImageFileSuffix = [ 3 | '.bmp', 4 | '.cod', 5 | '.gif', 6 | '.ief', 7 | '.jpe', 8 | '.jpeg', 9 | '.jpg', 10 | '.jfif', 11 | '.svg', 12 | '.tif', 13 | '.tiff', 14 | '.ras', 15 | '.ico', 16 | '.pbm', 17 | '.pgm', 18 | '.png', 19 | '.pnm', 20 | '.ppm', 21 | '.xbm', 22 | '.xpm', 23 | '.xwd', 24 | '.rgb' 25 | ]; 26 | 27 | static const List FileFileSuffix = [ 28 | '.txt', 29 | '.log', 30 | '.html', 31 | '.stm', 32 | '.uls', 33 | '.bas', 34 | '.c', 35 | '.h', 36 | '.rtx', 37 | '.sct', 38 | '.tsv', 39 | '.htt', 40 | '.htc', 41 | '.etx', 42 | '.vcf', 43 | ]; 44 | 45 | static const List VideoFileSuffix = [ 46 | '.rmvb', 47 | '.avi', 48 | '.mp4', 49 | '.mp2', 50 | '.mpa', 51 | '.mpe', 52 | '.mpeg', 53 | '.mpg', 54 | '.mpv2', 55 | '.mov', 56 | '.qt', 57 | '.lsf', 58 | '.lsx', 59 | '.asf', 60 | '.asr', 61 | '.asx', 62 | '.avi', 63 | '.movie', 64 | '.wmv', 65 | ]; 66 | 67 | static const List AudioFileSuffix = [ 68 | '.mp3', 69 | '.au', 70 | '.snd', 71 | '.mid', 72 | '.rmi', 73 | '.aif', 74 | '.aifc', 75 | '.aiff', 76 | '.m3u', 77 | '.ra', 78 | '.ram', 79 | '.wav', 80 | '.wma', 81 | ]; 82 | 83 | static const List WordFileSuffix = [ 84 | '.doc', 85 | '.dot', 86 | '.docx', 87 | ]; 88 | 89 | static const List ExcelFileSuffix = [ 90 | '.xla', 91 | '.xlc', 92 | '.xlm', 93 | '.xls', 94 | '.xlt', 95 | '.xlw', 96 | '.xlsx', 97 | ]; 98 | 99 | static const List PptFileSuffix = [ 100 | '.ppt', 101 | '.pptx', 102 | ]; 103 | 104 | static const List PdfFileSuffix = [ 105 | '.pdf', 106 | ]; 107 | 108 | static const List ApkFileSuffix = [ 109 | '.apk', 110 | ]; 111 | 112 | static const List OtherFileSuffix = [ 113 | '.doc', 114 | '.dot', 115 | '.xla', 116 | '.xlc', 117 | '.xlm', 118 | '.xls', 119 | '.xlt', 120 | '.xlw', 121 | '.gif', 122 | '.jpe', 123 | '.jpeg', 124 | '.jpg', 125 | ]; 126 | 127 | static const List KeyFileSuffix = [ 128 | '.key', 129 | ]; 130 | 131 | static const List PagesFileSuffix = [ 132 | '.pages', 133 | ]; 134 | 135 | static const List NumbersFileSuffix = [ 136 | '.numbers', 137 | ]; 138 | } 139 | -------------------------------------------------------------------------------- /ios/Runner/AppDelegate.m: -------------------------------------------------------------------------------- 1 | #include "AppDelegate.h" 2 | #include "GeneratedPluginRegistrant.h" 3 | #import 4 | #import "RCDTestMessage.h" 5 | #import 6 | 7 | @implementation AppDelegate 8 | 9 | - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { 10 | [GeneratedPluginRegistrant registerWithRegistry:self]; 11 | /** 12 | //注册自定义消息流程 13 | 用户只需要调用以下方法注册自定义消息。SDK 内部会在 connectWithToken: 方法里,链接 IM 之前 把所有自定义消息注册到底层 SDK。 14 | */ 15 | [[RCIMFlutterWrapper sharedWrapper] registerMessageType:[RCDTestMessage class]]; 16 | 17 | /** 18 | * 推送处理1 (申请推送权限) 19 | */ 20 | if ([application 21 | respondsToSelector:@selector(registerUserNotificationSettings:)]) { 22 | //注册推送, 用于iOS8以及iOS8之后的系统 23 | UIUserNotificationSettings *settings = [UIUserNotificationSettings 24 | settingsForTypes:(UIUserNotificationTypeBadge | 25 | UIUserNotificationTypeSound | 26 | UIUserNotificationTypeAlert) 27 | categories:nil]; 28 | [application registerUserNotificationSettings:settings]; 29 | } 30 | 31 | /** 32 | // 远程推送的内容 33 | NSDictionary *remoteNotificationUserInfo = launchOptions[UIApplicationLaunchOptionsRemoteNotificationKey]; 34 | // 传递远程推送数据 35 | if (remoteNotificationUserInfo != nil) { 36 | //远程推送的数据,延时之后再调用该接口,防止Flutter尚未初始化就调用,导致Flutter无法接受数据 37 | dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ 38 | [[RCIMFlutterWrapper sharedWrapper] sendDataToFlutter:remoteNotificationUserInfo]; 39 | }); 40 | } 41 | */ 42 | return [super application:application didFinishLaunchingWithOptions:launchOptions]; 43 | } 44 | 45 | /** 46 | * 推送处理2 47 | */ 48 | - (void)application:(UIApplication *)application didRegisterUserNotificationSettings:(UIUserNotificationSettings *)notificationSettings { 49 | // register to receive notifications 50 | [application registerForRemoteNotifications]; 51 | } 52 | 53 | /** 54 | * 推送处理3 55 | */ 56 | - (void)application:(UIApplication *)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken { 57 | // 如果您的 SDK 版本已升级到 2.9.25,请使用下面这种方式: 58 | [[RCIMClient sharedRCIMClient] setDeviceTokenData:deviceToken]; 59 | } 60 | 61 | - (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo{ 62 | 63 | // [[RCIMFlutterWrapper sharedWrapper] sendDataToFlutter:userInfo]; 64 | } 65 | 66 | 67 | 68 | 69 | @end 70 | -------------------------------------------------------------------------------- /lib/other/message_read_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/cupertino.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:cached_network_image/cached_network_image.dart'; 4 | import 'package:rongcloud_im_plugin/rongcloud_im_plugin.dart' as prefix; 5 | 6 | import '../im/util/user_info_datesource.dart' as example; 7 | 8 | class MessageReadPage extends StatefulWidget { 9 | final prefix.Message message; 10 | const MessageReadPage({Key key, this.message}) : super(key: key); 11 | 12 | @override 13 | State createState() => _MessageReadPageState(message); 14 | } 15 | 16 | class _MessageReadPageState extends State { 17 | final prefix.Message message; 18 | _MessageReadPageState(this.message); 19 | 20 | List widgetList = []; 21 | List userList = []; 22 | @override 23 | void initState() { 24 | super.initState(); 25 | _addFriends(); 26 | } 27 | 28 | _addFriends() async { 29 | await _getRandomUserInfos(); 30 | for (example.UserInfo u in this.userList) { 31 | this.widgetList.add(getWidget(u)); 32 | } 33 | setState(() {}); 34 | } 35 | 36 | Future _getRandomUserInfos() async { 37 | Map userIdList = message.readReceiptInfo.userIdList; 38 | if (userIdList != null) { 39 | for (String key in userIdList.keys) { 40 | example.UserInfo userInfo = 41 | example.UserInfoDataSource.cachedUserMap[key]; 42 | if (userInfo == null) { 43 | userInfo = await example.UserInfoDataSource.getUserInfo(key); 44 | } 45 | if (userInfo != null) { 46 | this.userList.add(userInfo); 47 | } 48 | } 49 | // return this.userList; 50 | } 51 | } 52 | 53 | Widget getWidget(example.UserInfo user) { 54 | return Container( 55 | height: 50.0, 56 | color: Colors.white, 57 | child: InkWell( 58 | child: new ListTile( 59 | title: new Text(user.id), 60 | leading: Container( 61 | width: 36, 62 | height: 36, 63 | child: ClipRRect( 64 | borderRadius: BorderRadius.circular(5), 65 | child: CachedNetworkImage( 66 | fit: BoxFit.fill, 67 | imageUrl: user.portraitUrl, 68 | ), 69 | ), 70 | ), 71 | ), 72 | ), 73 | ); 74 | } 75 | 76 | @override 77 | Widget build(BuildContext context) { 78 | return Scaffold( 79 | appBar: AppBar( 80 | title: Text("已读成员列表"), 81 | ), 82 | body: ListView.builder( 83 | scrollDirection: Axis.vertical, 84 | itemCount: this.widgetList.length, 85 | itemBuilder: (BuildContext context, int index) { 86 | return this.widgetList[index]; 87 | }, 88 | ), 89 | ); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /lib/im/util/file.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import '../util/file_suffix.dart'; 4 | 5 | class FileUtil { 6 | static int kilobyte = 1024; 7 | static int megabyte = 1024 * 1024; 8 | static int gigabyte = 1024 * 1024 * 1024; 9 | static String formatFileSize(int size) { 10 | if (size < kilobyte) { 11 | return "$size B"; 12 | } else if (size < megabyte) { 13 | String sizeStr = (size / kilobyte).toStringAsFixed(2); 14 | return "$sizeStr KB"; 15 | } else if (size < gigabyte) { 16 | String sizeStr = (size / megabyte).toStringAsFixed(2); 17 | return "$sizeStr MB"; 18 | } else { 19 | String sizeStr = (size / gigabyte).toStringAsFixed(2); 20 | return "$sizeStr G"; 21 | } 22 | } 23 | 24 | // 根据文件类型选择对应图片 25 | static String fileTypeImagePath(String fileName) { 26 | String imagePath; 27 | if (checkSuffix(fileName, FileSuffix.ImageFileSuffix)) 28 | imagePath = "assets/images/file_message_icon_picture.png"; 29 | else if (checkSuffix(fileName, FileSuffix.FileFileSuffix)) 30 | imagePath = "assets/images/file_message_icon_file.png"; 31 | else if (checkSuffix(fileName, FileSuffix.VideoFileSuffix)) 32 | imagePath = "assets/images/file_message_icon_video.png"; 33 | else if (checkSuffix(fileName, FileSuffix.AudioFileSuffix)) 34 | imagePath = "assets/images/file_message_icon_audio.png"; 35 | else if (checkSuffix(fileName, FileSuffix.WordFileSuffix)) 36 | imagePath = "assets/images/file_message_icon_word.png"; 37 | else if (checkSuffix(fileName, FileSuffix.ExcelFileSuffix)) 38 | imagePath = "assets/images/file_message_icon_excel.png"; 39 | else if (checkSuffix(fileName, FileSuffix.PptFileSuffix)) 40 | imagePath = "assets/images/file_message_icon_ppt.png"; 41 | else if (checkSuffix(fileName, FileSuffix.PdfFileSuffix)) 42 | imagePath = "assets/images/file_message_icon_pdf.png"; 43 | else if (checkSuffix(fileName, FileSuffix.ApkFileSuffix)) 44 | imagePath = "assets/images/file_message_icon_apk.png"; 45 | else if (checkSuffix(fileName, FileSuffix.KeyFileSuffix)) 46 | imagePath = "assets/images/file_message_icon_key.png"; 47 | else if (checkSuffix(fileName, FileSuffix.NumbersFileSuffix)) 48 | imagePath = "assets/images/file_message_icon_numbers.png"; 49 | else if (checkSuffix(fileName, FileSuffix.PagesFileSuffix)) 50 | imagePath = "assets/images/file_message_icon_pages.png"; 51 | else 52 | imagePath = "assets/images/file_message_icon_else.png"; 53 | return imagePath; 54 | } 55 | 56 | static bool checkSuffix(String fileName, List fileSuffix) { 57 | for (String suffix in fileSuffix) { 58 | if (fileName != null) { 59 | if (fileName.toLowerCase().endsWith(suffix)) { 60 | return true; 61 | } 62 | } 63 | } 64 | return false; 65 | } 66 | 67 | static Future writeStringToFile( 68 | String filePath, String fileName, String content) async { 69 | Directory file = Directory(filePath); 70 | if (!file.existsSync()) { 71 | file.create(); 72 | } 73 | return File("$filePath/$fileName").writeAsString(content); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /lib/im/widget/cachImage/cached_network_image_provider.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async' show Future; 2 | import 'dart:io' show File; 3 | import 'dart:ui' as ui show instantiateImageCodec, Codec; 4 | 5 | import 'package:flutter/foundation.dart'; 6 | import 'package:flutter/material.dart'; 7 | import 'package:flutter_cache_manager/flutter_cache_manager.dart'; 8 | 9 | typedef void ErrorListener(); 10 | 11 | class CachedNetworkImageProvider 12 | extends ImageProvider { 13 | /// Creates an ImageProvider which loads an image from the [url], using the [scale]. 14 | /// When the image fails to load [errorListener] is called. 15 | const CachedNetworkImageProvider(this.url, 16 | {this.scale = 1.0, this.errorListener, this.headers, this.cacheManager}) 17 | : assert(url != null), 18 | assert(scale != null); 19 | 20 | final BaseCacheManager cacheManager; 21 | 22 | /// Web url of the image to load 23 | final String url; 24 | 25 | /// Scale of the image 26 | final double scale; 27 | 28 | /// Listener to be called when images fails to load. 29 | final ErrorListener errorListener; 30 | 31 | // Set headers for the image provider, for example for authentication 32 | final Map headers; 33 | 34 | @override 35 | Future obtainKey( 36 | ImageConfiguration configuration) { 37 | return SynchronousFuture(this); 38 | } 39 | 40 | @override 41 | ImageStreamCompleter load( 42 | CachedNetworkImageProvider key, DecoderCallback decode) { 43 | return MultiFrameImageStreamCompleter( 44 | codec: _loadAsync(key), 45 | scale: key.scale, 46 | informationCollector: () sync* { 47 | yield DiagnosticsProperty( 48 | 'Image provider: $this \n Image key: $key', 49 | this, 50 | style: DiagnosticsTreeStyle.errorProperty, 51 | ); 52 | }, 53 | ); 54 | } 55 | 56 | Future _loadAsync(CachedNetworkImageProvider key) async { 57 | var mngr = cacheManager ?? DefaultCacheManager(); 58 | var file = await mngr.getSingleFile(url, headers: headers); 59 | if (file == null) { 60 | if (errorListener != null) errorListener(); 61 | throw Exception('Couldn\'t download or retrieve file: $url'); 62 | } 63 | return _loadAsyncFromFile(key, file); 64 | } 65 | 66 | Future _loadAsyncFromFile( 67 | CachedNetworkImageProvider key, File file) async { 68 | assert(key == this); 69 | 70 | final bytes = await file.readAsBytes(); 71 | 72 | if (bytes.lengthInBytes == 0) { 73 | if (errorListener != null) errorListener(); 74 | throw Exception('File was empty'); 75 | } 76 | 77 | return ui.instantiateImageCodec(bytes); 78 | } 79 | 80 | @override 81 | bool operator ==(dynamic other) { 82 | if (other is CachedNetworkImageProvider) { 83 | return url == other.url && scale == other.scale; 84 | } 85 | return false; 86 | } 87 | 88 | @override 89 | int get hashCode => hashValues(url, scale); 90 | 91 | @override 92 | String toString() => '$runtimeType("$url", scale: $scale)'; 93 | } 94 | -------------------------------------------------------------------------------- /lib/im/pages/image_preview_page.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:rongcloud_im_plugin/rongcloud_im_plugin.dart'; 5 | import '../util/media_util.dart'; 6 | 7 | class ImagePreviewPage extends StatefulWidget { 8 | final Message message; 9 | const ImagePreviewPage({Key key, this.message}) : super(key: key); 10 | 11 | @override 12 | State createState() { 13 | return _ImagePreviewPageState(message); 14 | } 15 | } 16 | 17 | class _ImagePreviewPageState extends State { 18 | final Message message; 19 | 20 | _ImagePreviewPageState(this.message); 21 | 22 | //优先加载本地路径图片,否则加载网络图片 23 | Widget getImageWidget() { 24 | String localPath; 25 | String remoteUrl; 26 | if (message.content is GifMessage) { 27 | GifMessage msg = message.content; 28 | localPath = msg.localPath; 29 | remoteUrl = msg.remoteUrl; 30 | } else { 31 | ImageMessage msg = message.content; 32 | localPath = msg.localPath; 33 | remoteUrl = msg.imageUri; 34 | } 35 | Widget widget; 36 | if (localPath != null) { 37 | String path = MediaUtil.instance.getCorrectedLocalPath(localPath); 38 | File file = File(path); 39 | if (file != null && file.existsSync()) { 40 | widget = Image.file(file); 41 | } else { 42 | widget = Image.network( 43 | remoteUrl, 44 | fit: BoxFit.cover, 45 | loadingBuilder: (BuildContext context, Widget child, 46 | ImageChunkEvent loadingProgress) { 47 | if (loadingProgress == null) return child; 48 | return Center( 49 | child: CircularProgressIndicator( 50 | value: loadingProgress.expectedTotalBytes != null 51 | ? loadingProgress.cumulativeBytesLoaded / 52 | loadingProgress.expectedTotalBytes 53 | : null, 54 | ), 55 | ); 56 | }, 57 | ); 58 | } 59 | } else { 60 | widget = Image.network( 61 | remoteUrl, 62 | fit: BoxFit.cover, 63 | loadingBuilder: (BuildContext context, Widget child, 64 | ImageChunkEvent loadingProgress) { 65 | if (loadingProgress == null) return child; 66 | return Center( 67 | child: CircularProgressIndicator( 68 | value: loadingProgress.expectedTotalBytes != null 69 | ? loadingProgress.cumulativeBytesLoaded / 70 | loadingProgress.expectedTotalBytes 71 | : null, 72 | ), 73 | ); 74 | }, 75 | ); 76 | } 77 | // Container container = Container( 78 | // margin: EdgeInsets.all(2), 79 | // child: widget, 80 | // alignment: Alignment.center, 81 | // ); 82 | // return container; 83 | return widget; 84 | // return container; 85 | } 86 | 87 | @override 88 | Widget build(BuildContext context) { 89 | return Scaffold( 90 | appBar: AppBar( 91 | title: Text("图片预览"), 92 | ), 93 | body: SingleChildScrollView( 94 | child: getImageWidget(), 95 | )); 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "size" : "20x20", 5 | "idiom" : "iphone", 6 | "filename" : "Icon-App-20x20@2x.png", 7 | "scale" : "2x" 8 | }, 9 | { 10 | "size" : "20x20", 11 | "idiom" : "iphone", 12 | "filename" : "Icon-App-20x20@3x.png", 13 | "scale" : "3x" 14 | }, 15 | { 16 | "size" : "29x29", 17 | "idiom" : "iphone", 18 | "filename" : "Icon-App-29x29@1x.png", 19 | "scale" : "1x" 20 | }, 21 | { 22 | "size" : "29x29", 23 | "idiom" : "iphone", 24 | "filename" : "Icon-App-29x29@2x.png", 25 | "scale" : "2x" 26 | }, 27 | { 28 | "size" : "29x29", 29 | "idiom" : "iphone", 30 | "filename" : "Icon-App-29x29@3x.png", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "size" : "40x40", 35 | "idiom" : "iphone", 36 | "filename" : "Icon-App-40x40@2x.png", 37 | "scale" : "2x" 38 | }, 39 | { 40 | "size" : "40x40", 41 | "idiom" : "iphone", 42 | "filename" : "Icon-App-40x40@3x.png", 43 | "scale" : "3x" 44 | }, 45 | { 46 | "size" : "60x60", 47 | "idiom" : "iphone", 48 | "filename" : "Icon-App-60x60@2x.png", 49 | "scale" : "2x" 50 | }, 51 | { 52 | "size" : "60x60", 53 | "idiom" : "iphone", 54 | "filename" : "Icon-App-60x60@3x.png", 55 | "scale" : "3x" 56 | }, 57 | { 58 | "size" : "20x20", 59 | "idiom" : "ipad", 60 | "filename" : "Icon-App-20x20@1x.png", 61 | "scale" : "1x" 62 | }, 63 | { 64 | "size" : "20x20", 65 | "idiom" : "ipad", 66 | "filename" : "Icon-App-20x20@2x.png", 67 | "scale" : "2x" 68 | }, 69 | { 70 | "size" : "29x29", 71 | "idiom" : "ipad", 72 | "filename" : "Icon-App-29x29@1x.png", 73 | "scale" : "1x" 74 | }, 75 | { 76 | "size" : "29x29", 77 | "idiom" : "ipad", 78 | "filename" : "Icon-App-29x29@2x.png", 79 | "scale" : "2x" 80 | }, 81 | { 82 | "size" : "40x40", 83 | "idiom" : "ipad", 84 | "filename" : "Icon-App-40x40@1x.png", 85 | "scale" : "1x" 86 | }, 87 | { 88 | "size" : "40x40", 89 | "idiom" : "ipad", 90 | "filename" : "Icon-App-40x40@2x.png", 91 | "scale" : "2x" 92 | }, 93 | { 94 | "size" : "76x76", 95 | "idiom" : "ipad", 96 | "filename" : "Icon-App-76x76@1x.png", 97 | "scale" : "1x" 98 | }, 99 | { 100 | "size" : "76x76", 101 | "idiom" : "ipad", 102 | "filename" : "Icon-App-76x76@2x.png", 103 | "scale" : "2x" 104 | }, 105 | { 106 | "size" : "83.5x83.5", 107 | "idiom" : "ipad", 108 | "filename" : "Icon-App-83.5x83.5@2x.png", 109 | "scale" : "2x" 110 | }, 111 | { 112 | "size" : "1024x1024", 113 | "idiom" : "ios-marketing", 114 | "filename" : "Icon-App-1024x1024@1x.png", 115 | "scale" : "1x" 116 | } 117 | ], 118 | "info" : { 119 | "version" : 1, 120 | "author" : "xcode" 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /lib/im/util/db_manager.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:path/path.dart'; 4 | import 'user_info_datesource.dart'; 5 | import 'package:sqflite/sqflite.dart'; 6 | 7 | class DbManager { 8 | factory DbManager() => _getInstance(); 9 | static DbManager get instance => _getInstance(); 10 | static DbManager _instance; 11 | static Database database; 12 | static String dbName = 'UserInfoCache.db'; 13 | static String userTableName = 'users'; 14 | static String groupTableName = 'groups'; 15 | 16 | DbManager._internal() { 17 | // 初始化 18 | } 19 | 20 | static DbManager _getInstance() { 21 | if (_instance == null) { 22 | _instance = new DbManager._internal(); 23 | } 24 | return _instance; 25 | } 26 | 27 | Future openDb() async { 28 | database = await openDatabase(join(await getDatabasesPath(), dbName), 29 | onCreate: (db, version) { 30 | db.execute( 31 | "CREATE TABLE $userTableName(userId TEXT PRIMARY KEY, name TEXT, portraitUrl TEXT) "); 32 | db.execute( 33 | "CREATE TABLE $groupTableName(groupId TEXT PRIMARY KEY, name TEXT, portraitUrl TEXT)"); 34 | }, version: 1); 35 | } 36 | 37 | Future setUserInfo(UserInfo info) async { 38 | if (database == null) { 39 | await openDb(); 40 | } 41 | await database?.insert(userTableName, info.toMap(), 42 | conflictAlgorithm: ConflictAlgorithm.replace); 43 | } 44 | 45 | Future> getUserInfo({String userId}) async { 46 | List> maps = []; 47 | if (database == null) { 48 | await openDb(); 49 | } 50 | if (userId == null || userId.isEmpty) { 51 | maps = await database?.query(userTableName); 52 | } else { 53 | maps = await database 54 | ?.query(userTableName, where: 'userId = ?', whereArgs: [userId]); 55 | } 56 | List infoList = []; 57 | if (maps.length > 0) { 58 | infoList = List.generate(maps.length, (i) { 59 | UserInfo info = UserInfo(); 60 | info.id = maps[i]['userId']; 61 | info.name = maps[i]['name']; 62 | info.portraitUrl = maps[i]['portraitUrl']; 63 | return info; 64 | }); 65 | } 66 | return infoList; 67 | } 68 | 69 | Future setGroupInfo(GroupInfo info) async { 70 | if (database == null) { 71 | await openDb(); 72 | } 73 | await database?.insert(groupTableName, info.toMap(), 74 | conflictAlgorithm: ConflictAlgorithm.replace); 75 | } 76 | 77 | Future> getGroupInfo({String groupId}) async { 78 | List> maps = []; 79 | if (database == null) { 80 | await openDb(); 81 | } 82 | if (groupId == null || groupId.isEmpty) { 83 | maps = await database?.query(groupTableName); 84 | } else { 85 | maps = await database 86 | ?.query(groupTableName, where: 'groupId = ?', whereArgs: [groupId]); 87 | } 88 | List infoList = []; 89 | if (maps.length > 0) { 90 | infoList = List.generate(maps.length, (i) { 91 | GroupInfo info = GroupInfo(); 92 | info.id = maps[i]['groupId']; 93 | info.name = maps[i]['name']; 94 | info.portraitUrl = maps[i]['portraitUrl']; 95 | return info; 96 | }); 97 | } 98 | return infoList; 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /android/app/build.gradle: -------------------------------------------------------------------------------- 1 | def localProperties = new Properties() 2 | def localPropertiesFile = rootProject.file('local.properties') 3 | if (localPropertiesFile.exists()) { 4 | localPropertiesFile.withReader('UTF-8') { reader -> 5 | localProperties.load(reader) 6 | } 7 | } 8 | 9 | def flutterRoot = localProperties.getProperty('flutter.sdk') 10 | if (flutterRoot == null) { 11 | throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") 12 | } 13 | 14 | def flutterVersionCode = localProperties.getProperty('flutter.versionCode') 15 | if (flutterVersionCode == null) { 16 | flutterVersionCode = '1' 17 | } 18 | 19 | def flutterVersionName = localProperties.getProperty('flutter.versionName') 20 | if (flutterVersionName == null) { 21 | flutterVersionName = '1.0' 22 | } 23 | 24 | apply plugin: 'com.android.application' 25 | apply plugin: 'com.google.gms.google-services' 26 | apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" 27 | 28 | android { 29 | compileSdkVersion 28 30 | 31 | lintOptions { 32 | disable 'InvalidPackage' 33 | } 34 | 35 | defaultConfig { 36 | // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). 37 | applicationId "com.example.rongcloud_im_plugin_example" 38 | minSdkVersion 23 39 | targetSdkVersion 28 40 | versionCode flutterVersionCode.toInteger() 41 | versionName flutterVersionName 42 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" 43 | 44 | ndk { 45 | abiFilters "armeabi-v7a" 46 | } 47 | //,"x86" 48 | } 49 | 50 | signingConfigs { 51 | config { 52 | storeFile file("rong_flutter.key") 53 | storePassword "rongcloud" 54 | keyAlias "rongcloud" 55 | keyPassword "rongcloud" 56 | } 57 | } 58 | 59 | buildTypes { 60 | release { 61 | // TODO: Add your own signing config for the release build. 62 | // Signing with the debug keys for now, so `flutter run --release` works. 63 | signingConfig signingConfigs.debug 64 | shrinkResources false 65 | minifyEnabled false 66 | } 67 | } 68 | 69 | configurations.all { 70 | resolutionStrategy.eachDependency { details -> 71 | def requested = details.requested 72 | if (requested.group == 'com.android.support') { 73 | if (!requested.name.startsWith("multidex")) { 74 | details.useVersion '26.1.0' 75 | } 76 | } 77 | } 78 | } 79 | } 80 | 81 | flutter { 82 | source '../..' 83 | } 84 | 85 | dependencies { 86 | compile fileTree(include: ['*.jar', '*.aar'], dir: 'lib') 87 | testImplementation 'junit:junit:4.12' 88 | androidTestImplementation 'com.android.support.test:runner:1.0.2' 89 | androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2' 90 | 91 | implementation 'com.android.support:appcompat-v7:26.0.0' 92 | implementation 'com.huawei.hms:push:4.0.2.300' 93 | implementation("com.google.android.gms:play-services-gcm:12.0.1") { 94 | force = true 95 | } 96 | implementation 'com.google.firebase:firebase-messaging:17.6.0' 97 | } 98 | apply plugin: 'com.huawei.agconnect' 99 | apply plugin: 'com.android.application' 100 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 37 | 38 | 39 | 40 | 41 | 42 | 52 | 54 | 60 | 61 | 62 | 63 | 69 | 71 | 77 | 78 | 79 | 80 | 82 | 83 | 86 | 87 | 88 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: flutter_rong 2 | description: A new Flutter project. 3 | 4 | # The following defines the version and build number for your application. 5 | # A version number is three numbers separated by dots, like 1.2.43 6 | # followed by an optional build number separated by a +. 7 | # Both the version and the builder number may be overridden in flutter 8 | # build by specifying --build-name and --build-number, respectively. 9 | # In Android, build-name is used as versionName while build-number used as versionCode. 10 | # Read more about Android versioning at https://developer.android.com/studio/publish/versioning 11 | # In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. 12 | # Read more about iOS versioning at 13 | # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html 14 | version: 1.0.0+1 15 | 16 | environment: 17 | sdk: ">=2.1.0 <3.0.0" 18 | 19 | dependencies: 20 | flutter: 21 | sdk: flutter 22 | 23 | rongcloud_im_plugin: 5.1.3 24 | cupertino_icons: ^0.1.2 25 | cached_network_image: ^2.0.0-rc 26 | image_picker: ^0.6.0+20 27 | file_picker: ^1.4.3+2 28 | open_file: ^3.0.1 29 | flutter_audio_recorder: ^0.5.5 30 | path_provider: ^1.2.0 31 | permission_handler: ^5.0.0+hotfix.3 32 | flutter_sound: ^2.1.1 33 | flutter_local_notifications: ^0.8.1 34 | camera: ^0.5.5+1 35 | video_player: ^0.10.2+3 36 | percent_indicator: 2.1.1+1 37 | fluttertoast: ^3.1.3 38 | dio: ^3.0.9 39 | shared_preferences: ^0.5.6+3 40 | flutter_webview_plugin: ^0.3.11 41 | webview_flutter: ^0.3.19+9 42 | image_gallery_saver: ^1.2.2 43 | url_launcher: ^5.4.2 44 | rxdart: ^0.24.0 45 | flutter_cache_manager: ^1.2.0 46 | connectivity: ^0.4.8+2 47 | 48 | dev_dependencies: 49 | flutter_test: 50 | sdk: flutter 51 | 52 | 53 | # For information on the generic Dart part of this file, see the 54 | # following page: https://www.dartlang.org/tools/pub/pubspec 55 | 56 | # The following section is specific to Flutter. 57 | flutter: 58 | 59 | # The following line ensures that the Material Icons font is 60 | # included with your application, so that you can use the icons in 61 | # the material Icons class. 62 | uses-material-design: true 63 | 64 | assets: 65 | - assets/RCFlutterConf.json 66 | - assets/images/ 67 | - assets/combine.json 68 | 69 | # To add assets to your application, add an assets section, like this: 70 | # assets: 71 | # - images/a_dot_burr.jpeg 72 | # - images/a_dot_ham.jpeg 73 | 74 | # An image asset can refer to one or more resolution-specific "variants", see 75 | # https://flutter.io/assets-and-images/#resolution-aware. 76 | 77 | # For details regarding adding assets from package dependencies, see 78 | # https://flutter.io/assets-and-images/#from-packages 79 | 80 | # To add custom fonts to your application, add a fonts section here, 81 | # in this "flutter" section. Each entry in this list should have a 82 | # "family" key with the font family name, and a "fonts" key with a 83 | # list giving the asset and other descriptors for the font. For 84 | # example: 85 | # fonts: 86 | # - family: Schyler 87 | # fonts: 88 | # - asset: fonts/Schyler-Regular.ttf 89 | # - asset: fonts/Schyler-Italic.ttf 90 | # style: italic 91 | # - family: Trajan Pro 92 | # fonts: 93 | # - asset: fonts/TrajanPro.ttf 94 | # - asset: fonts/TrajanPro_Bold.ttf 95 | # weight: 700 96 | # 97 | # For details regarding fonts from package dependencies, 98 | # see https://flutter.io/custom-fonts/#from-packages 99 | -------------------------------------------------------------------------------- /lib/other/home_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import '../im/util/db_manager.dart'; 3 | import '../im/util/user_info_datesource.dart'; 4 | import '../im/pages/conversation_list_page.dart'; 5 | import 'contacts_page.dart'; 6 | import 'package:rongcloud_im_plugin/rongcloud_im_plugin.dart'; 7 | 8 | import 'login_page.dart'; 9 | import 'package:shared_preferences/shared_preferences.dart'; 10 | import '../im/util/event_bus.dart'; 11 | import 'dart:developer' as developer; 12 | 13 | class HomePage extends StatefulWidget { 14 | @override 15 | State createState() { 16 | return new _HomePageState(); 17 | } 18 | } 19 | 20 | class _HomePageState extends State { 21 | String pageName = "example.HomePage"; 22 | final List tabbarList = [ 23 | new BottomNavigationBarItem( 24 | icon: new Icon(Icons.chat, color: Colors.grey), 25 | label: "会话", 26 | ), 27 | new BottomNavigationBarItem( 28 | icon: new Icon( 29 | Icons.perm_contact_calendar, 30 | color: Colors.grey, 31 | ), 32 | label: "通讯录", 33 | ), 34 | ]; 35 | final List vcList = [ 36 | new ConversationListPage(), 37 | new ContactsPage() 38 | ]; 39 | 40 | int curIndex = 0; 41 | 42 | @override 43 | void initState() { 44 | super.initState(); 45 | _initUserInfoCache(); 46 | initPlatformState(); 47 | } 48 | 49 | initPlatformState() async { 50 | //1.初始化 im SDK 51 | // RongIMClient.init(RongAppKey); 52 | 53 | //2.连接 im SDK 54 | SharedPreferences prefs = await SharedPreferences.getInstance(); 55 | String token = prefs.get("token"); 56 | if (token != null && token.length > 0) { 57 | // int rc = await RongIMClient.connect(token); 58 | RongIMClient.connect(token, (int code, String userId) { 59 | developer.log("connect result " + code.toString(), name: pageName); 60 | EventBus.instance.commit(EventKeys.UpdateNotificationQuietStatus, {}); 61 | if (code == 31004 || code == 12) { 62 | developer.log("connect result " + code.toString(), name: pageName); 63 | Navigator.of(context).pushAndRemoveUntil( 64 | new MaterialPageRoute(builder: (context) => new LoginPage()), 65 | (route) => route == null); 66 | } else if (code == 0) { 67 | developer.log("connect userId" + userId, name: pageName); 68 | // 连接成功后打开数据库 69 | // _initUserInfoCache(); 70 | } 71 | }); 72 | } else { 73 | Navigator.of(context).pushAndRemoveUntil( 74 | new MaterialPageRoute(builder: (context) => new LoginPage()), 75 | (route) => route == null); 76 | } 77 | } 78 | 79 | // 初始化用户信息缓存 80 | void _initUserInfoCache() { 81 | DbManager.instance.openDb(); 82 | UserInfoCacheListener cacheListener = UserInfoCacheListener(); 83 | cacheListener.getUserInfo = (String userId) { 84 | return UserInfoDataSource.generateUserInfo(userId); 85 | }; 86 | cacheListener.getGroupInfo = (String groupId) { 87 | return UserInfoDataSource.generateGroupInfo(groupId); 88 | }; 89 | UserInfoDataSource.setCacheListener(cacheListener); 90 | } 91 | 92 | @override 93 | Widget build(BuildContext context) { 94 | return new Scaffold( 95 | bottomNavigationBar: new BottomNavigationBar( 96 | items: tabbarList, 97 | type: BottomNavigationBarType.fixed, 98 | onTap: (int index) { 99 | setState(() { 100 | curIndex = index; 101 | }); 102 | }, 103 | currentIndex: curIndex, 104 | ), 105 | body: IndexedStack( 106 | index: curIndex, 107 | children: [new ConversationListPage(), new ContactsPage()], 108 | ), 109 | ); 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /lib/other/contacts_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/cupertino.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:cached_network_image/cached_network_image.dart'; 4 | import 'package:rongcloud_im_plugin/rongcloud_im_plugin.dart' as prefix; 5 | import 'package:shared_preferences/shared_preferences.dart'; 6 | 7 | import '../im/util/user_info_datesource.dart' as example; 8 | import 'login_page.dart'; 9 | 10 | class ContactsPage extends StatefulWidget { 11 | @override 12 | State createState() { 13 | return new _ContactsPageState(); 14 | } 15 | } 16 | 17 | class _ContactsPageState extends State { 18 | List widgetList = []; 19 | List userList = []; 20 | @override 21 | void initState() { 22 | super.initState(); 23 | _addFriends(); 24 | } 25 | 26 | void _addFriends() { 27 | // List users = await _getRandomUserInfos(); 28 | _getRandomUserInfos().then((users) { 29 | for (example.UserInfo u in users) { 30 | this.widgetList.add(getWidget(u)); 31 | _refreshUI(); 32 | } 33 | }); 34 | } 35 | 36 | void _refreshUI() { 37 | setState(() {}); 38 | } 39 | 40 | Future> _getRandomUserInfos() async { 41 | this.userList.add(await example.UserInfoDataSource.getUserInfo("SealTalk")); 42 | this.userList.add(await example.UserInfoDataSource.getUserInfo("RongRTC")); 43 | this.userList.add(await example.UserInfoDataSource.getUserInfo("RongIM")); 44 | return this.userList; 45 | } 46 | 47 | void _onTapUser(example.UserInfo user) { 48 | Map arg = { 49 | "coversationType": prefix.RCConversationType.Private, 50 | "targetId": user.id 51 | }; 52 | Navigator.pushNamed(context, "/conversation", arguments: arg); 53 | } 54 | 55 | void _pushToDebug() { 56 | Navigator.pushNamed(context, "/debug"); 57 | } 58 | 59 | void _logout() async { 60 | prefix.RongIMClient.disconnect(false); 61 | SharedPreferences prefs = await SharedPreferences.getInstance(); 62 | prefs.remove("token"); 63 | Navigator.of(context).pushAndRemoveUntil( 64 | new MaterialPageRoute(builder: (context) => new LoginPage()), 65 | (route) => route == null); 66 | } 67 | 68 | Widget getWidget(example.UserInfo user) { 69 | return Container( 70 | height: 50.0, 71 | color: Colors.white, 72 | child: InkWell( 73 | onTap: () { 74 | _onTapUser(user); 75 | }, 76 | child: new ListTile( 77 | title: new Text(user.id), 78 | leading: Container( 79 | width: 36, 80 | height: 36, 81 | child: ClipRRect( 82 | borderRadius: BorderRadius.circular(5), 83 | child: CachedNetworkImage( 84 | fit: BoxFit.fill, 85 | imageUrl: user.portraitUrl, 86 | ), 87 | ), 88 | ), 89 | ), 90 | ), 91 | ); 92 | } 93 | 94 | @override 95 | Widget build(BuildContext context) { 96 | return Scaffold( 97 | appBar: AppBar( 98 | centerTitle: true, 99 | title: Text("RongCloud IM"), 100 | actions: [ 101 | IconButton( 102 | icon: Icon(Icons.more), 103 | onPressed: () { 104 | _pushToDebug(); 105 | }, 106 | ), 107 | IconButton( 108 | icon: Icon(Icons.power_settings_new), 109 | onPressed: () { 110 | _logout(); 111 | }, 112 | ), 113 | ], 114 | ), 115 | body: ListView.builder( 116 | itemCount: widgetList.length, 117 | itemBuilder: (BuildContext context, int index) { 118 | return widgetList[index]; 119 | }, 120 | ), 121 | ); 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /lib/im/pages/sight/record_top_item.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:percent_indicator/linear_percent_indicator.dart'; 3 | import 'dart:developer' as developer; 4 | 5 | enum RecordState { 6 | //正常 [返回,切换摄像头] 7 | Normal, 8 | //录制 [返回,进度条] 9 | Recording, 10 | //预览 [返回] 11 | Preview 12 | } 13 | 14 | class TopRecordItem extends StatefulWidget { 15 | TopRecordItemDelegate delegate; 16 | _TopRecordItemState state; 17 | TopRecordItem(TopRecordItemDelegate delegate) { 18 | this.delegate = delegate; 19 | this.state = _TopRecordItemState(delegate); 20 | } 21 | 22 | void updateRecordState(RecordState s) { 23 | state.updateRecordState(s); 24 | } 25 | 26 | @override 27 | _TopRecordItemState createState() => state; 28 | } 29 | 30 | class _TopRecordItemState extends State { 31 | String pageName = "example.TopRecordItem"; 32 | TopRecordItemDelegate delegate; 33 | 34 | RecordState currentRecordState = RecordState.Normal; 35 | 36 | _TopRecordItemState(TopRecordItemDelegate delegate) { 37 | this.delegate = delegate; 38 | } 39 | 40 | void updateRecordState(RecordState s) { 41 | setState(() { 42 | currentRecordState = s; 43 | }); 44 | } 45 | 46 | Widget recordLine() { 47 | return LinearPercentIndicator( 48 | width: MediaQuery.of(context).size.width - 40 - 25 - 40 - 35 - 30, 49 | animation: true, 50 | animationDuration: 10000, 51 | percent: 1, 52 | progressColor: Colors.white, 53 | ); 54 | } 55 | 56 | @override 57 | Widget build(BuildContext context) { 58 | return Container( 59 | height: 100, 60 | width: MediaQuery.of(context).size.width, 61 | child: Row( 62 | children: [ 63 | SizedBox( 64 | width: 40, 65 | ), 66 | GestureDetector( 67 | onTap: () { 68 | onPop(); 69 | }, 70 | child: Container( 71 | width: 25, 72 | height: 25, 73 | child: currentRecordState != RecordState.Recording 74 | ? Image.asset("assets/images/sight_top_toolbar_close.png") 75 | : Container(), 76 | ), 77 | ), 78 | SizedBox( 79 | width: 15, 80 | ), 81 | Expanded( 82 | child: Container( 83 | child: currentRecordState == RecordState.Recording 84 | ? recordLine() 85 | : Container(), 86 | ), 87 | ), 88 | GestureDetector( 89 | onTap: () { 90 | currentRecordState == RecordState.Normal 91 | ? onSwitchCamera() 92 | : Container(); 93 | }, 94 | child: Container( 95 | width: 35, 96 | height: 35, 97 | child: currentRecordState == RecordState.Normal 98 | ? Image.asset("assets/images/sight_camera_switch.png") 99 | : Container(), 100 | ), 101 | ), 102 | SizedBox( 103 | width: 40, 104 | ), 105 | ], 106 | ), 107 | ); 108 | } 109 | 110 | void onPop() { 111 | if (delegate != null) { 112 | delegate.didPop(); 113 | } else { 114 | developer.log("没有实现 didPop", name: pageName); 115 | } 116 | } 117 | 118 | void onSwitchCamera() { 119 | if (delegate != null) { 120 | delegate.didSwitchCamera(); 121 | } else { 122 | developer.log("没有实现 didSwitchCamera", name: pageName); 123 | } 124 | } 125 | 126 | @override 127 | void dispose() { 128 | super.dispose(); 129 | } 130 | } 131 | 132 | abstract class TopRecordItemDelegate { 133 | //点击叉号按钮 134 | void didPop(); 135 | //切换摄像头 136 | void didSwitchCamera(); 137 | } 138 | -------------------------------------------------------------------------------- /lib/other/search_message_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import '../im/pages/item/widget_util.dart'; 3 | import 'package:rongcloud_im_plugin/rongcloud_im_plugin.dart'; 4 | 5 | class SearchMessagePage extends StatefulWidget { 6 | final Map arguments; 7 | SearchMessagePage({Key key, this.arguments}) : super(key: key); 8 | @override 9 | State createState() { 10 | return _SearchMessagePageState(arguments: arguments); 11 | } 12 | } 13 | 14 | class _SearchMessagePageState extends State { 15 | Map arguments; 16 | int conversationType; 17 | String targetId; 18 | List messageList; 19 | 20 | _SearchMessagePageState({this.arguments}); 21 | 22 | @override 23 | void initState() { 24 | super.initState(); 25 | conversationType = arguments["coversationType"]; 26 | targetId = arguments["targetId"]; 27 | messageList = []; 28 | } 29 | 30 | @override 31 | Widget build(BuildContext context) { 32 | return Scaffold( 33 | appBar: AppBar( 34 | title: Text("搜索会话历史消息"), 35 | ), 36 | body: Container( 37 | child: Column( 38 | mainAxisSize: MainAxisSize.max, 39 | crossAxisAlignment: CrossAxisAlignment.center, 40 | children: [ 41 | Container( 42 | margin: EdgeInsets.only(left: 12, right: 12, top: 20), 43 | height: 45, 44 | decoration: BoxDecoration( 45 | border: new Border.all(color: Colors.black54, width: 0.5), 46 | borderRadius: BorderRadius.circular(8)), 47 | child: TextField( 48 | textAlign: TextAlign.center, 49 | onSubmitted: _searchMessage, 50 | decoration: InputDecoration( 51 | border: InputBorder.none, hintText: '请输入关键词'), 52 | autofocus: true), 53 | ), 54 | messageList.length > 0 55 | ? Expanded( 56 | flex: 1, 57 | child: Container( 58 | margin: EdgeInsets.only(top: 14), 59 | child: ListView.separated( 60 | controller: ScrollController(), 61 | itemCount: messageList.length, 62 | itemBuilder: (BuildContext context, int index) { 63 | if (messageList.length != null && 64 | messageList.length > 0) { 65 | Message message = messageList[index]; 66 | return GestureDetector( 67 | child: Container( 68 | alignment: Alignment.center, 69 | child: Container( 70 | padding: EdgeInsets.all(6), 71 | child: Text(message.toString(), 72 | style: new TextStyle( 73 | fontSize: 15, //字体大��� 74 | ))), 75 | )); 76 | } else { 77 | return WidgetUtil.buildEmptyWidget(); 78 | } 79 | }, 80 | separatorBuilder: 81 | (BuildContext context, int index) { 82 | return Container( 83 | color: Color(0xffC8C8C8), 84 | height: 0.5, 85 | ); 86 | }))) 87 | : Text( 88 | "无记录", 89 | style: new TextStyle( 90 | fontSize: 14, color: const Color(0xffff0000)), 91 | textAlign: TextAlign.center, 92 | ) 93 | ], 94 | ), 95 | ), 96 | ); 97 | } 98 | 99 | void _searchMessage(String keyWord) { 100 | if (keyWord == null || keyWord.isEmpty) { 101 | messageList.clear(); 102 | _refreshUI(); 103 | } 104 | RongIMClient.searchMessages(conversationType, targetId, keyWord, 50, 0, 105 | (List/**/ msgList, int code) { 106 | if (code == 0 && msgList != null) { 107 | messageList = msgList; 108 | _refreshUI(); 109 | } 110 | }); 111 | } 112 | 113 | void _refreshUI() { 114 | setState(() {}); 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /lib/other/login_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import '../im/util/http_util.dart'; 3 | import 'package:shared_preferences/shared_preferences.dart'; 4 | import 'home_page.dart'; 5 | import 'package:fluttertoast/fluttertoast.dart'; 6 | import 'dart:developer' as developer; 7 | 8 | class LoginPage extends StatefulWidget { 9 | @override 10 | State createState() { 11 | return new _LoginPageState(); 12 | } 13 | } 14 | 15 | class _LoginPageState extends State { 16 | String pageName = "example.LoginPage"; 17 | TextEditingController _assount = TextEditingController(); 18 | TextEditingController _password = TextEditingController(); 19 | 20 | @override 21 | void initState() { 22 | super.initState(); 23 | initPlatformState(); 24 | } 25 | 26 | initPlatformState() async { 27 | SharedPreferences prefs = await SharedPreferences.getInstance(); 28 | String phone = prefs.get("phone"); 29 | String password = prefs.get("password"); 30 | 31 | _assount.text = phone; 32 | _password.text = password; 33 | } 34 | 35 | void _loginAction() { 36 | Map map = new Map(); 37 | map["region"] = 86; 38 | map["phone"] = int.parse(_assount.text); 39 | map["password"] = _password.text; 40 | 41 | HttpUtil.post("http://api.sealtalk.im/user/login", (data) { 42 | if (data != null) { 43 | Map body = data; 44 | int errorCode = body["code"]; 45 | if (errorCode == 200) { 46 | Map result = body["result"]; 47 | String id = result["id"]; 48 | String token = result["token"]; 49 | _saveUserInfo(id, token); 50 | developer.log("Login Success, $map", name: pageName); 51 | Navigator.of(context).pushAndRemoveUntil( 52 | new MaterialPageRoute(builder: (context) => new HomePage()), 53 | (route) => route == null); 54 | } else if (errorCode == -1) { 55 | Fluttertoast.showToast(msg: "网络未连接,请连接网络重试"); 56 | } else { 57 | Fluttertoast.showToast(msg: "服务器登录失败,errorCode: $errorCode"); 58 | } 59 | } else { 60 | developer.log("data is null", name: pageName); 61 | } 62 | }, params: map); 63 | } 64 | 65 | void _saveUserInfo(String id, String token) async { 66 | SharedPreferences prefs = await SharedPreferences.getInstance(); 67 | prefs.setString("id", id); 68 | prefs.setString("token", token); 69 | prefs.setString("phone", _assount.text); 70 | prefs.setString("password", _password.text); 71 | } 72 | 73 | @override 74 | Widget build(BuildContext context) { 75 | final logo = new Hero( 76 | tag: 'hero', 77 | child: Container( 78 | width: 100, 79 | height: 100, 80 | child: Image.asset('assets/images/logo.png'), 81 | ), 82 | ); 83 | 84 | final account = TextFormField( 85 | keyboardType: TextInputType.number, 86 | autofocus: false, 87 | controller: _assount, 88 | decoration: InputDecoration( 89 | hintText: 'SealTalk 账号', 90 | contentPadding: new EdgeInsets.fromLTRB(20.0, 10.0, 20.0, 10.0), 91 | border: 92 | OutlineInputBorder(borderRadius: BorderRadius.circular(32.0))), 93 | ); 94 | 95 | final password = TextFormField( 96 | autofocus: false, 97 | obscureText: true, 98 | controller: _password, 99 | decoration: InputDecoration( 100 | hintText: 'SealTalk 密码', 101 | contentPadding: EdgeInsets.fromLTRB(20.0, 10.0, 20.0, 10.0), 102 | border: 103 | OutlineInputBorder(borderRadius: BorderRadius.circular(32.0))), 104 | ); 105 | 106 | final loginButton = Padding( 107 | padding: EdgeInsets.symmetric(vertical: 16.0), 108 | child: MaterialButton( 109 | minWidth: 200.0, 110 | height: 42.0, 111 | onPressed: () { 112 | _loginAction(); 113 | }, 114 | color: Colors.lightBlueAccent, 115 | child: Text( 116 | '登 录', 117 | style: TextStyle(color: Colors.white), 118 | ), 119 | ), 120 | ); 121 | 122 | return Scaffold( 123 | appBar: AppBar( 124 | title: Text("Login"), 125 | ), 126 | body: Center( 127 | child: ListView( 128 | shrinkWrap: true, 129 | padding: EdgeInsets.only(left: 24.0, right: 24.0), 130 | children: [ 131 | logo, 132 | SizedBox(height: 48.0), 133 | account, 134 | SizedBox( 135 | height: 8.0, 136 | ), 137 | password, 138 | SizedBox( 139 | height: 24.0, 140 | ), 141 | loginButton 142 | ], 143 | )), 144 | ); 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /lib/im/pages/sight/record_bottom_item.dart: -------------------------------------------------------------------------------- 1 | import 'dart:ui'; 2 | import 'package:flutter/material.dart'; 3 | import 'dart:developer' as developer; 4 | 5 | class BottomRecordItem extends StatefulWidget { 6 | VideoBottomToolBarDelegate delegate; 7 | BottomRecordItem(VideoBottomToolBarDelegate delegate) { 8 | this.delegate = delegate; 9 | } 10 | @override 11 | _BottomRecordItemState createState() => _BottomRecordItemState(this.delegate); 12 | } 13 | 14 | class _BottomRecordItemState extends State 15 | with TickerProviderStateMixin { 16 | String pageName = "example.BottomRecordItem"; 17 | VideoBottomToolBarDelegate delegate; 18 | bool isNarmal = true; //正常状态,选择状态 19 | 20 | _BottomRecordItemState(VideoBottomToolBarDelegate delegate) { 21 | this.delegate = delegate; 22 | } 23 | 24 | @override 25 | void initState() { 26 | super.initState(); 27 | } 28 | 29 | Widget _getBottomChoiceToolbar() { 30 | double itemWidth = 70; 31 | return Container( 32 | child: Column( 33 | children: [ 34 | SizedBox( 35 | height: 50, 36 | ), 37 | Row( 38 | mainAxisAlignment: MainAxisAlignment.center, 39 | children: [ 40 | GestureDetector( 41 | onTap: () { 42 | onCancelEvent(); 43 | }, 44 | child: Container( 45 | width: itemWidth, 46 | height: itemWidth, 47 | child: Image.asset("assets/images/sight_preview_cancel.png"), 48 | ), 49 | ), 50 | SizedBox( 51 | width: 100, 52 | ), 53 | GestureDetector( 54 | onTap: () { 55 | onFinishEvent(); 56 | }, 57 | child: Container( 58 | width: itemWidth, 59 | height: itemWidth, 60 | child: Image.asset("assets/images/sight_preview_done.png"), 61 | ), 62 | ), 63 | ], 64 | ) 65 | ], 66 | ), 67 | ); 68 | } 69 | 70 | Widget _getBottomToolbarWidget() { 71 | Widget widget = _getBottomRecordToolbar(); 72 | 73 | if (isNarmal == false) { 74 | widget = _getBottomChoiceToolbar(); 75 | } 76 | 77 | return widget; 78 | } 79 | 80 | Widget _getBottomRecordToolbar() { 81 | return Container( 82 | child: Column( 83 | children: [ 84 | GestureDetector( 85 | onTap: () { 86 | onTapCamera(); 87 | }, 88 | onLongPress: () { 89 | onLongPressCamera(); 90 | }, 91 | onLongPressEnd: (LongPressEndDetails details) { 92 | onLongPressEndCamera(); 93 | }, 94 | child: Column( 95 | children: [ 96 | SizedBox( 97 | height: 50, 98 | ), 99 | ClipRRect( 100 | borderRadius: BorderRadius.circular(35), 101 | child: Container( 102 | width: 70, 103 | height: 70, 104 | color: Colors.white, 105 | ), 106 | ), 107 | ], 108 | )), 109 | ], 110 | ), 111 | ); 112 | } 113 | 114 | onLongPressCamera() { 115 | if (delegate != null) { 116 | delegate.didLongPressCamera(); 117 | } else { 118 | developer.log("没有实现 didLongPressCamera", name: pageName); 119 | } 120 | } 121 | 122 | onLongPressEndCamera() { 123 | setState(() { 124 | isNarmal = false; 125 | }); 126 | if (delegate != null) { 127 | delegate.didLongPressEndCamera(); 128 | } else { 129 | developer.log("没有实现 didLongPressEndCamera", name: pageName); 130 | } 131 | } 132 | 133 | onTapCamera() {} 134 | 135 | onCancelEvent() { 136 | if (delegate != null) { 137 | delegate.didCancelEvent(); 138 | } else { 139 | developer.log("没有实现 didLongPressEndCamera", name: pageName); 140 | } 141 | //重置为普通状态 142 | isNarmal = true; 143 | setState(() {}); 144 | } 145 | 146 | onFinishEvent() { 147 | if (delegate != null) { 148 | delegate.didFinishEvent(); 149 | } else { 150 | developer.log("没有实现 didLongPressEndCamera", name: pageName); 151 | } 152 | } 153 | 154 | @override 155 | Widget build(BuildContext context) { 156 | return Container( 157 | child: _getBottomToolbarWidget(), 158 | ); 159 | } 160 | } 161 | 162 | abstract class VideoBottomToolBarDelegate { 163 | //长按相机按钮 164 | void didLongPressCamera(); 165 | //长按结束 166 | void didLongPressEndCamera(); 167 | //取消发送 168 | void didCancelEvent(); 169 | //发送 170 | void didFinishEvent(); 171 | } 172 | -------------------------------------------------------------------------------- /lib/im/pages/webview_page.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | import 'package:flutter/foundation.dart'; 3 | 4 | import 'package:flutter/cupertino.dart'; 5 | import 'package:flutter/material.dart'; 6 | import 'package:flutter_webview_plugin/flutter_webview_plugin.dart'; 7 | import 'package:url_launcher/url_launcher.dart'; 8 | import 'package:rongcloud_im_plugin/rongcloud_im_plugin.dart'; 9 | import 'dart:developer' as developer; 10 | 11 | class WebViewPage extends StatefulWidget { 12 | final Map arguments; 13 | const WebViewPage({Key key, this.arguments}) : super(key: key); 14 | 15 | @override 16 | State createState() => 17 | _WebViewPageState(arguments["url"], arguments["title"]); 18 | } 19 | 20 | class _WebViewPageState extends State { 21 | String pageName = "example.WebViewPage"; 22 | final String url; 23 | final String title; 24 | // final Completer _controller = 25 | // Completer(); 26 | _WebViewPageState(this.url, this.title); 27 | 28 | @override 29 | void dispose() { 30 | super.dispose(); 31 | } 32 | 33 | @override 34 | Widget build(BuildContext context) { 35 | // return 36 | // Scaffold( 37 | // appBar: AppBar( 38 | // title: Text(title == null || title.isEmpty ? this.url : title), 39 | // ), 40 | // body: Container( 41 | // child: WebView( 42 | // initialUrl: url, 43 | // //JS执行模式 是否允许JS执行 44 | // javascriptMode: JavascriptMode.unrestricted, 45 | // javascriptChannels: [ 46 | // _getJavascriptChannel(context), 47 | // ].toSet(), 48 | // onWebViewCreated: (controller) { 49 | // _controller.complete(controller); 50 | // }, 51 | // onPageStarted: (String url) { 52 | // print('Page started loading: $url'); 53 | // }, 54 | // onPageFinished: (url) { 55 | // print('Page finished loading: $url'); 56 | // }, 57 | // ), 58 | // ), 59 | // ); 60 | String correctUrl = _getCorrectLocalPath(this.url); 61 | return WebviewScaffold( 62 | url: correctUrl, 63 | appBar: AppBar( 64 | title: Text(title == null && title.isEmpty ? this.url : this.title), 65 | ), 66 | withZoom: true, 67 | hidden: true, 68 | withLocalStorage: true, 69 | withJavascript: true, 70 | javascriptChannels: [ 71 | _getJavascriptChannel(context), 72 | ].toSet(), 73 | initialChild: Center( 74 | child: CupertinoActivityIndicator( 75 | radius: 15.0, 76 | animating: true, 77 | ), 78 | )); 79 | } 80 | 81 | String _getCorrectLocalPath(String url) { 82 | // iOS Android webView 加载本地路径的 html 文件需要在路径前面加 file:// 83 | if (!url.toLowerCase().startsWith("http") && !url.startsWith("file://")) { 84 | return "file://$url"; 85 | } 86 | return url; 87 | } 88 | 89 | JavascriptChannel _getJavascriptChannel(BuildContext context) { 90 | return JavascriptChannel( 91 | name: 'RCFlutterInterface', 92 | onMessageReceived: (JavascriptMessage message) { 93 | String jsonStr = message.message; 94 | handleInfo(jsonStr, context); 95 | }); 96 | } 97 | 98 | void handleInfo(String jsonStr, BuildContext context) { 99 | if (jsonStr != null && jsonStr.isNotEmpty) { 100 | Map map = json.decode(jsonStr); 101 | String type = map["type"]; 102 | switch (type) { 103 | case FileMessage.objectName: 104 | developer.log("FileMessage click coming", name: pageName); 105 | String fileName = map["fileName"]; 106 | String fileUrl = map["fileUrl"]; 107 | String fileSize = map["fileSize"]; 108 | FileMessage fileMessage = FileMessage.obtain(""); 109 | fileMessage.mName = fileName; 110 | fileMessage.mMediaUrl = fileUrl; 111 | fileMessage.mSize = int.parse(fileSize); 112 | Message message = Message(); 113 | message.content = fileMessage; 114 | _openFile(message, context); 115 | break; 116 | case CombineMessage.objectName: 117 | break; 118 | case "link": 119 | // String link = map["link"]; 120 | // _openLink(link, context); 121 | break; 122 | case "phone": 123 | int phoneNumber = map["phoneNum"]; 124 | _openPhone(phoneNumber, context); 125 | break; 126 | } 127 | } 128 | } 129 | 130 | void _openFile(Message message, BuildContext context) { 131 | Navigator.pushNamed(context, "/file_preview", arguments: message); 132 | } 133 | 134 | void _openLink(String url, BuildContext context) async { 135 | if (await canLaunch(url)) { 136 | await launch(url); 137 | } 138 | } 139 | 140 | void _openPhone(int phone, BuildContext context) async { 141 | String url = 'tel:$phone'; 142 | if (await canLaunch(url)) { 143 | await launch(url); 144 | } 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /lib/im/util/user_info_datesource.dart: -------------------------------------------------------------------------------- 1 | import 'dart:math'; 2 | 3 | import 'db_manager.dart'; 4 | 5 | class BaseInfo { 6 | String id; 7 | String name; 8 | String portraitUrl; 9 | } 10 | 11 | class UserInfo extends BaseInfo { 12 | //开发者可以按需自行增加字段 13 | Map toMap() { 14 | return {'userId': id, 'name': name, 'portraitUrl': portraitUrl}; 15 | } 16 | } 17 | 18 | class GroupInfo extends BaseInfo { 19 | //开发者可以按需自行增加字段 20 | Map toMap() { 21 | return {'groupId': id, 'name': name, 'portraitUrl': portraitUrl}; 22 | } 23 | } 24 | 25 | //用户信息,用户信息需要开发者自行处理(从 APP 服务获取用户信息并保存),此处只做了最简单的处理 26 | class UserInfoDataSource { 27 | static Map cachedUserMap = new Map(); //保证同一 userId 28 | static Map cachedGroupMap = new Map(); //保证同一 groupId 29 | static UserInfoCacheListener cacheListener; 30 | 31 | // 用来刷新用户信息,当有用户信息更新的时候 32 | static void setUserInfo(UserInfo info) { 33 | if (info == null) { 34 | return; 35 | } 36 | cachedUserMap[info.id] = info; 37 | DbManager.instance.setUserInfo(info); 38 | } 39 | 40 | // 获取用户信息 41 | static Future getUserInfo(String userId) async { 42 | UserInfo cachedUserInfo = cachedUserMap[userId]; 43 | if (cachedUserInfo != null) { 44 | return cachedUserInfo; 45 | } else { 46 | UserInfo info; 47 | List infoList = 48 | await DbManager.instance.getUserInfo(userId: userId); 49 | if (infoList != null && infoList.length > 0) { 50 | info = infoList[0]; 51 | } 52 | if (info == null) { 53 | if (cacheListener != null) { 54 | info = cacheListener.getUserInfo(userId); 55 | } 56 | if (info != null) { 57 | DbManager.instance.setUserInfo(info); 58 | } 59 | } 60 | if (info != null) { 61 | cachedUserMap[info.id] = info; 62 | } 63 | 64 | if (info == null) { 65 | info = UserInfo(); 66 | } 67 | return info; 68 | } 69 | } 70 | 71 | static UserInfo generateUserInfo(String userId) { 72 | List names = _getCachedNameList(); 73 | List urls = _getCachedPortraitList(); 74 | 75 | UserInfo user = new UserInfo(); 76 | user.id = userId; 77 | user.name = names[Random().nextInt(names.length)]; 78 | user.portraitUrl = urls[Random().nextInt(urls.length)]; 79 | 80 | cachedUserMap[userId] = user; 81 | return user; 82 | } 83 | 84 | static GroupInfo generateGroupInfo(String groupId) { 85 | List names = _getCachedNameList(); 86 | List urls = _getCachedPortraitList(); 87 | 88 | GroupInfo group = new GroupInfo(); 89 | group.id = groupId; 90 | group.name = names[Random().nextInt(names.length)]; 91 | group.portraitUrl = urls[Random().nextInt(urls.length)]; 92 | 93 | cachedGroupMap[groupId] = group; 94 | return group; 95 | } 96 | 97 | static void setGroupInfo(GroupInfo info) { 98 | if (info == null) { 99 | return; 100 | } 101 | cachedGroupMap[info.id] = info; 102 | DbManager.instance.setGroupInfo(info); 103 | } 104 | 105 | // 群组信息 106 | static Future getGroupInfo(String groupId) async { 107 | GroupInfo cachedGroupInfo = cachedGroupMap[groupId]; 108 | if (cachedGroupInfo != null) { 109 | return cachedGroupInfo; 110 | } else { 111 | GroupInfo info; 112 | List infoList = 113 | await DbManager.instance.getGroupInfo(groupId: groupId); 114 | if (infoList != null && infoList.length > 0) { 115 | info = infoList[0]; 116 | } 117 | if (info == null) { 118 | if (cacheListener != null) { 119 | info = cacheListener.getGroupInfo(groupId); 120 | } 121 | if (info != null) { 122 | DbManager.instance.setGroupInfo(info); 123 | } 124 | } 125 | if (info != null) { 126 | cachedGroupMap[info.id] = info; 127 | } 128 | 129 | if (info == null) { 130 | info = GroupInfo(); 131 | } 132 | return info; 133 | } 134 | } 135 | 136 | static void setCacheListener(UserInfoCacheListener listener) { 137 | cacheListener = listener; 138 | } 139 | 140 | static List _getCachedNameList() { 141 | List names = [ 142 | "丁春秋", 143 | "木婉清", 144 | "包不同", 145 | "王语嫣", 146 | "云中鹤", 147 | "天山童姥", 148 | "乔峰", 149 | "阿朱", 150 | "阿紫", 151 | "鸠摩智", 152 | "段誉", 153 | "段正淳", 154 | "萧远山", 155 | "虚竹" 156 | ]; 157 | return names; 158 | } 159 | 160 | static List _getCachedPortraitList() { 161 | List urls = [ 162 | "http://b-ssl.duitang.com/uploads/item/201804/24/20180424214451_5lJat.png", 163 | "http://i0.hdslb.com/bfs/article/64a47330d4c66553fe18bf6b63ab761099fd018c.jpg", 164 | "http://img.mp.itc.cn/upload/20161205/545bbfda38bd4d738266189901a25a61_th.jpeg", 165 | "http://b-ssl.duitang.com/uploads/item/201804/13/20180413141949_aFcZ3.png" 166 | ]; 167 | return urls; 168 | } 169 | 170 | static List _getCachaGroupNameList() { 171 | List names = [ 172 | "群组0", 173 | "群组1", 174 | "群组2", 175 | "群组3", 176 | "群组4", 177 | "群组5", 178 | "群组6", 179 | "群组7", 180 | "群组8", 181 | "群组9" 182 | ]; 183 | return names; 184 | } 185 | } 186 | 187 | class UserInfoCacheListener { 188 | UserInfo Function(String userId) getUserInfo; 189 | GroupInfo Function(String groupId) getGroupInfo; 190 | void Function(UserInfo info) onUserInfoUpdated; 191 | } 192 | -------------------------------------------------------------------------------- /lib/im/util/media_util.dart: -------------------------------------------------------------------------------- 1 | import 'package:file_picker/file_picker.dart'; 2 | import 'package:flutter/foundation.dart'; 3 | import 'package:flutter/material.dart'; 4 | import 'dart:io'; 5 | import 'package:flutter_audio_recorder/flutter_audio_recorder.dart'; 6 | import 'package:path_provider/path_provider.dart'; 7 | import 'package:image_picker/image_picker.dart'; 8 | import 'package:flutter_sound/flutter_sound.dart'; 9 | import 'package:fluttertoast/fluttertoast.dart'; 10 | import 'package:permission_handler/permission_handler.dart'; 11 | import 'dart:developer' as developer; 12 | 13 | ///媒体工具,负责申请权限,选照片,拍照,录音,播放语音 14 | class MediaUtil { 15 | FlutterSound flutterSound = new FlutterSound(); 16 | 17 | String pageName = "example.MediaUtil"; 18 | factory MediaUtil() => _getInstance(); 19 | static MediaUtil get instance => _getInstance(); 20 | static MediaUtil _instance; 21 | 22 | FlutterAudioRecorder _recorder; 23 | MediaUtil._internal() { 24 | // 初始化 25 | } 26 | static MediaUtil _getInstance() { 27 | if (_instance == null) { 28 | _instance = new MediaUtil._internal(); 29 | } 30 | return _instance; 31 | } 32 | 33 | //请求权限:相册,相机,麦克风 34 | void requestPermissions() async { 35 | Map statuses = await [ 36 | Permission.photos, 37 | Permission.camera, 38 | Permission.microphone, 39 | Permission.storage 40 | ].request(); 41 | for (var status in statuses.keys) { 42 | developer.log(status.toString() + ":" + statuses[status].toString(), 43 | name: pageName); 44 | } 45 | } 46 | 47 | //拍照,成功则返回照片的本地路径,注:Android 必须要加 file:// 头 48 | Future takePhoto() async { 49 | File imgfile = await ImagePicker.pickImage(source: ImageSource.camera); 50 | if (imgfile == null) { 51 | return null; 52 | } 53 | String imgPath = imgfile.path; 54 | if (TargetPlatform.android == defaultTargetPlatform) { 55 | imgPath = "file://" + imgfile.path; 56 | } 57 | return imgPath; 58 | } 59 | 60 | //从相册选照片,成功则返回照片的本地路径,注:Android 必须要加 file:// 头 61 | Future pickImage() async { 62 | File imgfile = await ImagePicker.pickImage(source: ImageSource.gallery); 63 | if (imgfile == null) { 64 | return null; 65 | } 66 | String imgPath = imgfile.path; 67 | if (TargetPlatform.android == defaultTargetPlatform) { 68 | imgPath = "file://" + imgfile.path; 69 | } 70 | return imgPath; 71 | } 72 | 73 | //选择本地文件,成功返回文件信息 74 | Future> pickFiles() async { 75 | List files = await FilePicker.getMultiFile(); 76 | return files; 77 | } 78 | 79 | //开始录音 80 | void startRecordAudio() async { 81 | developer.log("debug 准备录音并检查权限", name: pageName); 82 | bool hasPermission = await FlutterAudioRecorder.hasPermissions; 83 | if (hasPermission) { 84 | developer.log("debug 录音权限已开启", name: pageName); 85 | Directory tempDir = await getTemporaryDirectory(); 86 | String tempPath = tempDir.path + 87 | "/" + 88 | DateTime.now().millisecondsSinceEpoch.toString() + 89 | ".aac"; 90 | _recorder = FlutterAudioRecorder(tempPath, 91 | audioFormat: AudioFormat.AAC); // or AudioFormat.WAV 92 | await _recorder.initialized; 93 | await _recorder.start(); 94 | developer.log("debug 开始录音", name: pageName); 95 | } else { 96 | Fluttertoast.showToast( 97 | msg: "录音权限未开启", 98 | toastLength: Toast.LENGTH_SHORT, 99 | gravity: ToastGravity.CENTER, 100 | timeInSecForIos: 1, 101 | backgroundColor: Colors.grey[800], 102 | textColor: Colors.white, 103 | fontSize: 16.0); 104 | } 105 | } 106 | 107 | //录音结束,通过 finished 返回本地路径和语音时长,注:Android 必须要加 file:// 头 108 | void stopRecordAudio(Function(String path, int duration) finished) async { 109 | var result = await _recorder.stop(); 110 | developer.log( 111 | "Stop recording: path = ${result.path},duration = ${result.duration}", 112 | name: pageName); 113 | developer.log("Stop recording: duration = ${result.duration}", 114 | name: pageName); 115 | if (result.duration.inSeconds > 0) { 116 | String path = result.path; 117 | if (path == null) { 118 | if (finished != null) { 119 | finished(null, 0); 120 | } 121 | } 122 | if (TargetPlatform.android == defaultTargetPlatform) { 123 | path = "file://" + path; 124 | } 125 | if (finished != null) { 126 | finished(path, result.duration.inSeconds); 127 | } 128 | } else { 129 | Fluttertoast.showToast( 130 | msg: "说话时间太短", 131 | toastLength: Toast.LENGTH_SHORT, 132 | gravity: ToastGravity.CENTER, 133 | timeInSecForIos: 1, 134 | backgroundColor: Colors.grey[800], 135 | textColor: Colors.white, 136 | fontSize: 16.0); 137 | } 138 | } 139 | 140 | //播放语音 141 | void startPlayAudio(String path) { 142 | if (flutterSound.audioState == t_AUDIO_STATE.IS_PLAYING) { 143 | stopPlayAudio(); 144 | } 145 | flutterSound.startPlayer(path); 146 | } 147 | 148 | //停止播放语音 149 | void stopPlayAudio() { 150 | flutterSound.stopPlayer(); 151 | } 152 | 153 | String getCorrectedLocalPath(String localPath) { 154 | String path = localPath; 155 | //Android 本地路径需要删除 file:// 才能被 File 对象识别 156 | if (TargetPlatform.android == defaultTargetPlatform) { 157 | path = localPath.replaceFirst("file://", ""); 158 | } 159 | return path; 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /lib/other/select_conversation_page.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | import 'package:flutter/foundation.dart'; 3 | 4 | import 'package:flutter/cupertino.dart'; 5 | import 'package:flutter/material.dart'; 6 | import 'package:rongcloud_im_plugin/rongcloud_im_plugin.dart'; 7 | import '../im/util/combine_message_util.dart'; 8 | import '../im/util/dialog_util.dart'; 9 | import '../im/util/style.dart'; 10 | import '../im/util/event_bus.dart'; 11 | import 'dart:developer' as developer; 12 | 13 | class SelectConversationPage extends StatefulWidget { 14 | // final List selectMessages; 15 | final Map arguments; 16 | const SelectConversationPage({Key key, this.arguments}) : super(key: key); 17 | 18 | @override 19 | State createState() => 20 | _SelectConversationPageState(this.arguments); 21 | } 22 | 23 | class _SelectConversationPageState extends State { 24 | final Map arguments; 25 | _SelectConversationPageState(this.arguments); 26 | 27 | String pageName = "example.SelectConversationPage"; 28 | List selectMessages; 29 | int forwardType; // 0:逐条转发,1:合并转发 30 | List conList = []; 31 | List displayConversationType = [ 32 | RCConversationType.Private, 33 | RCConversationType.Group 34 | ]; 35 | ScrollController _scrollController; 36 | List selectConList = []; 37 | 38 | @override 39 | void initState() { 40 | super.initState(); 41 | selectMessages = List.from(arguments["selectMessages"]); 42 | forwardType = arguments["forwardType"]; 43 | updateConversationList(); 44 | } 45 | 46 | updateConversationList() async { 47 | List list = await RongIMClient.getConversationList(displayConversationType); 48 | if (list != null) { 49 | conList = list; 50 | } 51 | _renfreshUI(); 52 | } 53 | 54 | void _renfreshUI() { 55 | setState(() {}); 56 | } 57 | 58 | Widget _buildConversationListView() { 59 | return new ListView.separated( 60 | scrollDirection: Axis.vertical, 61 | itemCount: conList.length, 62 | controller: _scrollController, 63 | itemBuilder: (BuildContext context, int index) { 64 | if (conList.length <= 0) { 65 | // return WidgetUtil.buildEmptyWidget(); 66 | } 67 | return getWidget(conList[index]); 68 | }, 69 | separatorBuilder: (BuildContext context, int index) { 70 | return Container( 71 | height: 10, 72 | width: 1, 73 | ); 74 | }); 75 | } 76 | 77 | Widget getWidget(Conversation con) { 78 | return GestureDetector( 79 | onTap: () { 80 | didTapItem(con); 81 | }, 82 | child: Container( 83 | height: 50.0, 84 | color: Colors.white, 85 | child: InkWell( 86 | child: new ListTile( 87 | title: new Text((con.conversationType == RCConversationType.Private 88 | ? "单聊:" 89 | : "群聊:") + 90 | con.targetId), 91 | ), 92 | ), 93 | ), 94 | ); 95 | } 96 | 97 | void didTapItem(Conversation con) { 98 | selectConList.add(con); 99 | 100 | if (forwardType == 0) { 101 | sendMessageOneByOne(); 102 | } else { 103 | // 合并转发 104 | sendMessageByCombine(); 105 | } 106 | // RongIMClient.clearMessages(con.conversationType, con.targetId, (code) { 107 | // developer.log("result:$code", name: pageName); 108 | // }); 109 | } 110 | 111 | void sendMessageByCombine() async { 112 | CombineMessage combineMessage = 113 | await CombineMessageUtils().combineMessage(selectMessages); 114 | List messageList = []; 115 | Message message = Message(); 116 | message.content = combineMessage; 117 | messageList.add(message); 118 | // 这里不使用 loading,因为发消息时 sleep 会卡住动画 119 | DialogUtil.showAlertDiaLog(context, "消息转发中,请稍后...", 120 | confirmButton: TextButton(onPressed: () {}, child: Text("确认"))); 121 | sendMessage(messageList, isCombineMsg: true); 122 | } 123 | 124 | void sendMessageOneByOne() { 125 | developer.log( 126 | "sendMessageOneByOne" + 127 | selectMessages.toString() + 128 | "转发的会话个数:" + 129 | selectConList.length.toString(), 130 | name: pageName); 131 | // 这里不使用 loading,因为发消息时 sleep 会卡住动画 132 | DialogUtil.showAlertDiaLog(context, "消息转发中,请稍后...", 133 | confirmButton: TextButton(onPressed: () {}, child: Text("确认"))); 134 | sendMessage(selectMessages); 135 | } 136 | 137 | void sendMessage(List selectMessages, 138 | {bool isCombineMsg = false}) async { 139 | Future.delayed(Duration(milliseconds: 400), () { 140 | for (Message msg in selectMessages) { 141 | for (Conversation con in selectConList) { 142 | // 转发时去掉消息原先携带的 sendUserInfo 和 mentionedInfo 143 | msg.content.sendUserInfo = null; 144 | msg.content.mentionedInfo = null; 145 | if (TargetPlatform.android == defaultTargetPlatform && 146 | !isCombineMsg) { 147 | RongIMClient.forwardMessageByStep( 148 | con.conversationType, con.targetId, msg); 149 | } else { 150 | RongIMClient.sendMessage( 151 | con.conversationType, con.targetId, msg.content); 152 | } 153 | 154 | // 延迟400秒,防止过渡频繁的发送消息导致发送失败的问题 155 | sleep(Duration(milliseconds: 400)); 156 | } 157 | } 158 | selectConList.clear(); 159 | Navigator.pop(context); 160 | Navigator.pop(context); 161 | EventBus.instance.commit(EventKeys.ForwardMessageEnd, null); 162 | }); 163 | } 164 | 165 | @override 166 | Widget build(BuildContext context) { 167 | return Scaffold( 168 | appBar: AppBar( 169 | title: Text(RCString.SelectConTitle), 170 | actions: [ 171 | // IconButton( 172 | // icon: Icon(Icons.done), 173 | // onPressed: () { 174 | // // _pushToDebug(); 175 | // }, 176 | // ), 177 | ], 178 | ), 179 | body: _buildConversationListView(), 180 | ); 181 | } 182 | } 183 | -------------------------------------------------------------------------------- /lib/im/pages/file_preview_page.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:permission_handler/permission_handler.dart'; 5 | import 'package:rongcloud_im_plugin/rongcloud_im_plugin.dart'; 6 | import 'item/widget_util.dart'; 7 | import '../../im/util/file.dart'; 8 | import 'package:open_file/open_file.dart'; 9 | 10 | class FilePreviewPage extends StatefulWidget { 11 | final Message message; 12 | const FilePreviewPage({Key key, this.message}) : super(key: key); 13 | 14 | @override 15 | State createState() { 16 | return _FilePreviewState(message); 17 | } 18 | } 19 | 20 | class _FilePreviewState extends State { 21 | final Message message; 22 | FileMessage fileMessage; 23 | static const int DOWNLOAD_SUCCESS = 0; 24 | static const int DOWNLOAD_PROGRESS = 10; 25 | static const int DOWNLOAD_CANCELED = 20; 26 | int currentStatus = -1; 27 | int mProgress; 28 | _FilePreviewState(this.message); 29 | 30 | @override 31 | void initState() { 32 | super.initState(); 33 | fileMessage = message.content; 34 | _addIMHander(); 35 | } 36 | 37 | @override 38 | Widget build(BuildContext context) { 39 | String fileStatuStr; 40 | if (currentStatus == DOWNLOAD_PROGRESS) { 41 | fileStatuStr = "正在下载...$mProgress%"; 42 | } else { 43 | fileStatuStr = _isFileNeedDowload() ? "打开文件" : "开始下载"; 44 | } 45 | 46 | return Scaffold( 47 | appBar: AppBar( 48 | title: Text("文件预览"), 49 | ), 50 | body: Container( 51 | alignment: Alignment.center, 52 | child: Column( 53 | crossAxisAlignment: CrossAxisAlignment.center, 54 | children: [ 55 | Container( 56 | margin: EdgeInsets.only(top: 60), 57 | child: Image.asset( 58 | FileUtil.fileTypeImagePath(fileMessage.mName), 59 | width: 70, 60 | height: 70, 61 | )), 62 | Container( 63 | margin: EdgeInsets.only(top: 15), 64 | child: Text(fileMessage.mName, 65 | maxLines: 2, 66 | overflow: TextOverflow.ellipsis, 67 | style: TextStyle( 68 | fontSize: 16, color: const Color(0xff000000)))), 69 | Container( 70 | margin: EdgeInsets.only(top: 15), 71 | child: Text(FileUtil.formatFileSize(fileMessage.mSize), 72 | style: TextStyle( 73 | fontSize: 12, color: const Color(0xff888888)))), 74 | getProgress(), 75 | Container( 76 | margin: EdgeInsets.fromLTRB(40, 50, 40, 0), 77 | width: double.infinity, 78 | height: 60, 79 | child: TextButton( 80 | style: ButtonStyle( 81 | backgroundColor: MaterialStateProperty.all( 82 | Color(0xff4876FF)), 83 | ), 84 | onPressed: () { 85 | _fileButtonClick(); 86 | }, 87 | child: Text(fileStatuStr, 88 | style: TextStyle( 89 | fontSize: 16, 90 | color: const Color(0xFFFFFFFF))))), 91 | ]), 92 | )); 93 | } 94 | 95 | Widget getProgress() { 96 | if (currentStatus == DOWNLOAD_PROGRESS) { 97 | return Container( 98 | margin: EdgeInsets.only(top: 12), 99 | child: SizedBox( 100 | //限制进度条的高度 101 | height: 6.0, 102 | //限制进度条的宽度 103 | width: 300, 104 | child: new LinearProgressIndicator( 105 | //0~1的浮点数,用来表示进度多少;如果 value 为 null 或空,则显示一个动画,否则显示一个定值 106 | value: mProgress / 100, 107 | //背景颜色 108 | backgroundColor: const Color(0xff888888), 109 | //进度颜色 110 | valueColor: new AlwaysStoppedAnimation(Colors.blue)), 111 | ), 112 | ); 113 | } 114 | return WidgetUtil.buildEmptyWidget(); 115 | } 116 | 117 | _addIMHander() { 118 | RongIMClient.onDownloadMediaMessageResponse = 119 | (int code, int progress, int messageId, Message message) async { 120 | if (this.message.messageId == messageId) { 121 | if (code == DOWNLOAD_SUCCESS) { 122 | FileMessage content = message.content; 123 | currentStatus = DOWNLOAD_SUCCESS; 124 | fileMessage.localPath = content.localPath; 125 | } else if (code == DOWNLOAD_PROGRESS) { 126 | currentStatus = DOWNLOAD_PROGRESS; 127 | mProgress = progress; 128 | } else if (code == DOWNLOAD_CANCELED) { 129 | currentStatus = DOWNLOAD_CANCELED; 130 | } else { 131 | currentStatus = -1; 132 | } 133 | _refreshUI(); 134 | } 135 | }; 136 | } 137 | 138 | _refreshUI() { 139 | setState(() {}); 140 | } 141 | 142 | _fileButtonClick() { 143 | if (currentStatus != DOWNLOAD_PROGRESS) { 144 | if (!_isFileNeedDowload()) { 145 | _startDownload(); 146 | } else { 147 | _openFile(); 148 | } 149 | } 150 | } 151 | 152 | void _startDownload() async { 153 | if (await Permission.storage.status == PermissionStatus.granted) { 154 | RongIMClient.downloadMediaMessage(message); 155 | } else { 156 | Permission.storage.request(); 157 | } 158 | } 159 | 160 | void _openFile() { 161 | String path = handlePath(fileMessage.localPath); 162 | OpenFile.open(path); 163 | } 164 | 165 | bool _isFileNeedDowload() { 166 | if (fileMessage != null) { 167 | String localPath = fileMessage.localPath; 168 | if (localPath != null && localPath.isNotEmpty) { 169 | File localFile = File(handlePath(localPath)); 170 | bool isExists = localFile.existsSync(); 171 | return isExists; 172 | } 173 | } 174 | return false; 175 | } 176 | 177 | String handlePath(String path) { 178 | if (path.startsWith("file://")) { 179 | return path.replaceAll("file://", ""); 180 | } 181 | return path; 182 | } 183 | } 184 | -------------------------------------------------------------------------------- /lib/im/pages/item/widget_util.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:flutter/material.dart'; 4 | 5 | import 'package:cached_network_image/cached_network_image.dart'; 6 | 7 | import '../../util/time.dart'; 8 | import '../../util/style.dart'; 9 | 10 | class WidgetUtil { 11 | /// 会话页面加号扩展栏里面的 widget,上面图片,下面文本 12 | static Widget buildExtentionWidget( 13 | IconData icon, String text, Function() clicked) { 14 | return Column( 15 | children: [ 16 | SizedBox( 17 | height: 8, 18 | ), 19 | InkWell( 20 | onTap: () { 21 | if (clicked != null) { 22 | clicked(); 23 | } 24 | }, 25 | child: ClipRRect( 26 | borderRadius: BorderRadius.circular(8), 27 | child: Container( 28 | width: RCLayout.ExtIconLayoutSize, 29 | height: RCLayout.ExtIconLayoutSize, 30 | color: Colors.white, 31 | child: Icon( 32 | icon, 33 | size: RCFont.ExtIconSize, 34 | ), 35 | ), 36 | ), 37 | ), 38 | SizedBox( 39 | height: 5, 40 | ), 41 | Text(text, style: TextStyle(fontSize: RCFont.ExtTextFont)) 42 | ], 43 | ); 44 | } 45 | 46 | /// 用户头像 47 | static Widget buildUserPortrait(String path) { 48 | Widget protraitWidget; 49 | if (path == null || path.isEmpty) { 50 | protraitWidget = 51 | Image.asset("assets/images/default_portrait.png", fit: BoxFit.fill); 52 | } else { 53 | if (path.startsWith("http")) { 54 | protraitWidget = CachedNetworkImage( 55 | fit: BoxFit.fill, 56 | imageUrl: path, 57 | ); 58 | } else { 59 | File file = File(path); 60 | if (file.existsSync()) { 61 | protraitWidget = Image.file( 62 | file, 63 | fit: BoxFit.fill, 64 | ); 65 | } 66 | } 67 | } 68 | 69 | return ClipRRect( 70 | borderRadius: BorderRadius.circular(8), 71 | child: Container( 72 | height: RCLayout.ConListPortraitSize, 73 | width: RCLayout.ConListPortraitSize, 74 | child: protraitWidget, 75 | ), 76 | ); 77 | } 78 | 79 | /// 会话页面录音时的 widget,gif 动画 80 | static Widget buildVoiceRecorderWidget() { 81 | return Container( 82 | padding: EdgeInsets.fromLTRB(50, 0, 50, 200), 83 | alignment: Alignment.center, 84 | child: Container( 85 | width: 150, 86 | height: 150, 87 | child: Image.asset("assets/images/voice_recoder.gif"), 88 | ), 89 | ); 90 | } 91 | 92 | /// 消息 item 上的时间 93 | static Widget buildMessageTimeWidget(int sentTime) { 94 | return ClipRRect( 95 | borderRadius: BorderRadius.circular(5), 96 | child: Container( 97 | alignment: Alignment.center, 98 | margin: EdgeInsets.fromLTRB(0, 0, 0, 5), 99 | width: RCLayout.MessageTimeItemWidth, 100 | height: RCLayout.MessageTimeItemHeight, 101 | color: Color(RCColor.MessageTimeBgColor), 102 | child: Text( 103 | TimeUtil.convertTime(sentTime), 104 | style: 105 | TextStyle(color: Colors.white, fontSize: RCFont.MessageTimeFont), 106 | ), 107 | ), 108 | ); 109 | } 110 | 111 | /// 长按的 menu,用于处理会话列表页面和会话页面的长按 112 | static void showLongPressMenu(BuildContext context, Offset tapPos, 113 | Map map, Function(String key) onSelected) { 114 | final RenderBox overlay = Overlay.of(context).context.findRenderObject(); 115 | final RelativeRect position = RelativeRect.fromLTRB(tapPos.dx, tapPos.dy, 116 | overlay.size.width - tapPos.dx, overlay.size.height - tapPos.dy); 117 | List> items = []; 118 | map.keys.forEach((String key) { 119 | PopupMenuItem p = PopupMenuItem( 120 | child: Container( 121 | alignment: Alignment.center, 122 | child: Text( 123 | map[key], 124 | textAlign: TextAlign.center, 125 | ), 126 | ), 127 | value: key, 128 | ); 129 | items.add(p); 130 | }); 131 | showMenu(context: context, position: position, items: items) 132 | .then((String selectedStr) { 133 | if (onSelected != null) { 134 | if (selectedStr == null) { 135 | selectedStr = RCLongPressAction.UndefinedKey; 136 | } 137 | onSelected(selectedStr); 138 | } 139 | return selectedStr; 140 | }); 141 | } 142 | 143 | /// onTaped 点击事件,0~n 代表点击了对应下标,-1 代表点击了白透明空白区域,暂无用 144 | static Widget buildLongPressDialog( 145 | List titles, Function(int index) onTaped) { 146 | List wList = []; 147 | for (int i = 0; i < titles.length; i++) { 148 | Widget w = Container( 149 | alignment: Alignment.center, 150 | child: GestureDetector( 151 | onTap: () { 152 | if (onTaped != null) { 153 | onTaped(i); 154 | } 155 | }, 156 | child: Container( 157 | height: 40, 158 | alignment: Alignment.center, 159 | child: new Text( 160 | titles[i], 161 | style: new TextStyle(fontSize: 18.0), 162 | ), 163 | ), 164 | ), 165 | ); 166 | wList.add(w); 167 | } 168 | Widget bgWidget = Opacity( 169 | opacity: 0.3, 170 | child: GestureDetector( 171 | onTap: () { 172 | if (onTaped != null) { 173 | onTaped(-1); 174 | } 175 | }, 176 | child: Container( 177 | padding: EdgeInsets.all(0), 178 | color: Colors.black, 179 | ), 180 | ), 181 | ); 182 | return Stack( 183 | children: [ 184 | bgWidget, //半透明 widget 185 | new Center( 186 | //保证控件居中效果 187 | child: Container( 188 | width: 120, 189 | height: 60.0 * titles.length, 190 | color: Colors.white, 191 | child: new Column( 192 | mainAxisAlignment: MainAxisAlignment.center, 193 | crossAxisAlignment: CrossAxisAlignment.center, 194 | children: wList, 195 | )), 196 | ) 197 | ], 198 | ); 199 | } 200 | 201 | /// 空白 widget ,用于处理非法参数时的占位 202 | static Widget buildEmptyWidget() { 203 | return Container( 204 | height: 1, 205 | width: 1, 206 | ); 207 | } 208 | } 209 | -------------------------------------------------------------------------------- /android/app/src/main/java/com/example/rongcloud_im_plugin_example/TestMessage.java: -------------------------------------------------------------------------------- 1 | package com.example.rongcloud_im_plugin_example; 2 | 3 | import android.os.Parcel; 4 | import android.text.TextUtils; 5 | import android.util.Log; 6 | 7 | import org.json.JSONException; 8 | import org.json.JSONObject; 9 | 10 | import java.io.UnsupportedEncodingException; 11 | import java.util.ArrayList; 12 | import java.util.List; 13 | import java.util.regex.Matcher; 14 | import java.util.regex.Pattern; 15 | 16 | import io.rong.common.ParcelUtils; 17 | import io.rong.imlib.MessageTag; 18 | import io.rong.imlib.model.MentionedInfo; 19 | import io.rong.imlib.model.MessageContent; 20 | import io.rong.imlib.model.UserInfo; 21 | 22 | /** 23 | * Created by Beyond on 2016/12/5. 24 | */ 25 | 26 | @MessageTag(value = "RCD:TstMsg", flag = MessageTag.ISCOUNTED | MessageTag.ISPERSISTED) 27 | public class TestMessage extends MessageContent { 28 | private final static String TAG = "TestMessage"; 29 | 30 | private String content; 31 | protected String extra; 32 | 33 | /** 34 | * 将本地消息对象序列化为消息数据。 35 | * 36 | * @return 消息数据。 37 | */ 38 | @Override 39 | public byte[] encode() { 40 | 41 | JSONObject jsonObj = new JSONObject(); 42 | try { 43 | jsonObj.put("content", getEmotion(getContent())); 44 | 45 | if (!TextUtils.isEmpty(getExtra())) 46 | jsonObj.put("extra", getExtra()); 47 | 48 | if (getJSONUserInfo() != null) 49 | jsonObj.putOpt("user", getJSONUserInfo()); 50 | 51 | if (getJsonMentionInfo() != null) { 52 | jsonObj.putOpt("mentionedInfo", getJsonMentionInfo()); 53 | } 54 | } catch (JSONException e) { 55 | Log.e(TAG, "JSONException " + e.getMessage()); 56 | } 57 | 58 | try { 59 | return jsonObj.toString().getBytes("UTF-8"); 60 | } catch (UnsupportedEncodingException e) { 61 | // TODO Auto-generated catch block 62 | e.printStackTrace(); 63 | } 64 | return null; 65 | } 66 | 67 | /** 68 | * Android 的表情unicode跟ios不一样,为了做到两个平台统一,android 这边设置了表情映射,根据unicode来映射具体的图片。 69 | * 70 | */ 71 | private String getEmotion(String content) { 72 | 73 | Pattern pattern = Pattern.compile("\\[/u([0-9A-Fa-f]+)\\]"); 74 | Matcher matcher = pattern.matcher(content); 75 | 76 | StringBuffer sb = new StringBuffer(); 77 | 78 | while (matcher.find()) { 79 | int inthex = Integer.parseInt(matcher.group(1), 16); 80 | matcher.appendReplacement(sb, String.valueOf(Character.toChars(inthex))); 81 | } 82 | 83 | matcher.appendTail(sb); 84 | 85 | return sb.toString(); 86 | } 87 | 88 | public TestMessage() { 89 | } 90 | 91 | public static TestMessage obtain(String text) { 92 | TestMessage model = new TestMessage(); 93 | model.setContent(text); 94 | return model; 95 | } 96 | 97 | public TestMessage(byte[] data) { 98 | String jsonStr = null; 99 | try { 100 | jsonStr = new String(data, "UTF-8"); 101 | } catch (UnsupportedEncodingException e) { 102 | e.printStackTrace(); 103 | } 104 | 105 | try { 106 | JSONObject jsonObj = new JSONObject(jsonStr); 107 | 108 | if (jsonObj.has("content")) 109 | setContent(jsonObj.optString("content")); 110 | 111 | if (jsonObj.has("extra")) 112 | setExtra(jsonObj.optString("extra")); 113 | 114 | if (jsonObj.has("user")) { 115 | setUserInfo(parseJsonToUserInfo(jsonObj.getJSONObject("user"))); 116 | } 117 | 118 | if (jsonObj.has("mentionedInfo")) { 119 | setMentionedInfo(parseJsonToMentionInfo(jsonObj.getJSONObject("mentionedInfo"))); 120 | } 121 | } catch (JSONException e) { 122 | Log.e(TAG, "JSONException " + e.getMessage()); 123 | } 124 | 125 | } 126 | 127 | /** 128 | * 描述了包含在 Parcelable 对象排列信息中的特殊对象的类型。 129 | * 130 | * @return 一个标志位,表明Parcelable对象特殊对象类型集合的排列。 131 | */ 132 | public int describeContents() { 133 | return 0; 134 | } 135 | 136 | /** 137 | * 将类的数据写入外部提供的 Parcel 中。 138 | * 139 | * @param dest 对象被写入的 Parcel。 140 | * @param flags 对象如何被写入的附加标志,可能是 0 或 PARCELABLE_WRITE_RETURN_VALUE。 141 | */ 142 | @Override 143 | public void writeToParcel(Parcel dest, int flags) { 144 | ParcelUtils.writeToParcel(dest, getExtra()); 145 | ParcelUtils.writeToParcel(dest, content); 146 | ParcelUtils.writeToParcel(dest, getUserInfo()); 147 | ParcelUtils.writeToParcel(dest, getMentionedInfo()); 148 | } 149 | 150 | /** 151 | * 构造函数。 152 | * 153 | * @param in 初始化传入的 Parcel。 154 | */ 155 | public TestMessage(Parcel in) { 156 | setExtra(ParcelUtils.readFromParcel(in)); 157 | setContent(ParcelUtils.readFromParcel(in)); 158 | setUserInfo(ParcelUtils.readFromParcel(in, UserInfo.class)); 159 | setMentionedInfo(ParcelUtils.readFromParcel(in, MentionedInfo.class)); 160 | } 161 | 162 | /** 163 | * 读取接口,目的是要从Parcel中构造一个实现了Parcelable的类的实例处理。 164 | */ 165 | public static final Creator CREATOR = new Creator() { 166 | 167 | @Override 168 | public TestMessage createFromParcel(Parcel source) { 169 | return new TestMessage(source); 170 | } 171 | 172 | @Override 173 | public TestMessage[] newArray(int size) { 174 | return new TestMessage[size]; 175 | } 176 | }; 177 | 178 | /** 179 | * 构造函数。 180 | * 181 | * 182 | * @param content 文字消息的内容。 183 | */ 184 | public TestMessage(String content) { 185 | this.setContent(content); 186 | } 187 | 188 | /** 189 | * 获取文字消息的内容。 190 | * 191 | * @return 文字消息的内容。 192 | */ 193 | public String getContent() { 194 | return content; 195 | } 196 | 197 | /** 198 | * 设置文字消息的内容。 199 | * 200 | * @param content 文字消息的内容。 201 | */ 202 | public void setContent(String content) { 203 | this.content = content; 204 | } 205 | 206 | /** 207 | * 获取消息扩展信息 208 | * @return 扩展信息 209 | */ 210 | public String getExtra() { 211 | return extra; 212 | } 213 | 214 | /** 215 | * 设置消息扩展信息 216 | * @param extra 扩展信息 217 | */ 218 | public void setExtra(String extra) { 219 | this.extra = extra; 220 | } 221 | 222 | @Override 223 | public List getSearchableWord() { 224 | List words = new ArrayList<>(); 225 | words.add(content); 226 | return words; 227 | } 228 | } 229 | -------------------------------------------------------------------------------- /lib/other/chatroom_debug_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:rongcloud_im_plugin/rongcloud_im_plugin.dart'; 3 | import 'package:fluttertoast/fluttertoast.dart'; 4 | import '../im/util/code_util.dart'; 5 | import '../im/util/dialog_util.dart'; 6 | import 'dart:developer' as developer; 7 | 8 | class ChatRoomDebugPage extends StatefulWidget { 9 | @override 10 | State createState() => _ChatRoomDebugPageState(); 11 | } 12 | 13 | class _ChatRoomDebugPageState extends State { 14 | String pageName = "example.ChatRoomDebugPage"; 15 | List titles; 16 | String targetId = "kvchatroom1"; 17 | 18 | _ChatRoomDebugPageState(); 19 | @override 20 | void initState() { 21 | super.initState(); 22 | titles = [ 23 | "加入聊天室 1", 24 | "设置 KV", 25 | "强制设置 KV", 26 | "删除 KV", 27 | "强制删除 KV", 28 | "获取单个 KV", 29 | "获取所有 KV", 30 | "退出聊天室 1", 31 | "获取聊天室历史消息", 32 | "加入已存在的聊天室 1", 33 | "聊天室发送消息", 34 | ]; 35 | 36 | RongIMClient.onJoinChatRoom = (String targetId, int status) { 37 | Fluttertoast.showToast( 38 | msg: "加入聊天室 $targetId " + (status == 0 ? "成功" : "失败"), 39 | timeInSecForIos: 2); 40 | }; 41 | 42 | RongIMClient.onQuitChatRoom = (String targetId, int status) { 43 | Fluttertoast.showToast( 44 | msg: "退出聊天室 $targetId " + (status == 0 ? "成功" : "失败"), 45 | timeInSecForIos: 2); 46 | }; 47 | 48 | RongIMClient.onChatRoomReset = (String targetId) { 49 | Fluttertoast.showToast(msg: "聊天室被重制 $targetId ", timeInSecForIos: 2); 50 | }; 51 | 52 | RongIMClient.onChatRoomDestroyed = (String targetId, int type) { 53 | Fluttertoast.showToast( 54 | msg: "聊天室被销毁 $targetId " + 55 | (type == 0 ? "开发者主动销毁" : "聊天室长时间不活跃,被系统自动回收"), 56 | timeInSecForIos: 2); 57 | }; 58 | 59 | RongIMClient.chatRoomKVDidSync = (String roomId) { 60 | DialogUtil.showAlertDiaLog(context, "chatRoomKVDidSync $roomId "); 61 | }; 62 | 63 | RongIMClient.chatRoomKVDidUpdate = (String roomId, Map entry) { 64 | DialogUtil.showAlertDiaLog(context, "chatRoomKVDidUpdate $roomId $entry"); 65 | }; 66 | 67 | RongIMClient.chatRoomKVDidRemove = (String roomId, Map entry) { 68 | DialogUtil.showAlertDiaLog(context, "chatRoomKVDidRemove $roomId $entry"); 69 | }; 70 | 71 | } 72 | 73 | void _didTap(int index) { 74 | developer.log("did tap debug " + titles[index], name: pageName); 75 | switch (index) { 76 | case 0: 77 | _joinChatRoom(); 78 | break; 79 | case 1: 80 | _setEntry(); 81 | break; 82 | case 2: 83 | _forceSetEntry(); 84 | break; 85 | case 3: 86 | _removeEntry(); 87 | break; 88 | case 4: 89 | _forceRemoveEntry(); 90 | break; 91 | case 5: 92 | _getEntry(); 93 | break; 94 | case 6: 95 | _getAllEntry(); 96 | break; 97 | case 7: 98 | _quitChatRoom(); 99 | break; 100 | case 8: 101 | _getChatRoomHistoryMessage(); 102 | break; 103 | case 9: 104 | _joinExistChatRoom(); 105 | break; 106 | case 10: 107 | _sendChatMessage(); 108 | break; 109 | } 110 | } 111 | 112 | void _joinChatRoom() { 113 | RongIMClient.joinChatRoom(targetId, 10); 114 | } 115 | 116 | void _joinExistChatRoom() { 117 | RongIMClient.joinExistChatRoom(targetId, 10); 118 | } 119 | 120 | void _setEntry() { 121 | RongIMClient.setChatRoomEntry( 122 | targetId, "key1", "value1", true, true, "notificationExtra", 123 | (int code) { 124 | DialogUtil.showAlertDiaLog(context, 125 | "设置 KV:{key1: value1}, 发送通知,退出时删除,code:" + CodeUtil.codeString(code)); 126 | }); 127 | } 128 | 129 | void _forceSetEntry() { 130 | RongIMClient.forceSetChatRoomEntry( 131 | targetId, "key2", "value2", false, false, "notificationExtra", 132 | (int code) { 133 | DialogUtil.showAlertDiaLog( 134 | context, 135 | "强制删除 KV:{key2: value2}, 不发送通知,退出时不删除,code:" + 136 | CodeUtil.codeString(code)); 137 | }); 138 | } 139 | 140 | void _removeEntry() { 141 | RongIMClient.removeChatRoomEntry( 142 | targetId, "key1", true, "notificationExtra", (int code) { 143 | DialogUtil.showAlertDiaLog( 144 | context, "删除 KV:key1, 发送通知,code:" + CodeUtil.codeString(code)); 145 | }); 146 | } 147 | 148 | void _forceRemoveEntry() { 149 | RongIMClient.forceRemoveChatRoomEntry( 150 | targetId, "key2", false, "notificationExtra", (int code) { 151 | DialogUtil.showAlertDiaLog( 152 | context, "强制删除 KV:key2, 不发送通知,code:" + CodeUtil.codeString(code)); 153 | }); 154 | } 155 | 156 | void _getEntry() { 157 | RongIMClient.getChatRoomEntry(targetId, "key1", (Map entry, int code) { 158 | DialogUtil.showAlertDiaLog(context, 159 | "获取单个 KV:key1, code:" + CodeUtil.codeString(code) + ",entry:$entry"); 160 | }); 161 | } 162 | 163 | void _getAllEntry() { 164 | RongIMClient.getAllChatRoomEntries(targetId, (Map entry, int code) { 165 | DialogUtil.showAlertDiaLog(context, 166 | "获取所有 KV:code:" + CodeUtil.codeString(code) + ",entry:$entry"); 167 | }); 168 | } 169 | 170 | void _quitChatRoom() { 171 | RongIMClient.quitChatRoom(targetId); 172 | } 173 | 174 | void _sendChatMessage() async { 175 | TextMessage msg = new TextMessage(); 176 | msg.content = "测试文本消息携带用户信息"; 177 | Message message = 178 | await RongIMClient.sendMessage(RCConversationType.ChatRoom, targetId, msg); 179 | } 180 | 181 | void _getChatRoomHistoryMessage() { 182 | RongIMClient.getRemoteChatRoomHistoryMessages( 183 | targetId, 0, 20, RCTimestampOrder.RC_Timestamp_Desc, 184 | (List/**/ msgList, int syncTime, int code) { 185 | DialogUtil.showAlertDiaLog( 186 | context, 187 | "获取聊天室历史消息:code:" + 188 | CodeUtil.codeString(code) + 189 | ",msgListCount:${msgList.length} 条消息\n" + 190 | ",msgList:$msgList" + 191 | ",syncTime:$syncTime"); 192 | }); 193 | } 194 | 195 | @override 196 | Widget build(BuildContext context) { 197 | return Scaffold( 198 | appBar: AppBar( 199 | title: Text("ChatRoom Debug"), 200 | ), 201 | body: ListView.builder( 202 | scrollDirection: Axis.vertical, 203 | itemCount: titles.length, 204 | itemBuilder: (BuildContext context, int index) { 205 | return MaterialButton( 206 | onPressed: () { 207 | _didTap(index); 208 | }, 209 | child: Text(titles[index]), 210 | color: Colors.blue, 211 | ); 212 | }, 213 | ), 214 | ); 215 | } 216 | } 217 | -------------------------------------------------------------------------------- /lib/im/pages/conversation_list_page.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:core'; 3 | 4 | import 'package:flutter/cupertino.dart'; 5 | import 'package:flutter/material.dart'; 6 | import 'package:rongcloud_im_plugin/rongcloud_im_plugin.dart'; 7 | import 'package:shared_preferences/shared_preferences.dart'; 8 | 9 | import 'item/widget_util.dart'; 10 | import 'item/conversation_list_item.dart'; 11 | 12 | import '../util/style.dart'; 13 | import '../util/event_bus.dart'; 14 | import '../util/dialog_util.dart'; 15 | import '../../other/login_page.dart'; 16 | import 'dart:developer' as developer; 17 | 18 | class ConversationListPage extends StatefulWidget { 19 | @override 20 | State createState() { 21 | return new _ConversationListPageState(); 22 | } 23 | } 24 | 25 | class _ConversationListPageState extends State 26 | implements ConversationListItemDelegate { 27 | String pageName = "example.ConversationListPage"; 28 | List conList = []; 29 | List displayConversationType = [ 30 | RCConversationType.Private, 31 | RCConversationType.Group 32 | ]; 33 | ScrollController _scrollController; 34 | double mPosition = 0; 35 | 36 | @override 37 | void initState() { 38 | super.initState(); 39 | addIMhandler(); 40 | updateConversationList(); 41 | 42 | EventBus.instance.addListener(EventKeys.ConversationPageDispose, (arg) { 43 | Timer(Duration(milliseconds: 10), () { 44 | addIMhandler(); 45 | updateConversationList(); 46 | _renfreshUI(); 47 | }); 48 | }); 49 | } 50 | 51 | @override 52 | void dispose() { 53 | super.dispose(); 54 | EventBus.instance.removeListener(EventKeys.ConversationPageDispose); 55 | } 56 | 57 | updateConversationList() async { 58 | List list = await RongIMClient.getConversationList(displayConversationType); 59 | if (list != null) { 60 | // list.sort((a,b) => b.sentTime.compareTo(a.sentTime)); 61 | conList = list; 62 | } 63 | _renfreshUI(); 64 | } 65 | 66 | void _renfreshUI() { 67 | setState(() {}); 68 | } 69 | 70 | addIMhandler() { 71 | EventBus.instance.addListener(EventKeys.ReceiveMessage, (map) { 72 | Message msg = map["message"]; 73 | int left = map["left"]; 74 | bool hasPackage = map["hasPackage"]; 75 | bool isDisplayConversation = msg.conversationType != null && 76 | displayConversationType.contains(msg.conversationType); 77 | //如果离线消息过多,那么可以等到 hasPackage 为 false 并且 left == 0 时更新会话列表 78 | if (!hasPackage && left == 0 && isDisplayConversation) { 79 | updateConversationList(); 80 | } 81 | }); 82 | 83 | RongIMClient.onConnectionStatusChange = (int connectionStatus) { 84 | if (RCConnectionStatus.KickedByOtherClient == connectionStatus || 85 | RCConnectionStatus.TokenIncorrect == connectionStatus || 86 | RCConnectionStatus.UserBlocked == connectionStatus) { 87 | String toast = "连接状态变化 $connectionStatus, 请退出后重新登录"; 88 | DialogUtil.showAlertDiaLog(context, toast, 89 | confirmButton: TextButton( 90 | onPressed: () async { 91 | SharedPreferences prefs = 92 | await SharedPreferences.getInstance(); 93 | prefs.remove("token"); 94 | Navigator.of(context).pushAndRemoveUntil( 95 | new MaterialPageRoute( 96 | builder: (context) => new LoginPage()), 97 | (route) => route == null); 98 | }, 99 | child: Text("重新登录"))); 100 | } else if (RCConnectionStatus.Connected == connectionStatus) { 101 | updateConversationList(); 102 | } 103 | }; 104 | 105 | RongIMClient.onRecallMessageReceived = (Message message) { 106 | updateConversationList(); 107 | }; 108 | 109 | RongIMClient.onDatabaseOpened = (int status) { 110 | updateConversationList(); 111 | }; 112 | } 113 | 114 | void _deleteConversation(Conversation conversation) { 115 | //删除会话需要刷新会话列表数据 116 | RongIMClient.removeConversation( 117 | conversation.conversationType, conversation.targetId, (bool success) { 118 | if (success) { 119 | updateConversationList(); 120 | // // 如果需要删除会话中的消息调用下面的接口 121 | // RongIMClient.deleteMessages( 122 | // conversation.conversationType, conversation.targetId, (int code) { 123 | // updateConversationList(); 124 | // }); 125 | } 126 | }); 127 | } 128 | 129 | void _clearConversationUnread(Conversation conversation) async { 130 | //清空未读需要刷新会话列表数据 131 | bool success = await RongIMClient.clearMessagesUnreadStatus( 132 | conversation.conversationType, conversation.targetId); 133 | if (success) { 134 | updateConversationList(); 135 | } 136 | } 137 | 138 | void _setConversationToTop(Conversation conversation, bool isTop) { 139 | RongIMClient.setConversationToTop( 140 | conversation.conversationType, conversation.targetId, isTop, 141 | (bool status, int code) { 142 | if (code == 0) { 143 | updateConversationList(); 144 | } 145 | }); 146 | } 147 | 148 | void _addScroolListener() { 149 | _scrollController.addListener(() { 150 | mPosition = _scrollController.position.pixels; 151 | }); 152 | } 153 | 154 | Widget _buildConversationListView() { 155 | return new ListView.builder( 156 | scrollDirection: Axis.vertical, 157 | itemCount: conList.length, 158 | controller: _scrollController, 159 | itemBuilder: (BuildContext context, int index) { 160 | if (conList.length <= 0) { 161 | return WidgetUtil.buildEmptyWidget(); 162 | } 163 | return ConversationListItem( 164 | delegate: this, conversation: conList[index]); 165 | }, 166 | ); 167 | } 168 | 169 | @override 170 | Widget build(BuildContext context) { 171 | this._scrollController = ScrollController(initialScrollOffset: mPosition); 172 | _addScroolListener(); 173 | return new Scaffold( 174 | appBar: AppBar( 175 | title: Text("RongCloud IM"), 176 | ), 177 | key: UniqueKey(), 178 | body: _buildConversationListView(), 179 | ); 180 | } 181 | 182 | @override 183 | void didLongPressConversation(Conversation conversation, Offset tapPos) { 184 | Map actionMap = { 185 | RCLongPressAction.DeleteConversationKey: 186 | RCLongPressAction.DeleteConversationValue, 187 | RCLongPressAction.ClearUnreadKey: RCLongPressAction.ClearUnreadValue, 188 | RCLongPressAction.SetConversationToTopKey: conversation.isTop 189 | ? RCLongPressAction.CancelConversationToTopValue 190 | : RCLongPressAction.SetConversationToTopValue 191 | }; 192 | WidgetUtil.showLongPressMenu(context, tapPos, actionMap, (String key) { 193 | developer.log("当前选中的是 " + key, name: pageName); 194 | if (key == RCLongPressAction.DeleteConversationKey) { 195 | _deleteConversation(conversation); 196 | } else if (key == RCLongPressAction.ClearUnreadKey) { 197 | _clearConversationUnread(conversation); 198 | } else if (key == RCLongPressAction.SetConversationToTopKey) { 199 | bool isTop = true; 200 | if (conversation.isTop) { 201 | isTop = false; 202 | } 203 | _setConversationToTop(conversation, isTop); 204 | } else { 205 | developer.log("未实现操作 " + key, name: pageName); 206 | } 207 | }); 208 | } 209 | 210 | @override 211 | void didTapConversation(Conversation conversation) { 212 | Map arg = { 213 | "coversationType": conversation.conversationType, 214 | "targetId": conversation.targetId 215 | }; 216 | Navigator.pushNamed(context, "/conversation", arguments: arg); 217 | } 218 | } 219 | -------------------------------------------------------------------------------- /.flutter-plugins-dependencies: -------------------------------------------------------------------------------- 1 | {"info":"This is a generated file; do not edit or check into version control.","plugins":{"ios":[{"name":"camera","path":"/Users/zoulu/tool/flutter/.pub-cache/hosted/pub.flutter-io.cn/camera-0.5.7+4/","dependencies":[]},{"name":"connectivity","path":"/Users/zoulu/tool/flutter/.pub-cache/hosted/pub.flutter-io.cn/connectivity-0.4.8+6/","dependencies":[]},{"name":"file_picker","path":"/Users/zoulu/tool/flutter/.pub-cache/hosted/pub.flutter-io.cn/file_picker-1.7.0/","dependencies":[]},{"name":"flutter_audio_recorder","path":"/Users/zoulu/tool/flutter/.pub-cache/hosted/pub.flutter-io.cn/flutter_audio_recorder-0.5.5/","dependencies":[]},{"name":"flutter_local_notifications","path":"/Users/zoulu/tool/flutter/.pub-cache/hosted/pub.flutter-io.cn/flutter_local_notifications-0.8.4+3/","dependencies":[]},{"name":"flutter_sound","path":"/Users/zoulu/tool/flutter/.pub-cache/hosted/pub.flutter-io.cn/flutter_sound-2.1.1/","dependencies":["path_provider"]},{"name":"flutter_webview_plugin","path":"/Users/zoulu/tool/flutter/.pub-cache/hosted/pub.flutter-io.cn/flutter_webview_plugin-0.3.11/","dependencies":[]},{"name":"fluttertoast","path":"/Users/zoulu/tool/flutter/.pub-cache/hosted/pub.flutter-io.cn/fluttertoast-3.1.3/","dependencies":[]},{"name":"image_gallery_saver","path":"/Users/zoulu/tool/flutter/.pub-cache/hosted/pub.flutter-io.cn/image_gallery_saver-1.2.2/","dependencies":[]},{"name":"image_picker","path":"/Users/zoulu/tool/flutter/.pub-cache/hosted/pub.flutter-io.cn/image_picker-0.6.5+2/","dependencies":[]},{"name":"open_file","path":"/Users/zoulu/tool/flutter/.pub-cache/hosted/pub.flutter-io.cn/open_file-3.0.1/","dependencies":[]},{"name":"path_provider","path":"/Users/zoulu/tool/flutter/.pub-cache/hosted/pub.flutter-io.cn/path_provider-1.6.7/","dependencies":[]},{"name":"permission_handler","path":"/Users/zoulu/tool/flutter/.pub-cache/hosted/pub.flutter-io.cn/permission_handler-5.0.0+hotfix.6/","dependencies":[]},{"name":"rongcloud_im_plugin","path":"/Users/zoulu/tool/flutter/.pub-cache/hosted/pub.flutter-io.cn/rongcloud_im_plugin-5.1.3/","dependencies":[]},{"name":"shared_preferences","path":"/Users/zoulu/tool/flutter/.pub-cache/hosted/pub.flutter-io.cn/shared_preferences-0.5.7/","dependencies":[]},{"name":"sqflite","path":"/Users/zoulu/tool/flutter/.pub-cache/hosted/pub.flutter-io.cn/sqflite-1.3.0/","dependencies":[]},{"name":"url_launcher","path":"/Users/zoulu/tool/flutter/.pub-cache/hosted/pub.flutter-io.cn/url_launcher-5.4.10/","dependencies":[]},{"name":"video_player","path":"/Users/zoulu/tool/flutter/.pub-cache/hosted/pub.flutter-io.cn/video_player-0.10.9+1/","dependencies":[]},{"name":"webview_flutter","path":"/Users/zoulu/tool/flutter/.pub-cache/hosted/pub.flutter-io.cn/webview_flutter-0.3.22+1/","dependencies":[]}],"android":[{"name":"camera","path":"/Users/zoulu/tool/flutter/.pub-cache/hosted/pub.flutter-io.cn/camera-0.5.7+4/","dependencies":[]},{"name":"connectivity","path":"/Users/zoulu/tool/flutter/.pub-cache/hosted/pub.flutter-io.cn/connectivity-0.4.8+6/","dependencies":[]},{"name":"file_picker","path":"/Users/zoulu/tool/flutter/.pub-cache/hosted/pub.flutter-io.cn/file_picker-1.7.0/","dependencies":["flutter_plugin_android_lifecycle"]},{"name":"flutter_audio_recorder","path":"/Users/zoulu/tool/flutter/.pub-cache/hosted/pub.flutter-io.cn/flutter_audio_recorder-0.5.5/","dependencies":[]},{"name":"flutter_local_notifications","path":"/Users/zoulu/tool/flutter/.pub-cache/hosted/pub.flutter-io.cn/flutter_local_notifications-0.8.4+3/","dependencies":[]},{"name":"flutter_plugin_android_lifecycle","path":"/Users/zoulu/tool/flutter/.pub-cache/hosted/pub.flutter-io.cn/flutter_plugin_android_lifecycle-1.0.7/","dependencies":[]},{"name":"flutter_sound","path":"/Users/zoulu/tool/flutter/.pub-cache/hosted/pub.flutter-io.cn/flutter_sound-2.1.1/","dependencies":["path_provider"]},{"name":"flutter_webview_plugin","path":"/Users/zoulu/tool/flutter/.pub-cache/hosted/pub.flutter-io.cn/flutter_webview_plugin-0.3.11/","dependencies":[]},{"name":"fluttertoast","path":"/Users/zoulu/tool/flutter/.pub-cache/hosted/pub.flutter-io.cn/fluttertoast-3.1.3/","dependencies":[]},{"name":"image_gallery_saver","path":"/Users/zoulu/tool/flutter/.pub-cache/hosted/pub.flutter-io.cn/image_gallery_saver-1.2.2/","dependencies":[]},{"name":"image_picker","path":"/Users/zoulu/tool/flutter/.pub-cache/hosted/pub.flutter-io.cn/image_picker-0.6.5+2/","dependencies":["flutter_plugin_android_lifecycle"]},{"name":"open_file","path":"/Users/zoulu/tool/flutter/.pub-cache/hosted/pub.flutter-io.cn/open_file-3.0.1/","dependencies":[]},{"name":"path_provider","path":"/Users/zoulu/tool/flutter/.pub-cache/hosted/pub.flutter-io.cn/path_provider-1.6.7/","dependencies":[]},{"name":"permission_handler","path":"/Users/zoulu/tool/flutter/.pub-cache/hosted/pub.flutter-io.cn/permission_handler-5.0.0+hotfix.6/","dependencies":[]},{"name":"rongcloud_im_plugin","path":"/Users/zoulu/tool/flutter/.pub-cache/hosted/pub.flutter-io.cn/rongcloud_im_plugin-5.1.3/","dependencies":[]},{"name":"shared_preferences","path":"/Users/zoulu/tool/flutter/.pub-cache/hosted/pub.flutter-io.cn/shared_preferences-0.5.7/","dependencies":[]},{"name":"sqflite","path":"/Users/zoulu/tool/flutter/.pub-cache/hosted/pub.flutter-io.cn/sqflite-1.3.0/","dependencies":[]},{"name":"url_launcher","path":"/Users/zoulu/tool/flutter/.pub-cache/hosted/pub.flutter-io.cn/url_launcher-5.4.10/","dependencies":[]},{"name":"video_player","path":"/Users/zoulu/tool/flutter/.pub-cache/hosted/pub.flutter-io.cn/video_player-0.10.9+1/","dependencies":[]},{"name":"webview_flutter","path":"/Users/zoulu/tool/flutter/.pub-cache/hosted/pub.flutter-io.cn/webview_flutter-0.3.22+1/","dependencies":[]}],"macos":[{"name":"connectivity_macos","path":"/Users/zoulu/tool/flutter/.pub-cache/hosted/pub.flutter-io.cn/connectivity_macos-0.1.0+3/","dependencies":[]},{"name":"path_provider_macos","path":"/Users/zoulu/tool/flutter/.pub-cache/hosted/pub.flutter-io.cn/path_provider_macos-0.0.4+1/","dependencies":[]},{"name":"shared_preferences_macos","path":"/Users/zoulu/tool/flutter/.pub-cache/hosted/pub.flutter-io.cn/shared_preferences_macos-0.0.1+7/","dependencies":[]},{"name":"sqflite","path":"/Users/zoulu/tool/flutter/.pub-cache/hosted/pub.flutter-io.cn/sqflite-1.3.0/","dependencies":[]},{"name":"url_launcher_macos","path":"/Users/zoulu/tool/flutter/.pub-cache/hosted/pub.flutter-io.cn/url_launcher_macos-0.0.1+7/","dependencies":[]}],"linux":[],"windows":[],"web":[{"name":"shared_preferences_web","path":"/Users/zoulu/tool/flutter/.pub-cache/hosted/pub.flutter-io.cn/shared_preferences_web-0.1.2+4/","dependencies":[]},{"name":"url_launcher_web","path":"/Users/zoulu/tool/flutter/.pub-cache/hosted/pub.flutter-io.cn/url_launcher_web-0.1.1+6/","dependencies":[]},{"name":"video_player_web","path":"/Users/zoulu/tool/flutter/.pub-cache/hosted/pub.flutter-io.cn/video_player_web-0.1.2+1/","dependencies":[]}]},"dependencyGraph":[{"name":"camera","dependencies":[]},{"name":"connectivity","dependencies":["connectivity_macos"]},{"name":"connectivity_macos","dependencies":[]},{"name":"file_picker","dependencies":["flutter_plugin_android_lifecycle"]},{"name":"flutter_audio_recorder","dependencies":[]},{"name":"flutter_local_notifications","dependencies":[]},{"name":"flutter_plugin_android_lifecycle","dependencies":[]},{"name":"flutter_sound","dependencies":["path_provider"]},{"name":"flutter_webview_plugin","dependencies":[]},{"name":"fluttertoast","dependencies":[]},{"name":"image_gallery_saver","dependencies":[]},{"name":"image_picker","dependencies":["flutter_plugin_android_lifecycle"]},{"name":"open_file","dependencies":[]},{"name":"path_provider","dependencies":["path_provider_macos"]},{"name":"path_provider_macos","dependencies":[]},{"name":"permission_handler","dependencies":[]},{"name":"rongcloud_im_plugin","dependencies":[]},{"name":"shared_preferences","dependencies":["shared_preferences_macos","shared_preferences_web"]},{"name":"shared_preferences_macos","dependencies":[]},{"name":"shared_preferences_web","dependencies":[]},{"name":"sqflite","dependencies":[]},{"name":"url_launcher","dependencies":["url_launcher_web","url_launcher_macos"]},{"name":"url_launcher_macos","dependencies":[]},{"name":"url_launcher_web","dependencies":[]},{"name":"video_player","dependencies":["video_player_web"]},{"name":"video_player_web","dependencies":[]},{"name":"webview_flutter","dependencies":[]}],"date_created":"2021-07-05 15:20:01.319978","version":"2.0.5"} -------------------------------------------------------------------------------- /lib/main.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_rong/test_message.dart'; 3 | import 'package:rongcloud_im_plugin/rongcloud_im_plugin.dart' as prefix; 4 | import 'package:flutter_local_notifications/flutter_local_notifications.dart'; 5 | import 'other/home_page.dart'; 6 | 7 | import 'im/util/event_bus.dart'; 8 | import 'user_data.dart'; 9 | import 'router.dart'; 10 | import 'dart:developer' as developer; 11 | 12 | void main() => runApp(MyApp()); 13 | 14 | class MyApp extends StatefulWidget { 15 | @override 16 | _MyAppState createState() => _MyAppState(); 17 | 18 | static BuildContext getContext() { 19 | return _MyAppState.getContext(); 20 | } 21 | } 22 | 23 | class _MyAppState extends State with WidgetsBindingObserver { 24 | String pageName = "example.main"; 25 | AppLifecycleState currentState = AppLifecycleState.resumed; 26 | DateTime notificationQuietEndTime; 27 | DateTime notificationQuietStartTime; 28 | static BuildContext appContext; 29 | 30 | static BuildContext getContext() { 31 | return appContext; 32 | } 33 | 34 | @override 35 | void initState() { 36 | super.initState(); 37 | 38 | // prefix.PushConfig pushConfig = prefix.PushConfig(); 39 | // pushConfig.enableHWPush = true; 40 | // pushConfig.enableVivoPush = true; 41 | 42 | // 小米推送,请填入自己申请的 appkey 和 id 43 | // pushConfig.miAppKey = "1111147338625"; 44 | // pushConfig.miAppId = "2222203761517473625"; 45 | 46 | // oppo 推送,请填入自己申请的 appkey 和 secret 47 | // pushConfig.oppoAppKey = "11111146d261446dbd3c94bb04d322de"; 48 | // pushConfig.oppoAppSecret = "2222223d5ce1414ea4b6d75c880a3031"; 49 | 50 | //魅族推送 请填入自己申请的 appkey 和 id 51 | // pushConfig.mzAppKey = "11111802ac4bd5843d694517307896"; 52 | // pushConfig.mzAppId = "222288"; 53 | // pushConfig.enableFCM = true; 54 | 55 | // prefix.RongIMClient.setAndroidPushConfig(pushConfig); 56 | 57 | //1.初始化 im SDK 58 | prefix.RongIMClient.init(RongAppKey); 59 | //注册自定义消息 60 | prefix.RongIMClient.addMessageDecoder(TestMessage.objectName, (content) { 61 | TestMessage msg = new TestMessage(); 62 | msg.decode(content); 63 | return msg; 64 | }); 65 | // _initUserInfoCache(); 66 | 67 | WidgetsBinding.instance.addObserver(this); 68 | 69 | EventBus.instance.addListener(EventKeys.UpdateNotificationQuietStatus, 70 | (map) { 71 | _getNotificationQuietHours(); 72 | }); 73 | 74 | prefix.RongIMClient.onMessageReceivedWrapper = 75 | (prefix.Message msg, int left, bool hasPackage, bool offline) { 76 | String hasP = hasPackage ? "true" : "false"; 77 | String off = offline ? "true" : "false"; 78 | if (msg.content != null) { 79 | developer.log( 80 | "object onMessageReceivedWrapper objName:" + 81 | msg.content.getObjectName() + 82 | " msgContent:" + 83 | msg.content.encode() + 84 | " left:" + 85 | left.toString() + 86 | " hasPackage:" + 87 | hasP + 88 | " offline:" + 89 | off, 90 | name: pageName); 91 | } else { 92 | developer.log( 93 | "object onMessageReceivedWrapper objName: ${msg.objectName} content is null left:${left.toString()} hasPackage:$hasP offline:$off", 94 | name: pageName); 95 | } 96 | if (currentState == AppLifecycleState.paused && 97 | !checkNoficationQuietStatus()) { 98 | EventBus.instance.commit(EventKeys.ReceiveMessage, 99 | {"message": msg, "left": left, "hasPackage": hasPackage}); 100 | prefix.RongIMClient.getConversationNotificationStatus( 101 | msg.conversationType, msg.targetId, (int status, int code) { 102 | if (status == 1) { 103 | _postLocalNotification(msg, left); 104 | } 105 | }); 106 | } else { 107 | //通知其他页面收到消息 108 | EventBus.instance.commit(EventKeys.ReceiveMessage, 109 | {"message": msg, "left": left, "hasPackage": hasPackage}); 110 | } 111 | }; 112 | 113 | prefix.RongIMClient.onDataReceived = (Map map) { 114 | developer.log("object onDataReceived " + map.toString(), name: pageName); 115 | }; 116 | 117 | prefix.RongIMClient.onMessageReceiptRequest = (Map map) { 118 | EventBus.instance.commit(EventKeys.ReceiveReceiptRequest, map); 119 | developer.log("object onMessageReceiptRequest " + map.toString(), 120 | name: pageName); 121 | }; 122 | 123 | prefix.RongIMClient.onMessageReceiptResponse = (Map map) { 124 | EventBus.instance.commit(EventKeys.ReceiveReceiptResponse, map); 125 | developer.log("object onMessageReceiptResponse " + map.toString(), 126 | name: pageName); 127 | }; 128 | 129 | prefix.RongIMClient.onReceiveReadReceipt = (Map map) { 130 | EventBus.instance.commit(EventKeys.ReceiveReadReceipt, map); 131 | developer.log("object onReceiveReadReceipt " + map.toString(), 132 | name: pageName); 133 | }; 134 | } 135 | 136 | void _getNotificationQuietHours() { 137 | prefix.RongIMClient.getNotificationQuietHours( 138 | (int code, String startTime, int spansMin) { 139 | if (startTime != null && startTime.length > 0 && spansMin > 0) { 140 | DateTime now = DateTime.now(); 141 | String nowString = now.year.toString() + 142 | "-" + 143 | now.month.toString().padLeft(2, '0') + 144 | "-" + 145 | now.day.toString().padLeft(2, '0') + 146 | " " + 147 | startTime; 148 | DateTime start = DateTime.parse(nowString); 149 | notificationQuietStartTime = start; 150 | notificationQuietEndTime = start.add(Duration(minutes: spansMin)); 151 | } else { 152 | notificationQuietStartTime = null; 153 | notificationQuietEndTime = null; 154 | } 155 | }); 156 | } 157 | 158 | bool checkNoficationQuietStatus() { 159 | bool isNotificationQuiet = false; 160 | 161 | DateTime now = DateTime.now(); 162 | if (notificationQuietStartTime != null && 163 | notificationQuietEndTime != null && 164 | now.isAfter(notificationQuietStartTime) && 165 | now.isBefore(notificationQuietEndTime)) { 166 | isNotificationQuiet = true; 167 | } 168 | 169 | return isNotificationQuiet; 170 | } 171 | 172 | void _postLocalNotification(prefix.Message msg, int left) async { 173 | FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin = 174 | new FlutterLocalNotificationsPlugin(); 175 | var initializationSettingsAndroid = new AndroidInitializationSettings( 176 | "app_icon"); // app_icon 所在目录为 res/drawable/ 177 | var initializationSettingsIOS = new IOSInitializationSettings( 178 | requestAlertPermission: true, requestSoundPermission: true); 179 | var initializationSettings = new InitializationSettings( 180 | initializationSettingsAndroid, initializationSettingsIOS); 181 | flutterLocalNotificationsPlugin.initialize(initializationSettings, 182 | onSelectNotification: null); 183 | 184 | var androidPlatformChannelSpecifics = AndroidNotificationDetails( 185 | 'your channel id', 'your channel name', 'your channel description', 186 | importance: Importance.Max, priority: Priority.High, ticker: '本地通知'); 187 | 188 | var platformChannelSpecifics = 189 | NotificationDetails(androidPlatformChannelSpecifics, null); 190 | 191 | String content = "测试本地通知"; 192 | 193 | await flutterLocalNotificationsPlugin.show( 194 | 0, 'RongCloud IM', content, platformChannelSpecifics, 195 | payload: 'item x'); 196 | } 197 | 198 | @override 199 | Widget build(BuildContext context) { 200 | appContext = context; 201 | return MaterialApp( 202 | onGenerateRoute: onGenerateRoute, 203 | theme: ThemeData(primaryColor: Colors.blue), 204 | home: HomePage(), 205 | ); 206 | } 207 | 208 | @override 209 | void didChangeAppLifecycleState(AppLifecycleState state) { 210 | developer.log("--" + state.toString(), name: pageName); 211 | currentState = state; 212 | switch (state) { 213 | case AppLifecycleState.inactive: // 处于这种状态的应用程序应该假设它们可能在任何时候暂停。 214 | break; 215 | case AppLifecycleState.resumed: // 应用程序可见,前台 216 | break; 217 | case AppLifecycleState.paused: // 应用程序不可见,后台 218 | break; 219 | case AppLifecycleState.detached: // 申请将暂时暂停 220 | break; 221 | } 222 | } 223 | } 224 | -------------------------------------------------------------------------------- /lib/other/debug_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:rongcloud_im_plugin/rongcloud_im_plugin.dart' as prefix; 3 | import '../im/util/dialog_util.dart'; 4 | import '../im/util/event_bus.dart'; 5 | import 'dart:developer' as developer; 6 | 7 | class DebugPage extends StatefulWidget { 8 | @override 9 | State createState() => _DebugPageState(); 10 | } 11 | 12 | class _DebugPageState extends State { 13 | String pageName = "example.DebugPage"; 14 | List titles = [ 15 | "设置全局屏蔽某个时间段的消息提醒", 16 | "查询已设置的全局时间段消息提醒屏蔽", 17 | "删除已设置的全局时间段消息提醒屏蔽", 18 | "获取特定会话", 19 | "获取特定方向的消息列表", 20 | "分页获取会话", 21 | "消息携带用户信息", 22 | "聊天室状态存储测试", 23 | "获取免打扰的会话列表", 24 | ]; 25 | 26 | void _didTap(int index, BuildContext context) { 27 | developer.log("did tap debug " + titles[index], name: pageName); 28 | switch (index) { 29 | case 0: 30 | _setNotificationQuietHours(); 31 | break; 32 | case 1: 33 | _getNotificationQuietHours(); 34 | break; 35 | case 2: 36 | _removeNotificationQuietHours(); 37 | break; 38 | case 3: 39 | _getCons(); 40 | break; 41 | case 4: 42 | _getMessagesByDirection(); 43 | break; 44 | case 5: 45 | _getConversationListByPage(); 46 | break; 47 | case 6: 48 | _sendMessageAddSendUserInfo(); 49 | break; 50 | case 7: 51 | _pushToChatRoomDebug(context); 52 | break; 53 | case 8: 54 | _getBlockedConversationList(); 55 | break; 56 | } 57 | } 58 | 59 | void _setNotificationQuietHours() { 60 | developer.log("_setNotificationQuietHours", name: pageName); 61 | prefix.RongIMClient.setNotificationQuietHours("09:00:00", 600, (int code) { 62 | EventBus.instance.commit(EventKeys.UpdateNotificationQuietStatus, {}); 63 | String toast = "设置全局屏蔽某个时间段的消息提醒:\n" + 64 | (code == 0 ? "设置成功" : "设置失败, code:" + code.toString()); 65 | developer.log(toast, name: pageName); 66 | DialogUtil.showAlertDiaLog(context, toast); 67 | }); 68 | } 69 | 70 | void _getNotificationQuietHours() { 71 | developer.log("_getNotificationQuietHours", name: pageName); 72 | prefix.RongIMClient.getNotificationQuietHours( 73 | (int code, String startTime, int spansMin) { 74 | String toast = "查询已设置的全局时间段消息提醒屏蔽\n: startTime:" + 75 | startTime + 76 | " spansMin:" + 77 | spansMin.toString() + 78 | (code == 0 ? "" : "\n设置失败, code:" + code.toString()); 79 | developer.log(toast, name: pageName); 80 | DialogUtil.showAlertDiaLog(context, toast); 81 | }); 82 | } 83 | 84 | void _removeNotificationQuietHours() { 85 | developer.log("_removeNotificationQuietHours", name: pageName); 86 | prefix.RongIMClient.removeNotificationQuietHours((int code) { 87 | EventBus.instance.commit(EventKeys.UpdateNotificationQuietStatus, {}); 88 | String toast = "删除已设置的全局时间段消息提醒屏蔽:\n" + 89 | (code == 0 ? "删除成功" : "删除失败, code:" + code.toString()); 90 | developer.log(toast, name: pageName); 91 | DialogUtil.showAlertDiaLog(context, toast); 92 | }); 93 | } 94 | 95 | void _getCons() async { 96 | int conversationType = prefix.RCConversationType.Private; 97 | String targetId = "SealTalk"; 98 | prefix.Conversation con = 99 | await prefix.RongIMClient.getConversation(conversationType, targetId); 100 | if (con != null) { 101 | developer.log( 102 | "getConversation type:" + 103 | con.conversationType.toString() + 104 | " targetId:" + 105 | con.targetId, 106 | name: pageName); 107 | } else { 108 | developer.log( 109 | "不存在该会话 type:" + 110 | conversationType.toString() + 111 | " targetId:" + 112 | targetId, 113 | name: pageName); 114 | } 115 | } 116 | 117 | void _getMessagesByDirection() async { 118 | int conversationType = prefix.RCConversationType.Private; 119 | String targetId = "SealTalk"; 120 | int sentTime = 1567756686643; 121 | int beforeCount = 10; 122 | int afterCount = 10; 123 | List msgs = await prefix.RongIMClient.getHistoryMessages( 124 | conversationType, targetId, sentTime, beforeCount, afterCount); 125 | if (msgs == null) { 126 | developer.log( 127 | "未获取消息列表 type:" + 128 | conversationType.toString() + 129 | " targetId:" + 130 | targetId, 131 | name: pageName); 132 | } else { 133 | for (prefix.Message msg in msgs) { 134 | developer.log( 135 | "getHistoryMessages messageId:" + 136 | msg.messageId.toString() + 137 | " objName:" + 138 | msg.objectName + 139 | " sentTime:" + 140 | msg.sentTime.toString(), 141 | name: pageName); 142 | } 143 | } 144 | } 145 | 146 | void _getConversationListByPage() async { 147 | List list = await prefix.RongIMClient.getConversationListByPage( 148 | [prefix.RCConversationType.Private, prefix.RCConversationType.Group], 149 | 2, 150 | 0); 151 | prefix.Conversation lastCon; 152 | if (list != null && list.length > 0) { 153 | list.sort((a, b) => b.sentTime.compareTo(a.sentTime)); 154 | for (int i = 0; i < list.length; i++) { 155 | prefix.Conversation con = list[i]; 156 | developer.log( 157 | "first targetId:" + 158 | con.targetId + 159 | " " + 160 | "time:" + 161 | con.sentTime.toString(), 162 | name: pageName); 163 | lastCon = con; 164 | } 165 | } 166 | if (lastCon != null) { 167 | list = await prefix.RongIMClient.getConversationListByPage( 168 | [prefix.RCConversationType.Private, prefix.RCConversationType.Group], 169 | 2, 170 | lastCon.sentTime); 171 | if (list != null && list.length > 0) { 172 | list.sort((a, b) => b.sentTime.compareTo(a.sentTime)); 173 | for (int i = 0; i < list.length; i++) { 174 | prefix.Conversation con = list[i]; 175 | developer.log( 176 | "last targetId:" + 177 | con.targetId + 178 | " " + 179 | "time:" + 180 | con.sentTime.toString(), 181 | name: pageName); 182 | } 183 | } 184 | } 185 | } 186 | 187 | void _sendMessageAddSendUserInfo() async { 188 | prefix.TextMessage msg = new prefix.TextMessage(); 189 | msg.content = "测试文本消息携带用户信息"; 190 | /* 191 | 测试携带用户信息 192 | */ 193 | prefix.UserInfo sendUserInfo = new prefix.UserInfo(); 194 | sendUserInfo.name = "textSendUser.name"; 195 | sendUserInfo.userId = "textSendUser.userId"; 196 | sendUserInfo.portraitUri = "textSendUser.portraitUrl"; 197 | sendUserInfo.extra = "textSendUser.extra"; 198 | msg.sendUserInfo = sendUserInfo; 199 | 200 | prefix.Message message = await prefix.RongIMClient.sendMessage( 201 | prefix.RCConversationType.Private, "SealTalk", msg); 202 | String toast = "发送消息携带用户信息:\n 消息的 objectName:" + 203 | message.content.getObjectName() + 204 | "\nmsgContent:" + 205 | message.content.encode(); 206 | developer.log(toast, name: pageName); 207 | DialogUtil.showAlertDiaLog(context, toast); 208 | } 209 | 210 | void _pushToChatRoomDebug(BuildContext context) { 211 | Navigator.pushNamed(context, "/chatroom_debug"); 212 | } 213 | 214 | void _getBlockedConversationList() { 215 | prefix.RongIMClient.getBlockedConversationList( 216 | [prefix.RCConversationType.Private, prefix.RCConversationType.Group], 217 | (List convertionList, int code) { 218 | String toast = "消息免打扰会话数量:\n ${convertionList.length}"; 219 | // for (prefix.Conversation conversation in convertionList) { 220 | // toast = toast + conversation.toString(); 221 | // } 222 | DialogUtil.showAlertDiaLog(context, toast); 223 | }); 224 | } 225 | 226 | @override 227 | Widget build(BuildContext context) { 228 | return Scaffold( 229 | appBar: AppBar( 230 | title: Text("Debug"), 231 | ), 232 | body: ListView.builder( 233 | scrollDirection: Axis.vertical, 234 | itemCount: titles.length, 235 | itemBuilder: (BuildContext context, int index) { 236 | return MaterialButton( 237 | onPressed: () { 238 | _didTap(index, context); 239 | }, 240 | child: Text(titles[index]), 241 | color: Colors.blue, 242 | ); 243 | }, 244 | ), 245 | ); 246 | } 247 | } 248 | -------------------------------------------------------------------------------- /lib/im/util/style.dart: -------------------------------------------------------------------------------- 1 | class RCColor { 2 | static const GeneralBgColor = 0xffEFEFEF; //通用背景色 3 | 4 | //会话列表相关颜色 5 | static const ConListTitleColor = 0xff000000; 6 | static const ConListDigestColor = 0xff6C7B8B; 7 | static const ConListUnreadColor = 0xffCD3333; 8 | static const ConListUnreadTextColor = 0xffffffff; 9 | static const ConListTimeColor = 0xff6C7B8B; 10 | static const ConListItemBgColor = 0xffffffff; 11 | static const ConListBorderColor = 0xff6C7B8B; 12 | static const ConListTopBgColor = 0xFFBBDEFB; 13 | static const ConCombineMsgContentColor = 0xFF9E9E9E; 14 | static const ConReferenceMsgContentColor = 0xFF9E9E9E; 15 | //会话页面,消息相关颜色 16 | static const MessageSendBgColor = 0xffC8E9FD; 17 | static const MessageReceiveBgColor = 0xffffffff; 18 | static const MessageTimeBgColor = 0xffC8C8C8; 19 | static const MessageNameBgColor = 0xff9B9B9B; 20 | 21 | // 底部引用消息 22 | static const BottomReferenceNameColor = 0xff999999; 23 | static const BottomReferenceContentColor = 0xff333333; 24 | static const BottomReferenceContentColorFile = 0xff436EEE; 25 | } 26 | 27 | class RCFont { 28 | //会话列表相关字体大小 29 | static const double ConListTitleFont = 16; 30 | static const double ConListTimeFont = 12; 31 | static const double ConListUnreadFont = 8; 32 | static const double ConListDigestFont = 12; 33 | 34 | //会话页面,消息相关字体大小 35 | static const double MessageTextFont = 18; 36 | static const double MessageTimeFont = 12; 37 | static const double MessageNameFont = 14; 38 | static const double MessageNotifiFont = 15; 39 | static const double MessageCombineTitleFont = 12; 40 | static const double MessageCombineContentFont = 10; 41 | static const double MessageReferenceTitleFont = 12; 42 | static const double MessageReferenceContentFont = 10; 43 | 44 | //加号扩展栏 45 | static const double ExtIconSize = 40; 46 | static const double ExtTextFont = 13; 47 | static const double CommonPhrasesSize = 14; 48 | 49 | // 底部引用消息 50 | static const double BottomReferenceNameSize = 13; 51 | static const double BottomReferenceContentSize = 14; 52 | } 53 | 54 | class RCLayout { 55 | //会话列表页面布局 56 | static const double ConListPortraitSize = 45; //会话列表头像大小 57 | static const double ConListItemHeight = 70; //会话列表 item 高度 58 | static const double ConListUnreadSize = 15; //会话列表未读数大小 59 | 60 | //消息页面布局 61 | static const double MessageTimeItemWidth = 80; 62 | static const double MessageTimeItemHeight = 22; 63 | static const double MessageErrorHeight = 20; 64 | static const double RichMessageImageSize = 45; 65 | 66 | //小灰条消息宽高 67 | static const double MessageNotifiItemWidth = 140; 68 | static const double MessageNotifiItemHeight = 30; 69 | 70 | //加号扩展栏 71 | static const double ExtIconLayoutSize = 50; 72 | static const double ExtentionLayoutWidth = 200; 73 | static const double CommonPhrasesHeight = 36; 74 | 75 | //底部输入框 76 | static const double BottomIconLayoutSize = 32; 77 | } 78 | 79 | //长按 menu 的 Action 80 | class RCLongPressAction { 81 | //如果用户点击了空白,会触发 UndefinedKey 82 | static const String UndefinedKey = "UndefinedKey"; 83 | 84 | static const String DeleteConversationKey = "DeleteConversationKey"; 85 | static const String DeleteConversationValue = "删除会话"; 86 | 87 | static const String ClearUnreadKey = "ClearUnreadKey"; 88 | static const String ClearUnreadValue = "清除未读"; 89 | 90 | static const String SetConversationToTopKey = "SetConversationToTopKey"; 91 | static const String SetConversationToTopValue = "设置置顶"; 92 | static const String CancelConversationToTopValue = "取消置顶"; 93 | 94 | static const String CopyKey = "CopyKey"; 95 | static const String CopyValue = "复制"; 96 | 97 | static const String DeleteKey = "DeleteKey"; 98 | static const String DeleteValue = "删除"; 99 | 100 | static const String RecallKey = "RecallMessage"; 101 | static const String RecallValue = "撤回消息"; 102 | 103 | static const String MutiSelectKey = "MutiSelectMessage"; 104 | static const String MutiSelectValue = "多选"; 105 | 106 | static const String ReferenceKey = "ReferenceMessage"; 107 | static const String ReferenceValue = "引用消息"; 108 | } 109 | 110 | class RCString { 111 | static const String BottomInputTextHint = "随便说点什么吧"; 112 | static const String BottomTapSpeak = "按住 说话"; 113 | static const String BottomCommonPhrases = "快捷回复"; 114 | static const String ConRecallMessageSuccess = "成功撤回一条消息"; 115 | static const String ConHaveMentioned = "[有人@我] "; 116 | static const String ConDraft = "[草稿] "; 117 | static const String ConNoIdentify = ""; 118 | static const String ConTyping = "对方正在输入..."; 119 | static const String ConSpeaking = "对方正在讲话..."; 120 | static const String ExtPhoto = "相册"; 121 | static const String ExtCamera = "相机"; 122 | static const String ExtVideo = "视频"; 123 | static const String ExtFolder = "文件"; 124 | static const String ConCancel = "取消"; 125 | static const String SelectConTitle = "选择会话"; 126 | static const String ForwardHint = 127 | "目前转发仅支持文本、语音、图片、Gif、视频、文件和图文消息,其他无法识别的消息类型暂不支持转发。"; 128 | static const String ChatRecord = "聊天记录"; 129 | static const String GroupChatRecord = "群聊的聊天记录"; 130 | static const String ExtSecretChat = "阅后即焚"; 131 | 132 | // 合并消息 133 | static const String RCMessageContentImage = "[图片]"; 134 | static const String RCMessageContentVoice = "[语音]"; 135 | static const String RCMessageContentRichText = "[图文]"; 136 | static const String RCMessageContentLocation = "[位置]"; 137 | static const String RCMessageContentDraft = "[草稿]"; 138 | static const String RCMessageContentMentioned = "[有人@我]"; 139 | static const String RCMessageContentFile = "[文件]"; 140 | static const String RCMessageContentSight = "[小视频]"; 141 | static const String RCMessageContentBurn = "[阅后即焚]"; 142 | static const String RCMessageContentCombine = "[聊天记录]"; 143 | static const String RCMessageContentCard = "[个人名片]"; 144 | static const String RCMessageContentSticker = "[动态表情]"; 145 | static const String RCMessageContentRp = "[红包]"; 146 | static const String RCMessageContentVst = "[音视频通话]"; 147 | static const String RCCombineChatHistory = "聊天记录"; 148 | static const String RCCombineGroupChatHistory = "群聊的聊天记录"; 149 | } 150 | 151 | class RCDuration { 152 | static const int TextMessageBurnDuration = 10; 153 | static const int MediaMessageBurnDuration = 30; 154 | } 155 | 156 | class Emoji { 157 | // 格式:"\u{1f3a4}" 158 | static const List emojiList = [ 159 | "\u{1f603}", 160 | "\u{1f600}", 161 | "\u{1f60a}", 162 | "\u{263a}", 163 | "\u{1f609}", 164 | "\u{1f60d}", 165 | "\u{1f618}", 166 | "\u{1f61a}", 167 | "\u{1f61c}", 168 | "\u{1f61d}", 169 | "\u{1f633}", 170 | "\u{1f601}", 171 | "\u{1f614}", 172 | "\u{1f60c}", 173 | "\u{1f612}", 174 | "\u{1f61f}", 175 | "\u{1f61e}", 176 | "\u{1f623}", 177 | "\u{1f622}", 178 | "\u{1f602}", 179 | "\u{1f62d}", 180 | "\u{1f62a}", 181 | "\u{1f630}", 182 | "\u{1f605}", 183 | "\u{1f613}", 184 | "\u{1f62b}", 185 | "\u{1f629}", 186 | "\u{1f628}", 187 | "\u{1f631}", 188 | "\u{1f621}", 189 | "\u{1f624}", 190 | "\u{1f616}", 191 | "\u{1f606}", 192 | "\u{1f60b}", 193 | "\u{1f637}", 194 | "\u{1f60e}", 195 | "\u{1f634}", 196 | "\u{1f632}", 197 | "\u{1f635}", 198 | "\u{1f608}", 199 | "\u{1f47f}", 200 | "\u{1f62f}", 201 | "\u{1f62c}", 202 | "\u{1f615}", 203 | "\u{1f636}", 204 | "\u{1f607}", 205 | "\u{1f60f}", 206 | "\u{1f611}", 207 | "\u{1f648}", 208 | "\u{1f649}", 209 | "\u{1f64a}", 210 | "\u{1f47d}", 211 | "\u{1f4a9}", 212 | "\u{2764}", 213 | "\u{1f494}", 214 | "\u{1f525}", 215 | "\u{1f4a2}", 216 | "\u{1f4a4}", 217 | "\u{1f6ab}", 218 | "\u{2b50}", 219 | "\u{26a1}", 220 | "\u{1f319}", 221 | "\u{2600}", 222 | "\u{26c5}", 223 | "\u{2601}", 224 | "\u{2744}", 225 | "\u{2614}", 226 | "\u{26c4}", 227 | "\u{1f44d}", 228 | "\u{1f44e}", 229 | "\u{1f91d}", 230 | "\u{1f44c}", 231 | "\u{1f44a}", 232 | "\u{270a}", 233 | "\u{270c}", 234 | "\u{270b}", 235 | "\u{1f64f}", 236 | "\u{261d}", 237 | "\u{1f44f}", 238 | "\u{1f4aa}", 239 | "\u{1f46a}", 240 | "\u{1f46b}", 241 | "\u{1f47c}", 242 | "\u{1f434}", 243 | "\u{1f436}", 244 | "\u{1f437}", 245 | "\u{1f47b}", 246 | "\u{1f339}", 247 | "\u{1f33b}", 248 | "\u{1f332}", 249 | "\u{1f384}", 250 | "\u{1f381}", 251 | "\u{1f389}", 252 | "\u{1f4b0}", 253 | "\u{1f382}", 254 | "\u{1f356}", 255 | "\u{1f35a}", 256 | "\u{1f366}", 257 | "\u{1f36b}", 258 | "\u{1f349}", 259 | "\u{1f377}", 260 | "\u{1f37b}", 261 | "\u{2615}", 262 | "\u{1f3c0}", 263 | "\u{26bd}", 264 | "\u{1f3c2}", 265 | "\u{1f3a4}", 266 | "\u{1f3b5}", 267 | "\u{1f3b2}", 268 | "\u{1f004}", 269 | "\u{1f451}", 270 | "\u{1f484}", 271 | "\u{1f48b}", 272 | "\u{1f48d}", 273 | "\u{1f4da}", 274 | "\u{1f393}", 275 | "\u{270f}", 276 | "\u{1f3e1}", 277 | "\u{1f6bf}", 278 | "\u{1f4a1}", 279 | "\u{1f4de}", 280 | "\u{1f4e2}", 281 | "\u{1f556}", 282 | "\u{23f0}", 283 | "\u{23f3}", 284 | "\u{1f4a3}", 285 | "\u{1f52b}", 286 | "\u{1f48a}", 287 | "\u{1f680}", 288 | "\u{1f30f}" 289 | ]; 290 | } 291 | --------------------------------------------------------------------------------