├── ios ├── Assets │ └── .gitkeep ├── Classes │ ├── FlutterNimPlugin.h │ ├── FlutterNimPlugin.m │ └── Helper │ │ ├── NIMSessionUtil.swift │ │ ├── NIMSessionParser.swift │ │ └── NIMSessionInteractor.swift ├── .gitignore └── flutter_nim.podspec ├── android ├── settings.gradle ├── gradle.properties ├── libs │ ├── vivopushsdk_v2.3.4.jar │ └── MiPush_SDK_Client_3_6_19.jar ├── .gitignore ├── src │ └── main │ │ ├── AndroidManifest.xml │ │ └── java │ │ └── cn │ │ └── cgm │ │ └── flutter_nim │ │ ├── Helper │ │ ├── FlutterNIMCustomAttachment.java │ │ ├── FlutterNIMCustomAttachParser.java │ │ ├── FlutterNIMPreferences.java │ │ ├── NIMKickoutInteractor.java │ │ ├── FlutterNIMSDKOptionConfig.java │ │ ├── FlutterNIMHelper.java │ │ ├── NIMRecentSessionsInteractor.java │ │ └── NIMSessionParser.java │ │ └── FlutterNimPlugin.java └── build.gradle ├── example ├── lib │ ├── zeus_kit │ │ ├── res │ │ │ ├── zk_res.dart │ │ │ └── zk_colors.dart │ │ ├── ui │ │ │ ├── zk_ui.dart │ │ │ ├── zk_grid_image.dart │ │ │ └── zk_network_image.dart │ │ ├── utils │ │ │ ├── zk_list_util.dart │ │ │ ├── zk_utils.dart │ │ │ ├── zk_text_util.dart │ │ │ ├── zk_date_util.dart │ │ │ ├── zk_route_util.dart │ │ │ ├── zk_common_util.dart │ │ │ └── zk_sp_util.dart │ │ └── zeus_kit.dart │ ├── provider │ │ ├── provider.dart │ │ ├── provider_nim.dart │ │ └── provider_chat.dart │ ├── main.dart │ ├── utils │ │ └── user_utils.dart │ └── ui │ │ ├── page_login.dart │ │ └── page_recent_sessions.dart ├── ios │ ├── Runner │ │ ├── Runner-Bridging-Header.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 │ │ ├── AppDelegate.swift │ │ ├── Base.lproj │ │ │ ├── Main.storyboard │ │ │ └── LaunchScreen.storyboard │ │ └── Info.plist │ ├── Flutter │ │ ├── Debug.xcconfig │ │ ├── Release.xcconfig │ │ ├── Flutter.podspec │ │ └── AppFrameworkInfo.plist │ ├── Runner.xcodeproj │ │ ├── project.xcworkspace │ │ │ └── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ └── xcschemes │ │ │ └── Runner.xcscheme │ ├── Runner.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ └── IDEWorkspaceChecks.plist │ ├── Podfile │ └── Podfile.lock ├── images │ ├── im_video_play.png │ ├── default_user_avatar.png │ ├── im_audio_play_left.gif │ ├── im_audio_play_right.gif │ ├── im_audio_stop_left.png │ ├── im_audio_stop_right.png │ ├── img_not_available.png │ ├── im_action_panel_house.png │ ├── im_message_cell_error.png │ ├── im_action_panel_camera.png │ └── im_action_panel_gallery.png ├── android │ ├── gradle.properties │ ├── app │ │ ├── src │ │ │ ├── main │ │ │ │ ├── res │ │ │ │ │ ├── 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 │ │ │ │ │ └── drawable │ │ │ │ │ │ └── launch_background.xml │ │ │ │ ├── java │ │ │ │ │ └── cn │ │ │ │ │ │ └── cgm │ │ │ │ │ │ └── flutter_nim_example │ │ │ │ │ │ ├── MainActivity.java │ │ │ │ │ │ └── MyApplication.java │ │ │ │ └── AndroidManifest.xml │ │ │ ├── debug │ │ │ │ └── AndroidManifest.xml │ │ │ └── profile │ │ │ │ └── AndroidManifest.xml │ │ └── build.gradle │ ├── gradle │ │ └── wrapper │ │ │ └── gradle-wrapper.properties │ ├── settings.gradle │ └── build.gradle ├── .metadata ├── README.md ├── .gitignore ├── pubspec.yaml └── pubspec.lock ├── screenshot └── Screen Shot 1.png ├── lib ├── models │ ├── nim_kick_reason.dart │ ├── nim_user_model.dart │ ├── nim_session_model.dart │ └── nim_message_model.dart └── flutter_nim.dart ├── CHANGELOG.md ├── test └── flutter_nim_test.dart ├── flutter_nim.iml ├── LICENSE ├── .gitignore ├── pubspec.yaml ├── README.md └── pubspec.lock /ios/Assets/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /android/settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'flutter_nim' 2 | -------------------------------------------------------------------------------- /example/lib/zeus_kit/res/zk_res.dart: -------------------------------------------------------------------------------- 1 | export 'zk_colors.dart'; -------------------------------------------------------------------------------- /example/ios/Runner/Runner-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | #import "GeneratedPluginRegistrant.h" -------------------------------------------------------------------------------- /example/lib/zeus_kit/ui/zk_ui.dart: -------------------------------------------------------------------------------- 1 | export 'zk_network_image.dart'; 2 | export 'zk_grid_image.dart'; 3 | -------------------------------------------------------------------------------- /screenshot/Screen Shot 1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GuiminChu/flutter_nim/HEAD/screenshot/Screen Shot 1.png -------------------------------------------------------------------------------- /example/images/im_video_play.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GuiminChu/flutter_nim/HEAD/example/images/im_video_play.png -------------------------------------------------------------------------------- /android/gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx1536M 2 | 3 | android.useAndroidX=true 4 | android.enableJetifier=true 5 | -------------------------------------------------------------------------------- /android/libs/vivopushsdk_v2.3.4.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GuiminChu/flutter_nim/HEAD/android/libs/vivopushsdk_v2.3.4.jar -------------------------------------------------------------------------------- /example/images/default_user_avatar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GuiminChu/flutter_nim/HEAD/example/images/default_user_avatar.png -------------------------------------------------------------------------------- /example/images/im_audio_play_left.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GuiminChu/flutter_nim/HEAD/example/images/im_audio_play_left.gif -------------------------------------------------------------------------------- /example/images/im_audio_play_right.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GuiminChu/flutter_nim/HEAD/example/images/im_audio_play_right.gif -------------------------------------------------------------------------------- /example/images/im_audio_stop_left.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GuiminChu/flutter_nim/HEAD/example/images/im_audio_stop_left.png -------------------------------------------------------------------------------- /example/images/im_audio_stop_right.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GuiminChu/flutter_nim/HEAD/example/images/im_audio_stop_right.png -------------------------------------------------------------------------------- /example/images/img_not_available.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GuiminChu/flutter_nim/HEAD/example/images/img_not_available.png -------------------------------------------------------------------------------- /example/images/im_action_panel_house.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GuiminChu/flutter_nim/HEAD/example/images/im_action_panel_house.png -------------------------------------------------------------------------------- /example/images/im_message_cell_error.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GuiminChu/flutter_nim/HEAD/example/images/im_message_cell_error.png -------------------------------------------------------------------------------- /ios/Classes/FlutterNimPlugin.h: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | @interface FlutterNimPlugin : NSObject 4 | @end 5 | -------------------------------------------------------------------------------- /android/libs/MiPush_SDK_Client_3_6_19.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GuiminChu/flutter_nim/HEAD/android/libs/MiPush_SDK_Client_3_6_19.jar -------------------------------------------------------------------------------- /example/images/im_action_panel_camera.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GuiminChu/flutter_nim/HEAD/example/images/im_action_panel_camera.png -------------------------------------------------------------------------------- /example/images/im_action_panel_gallery.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GuiminChu/flutter_nim/HEAD/example/images/im_action_panel_gallery.png -------------------------------------------------------------------------------- /android/.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/workspace.xml 5 | /.idea/libraries 6 | .DS_Store 7 | /build 8 | /captures 9 | -------------------------------------------------------------------------------- /example/ios/Flutter/Debug.xcconfig: -------------------------------------------------------------------------------- 1 | #include "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" 2 | #include "Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /example/ios/Flutter/Release.xcconfig: -------------------------------------------------------------------------------- 1 | #include "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" 2 | #include "Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /example/android/gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx1536M 2 | 3 | android.useAndroidX=true 4 | android.enableJetifier=true 5 | android.enableR8=true 6 | -------------------------------------------------------------------------------- /android/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GuiminChu/flutter_nim/HEAD/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GuiminChu/flutter_nim/HEAD/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GuiminChu/flutter_nim/HEAD/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GuiminChu/flutter_nim/HEAD/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GuiminChu/flutter_nim/HEAD/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/lib/zeus_kit/utils/zk_list_util.dart: -------------------------------------------------------------------------------- 1 | class ZKListUtil { 2 | /// isEmpty 3 | static bool isEmpty(List list) { 4 | return list == null || list.isEmpty; 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GuiminChu/flutter_nim/HEAD/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GuiminChu/flutter_nim/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GuiminChu/flutter_nim/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GuiminChu/flutter_nim/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GuiminChu/flutter_nim/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GuiminChu/flutter_nim/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GuiminChu/flutter_nim/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GuiminChu/flutter_nim/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GuiminChu/flutter_nim/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GuiminChu/flutter_nim/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GuiminChu/flutter_nim/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GuiminChu/flutter_nim/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GuiminChu/flutter_nim/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GuiminChu/flutter_nim/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GuiminChu/flutter_nim/HEAD/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GuiminChu/flutter_nim/HEAD/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GuiminChu/flutter_nim/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GuiminChu/flutter_nim/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png -------------------------------------------------------------------------------- /example/lib/provider/provider.dart: -------------------------------------------------------------------------------- 1 | export 'package:provider/provider.dart'; 2 | 3 | export 'package:flutter_nim_example/provider/provider_nim.dart'; 4 | export 'package:flutter_nim_example/provider/provider_chat.dart'; 5 | -------------------------------------------------------------------------------- /example/lib/zeus_kit/utils/zk_utils.dart: -------------------------------------------------------------------------------- 1 | export 'zk_text_util.dart'; 2 | export 'zk_list_util.dart'; 3 | export 'zk_sp_util.dart'; 4 | export 'zk_route_util.dart'; 5 | export 'zk_date_util.dart'; 6 | export 'zk_common_util.dart'; -------------------------------------------------------------------------------- /lib/models/nim_kick_reason.dart: -------------------------------------------------------------------------------- 1 | enum NIMKickReason { 2 | /// 被另外一个客户端踢下线 (互斥客户端一端登录挤掉上一个登录中的客户端) 3 | byClient, 4 | 5 | /// 被服务器踢下线 6 | byServer, 7 | 8 | /// 被另外一个客户端手动选择踢下线 9 | byClientManually, 10 | } 11 | -------------------------------------------------------------------------------- /example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /example/lib/zeus_kit/zeus_kit.dart: -------------------------------------------------------------------------------- 1 | /// 公共组件 2 | /// by cgm 3 | /// 4 | /// 需添加依赖包 5 | /// cached_network_image 6 | /// shared_preferences 7 | 8 | export 'ui/zk_ui.dart'; 9 | export 'res/zk_res.dart'; 10 | export 'utils/zk_utils.dart'; 11 | -------------------------------------------------------------------------------- /example/android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Wed Dec 25 09:06:13 CST 2019 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.4.1-all.zip 7 | -------------------------------------------------------------------------------- /ios/Classes/FlutterNimPlugin.m: -------------------------------------------------------------------------------- 1 | #import "FlutterNimPlugin.h" 2 | #import 3 | 4 | @implementation FlutterNimPlugin 5 | + (void)registerWithRegistrar:(NSObject*)registrar { 6 | [SwiftFlutterNIMPlugin registerWithRegistrar:registrar]; 7 | } 8 | @end 9 | -------------------------------------------------------------------------------- /example/ios/Runner.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /example/.metadata: -------------------------------------------------------------------------------- 1 | # This file tracks properties of this Flutter project. 2 | # Used by Flutter tool to assess capabilities and perform upgrades etc. 3 | # 4 | # This file should be version controlled and should not be manually edited. 5 | 6 | version: 7 | revision: 20e59316b8b8474554b38493b8ca888794b0234a 8 | channel: stable 9 | 10 | project_type: app 11 | -------------------------------------------------------------------------------- /example/android/app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /example/android/app/src/profile/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /example/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. -------------------------------------------------------------------------------- /lib/models/nim_user_model.dart: -------------------------------------------------------------------------------- 1 | class NIMUser { 2 | String nickname; 3 | String avatarUrl; 4 | String userExt; 5 | 6 | NIMUser({ 7 | this.nickname, 8 | this.avatarUrl, 9 | this.userExt, 10 | }); 11 | 12 | NIMUser.fromJson(Map json) { 13 | avatarUrl = json['avatarUrl']; 14 | userExt = json['userExt']; 15 | nickname = json['nickname']; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /example/android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 0.1.7 2 | 3 | * Add kick out callback. 4 | 5 | ## 0.1.6 6 | 7 | * Update nim sdk. 8 | * Require Flutter SDK 1.12.13+hotfix.4 or greater. 9 | 10 | ## 0.1.5+1 11 | 12 | * Add screenshot. 13 | 14 | ## 0.1.5 15 | 16 | * Fix bugs. 17 | * Add example. 18 | 19 | ## 0.1.4 20 | 21 | * Update nim sdk. 22 | 23 | ## 0.1.3 24 | 25 | * Fix bugs. 26 | 27 | ## 0.1.2 28 | 29 | * Fix bugs. 30 | 31 | ## 0.1.1 32 | 33 | * Initial release. 34 | 35 | 36 | -------------------------------------------------------------------------------- /example/ios/Runner/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import Flutter 3 | 4 | @UIApplicationMain 5 | @objc class AppDelegate: FlutterAppDelegate { 6 | override func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool { 7 | GeneratedPluginRegistrant.register(with: self) 8 | return super.application(application, didFinishLaunchingWithOptions: launchOptions) 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /example/android/app/src/main/java/cn/cgm/flutter_nim_example/MainActivity.java: -------------------------------------------------------------------------------- 1 | package cn.cgm.flutter_nim_example; 2 | 3 | import android.os.Bundle; 4 | import io.flutter.app.FlutterActivity; 5 | import io.flutter.plugins.GeneratedPluginRegistrant; 6 | 7 | public class MainActivity extends FlutterActivity { 8 | @Override 9 | protected void onCreate(Bundle savedInstanceState) { 10 | super.onCreate(savedInstanceState); 11 | GeneratedPluginRegistrant.registerWith(this); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /example/android/app/src/main/res/drawable/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /ios/.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | .vagrant/ 3 | .sconsign.dblite 4 | .svn/ 5 | 6 | .DS_Store 7 | *.swp 8 | profile 9 | 10 | DerivedData/ 11 | build/ 12 | GeneratedPluginRegistrant.h 13 | GeneratedPluginRegistrant.m 14 | 15 | .generated/ 16 | 17 | *.pbxuser 18 | *.mode1v3 19 | *.mode2v3 20 | *.perspectivev3 21 | 22 | !default.pbxuser 23 | !default.mode1v3 24 | !default.mode2v3 25 | !default.perspectivev3 26 | 27 | xcuserdata 28 | 29 | *.moved-aside 30 | 31 | *.pyc 32 | *sync/ 33 | Icon? 34 | .tags* 35 | 36 | /Flutter/Generated.xcconfig 37 | -------------------------------------------------------------------------------- /example/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 | -------------------------------------------------------------------------------- /example/lib/zeus_kit/utils/zk_text_util.dart: -------------------------------------------------------------------------------- 1 | class ZKTextUtil { 2 | /// isEmpty 3 | static bool isEmpty(String text) { 4 | return text == null || text.isEmpty; 5 | } 6 | 7 | /// isNotEmpty 8 | static bool isNotEmpty(String text) { 9 | return text != null && text.isNotEmpty; 10 | } 11 | 12 | /// isDouble 13 | static bool isDouble(String text) { 14 | if (isEmpty(text)) { 15 | return false; 16 | } 17 | 18 | if (double.tryParse(text) == null) { 19 | return false; 20 | } 21 | 22 | return true; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /example/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 | -------------------------------------------------------------------------------- /test/flutter_nim_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/services.dart'; 2 | import 'package:flutter_test/flutter_test.dart'; 3 | import 'package:flutter_nim/flutter_nim.dart'; 4 | 5 | void main() { 6 | const MethodChannel channel = MethodChannel('flutter_nim'); 7 | 8 | setUp(() { 9 | channel.setMockMethodCallHandler((MethodCall methodCall) async { 10 | return '42'; 11 | }); 12 | }); 13 | 14 | tearDown(() { 15 | channel.setMockMethodCallHandler(null); 16 | }); 17 | 18 | test('getPlatformVersion', () async { 19 | expect(await FlutterNIM().platformVersion, '42'); 20 | }); 21 | } 22 | -------------------------------------------------------------------------------- /example/android/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | repositories { 3 | google() 4 | jcenter() 5 | } 6 | 7 | dependencies { 8 | classpath 'com.android.tools.build:gradle:3.5.3' 9 | } 10 | } 11 | 12 | allprojects { 13 | repositories { 14 | google() 15 | jcenter() 16 | } 17 | } 18 | 19 | rootProject.buildDir = '../build' 20 | subprojects { 21 | project.buildDir = "${rootProject.buildDir}/${project.name}" 22 | } 23 | subprojects { 24 | project.evaluationDependsOn(':app') 25 | } 26 | 27 | task clean(type: Delete) { 28 | delete rootProject.buildDir 29 | } 30 | -------------------------------------------------------------------------------- /example/README.md: -------------------------------------------------------------------------------- 1 | # flutter_nim_example 2 | 3 | Demonstrates how to use the flutter_nim plugin. 4 | 5 | ## Getting Started 6 | 7 | This project is a starting point for a Flutter application. 8 | 9 | A few resources to get you started if this is your first Flutter project: 10 | 11 | - [Lab: Write your first Flutter app](https://flutter.dev/docs/get-started/codelab) 12 | - [Cookbook: Useful Flutter samples](https://flutter.dev/docs/cookbook) 13 | 14 | For help getting started with Flutter, view our 15 | [online documentation](https://flutter.dev/docs), which offers tutorials, 16 | samples, guidance on mobile development, and a full API reference. 17 | -------------------------------------------------------------------------------- /android/src/main/java/cn/cgm/flutter_nim/Helper/FlutterNIMCustomAttachment.java: -------------------------------------------------------------------------------- 1 | package cn.cgm.flutter_nim.Helper; 2 | 3 | import com.netease.nimlib.sdk.msg.attachment.MsgAttachment; 4 | 5 | /** 6 | * 自定义消息的附件 7 | *

8 | * 9 | * @author chuguimin 10 | */ 11 | class FlutterNIMCustomAttachment implements MsgAttachment { 12 | private String customEncodeString; 13 | 14 | void setCustomEncodeString(String customEncodeString) { 15 | this.customEncodeString = customEncodeString; 16 | } 17 | 18 | @Override 19 | public String toJson(boolean send) { 20 | return customEncodeString; 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /android/src/main/java/cn/cgm/flutter_nim/Helper/FlutterNIMCustomAttachParser.java: -------------------------------------------------------------------------------- 1 | package cn.cgm.flutter_nim.Helper; 2 | 3 | import com.netease.nimlib.sdk.msg.attachment.MsgAttachment; 4 | import com.netease.nimlib.sdk.msg.attachment.MsgAttachmentParser; 5 | 6 | /** 7 | * 自定义消息的附件解析器 8 | *

9 | * 10 | * @author chuguimin 11 | */ 12 | public class FlutterNIMCustomAttachParser implements MsgAttachmentParser { 13 | 14 | @Override 15 | public MsgAttachment parse(String json) { 16 | FlutterNIMCustomAttachment attachment = new FlutterNIMCustomAttachment(); 17 | attachment.setCustomEncodeString(json); 18 | 19 | return attachment; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /ios/flutter_nim.podspec: -------------------------------------------------------------------------------- 1 | # 2 | # To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html 3 | # 4 | Pod::Spec.new do |s| 5 | s.name = 'flutter_nim' 6 | s.version = '0.1.7' 7 | s.summary = 'A new Flutter plugin for netease im.' 8 | s.description = <<-DESC 9 | A new Flutter plugin for netease im. 10 | DESC 11 | s.homepage = 'http://example.com' 12 | s.license = { :file => '../LICENSE' } 13 | s.author = { 'Your Company' => 'email@example.com' } 14 | s.source = { :path => '.' } 15 | s.source_files = 'Classes/**/*' 16 | s.public_header_files = 'Classes/**/*.h' 17 | s.dependency 'Flutter' 18 | s.dependency 'NIMSDK', '7.0.3' 19 | 20 | s.ios.deployment_target = '9.0' 21 | end 22 | 23 | -------------------------------------------------------------------------------- /example/lib/zeus_kit/res/zk_colors.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class ZKColors { 4 | static const Color text_dark = Color(0xFF333333); 5 | static const Color text_normal = Color(0xFF666666); 6 | static const Color text_gray = Color(0xFF999999); 7 | static const Color text_red = Color(0xFFFF5B05); 8 | static const Color text_light = Color(0xFFE5E5E5); 9 | 10 | // 已读颜色 11 | static const Color text_read = Color(0xFF999999); 12 | 13 | // 占位符颜色 14 | static const Color place_holder = Color(0xFFC7C7CD); 15 | 16 | // 分割线颜色 17 | static const Color divider = Color(0xFFE5E5E5); 18 | 19 | // 输入框背景色 20 | static const Color textFieldBackgroundColor = Color(0xFFF0F0F0); 21 | 22 | // 背景色(官方默认此值) 23 | static const Color scaffoldBackgroundColor = const Color(0xFFFAFAFA); 24 | } 25 | -------------------------------------------------------------------------------- /example/ios/Flutter/Flutter.podspec: -------------------------------------------------------------------------------- 1 | # 2 | # NOTE: This podspec is NOT to be published. It is only used as a local source! 3 | # 4 | 5 | Pod::Spec.new do |s| 6 | s.name = 'Flutter' 7 | s.version = '1.0.0' 8 | s.summary = 'High-performance, high-fidelity mobile apps.' 9 | s.description = <<-DESC 10 | Flutter provides an easy and productive way to build and deploy high-performance mobile apps for Android and iOS. 11 | DESC 12 | s.homepage = 'https://flutter.io' 13 | s.license = { :type => 'MIT' } 14 | s.author = { 'Flutter Dev Team' => 'flutter-dev@googlegroups.com' } 15 | s.source = { :git => 'https://github.com/flutter/engine', :tag => s.version.to_s } 16 | s.ios.deployment_target = '8.0' 17 | s.vendored_frameworks = 'Flutter.framework' 18 | end 19 | -------------------------------------------------------------------------------- /example/lib/main.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | import 'package:flutter_nim/flutter_nim.dart'; 4 | import 'package:flutter_nim_example/utils/user_utils.dart'; 5 | import 'package:flutter_nim_example/ui/page_login.dart'; 6 | import 'package:flutter_nim_example/ui/page_recent_sessions.dart'; 7 | 8 | void main() async { 9 | WidgetsFlutterBinding.ensureInitialized(); 10 | 11 | final imAccount = await UserUtils.imAccount; 12 | final imToken = await UserUtils.imToken; 13 | 14 | FlutterNIM().init( 15 | appKey: "45c6af3c98409b18a84451215d0bdd6e", 16 | apnsCername: "ENTERPRISE", 17 | apnsCernameDevelop: "DEVELOPER", 18 | imAccount: imAccount, 19 | imToken: imToken, 20 | ); 21 | 22 | bool isLogin = await UserUtils.isLogin(); 23 | 24 | if (isLogin) { 25 | runApp(MyApp()); 26 | } else { 27 | runApp(LoginHomePage()); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /example/ios/Flutter/AppFrameworkInfo.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 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 | -------------------------------------------------------------------------------- /flutter_nim.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /lib/models/nim_session_model.dart: -------------------------------------------------------------------------------- 1 | import 'nim_message_model.dart'; 2 | import 'nim_user_model.dart'; 3 | 4 | class NIMRecentSession { 5 | /// 会话Id,如果当前session为team,则sessionId为teamId,如果是P2P则为对方帐号 6 | String sessionId; 7 | 8 | /// 消息文本 9 | String messageContent; 10 | 11 | /// 未读消息数 12 | int unreadCount; 13 | 14 | /// 最后一条消息时间戳,单位 ms 15 | int timestamp; 16 | 17 | NIMUser userInfo; 18 | NIMMessage lastMessage; 19 | 20 | NIMRecentSession({ 21 | this.sessionId, 22 | this.messageContent, 23 | this.unreadCount, 24 | this.timestamp, 25 | this.userInfo, 26 | this.lastMessage, 27 | }); 28 | 29 | NIMRecentSession.fromJson(Map json) { 30 | sessionId = json['sessionId']; 31 | messageContent = json['messageContent']; 32 | unreadCount = json['unreadCount']; 33 | timestamp = json['timestamp']; 34 | 35 | userInfo = 36 | json['userInfo'] != null ? NIMUser.fromJson(json['userInfo']) : null; 37 | 38 | lastMessage = json['lastMessage'] != null 39 | ? NIMMessage.fromJson(json['lastMessage']) 40 | : null; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /example/lib/zeus_kit/utils/zk_date_util.dart: -------------------------------------------------------------------------------- 1 | class ZKDateUtil { 2 | /// 毫秒时间戳 -> yyyy-MM-dd HH:mm 3 | static String timestampToYMDHM(int timestamp) { 4 | if (timestamp == null) { 5 | return ""; 6 | } 7 | var date = DateTime.fromMillisecondsSinceEpoch(timestamp); 8 | var dateString = date.toString(); 9 | dateString = dateString.substring(0, "yyyy-MM-dd HH:mm".length); 10 | return dateString; 11 | } 12 | 13 | /// 毫秒时间戳 -> yyyy-MM 14 | static String timestampToYM(int timestamp) { 15 | if (timestamp == null) { 16 | return ""; 17 | } 18 | var date = DateTime.fromMillisecondsSinceEpoch(timestamp); 19 | var dateString = date.toString(); 20 | dateString = dateString.substring(0, "yyyy-MM".length); 21 | return dateString; 22 | } 23 | 24 | /// 毫秒时间戳 -> HH:mm 25 | static String timestampToHM(int timestamp) { 26 | if (timestamp == null) { 27 | return ""; 28 | } 29 | var date = DateTime.fromMillisecondsSinceEpoch(timestamp); 30 | var dateString = date.toString(); 31 | dateString = dateString.substring(11, 16); 32 | return dateString; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 GuiminChu 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 | -------------------------------------------------------------------------------- /example/lib/zeus_kit/utils/zk_route_util.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter/cupertino.dart'; 3 | 4 | class ZKRouter { 5 | static Future pushWidget( 6 | BuildContext context, 7 | Widget widget, { 8 | bool replaceRoot = false, 9 | bool replaceCurrent = false, 10 | }) { 11 | return pushRoute( 12 | context, 13 | CupertinoPageRoute(builder: (ctx) => widget), 14 | replaceRoot: replaceRoot, 15 | replaceCurrent: replaceCurrent, 16 | ); 17 | } 18 | 19 | static Future pushRoute( 20 | BuildContext context, 21 | PageRoute route, { 22 | bool replaceRoot = false, 23 | bool replaceCurrent = false, 24 | }) { 25 | assert(!(replaceRoot == true && replaceCurrent == true)); 26 | if (replaceRoot == true) { 27 | return Navigator.pushAndRemoveUntil( 28 | context, 29 | route, 30 | _rootRoute, 31 | ); 32 | } 33 | if (replaceCurrent == true) { 34 | return Navigator.pushReplacement(context, route); 35 | } 36 | return Navigator.push(context, route); 37 | } 38 | } 39 | 40 | var _rootRoute = ModalRoute.withName("home"); 41 | -------------------------------------------------------------------------------- /android/build.gradle: -------------------------------------------------------------------------------- 1 | group 'cn.cgm.flutter_nim' 2 | version '1.0-SNAPSHOT' 3 | 4 | buildscript { 5 | repositories { 6 | google() 7 | jcenter() 8 | } 9 | 10 | dependencies { 11 | classpath 'com.android.tools.build:gradle:3.4.2' 12 | } 13 | } 14 | 15 | rootProject.allprojects { 16 | repositories { 17 | google() 18 | jcenter() 19 | maven { url 'http://developer.huawei.com/repo' } 20 | } 21 | } 22 | 23 | apply plugin: 'com.android.library' 24 | 25 | android { 26 | compileSdkVersion 28 27 | 28 | defaultConfig { 29 | minSdkVersion 21 30 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 31 | } 32 | lintOptions { 33 | disable 'InvalidPackage' 34 | } 35 | } 36 | 37 | dependencies { 38 | // 添加网易云信依赖。注意,版本号必须一致。 39 | // 基础功能 (必需) 40 | api 'com.netease.nimlib:basesdk:7.2.0' 41 | // 聊天室需要 42 | api 'com.netease.nimlib:chatroom:7.2.0' 43 | // 全文检索服务需要 44 | api 'com.netease.nimlib:lucene:7.2.0' 45 | // 小米、华为、魅族、fcm 推送 46 | api 'com.netease.nimlib:push:7.2.0' 47 | // 华为 48 | api 'com.huawei.android.hms:push:2.6.1.301' 49 | api files('libs/MiPush_SDK_Client_3_6_19.jar') 50 | api files('libs/vivopushsdk_v2.3.4.jar') 51 | } -------------------------------------------------------------------------------- /example/lib/utils/user_utils.dart: -------------------------------------------------------------------------------- 1 | import 'package:shared_preferences/shared_preferences.dart'; 2 | 3 | class UserUtils { 4 | static const _imAccountKey = "imAccount"; 5 | static const _imTokenKey = "imToken"; 6 | 7 | static void saveIMLoginInfo(String imAccount, String imToken) async { 8 | SharedPreferences preferences = await SharedPreferences.getInstance(); 9 | preferences.setString(_imAccountKey, imAccount); 10 | preferences.setString(_imTokenKey, imToken); 11 | } 12 | 13 | static Future get imAccount async { 14 | SharedPreferences preferences = await SharedPreferences.getInstance(); 15 | return preferences.getString(_imAccountKey) ?? ""; 16 | } 17 | 18 | static Future get imToken async { 19 | SharedPreferences preferences = await SharedPreferences.getInstance(); 20 | return preferences.getString(_imTokenKey) ?? ""; 21 | } 22 | 23 | /// 是否登录 24 | static Future isLogin() async { 25 | var accessToken = await imToken; 26 | if (accessToken == null || accessToken.isEmpty) { 27 | return false; 28 | } 29 | return true; 30 | } 31 | 32 | /// 清空, 登出时使用 33 | static void clearLoginInfo() async { 34 | SharedPreferences preferences = await SharedPreferences.getInstance(); 35 | 36 | preferences.remove(_imAccountKey); 37 | preferences.remove(_imTokenKey); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /example/lib/provider/provider_nim.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:flutter/services.dart'; 5 | import 'package:flutter_nim/flutter_nim.dart'; 6 | 7 | class NIMProvider with ChangeNotifier { 8 | // 会话消息未读数 9 | int _badgeNumber = 0; 10 | 11 | // 最近会话列表 12 | List _recentSessions = []; 13 | 14 | // 单个会话消息相关 15 | List _messages = []; 16 | bool _hasMoreData = false; 17 | 18 | int get badgeNumber => _badgeNumber; 19 | 20 | List get recentSessions => _recentSessions; 21 | 22 | List get messages => _messages; 23 | 24 | bool get hasMoreData => _hasMoreData; 25 | 26 | NIMProvider() { 27 | FlutterNIM().loadRecentSessions(); 28 | 29 | FlutterNIM().recentSessionsResponse.listen((recentSessions) { 30 | this._recentSessions = recentSessions; 31 | 32 | this._badgeNumber = recentSessions 33 | .map((s) => s.unreadCount) 34 | .fold(0, (curr, next) => curr + next); 35 | 36 | notifyListeners(); 37 | }); 38 | 39 | FlutterNIM().messagesResponse.listen((messages) { 40 | this._messages = messages.reversed.toList(); 41 | 42 | // 求余,如果结果等于零则可能还有更多数据 43 | int remainder = this._messages.length % 20; 44 | 45 | _hasMoreData = remainder == 0; 46 | 47 | notifyListeners(); 48 | }); 49 | 50 | FlutterNIM().kickReasonResponse.listen((NIMKickReason reason) { 51 | // 处理被踢下线逻辑 52 | print(reason); 53 | }); 54 | } 55 | 56 | @override 57 | void dispose() { 58 | super.dispose(); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.log 4 | *.pyc 5 | *.swp 6 | .DS_Store 7 | .atom/ 8 | .buildlog/ 9 | .history 10 | .svn/ 11 | 12 | # IntelliJ related 13 | *.iml 14 | *.ipr 15 | *.iws 16 | .idea/ 17 | 18 | # Visual Studio Code related 19 | .vscode/ 20 | 21 | # Flutter/Dart/Pub related 22 | **/doc/api/ 23 | .dart_tool/ 24 | .flutter-plugins 25 | .packages 26 | .pub-cache/ 27 | .pub/ 28 | /build/ 29 | 30 | # Android related 31 | **/android/**/gradle-wrapper.jar 32 | **/android/.gradle 33 | **/android/captures/ 34 | **/android/gradlew 35 | **/android/gradlew.bat 36 | **/android/local.properties 37 | **/android/**/GeneratedPluginRegistrant.java 38 | 39 | # iOS/XCode related 40 | **/ios/**/*.mode1v3 41 | **/ios/**/*.mode2v3 42 | **/ios/**/*.moved-aside 43 | **/ios/**/*.pbxuser 44 | **/ios/**/*.perspectivev3 45 | **/ios/**/*sync/ 46 | **/ios/**/.sconsign.dblite 47 | **/ios/**/.tags* 48 | **/ios/**/.vagrant/ 49 | **/ios/**/DerivedData/ 50 | **/ios/**/Icon? 51 | **/ios/**/Pods/ 52 | **/ios/**/.symlinks/ 53 | **/ios/**/profile 54 | **/ios/**/xcuserdata 55 | **/ios/.generated/ 56 | **/ios/Flutter/App.framework 57 | **/ios/Flutter/Flutter.framework 58 | **/ios/Flutter/Generated.xcconfig 59 | **/ios/Flutter/app.flx 60 | **/ios/Flutter/app.zip 61 | **/ios/Flutter/flutter_assets/ 62 | **/ios/ServiceDefinitions.json 63 | **/ios/Runner/GeneratedPluginRegistrant.* 64 | 65 | # Exceptions to above rules. 66 | !**/ios/**/default.mode1v3 67 | !**/ios/**/default.mode2v3 68 | !**/ios/**/default.pbxuser 69 | !**/ios/**/default.perspectivev3 70 | !/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages 71 | 72 | #pub工具生成的文件 73 | pubspec.lock 74 | -------------------------------------------------------------------------------- /example/lib/zeus_kit/utils/zk_common_util.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | import 'package:convert/convert.dart'; 3 | import 'package:crypto/crypto.dart'; 4 | import 'package:flutter/material.dart'; 5 | import 'package:fluttertoast/fluttertoast.dart'; 6 | 7 | class ZKCommonUtils { 8 | /// 获取屏幕宽度 9 | static double getScreenWidth(BuildContext context) { 10 | return MediaQuery.of(context).size.width; 11 | } 12 | 13 | /// 获取屏幕高度 14 | static double getScreenHeight(BuildContext context) { 15 | return MediaQuery.of(context).size.height; 16 | } 17 | 18 | /// 获取系统状态栏高度 19 | static double getStatusBarHeight(BuildContext context) { 20 | return MediaQuery.of(context).padding.top; 21 | } 22 | 23 | /// 获取系统底边栏高度(iPhoneX底部横条) 24 | static double getBottomBarHeight(BuildContext context) { 25 | return MediaQuery.of(context).padding.bottom; 26 | } 27 | 28 | static void showToast(String message) { 29 | Fluttertoast.showToast( 30 | msg: message, 31 | toastLength: Toast.LENGTH_SHORT, 32 | gravity: ToastGravity.CENTER, 33 | timeInSecForIos: 1, 34 | ); 35 | } 36 | 37 | static void showLongToast(String message) { 38 | Fluttertoast.showToast( 39 | msg: message, 40 | toastLength: Toast.LENGTH_LONG, 41 | gravity: ToastGravity.CENTER, 42 | ); 43 | } 44 | 45 | static void hideToast() { 46 | Fluttertoast.cancel(); 47 | } 48 | 49 | /// md5 加密 50 | static String generateMd5(String data) { 51 | var content = new Utf8Encoder().convert(data); 52 | var digest = md5.convert(content); 53 | // 这里其实就是 digest.toString() 54 | return hex.encode(digest.bytes); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /example/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 | -------------------------------------------------------------------------------- /example/android/app/src/main/java/cn/cgm/flutter_nim_example/MyApplication.java: -------------------------------------------------------------------------------- 1 | package cn.cgm.flutter_nim_example; 2 | 3 | import com.netease.nimlib.sdk.NIMClient; 4 | import com.netease.nimlib.sdk.SDKOptions; 5 | import com.netease.nimlib.sdk.mixpush.MixPushConfig; 6 | 7 | import cn.cgm.flutter_nim.Helper.FlutterNIMPreferences; 8 | import cn.cgm.flutter_nim.Helper.FlutterNIMSDKOptionConfig; 9 | import io.flutter.app.FlutterApplication; 10 | 11 | public class MyApplication extends FlutterApplication { 12 | @Override 13 | public void onCreate() { 14 | super.onCreate(); 15 | 16 | FlutterNIMPreferences.setContext(this); 17 | // SDK初始化(启动后台服务,若已经存在用户登录信息, SDK 将完成自动登录) 18 | NIMClient.init(this, FlutterNIMPreferences.getLoginInfo(), buildSDKOptions()); 19 | } 20 | 21 | // 网易云信配置 22 | private SDKOptions buildSDKOptions() { 23 | return FlutterNIMSDKOptionConfig.getSDKOptions(this, "45c6af3c98409b18a84451215d0bdd6e", buildMixPushConfig()); 24 | } 25 | 26 | // 网易云信第三方推送配置 27 | private MixPushConfig buildMixPushConfig() { 28 | 29 | MixPushConfig config = new MixPushConfig(); 30 | 31 | // 小米推送 32 | // config.xmAppId = "123"; 33 | // config.xmAppKey = "123"; 34 | // config.xmCertificateName = "abc"; 35 | 36 | // 华为推送 37 | // config.hwCertificateName = "abc"; 38 | 39 | // Vivo推送 40 | // config.vivoCertificateName = "abc"; 41 | 42 | // 魅族推送 43 | // config.mzAppId = "123"; 44 | // config.mzAppKey = "123"; 45 | // config.mzCertificateName = "abc"; 46 | 47 | // fcm 推送,适用于海外用户,不使用fcm请不要配置 48 | // config.fcmCertificateName = "DEMO_FCM_PUSH"; 49 | 50 | return config; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /example/.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.log 4 | *.pyc 5 | *.swp 6 | .DS_Store 7 | .atom/ 8 | .buildlog/ 9 | .history 10 | .svn/ 11 | 12 | # IntelliJ related 13 | *.iml 14 | *.ipr 15 | *.iws 16 | .idea/ 17 | 18 | # The .vscode folder contains launch configuration and tasks you configure in 19 | # VS Code which you may wish to be included in version control, so this line 20 | # is commented out by default. 21 | #.vscode/ 22 | 23 | # Flutter/Dart/Pub related 24 | **/doc/api/ 25 | .dart_tool/ 26 | .flutter-plugins 27 | .packages 28 | .pub-cache/ 29 | .pub/ 30 | /build/ 31 | 32 | # Android related 33 | **/android/**/gradle-wrapper.jar 34 | **/android/.gradle 35 | **/android/captures/ 36 | **/android/gradlew 37 | **/android/gradlew.bat 38 | **/android/local.properties 39 | **/android/**/GeneratedPluginRegistrant.java 40 | 41 | # iOS/XCode related 42 | **/ios/**/*.mode1v3 43 | **/ios/**/*.mode2v3 44 | **/ios/**/*.moved-aside 45 | **/ios/**/*.pbxuser 46 | **/ios/**/*.perspectivev3 47 | **/ios/**/*sync/ 48 | **/ios/**/.sconsign.dblite 49 | **/ios/**/.tags* 50 | **/ios/**/.vagrant/ 51 | **/ios/**/DerivedData/ 52 | **/ios/**/Icon? 53 | **/ios/**/Pods/ 54 | **/ios/**/.symlinks/ 55 | **/ios/**/profile 56 | **/ios/**/xcuserdata 57 | **/ios/.generated/ 58 | **/ios/Flutter/App.framework 59 | **/ios/Flutter/Flutter.framework 60 | **/ios/Flutter/Generated.xcconfig 61 | **/ios/Flutter/app.flx 62 | **/ios/Flutter/app.zip 63 | **/ios/Flutter/flutter_assets/ 64 | **/ios/ServiceDefinitions.json 65 | **/ios/Runner/GeneratedPluginRegistrant.* 66 | 67 | # Exceptions to above rules. 68 | !**/ios/**/default.mode1v3 69 | !**/ios/**/default.mode2v3 70 | !**/ios/**/default.pbxuser 71 | !**/ios/**/default.perspectivev3 72 | !/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages 73 | -------------------------------------------------------------------------------- /example/ios/Runner/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | NSMicrophoneUsageDescription 6 | 麦克风 7 | NSPhotoLibraryUsageDescription 8 | 相册 9 | NSCameraUsageDescription 10 | 相机 11 | NSAppTransportSecurity 12 | 13 | NSAllowsArbitraryLoads 14 | 15 | 16 | CFBundleDevelopmentRegion 17 | $(DEVELOPMENT_LANGUAGE) 18 | CFBundleExecutable 19 | $(EXECUTABLE_NAME) 20 | CFBundleIdentifier 21 | $(PRODUCT_BUNDLE_IDENTIFIER) 22 | CFBundleInfoDictionaryVersion 23 | 6.0 24 | CFBundleName 25 | flutter_nim_example 26 | CFBundlePackageType 27 | APPL 28 | CFBundleShortVersionString 29 | $(MARKETING_VERSION) 30 | CFBundleSignature 31 | ???? 32 | CFBundleVersion 33 | $(CURRENT_PROJECT_VERSION) 34 | LSRequiresIPhoneOS 35 | 36 | UILaunchStoryboardName 37 | LaunchScreen 38 | UIMainStoryboardFile 39 | Main 40 | UISupportedInterfaceOrientations 41 | 42 | UIInterfaceOrientationPortrait 43 | 44 | UISupportedInterfaceOrientations~ipad 45 | 46 | UIInterfaceOrientationPortrait 47 | UIInterfaceOrientationPortraitUpsideDown 48 | UIInterfaceOrientationLandscapeLeft 49 | UIInterfaceOrientationLandscapeRight 50 | 51 | UIViewControllerBasedStatusBarAppearance 52 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /example/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 from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" 26 | 27 | android { 28 | compileSdkVersion 28 29 | 30 | lintOptions { 31 | disable 'InvalidPackage' 32 | disable 'GoogleAppIndexingWarning' 33 | } 34 | 35 | defaultConfig { 36 | // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). 37 | applicationId "cn.cgm.flutter_nim_example" 38 | minSdkVersion 21 39 | targetSdkVersion 28 40 | versionCode flutterVersionCode.toInteger() 41 | versionName flutterVersionName 42 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 43 | } 44 | 45 | buildTypes { 46 | release { 47 | // TODO: Add your own signing config for the release build. 48 | // Signing with the debug keys for now, so `flutter run --release` works. 49 | signingConfig signingConfigs.debug 50 | } 51 | } 52 | } 53 | 54 | flutter { 55 | source '../..' 56 | } 57 | 58 | dependencies { 59 | testImplementation 'junit:junit:4.12' 60 | androidTestImplementation 'androidx.test:runner:1.2.0' 61 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' 62 | } 63 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: flutter_nim 2 | description: A new Flutter plugin for netease im, can be used to receive and send messages. 3 | version: 0.1.7 4 | author: GuiminChu 5 | homepage: https://github.com/GuiminChu/flutter_nim 6 | 7 | environment: 8 | sdk: ">=2.1.0 <3.0.0" 9 | 10 | dependencies: 11 | flutter: 12 | sdk: flutter 13 | 14 | dev_dependencies: 15 | flutter_test: 16 | sdk: flutter 17 | 18 | # For information on the generic Dart part of this file, see the 19 | # following page: https://dart.dev/tools/pub/pubspec 20 | 21 | # The following section is specific to Flutter. 22 | flutter: 23 | # This section identifies this Flutter project as a plugin project. 24 | # The androidPackage and pluginClass identifiers should not ordinarily 25 | # be modified. They are used by the tooling to maintain consistency when 26 | # adding or updating assets for this project. 27 | plugin: 28 | androidPackage: cn.cgm.flutter_nim 29 | pluginClass: FlutterNimPlugin 30 | 31 | # To add assets to your plugin package, add an assets section, like this: 32 | # assets: 33 | # - images/a_dot_burr.jpeg 34 | # - images/a_dot_ham.jpeg 35 | # 36 | # For details regarding assets in packages, see 37 | # https://flutter.dev/assets-and-images/#from-packages 38 | # 39 | # An image asset can refer to one or more resolution-specific "variants", see 40 | # https://flutter.dev/assets-and-images/#resolution-aware. 41 | 42 | # To add custom fonts to your plugin package, add a fonts section here, 43 | # in this "flutter" section. Each entry in this list should have a 44 | # "family" key with the font family name, and a "fonts" key with a 45 | # list giving the asset and other descriptors for the font. For 46 | # example: 47 | # fonts: 48 | # - family: Schyler 49 | # fonts: 50 | # - asset: fonts/Schyler-Regular.ttf 51 | # - asset: fonts/Schyler-Italic.ttf 52 | # style: italic 53 | # - family: Trajan Pro 54 | # fonts: 55 | # - asset: fonts/TrajanPro.ttf 56 | # - asset: fonts/TrajanPro_Bold.ttf 57 | # weight: 700 58 | # 59 | # For details regarding fonts in packages, see 60 | # https://flutter.dev/custom-fonts/#from-packages 61 | -------------------------------------------------------------------------------- /android/src/main/java/cn/cgm/flutter_nim/Helper/FlutterNIMPreferences.java: -------------------------------------------------------------------------------- 1 | package cn.cgm.flutter_nim.Helper; 2 | 3 | import android.content.Context; 4 | import android.content.SharedPreferences; 5 | import android.text.TextUtils; 6 | 7 | import com.netease.nimlib.sdk.auth.LoginInfo; 8 | 9 | public class FlutterNIMPreferences { 10 | private static final String KEY_USER_ACCOUNT = "flutter_nim_account"; 11 | private static final String KEY_USER_TOKEN = "flutter_nim_token"; 12 | 13 | private static Context context; 14 | 15 | private static Context getContext() { 16 | return context; 17 | } 18 | 19 | public static void setContext(Context context) { 20 | FlutterNIMPreferences.context = context.getApplicationContext(); 21 | } 22 | 23 | /** 24 | * 获取云信用户登录信息,用于自动登录 25 | */ 26 | public static LoginInfo getLoginInfo() { 27 | String account = getUserAccount(); 28 | String token = getUserToken(); 29 | 30 | if (!TextUtils.isEmpty(account) && !TextUtils.isEmpty(token)) { 31 | return new LoginInfo(account, token); 32 | } else { 33 | return null; 34 | } 35 | } 36 | 37 | static void saveUserAccount(String account) { 38 | saveString(KEY_USER_ACCOUNT, account); 39 | } 40 | 41 | private static String getUserAccount() { 42 | return getString(KEY_USER_ACCOUNT); 43 | } 44 | 45 | static void saveUserToken(String token) { 46 | saveString(KEY_USER_TOKEN, token); 47 | } 48 | 49 | private static String getUserToken() { 50 | return getString(KEY_USER_TOKEN); 51 | } 52 | 53 | public static void saveString(String key, String value) { 54 | SharedPreferences.Editor editor = getSharedPreferences().edit(); 55 | editor.putString(key, value); 56 | editor.apply(); 57 | } 58 | 59 | public static String getString(String key) { 60 | return getSharedPreferences().getString(key, null); 61 | } 62 | 63 | public static void clear() { 64 | SharedPreferences.Editor editor = getSharedPreferences().edit(); 65 | editor.clear(); 66 | editor.apply(); 67 | } 68 | 69 | private static SharedPreferences getSharedPreferences() { 70 | return getContext().getSharedPreferences("FlutterNIM", Context.MODE_PRIVATE); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /example/lib/zeus_kit/ui/zk_grid_image.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'zk_network_image.dart'; 3 | 4 | typedef ZKImagesGridViewTaper = Function(int index); 5 | 6 | class ZKImagesGridView extends StatelessWidget { 7 | final String title; 8 | final TextStyle titleStyle; 9 | final List imageUrls; 10 | final int crossAxisCount; 11 | final EdgeInsets margin; 12 | 13 | final ZKImagesGridViewTaper taper; 14 | 15 | ZKImagesGridView({ 16 | Key key, 17 | this.title, 18 | this.titleStyle = const TextStyle( 19 | color: const Color(0xFF666666), 20 | fontSize: 14.0, 21 | ), 22 | this.imageUrls, 23 | this.crossAxisCount = 3, 24 | this.margin: EdgeInsets.zero, 25 | this.taper, 26 | }) : super(key: key); 27 | 28 | @override 29 | Widget build(BuildContext context) { 30 | return Container( 31 | color: Colors.white, 32 | margin: margin, 33 | child: Column( 34 | children: [ 35 | Offstage( 36 | offstage: title == null, 37 | child: Container( 38 | height: 40.0, 39 | alignment: Alignment.centerLeft, 40 | child: Text( 41 | title ?? "", 42 | style: titleStyle, 43 | ), 44 | ), 45 | ), 46 | Offstage( 47 | offstage: (imageUrls?.length ?? 0) == 0, 48 | child: GridView.builder( 49 | primary: false, 50 | shrinkWrap: true, 51 | itemCount: imageUrls?.length ?? 0, 52 | gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( 53 | crossAxisCount: crossAxisCount, 54 | mainAxisSpacing: 10.0, 55 | crossAxisSpacing: 10.0, 56 | ), 57 | itemBuilder: (context, index) { 58 | return _buildImageCell(index, imageUrls[index]); 59 | }, 60 | ), 61 | ), 62 | ], 63 | ), 64 | ); 65 | } 66 | 67 | Widget _buildImageCell(int index, String imageUrl) { 68 | return GestureDetector( 69 | onTap: () { 70 | if (taper != null) { 71 | taper(index); 72 | } 73 | }, 74 | child: ZKNetworkImage( 75 | imageUrl: imageUrl, 76 | width: double.infinity, 77 | height: double.infinity, 78 | borderRadius: BorderRadius.circular(5.0), 79 | ), 80 | ); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /example/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 | -------------------------------------------------------------------------------- /example/lib/ui/page_login.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_nim/flutter_nim.dart'; 3 | import 'package:flutter_nim_example/utils/user_utils.dart'; 4 | import 'package:flutter_nim_example/zeus_kit/zeus_kit.dart'; 5 | import 'package:flutter_nim_example/ui/page_recent_sessions.dart'; 6 | 7 | class LoginHomePage extends StatelessWidget { 8 | @override 9 | Widget build(BuildContext context) { 10 | return MaterialApp( 11 | home: LoginPage(), 12 | ); 13 | } 14 | } 15 | 16 | /// 登录页面 17 | class LoginPage extends StatefulWidget { 18 | @override 19 | _LoginPageState createState() => _LoginPageState(); 20 | } 21 | 22 | class _LoginPageState extends State { 23 | TextEditingController _accountEditingController = TextEditingController(); 24 | TextEditingController _passwordEditingController = TextEditingController(); 25 | 26 | @override 27 | Widget build(BuildContext context) { 28 | return Scaffold( 29 | appBar: AppBar( 30 | title: Text("登录"), 31 | ), 32 | body: Padding( 33 | padding: const EdgeInsets.all(16.0), 34 | child: Column( 35 | children: [ 36 | TextField( 37 | controller: _accountEditingController, 38 | decoration: InputDecoration(hintText: "请输入账号"), 39 | ), 40 | TextField( 41 | controller: _passwordEditingController, 42 | decoration: InputDecoration(hintText: "请输入密码"), 43 | ), 44 | SizedBox( 45 | height: 20, 46 | ), 47 | Text( 48 | "请使用您在云信Demo中注册的账号密码", 49 | style: TextStyle( 50 | color: Colors.grey, 51 | fontSize: 12.0, 52 | ), 53 | ), 54 | SizedBox( 55 | height: 20, 56 | ), 57 | RaisedButton( 58 | child: Text("登录"), 59 | onPressed: () { 60 | _login(); 61 | }, 62 | ), 63 | ], 64 | ), 65 | ), 66 | ); 67 | } 68 | 69 | void _login() async { 70 | final imAccount = _accountEditingController.text; 71 | final imToken = ZKCommonUtils.generateMd5(_passwordEditingController.text); 72 | 73 | bool isLoginSuccess = await FlutterNIM().login(imAccount, imToken); 74 | 75 | if (isLoginSuccess) { 76 | UserUtils.saveIMLoginInfo(imAccount, imToken); 77 | ZKRouter.pushWidget(context, MyApp(), replaceCurrent: true); 78 | } else { 79 | ZKCommonUtils.showToast("登录失败,请检查您的账号密码"); 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /example/ios/Podfile: -------------------------------------------------------------------------------- 1 | source 'https://github.com/CocoaPods/Specs.git' 2 | platform :ios, '9.0' 3 | inhibit_all_warnings! 4 | 5 | # CocoaPods analytics sends network stats synchronously affecting flutter build latency. 6 | ENV['COCOAPODS_DISABLE_STATS'] = 'true' 7 | 8 | project 'Runner', { 9 | 'Debug' => :debug, 10 | 'Profile' => :release, 11 | 'Release' => :release, 12 | } 13 | 14 | def parse_KV_file(file, separator='=') 15 | file_abs_path = File.expand_path(file) 16 | if !File.exists? file_abs_path 17 | return []; 18 | end 19 | pods_ary = [] 20 | skip_line_start_symbols = ["#", "/"] 21 | File.foreach(file_abs_path) { |line| 22 | next if skip_line_start_symbols.any? { |symbol| line =~ /^\s*#{symbol}/ } 23 | plugin = line.split(pattern=separator) 24 | if plugin.length == 2 25 | podname = plugin[0].strip() 26 | path = plugin[1].strip() 27 | podpath = File.expand_path("#{path}", file_abs_path) 28 | pods_ary.push({:name => podname, :path => podpath}); 29 | else 30 | puts "Invalid plugin specification: #{line}" 31 | end 32 | } 33 | return pods_ary 34 | end 35 | 36 | target 'Runner' do 37 | use_frameworks! 38 | 39 | # Prepare symlinks folder. We use symlinks to avoid having Podfile.lock 40 | # referring to absolute paths on developers' machines. 41 | system('rm -rf .symlinks') 42 | system('mkdir -p .symlinks/plugins') 43 | 44 | # Flutter Pods 45 | generated_xcode_build_settings = parse_KV_file('./Flutter/Generated.xcconfig') 46 | if generated_xcode_build_settings.empty? 47 | puts "Generated.xcconfig must exist. If you're running pod install manually, make sure flutter pub get is executed first." 48 | end 49 | generated_xcode_build_settings.map { |p| 50 | if p[:name] == 'FLUTTER_FRAMEWORK_DIR' 51 | symlink = File.join('.symlinks', 'flutter') 52 | File.symlink(File.dirname(p[:path]), symlink) 53 | pod 'Flutter', :path => File.join(symlink, File.basename(p[:path])) 54 | end 55 | } 56 | 57 | # Plugin Pods 58 | plugin_pods = parse_KV_file('../.flutter-plugins') 59 | plugin_pods.map { |p| 60 | symlink = File.join('.symlinks', 'plugins', p[:name]) 61 | File.symlink(p[:path], symlink) 62 | pod p[:name], :path => File.join(symlink, 'ios') 63 | } 64 | end 65 | 66 | # Prevent Cocoapods from embedding a second Flutter framework and causing an error with the new Xcode build system. 67 | install! 'cocoapods', :disable_input_output_paths => true 68 | 69 | post_install do |installer| 70 | installer.pods_project.targets.each do |target| 71 | target.build_configurations.each do |config| 72 | config.build_settings['ENABLE_BITCODE'] = 'NO' 73 | end 74 | end 75 | end 76 | -------------------------------------------------------------------------------- /example/lib/provider/provider_chat.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | import 'package:flutter/material.dart'; 3 | 4 | class ChatProvider with ChangeNotifier { 5 | bool _isEditing = false; 6 | bool _isShowAudioRecorder = false; 7 | bool _isShowStickerPanel = false; 8 | bool _isShowActionPanel = false; 9 | bool _isShowSendButton = false; 10 | 11 | bool get isEditing => _isEditing; 12 | 13 | bool get isShowAudioRecorder => _isShowAudioRecorder; 14 | 15 | bool get isShowStickerPanel => _isShowStickerPanel; 16 | 17 | bool get isShowActionPanel => _isShowActionPanel; 18 | 19 | bool get isShowSendButton { 20 | if (Platform.isIOS) { 21 | return false; 22 | } else { 23 | if (_isShowAudioRecorder || _isShowActionPanel) { 24 | return false; 25 | } else { 26 | return _isShowSendButton; 27 | } 28 | } 29 | } 30 | 31 | set isShowSendButton(bool value) { 32 | _isShowSendButton = value; 33 | 34 | notifyListeners(); 35 | } 36 | 37 | // 是否在录音 38 | bool _isRecording = false; 39 | 40 | bool get isRecording => _isRecording; 41 | 42 | set isRecording(bool value) { 43 | _isRecording = value; 44 | 45 | notifyListeners(); 46 | } 47 | 48 | // 是否上滑取消发送录音 49 | bool _willCancelRecording = false; 50 | 51 | get willCancelRecording => _willCancelRecording; 52 | 53 | set willCancelRecording(bool value) { 54 | _willCancelRecording = value; 55 | 56 | notifyListeners(); 57 | } 58 | 59 | // 是否快速点击 60 | bool isQuickTap = false; 61 | 62 | void beginEditing() { 63 | _isEditing = true; 64 | 65 | _isShowAudioRecorder = false; 66 | 67 | hideAllPanels(); 68 | 69 | notifyListeners(); 70 | } 71 | 72 | void endEditing() { 73 | _isEditing = false; 74 | } 75 | 76 | void willShowAudioRecorder() { 77 | _isShowAudioRecorder = true; 78 | _isShowStickerPanel = false; 79 | _isShowActionPanel = false; 80 | } 81 | 82 | void showAudioRecorder() { 83 | // _isShowAudioRecorder = true; 84 | 85 | // hideAllPanels(); 86 | 87 | notifyListeners(); 88 | } 89 | 90 | void willShowStickerPanel() { 91 | _isShowAudioRecorder = false; 92 | 93 | _isShowActionPanel = false; 94 | _isShowStickerPanel = true; 95 | } 96 | 97 | void showStickerPanel() { 98 | notifyListeners(); 99 | } 100 | 101 | void willShowActionPanel() { 102 | _isShowAudioRecorder = false; 103 | 104 | _isShowStickerPanel = false; 105 | _isShowActionPanel = true; 106 | } 107 | 108 | void showActionPanel() { 109 | notifyListeners(); 110 | } 111 | 112 | void hideAllPanels() { 113 | _isShowStickerPanel = false; 114 | _isShowActionPanel = false; 115 | 116 | notifyListeners(); 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /android/src/main/java/cn/cgm/flutter_nim/Helper/NIMKickoutInteractor.java: -------------------------------------------------------------------------------- 1 | package cn.cgm.flutter_nim.Helper; 2 | 3 | import com.netease.nimlib.sdk.NIMClient; 4 | import com.netease.nimlib.sdk.Observer; 5 | import com.netease.nimlib.sdk.StatusCode; 6 | import com.netease.nimlib.sdk.auth.AuthServiceObserver; 7 | 8 | import org.json.JSONException; 9 | import org.json.JSONObject; 10 | 11 | import io.flutter.plugin.common.EventChannel; 12 | 13 | public class NIMKickoutInteractor { 14 | private EventChannel.EventSink eventSink; 15 | 16 | public NIMKickoutInteractor(EventChannel.EventSink eventSink) { 17 | 18 | this.eventSink = eventSink; 19 | 20 | registerObservers(true); 21 | } 22 | 23 | private void registerObservers(boolean register) { 24 | // 用户状态监听 25 | Observer userStatusObserver = 26 | new Observer() { 27 | 28 | @Override 29 | public void onEvent(StatusCode code) { 30 | if (code.wontAutoLogin()) { 31 | // 账号在其他设备登录 32 | if (code == StatusCode.KICKOUT) { 33 | handleKickCode(1); 34 | } else if (code == StatusCode.FORBIDDEN) { 35 | handleKickCode(2); 36 | } else if (code == StatusCode.KICK_BY_OTHER_CLIENT) { 37 | handleKickCode(3); 38 | } 39 | } else { 40 | if (code == StatusCode.NET_BROKEN) { 41 | // 42 | } else if (code == StatusCode.UNLOGIN) { 43 | // 44 | } else if (code == StatusCode.CONNECTING) { 45 | // 46 | } else if (code == StatusCode.LOGINING) { 47 | // 48 | } else { 49 | // 50 | } 51 | } 52 | } 53 | }; 54 | 55 | NIMClient.getService(AuthServiceObserver.class) 56 | .observeOnlineStatus(userStatusObserver, register); 57 | } 58 | 59 | private void handleKickCode(int kickCode) { 60 | if (this.eventSink == null) { 61 | return; 62 | } 63 | 64 | JSONObject imObject = new JSONObject(); 65 | 66 | try { 67 | imObject.put("kickCode", kickCode); 68 | } catch (JSONException exception) { 69 | exception.printStackTrace(); 70 | } 71 | 72 | String result = imObject.toString(); 73 | 74 | eventSink.success(result); 75 | } 76 | 77 | } 78 | -------------------------------------------------------------------------------- /android/src/main/java/cn/cgm/flutter_nim/Helper/FlutterNIMSDKOptionConfig.java: -------------------------------------------------------------------------------- 1 | package cn.cgm.flutter_nim.Helper; 2 | 3 | import android.content.Context; 4 | import android.os.Environment; 5 | import android.text.TextUtils; 6 | 7 | import com.netease.nimlib.sdk.SDKOptions; 8 | import com.netease.nimlib.sdk.mixpush.MixPushConfig; 9 | 10 | import java.io.IOException; 11 | 12 | /** 13 | * 云信sdk 自定义的SDK选项设置 14 | */ 15 | public class FlutterNIMSDKOptionConfig { 16 | 17 | public static SDKOptions getSDKOptions(Context context, String appKey, MixPushConfig mixPushConfig) { 18 | SDKOptions options = new SDKOptions(); 19 | 20 | options.appKey = appKey; 21 | 22 | // 配置保存图片,文件,log 等数据的目录 23 | // 如果 options 中没有设置这个值,SDK 会使用采用默认路径作为 SDK 的数据目录。 24 | // 该目录目前包含 log, file, image, audio, video, thumb 这6个目录。 25 | String sdkPath = getAppCacheDir(context) + "/FlutterNIM"; // 可以不设置,那么将采用默认路径 26 | // 如果第三方 APP 需要缓存清理功能,清理这个目录下面个子目录的内容即可。 27 | options.sdkStorageRootPath = sdkPath; 28 | 29 | // 配置是否需要预下载附件缩略图,默认为 true 30 | options.preloadAttach = true; 31 | 32 | // 在线多端同步未读数 33 | options.sessionReadAck = true; 34 | 35 | // 动图的缩略图直接下载原图 36 | options.animatedImageThumbnailEnabled = true; 37 | 38 | // 采用异步加载SDK 39 | options.asyncInitSDK = true; 40 | 41 | // 是否是弱IM场景 42 | options.reducedIM = false; 43 | 44 | // 是否检查manifest 配置,调试阶段打开,调试通过之后请关掉 45 | options.checkManifestConfig = false; 46 | 47 | // 是否启用群消息已读功能,默认关闭 48 | options.enableTeamMsgAck = true; 49 | 50 | // 打开消息撤回未读数-1的开关 51 | options.shouldConsiderRevokedMessageUnreadCount = true; 52 | 53 | options.mixPushConfig = mixPushConfig; 54 | 55 | return options; 56 | } 57 | 58 | /** 59 | * 配置 APP 保存图片/语音/文件/log等数据的目录 60 | * 这里示例用SD卡的应用扩展存储目录 61 | */ 62 | private static String getAppCacheDir(Context context) { 63 | String storageRootPath = null; 64 | try { 65 | // SD卡应用扩展存储区(APP卸载后,该目录下被清除,用户也可以在设置界面中手动清除),请根据APP对数据缓存的重要性及生命周期来决定是否采用此缓存目录. 66 | // 该存储区在API 19以上不需要写权限,即可配置 67 | if (context.getExternalCacheDir() != null) { 68 | storageRootPath = context.getExternalCacheDir().getCanonicalPath(); 69 | } 70 | } catch (IOException e) { 71 | e.printStackTrace(); 72 | } 73 | if (TextUtils.isEmpty(storageRootPath)) { 74 | // SD卡应用公共存储区(APP卸载后,该目录不会被清除,下载安装APP后,缓存数据依然可以被加载。SDK默认使用此目录),该存储区域需要写权限! 75 | storageRootPath = Environment.getExternalStorageDirectory() + "/" + context.getPackageName(); 76 | } 77 | 78 | return storageRootPath; 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /example/pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: flutter_nim_example 2 | description: Demonstrates how to use the flutter_nim plugin. 3 | publish_to: 'none' 4 | version: 1.0.0+1 5 | 6 | environment: 7 | sdk: ">=2.2.2 <3.0.0" 8 | 9 | dependencies: 10 | flutter: 11 | sdk: flutter 12 | 13 | # The following adds the Cupertino Icons font to your application. 14 | # Use with the CupertinoIcons class for iOS style icons. 15 | cupertino_icons: ^0.1.3 16 | 17 | # flutter_nim: 0.1.6 18 | 19 | provider: 4.0.2 20 | shared_preferences: 0.5.6+1 21 | cached_network_image: 2.0.0 22 | audioplayers: 0.14.0 23 | image_picker: 0.6.3+1 24 | permission_handler: 4.2.0+hotfix.3 25 | fluttertoast: 3.1.3 26 | keyboard_visibility: 0.5.6 27 | flutter_easyrefresh: 2.0.9 28 | 29 | 30 | dev_dependencies: 31 | flutter_test: 32 | sdk: flutter 33 | 34 | flutter_nim: 35 | path: ../ 36 | 37 | # For information on the generic Dart part of this file, see the 38 | # following page: https://dart.dev/tools/pub/pubspec 39 | 40 | # The following section is specific to Flutter. 41 | flutter: 42 | 43 | # The following line ensures that the Material Icons font is 44 | # included with your application, so that you can use the icons in 45 | # the material Icons class. 46 | uses-material-design: true 47 | 48 | # To add assets to your application, add an assets section, like this: 49 | # assets: 50 | # - images/a_dot_burr.jpeg 51 | # - images/a_dot_ham.jpeg 52 | 53 | assets: 54 | - images/default_user_avatar.png 55 | - images/img_not_available.png 56 | 57 | - images/im_message_cell_error.png 58 | - images/im_video_play.png 59 | - images/im_audio_play_left.gif 60 | - images/im_audio_play_right.gif 61 | - images/im_audio_stop_left.png 62 | - images/im_audio_stop_right.png 63 | - images/im_action_panel_gallery.png 64 | - images/im_action_panel_camera.png 65 | - images/im_action_panel_house.png 66 | 67 | # An image asset can refer to one or more resolution-specific "variants", see 68 | # https://flutter.dev/assets-and-images/#resolution-aware. 69 | 70 | # For details regarding adding assets from package dependencies, see 71 | # https://flutter.dev/assets-and-images/#from-packages 72 | 73 | # To add custom fonts to your application, add a fonts section here, 74 | # in this "flutter" section. Each entry in this list should have a 75 | # "family" key with the font family name, and a "fonts" key with a 76 | # list giving the asset and other descriptors for the font. For 77 | # example: 78 | # fonts: 79 | # - family: Schyler 80 | # fonts: 81 | # - asset: fonts/Schyler-Regular.ttf 82 | # - asset: fonts/Schyler-Italic.ttf 83 | # style: italic 84 | # - family: Trajan Pro 85 | # fonts: 86 | # - asset: fonts/TrajanPro.ttf 87 | # - asset: fonts/TrajanPro_Bold.ttf 88 | # weight: 700 89 | # 90 | # For details regarding fonts from package dependencies, 91 | # see https://flutter.dev/custom-fonts/#from-packages 92 | -------------------------------------------------------------------------------- /example/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 | -------------------------------------------------------------------------------- /example/android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 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 | 39 | 44 | 48 | 56 | 60 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | -------------------------------------------------------------------------------- /example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 39 | 40 | 41 | 42 | 43 | 44 | 54 | 56 | 62 | 63 | 64 | 65 | 66 | 67 | 73 | 75 | 81 | 82 | 83 | 84 | 86 | 87 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /example/lib/zeus_kit/ui/zk_network_image.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter/cupertino.dart'; 3 | import 'package:cached_network_image/cached_network_image.dart'; 4 | 5 | class ZKNetworkImage extends StatelessWidget { 6 | final String imageUrl; 7 | final double width; 8 | final double height; 9 | final BoxFit fit; 10 | final BorderRadius borderRadius; 11 | final String placeholderImagePath; 12 | final String errorImagePath; 13 | 14 | ZKNetworkImage({ 15 | @required this.imageUrl, 16 | this.width, 17 | this.height, 18 | this.fit: BoxFit.cover, 19 | this.borderRadius, 20 | this.placeholderImagePath: "", 21 | this.errorImagePath: "", 22 | }); 23 | 24 | @override 25 | Widget build(BuildContext context) { 26 | if (imageUrl == null || imageUrl.isEmpty) { 27 | return ClipRRect( 28 | borderRadius: borderRadius, 29 | child: placeholderImagePath.isEmpty 30 | ? Container( 31 | width: width, 32 | height: height, 33 | ) 34 | : Image.asset( 35 | placeholderImagePath, 36 | width: width, 37 | height: height, 38 | fit: BoxFit.cover, 39 | ), 40 | ); 41 | } 42 | 43 | return Container( 44 | child: ClipRRect( 45 | borderRadius: borderRadius, 46 | child: CachedNetworkImage( 47 | imageUrl: imageUrl, 48 | width: width, 49 | height: height, 50 | fit: fit, 51 | placeholder: (context, url) { 52 | if (placeholderImagePath.isEmpty) { 53 | return Container( 54 | width: width, 55 | height: height, 56 | color: const Color(0xFFE8E8E8), 57 | alignment: Alignment.center, 58 | child: CupertinoActivityIndicator(), 59 | ); 60 | } else { 61 | return Image.asset( 62 | placeholderImagePath, 63 | width: width, 64 | height: height, 65 | fit: BoxFit.cover, 66 | ); 67 | } 68 | }, 69 | errorWidget: (context, url, error) { 70 | if (errorImagePath.isEmpty) { 71 | return Icon(Icons.error); 72 | } else { 73 | return Image.asset( 74 | errorImagePath, 75 | width: width, 76 | height: height, 77 | fit: BoxFit.cover, 78 | ); 79 | } 80 | }, 81 | ), 82 | ), 83 | ); 84 | } 85 | } 86 | 87 | class ZKCircleAvatar extends StatelessWidget { 88 | final String avatarUrl; 89 | final double size; 90 | final String defaultAvatar; 91 | 92 | ZKCircleAvatar({ 93 | Key key, 94 | @required this.avatarUrl, 95 | @required this.size, 96 | @required this.defaultAvatar, 97 | }) : super(key: key); 98 | 99 | @override 100 | Widget build(BuildContext context) { 101 | if (avatarUrl == null || avatarUrl.isEmpty) { 102 | return ClipOval( 103 | child: Image.asset( 104 | defaultAvatar, 105 | width: size, 106 | height: size, 107 | fit: BoxFit.cover, 108 | ), 109 | ); 110 | } 111 | 112 | return ClipOval( 113 | child: CachedNetworkImage( 114 | imageUrl: avatarUrl, 115 | width: size, 116 | height: size, 117 | fit: BoxFit.cover, 118 | placeholder: (context, url) { 119 | return CupertinoActivityIndicator(); 120 | }, 121 | errorWidget: (context, url, error) { 122 | return Image.asset( 123 | defaultAvatar, 124 | width: size, 125 | height: size, 126 | fit: BoxFit.cover, 127 | ); 128 | }, 129 | ), 130 | ); 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /example/ios/Podfile.lock: -------------------------------------------------------------------------------- 1 | PODS: 2 | - audioplayers (0.0.1): 3 | - Flutter 4 | - Flutter (1.0.0) 5 | - flutter_nim (0.1.7): 6 | - Flutter 7 | - NIMSDK (= 7.0.3) 8 | - flutter_plugin_android_lifecycle (0.0.1): 9 | - Flutter 10 | - fluttertoast (0.0.2): 11 | - Flutter 12 | - FMDB (2.7.5): 13 | - FMDB/standard (= 2.7.5) 14 | - FMDB/standard (2.7.5) 15 | - image_picker (0.0.1): 16 | - Flutter 17 | - keyboard_visibility (0.5.0): 18 | - Flutter 19 | - Reachability 20 | - NIMSDK (7.0.3) 21 | - path_provider (0.0.1): 22 | - Flutter 23 | - "permission_handler (4.2.0+hotfix.3)": 24 | - Flutter 25 | - Reachability (3.2) 26 | - shared_preferences (0.0.1): 27 | - Flutter 28 | - shared_preferences_macos (0.0.1): 29 | - Flutter 30 | - shared_preferences_web (0.0.1): 31 | - Flutter 32 | - sqflite (0.0.1): 33 | - Flutter 34 | - FMDB (~> 2.7.2) 35 | 36 | DEPENDENCIES: 37 | - audioplayers (from `.symlinks/plugins/audioplayers/ios`) 38 | - Flutter (from `.symlinks/flutter/ios`) 39 | - flutter_nim (from `.symlinks/plugins/flutter_nim/ios`) 40 | - flutter_plugin_android_lifecycle (from `.symlinks/plugins/flutter_plugin_android_lifecycle/ios`) 41 | - fluttertoast (from `.symlinks/plugins/fluttertoast/ios`) 42 | - image_picker (from `.symlinks/plugins/image_picker/ios`) 43 | - keyboard_visibility (from `.symlinks/plugins/keyboard_visibility/ios`) 44 | - path_provider (from `.symlinks/plugins/path_provider/ios`) 45 | - permission_handler (from `.symlinks/plugins/permission_handler/ios`) 46 | - shared_preferences (from `.symlinks/plugins/shared_preferences/ios`) 47 | - shared_preferences_macos (from `.symlinks/plugins/shared_preferences_macos/ios`) 48 | - shared_preferences_web (from `.symlinks/plugins/shared_preferences_web/ios`) 49 | - sqflite (from `.symlinks/plugins/sqflite/ios`) 50 | 51 | SPEC REPOS: 52 | https://github.com/CocoaPods/Specs.git: 53 | - FMDB 54 | - NIMSDK 55 | - Reachability 56 | 57 | EXTERNAL SOURCES: 58 | audioplayers: 59 | :path: ".symlinks/plugins/audioplayers/ios" 60 | Flutter: 61 | :path: ".symlinks/flutter/ios" 62 | flutter_nim: 63 | :path: ".symlinks/plugins/flutter_nim/ios" 64 | flutter_plugin_android_lifecycle: 65 | :path: ".symlinks/plugins/flutter_plugin_android_lifecycle/ios" 66 | fluttertoast: 67 | :path: ".symlinks/plugins/fluttertoast/ios" 68 | image_picker: 69 | :path: ".symlinks/plugins/image_picker/ios" 70 | keyboard_visibility: 71 | :path: ".symlinks/plugins/keyboard_visibility/ios" 72 | path_provider: 73 | :path: ".symlinks/plugins/path_provider/ios" 74 | permission_handler: 75 | :path: ".symlinks/plugins/permission_handler/ios" 76 | shared_preferences: 77 | :path: ".symlinks/plugins/shared_preferences/ios" 78 | shared_preferences_macos: 79 | :path: ".symlinks/plugins/shared_preferences_macos/ios" 80 | shared_preferences_web: 81 | :path: ".symlinks/plugins/shared_preferences_web/ios" 82 | sqflite: 83 | :path: ".symlinks/plugins/sqflite/ios" 84 | 85 | SPEC CHECKSUMS: 86 | audioplayers: 84f968cea3f2deab00ec4f8ff53358b3c0b3992c 87 | Flutter: 0e3d915762c693b495b44d77113d4970485de6ec 88 | flutter_nim: 114522087f8b77a4259715af3b5fa44348adfb99 89 | flutter_plugin_android_lifecycle: 47de533a02850f070f5696a623995e93eddcdb9b 90 | fluttertoast: b644586ef3b16f67fae9a1f8754cef6b2d6b634b 91 | FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a 92 | image_picker: e3eacd46b94694dde7cf2705955cece853aa1a8f 93 | keyboard_visibility: 96a24de806fe6823c3ad956c01ba2ec6d056616f 94 | NIMSDK: 76dfea3b739f837db806e6fc02113dab8ec97d4a 95 | path_provider: fb74bd0465e96b594bb3b5088ee4a4e7bb1f2a9d 96 | permission_handler: 40520ab8ad1bb78a282b832464e995ec87f77ec6 97 | Reachability: 33e18b67625424e47b6cde6d202dce689ad7af96 98 | shared_preferences: 430726339841afefe5142b9c1f50cb6bd7793e01 99 | shared_preferences_macos: f3f29b71ccbb56bf40c9dd6396c9acf15e214087 100 | shared_preferences_web: 141cce0c3ed1a1c5bf2a0e44f52d31eeb66e5ea9 101 | sqflite: ff1d9da63c06588cc8d1faf7256d741f16989d5a 102 | 103 | PODFILE CHECKSUM: 33fc79ba6f4c14cbfc335357e1271d639f1c9cc4 104 | 105 | COCOAPODS: 1.8.3 106 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # flutter_nim 2 | 用于`Flutter`的网易云信SDK 3 | 4 | 5 | ## 目前功能 6 | 7 | * 登录 8 | * 自动登录 9 | * 退出登录 10 | * 踢出 11 | * 获取最近会话列表 12 | * 删除一个最近会话列表 13 | * 开启新会话 14 | * 关闭打开的会话 15 | * 获取会话消息 16 | * 发送接收文本、图片、视频、音频消息 17 | * 发送接收自定义消息 18 | * 标记音频已读 19 | * 调用`NIMSDK`能力实现语音消息的录制、发送 20 | 21 | 22 | > ##### 不支持群组聊天 23 | 24 | 25 | ## Screenshot 26 | 27 | ![Screenshot1](https://github.com/GuiminChu/flutter_nim/blob/develop/screenshot/Screen%20Shot%201.png) 28 | 29 | ## 部分示例 30 | 31 | ### 初始化 32 | 33 | 使用前,先进行初始化: 34 | 35 | ```dart 36 | void main() async { 37 | FlutterNIM().init( 38 | appKey: "123456", 39 | apnsCername: "ABCDEFG", // iOS 生产环境证书名称 40 | apnsCernameDevelop: "ABCDEFG", // iOS 测试环境证书名称 41 | imAccount: "123456", 42 | imToken: "123456", 43 | ); 44 | 45 | runApp(MyApp()); 46 | } 47 | ``` 48 | 49 | 由于 SDK 限制,Android 平台依然需要去 Application 中初始化,可以参照以下代码: 50 | 51 | ```java 52 | public class MyApplication extends FlutterApplication { 53 | @Override 54 | public void onCreate() { 55 | super.onCreate(); 56 | 57 | FlutterNIMPreferences.setContext(this); 58 | // SDK初始化(启动后台服务,若已经存在用户登录信息, SDK 将完成自动登录) 59 | NIMClient.init(this, FlutterNIMPreferences.getLoginInfo(), buildSDKOptions()); 60 | } 61 | 62 | // 网易云信配置(此处也可以使用自己的配置) 63 | private SDKOptions buildSDKOptions() { 64 | return FlutterNIMSDKOptionConfig.getSDKOptions(this, "123456", buildMixPushConfig()); 65 | } 66 | 67 | // 网易云信第三方推送配置 68 | private MixPushConfig buildMixPushConfig() { 69 | 70 | MixPushConfig config = new MixPushConfig(); 71 | 72 | // 小米推送 73 | config.xmAppId = "123456"; 74 | config.xmAppKey = "123456"; 75 | config.xmCertificateName = "ABCDEFG"; 76 | 77 | // 华为推送 78 | config.hwCertificateName = "ABCDEFG"; 79 | ... 80 | 81 | return config; 82 | } 83 | } 84 | 85 | ``` 86 | 87 | ### 登录 88 | 89 | ```dart 90 | bool isSuccess = await FlutterNIM().login(imAccount, imToken); 91 | ``` 92 | 93 | ### 退出登录 94 | 95 | ```dart 96 | FlutterNIM().logout(); 97 | ``` 98 | 99 | ### 踢出 100 | 101 | ```dart 102 | FlutterNIM().kickReasonResponse.listen((NIMKickReason reason) { 103 | // 处理被踢下线逻辑 104 | }); 105 | ``` 106 | 107 | ### 最近会话列表 108 | 109 | 手动获取最近会话列表: 110 | 111 | ```dart 112 | FlutterNIM().loadRecentSessions(); 113 | ``` 114 | 115 | 监听最近会话,手动调用获取最近会话列表或最近会话列表有变更(新增、更新、删除、标记已读)都会触发此回调。 116 | 117 | ```dart 118 | FlutterNIM().recentSessionsResponse.listen((recentSessions) { 119 | List _recentSessions = recentSessions; 120 | 121 | // 在此可计算最近会话总未读数 122 | int unreadNum = recentSessions 123 | .map((s) => s.unreadCount) 124 | .fold(0, (curr, next) => curr + next); 125 | }); 126 | ``` 127 | 128 | 删除某项会话(只删除本地缓存,不会删除云端记录) 129 | 130 | ```dart 131 | FlutterNIM().deleteRecentSession(session.sessionId); 132 | ``` 133 | 134 | ### 会话 135 | 136 | 开启一个新会话 137 | 138 | ```dart 139 | bool isSuccess = await FlutterNIM().startChat(session.sessionId); 140 | ``` 141 | 142 | 监听会话消息 143 | 144 | ```dart 145 | FlutterNIM().messagesResponse.listen((messages) { 146 | List _messages = messages; 147 | 148 | // 求余,如果结果等于零则可能还有更多数据 149 | int _remainder = this._messages.length % 20; 150 | bool _hasMoreData = remainder == 0; 151 | }); 152 | ``` 153 | 154 | 退出聊天页面时,需要将会话关闭 155 | 156 | ```dart 157 | FlutterNIM().exitChat(); 158 | ``` 159 | 160 | 发送消息 161 | 162 | ```dart 163 | // 发送文本消息 164 | FlutterNIM().sendText(content); 165 | 166 | // 发送图片消息 167 | FlutterNIM().sendImage(file.path); 168 | 169 | // 发送视频消息 170 | FlutterNIM().sendVideo(file.path); 171 | ``` 172 | 173 | 语音消息调用了原生云信SDK的录音能力 174 | 175 | ```dart 176 | // 开始录音 177 | FlutterNIM().onStartRecording(); 178 | 179 | // 结束录音,结束后自动发送语音消息 180 | FlutterNIM().onStopRecording(); 181 | 182 | // 取消录音,不会发送 183 | FlutterNIM().onCancelRecording(); 184 | ``` 185 | 186 | ### 自定义消息 187 | 188 | 把自定义消息内容放在`Map`中: 189 | 190 | ```dart 191 | 192 | // 随便自己定义 193 | Map customObject = { 194 | "type": "自定义", 195 | "url": "xxx", 196 | ... 197 | }; 198 | 199 | FlutterNIM().sendCustomMessage( 200 | customObject, 201 | apnsContent: "[发来了一条自定义消息]", 202 | ); 203 | 204 | ``` 205 | 206 | 接收到的自定义消息体以`JSON`字符串的格式存在`NIMMessage`的`customMessageContent`中,然后自己去解析: 207 | 208 | ```dart 209 | final customObjectMap = json.decode(customMessageContent); 210 | // 自定义 211 | var model = CustomModel.fromJson(customObjectMap); 212 | ``` 213 | 214 | 215 | -------------------------------------------------------------------------------- /lib/models/nim_message_model.dart: -------------------------------------------------------------------------------- 1 | class NIMMessage { 2 | /// 消息来源 3 | String from; 4 | 5 | /// 消息ID,唯一标识 6 | String messageId; 7 | 8 | /// 消息发送时间戳,单位 ms 9 | int timestamp; 10 | 11 | /// 消息文本 12 | /// 消息中除 NIMMessageTypeText 和 NIMMessageTypeTip 外,其他消息 text 字段都为 nil 13 | String text; 14 | 15 | /// 是否是往外发的消息 16 | /// 由于能对自己发消息,所以并不是所有来源是自己的消息都是往外发的消息,这个字段用于判断头像排版位置(是左还是右)。 17 | bool isOutgoingMsg; 18 | 19 | /// 是否显示时间戳 20 | bool isShowTimeTag = false; 21 | 22 | NIMMessageType messageType; 23 | NIMMessageObject messageObject; 24 | NIMMessageDeliveryState deliveryState; 25 | 26 | String customMessageContent; 27 | dynamic customMessageObject; 28 | 29 | NIMMessage({ 30 | this.from, 31 | this.messageId, 32 | this.timestamp, 33 | this.text, 34 | this.isOutgoingMsg, 35 | this.messageType, 36 | this.messageObject, 37 | this.deliveryState, 38 | this.customMessageContent, 39 | }); 40 | 41 | NIMMessage.fromJson(Map json) { 42 | from = json['from']; 43 | messageId = json['messageId']; 44 | timestamp = json['timestamp']; 45 | text = json['text']; 46 | isOutgoingMsg = json['isOutgoingMsg']; 47 | 48 | customMessageContent = json['customMessageContent']; 49 | 50 | messageObject = json['messageObject'] != null 51 | ? NIMMessageObject.fromJson(json['messageObject']) 52 | : null; 53 | 54 | // 消息类型 55 | // 原生传过来的是 rawValue,这里手动对应一下 56 | int messageTypeRawValue = json["messageType"]; 57 | 58 | switch (messageTypeRawValue) { 59 | case 0: 60 | messageType = NIMMessageType.Text; 61 | break; 62 | case 1: 63 | messageType = NIMMessageType.Image; 64 | break; 65 | case 2: 66 | messageType = NIMMessageType.Audio; 67 | break; 68 | case 3: 69 | messageType = NIMMessageType.Video; 70 | break; 71 | case 4: 72 | messageType = NIMMessageType.Location; 73 | break; 74 | case 5: 75 | messageType = NIMMessageType.Notification; 76 | break; 77 | case 6: 78 | messageType = NIMMessageType.File; 79 | break; 80 | case 10: 81 | messageType = NIMMessageType.Tip; 82 | break; 83 | case 11: 84 | messageType = NIMMessageType.Robot; 85 | break; 86 | case 100: 87 | messageType = NIMMessageType.Custom; 88 | break; 89 | } 90 | 91 | // 发送状态 92 | // 原生传过来的是 rawValue,这里手动对应一下 93 | int deliveryStateRawValue = json["deliveryState"]; 94 | 95 | switch (deliveryStateRawValue) { 96 | case 0: 97 | deliveryState = NIMMessageDeliveryState.Failed; 98 | break; 99 | case 1: 100 | deliveryState = NIMMessageDeliveryState.Delivering; 101 | break; 102 | case 2: 103 | deliveryState = NIMMessageDeliveryState.Delivered; 104 | break; 105 | } 106 | } 107 | } 108 | 109 | class NIMMessageObject { 110 | String url; // 图片、音频、视频的远程路径 111 | String thumbUrl; // 图片缩略图远程路径 112 | String thumbPath; // 图片缩略图本地路径 113 | String coverUrl; // 视频封面图远程路径 114 | String path; // 音视频图片等文件本地路径 115 | int duration; // 音频、视频的时长(毫秒) 116 | 117 | bool isPlayed; // 音频消息是否播放过 118 | 119 | // 图片或视频的宽高 120 | int width; 121 | int height; 122 | 123 | // 音频时长描述 124 | String get audioDurationDesc { 125 | int seconds = (duration / 1000).ceil(); 126 | 127 | return "$seconds\""; 128 | } 129 | 130 | // 视频时长描述 分:秒 131 | String get videoDurationDesc { 132 | int seconds = (duration / 1000).round(); 133 | 134 | int minute = (seconds - (seconds % 60)) ~/ 60; 135 | int second = (seconds - minute * 60); 136 | 137 | if (second > 9) { 138 | return "$minute:$second"; 139 | } else { 140 | return "$minute:0$second"; 141 | } 142 | } 143 | 144 | NIMMessageObject({ 145 | this.url, 146 | this.thumbUrl, 147 | this.thumbPath, 148 | this.coverUrl, 149 | this.path, 150 | this.duration: 0, 151 | this.isPlayed, 152 | this.width: 0, 153 | this.height: 0, 154 | }); 155 | 156 | NIMMessageObject.fromJson(Map json) { 157 | url = json['url'] ?? ""; 158 | thumbUrl = json['thumbUrl'] ?? ""; 159 | thumbPath = json['thumbPath'] ?? ""; 160 | coverUrl = json['coverUrl'] ?? ""; 161 | path = json['path'] ?? ""; 162 | duration = json['duration'] ?? 0; 163 | isPlayed = json['isPlayed'] ?? true; 164 | width = json['width'] ?? 0; 165 | height = json['height'] ?? 0; 166 | } 167 | } 168 | 169 | /// 消息内容类型枚举 170 | enum NIMMessageType { 171 | Text, // 文本类型消息 0 172 | Image, // 图片类型消息 1 173 | Audio, // 声音类型消息 2 174 | Video, // 视频类型消息 3 175 | Location, // 位置类型消息 4 176 | Notification, // 通知类型消息 5 177 | File, // 文件类型消息 6 178 | Tip, // 提醒类型消息 10 179 | Robot, // 机器人类型消息 11 180 | Custom, // 自定义类型消息 100 181 | } 182 | 183 | /// 消息投递状态(仅针对发送的消息) 184 | enum NIMMessageDeliveryState { 185 | Failed, // 消息发送失败 0 186 | Delivering, // 消息发送中 1 187 | Delivered, // 消息发送成功 2 188 | } 189 | -------------------------------------------------------------------------------- /pubspec.lock: -------------------------------------------------------------------------------- 1 | # Generated by pub 2 | # See https://dart.dev/tools/pub/glossary#lockfile 3 | packages: 4 | archive: 5 | dependency: transitive 6 | description: 7 | name: archive 8 | url: "https://pub.flutter-io.cn" 9 | source: hosted 10 | version: "2.0.11" 11 | args: 12 | dependency: transitive 13 | description: 14 | name: args 15 | url: "https://pub.flutter-io.cn" 16 | source: hosted 17 | version: "1.5.2" 18 | async: 19 | dependency: transitive 20 | description: 21 | name: async 22 | url: "https://pub.flutter-io.cn" 23 | source: hosted 24 | version: "2.4.0" 25 | boolean_selector: 26 | dependency: transitive 27 | description: 28 | name: boolean_selector 29 | url: "https://pub.flutter-io.cn" 30 | source: hosted 31 | version: "1.0.5" 32 | charcode: 33 | dependency: transitive 34 | description: 35 | name: charcode 36 | url: "https://pub.flutter-io.cn" 37 | source: hosted 38 | version: "1.1.2" 39 | collection: 40 | dependency: transitive 41 | description: 42 | name: collection 43 | url: "https://pub.flutter-io.cn" 44 | source: hosted 45 | version: "1.14.11" 46 | convert: 47 | dependency: transitive 48 | description: 49 | name: convert 50 | url: "https://pub.flutter-io.cn" 51 | source: hosted 52 | version: "2.1.1" 53 | crypto: 54 | dependency: transitive 55 | description: 56 | name: crypto 57 | url: "https://pub.flutter-io.cn" 58 | source: hosted 59 | version: "2.1.3" 60 | flutter: 61 | dependency: "direct main" 62 | description: flutter 63 | source: sdk 64 | version: "0.0.0" 65 | flutter_test: 66 | dependency: "direct dev" 67 | description: flutter 68 | source: sdk 69 | version: "0.0.0" 70 | image: 71 | dependency: transitive 72 | description: 73 | name: image 74 | url: "https://pub.flutter-io.cn" 75 | source: hosted 76 | version: "2.1.4" 77 | matcher: 78 | dependency: transitive 79 | description: 80 | name: matcher 81 | url: "https://pub.flutter-io.cn" 82 | source: hosted 83 | version: "0.12.6" 84 | meta: 85 | dependency: transitive 86 | description: 87 | name: meta 88 | url: "https://pub.flutter-io.cn" 89 | source: hosted 90 | version: "1.1.8" 91 | path: 92 | dependency: transitive 93 | description: 94 | name: path 95 | url: "https://pub.flutter-io.cn" 96 | source: hosted 97 | version: "1.6.4" 98 | pedantic: 99 | dependency: transitive 100 | description: 101 | name: pedantic 102 | url: "https://pub.flutter-io.cn" 103 | source: hosted 104 | version: "1.8.0+1" 105 | petitparser: 106 | dependency: transitive 107 | description: 108 | name: petitparser 109 | url: "https://pub.flutter-io.cn" 110 | source: hosted 111 | version: "2.4.0" 112 | quiver: 113 | dependency: transitive 114 | description: 115 | name: quiver 116 | url: "https://pub.flutter-io.cn" 117 | source: hosted 118 | version: "2.0.5" 119 | sky_engine: 120 | dependency: transitive 121 | description: flutter 122 | source: sdk 123 | version: "0.0.99" 124 | source_span: 125 | dependency: transitive 126 | description: 127 | name: source_span 128 | url: "https://pub.flutter-io.cn" 129 | source: hosted 130 | version: "1.5.5" 131 | stack_trace: 132 | dependency: transitive 133 | description: 134 | name: stack_trace 135 | url: "https://pub.flutter-io.cn" 136 | source: hosted 137 | version: "1.9.3" 138 | stream_channel: 139 | dependency: transitive 140 | description: 141 | name: stream_channel 142 | url: "https://pub.flutter-io.cn" 143 | source: hosted 144 | version: "2.0.0" 145 | string_scanner: 146 | dependency: transitive 147 | description: 148 | name: string_scanner 149 | url: "https://pub.flutter-io.cn" 150 | source: hosted 151 | version: "1.0.5" 152 | term_glyph: 153 | dependency: transitive 154 | description: 155 | name: term_glyph 156 | url: "https://pub.flutter-io.cn" 157 | source: hosted 158 | version: "1.1.0" 159 | test_api: 160 | dependency: transitive 161 | description: 162 | name: test_api 163 | url: "https://pub.flutter-io.cn" 164 | source: hosted 165 | version: "0.2.11" 166 | typed_data: 167 | dependency: transitive 168 | description: 169 | name: typed_data 170 | url: "https://pub.flutter-io.cn" 171 | source: hosted 172 | version: "1.1.6" 173 | vector_math: 174 | dependency: transitive 175 | description: 176 | name: vector_math 177 | url: "https://pub.flutter-io.cn" 178 | source: hosted 179 | version: "2.0.8" 180 | xml: 181 | dependency: transitive 182 | description: 183 | name: xml 184 | url: "https://pub.flutter-io.cn" 185 | source: hosted 186 | version: "3.5.0" 187 | sdks: 188 | dart: ">=2.4.0 <3.0.0" 189 | -------------------------------------------------------------------------------- /example/lib/zeus_kit/utils/zk_sp_util.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:convert'; 3 | 4 | import 'package:synchronized/synchronized.dart'; 5 | import 'package:shared_preferences/shared_preferences.dart'; 6 | 7 | /** 8 | * @Author: thl 9 | * @GitHub: https://github.com/Sky24n 10 | * @Email: 863764940@qq.com 11 | * @Email: sky24no@gmail.com 12 | * @Date: 2018/9/8 13 | * @Description: Sp Util. 14 | * 请勿删除作者个人信息!谢谢! 15 | */ 16 | 17 | /// SharedPreferences Util. 18 | /// 内部使用,为了名称统一改了类名,请关注原作者。 19 | /// 20 | /// 使用方法: 21 | /// 22 | /// 等待sp初始化完成后再运行app。 23 | /// sp初始化时间 release模式下30ms左右,debug模式下100多ms。 24 | /// 25 | class ZKSpUtil { 26 | static ZKSpUtil _singleton; 27 | static SharedPreferences _prefs; 28 | static Lock _lock = Lock(); 29 | 30 | static Future getInstance() async { 31 | if (_singleton == null) { 32 | await _lock.synchronized(() async { 33 | if (_singleton == null) { 34 | // keep local instance till it is fully initialized. 35 | // 保持本地实例直到完全初始化。 36 | var singleton = ZKSpUtil._(); 37 | await singleton._init(); 38 | _singleton = singleton; 39 | } 40 | }); 41 | } 42 | return _singleton; 43 | } 44 | 45 | ZKSpUtil._(); 46 | 47 | Future _init() async { 48 | _prefs = await SharedPreferences.getInstance(); 49 | } 50 | 51 | /// put object. 52 | static Future putObject(String key, Object value) { 53 | if (_prefs == null) return null; 54 | return _prefs.setString(key, value == null ? "" : json.encode(value)); 55 | } 56 | 57 | /// get obj. 58 | static T getObj(String key, T f(Map v), {T defValue}) { 59 | Map map = getObject(key); 60 | return map == null ? defValue : f(map); 61 | } 62 | 63 | /// get object. 64 | static Map getObject(String key) { 65 | if (_prefs == null) return null; 66 | String _data = _prefs.getString(key); 67 | return (_data == null || _data.isEmpty) ? null : json.decode(_data); 68 | } 69 | 70 | /// put object list. 71 | static Future putObjectList(String key, List list) { 72 | if (_prefs == null) return null; 73 | List _dataList = list?.map((value) { 74 | return json.encode(value); 75 | })?.toList(); 76 | return _prefs.setStringList(key, _dataList); 77 | } 78 | 79 | /// get obj list. 80 | static List getObjList(String key, T f(Map v), 81 | {List defValue = const []}) { 82 | List dataList = getObjectList(key); 83 | List list = dataList?.map((value) { 84 | return f(value); 85 | })?.toList(); 86 | return list ?? defValue; 87 | } 88 | 89 | /// get object list. 90 | static List getObjectList(String key) { 91 | if (_prefs == null) return null; 92 | List dataLis = _prefs.getStringList(key); 93 | return dataLis?.map((value) { 94 | Map _dataMap = json.decode(value); 95 | return _dataMap; 96 | })?.toList(); 97 | } 98 | 99 | /// get string. 100 | static String getString(String key, {String defValue = ''}) { 101 | if (_prefs == null) return defValue; 102 | return _prefs.getString(key) ?? defValue; 103 | } 104 | 105 | /// put string. 106 | static Future putString(String key, String value) { 107 | if (_prefs == null) return null; 108 | return _prefs.setString(key, value); 109 | } 110 | 111 | /// get bool. 112 | static bool getBool(String key, {bool defValue = false}) { 113 | if (_prefs == null) return defValue; 114 | return _prefs.getBool(key) ?? defValue; 115 | } 116 | 117 | /// put bool. 118 | static Future putBool(String key, bool value) { 119 | if (_prefs == null) return null; 120 | return _prefs.setBool(key, value); 121 | } 122 | 123 | /// get int. 124 | static int getInt(String key, {int defValue = 0}) { 125 | if (_prefs == null) return defValue; 126 | return _prefs.getInt(key) ?? defValue; 127 | } 128 | 129 | /// put int. 130 | static Future putInt(String key, int value) { 131 | if (_prefs == null) return null; 132 | return _prefs.setInt(key, value); 133 | } 134 | 135 | /// get double. 136 | static double getDouble(String key, {double defValue = 0.0}) { 137 | if (_prefs == null) return defValue; 138 | return _prefs.getDouble(key) ?? defValue; 139 | } 140 | 141 | /// put double. 142 | static Future putDouble(String key, double value) { 143 | if (_prefs == null) return null; 144 | return _prefs.setDouble(key, value); 145 | } 146 | 147 | /// get string list. 148 | static List getStringList(String key, 149 | {List defValue = const []}) { 150 | if (_prefs == null) return defValue; 151 | return _prefs.getStringList(key) ?? defValue; 152 | } 153 | 154 | /// put string list. 155 | static Future putStringList(String key, List value) { 156 | if (_prefs == null) return null; 157 | return _prefs.setStringList(key, value); 158 | } 159 | 160 | /// get dynamic. 161 | static dynamic getDynamic(String key, {Object defValue}) { 162 | if (_prefs == null) return defValue; 163 | return _prefs.get(key) ?? defValue; 164 | } 165 | 166 | /// have key. 167 | static bool haveKey(String key) { 168 | if (_prefs == null) return null; 169 | return _prefs.getKeys().contains(key); 170 | } 171 | 172 | /// get keys. 173 | static Set getKeys() { 174 | if (_prefs == null) return null; 175 | return _prefs.getKeys(); 176 | } 177 | 178 | /// remove. 179 | static Future remove(String key) { 180 | if (_prefs == null) return null; 181 | return _prefs.remove(key); 182 | } 183 | 184 | /// clear. 185 | static Future clear() { 186 | if (_prefs == null) return null; 187 | return _prefs.clear(); 188 | } 189 | 190 | ///Sp is initialized. 191 | static bool isInitialized() { 192 | return _prefs != null; 193 | } 194 | } 195 | -------------------------------------------------------------------------------- /android/src/main/java/cn/cgm/flutter_nim/Helper/FlutterNIMHelper.java: -------------------------------------------------------------------------------- 1 | package cn.cgm.flutter_nim.Helper; 2 | 3 | import android.content.Context; 4 | import android.text.TextUtils; 5 | import android.util.Log; 6 | 7 | import com.netease.nimlib.sdk.NIMClient; 8 | import com.netease.nimlib.sdk.Observer; 9 | import com.netease.nimlib.sdk.RequestCallback; 10 | import com.netease.nimlib.sdk.ResponseCode; 11 | import com.netease.nimlib.sdk.auth.AuthService; 12 | import com.netease.nimlib.sdk.auth.LoginInfo; 13 | import com.netease.nimlib.sdk.mixpush.MixPushService; 14 | import com.netease.nimlib.sdk.msg.MsgService; 15 | import com.netease.nimlib.sdk.msg.MsgServiceObserve; 16 | import com.netease.nimlib.sdk.msg.model.CustomNotification; 17 | import com.netease.nimlib.sdk.util.NIMUtil; 18 | 19 | public class FlutterNIMHelper { 20 | 21 | public interface IMHelperNotificationCallback { 22 | void onEvent(CustomNotification message); 23 | } 24 | 25 | public interface IMLoginCallback { 26 | void onResult(boolean isSuccess); 27 | } 28 | 29 | private static IMHelperNotificationCallback imHelperNotificationCallback; 30 | 31 | // singleton 32 | private static FlutterNIMHelper instance; 33 | 34 | public static synchronized FlutterNIMHelper getInstance() { 35 | if (instance == null) { 36 | instance = new FlutterNIMHelper(); 37 | } 38 | 39 | return instance; 40 | } 41 | 42 | private FlutterNIMHelper() { 43 | registerObservers(true); 44 | } 45 | 46 | 47 | public void registerNotificationCallback(IMHelperNotificationCallback cb) { 48 | imHelperNotificationCallback = cb; 49 | } 50 | 51 | public static void initIM(Context context) { 52 | if (NIMUtil.isMainProcess(context)) { 53 | // 注册自定义消息附件解析器 54 | NIMClient.getService(MsgService.class).registerCustomAttachmentParser(new FlutterNIMCustomAttachParser()); 55 | 56 | setMessageNotify(true); 57 | } 58 | } 59 | 60 | /** 61 | * IM登录 62 | */ 63 | public void doIMLogin(String account, String token, final IMLoginCallback loginCallback) { 64 | final String imAccount = account.toLowerCase(); 65 | final String imToken = token.toLowerCase(); 66 | 67 | LoginInfo info = new LoginInfo(imAccount, imToken); 68 | 69 | RequestCallback callback = 70 | new RequestCallback() { 71 | @Override 72 | public void onSuccess(LoginInfo param) { 73 | saveLoginInfo(imAccount, imToken); 74 | 75 | loginCallback.onResult(true); 76 | } 77 | 78 | @Override 79 | public void onFailed(int code) { 80 | Log.e("FlutterNIM", "im login failure" + code); 81 | 82 | loginCallback.onResult(false); 83 | } 84 | 85 | @Override 86 | public void onException(Throwable exception) { 87 | Log.e("FlutterNIM", "im login error"); 88 | } 89 | }; 90 | 91 | NIMClient.getService(AuthService.class).login(info) 92 | .setCallback(callback); 93 | } 94 | 95 | private static LoginInfo getLoginInfo(String account, String token) { 96 | if (!TextUtils.isEmpty(account) && !TextUtils.isEmpty(token)) { 97 | return new LoginInfo(account, token); 98 | } else { 99 | return null; 100 | } 101 | } 102 | 103 | private static void saveLoginInfo(final String account, final String token) { 104 | FlutterNIMPreferences.saveUserAccount(account); 105 | FlutterNIMPreferences.saveUserToken(token); 106 | } 107 | 108 | /** 109 | * IM登出 110 | */ 111 | public void doIMLogout() { 112 | NIMClient.getService(AuthService.class).logout(); 113 | } 114 | 115 | 116 | /** 117 | * ********************** 收消息,处理状态变化 ************************ 118 | */ 119 | private void registerObservers(boolean register) { 120 | MsgServiceObserve service = NIMClient.getService(MsgServiceObserve.class); 121 | 122 | // 监听自定义通知 123 | service.observeCustomNotification(customNotificationObserver, register); 124 | } 125 | 126 | // 接收自定义通知 127 | Observer customNotificationObserver = new Observer() { 128 | @Override 129 | public void onEvent(CustomNotification message) { 130 | // 在这里处理自定义通知。 131 | if (imHelperNotificationCallback != null) { 132 | imHelperNotificationCallback.onEvent(message); 133 | } 134 | } 135 | }; 136 | 137 | 138 | /** 139 | * **************************** 推送 *********************************** 140 | */ 141 | private static void setMessageNotify(final boolean checkState) { 142 | // 如果接入第三方推送(小米),则同样应该设置开、关推送提醒 143 | // 如果关闭消息提醒,则第三方推送消息提醒也应该关闭。 144 | // 如果打开消息提醒,则同时打开第三方推送消息提醒。 145 | NIMClient.getService(MixPushService.class).enable(checkState).setCallback(new RequestCallback() { 146 | @Override 147 | public void onSuccess(Void param) { 148 | // Toast.makeText(SettingsActivity.this, R.string.user_info_update_success, Toast.LENGTH_SHORT).show(); 149 | // notificationItem.setChecked(checkState); 150 | // setToggleNotification(checkState); 151 | } 152 | 153 | @Override 154 | public void onFailed(int code) { 155 | // notificationItem.setChecked(!checkState); 156 | // 这种情况是客户端不支持第三方推送 157 | if (code == ResponseCode.RES_UNSUPPORT) { 158 | } else if (code == ResponseCode.RES_EFREQUENTLY) { 159 | // Toast.makeText(SettingsActivity.this, R.string.operation_too_frequent, Toast.LENGTH_SHORT).show(); 160 | } else { 161 | // Toast.makeText(SettingsActivity.this, R.string.user_info_update_failed, Toast.LENGTH_SHORT).show(); 162 | } 163 | // adapter.notifyDataSetChanged(); 164 | } 165 | 166 | @Override 167 | public void onException(Throwable exception) { 168 | 169 | } 170 | }); 171 | } 172 | } 173 | -------------------------------------------------------------------------------- /ios/Classes/Helper/NIMSessionUtil.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NIMSessionUtil.swift 3 | // flutter_nim 4 | // 5 | // Created by GuiminChu on 2019/7/19. 6 | // 7 | // Copyright (c) 2019 GuiminChu 8 | // 9 | // Permission is hereby granted, free of charge, to any person obtaining a copy 10 | // of this software and associated documentation files (the "Software"), to deal 11 | // in the Software without restriction, including without limitation the rights 12 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | // copies of the Software, and to permit persons to whom the Software is 14 | // furnished to do so, subject to the following conditions: 15 | // 16 | // The above copyright notice and this permission notice shall be included in 17 | // all copies or substantial portions of the Software. 18 | // 19 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 25 | // THE SOFTWARE. 26 | 27 | import Foundation 28 | import NIMSDK 29 | 30 | // 最近会话本地扩展标记类型 31 | enum NIMRecentSessionMarkType: String { 32 | // @ 标记 33 | case markTypeAt 34 | // 置顶标记 35 | case markTypeTop 36 | } 37 | 38 | struct NIMSessionUtil { 39 | 40 | /// 判断消息是否可以转发 41 | static func canMessageBeForwarded(_ message: NIMMessage) -> Bool { 42 | if message.isReceivedMsg || message.deliveryState == NIMMessageDeliveryState.failed { 43 | return false 44 | } 45 | 46 | if let messageObject = message.messageObject { 47 | 48 | if let object = messageObject as? NIMCustomObject, let attachment = object.attachment as? IMCustomAttachment { 49 | return attachment.canBeForwarded() 50 | } 51 | 52 | if messageObject is NIMNotificationObject { 53 | return false 54 | } 55 | 56 | if messageObject is NIMTipObject { 57 | return false 58 | } 59 | 60 | if messageObject is NIMRobotObject { 61 | return false 62 | } 63 | } 64 | 65 | return true 66 | } 67 | 68 | /// 判断消息是否可以撤回 69 | static func canMessageBeRevoked(_ message: NIMMessage) -> Bool { 70 | let canRevokeMessageByRole = self.canRevokeMessageByRole(message) 71 | let isDeliverFailed = !message.isReceivedMsg && message.deliveryState == NIMMessageDeliveryState.failed 72 | 73 | if !canRevokeMessageByRole || isDeliverFailed { 74 | return false 75 | } 76 | 77 | if let messageObject = message.messageObject { 78 | 79 | if let object = messageObject as? NIMCustomObject, let attachment = object.attachment as? IMCustomAttachment { 80 | return attachment.canBeRevoked() 81 | } 82 | 83 | if messageObject is NIMNotificationObject { 84 | return false 85 | } 86 | 87 | if messageObject is NIMTipObject { 88 | return false 89 | } 90 | 91 | if messageObject is NIMRobotObject { 92 | return false 93 | } 94 | } 95 | 96 | return true 97 | } 98 | 99 | /// 判断撤回规则 100 | static func canRevokeMessageByRole(_ message: NIMMessage) -> Bool { 101 | let isFromMe = message.from! == NIMSDK.shared().loginManager.currentAccount() 102 | let isToMe = message.session!.sessionId == NIMSDK.shared().loginManager.currentAccount() 103 | let isTeamManager = false 104 | let isRobotMessage = false 105 | 106 | // 我发出去的消息并且不是发给我的电脑的消息并且不是机器人的消息,可以撤回 107 | // 群消息里如果我是管理员可以撤回以上所有消息(暂未判断) 108 | return (isFromMe && !isToMe && !isRobotMessage) || isTeamManager 109 | } 110 | 111 | static func tipOnMessageRevoked(_ notification: NIMRevokeMessageNotification?) -> String { 112 | var tip = "" 113 | 114 | if let notification = notification { 115 | let session = notification.session 116 | if session.sessionType == NIMSessionType.team { 117 | // TODO: 群组撤回 118 | } else { 119 | tip = tipTitleFromMessageRevokeNotificationP2P(notification) 120 | } 121 | } else { 122 | tip = "你" 123 | } 124 | 125 | return "\(tip)撤回了一条消息" 126 | } 127 | 128 | static func tipTitleFromMessageRevokeNotificationP2P(_ notification: NIMRevokeMessageNotification) -> String { 129 | let fromUid = notification.messageFromUserId 130 | if fromUid == NIMSDK.shared().loginManager.currentAccount() { 131 | return "你" 132 | } else { 133 | return "对方" 134 | } 135 | } 136 | 137 | // 添加标记 138 | static func addRecentSessionMark(_ session: NIMSession, type: NIMRecentSessionMarkType) { 139 | guard let recent = NIMSDK.shared().conversationManager.recentSession(by: session) else { 140 | return 141 | } 142 | 143 | var localExt = recent.localExt ?? [:] 144 | localExt[type.rawValue] = true 145 | NIMSDK.shared().conversationManager.updateRecentLocalExt(localExt, recentSession: recent) 146 | } 147 | 148 | // 移除标记 149 | static func removeRecentSessionMark(_ session: NIMSession, type: NIMRecentSessionMarkType) { 150 | guard let recent = NIMSDK.shared().conversationManager.recentSession(by: session) else { 151 | return 152 | } 153 | 154 | var localExt = recent.localExt 155 | localExt?.removeValue(forKey: type.rawValue) 156 | NIMSDK.shared().conversationManager.updateRecentLocalExt(localExt, recentSession: recent) 157 | } 158 | // 判断是否标记过某种类型 159 | static func isRecentSessionMark(_ session: NIMRecentSession, type: NIMRecentSessionMarkType) -> Bool { 160 | let localExt = session.localExt 161 | if let value = localExt?[type.rawValue] as? Bool { 162 | return value 163 | } else { 164 | return false 165 | } 166 | } 167 | 168 | } 169 | -------------------------------------------------------------------------------- /example/lib/ui/page_recent_sessions.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_nim/flutter_nim.dart'; 3 | 4 | import 'package:flutter_nim_example/provider/provider.dart'; 5 | import 'package:flutter_nim_example/ui/page_login.dart'; 6 | import 'package:flutter_nim_example/utils/user_utils.dart'; 7 | import 'package:flutter_nim_example/zeus_kit/zeus_kit.dart'; 8 | import 'package:flutter_nim_example/ui/page_chat.dart'; 9 | 10 | class MyApp extends StatelessWidget { 11 | @override 12 | Widget build(BuildContext context) { 13 | return MultiProvider( 14 | providers: [ 15 | ChangeNotifierProvider(create: (_) => NIMProvider()), 16 | ChangeNotifierProvider(create: (_) => ChatProvider()), 17 | ], 18 | child: MaterialApp( 19 | home: RecentSessionsPage(), 20 | ), 21 | ); 22 | } 23 | } 24 | 25 | /// 最近会话 26 | class RecentSessionsPage extends StatefulWidget { 27 | @override 28 | _RecentSessionsPageState createState() => _RecentSessionsPageState(); 29 | } 30 | 31 | class _RecentSessionsPageState extends State { 32 | @override 33 | void initState() { 34 | super.initState(); 35 | } 36 | 37 | @override 38 | Widget build(BuildContext context) { 39 | return MaterialApp( 40 | home: Scaffold( 41 | appBar: AppBar( 42 | title: const Text("消息"), 43 | centerTitle: true, 44 | elevation: 0.5, 45 | actions: _buildAppBarActions(), 46 | ), 47 | body: _buildBodyWidget(), 48 | ), 49 | ); 50 | } 51 | 52 | List _buildAppBarActions() { 53 | return [ 54 | GestureDetector( 55 | onTap: () { 56 | FlutterNIM().logout(); 57 | 58 | UserUtils.clearLoginInfo(); 59 | 60 | ZKRouter.pushWidget( 61 | context, 62 | LoginHomePage(), 63 | replaceCurrent: true, 64 | ); 65 | }, 66 | child: Container( 67 | padding: const EdgeInsets.only(left: 16.0, right: 16.0), 68 | alignment: Alignment.center, 69 | child: Text( 70 | "登出", 71 | style: TextStyle( 72 | fontSize: 15.0, 73 | ), 74 | ), 75 | ), 76 | ) 77 | ]; 78 | } 79 | 80 | Widget _buildBodyWidget() { 81 | return Container( 82 | color: Colors.white, 83 | child: _buildListView(), 84 | ); 85 | } 86 | 87 | Widget _buildListView() { 88 | return Consumer(builder: (context, provider, _) { 89 | return Stack( 90 | children: [ 91 | Offstage( 92 | offstage: provider.recentSessions.isEmpty, 93 | child: ListView.builder( 94 | itemCount: provider.recentSessions.length, 95 | itemBuilder: (context, index) { 96 | return _IMRecentSessionListItem( 97 | recentSession: provider.recentSessions[index]); 98 | }, 99 | ), 100 | ), 101 | Offstage( 102 | offstage: provider.recentSessions.isNotEmpty, 103 | child: Text("暂时没有会话嗷!"), 104 | ), 105 | ], 106 | ); 107 | }); 108 | } 109 | } 110 | 111 | class _IMRecentSessionListItem extends StatelessWidget { 112 | final NIMRecentSession recentSession; 113 | 114 | _IMRecentSessionListItem({ 115 | Key key, 116 | @required this.recentSession, 117 | }) : super(key: key); 118 | 119 | @override 120 | Widget build(BuildContext context) { 121 | return GestureDetector( 122 | onTap: () async { 123 | bool isSuccess = 124 | await FlutterNIM().startChat(recentSession.sessionId.toLowerCase()); 125 | if (isSuccess) { 126 | ZKRouter.pushWidget( 127 | context, 128 | Chat( 129 | sessionId: recentSession.sessionId, 130 | chatName: recentSession.userInfo?.nickname ?? "", 131 | ), 132 | ); 133 | } 134 | }, 135 | child: Container( 136 | padding: const EdgeInsets.all(15.0), 137 | decoration: BoxDecoration( 138 | color: Colors.white, 139 | border: Border( 140 | bottom: BorderSide( 141 | width: 0.0, 142 | color: Theme.of(context).dividerColor, 143 | ), 144 | ), 145 | ), 146 | child: Row( 147 | children: [ 148 | ZKCircleAvatar( 149 | avatarUrl: recentSession.userInfo?.avatarUrl ?? "", 150 | size: 36.0, 151 | defaultAvatar: "images/default_user_avatar.png", 152 | ), 153 | Expanded( 154 | child: Container( 155 | margin: const EdgeInsets.only(left: 12.0), 156 | child: Column( 157 | mainAxisAlignment: MainAxisAlignment.spaceEvenly, 158 | crossAxisAlignment: CrossAxisAlignment.start, 159 | children: [ 160 | Row( 161 | children: [ 162 | Expanded( 163 | child: Text( 164 | recentSession.userInfo?.nickname ?? "无名氏", 165 | style: TextStyle( 166 | color: ZKColors.text_dark, 167 | fontSize: 15.0, 168 | fontWeight: FontWeight.w500, 169 | ), 170 | overflow: TextOverflow.ellipsis, 171 | ), 172 | ), 173 | Text( 174 | ZKDateUtil.timestampToYMDHM(recentSession.timestamp), 175 | style: TextStyle( 176 | color: ZKColors.text_gray, 177 | fontSize: 14.0, 178 | ), 179 | ), 180 | ], 181 | ), 182 | Padding(padding: EdgeInsets.symmetric(vertical: 1.0)), 183 | Row( 184 | children: [ 185 | // 最后消息文本 186 | Expanded( 187 | child: Text( 188 | recentSession.messageContent, 189 | style: TextStyle( 190 | color: ZKColors.text_gray, 191 | fontSize: 13.0, 192 | ), 193 | maxLines: 1, 194 | overflow: TextOverflow.ellipsis, 195 | ), 196 | ), 197 | 198 | // 未读数 199 | Offstage( 200 | offstage: recentSession.unreadCount == 0, 201 | child: Container( 202 | child: Center( 203 | child: Text( 204 | "${recentSession.unreadCount}", 205 | style: TextStyle( 206 | color: Colors.white, 207 | fontSize: 12.0, 208 | ), 209 | ), 210 | ), 211 | decoration: BoxDecoration( 212 | color: Colors.red, 213 | borderRadius: BorderRadius.circular(9.0), 214 | ), 215 | constraints: BoxConstraints( 216 | minWidth: 18.0, 217 | minHeight: 18.0, 218 | ), 219 | ), 220 | ), 221 | ], 222 | ), 223 | ], 224 | ), 225 | ), 226 | ) 227 | ], 228 | ), 229 | ), 230 | ); 231 | } 232 | } 233 | -------------------------------------------------------------------------------- /android/src/main/java/cn/cgm/flutter_nim/Helper/NIMRecentSessionsInteractor.java: -------------------------------------------------------------------------------- 1 | package cn.cgm.flutter_nim.Helper; 2 | 3 | import android.text.TextUtils; 4 | 5 | import com.netease.nimlib.sdk.NIMClient; 6 | import com.netease.nimlib.sdk.Observer; 7 | import com.netease.nimlib.sdk.RequestCallback; 8 | import com.netease.nimlib.sdk.RequestCallbackWrapper; 9 | import com.netease.nimlib.sdk.msg.MsgService; 10 | import com.netease.nimlib.sdk.msg.MsgServiceObserve; 11 | import com.netease.nimlib.sdk.msg.constant.SessionTypeEnum; 12 | import com.netease.nimlib.sdk.msg.model.RecentContact; 13 | import com.netease.nimlib.sdk.uinfo.UserService; 14 | import com.netease.nimlib.sdk.uinfo.model.NimUserInfo; 15 | 16 | import java.util.ArrayList; 17 | import java.util.Collections; 18 | import java.util.Comparator; 19 | import java.util.List; 20 | 21 | import io.flutter.plugin.common.EventChannel.EventSink; 22 | 23 | public class NIMRecentSessionsInteractor { 24 | private EventSink eventSink; 25 | 26 | private List recentSessions = new ArrayList<>(); 27 | private List recentSessionUserIds; 28 | 29 | public NIMRecentSessionsInteractor(EventSink eventSink) { 30 | 31 | this.eventSink = eventSink; 32 | 33 | registerObservers(true); 34 | } 35 | 36 | 37 | private void registerObservers(boolean register) { 38 | MsgServiceObserve service = NIMClient.getService(MsgServiceObserve.class); 39 | 40 | service.observeRecentContact(recentContactsObserver, register); 41 | service.observeRecentContactDeleted(recentContactDeletedObserver, register); 42 | } 43 | 44 | /** 45 | * 最近联系人列表变化观察者 46 | */ 47 | private Observer> recentContactsObserver = new Observer>() { 48 | @Override 49 | public void onEvent(List recentContacts) { 50 | if (recentContacts == null || recentContacts.isEmpty()) { 51 | return; 52 | } 53 | 54 | onRecentContactChanged(recentContacts); 55 | } 56 | }; 57 | 58 | 59 | private void onRecentContactChanged(List recentContacts) { 60 | if (recentSessions == null) { 61 | recentSessions = new ArrayList<>(); 62 | } 63 | 64 | if (recentSessionUserIds == null) { 65 | recentSessionUserIds = new ArrayList<>(); 66 | } else { 67 | recentSessionUserIds.clear(); 68 | } 69 | 70 | // 记录用户信息是否有本地缓存 71 | boolean hasNullUser = false; 72 | 73 | int index; 74 | for (RecentContact r : recentContacts) { 75 | index = -1; 76 | for (int i = 0; i < recentSessions.size(); i++) { 77 | if (r.getContactId().equals(recentSessions.get(i).getContactId()) 78 | && r.getSessionType() == (recentSessions.get(i).getSessionType())) { 79 | index = i; 80 | break; 81 | } 82 | } 83 | 84 | if (index >= 0) { 85 | recentSessions.remove(index); 86 | } 87 | 88 | recentSessions.add(r); 89 | 90 | // 获取本地用户资料 91 | NimUserInfo userInfo = NIMClient.getService(UserService.class).getUserInfo(r.getContactId()); 92 | 93 | if (userInfo == null) { 94 | recentSessionUserIds.add(r.getContactId()); 95 | hasNullUser = true; 96 | } 97 | } 98 | 99 | // 本地无这些用户资料,从云端拉取一下 100 | if (hasNullUser) { 101 | NIMClient.getService(UserService.class).fetchUserInfo(recentSessionUserIds).setCallback(new RequestCallback>() { 102 | @Override 103 | public void onSuccess(List param) { 104 | refreshMessages(); 105 | } 106 | 107 | @Override 108 | public void onFailed(int code) { 109 | refreshMessages(); 110 | } 111 | 112 | @Override 113 | public void onException(Throwable exception) { 114 | 115 | } 116 | }); 117 | } else { 118 | refreshMessages(); 119 | } 120 | } 121 | 122 | private Observer recentContactDeletedObserver = new Observer() { 123 | @Override 124 | public void onEvent(RecentContact recentContact) { 125 | if (recentContact != null) { 126 | for (RecentContact item : recentSessions) { 127 | if (TextUtils.equals(item.getContactId(), recentContact.getContactId()) 128 | && item.getSessionType() == recentContact.getSessionType()) { 129 | recentSessions.remove(item); 130 | refreshMessages(); 131 | break; 132 | } 133 | } 134 | } else { 135 | recentSessions.clear(); 136 | refreshMessages(); 137 | } 138 | } 139 | }; 140 | 141 | 142 | private void refreshMessages() { 143 | sortRecentContacts(recentSessions); 144 | 145 | // 主动给 flutter 发消息 146 | if (eventSink != null) { 147 | eventSink.success(NIMSessionParser.handleRecentSessionsData(recentSessions)); 148 | } 149 | 150 | } 151 | 152 | /// 手动获取最近会话列表 153 | public void loadRecentSessions() { 154 | NIMClient.getService(MsgService.class).queryRecentContacts() 155 | .setCallback(new RequestCallbackWrapper>() { 156 | @Override 157 | public void onResult(int code, List recents, Throwable e) { 158 | recentSessions = recents; 159 | 160 | if (recentSessions == null || recentSessions.isEmpty()) { 161 | return; 162 | } 163 | 164 | if (recentSessionUserIds == null) { 165 | recentSessionUserIds = new ArrayList<>(); 166 | } else { 167 | recentSessionUserIds.clear(); 168 | } 169 | 170 | for (RecentContact r : recentSessions) { 171 | recentSessionUserIds.add(r.getContactId()); 172 | } 173 | 174 | // 手动获取最近会话不是频繁操作,可以在云端拉取一次用户信息 175 | NIMClient.getService(UserService.class).fetchUserInfo(recentSessionUserIds).setCallback(new RequestCallback>() { 176 | @Override 177 | public void onSuccess(List param) { 178 | refreshMessages(); 179 | } 180 | 181 | @Override 182 | public void onFailed(int code) { 183 | refreshMessages(); 184 | } 185 | 186 | @Override 187 | public void onException(Throwable exception) { 188 | 189 | } 190 | }); 191 | 192 | } 193 | }); 194 | } 195 | 196 | 197 | private int getItemIndex(String uuid) { 198 | for (int i = 0; i < recentSessions.size(); i++) { 199 | RecentContact item = recentSessions.get(i); 200 | if (TextUtils.equals(item.getRecentMessageId(), uuid)) { 201 | return i; 202 | } 203 | } 204 | 205 | return -1; 206 | } 207 | 208 | public void deleteRecentContact2(String account) { 209 | NIMClient.getService(MsgService.class).deleteRecentContact2(account, SessionTypeEnum.P2P); 210 | } 211 | 212 | /** 213 | * **************************** 排序 *********************************** 214 | */ 215 | 216 | // 置顶功能可直接使用,也可作为思路,供开发者充分利用RecentContact的tag字段 217 | // 联系人置顶tag 218 | private final long RECENT_TAG_STICKY = 0x0000000000000001; 219 | 220 | private void sortRecentContacts(List list) { 221 | if (list == null || list.isEmpty()) { 222 | return; 223 | } 224 | 225 | Collections.sort(list, comp); 226 | } 227 | 228 | private Comparator comp = new Comparator() { 229 | 230 | @Override 231 | public int compare(RecentContact o1, RecentContact o2) { 232 | // 先比较置顶tag 233 | long sticky = (o1.getTag() & RECENT_TAG_STICKY) - (o2.getTag() & RECENT_TAG_STICKY); 234 | if (sticky != 0) { 235 | return sticky > 0 ? -1 : 1; 236 | } else { 237 | long time = o1.getTime() - o2.getTime(); 238 | return time == 0 ? 0 : (time > 0 ? -1 : 1); 239 | } 240 | } 241 | }; 242 | 243 | } 244 | -------------------------------------------------------------------------------- /example/pubspec.lock: -------------------------------------------------------------------------------- 1 | # Generated by pub 2 | # See https://dart.dev/tools/pub/glossary#lockfile 3 | packages: 4 | archive: 5 | dependency: transitive 6 | description: 7 | name: archive 8 | url: "https://pub.flutter-io.cn" 9 | source: hosted 10 | version: "2.0.11" 11 | args: 12 | dependency: transitive 13 | description: 14 | name: args 15 | url: "https://pub.flutter-io.cn" 16 | source: hosted 17 | version: "1.5.2" 18 | async: 19 | dependency: transitive 20 | description: 21 | name: async 22 | url: "https://pub.flutter-io.cn" 23 | source: hosted 24 | version: "2.4.0" 25 | audioplayers: 26 | dependency: "direct main" 27 | description: 28 | name: audioplayers 29 | url: "https://pub.flutter-io.cn" 30 | source: hosted 31 | version: "0.14.0" 32 | boolean_selector: 33 | dependency: transitive 34 | description: 35 | name: boolean_selector 36 | url: "https://pub.flutter-io.cn" 37 | source: hosted 38 | version: "1.0.5" 39 | cached_network_image: 40 | dependency: "direct main" 41 | description: 42 | name: cached_network_image 43 | url: "https://pub.flutter-io.cn" 44 | source: hosted 45 | version: "2.0.0" 46 | charcode: 47 | dependency: transitive 48 | description: 49 | name: charcode 50 | url: "https://pub.flutter-io.cn" 51 | source: hosted 52 | version: "1.1.2" 53 | collection: 54 | dependency: transitive 55 | description: 56 | name: collection 57 | url: "https://pub.flutter-io.cn" 58 | source: hosted 59 | version: "1.14.11" 60 | convert: 61 | dependency: transitive 62 | description: 63 | name: convert 64 | url: "https://pub.flutter-io.cn" 65 | source: hosted 66 | version: "2.1.1" 67 | crypto: 68 | dependency: transitive 69 | description: 70 | name: crypto 71 | url: "https://pub.flutter-io.cn" 72 | source: hosted 73 | version: "2.1.3" 74 | cupertino_icons: 75 | dependency: "direct main" 76 | description: 77 | name: cupertino_icons 78 | url: "https://pub.flutter-io.cn" 79 | source: hosted 80 | version: "0.1.3" 81 | flutter: 82 | dependency: "direct main" 83 | description: flutter 84 | source: sdk 85 | version: "0.0.0" 86 | flutter_cache_manager: 87 | dependency: transitive 88 | description: 89 | name: flutter_cache_manager 90 | url: "https://pub.flutter-io.cn" 91 | source: hosted 92 | version: "1.1.3" 93 | flutter_easyrefresh: 94 | dependency: "direct main" 95 | description: 96 | name: flutter_easyrefresh 97 | url: "https://pub.flutter-io.cn" 98 | source: hosted 99 | version: "2.0.9" 100 | flutter_nim: 101 | dependency: "direct dev" 102 | description: 103 | path: ".." 104 | relative: true 105 | source: path 106 | version: "0.1.7" 107 | flutter_plugin_android_lifecycle: 108 | dependency: transitive 109 | description: 110 | name: flutter_plugin_android_lifecycle 111 | url: "https://pub.flutter-io.cn" 112 | source: hosted 113 | version: "1.0.5" 114 | flutter_test: 115 | dependency: "direct dev" 116 | description: flutter 117 | source: sdk 118 | version: "0.0.0" 119 | flutter_web_plugins: 120 | dependency: transitive 121 | description: flutter 122 | source: sdk 123 | version: "0.0.0" 124 | fluttertoast: 125 | dependency: "direct main" 126 | description: 127 | name: fluttertoast 128 | url: "https://pub.flutter-io.cn" 129 | source: hosted 130 | version: "3.1.3" 131 | http: 132 | dependency: transitive 133 | description: 134 | name: http 135 | url: "https://pub.flutter-io.cn" 136 | source: hosted 137 | version: "0.12.0+2" 138 | http_parser: 139 | dependency: transitive 140 | description: 141 | name: http_parser 142 | url: "https://pub.flutter-io.cn" 143 | source: hosted 144 | version: "3.1.3" 145 | image: 146 | dependency: transitive 147 | description: 148 | name: image 149 | url: "https://pub.flutter-io.cn" 150 | source: hosted 151 | version: "2.1.4" 152 | image_picker: 153 | dependency: "direct main" 154 | description: 155 | name: image_picker 156 | url: "https://pub.flutter-io.cn" 157 | source: hosted 158 | version: "0.6.3+1" 159 | keyboard_visibility: 160 | dependency: "direct main" 161 | description: 162 | name: keyboard_visibility 163 | url: "https://pub.flutter-io.cn" 164 | source: hosted 165 | version: "0.5.6" 166 | matcher: 167 | dependency: transitive 168 | description: 169 | name: matcher 170 | url: "https://pub.flutter-io.cn" 171 | source: hosted 172 | version: "0.12.6" 173 | meta: 174 | dependency: transitive 175 | description: 176 | name: meta 177 | url: "https://pub.flutter-io.cn" 178 | source: hosted 179 | version: "1.1.8" 180 | nested: 181 | dependency: transitive 182 | description: 183 | name: nested 184 | url: "https://pub.flutter-io.cn" 185 | source: hosted 186 | version: "0.0.4" 187 | path: 188 | dependency: transitive 189 | description: 190 | name: path 191 | url: "https://pub.flutter-io.cn" 192 | source: hosted 193 | version: "1.6.4" 194 | path_provider: 195 | dependency: transitive 196 | description: 197 | name: path_provider 198 | url: "https://pub.flutter-io.cn" 199 | source: hosted 200 | version: "1.6.0" 201 | pedantic: 202 | dependency: transitive 203 | description: 204 | name: pedantic 205 | url: "https://pub.flutter-io.cn" 206 | source: hosted 207 | version: "1.8.0+1" 208 | permission_handler: 209 | dependency: "direct main" 210 | description: 211 | name: permission_handler 212 | url: "https://pub.flutter-io.cn" 213 | source: hosted 214 | version: "4.2.0+hotfix.3" 215 | petitparser: 216 | dependency: transitive 217 | description: 218 | name: petitparser 219 | url: "https://pub.flutter-io.cn" 220 | source: hosted 221 | version: "2.4.0" 222 | platform: 223 | dependency: transitive 224 | description: 225 | name: platform 226 | url: "https://pub.flutter-io.cn" 227 | source: hosted 228 | version: "2.2.1" 229 | provider: 230 | dependency: "direct main" 231 | description: 232 | name: provider 233 | url: "https://pub.flutter-io.cn" 234 | source: hosted 235 | version: "4.0.2" 236 | quiver: 237 | dependency: transitive 238 | description: 239 | name: quiver 240 | url: "https://pub.flutter-io.cn" 241 | source: hosted 242 | version: "2.0.5" 243 | shared_preferences: 244 | dependency: "direct main" 245 | description: 246 | name: shared_preferences 247 | url: "https://pub.flutter-io.cn" 248 | source: hosted 249 | version: "0.5.6+1" 250 | shared_preferences_macos: 251 | dependency: transitive 252 | description: 253 | name: shared_preferences_macos 254 | url: "https://pub.flutter-io.cn" 255 | source: hosted 256 | version: "0.0.1+3" 257 | shared_preferences_platform_interface: 258 | dependency: transitive 259 | description: 260 | name: shared_preferences_platform_interface 261 | url: "https://pub.flutter-io.cn" 262 | source: hosted 263 | version: "1.0.1" 264 | shared_preferences_web: 265 | dependency: transitive 266 | description: 267 | name: shared_preferences_web 268 | url: "https://pub.flutter-io.cn" 269 | source: hosted 270 | version: "0.1.2+2" 271 | sky_engine: 272 | dependency: transitive 273 | description: flutter 274 | source: sdk 275 | version: "0.0.99" 276 | source_span: 277 | dependency: transitive 278 | description: 279 | name: source_span 280 | url: "https://pub.flutter-io.cn" 281 | source: hosted 282 | version: "1.5.5" 283 | sqflite: 284 | dependency: transitive 285 | description: 286 | name: sqflite 287 | url: "https://pub.flutter-io.cn" 288 | source: hosted 289 | version: "1.1.7+1" 290 | stack_trace: 291 | dependency: transitive 292 | description: 293 | name: stack_trace 294 | url: "https://pub.flutter-io.cn" 295 | source: hosted 296 | version: "1.9.3" 297 | stream_channel: 298 | dependency: transitive 299 | description: 300 | name: stream_channel 301 | url: "https://pub.flutter-io.cn" 302 | source: hosted 303 | version: "2.0.0" 304 | string_scanner: 305 | dependency: transitive 306 | description: 307 | name: string_scanner 308 | url: "https://pub.flutter-io.cn" 309 | source: hosted 310 | version: "1.0.5" 311 | synchronized: 312 | dependency: transitive 313 | description: 314 | name: synchronized 315 | url: "https://pub.flutter-io.cn" 316 | source: hosted 317 | version: "2.1.0+1" 318 | term_glyph: 319 | dependency: transitive 320 | description: 321 | name: term_glyph 322 | url: "https://pub.flutter-io.cn" 323 | source: hosted 324 | version: "1.1.0" 325 | test_api: 326 | dependency: transitive 327 | description: 328 | name: test_api 329 | url: "https://pub.flutter-io.cn" 330 | source: hosted 331 | version: "0.2.11" 332 | typed_data: 333 | dependency: transitive 334 | description: 335 | name: typed_data 336 | url: "https://pub.flutter-io.cn" 337 | source: hosted 338 | version: "1.1.6" 339 | uuid: 340 | dependency: transitive 341 | description: 342 | name: uuid 343 | url: "https://pub.flutter-io.cn" 344 | source: hosted 345 | version: "2.0.4" 346 | vector_math: 347 | dependency: transitive 348 | description: 349 | name: vector_math 350 | url: "https://pub.flutter-io.cn" 351 | source: hosted 352 | version: "2.0.8" 353 | xml: 354 | dependency: transitive 355 | description: 356 | name: xml 357 | url: "https://pub.flutter-io.cn" 358 | source: hosted 359 | version: "3.5.0" 360 | sdks: 361 | dart: ">=2.5.0 <3.0.0" 362 | flutter: ">=1.12.13+hotfix.4 <2.0.0" 363 | -------------------------------------------------------------------------------- /android/src/main/java/cn/cgm/flutter_nim/FlutterNimPlugin.java: -------------------------------------------------------------------------------- 1 | package cn.cgm.flutter_nim; 2 | 3 | import android.app.Application; 4 | 5 | import cn.cgm.flutter_nim.Helper.FlutterNIMHelper; 6 | import cn.cgm.flutter_nim.Helper.FlutterNIMPreferences; 7 | import cn.cgm.flutter_nim.Helper.NIMKickoutInteractor; 8 | import cn.cgm.flutter_nim.Helper.NIMRecentSessionsInteractor; 9 | import cn.cgm.flutter_nim.Helper.NIMSessionInteractor; 10 | import io.flutter.plugin.common.EventChannel; 11 | import io.flutter.plugin.common.MethodCall; 12 | import io.flutter.plugin.common.MethodChannel; 13 | import io.flutter.plugin.common.MethodChannel.MethodCallHandler; 14 | import io.flutter.plugin.common.MethodChannel.Result; 15 | import io.flutter.plugin.common.PluginRegistry; 16 | import io.flutter.plugin.common.PluginRegistry.Registrar; 17 | 18 | /** 19 | * FlutterNimPlugin 20 | *

21 | * 22 | * @author chuguimin 23 | */ 24 | public class FlutterNimPlugin implements MethodCallHandler, EventChannel.StreamHandler { 25 | private static final String METHOD_CHANNEL_NAME = "flutter_nim_method"; 26 | private static final String EVENT_CHANNEL_NAME = "flutter_nim_event"; 27 | 28 | private static final String METHOD_IM_INIT = "imInit"; 29 | private static final String METHOD_IM_LOGIN = "imLogin"; 30 | private static final String METHOD_IM_LOGOUT = "imLogout"; 31 | private static final String METHOD_IM_RECENT_SESSIONS = "imRecentSessions"; 32 | private static final String METHOD_IM_DELETE_RECENT_SESSION = "imDeleteRecentSession"; 33 | private static final String METHOD_IM_START_CHAT = "imStartChat"; 34 | private static final String METHOD_IM_EXIT_CHAT = "imExitChat"; 35 | private static final String METHOD_IM_MESSAGES = "imMessages"; 36 | private static final String METHOD_IM_SEND_TEXT = "imSendText"; 37 | private static final String METHOD_IM_SEND_IMAGE = "imSendImage"; 38 | private static final String METHOD_IM_SEND_VIDEO = "imSendVideo"; 39 | private static final String METHOD_IM_SEND_AUDIO = "imSendAudio"; 40 | private static final String METHOD_IM_SEND_CUSTOM = "imSendCustom"; 41 | private static final String METHOD_IM_SEND_CUSTOM_2 = "imSendCustomToSession"; 42 | private static final String METHOD_IM_RESEND_MESSAGE = "imResendMessage"; 43 | private static final String METHOD_IM_MARK_READ = "imMarkAudioMessageRead"; 44 | private static final String METHOD_IM_RECORD_START = "onStartRecording"; 45 | private static final String METHOD_IM_RECORD_STOP = "onStopRecording"; 46 | private static final String METHOD_IM_RECORD_CANCEL = "onCancelRecording"; 47 | 48 | 49 | private final PluginRegistry.Registrar registrar; 50 | private EventChannel.EventSink eventSink; 51 | 52 | private NIMRecentSessionsInteractor recentSessionsInteractor; 53 | 54 | private NIMSessionInteractor sessionInteractor; 55 | 56 | private NIMKickoutInteractor kickoutInteractor; 57 | 58 | /** 59 | * Plugin registration. 60 | */ 61 | public static void registerWith(Registrar registrar) { 62 | final MethodChannel methodChannel = new MethodChannel(registrar.messenger(), METHOD_CHANNEL_NAME); 63 | final EventChannel eventChannel = 64 | new EventChannel(registrar.messenger(), EVENT_CHANNEL_NAME); 65 | 66 | final FlutterNimPlugin instance = new FlutterNimPlugin(registrar); 67 | 68 | methodChannel.setMethodCallHandler(instance); 69 | eventChannel.setStreamHandler(instance); 70 | } 71 | 72 | private FlutterNimPlugin(PluginRegistry.Registrar registrar) { 73 | this.registrar = registrar; 74 | } 75 | 76 | @Override 77 | public void onMethodCall(MethodCall call, Result result) { 78 | handleMethodChannel(call, result); 79 | } 80 | 81 | @Override 82 | public void onListen(Object o, EventChannel.EventSink eventSink) { 83 | this.eventSink = eventSink; 84 | recentSessionsInteractor = new NIMRecentSessionsInteractor(eventSink); 85 | kickoutInteractor = new NIMKickoutInteractor(eventSink); 86 | } 87 | 88 | @Override 89 | public void onCancel(Object o) { 90 | this.eventSink = null; 91 | } 92 | 93 | 94 | private void handleMethodChannel(MethodCall methodCall, MethodChannel.Result result) { 95 | switch (methodCall.method) { 96 | case METHOD_IM_INIT: 97 | imInit(); 98 | 99 | break; 100 | case METHOD_IM_LOGIN: 101 | String account = methodCall.argument("imAccount"); 102 | String token = methodCall.argument("imToken"); 103 | doIMLogin(account, token, result); 104 | 105 | break; 106 | case METHOD_IM_LOGOUT: 107 | FlutterNIMHelper.getInstance().doIMLogout(); 108 | FlutterNIMPreferences.clear(); 109 | 110 | break; 111 | case METHOD_IM_RECENT_SESSIONS: 112 | recentSessionsInteractor.loadRecentSessions(); 113 | 114 | break; 115 | case METHOD_IM_DELETE_RECENT_SESSION: 116 | String deletedSessionId = methodCall.argument("sessionId"); 117 | 118 | recentSessionsInteractor.deleteRecentContact2(deletedSessionId); 119 | break; 120 | case METHOD_IM_START_CHAT: 121 | String sessionId = methodCall.argument("sessionId"); 122 | startChat(sessionId, result); 123 | 124 | break; 125 | case METHOD_IM_EXIT_CHAT: 126 | if (sessionInteractor != null) { 127 | sessionInteractor.onDestroy(); 128 | sessionInteractor = null; 129 | } 130 | 131 | break; 132 | case METHOD_IM_MESSAGES: 133 | int messageIndex = methodCall.argument("messageIndex"); 134 | sessionInteractor.loadHistoryMessages(messageIndex); 135 | 136 | break; 137 | case METHOD_IM_SEND_TEXT: 138 | String text = methodCall.argument("text"); 139 | if (sessionInteractor != null) { 140 | sessionInteractor.sendTextMessage(text); 141 | } 142 | 143 | break; 144 | case METHOD_IM_SEND_IMAGE: 145 | String imagePath = methodCall.argument("imagePath"); 146 | if (sessionInteractor != null) { 147 | sessionInteractor.sendImageMessage(imagePath); 148 | } 149 | 150 | break; 151 | case METHOD_IM_SEND_VIDEO: 152 | String videoPath = methodCall.argument("videoPath"); 153 | if (sessionInteractor != null) { 154 | sessionInteractor.sendVideoMessage(videoPath); 155 | } 156 | 157 | break; 158 | case METHOD_IM_SEND_AUDIO: 159 | String audioPath = methodCall.argument("audioPath"); 160 | if (sessionInteractor != null) { 161 | sessionInteractor.sendAudioMessage(audioPath); 162 | } 163 | 164 | break; 165 | case METHOD_IM_SEND_CUSTOM: 166 | String customEncodeString = methodCall.argument("customEncodeString"); 167 | String apnsContent = methodCall.argument("apnsContent"); 168 | 169 | if (sessionInteractor != null) { 170 | sessionInteractor.sendCustomMessage(customEncodeString, apnsContent); 171 | } 172 | 173 | break; 174 | case METHOD_IM_SEND_CUSTOM_2: 175 | String sessionId2 = methodCall.argument("sessionId"); 176 | String customEncodeString2 = methodCall.argument("customEncodeString"); 177 | String apnsContent2 = methodCall.argument("apnsContent"); 178 | 179 | NIMSessionInteractor.sendCustomMessageToSession(sessionId2, customEncodeString2, apnsContent2); 180 | result.success(true); 181 | 182 | break; 183 | case METHOD_IM_RESEND_MESSAGE: 184 | String messageId = methodCall.argument("messageId"); 185 | if (sessionInteractor != null) { 186 | sessionInteractor.resendMessage(messageId); 187 | } 188 | 189 | break; 190 | case METHOD_IM_MARK_READ: 191 | String audioMessageId = methodCall.argument("messageId"); 192 | if (sessionInteractor != null) { 193 | sessionInteractor.markAudioMessageRead(audioMessageId); 194 | result.success(true); 195 | } 196 | 197 | break; 198 | case METHOD_IM_RECORD_START: 199 | if (sessionInteractor != null) { 200 | sessionInteractor.onStartRecording(); 201 | } 202 | 203 | break; 204 | case METHOD_IM_RECORD_STOP: 205 | if (sessionInteractor != null) { 206 | sessionInteractor.onStopRecording(); 207 | } 208 | 209 | break; 210 | case METHOD_IM_RECORD_CANCEL: 211 | if (sessionInteractor != null) { 212 | sessionInteractor.onCancelRecording(); 213 | } 214 | 215 | break; 216 | } 217 | } 218 | 219 | /** 220 | * 初始化... 221 | *

222 | * 由于 Android NIMSDK 必须在{@link Application#onCreate()}中初始化 223 | * 所以这里仅初始化自定义消息附件解析器,SDK的初始化还需放在在 Android 工程的 Application onCreate() 中 224 | */ 225 | private void imInit() { 226 | FlutterNIMHelper.initIM(registrar.activity()); 227 | } 228 | 229 | /** 230 | * IM登录 231 | */ 232 | private void doIMLogin(String account, String token, final MethodChannel.Result result) { 233 | 234 | FlutterNIMHelper.getInstance().doIMLogin(account, token, new FlutterNIMHelper.IMLoginCallback() { 235 | @Override 236 | public void onResult(boolean isSuccess) { 237 | result.success(isSuccess); 238 | } 239 | }); 240 | } 241 | 242 | /** 243 | * 开始聊天 244 | */ 245 | private void startChat(String sessionId, final MethodChannel.Result result) { 246 | NIMSessionInteractor sessionInteractor = new NIMSessionInteractor(registrar.activity(), sessionId, eventSink); 247 | this.sessionInteractor = sessionInteractor; 248 | 249 | result.success(true); 250 | } 251 | } 252 | -------------------------------------------------------------------------------- /ios/Classes/Helper/NIMSessionParser.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NIMSessionParser.swift 3 | // flutter_nim 4 | // 5 | // Created by GuiminChu on 2019/7/19. 6 | // 7 | // Copyright (c) 2019 GuiminChu 8 | // 9 | // Permission is hereby granted, free of charge, to any person obtaining a copy 10 | // of this software and associated documentation files (the "Software"), to deal 11 | // in the Software without restriction, including without limitation the rights 12 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | // copies of the Software, and to permit persons to whom the Software is 14 | // furnished to do so, subject to the following conditions: 15 | // 16 | // The above copyright notice and this permission notice shall be included in 17 | // all copies or substantial portions of the Software. 18 | // 19 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 25 | // THE SOFTWARE. 26 | 27 | import NIMSDK 28 | 29 | struct NIMSessionParser { 30 | // 处理最近会话数据 31 | static func handleRecentSessionsData(recentSessions: [NIMRecentSession]) -> String { 32 | var result = "" 33 | 34 | var sessionDictArray = [[String: Any]]() 35 | for recentSession in recentSessions { 36 | var sessionDict = [String: Any]() 37 | sessionDict["sessionId"] = recentSession.session!.sessionId 38 | sessionDict["unreadCount"] = recentSession.unreadCount 39 | sessionDict["timestamp"] = recentSession.timestamp 40 | 41 | if let lastMessage = recentSession.lastMessage { 42 | let lastMessageDict = getMessageDict(message: lastMessage) 43 | sessionDict["messageContent"] = messageContent(lastMessage: lastMessage) 44 | sessionDict["lastMessage"] = lastMessageDict 45 | } 46 | 47 | var userInfoDict = [String: Any]() 48 | userInfoDict["nickname"] = recentSession.userInfo.nickName 49 | userInfoDict["avatarUrl"] = recentSession.userInfo.avatarUrl 50 | userInfoDict["userExt"] = recentSession.userInfo.ext 51 | sessionDict["userInfo"] = userInfoDict 52 | 53 | sessionDictArray.append(sessionDict) 54 | } 55 | 56 | let dict: [String: Any] = ["recentSessions": sessionDictArray] 57 | 58 | if let data = try? JSONSerialization.data(withJSONObject: dict, options: .prettyPrinted), 59 | let jsonString = String(data: data, encoding: String.Encoding.utf8) { 60 | result = jsonString 61 | } 62 | 63 | return result 64 | } 65 | 66 | // 处理会话消息数据 67 | static func handleMessages(messages: [NIMMessage]) -> String { 68 | var result = "" 69 | 70 | var messageDictArray = [[String: Any]]() 71 | for message in messages { 72 | let messageDict = getMessageDict(message: message) 73 | messageDictArray.append(messageDict) 74 | } 75 | 76 | let dict: [String: Any] = ["messages": messageDictArray] 77 | 78 | if let data = try? JSONSerialization.data(withJSONObject: dict, options: .prettyPrinted), 79 | let jsonString = String(data: data, encoding: String.Encoding.utf8) { 80 | result = jsonString 81 | } 82 | 83 | return result 84 | } 85 | 86 | // 可用的消息字典 87 | private static func getMessageDict(message: NIMMessage) -> [String: Any] { 88 | var messageDict = [String: Any]() 89 | 90 | messageDict["messageId"] = message.messageId 91 | messageDict["from"] = message.from 92 | messageDict["text"] = message.text 93 | messageDict["isOutgoingMsg"] = message.isOutgoingMsg 94 | messageDict["messageType"] = message.messageType.rawValue 95 | messageDict["deliveryState"] = message.deliveryState.rawValue 96 | messageDict["timestamp"] = Int(message.timestamp * 1000) 97 | 98 | if message.messageType == NIMMessageType.image { 99 | if let imageObject = message.messageObject as? NIMImageObject { 100 | var messageObjectDict = [String: Any]() 101 | messageObjectDict["url"] = imageObject.url 102 | messageObjectDict["thumbUrl"] = imageObject.thumbUrl 103 | messageObjectDict["thumbPath"] = imageObject.thumbPath 104 | messageObjectDict["path"] = imageObject.path 105 | messageObjectDict["width"] = Int(imageObject.size.width) 106 | messageObjectDict["height"] = Int(imageObject.size.height) 107 | 108 | messageDict["messageObject"] = messageObjectDict 109 | } 110 | } else if message.messageType == NIMMessageType.audio { 111 | if let audioObject = message.messageObject as? NIMAudioObject { 112 | var messageObjectDict = [String: Any]() 113 | messageObjectDict["url"] = audioObject.url 114 | messageObjectDict["path"] = audioObject.path 115 | messageObjectDict["duration"] = audioObject.duration 116 | messageObjectDict["isPlayed"] = message.isPlayed 117 | 118 | messageDict["messageObject"] = messageObjectDict 119 | } 120 | } else if message.messageType == NIMMessageType.video { 121 | if let videoObject = message.messageObject as? NIMVideoObject { 122 | var messageObjectDict = [String: Any]() 123 | messageObjectDict["url"] = videoObject.url 124 | messageObjectDict["coverUrl"] = videoObject.coverUrl 125 | messageObjectDict["path"] = videoObject.path 126 | messageObjectDict["duration"] = videoObject.duration 127 | messageObjectDict["width"] = Int(videoObject.coverSize.width) 128 | messageObjectDict["height"] = Int(videoObject.coverSize.height) 129 | 130 | messageDict["messageObject"] = messageObjectDict 131 | } 132 | } else if message.messageType == NIMMessageType.custom { 133 | if let customObject = message.messageObject as? NIMCustomObject, let attachment = customObject.attachment { 134 | messageDict["customMessageContent"] = attachment.encode() 135 | } 136 | } 137 | 138 | return messageDict 139 | } 140 | } 141 | 142 | // MARK: - Help Methods 143 | 144 | extension NIMSessionParser { 145 | // 最近会话列表中显示的消息文本 146 | private static func messageContent(lastMessage: NIMMessage?) -> String { 147 | guard let lastMessage = lastMessage else { 148 | return "" 149 | } 150 | 151 | var text = "" 152 | switch lastMessage.messageType { 153 | case .text: 154 | text = lastMessage.text ?? "" 155 | case .image: 156 | text = "[图片]" 157 | case .audio: 158 | text = "[语音]" 159 | case .video: 160 | text = "[视频]" 161 | case .location: 162 | text = "[位置]" 163 | case .file: 164 | text = "[文件]" 165 | case .tip: 166 | text = lastMessage.text ?? "" 167 | case .notification: 168 | // FIXME: 省略 169 | text = "" 170 | case .robot: 171 | // FIXME: 省略 172 | text = "" 173 | case .custom: 174 | text = "[自定义消息]" 175 | default: 176 | text = "" 177 | } 178 | 179 | return text 180 | } 181 | 182 | // 最近会话排序 183 | static func sortRecentSessions(recentSessions: inout [NIMRecentSession]) { 184 | recentSessions.sort { (item1, item2) -> Bool in 185 | // 先判断是否有置顶标记 186 | var score1 = NIMSessionUtil.isRecentSessionMark(item1, type: NIMRecentSessionMarkType.markTypeTop) ? 10 : 0 187 | var score2 = NIMSessionUtil.isRecentSessionMark(item2, type: NIMRecentSessionMarkType.markTypeTop) ? 10 : 0 188 | 189 | if let message1 = item1.lastMessage, let message2 = item2.lastMessage { 190 | if message1.timestamp > message2.timestamp { 191 | score1 += 1; 192 | } else { 193 | score2 += 1; 194 | } 195 | } 196 | 197 | if score1 > score2 { 198 | return true 199 | } else { 200 | return false 201 | } 202 | } 203 | } 204 | 205 | // 计算新会话插入的位置 206 | static func findInsertPlace(_ recentSession: NIMRecentSession, in recentSessions: [NIMRecentSession]) -> Int { 207 | var matchIndex: Int = 0 208 | var find = false 209 | 210 | for (index, item) in recentSessions.enumerated() { 211 | if let itemLastMessage = item.lastMessage, let lastMessage = recentSession.lastMessage { 212 | if itemLastMessage.timestamp <= lastMessage.timestamp { 213 | find = true 214 | matchIndex = index 215 | break 216 | } 217 | } 218 | } 219 | 220 | if find { 221 | return matchIndex 222 | } else { 223 | return recentSessions.count 224 | } 225 | } 226 | } 227 | 228 | extension NIMRecentSession { 229 | /// 元组:用户名、头像链接、用户额外信息 230 | var userInfo: (nickName: String, avatarUrl: String, ext: String) { 231 | guard let _session = self.session else { 232 | return ("", "", "") 233 | } 234 | 235 | if _session.sessionType == NIMSessionType.P2P { 236 | if let _userInfo = NIMSDK.shared().userManager.userInfo(_session.sessionId)?.userInfo { 237 | return (_userInfo.nickName ?? "", _userInfo.avatarUrl ?? "", _userInfo.ext ?? "") 238 | } 239 | } else { 240 | if let _team = NIMSDK.shared().teamManager.team(byId: _session.sessionId) { 241 | return (_team.teamName ?? "", _team.avatarUrl ?? "", "") 242 | } 243 | } 244 | 245 | return ("", "", "") 246 | } 247 | 248 | /// 最后一条消息时间戳,单位 ms 249 | var timestamp: Int { 250 | guard let _lastMessage = self.lastMessage else { 251 | return 0 252 | } 253 | 254 | return Int(_lastMessage.timestamp * 1000) 255 | } 256 | } 257 | -------------------------------------------------------------------------------- /ios/Classes/Helper/NIMSessionInteractor.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NIMSessionInteractor.swift 3 | // flutter_nim 4 | // 5 | // Created by GuiminChu on 2019/7/19. 6 | // 7 | // Copyright (c) 2019 GuiminChu 8 | // 9 | // Permission is hereby granted, free of charge, to any person obtaining a copy 10 | // of this software and associated documentation files (the "Software"), to deal 11 | // in the Software without restriction, including without limitation the rights 12 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | // copies of the Software, and to permit persons to whom the Software is 14 | // furnished to do so, subject to the following conditions: 15 | // 16 | // The above copyright notice and this permission notice shall be included in 17 | // all copies or substantial portions of the Software. 18 | // 19 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 25 | // THE SOFTWARE. 26 | 27 | import Foundation 28 | import NIMSDK 29 | 30 | class NIMSessionInteractor: NSObject { 31 | var session: NIMSession 32 | var flutterEventSink: FlutterEventSink? 33 | 34 | var nickName = "" 35 | var avatarUrl = "" 36 | 37 | var allMessages: [NIMMessage] = [] 38 | 39 | // 录音时间 40 | var recordTime: TimeInterval = 0 41 | 42 | init(session: NIMSession, flutterEventSink: FlutterEventSink? = nil) { 43 | 44 | self.session = session 45 | self.flutterEventSink = flutterEventSink 46 | 47 | super.init() 48 | 49 | addListener() 50 | 51 | NIMSDK.shared().mediaManager.add(self) 52 | 53 | // 进入会话时,标记当前会话消息已读 54 | markRead() 55 | 56 | // 如果本地没有用户资料,去服务端同步一下 57 | getUserInfo(); 58 | } 59 | 60 | private func getUserInfo () { 61 | // 获取云信用户信息 62 | if let userInfo = NIMSDK.shared().userManager.userInfo(session.sessionId)?.userInfo { 63 | self.nickName = userInfo.nickName ?? "" 64 | self.avatarUrl = userInfo.avatarUrl ?? "" 65 | } else { 66 | NIMSDK.shared().userManager.fetchUserInfos([session.sessionId]) { [weak self] (users, error) in 67 | guard let `self` = self else { 68 | return 69 | } 70 | 71 | if let users = users, users.count > 0 { 72 | self.nickName = users[0].userInfo?.nickName ?? "" 73 | self.avatarUrl = users[0].userInfo?.avatarUrl ?? "" 74 | } 75 | } 76 | } 77 | } 78 | 79 | private func markRead() { 80 | NIMSDK.shared().conversationManager.markAllMessagesRead(in: session) 81 | } 82 | 83 | private func sendMessage(message: NIMMessage) { 84 | NIMSDK.shared().chatManager.send(message, to: session, completion: nil) 85 | } 86 | 87 | private func resendMessage(message: NIMMessage) { 88 | try? NIMSDK.shared().chatManager.resend(message) 89 | } 90 | 91 | private func refreshDataSource() { 92 | // 主动给 flutter 发消息 93 | if let eventSink = self.flutterEventSink { 94 | eventSink(NIMSessionParser.handleMessages(messages: self.allMessages)) 95 | } 96 | } 97 | 98 | private func addListener() { 99 | NIMSDK.shared().chatManager.add(self) 100 | } 101 | 102 | private func removeListener() { 103 | NIMSDK.shared().chatManager.remove(self) 104 | } 105 | 106 | deinit { 107 | removeListener() 108 | 109 | NIMSDK.shared().mediaManager.remove(self) 110 | } 111 | } 112 | 113 | // MARK: - public 114 | 115 | extension NIMSessionInteractor { 116 | 117 | func loadMessages(messageIndex: Int) { 118 | 119 | var message: NIMMessage? 120 | if messageIndex >= 0 { 121 | message = self.allMessages[messageIndex] 122 | } 123 | 124 | if let messages = NIMSDK.shared().conversationManager.messages(in: session, message: message, limit: 20) { 125 | self.allMessages.insert(contentsOf: messages, at: 0) 126 | refreshDataSource() 127 | } 128 | } 129 | 130 | func sendTextMessage(text: String) { 131 | let message = NIMMessage() 132 | message.text = text 133 | 134 | sendMessage(message: message) 135 | } 136 | 137 | func sendImageMessage(path: String) { 138 | let message = NIMMessage() 139 | let imageObject = NIMImageObject(filepath: path) 140 | message.messageObject = imageObject 141 | 142 | sendMessage(message: message) 143 | } 144 | 145 | func sendAudioMessage(filePath: String) { 146 | let message = NIMMessage() 147 | let audioObject = NIMAudioObject(sourcePath: filePath, scene: NIMNOSSceneTypeMessage) 148 | message.messageObject = audioObject 149 | sendMessage(message: message) 150 | } 151 | 152 | func sendVideoMessage(filePath: String) { 153 | let message = NIMMessage() 154 | let audioObject = NIMVideoObject(sourcePath: filePath, scene: NIMNOSSceneTypeMessage) 155 | message.messageObject = audioObject 156 | sendMessage(message: message) 157 | } 158 | 159 | func sendCustomMessage(customEncodeString: String, apnsContent: String) { 160 | let attachment = IMCustomAttachment() 161 | attachment.customEncodeString = customEncodeString; 162 | 163 | let message = NIMMessage() 164 | let customObject = NIMCustomObject() 165 | customObject.attachment = attachment 166 | message.messageObject = customObject 167 | message.apnsContent = apnsContent 168 | 169 | sendMessage(message: message) 170 | } 171 | 172 | // 会话外发送自定义消息 173 | static func sendCustomMessageTo(sessionID: String, customEncodeString: String, apnsContent: String, result: @escaping FlutterResult) { 174 | let attachment = IMCustomAttachment() 175 | attachment.customEncodeString = customEncodeString; 176 | 177 | let message = NIMMessage() 178 | let customObject = NIMCustomObject() 179 | customObject.attachment = attachment 180 | message.messageObject = customObject 181 | message.apnsContent = apnsContent 182 | 183 | NIMSDK.shared().chatManager.send(message, to: NIMSession(sessionID, type: NIMSessionType.P2P)) { (error) in 184 | if (error == nil) { 185 | result(true) 186 | } else { 187 | result(false) 188 | } 189 | } 190 | } 191 | 192 | /// 重发消息 193 | func resendMessage(messageId: String) { 194 | if let index = self.allMessages.map({ $0.messageId }).firstIndex(of: messageId) { 195 | let message = self.allMessages[index] 196 | resendMessage(message: message) 197 | } 198 | } 199 | 200 | /// 开始录音 201 | func onStartRecording() { 202 | let type = NIMAudioType.AAC 203 | // let duration = NIMKit.shared()!.config.recordMaxDuration 204 | let duration = 60.0 205 | 206 | NIMSDK.shared().mediaManager.record(type, duration: duration) 207 | } 208 | 209 | /// 结束录音 210 | func onStopRecording() { 211 | NIMSDK.shared().mediaManager.stopRecord() 212 | } 213 | 214 | /// 取消录音 215 | func onCancelRecording() { 216 | NIMSDK.shared().mediaManager.cancelRecord() 217 | } 218 | 219 | // 标记语音已读 220 | func markAudioMessageRead(messageId: String) { 221 | if let index = self.allMessages.map({ $0.messageId }).firstIndex(of: messageId) { 222 | let message = self.allMessages[index] 223 | message.isPlayed = true 224 | } 225 | } 226 | } 227 | 228 | // MARK: - NIMChatManagerDelegate 229 | 230 | extension NIMSessionInteractor: NIMChatManagerDelegate { 231 | 232 | func willSend(_ message: NIMMessage) { 233 | if message.session == self.session { 234 | // 用来判断是否是发送失败重发 235 | var hasThisMessage = false 236 | for (index, msg) in allMessages.enumerated() { 237 | if msg.messageId == message.messageId { 238 | allMessages[index] = message 239 | hasThisMessage = true 240 | break 241 | } 242 | } 243 | 244 | if !hasThisMessage { 245 | allMessages.append(message) 246 | } 247 | 248 | refreshDataSource() 249 | } 250 | } 251 | 252 | func send(_ message: NIMMessage, didCompleteWithError error: Error?) { 253 | if (error == nil) { 254 | if message.session == self.session { 255 | for (index, msg) in allMessages.enumerated() { 256 | if msg.messageId == message.messageId { 257 | allMessages[index] = message 258 | break 259 | } 260 | } 261 | refreshDataSource() 262 | } 263 | } else { 264 | print("FlutterNIM:消息发送失败") 265 | } 266 | } 267 | 268 | func onRecvMessages(_ messages: [NIMMessage]) { 269 | if let newMessage = messages.first { 270 | 271 | if newMessage.session?.sessionId == self.session.sessionId { 272 | allMessages.append(newMessage) 273 | refreshDataSource() 274 | 275 | // 收到新消息时,标记已读 276 | markRead() 277 | } 278 | } 279 | } 280 | } 281 | 282 | // MARK: - NIMMediaManagerDelegate 283 | 284 | extension NIMSessionInteractor: NIMMediaManagerDelegate { 285 | // 开始录制音频的回调 286 | func recordAudio(_ filePath: String?, didBeganWithError error: Error?) { 287 | self.recordTime = 0 288 | } 289 | 290 | // 录制音频完成后的回调 291 | func recordAudio(_ filePath: String?, didCompletedWithError error: Error?) { 292 | if (error == nil && filePath != nil) { 293 | if (self.recordTime > 1) { 294 | sendAudioMessage(filePath: filePath!) 295 | } else { 296 | print("FlutterNIM:说话时间太短") 297 | } 298 | } 299 | } 300 | 301 | // 录音被取消的回调 302 | func recordAudioDidCancelled() { 303 | self.recordTime = 0 304 | } 305 | 306 | func recordAudioProgress(_ currentTime: TimeInterval) { 307 | self.recordTime = currentTime 308 | } 309 | 310 | func recordAudioInterruptionBegin() { 311 | NIMSDK.shared().mediaManager.cancelRecord() 312 | } 313 | } 314 | -------------------------------------------------------------------------------- /lib/flutter_nim.dart: -------------------------------------------------------------------------------- 1 | // 2 | // flutter_nim.dart 3 | // flutter_nim 4 | // 5 | // Created by GuiminChu on 2019/7/19. 6 | // 7 | // Copyright (c) 2019 GuiminChu 8 | // 9 | // Permission is hereby granted, free of charge, to any person obtaining a copy 10 | // of this software and associated documentation files (the "Software"), to deal 11 | // in the Software without restriction, including without limitation the rights 12 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | // copies of the Software, and to permit persons to whom the Software is 14 | // furnished to do so, subject to the following conditions: 15 | // 16 | // The above copyright notice and this permission notice shall be included in 17 | // all copies or substantial portions of the Software. 18 | // 19 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 25 | // THE SOFTWARE. 26 | // 27 | 28 | import 'dart:async'; 29 | import 'dart:convert'; 30 | 31 | import 'package:flutter/services.dart'; 32 | 33 | import 'models/nim_session_model.dart'; 34 | import 'models/nim_message_model.dart'; 35 | import 'models/nim_kick_reason.dart'; 36 | 37 | export 'models/nim_session_model.dart'; 38 | export 'models/nim_message_model.dart'; 39 | export 'models/nim_user_model.dart'; 40 | export 'models/nim_kick_reason.dart'; 41 | 42 | class FlutterNIM { 43 | factory FlutterNIM() { 44 | if (_instance == null) { 45 | final MethodChannel methodChannel = 46 | const MethodChannel("flutter_nim_method"); 47 | final EventChannel eventChannel = const EventChannel('flutter_nim_event'); 48 | 49 | _instance = FlutterNIM._private(methodChannel, eventChannel); 50 | } 51 | return _instance; 52 | } 53 | 54 | FlutterNIM._private(this._methodChannel, this._eventChannel) { 55 | _eventChannel.receiveBroadcastStream().listen(_onEvent, onError: _onError); 56 | } 57 | 58 | static FlutterNIM _instance; 59 | 60 | final MethodChannel _methodChannel; 61 | final EventChannel _eventChannel; 62 | 63 | StreamController> _recentSessionsController = 64 | StreamController.broadcast(); 65 | 66 | StreamController> _messagesController = 67 | StreamController.broadcast(); 68 | 69 | StreamController _kickReasonController = 70 | StreamController.broadcast(); 71 | 72 | /// Response for recent sessions 73 | Stream> get recentSessionsResponse => 74 | _recentSessionsController.stream; 75 | 76 | /// Response for chat messages 77 | Stream> get messagesResponse => _messagesController.stream; 78 | 79 | /// Response for kick reason 80 | Stream get kickReasonResponse => _kickReasonController.stream; 81 | 82 | Future get platformVersion async { 83 | final String version = 84 | await _methodChannel.invokeMethod('getPlatformVersion'); 85 | return version; 86 | } 87 | 88 | /// 初始化 89 | /// 90 | /// [appKey]必须要传 91 | /// 如果[imAccount]和[imToken]不为空,则会调用云信[autoLogin]方法 92 | Future init({ 93 | String appKey = "", 94 | String apnsCername = "", 95 | String apnsCernameDevelop = "", 96 | String xmAppId = "", 97 | String xmAppKey = "", 98 | String xmCertificateName = "", 99 | String hwCertificateName = "", 100 | String vivoCertificateName = "", 101 | String imAccount = "", 102 | String imToken = "", 103 | }) async { 104 | await _methodChannel.invokeMethod("imInit", { 105 | "appKey": appKey, 106 | "apnsCername": apnsCername, 107 | "apnsCernameDevelop": apnsCernameDevelop, 108 | "xmAppId": xmAppId, 109 | "xmAppKey": xmAppKey, 110 | "xmCertificateName": xmCertificateName, 111 | "hwCertificateName": hwCertificateName, 112 | "vivoCertificateName": vivoCertificateName, 113 | "imAccount": imAccount, 114 | "imToken": imToken, 115 | }); 116 | } 117 | 118 | /// IM 登录 119 | Future login(String imAccount, String imToken) async { 120 | Map map = { 121 | "imAccount": imAccount, 122 | "imToken": imToken, 123 | }; 124 | 125 | final bool isLoginSuccess = 126 | await _methodChannel.invokeMethod("imLogin", map); 127 | return isLoginSuccess; 128 | } 129 | 130 | /// IM 退出登录 131 | Future logout() async { 132 | await _methodChannel.invokeMethod("imLogout"); 133 | } 134 | 135 | /// 获取会话列表 136 | Future loadRecentSessions() async { 137 | await _methodChannel.invokeMethod("imRecentSessions"); 138 | } 139 | 140 | /// 删除某项最近会话 141 | Future deleteRecentSession(String sessionId) async { 142 | Map map = { 143 | "sessionId": sessionId, 144 | }; 145 | 146 | await _methodChannel.invokeMethod("imDeleteRecentSession", map); 147 | } 148 | 149 | /// 开始会话 150 | Future startChat(String sessionId) async { 151 | Map map = { 152 | "sessionId": sessionId, 153 | }; 154 | 155 | final bool isSuccess = 156 | await _methodChannel.invokeMethod("imStartChat", map); 157 | 158 | return isSuccess; 159 | } 160 | 161 | Future exitChat() async { 162 | await _methodChannel.invokeMethod("imExitChat"); 163 | } 164 | 165 | /// 获取会话消息 166 | /// 167 | /// [messageIndex] 传[-1]时返回最新的20条消息列表。 168 | /// 加载更多时传最上面那条消息的[index] 169 | Future loadMessages(int messageIndex) async { 170 | Map map = { 171 | "messageIndex": messageIndex, 172 | }; 173 | 174 | await _methodChannel.invokeMethod('imMessages', map); 175 | } 176 | 177 | /// 会话内发送文本消息 178 | Future sendText(String text) async { 179 | Map map = { 180 | "text": text ?? "", 181 | }; 182 | 183 | await _methodChannel.invokeMethod("imSendText", map); 184 | } 185 | 186 | /// 会话内发送图片消息 187 | Future sendImage(String imagePath) async { 188 | Map map = { 189 | "imagePath": imagePath ?? "", 190 | }; 191 | 192 | await _methodChannel.invokeMethod("imSendImage", map); 193 | } 194 | 195 | /// 会话内发送视频消息 196 | Future sendVideo(String videoPath) async { 197 | Map map = { 198 | "videoPath": videoPath ?? "", 199 | }; 200 | 201 | await _methodChannel.invokeMethod("imSendVideo", map); 202 | } 203 | 204 | /// 会话内发送音频消息 205 | Future sendAudio(String audioPath) async { 206 | Map map = { 207 | "audioPath": audioPath ?? "", 208 | }; 209 | 210 | await _methodChannel.invokeMethod("imSendAudio", map); 211 | } 212 | 213 | /// 会话内发送自定义消息 214 | Future sendCustomMessage(Map customObject, {String apnsContent}) async { 215 | final String customEncodeString = json.encode(customObject); 216 | 217 | Map map = { 218 | "customEncodeString": customEncodeString, 219 | "apnsContent": apnsContent ?? "[自定义消息]", 220 | }; 221 | 222 | await _methodChannel.invokeMethod("imSendCustom", map); 223 | } 224 | 225 | /// 会话外发送自定义消息 226 | Future sendCustomMessageToSession(String sessionId, Map customObject, 227 | {String apnsContent}) async { 228 | final String customEncodeString = json.encode(customObject); 229 | 230 | Map map = { 231 | "sessionId": sessionId, 232 | "customEncodeString": customEncodeString, 233 | "apnsContent": apnsContent ?? "[自定义消息]", 234 | }; 235 | final bool isSendSuccess = 236 | await _methodChannel.invokeMethod("imSendCustomToSession", map); 237 | return isSendSuccess; 238 | } 239 | 240 | /// 会话内重发消息 241 | Future resendMessage(String messageId) async { 242 | Map map = { 243 | "messageId": messageId ?? "", 244 | }; 245 | 246 | await _methodChannel.invokeMethod("imResendMessage", map); 247 | } 248 | 249 | /// iOS 和 Android 均使用了 NIMSDK 中的录音功能 250 | /// 251 | /// 开始录音 252 | Future onStartRecording() async { 253 | await _methodChannel.invokeMethod("onStartRecording"); 254 | } 255 | 256 | /// 结束录音 257 | Future onStopRecording() async { 258 | await _methodChannel.invokeMethod("onStopRecording"); 259 | } 260 | 261 | /// 取消录音 262 | Future onCancelRecording() async { 263 | await _methodChannel.invokeMethod("onCancelRecording"); 264 | } 265 | 266 | /// 标记音频已读 267 | Future markAudioMessageRead(String messageId) async { 268 | Map map = { 269 | "messageId": messageId, 270 | }; 271 | 272 | await _methodChannel.invokeMethod("imMarkAudioMessageRead", map); 273 | } 274 | 275 | ////////////// 276 | 277 | // 解析最近会话 JSON 278 | void _parseRecentSessionsData(dynamic imMap) { 279 | final recentSessionsMap = imMap["recentSessions"]; 280 | 281 | if (recentSessionsMap != null) { 282 | List recentSessions = recentSessionsMap 283 | .map( 284 | (itemJson) => NIMRecentSession.fromJson(itemJson)) 285 | .toList(); 286 | 287 | _recentSessionsController.add(recentSessions); 288 | } 289 | } 290 | 291 | // 解析聊天记录 JSON 292 | void _parseMessagesData(dynamic imMap) { 293 | final messagesMap = imMap["messages"]; 294 | 295 | if (messagesMap != null) { 296 | List messages = messagesMap 297 | .map((itemJson) => NIMMessage.fromJson(itemJson)) 298 | .toList(); 299 | 300 | if (messages != null) { 301 | if (messages.isNotEmpty) { 302 | List.generate(messages.length, (index) { 303 | if (index == 0) { 304 | messages[index].isShowTimeTag = true; 305 | } else { 306 | // 两条消息相隔 300 秒则显示时间戳 307 | if (messages[index].timestamp - messages[index - 1].timestamp > 308 | 300000) { 309 | messages[index].isShowTimeTag = true; 310 | } 311 | } 312 | }); 313 | } 314 | 315 | _messagesController.add(messages); 316 | } 317 | } 318 | } 319 | 320 | // 解析被踢下线 JSON 321 | void _parseKickReasonData(dynamic imMap) { 322 | final kickCode = imMap["kickCode"]; 323 | 324 | if (kickCode != null) { 325 | if (kickCode == 1) { 326 | _kickReasonController.add(NIMKickReason.byClient); 327 | } else if (kickCode == 2) { 328 | _kickReasonController.add(NIMKickReason.byServer); 329 | } else if (kickCode == 3) { 330 | _kickReasonController.add(NIMKickReason.byClientManually); 331 | } 332 | } 333 | } 334 | 335 | void _onEvent(Object event) { 336 | if (event != null) { 337 | String eventString = event; 338 | try { 339 | final imMap = json.decode(eventString); 340 | 341 | _parseRecentSessionsData(imMap); 342 | _parseMessagesData(imMap); 343 | _parseKickReasonData(imMap); 344 | } on FormatException catch (e) { 345 | print("FlutterNIM - That string didn't look like Json."); 346 | print(e.message); 347 | } on NoSuchMethodError catch (e) { 348 | print('FlutterNIM - That string was null!'); 349 | print(e.toString()); 350 | } 351 | } 352 | } 353 | 354 | // EventChannel 错误返回 355 | void _onError(Object error) { 356 | print("FlutterNIM - ${error.toString()}"); 357 | } 358 | } 359 | -------------------------------------------------------------------------------- /android/src/main/java/cn/cgm/flutter_nim/Helper/NIMSessionParser.java: -------------------------------------------------------------------------------- 1 | package cn.cgm.flutter_nim.Helper; 2 | 3 | import com.netease.nimlib.sdk.NIMClient; 4 | import com.netease.nimlib.sdk.msg.MsgService; 5 | import com.netease.nimlib.sdk.msg.attachment.AudioAttachment; 6 | import com.netease.nimlib.sdk.msg.attachment.ImageAttachment; 7 | import com.netease.nimlib.sdk.msg.attachment.VideoAttachment; 8 | import com.netease.nimlib.sdk.msg.constant.AttachStatusEnum; 9 | import com.netease.nimlib.sdk.msg.constant.MsgDirectionEnum; 10 | import com.netease.nimlib.sdk.msg.constant.MsgStatusEnum; 11 | import com.netease.nimlib.sdk.msg.constant.MsgTypeEnum; 12 | import com.netease.nimlib.sdk.msg.model.IMMessage; 13 | import com.netease.nimlib.sdk.msg.model.RecentContact; 14 | import com.netease.nimlib.sdk.uinfo.UserService; 15 | import com.netease.nimlib.sdk.uinfo.model.NimUserInfo; 16 | 17 | import org.json.JSONArray; 18 | import org.json.JSONException; 19 | import org.json.JSONObject; 20 | 21 | import java.util.ArrayList; 22 | import java.util.List; 23 | 24 | class NIMSessionParser { 25 | 26 | // 处理最近会话数据 27 | static String handleRecentSessionsData(List recents) { 28 | String result = ""; 29 | 30 | if (recents != null) { 31 | JSONArray recentSessionJSONArray = new JSONArray(); 32 | 33 | for (int i = 0; i < recents.size(); i++) { 34 | RecentContact recent = recents.get(i); 35 | 36 | // 最近联系人ID 37 | String contactId = recent.getContactId(); 38 | 39 | JSONObject recentObject = new JSONObject(); 40 | try { 41 | recentObject.put("sessionId", recent.getContactId()); 42 | recentObject.put("unreadCount", recent.getUnreadCount()); 43 | recentObject.put("timestamp", recent.getTime()); 44 | recentObject.put("messageContent", getDefaultDigest(recent)); 45 | 46 | // 最后一条消息信息 47 | JSONObject lastMessageObject = new JSONObject(); 48 | 49 | lastMessageObject.put("messageId", recent.getRecentMessageId()); 50 | lastMessageObject.put("from", recent.getFromAccount()); 51 | lastMessageObject.put("text", recent.getContent()); 52 | lastMessageObject.put("messageType", recent.getMsgType().getValue()); 53 | lastMessageObject.put("timestamp", recent.getTime()); 54 | 55 | if (recent.getMsgType() == MsgTypeEnum.custom) { 56 | FlutterNIMCustomAttachment customAttachment = (FlutterNIMCustomAttachment) recent.getAttachment(); 57 | lastMessageObject.put("customMessageContent", customAttachment.toJson(false)); 58 | } 59 | 60 | switch (recent.getMsgStatus()) { 61 | case fail: 62 | lastMessageObject.put("deliveryState", 0); 63 | break; 64 | case sending: 65 | lastMessageObject.put("deliveryState", 1); 66 | break; 67 | case success: 68 | lastMessageObject.put("deliveryState", 2); 69 | break; 70 | default: 71 | break; 72 | } 73 | recentObject.put("lastMessage", lastMessageObject); 74 | 75 | // 用户信息 76 | JSONObject userObject = new JSONObject(); 77 | NimUserInfo userInfo = NIMClient.getService(UserService.class).getUserInfo(contactId); 78 | if (userInfo != null) { 79 | userObject.put("nickname", userInfo.getName()); 80 | userObject.put("avatarUrl", userInfo.getAvatar()); 81 | userObject.put("userExt", userInfo.getExtension()); 82 | } 83 | recentObject.put("userInfo", userObject); 84 | 85 | recentSessionJSONArray.put(recentObject); 86 | } catch (JSONException exception) { 87 | exception.printStackTrace(); 88 | } 89 | } 90 | 91 | JSONObject imObject = new JSONObject(); 92 | 93 | try { 94 | imObject.put("recentSessions", recentSessionJSONArray); 95 | } catch (JSONException exception) { 96 | exception.printStackTrace(); 97 | } 98 | 99 | result = imObject.toString(); 100 | 101 | return result; 102 | } 103 | 104 | return result; 105 | } 106 | 107 | // 处理会话消息数据 108 | static String handleMessages(List messages) { 109 | String result = ""; 110 | 111 | if (messages != null) { 112 | JSONArray messageJSONArray = new JSONArray(); 113 | 114 | for (int i = 0; i < messages.size(); i++) { 115 | IMMessage message = messages.get(i); 116 | messageJSONArray.put(getMessageJSONObject(message)); 117 | } 118 | 119 | JSONObject imObject = new JSONObject(); 120 | 121 | try { 122 | imObject.put("messages", messageJSONArray); 123 | } catch (JSONException exception) { 124 | exception.printStackTrace(); 125 | } 126 | 127 | result = imObject.toString(); 128 | 129 | return result; 130 | } 131 | 132 | return result; 133 | } 134 | 135 | // 可用的消息对象 136 | private static JSONObject getMessageJSONObject(IMMessage message) { 137 | JSONObject object = new JSONObject(); 138 | 139 | try { 140 | object.put("messageId", message.getUuid()); 141 | object.put("from", message.getFromAccount()); 142 | object.put("text", message.getContent()); 143 | object.put("messageType", message.getMsgType().getValue()); 144 | object.put("timestamp", message.getTime()); 145 | 146 | // 为了与 iOS 端兼容,做特殊处理 147 | 148 | switch (message.getDirect()) { 149 | case In: 150 | object.put("isOutgoingMsg", false); 151 | break; 152 | case Out: 153 | object.put("isOutgoingMsg", true); 154 | break; 155 | } 156 | 157 | switch (message.getStatus()) { 158 | case fail: 159 | object.put("deliveryState", 0); 160 | break; 161 | case sending: 162 | object.put("deliveryState", 1); 163 | break; 164 | case success: 165 | object.put("deliveryState", 2); 166 | break; 167 | default: 168 | break; 169 | } 170 | 171 | switch (message.getMsgType()) { 172 | case image: 173 | ImageAttachment imageAttachment = (ImageAttachment) message.getAttachment(); 174 | 175 | JSONObject imageObject = new JSONObject(); 176 | imageObject.put("url", imageAttachment.getUrl()); 177 | imageObject.put("thumbUrl", imageAttachment.getThumbUrl()); 178 | imageObject.put("thumbPath", imageAttachment.getThumbPath()); 179 | imageObject.put("path", imageAttachment.getPath()); 180 | imageObject.put("width", imageAttachment.getWidth()); 181 | imageObject.put("height", imageAttachment.getHeight()); 182 | 183 | object.put("messageObject", imageObject); 184 | 185 | break; 186 | case audio: 187 | AudioAttachment audioAttachment = (AudioAttachment) message.getAttachment(); 188 | 189 | JSONObject audioObject = new JSONObject(); 190 | audioObject.put("url", audioAttachment.getUrl()); 191 | audioObject.put("path", audioAttachment.getPath()); 192 | audioObject.put("duration", audioAttachment.getDuration()); 193 | audioObject.put("isPlayed", !NIMSessionParser.isUnreadAudioMessage(message)); 194 | 195 | object.put("messageObject", audioObject); 196 | 197 | break; 198 | case video: 199 | VideoAttachment videoAttachment = (VideoAttachment) message.getAttachment(); 200 | 201 | JSONObject videoObject = new JSONObject(); 202 | videoObject.put("url", videoAttachment.getUrl()); 203 | videoObject.put("coverUrl", videoAttachment.getThumbUrl()); 204 | videoObject.put("path", videoAttachment.getPath()); 205 | videoObject.put("duration", videoAttachment.getDuration()); 206 | videoObject.put("width", videoAttachment.getWidth()); 207 | videoObject.put("height", videoAttachment.getHeight()); 208 | 209 | object.put("messageObject", videoObject); 210 | 211 | break; 212 | case custom: 213 | FlutterNIMCustomAttachment customAttachment = (FlutterNIMCustomAttachment) message.getAttachment(); 214 | object.put("customMessageContent", customAttachment.toJson(false)); 215 | 216 | break; 217 | default: 218 | break; 219 | } 220 | 221 | } catch (JSONException exception) { 222 | exception.printStackTrace(); 223 | } 224 | 225 | return object; 226 | } 227 | 228 | private static String getUserExt(String account) { 229 | NimUserInfo user = NIMClient.getService(UserService.class).getUserInfo(account); 230 | if (user == null || user.getExtension() == null) { 231 | return ""; 232 | } else { 233 | return user.getExtension(); 234 | } 235 | } 236 | 237 | /** 238 | * @param account 用户帐号 239 | * @return 用户名 240 | */ 241 | private static String getUserDisplayName(String account) { 242 | 243 | NimUserInfo user = NIMClient.getService(UserService.class).getUserInfo(account); 244 | if (user == null) { 245 | return "买家"; 246 | } else { 247 | return user.getName(); 248 | } 249 | } 250 | 251 | /** 252 | * @param account 用户帐号 253 | * @return 用户头像链接地址 254 | */ 255 | private static String getUserAvatar(String account) { 256 | 257 | NimUserInfo user = NIMClient.getService(UserService.class).getUserInfo(account); 258 | if (user != null) { 259 | return user.getAvatar(); 260 | } else { 261 | return null; 262 | } 263 | } 264 | 265 | /** 266 | * 最近联系人列表项文案定制 267 | * 268 | * @param recent 最近联系人 269 | * @return 默认文案 270 | */ 271 | private static String getDefaultDigest(RecentContact recent) { 272 | switch (recent.getMsgType()) { 273 | case text: 274 | return recent.getContent(); 275 | case image: 276 | return "[图片]"; 277 | case video: 278 | return "[视频]"; 279 | case audio: 280 | return "[语音消息]"; 281 | case location: 282 | return "[位置]"; 283 | case file: 284 | return "[文件]"; 285 | case tip: 286 | List uuids = new ArrayList<>(); 287 | uuids.add(recent.getRecentMessageId()); 288 | List messages = NIMClient.getService(MsgService.class).queryMessageListByUuidBlock(uuids); 289 | if (messages != null && messages.size() > 0) { 290 | return messages.get(0).getContent(); 291 | } 292 | return "[通知提醒]"; 293 | case notification: 294 | return "[通知消息]"; 295 | case robot: 296 | return "[机器人消息]"; 297 | case custom: 298 | return "[自定义消息]"; 299 | default: 300 | return "[自定义消息]"; 301 | } 302 | } 303 | 304 | static boolean isUnreadAudioMessage(IMMessage message) { 305 | if ((message.getMsgType() == MsgTypeEnum.audio) 306 | && message.getDirect() == MsgDirectionEnum.In 307 | && message.getAttachStatus() == AttachStatusEnum.transferred 308 | && message.getStatus() != MsgStatusEnum.read) { 309 | return true; 310 | } else { 311 | return false; 312 | } 313 | } 314 | 315 | } 316 | --------------------------------------------------------------------------------