From 42ba10ec616849c5cfc9d734f6329ae745dea836 Mon Sep 17 00:00:00 2001 From: Jason Date: Thu, 20 Oct 2022 14:21:39 +0800 Subject: [PATCH] =?UTF-8?q?=E5=9F=BA=E7=A1=80=E9=A1=B5=E9=9D=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- assets/icons/tabBar_03.png | Bin 2454 -> 0 bytes ios/Podfile | 2 +- ios/Podfile.lock | 108 +++++ ios/Runner.xcodeproj/project.pbxproj | 78 +++- .../contents.xcworkspacedata | 3 + ios/Runner/Info.plist | 8 +- lib/configs/themes.dart | 2 +- lib/controllers/group_controller.dart | 228 ++++++++++ lib/controllers/private_controller.dart | 111 +++++ lib/models/im/calling_model.dart | 121 ++++++ lib/models/im/contact_info_model.dart | 21 + lib/models/im/custom_message_model.dart | 25 ++ lib/models/im/emoji_model.dart | 14 + lib/models/im/evaluation_model.dart | 29 ++ lib/models/im/group_card_model.dart | 33 ++ lib/models/im/group_conversation_model.dart | 25 ++ lib/models/im/location_model.dart | 23 + lib/models/im/name_card_model.dart | 33 ++ lib/models/im/private_conversation_model.dart | 40 ++ lib/models/im/search_user_model.dart | 18 + lib/models/im/transfer_model.dart | 32 ++ lib/routes/app_router.dart | 8 +- lib/routes/app_routes.dart | 13 +- lib/routes/contact_routes.dart | 47 ++ lib/routes/conversation_routes.dart | 36 ++ lib/routes/user_routes.dart | 24 ++ lib/services/auth_service.dart | 32 +- lib/services/tim/apply_service.dart | 50 +++ lib/services/tim/block_service.dart | 56 +++ lib/services/tim/conversation_service.dart | 379 ++++++++++++++++ lib/services/tim/friend_service.dart | 241 +++++++++++ lib/services/tim/group_service.dart | 403 ++++++++++++++++++ lib/services/tim/message_service.dart | 77 ++++ lib/services/tim_service.dart | 392 ++++++++++++++++- lib/utils/convert.dart | 30 ++ lib/utils/im_tools.dart | 251 +++++++++++ lib/utils/request/http.dart | 121 ++++++ lib/utils/request/http_interceptor.dart | 97 +++++ lib/utils/request/http_options.dart | 5 + lib/utils/request/http_request.dart | 91 ++++ .../contact/group/create/index_page.dart | 15 + .../contact/group/manage/index_page.dart | 15 + .../group/notification/index_page.dart | 17 + lib/views/conversation/index_page.dart | 82 ++++ lib/views/conversation/info/group_page.dart | 235 ++++++++++ lib/views/conversation/info/private_page.dart | 157 +++++++ .../info/widgets/group_member_preview.dart | 209 +++++++++ lib/views/home/index_page.dart | 216 +++++++++- lib/views/home/widgets/action_button.dart | 39 ++ lib/views/home/widgets/action_item.dart | 76 ++++ lib/views/home/widgets/conversation_item.dart | 212 +++++++++ lib/views/home/widgets/friend_selector.dart | 140 ++++++ lib/views/home/widgets/group_avatar.dart | 68 +++ .../home/widgets/group_user_selector.dart | 111 +++++ .../home/widgets/message_preview_widget.dart | 26 ++ lib/views/home/widgets/pop_menu_item.dart | 29 ++ lib/views/public/app_page.dart | 26 +- lib/views/user/qr_code/index_page.dart | 15 + pubspec.lock | 177 +++++++- pubspec.yaml | 10 +- .../flutter/generated_plugin_registrant.cc | 3 + windows/flutter/generated_plugins.cmake | 1 + 62 files changed, 5132 insertions(+), 54 deletions(-) delete mode 100644 assets/icons/tabBar_03.png create mode 100644 ios/Podfile.lock create mode 100644 lib/controllers/group_controller.dart create mode 100644 lib/controllers/private_controller.dart create mode 100644 lib/models/im/calling_model.dart create mode 100644 lib/models/im/contact_info_model.dart create mode 100644 lib/models/im/custom_message_model.dart create mode 100644 lib/models/im/emoji_model.dart create mode 100644 lib/models/im/evaluation_model.dart create mode 100644 lib/models/im/group_card_model.dart create mode 100644 lib/models/im/group_conversation_model.dart create mode 100644 lib/models/im/location_model.dart create mode 100644 lib/models/im/name_card_model.dart create mode 100644 lib/models/im/private_conversation_model.dart create mode 100644 lib/models/im/search_user_model.dart create mode 100644 lib/models/im/transfer_model.dart create mode 100644 lib/routes/conversation_routes.dart create mode 100644 lib/routes/user_routes.dart create mode 100644 lib/services/tim/apply_service.dart create mode 100644 lib/services/tim/block_service.dart create mode 100644 lib/services/tim/conversation_service.dart create mode 100644 lib/services/tim/friend_service.dart create mode 100644 lib/services/tim/group_service.dart create mode 100644 lib/services/tim/message_service.dart create mode 100644 lib/utils/convert.dart create mode 100644 lib/utils/im_tools.dart create mode 100644 lib/utils/request/http.dart create mode 100644 lib/utils/request/http_interceptor.dart create mode 100644 lib/utils/request/http_options.dart create mode 100644 lib/utils/request/http_request.dart create mode 100644 lib/views/contact/group/create/index_page.dart create mode 100644 lib/views/contact/group/manage/index_page.dart create mode 100644 lib/views/contact/group/notification/index_page.dart create mode 100644 lib/views/conversation/index_page.dart create mode 100644 lib/views/conversation/info/group_page.dart create mode 100644 lib/views/conversation/info/private_page.dart create mode 100644 lib/views/conversation/info/widgets/group_member_preview.dart create mode 100644 lib/views/home/widgets/action_button.dart create mode 100644 lib/views/home/widgets/action_item.dart create mode 100644 lib/views/home/widgets/conversation_item.dart create mode 100644 lib/views/home/widgets/friend_selector.dart create mode 100644 lib/views/home/widgets/group_avatar.dart create mode 100644 lib/views/home/widgets/group_user_selector.dart create mode 100644 lib/views/home/widgets/message_preview_widget.dart create mode 100644 lib/views/home/widgets/pop_menu_item.dart create mode 100644 lib/views/user/qr_code/index_page.dart diff --git a/assets/icons/tabBar_03.png b/assets/icons/tabBar_03.png deleted file mode 100644 index 4220624d8e7ba60e3b08766d04c7384dafb26652..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2454 zcmbVO3se(V8V&)%+Xo<`w%uW3FqJ%(k~lw+$K$0Kigjl2UL3mP1>pJ6=87orO7Iq!vpil>TIi1A z?K_akeq06rqO8 zWJa;kt0kROaXmw>tS_#OZ50$y4GE%9T(ft%!T$nSP}Sj?}6C;ehz9GXU8XcEFz ztKq`XhLF{0g0wV4LUNHGG_@D_Qo~M;^I`~6TU#rtm5OMl3PDvW6(W`(5{VEXglwIM!~H@Jn?3D8N3wQ? z@^TdIfkGZ}8(qVxVGwDUg4-K$>tUy40)ioa+>4+hafs41&}jVMP`5jRW;rwYQ@nR| z%v$QaBw{96x`wfn0G>U)70c;;AjB!M_hGE}ksdB$PS_*1-WtX=DHCBwNEhh_Fcz>v zBXN42G)J>e`W_-gmLpPlF)c&l9L-p0+BJR3rfDybRtx2nQ67S>WgiXEnSzr#oFmmR zIB-;mN`z9>g32*eilNF}u@V!D!%!nlP>#C0L8Tx)q<26;wh=go-w)Vm#0(ym!##G= zpi{#jC=o>wm=bkJB}$n>C{f|KFwaKFg-V;;CL{=(1D7gQQiqJ3j0@Q7XnRdaylHzP zWG~TZ6-?ky8~Pg^7sOfkiR#HyPr{gvd}sga8Uv4N7naC@kX z0U;d33{OqoHQ@U?+pyDGCkb5MnVNUxTNuXD4z3nw$iga+2mhV;5+Ey-W5n&0Csa@% z`NL**C+?{tL1RVW5K$!5g2K#@@Cp1xdUEY&?P?khdSGZ6X};hi(!NO#aLs_`jC@x! zm&c2?7<7de>z3g?D~c_J4|FeG`Rc2t=I1NR{-qvWq)Y!IOP3cvr=`=76??E#*9>Ln z7-CZvW4@t`Jv(;KXwj9%ZGo|Pz0NfJr(U&g(UxT6u`|EgPxmWMDXw+A7(b>SK0m(x z)V1b|d+~w8SFVuq7WomyGRvyQb4L=r9joRndUWTm zV+$oit`u?65PtpH?W&UYu7|sCSr_m-w?3%NUw$FB4^2pZI(0Ai&V&9Z56ruiwIHs! z)>_J#&+h~~xP&7DC478WOhz#J6QcZV-P6l@Tb$YH@_jMZB}Z498#8(@J9j6~I<392 z!*OWt>nS-OjXW9jAF|KhI6Ery4F80GU_(n|zO^H*&D~$Ly68gdrJ7BtBkTW&e`w55 zkG|}&@C#;rTl)L7tQ>6O%T|%5b4^cz>xuW?MJ}#s>Yw4dt{?064YfQR{4;O!_)D{c zTXlBv@lAm%t7>k3`s2;|N_|G}SJy_UhxS}}qbX2Pn%aF=9e=d!s|6oleMC_< zN7TKmFE-fmO50~u!IRYagC(p%Gi2uOp!o#o^E6O+^D<~WUiFNhobL4%C!eJW}GVB_9~ znd@TK*AJ@$c%QMeyqBmuS@G6~xArxv2MdF5L>(g*$~R=??ix%vVjI73;H>(C2Ibnc zCzA3VucuZSf2prq{f(gL&_9m77Spipo4551x7Qx%OKF|i`>bir*A+2>Hp>^j^a<;V zRoiz~$F@fw)KG6#^E2NuCtQz7Y@45^oVc0ijdI5M1_$5&=L1*K+n+qXuq%6i{nqdL zzWz*kwWIu{F<}qyTfwf0irb0f8Z>(Omj&;3)t@|{+mn3sO?h9^y4d&n-sbIIJ;570 Wd>Y-C+_W+D&&i-K(Y0$TH~t4eRCx&i diff --git a/ios/Podfile b/ios/Podfile index 1e8c3c9..252d9ec 100644 --- a/ios/Podfile +++ b/ios/Podfile @@ -1,5 +1,5 @@ # Uncomment this line to define a global platform for your project -# platform :ios, '9.0' +platform :ios, '9.0' # CocoaPods analytics sends network stats synchronously affecting flutter build latency. ENV['COCOAPODS_DISABLE_STATS'] = 'true' diff --git a/ios/Podfile.lock b/ios/Podfile.lock new file mode 100644 index 0000000..d75152d --- /dev/null +++ b/ios/Podfile.lock @@ -0,0 +1,108 @@ +PODS: + - Flutter (1.0.0) + - fluttertoast (0.0.2): + - Flutter + - Toast + - FMDB (2.7.5): + - FMDB/standard (= 2.7.5) + - FMDB/standard (2.7.5) + - HydraAsync (2.0.6) + - image_cropper (0.0.4): + - Flutter + - TOCropViewController (~> 2.6.1) + - open_file (0.0.1): + - Flutter + - path_provider_ios (0.0.1): + - Flutter + - photo_manager (2.0.0): + - Flutter + - FlutterMacOS + - scan (0.0.1): + - Flutter + - smart_auth (0.0.1): + - Flutter + - sqflite (0.0.2): + - Flutter + - FMDB (>= 2.7.5) + - tencent_im_sdk_plugin (4.0.3): + - Flutter + - HydraAsync + - TXIMSDK_Plus_iOS (= 6.7.3184) + - Toast (4.0.0) + - TOCropViewController (2.6.1) + - TXIMSDK_Plus_iOS (6.7.3184) + - vibration (1.7.5): + - Flutter + - video_player_avfoundation (0.0.1): + - Flutter + +DEPENDENCIES: + - Flutter (from `Flutter`) + - fluttertoast (from `.symlinks/plugins/fluttertoast/ios`) + - image_cropper (from `.symlinks/plugins/image_cropper/ios`) + - open_file (from `.symlinks/plugins/open_file/ios`) + - path_provider_ios (from `.symlinks/plugins/path_provider_ios/ios`) + - photo_manager (from `.symlinks/plugins/photo_manager/ios`) + - scan (from `.symlinks/plugins/scan/ios`) + - smart_auth (from `.symlinks/plugins/smart_auth/ios`) + - sqflite (from `.symlinks/plugins/sqflite/ios`) + - tencent_im_sdk_plugin (from `.symlinks/plugins/tencent_im_sdk_plugin/ios`) + - vibration (from `.symlinks/plugins/vibration/ios`) + - video_player_avfoundation (from `.symlinks/plugins/video_player_avfoundation/ios`) + +SPEC REPOS: + trunk: + - FMDB + - HydraAsync + - Toast + - TOCropViewController + - TXIMSDK_Plus_iOS + +EXTERNAL SOURCES: + Flutter: + :path: Flutter + fluttertoast: + :path: ".symlinks/plugins/fluttertoast/ios" + image_cropper: + :path: ".symlinks/plugins/image_cropper/ios" + open_file: + :path: ".symlinks/plugins/open_file/ios" + path_provider_ios: + :path: ".symlinks/plugins/path_provider_ios/ios" + photo_manager: + :path: ".symlinks/plugins/photo_manager/ios" + scan: + :path: ".symlinks/plugins/scan/ios" + smart_auth: + :path: ".symlinks/plugins/smart_auth/ios" + sqflite: + :path: ".symlinks/plugins/sqflite/ios" + tencent_im_sdk_plugin: + :path: ".symlinks/plugins/tencent_im_sdk_plugin/ios" + vibration: + :path: ".symlinks/plugins/vibration/ios" + video_player_avfoundation: + :path: ".symlinks/plugins/video_player_avfoundation/ios" + +SPEC CHECKSUMS: + Flutter: 50d75fe2f02b26cc09d224853bb45737f8b3214a + fluttertoast: 74526702fea2c060ea55dde75895b7e1bde1c86b + FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a + HydraAsync: 8d589bd725b0224f899afafc9a396327405f8063 + image_cropper: 60c2789d1f1a78c873235d4319ca0c34a69f2d98 + open_file: 02eb5cb6b21264bd3a696876f5afbfb7ca4f4b7d + path_provider_ios: 14f3d2fd28c4fdb42f44e0f751d12861c43cee02 + photo_manager: 4f6810b7dfc4feb03b461ac1a70dacf91fba7604 + scan: aea35bb4aa59ccc8839c576a18cd57c7d492cc86 + smart_auth: 4bedbc118723912d0e45a07e8ab34039c19e04f2 + sqflite: 6d358c025f5b867b29ed92fc697fd34924e11904 + tencent_im_sdk_plugin: 26c668a5d2f456a5541e2c820dcfcb3d15fdba9a + Toast: 91b396c56ee72a5790816f40d3a94dd357abc196 + TOCropViewController: edfd4f25713d56905ad1e0b9f5be3fbe0f59c863 + TXIMSDK_Plus_iOS: 5412f55a77f058b2b5a8575900334daccbae3b08 + vibration: 7d883d141656a1c1a6d8d238616b2042a51a1241 + video_player_avfoundation: e489aac24ef5cf7af82702979ed16f2a5ef84cff + +PODFILE CHECKSUM: a75497545d4391e2d394c3668e20cfb1c2bbd4aa + +COCOAPODS: 1.11.3 diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index c4ffba9..78c973f 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -3,12 +3,13 @@ archiveVersion = 1; classes = { }; - objectVersion = 50; + objectVersion = 51; objects = { /* Begin PBXBuildFile section */ 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; + 70F84DE20E16DC49868EF51A /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7CDE8CA2C88702AA59A75259 /* Pods_Runner.framework */; }; 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; @@ -32,9 +33,12 @@ 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; + 3F9F50CF9C873D1460CCEE80 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; + 5F1992123E9973AEE97006AF /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; + 7CDE8CA2C88702AA59A75259 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -42,6 +46,7 @@ 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + E29514A41E605422DF139C6D /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -49,6 +54,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 70F84DE20E16DC49868EF51A /* Pods_Runner.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -72,6 +78,8 @@ 9740EEB11CF90186004384FC /* Flutter */, 97C146F01CF9000F007C117D /* Runner */, 97C146EF1CF9000F007C117D /* Products */, + C394D966EF1617064336C312 /* Pods */, + C37FF88E875204B875862A7F /* Frameworks */, ); sourceTree = ""; }; @@ -98,6 +106,24 @@ path = Runner; sourceTree = ""; }; + C37FF88E875204B875862A7F /* Frameworks */ = { + isa = PBXGroup; + children = ( + 7CDE8CA2C88702AA59A75259 /* Pods_Runner.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; + C394D966EF1617064336C312 /* Pods */ = { + isa = PBXGroup; + children = ( + 5F1992123E9973AEE97006AF /* Pods-Runner.debug.xcconfig */, + 3F9F50CF9C873D1460CCEE80 /* Pods-Runner.release.xcconfig */, + E29514A41E605422DF139C6D /* Pods-Runner.profile.xcconfig */, + ); + path = Pods; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -105,12 +131,14 @@ isa = PBXNativeTarget; buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; buildPhases = ( + 70B6D40A25B40589C6B223C0 /* [CP] Check Pods Manifest.lock */, 9740EEB61CF901F6004384FC /* Run Script */, 97C146EA1CF9000F007C117D /* Sources */, 97C146EB1CF9000F007C117D /* Frameworks */, 97C146EC1CF9000F007C117D /* Resources */, 9705A1C41CF9048500538489 /* Embed Frameworks */, 3B06AD1E1E4923F5004D2608 /* Thin Binary */, + 2EC1B54F88AAEC33384CE737 /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); @@ -169,6 +197,23 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ + 2EC1B54F88AAEC33384CE737 /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; @@ -183,6 +228,28 @@ shellPath = /bin/sh; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; }; + 70B6D40A25B40589C6B223C0 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; 9740EEB61CF901F6004384FC /* Run Script */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; @@ -287,13 +354,14 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; - CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + CURRENT_PROJECT_VERSION = 1; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); + MARKETING_VERSION = 1.0.1; PRODUCT_BUNDLE_IDENTIFIER = site.zhchain.chat; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; @@ -415,13 +483,14 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; - CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + CURRENT_PROJECT_VERSION = 1; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); + MARKETING_VERSION = 1.0.1; PRODUCT_BUNDLE_IDENTIFIER = site.zhchain.chat; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; @@ -437,13 +506,14 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; - CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + CURRENT_PROJECT_VERSION = 1; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); + MARKETING_VERSION = 1.0.1; PRODUCT_BUNDLE_IDENTIFIER = site.zhchain.chat; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; diff --git a/ios/Runner.xcworkspace/contents.xcworkspacedata b/ios/Runner.xcworkspace/contents.xcworkspacedata index 1d526a1..21a3cc1 100644 --- a/ios/Runner.xcworkspace/contents.xcworkspacedata +++ b/ios/Runner.xcworkspace/contents.xcworkspacedata @@ -4,4 +4,7 @@ + + diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist index 4a4bca8..50f3119 100644 --- a/ios/Runner/Info.plist +++ b/ios/Runner/Info.plist @@ -5,7 +5,7 @@ CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) CFBundleDisplayName - Chat + ZH-Chat CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIdentifier @@ -13,15 +13,15 @@ CFBundleInfoDictionaryVersion 6.0 CFBundleName - chat + zhchat CFBundlePackageType APPL CFBundleShortVersionString - $(FLUTTER_BUILD_NAME) + 1.0.0 CFBundleSignature ???? CFBundleVersion - $(FLUTTER_BUILD_NUMBER) + 1 LSRequiresIPhoneOS UILaunchStoryboardName diff --git a/lib/configs/themes.dart b/lib/configs/themes.dart index 3bd59db..ae9c294 100644 --- a/lib/configs/themes.dart +++ b/lib/configs/themes.dart @@ -52,7 +52,7 @@ class Themes { fontSize: 11, ), unselectedLabelStyle: TextStyle( - fontSize: 10, + fontSize: 11, ), ), ); diff --git a/lib/controllers/group_controller.dart b/lib/controllers/group_controller.dart new file mode 100644 index 0000000..96ed1ff --- /dev/null +++ b/lib/controllers/group_controller.dart @@ -0,0 +1,228 @@ +import 'package:adaptive_dialog/adaptive_dialog.dart'; +import 'package:chat/models/im/group_conversation_model.dart'; +import 'package:chat/services/auth_service.dart'; +import 'package:chat/services/tim/conversation_service.dart'; +import 'package:chat/services/tim/group_service.dart'; +import 'package:chat/services/tim_service.dart'; +import 'package:chat/utils/ui_tools.dart'; +import 'package:get/get.dart'; +import 'package:tencent_im_sdk_plugin/enum/group_member_filter_enum.dart'; +import 'package:tencent_im_sdk_plugin/enum/group_member_role.dart'; +import 'package:tencent_im_sdk_plugin/enum/group_member_role_enum.dart'; +import 'package:tencent_im_sdk_plugin/models/v2_tim_group_member_full_info.dart'; + +class GroupController extends GetxController { + static GroupController get to => Get.find(); + + Rx currentGroup = + GroupConversationModel(groupID: '').obs; + + @override + void onClose() { + currentGroup.value = GroupConversationModel(groupID: ''); + TimService.to.currentConversationId.value = ''; + super.onClose(); + } + + /// 设置当前操作的群组 + Future setCurrentGroup(String groupId) async { + var group = await TimGroupService.to.info(groupId); + + if (group != null) { + TimService.to.currentConversationId.value = 'group_' + groupId; + + currentGroup.value.group = group; + + currentGroup.value.groupID = groupId; + + var selfInfo = await TimGroupService.to.getMemberInfo( + group, + AuthService.to.userId, + ); + + if (selfInfo?.role == GroupMemberRoleType.V2TIM_GROUP_MEMBER_ROLE_ADMIN) { + currentGroup.value.isAdmin = true; + } + + if (selfInfo?.role == GroupMemberRoleType.V2TIM_GROUP_MEMBER_ROLE_OWNER) { + currentGroup.value.isOwner = true; + } + + currentGroup.value.selfInfo = await TimGroupService.to.getMemberInfo( + group, + AuthService.to.userId, + ); + + currentGroup.value.conversation = await TimConversationService.to.getById( + 'group_' + groupId, + ); + + currentGroup.refresh(); + } + } + + /// 获取群成员列表 + Future fetchGroupMemberList() async { + var members = await TimGroupService.to.members( + currentGroup.value.groupID, + count: 13, + ); + + currentGroup.value.memberList = members; + + var admins = await TimGroupService.to.members( + currentGroup.value.groupID, + count: 100, + filter: GroupMemberFilterTypeEnum.V2TIM_GROUP_MEMBER_FILTER_ADMIN, + ); + + currentGroup.value.adminList = admins; + currentGroup.refresh(); + } + + /// 更新群名称 + Future updateGroupName(String name) async { + var result = await TimGroupService.to.updateName( + currentGroup.value.group!, + name, + ); + + if (result) { + currentGroup.value.group!.groupName = name; + currentGroup.value.conversation!.showName = name; + currentGroup.refresh(); + UiTools.toast('群名称修改成功'); + Get.back(); + } + } + + /// 更新我的群名片 + Future updateGroupNameCard(String nameCard) async { + var res = await TimGroupService.to.setMemberInfo( + currentGroup.value.group!, + AuthService.to.userId, + nameCard, + ); + if (res) { + currentGroup.value.selfInfo!.nameCard = nameCard; + currentGroup.refresh(); + + UiTools.toast('群名片修改成功'); + Get.back(); + } + } + + Future updateGroupNotification(String notification) async { + var res = await TimGroupService.to.updateNotification( + currentGroup.value.group!, + notification, + ); + if (res) { + currentGroup.value.group!.notification = notification; + currentGroup.refresh(); + + UiTools.toast('群公告更新成功'); + Get.back(); + } + } + + Future togglePinned() async { + var res = await TimConversationService.to.setOnTop( + currentGroup.value.conversation!, + ); + + if (res) { + currentGroup.value.conversation = await TimConversationService.to.getById( + 'group_' + currentGroup.value.groupID, + ); + + currentGroup.refresh(); + UiTools.toast('修改成功'); + } + } + + Future toggleReceiveOpt() async { + var res = await TimConversationService.to.setReceiveOpt( + currentGroup.value.conversation!, + ); + + if (res) { + currentGroup.value.conversation = await TimConversationService.to.getById( + 'group_' + currentGroup.value.groupID, + ); + currentGroup.refresh(); + + UiTools.toast('修改成功'); + } + } + + /// 移除群成员 + Future kick(List ids) async { + var result = await TimGroupService.to.kickMember( + GroupController.to.currentGroup.value.group!, + ids, + ); + + if (result) { + setCurrentGroup(currentGroup.value.groupID); + fetchGroupMemberList(); + } + + return result; + } + + Future transfer(V2TimGroupMemberFullInfo member) async { + OkCancelResult result = await showOkCancelAlertDialog( + style: AdaptiveStyle.iOS, + context: Get.context!, + title: '操作提示', + message: '确定选择 ${member.nickName} 为新群主,您将自动放弃群主身份。', + okLabel: '确定', + cancelLabel: '取消', + defaultType: OkCancelAlertDefaultType.ok, + ); + + if (result == OkCancelResult.ok) { + var res = await TimGroupService.to.transfer( + currentGroup.value.group!, + member.userID, + ); + + if (res) { + /// 直接修改当前用户的身份,为普通用户 + currentGroup.value.isAdmin = false; + currentGroup.value.isOwner = false; + await fetchGroupMemberList(); + } + + return res; + } + return false; + } + + Future setAdmin(String userID) async { + var result = await TimGroupService.to.setMemberRole( + currentGroup.value.group!, + userID, + GroupMemberRoleTypeEnum.V2TIM_GROUP_MEMBER_ROLE_ADMIN, + ); + + if (result) { + UiTools.toast('设置群管理成功'); + } + return result; + } + + Future cancelAdmin(String userID) async { + var result = await TimGroupService.to.setMemberRole( + currentGroup.value.group!, + userID, + GroupMemberRoleTypeEnum.V2TIM_GROUP_MEMBER_ROLE_MEMBER, + ); + if (result) { + UiTools.toast('取消群管理成功'); + } + + return result; + } +} diff --git a/lib/controllers/private_controller.dart b/lib/controllers/private_controller.dart new file mode 100644 index 0000000..1b1c0f9 --- /dev/null +++ b/lib/controllers/private_controller.dart @@ -0,0 +1,111 @@ +import 'package:chat/models/im/private_conversation_model.dart'; +import 'package:chat/services/tim/conversation_service.dart'; +import 'package:chat/services/tim/friend_service.dart'; +import 'package:chat/services/tim_service.dart'; +import 'package:chat/utils/ui_tools.dart'; +import 'package:get/get.dart'; +import 'package:tencent_im_sdk_plugin/models/v2_tim_friend_info_result.dart'; + +class PrivateController extends GetxController { + static PrivateController get to => Get.find(); + + Rx currentFriend = + PrivateConversationModel(userID: '').obs; + + @override + void onClose() { + currentFriend.value = PrivateConversationModel(userID: ''); + TimService.to.currentConversationId.value = ''; + super.onClose(); + } + + /// 设置当前好友 + Future setCurrentFriend(String userID) async { + V2TimFriendInfoResult? info = await TimFriendService.to.friendInfo(userID); + + if (info != null) { + TimService.to.currentConversationId.value = 'c2c_' + userID; + + currentFriend.value.userID = userID; + currentFriend.value.isFriend = + info.relation == UserRelationEnum.V2TIM_FRIEND_RELATION_TYPE_BOTH_WAY; + + currentFriend.value.friendRemark = info.friendInfo!.friendRemark!; + currentFriend.value.userProfile = info.friendInfo!.userProfile; + + /// 通过自定义Staffer字段,判断是否是客服,属于哪个店铺 + if (info.friendInfo!.userProfile?.customInfo!['Staffer']?.isNotEmpty == + true) { + currentFriend.value.shopId = + info.friendInfo!.userProfile?.customInfo!['Staffer']; + currentFriend.value.isStaffer = true; + } + + /// 通过用户自定义字段,判断是否允许陌生人消息 + if (info.friendInfo!.userProfile?.customInfo!['Stranger']?.isNotEmpty == + true) { + currentFriend.value.allowStranger = false; + } + + /// 设置会话 + currentFriend.value.conversation = + await TimConversationService.to.getById('c2c_' + userID); + + currentFriend.refresh(); + } + } + + Future togglePinned() async { + var res = await TimConversationService.to.setOnTop( + currentFriend.value.conversation!, + ); + + if (res) { + currentFriend.value.conversation = + await TimConversationService.to.getById( + 'c2c_' + currentFriend.value.userID, + ); + + currentFriend.refresh(); + UiTools.toast('修改成功'); + } + } + + Future changeReceiveOpt() async { + var res = await TimConversationService.to.setReceiveOpt( + currentFriend.value.conversation!, + ); + + if (res) { + currentFriend.value.conversation = + await TimConversationService.to.getById( + 'c2c_' + currentFriend.value.userID, + ); + currentFriend.refresh(); + + UiTools.toast('修改成功'); + } + } + + Future setRemark(String remark) async { + var result = await TimFriendService.to.setFriendRemark( + currentFriend.value.userID, + remark, + ); + + if (result) { + currentFriend.value.friendRemark = remark; + + currentFriend.value.conversation = + await TimConversationService.to.getById( + 'c2c_' + currentFriend.value.userID, + ); + + currentFriend.refresh(); + + TimConversationService.to.fetchList(); + UiTools.toast('备注修改成功'); + Get.back(); + } + } +} diff --git a/lib/models/im/calling_model.dart b/lib/models/im/calling_model.dart new file mode 100644 index 0000000..0a5fdad --- /dev/null +++ b/lib/models/im/calling_model.dart @@ -0,0 +1,121 @@ +import 'dart:convert'; + +import 'package:chat/models/im/custom_message_model.dart'; + +class CallingModel extends CustomMessageModel { + CallingModel({ + this.businessID = CustomMessageType.CALL, + required this.callType, + required this.inviter, + required this.inviteeList, + required this.data, + required this.timeout, + required this.actionType, + required this.onlineUserOnly, + required this.isGroup, + }); + + @override + String businessID; + + /// 通话类型 videoCall audioCall + String callType; + + /// 邀请人 + String inviter; + + /// 被邀请人 + List inviteeList; + + /// 通话时长 + int timeout; + // 1: 邀请方发起邀请 + // 2: 邀请方取消邀请 + // 3: 被邀请方接受邀请 + // 4: 被邀请方拒绝邀请 + // 5: 邀请超时 + int actionType; + + bool onlineUserOnly; + + /// 是否是群语音 + bool isGroup; + CallingModelData data; + + String get actionTypeText { + final actionMessage = { + 1: "发起通话", + 2: "取消通话", + 3: "接受通话", + 4: "拒绝通话", + 5: "超时未接听", + }; + return actionMessage[actionType] ?? ""; + } + + factory CallingModel.fromJson(Map json) => CallingModel( + callType: jsonDecode(json['data'])['data']['cmd'], + inviter: json['inviter'], + inviteeList: List.from( + json['inviteeList'].map( + (x) => x.toString(), + ), + ), + data: CallingModelData.fromJson(jsonDecode(json['data'])), + timeout: json['timeout'], + actionType: json['actionType'], + onlineUserOnly: json['onlineUserOnly'], + isGroup: jsonDecode(json['data'])['is_group'], + ); + + @override + String toJson() { + return ''; + } +} + +class CallingModelData { + CallingModelData({ + required this.version, + required this.callType, + required this.data, + required this.roomId, + required this.isGroup, + }); + + int version; + int callType; + DataData data; + int roomId; + bool isGroup; + + factory CallingModelData.fromJson(Map json) => + CallingModelData( + version: json['version'], + callType: json['call_type'], + data: DataData.fromJson(json['data']), + roomId: json['room_id'], + isGroup: json['is_group'], + ); +} + +class DataData { + DataData({ + required this.cmd, + required this.roomId, + required this.message, + required this.cmdInfo, + }); + + String cmd; // videoCall audioCall + int roomId; + String message; + String cmdInfo; + + factory DataData.fromJson(Map json) => DataData( + cmd: json['cmd'], + roomId: json['room_id'], + message: json['message'], + cmdInfo: json['cmd_info'], + ); +} diff --git a/lib/models/im/contact_info_model.dart b/lib/models/im/contact_info_model.dart new file mode 100644 index 0000000..29d8de0 --- /dev/null +++ b/lib/models/im/contact_info_model.dart @@ -0,0 +1,21 @@ +import 'package:azlistview/azlistview.dart'; +import 'package:tencent_im_sdk_plugin/models/v2_tim_friend_info.dart'; + +class ContactInfoModel extends ISuspensionBean { + String name; + String userID; + String? tagIndex; + String? namePinyin; + V2TimFriendInfo? friendInfo; + + ContactInfoModel({ + required this.name, + required this.userID, + this.tagIndex, + this.namePinyin, + this.friendInfo, + }); + + @override + String getSuspensionTag() => tagIndex!; +} diff --git a/lib/models/im/custom_message_model.dart b/lib/models/im/custom_message_model.dart new file mode 100644 index 0000000..e7d7ce7 --- /dev/null +++ b/lib/models/im/custom_message_model.dart @@ -0,0 +1,25 @@ +abstract class CustomMessageModel { + abstract String businessID; + + String toJson(); +} + +class CustomMessageType { + // ignore: constant_identifier_names + static const String NAME_CARD = 'name_card'; + // ignore: constant_identifier_names + static const String GROUP_CARD = 'group_card'; + // ignore: constant_identifier_names + static const String DT_TRANSFER = 'dt_transfer'; + // ignore: constant_identifier_names + static const String TYPING_STATUS = 'user_typing_status'; + // ignore: constant_identifier_names + static const String EVALUATION = 'evaluation'; + // ignore: constant_identifier_names + static const String CALL = '1'; +} + +class CallingType { + static const String audioCall = 'audioCall'; + static const String videoCall = 'videoCall'; +} diff --git a/lib/models/im/emoji_model.dart b/lib/models/im/emoji_model.dart new file mode 100644 index 0000000..099df36 --- /dev/null +++ b/lib/models/im/emoji_model.dart @@ -0,0 +1,14 @@ +class EmojiModel { + EmojiModel({ + required this.name, + required this.unicode, + }); + + String name; + int unicode; + + factory EmojiModel.fromJson(Map json) => EmojiModel( + name: json['name'], + unicode: json['unicode'], + ); +} diff --git a/lib/models/im/evaluation_model.dart b/lib/models/im/evaluation_model.dart new file mode 100644 index 0000000..b06a02b --- /dev/null +++ b/lib/models/im/evaluation_model.dart @@ -0,0 +1,29 @@ +import 'package:chat/models/im/custom_message_model.dart'; + +class EvaluationModel extends CustomMessageModel { + @override + String businessID; + int version; + double score; + String comment; + + EvaluationModel({ + this.businessID = 'evaluation', + required this.version, + required this.score, + required this.comment, + }); + + factory EvaluationModel.fromJson(Map json) => + EvaluationModel( + businessID: json['businessID'], + version: json['version'], + score: double.parse(json['score']), + comment: json['comment'], + ); + + @override + String toJson() { + return ''; + } +} diff --git a/lib/models/im/group_card_model.dart b/lib/models/im/group_card_model.dart new file mode 100644 index 0000000..d3d636d --- /dev/null +++ b/lib/models/im/group_card_model.dart @@ -0,0 +1,33 @@ +import 'dart:convert'; + +import 'package:chat/models/im/custom_message_model.dart'; + +class GroupCardModel extends CustomMessageModel { + @override + String businessID; + String groupID; + String groupName; + String inviterID; + + GroupCardModel({ + this.businessID = CustomMessageType.GROUP_CARD, + required this.groupID, + required this.groupName, + required this.inviterID, + }); + + factory GroupCardModel.fromJson(Map json) => GroupCardModel( + businessID: json['businessID'], + groupID: json['groupID'], + groupName: json['groupName'], + inviterID: json['inviterID'], + ); + + @override + String toJson() => json.encode({ + 'businessID': businessID, + 'groupID': groupID, + 'groupName': groupName, + 'inviterID': inviterID, + }); +} diff --git a/lib/models/im/group_conversation_model.dart b/lib/models/im/group_conversation_model.dart new file mode 100644 index 0000000..1c373f3 --- /dev/null +++ b/lib/models/im/group_conversation_model.dart @@ -0,0 +1,25 @@ +import 'package:tencent_im_sdk_plugin/models/v2_tim_conversation.dart'; +import 'package:tencent_im_sdk_plugin/models/v2_tim_group_info.dart'; +import 'package:tencent_im_sdk_plugin/models/v2_tim_group_member_full_info.dart'; + +class GroupConversationModel { + String groupID; + V2TimGroupInfo? group; + List? memberList; + List? adminList; + V2TimGroupMemberFullInfo? selfInfo; + V2TimConversation? conversation; + bool isAdmin; + bool isOwner; + + GroupConversationModel({ + required this.groupID, + this.group, + this.memberList, + this.adminList, + this.selfInfo, + this.conversation, + this.isAdmin = false, + this.isOwner = false, + }); +} diff --git a/lib/models/im/location_model.dart b/lib/models/im/location_model.dart new file mode 100644 index 0000000..ff58fc8 --- /dev/null +++ b/lib/models/im/location_model.dart @@ -0,0 +1,23 @@ +class LocationModel { + LocationModel({ + required this.name, + required this.address, + required this.list, + required this.latitude, + required this.longitude, + }); + + String name; + String address; + List list; + double latitude; + double longitude; + + factory LocationModel.fromJson(Map json) => LocationModel( + name: json['name'], + address: json['address'], + list: List.from(json['list'].map((x) => x)), + latitude: json['latitude'].toDouble(), + longitude: json['longitude'].toDouble(), + ); +} diff --git a/lib/models/im/name_card_model.dart b/lib/models/im/name_card_model.dart new file mode 100644 index 0000000..65d858d --- /dev/null +++ b/lib/models/im/name_card_model.dart @@ -0,0 +1,33 @@ +import 'dart:convert'; + +import 'package:chat/models/im/custom_message_model.dart'; + +class NameCardModel extends CustomMessageModel { + @override + String businessID; + String avatar; + String userID; + String userName; + + NameCardModel({ + this.businessID = CustomMessageType.NAME_CARD, + required this.avatar, + required this.userID, + required this.userName, + }); + + factory NameCardModel.fromJson(Map json) => NameCardModel( + businessID: json['businessID'], + avatar: json['avatar'], + userID: json['userID'], + userName: json['userName'], + ); + + @override + String toJson() => json.encode({ + 'businessID': businessID, + 'avatar': avatar, + 'userID': userID, + 'userName': userName, + }); +} diff --git a/lib/models/im/private_conversation_model.dart b/lib/models/im/private_conversation_model.dart new file mode 100644 index 0000000..e73fbdc --- /dev/null +++ b/lib/models/im/private_conversation_model.dart @@ -0,0 +1,40 @@ +// ignore_for_file: constant_identifier_names + +import 'package:tencent_im_sdk_plugin/models/v2_tim_conversation.dart'; +import 'package:tencent_im_sdk_plugin/models/v2_tim_user_full_info.dart'; + +class PrivateConversationModel { + String userID; + bool isFriend; + String friendRemark; + V2TimConversation? conversation; + V2TimUserFullInfo? userProfile; + bool isStaffer; + bool allowStranger; // 允许陌生人消息 + String? shopId; // 他是哪个店铺的客服 + + PrivateConversationModel({ + required this.userID, + this.friendRemark = '', + this.isFriend = false, + this.conversation, + this.userProfile, + this.isStaffer = false, + this.allowStranger = true, + this.shopId, + }); +} + +class UserRelationEnum { + /// 不是好友 + static const int V2TIM_FRIEND_RELATION_TYPE_NONE = 0; + + /// 对方在我的好友列表 + static const int V2TIM_FRIEND_RELATION_TYPE_IN_MY_FRIEND_LIST = 1; + + /// 我在对方的好友列表 + static const int V2TIM_FRIEND_RELATION_TYPE_IN_OTHER_FRIEND_LIST = 2; + + /// 表示对方在我的好友列表中 + static const int V2TIM_FRIEND_RELATION_TYPE_BOTH_WAY = 3; +} diff --git a/lib/models/im/search_user_model.dart b/lib/models/im/search_user_model.dart new file mode 100644 index 0000000..de842c4 --- /dev/null +++ b/lib/models/im/search_user_model.dart @@ -0,0 +1,18 @@ +class SearchUserModel { + SearchUserModel({ + required this.userID, + required this.nickname, + required this.avatar, + }); + + String userID; + String nickname; + String avatar; + + factory SearchUserModel.fromJson(Map json) => + SearchUserModel( + userID: json['user_id'].toString(), + nickname: json['nickname'], + avatar: json['avatar'], + ); +} diff --git a/lib/models/im/transfer_model.dart b/lib/models/im/transfer_model.dart new file mode 100644 index 0000000..1e0ba00 --- /dev/null +++ b/lib/models/im/transfer_model.dart @@ -0,0 +1,32 @@ +import 'dart:convert'; + +import 'package:chat/models/im/custom_message_model.dart'; + +class TransferModel extends CustomMessageModel { + @override + String businessID; + String amount; + int orderId; + bool isReceived; + + TransferModel({ + this.businessID = CustomMessageType.DT_TRANSFER, + required this.amount, + required this.orderId, + this.isReceived = false, + }); + + factory TransferModel.fromJson(Map json) => TransferModel( + businessID: json['businessID'], + amount: json['amount'], + orderId: json['orderId'] ?? 33, + ); + + @override + String toJson() => json.encode({ + 'businessID': businessID, + 'amount': amount, + 'orderId': orderId, + 'isReceived': isReceived, + }); +} diff --git a/lib/routes/app_router.dart b/lib/routes/app_router.dart index a5c3440..e90c3d0 100644 --- a/lib/routes/app_router.dart +++ b/lib/routes/app_router.dart @@ -1,11 +1,17 @@ import 'package:chat/routes/app_routes.dart'; import 'package:chat/routes/auth_routes.dart'; +import 'package:chat/routes/contact_routes.dart'; +import 'package:chat/routes/conversation_routes.dart'; +import 'package:chat/routes/user_routes.dart'; import 'package:get/get.dart'; class AppRouter { // 路由页面 static final List> getPages = [ - AppRoutes.router, AuthRoutes.router, + AppRoutes.router, + ConversationRoutes.router, + ContactRoutes.router, + UserRoutes.router, ]; } diff --git a/lib/routes/app_routes.dart b/lib/routes/app_routes.dart index d6a5d6c..034137f 100644 --- a/lib/routes/app_routes.dart +++ b/lib/routes/app_routes.dart @@ -1,3 +1,4 @@ +import 'package:chat/views/conversation/index_page.dart'; import 'package:chat/views/home/index_page.dart'; import 'package:chat/views/public/app_page.dart'; import 'package:chat/views/public/scan_page.dart'; @@ -10,9 +11,11 @@ abstract class AppRoutes { static const String app = '/'; static const String transit = '/transit'; static const String notfound = '/notfound'; - static const String home = '/home'; static const String scan = '/scan'; + static const String home = '/home'; + static const String search = '/search'; + static GetPage router = GetPage( name: '/', page: () => AppPage(), @@ -21,13 +24,17 @@ abstract class AppRoutes { name: AppRoutes.transit, page: () => const TransitPage(), ), + GetPage( + name: AppRoutes.scan, + page: () => const ScanPage(), + ), GetPage( name: AppRoutes.home, page: () => const HomePage(), ), GetPage( - name: AppRoutes.scan, - page: () => const ScanPage(), + name: AppRoutes.search, + page: () => const ConversationPage(), ), ], ); diff --git a/lib/routes/contact_routes.dart b/lib/routes/contact_routes.dart index 563bd0d..27014c7 100644 --- a/lib/routes/contact_routes.dart +++ b/lib/routes/contact_routes.dart @@ -1,12 +1,27 @@ import 'package:chat/middleware/auth_middleware.dart'; +import 'package:chat/views/contact/group/create/index_page.dart'; import 'package:chat/views/contact/group/index_page.dart'; +import 'package:chat/views/contact/group/manage/index_page.dart'; +import 'package:chat/views/contact/group/notification/index_page.dart'; import 'package:chat/views/contact/index/index_page.dart'; import 'package:get/get.dart'; abstract class ContactRoutes { /// 身份验证页面 static const String index = '/contact'; + + static const String friend = '/contact/friend'; + static const String friendSearch = '/contact/friend/search'; + static const String friendProfile = '/contact/friend/profile'; + static const String group = '/contact/group'; + static const String groupQrCode = '/contact/group/qrCode'; + static const String groupCreate = '/contact/group/create'; + static const String groupNotification = '/contact/group/notification'; + static const String groupManage = '/contact/group/manage'; + static const String groupApprove = '/contact/group/approve'; + static const String groupNickname = '/contact/group/nickname'; + static const String groupKick = '/contact/group/kick'; static GetPage router = GetPage( name: ContactRoutes.index, @@ -15,9 +30,41 @@ abstract class ContactRoutes { ], page: () => const ContactPage(), children: [ + GetPage( + name: '/friend', + page: () => const ContactGroupPage(), + children: [ + GetPage( + name: '/search', + page: () => const ContactGroupCreatePage(), + ), + ], + ), GetPage( name: '/group', page: () => const ContactGroupPage(), + children: [ + GetPage( + name: '/create', + page: () => const ContactGroupCreatePage(), + ), + GetPage( + name: '/qrCode', + page: () => const ContactGroupCreatePage(), + ), + GetPage( + name: '/notification', + page: () => const ContactGroupNotificationPage(), + ), + GetPage( + name: '/manage', + page: () => const ContactGroupManagePage(), + ), + GetPage( + name: '/approve', + page: () => const ContactGroupManagePage(), + ), + ], ), ], ); diff --git a/lib/routes/conversation_routes.dart b/lib/routes/conversation_routes.dart new file mode 100644 index 0000000..76d6c23 --- /dev/null +++ b/lib/routes/conversation_routes.dart @@ -0,0 +1,36 @@ +import 'package:chat/middleware/auth_middleware.dart'; +import 'package:chat/views/conversation/index_page.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; + +abstract class ConversationRoutes { + /// 身份验证页面 + static const String index = '/conversation'; + + static const String infoGroup = '/conversation/info/group'; + static const String infoPrivate = '/conversation/info/private'; + + static GetPage router = GetPage( + name: ConversationRoutes.index, + middlewares: [ + EnsureAuthMiddleware(), + ], + page: () => const ConversationPage(), + children: [ + GetPage( + name: '/info', + page: () => Container(), + children: [ + GetPage( + name: '/private', + page: () => Container(), + ), + GetPage( + name: '/group', + page: () => Container(), + ), + ], + ), + ], + ); +} diff --git a/lib/routes/user_routes.dart b/lib/routes/user_routes.dart new file mode 100644 index 0000000..15823f8 --- /dev/null +++ b/lib/routes/user_routes.dart @@ -0,0 +1,24 @@ +import 'package:chat/middleware/auth_middleware.dart'; +import 'package:chat/views/contact/index/index_page.dart'; +import 'package:chat/views/user/qr_code/index_page.dart'; +import 'package:get/get.dart'; + +abstract class UserRoutes { + /// 身份验证页面 + static const String index = '/user'; + static const String qrCode = '/user/qrCode'; + + static GetPage router = GetPage( + name: UserRoutes.index, + middlewares: [ + EnsureAuthMiddleware(), + ], + page: () => const ContactPage(), + children: [ + GetPage( + name: '/qrCode', + page: () => const UserQrCodePage(), + ), + ], + ); +} diff --git a/lib/services/auth_service.dart b/lib/services/auth_service.dart index c2c501b..ee3e709 100644 --- a/lib/services/auth_service.dart +++ b/lib/services/auth_service.dart @@ -1,5 +1,7 @@ +import 'package:chat/routes/auth_routes.dart'; import 'package:get/get.dart'; import 'package:get_storage/get_storage.dart'; +import 'package:tencent_im_sdk_plugin/tencent_im_sdk_plugin.dart'; class AuthService extends GetxService { static AuthService get to => Get.find(); @@ -12,19 +14,24 @@ class AuthService extends GetxService { /// 登录状态记录,可监听的,这样ever才能监听到 final RxBool isLogin = false.obs; - /// 登录的token,供请求时调用,载入内存,是为了每次使用的时候,不需要从磁盘获取 - late String userToken = ''; + /// 供请求时调用,载入内存,是为了每次使用的时候,不需要从磁盘获取 + late String userId = ''; + late String userSig = ''; /// 获取存储的token,这个可以做到持久化存储 - String get _userToken => _box.read('userToken') ?? ''; + String get _userSig => + _box.read('userSig') ?? + 'eJwtzEELgjAYxvHvsnPIu7VNJ3ToIIKsIAp2ljbrRYylJs3ou2fq8fk98P*Qiz5Hg2tJSlgEZDNvtO7RY4UzixU7W5feoyUp5QAxVVzR5XFvj62bXAjBAGDRHpu-SSnZlseKrxW8TU1p9sV4v3YJoyFrDrV*QYu5yrQ*2cqUzyHkPpixOMbJjnx-EqUv9A__'; + String get _userId => _box.read('userId') ?? '5'; @override void onInit() { super.onInit(); - if (_userToken.isNotEmpty) { + if (_userSig.isNotEmpty) { isLogin.value = true; - userToken = _userToken; + userSig = _userSig; + userId = _userId; } // ever(_isLogin, (_) { @@ -37,10 +44,21 @@ class AuthService extends GetxService { } Future login(String address) async { - _box.write('userToken', address); - userToken = address; + _box.write('userId', '5'); + userId = '5'; isLogin.value = true; return true; } + + /// 退出登录 + void logout() async { + await TencentImSDKPlugin.v2TIMManager.logout(); + _box.remove('userSig'); + _box.remove('userId'); + userSig = ''; + userId = ''; + isLogin.value = false; + Get.offAllNamed(AuthRoutes.index); + } } diff --git a/lib/services/tim/apply_service.dart b/lib/services/tim/apply_service.dart new file mode 100644 index 0000000..f871669 --- /dev/null +++ b/lib/services/tim/apply_service.dart @@ -0,0 +1,50 @@ +import 'package:chat/services/tim_service.dart'; +import 'package:chat/utils/ui_tools.dart'; +import 'package:get/get.dart'; +import 'package:tencent_im_sdk_plugin/enum/friend_application_type_enum.dart'; +import 'package:tencent_im_sdk_plugin/enum/friend_response_type_enum.dart'; +import 'package:tencent_im_sdk_plugin/manager/v2_tim_friendship_manager.dart'; +import 'package:tencent_im_sdk_plugin/models/v2_tim_friend_application.dart'; + +class TimApplyService extends GetxService { + static TimApplyService get to => Get.find(); + + /// 好友申请 + RxList applies = + List.empty(growable: true).obs; + + /// 好友关系 + V2TIMFriendshipManager get friendshipManager => + TimService.to.instance.v2TIMFriendshipManager; + + @override + void onInit() async { + super.onInit(); + await fetchList(); + } + + /// 获取申请列表 + Future fetchList() async { + var applyList = await friendshipManager.getFriendApplicationList(); + if (applyList.code == 0) { + applies.value = applyList.data!.friendApplicationList!; + } + } + + /// 接受好友请求 + Future accept(String userID) async { + var result = await friendshipManager.acceptFriendApplication( + responseType: FriendResponseTypeEnum.V2TIM_FRIEND_ACCEPT_AGREE_AND_ADD, + type: FriendApplicationTypeEnum.V2TIM_FRIEND_APPLICATION_COME_IN, + userID: userID, + ); + + if (result.code == 0) { + await fetchList(); + return true; + } else { + UiTools.toast(result.desc); + return false; + } + } +} diff --git a/lib/services/tim/block_service.dart b/lib/services/tim/block_service.dart new file mode 100644 index 0000000..c35666c --- /dev/null +++ b/lib/services/tim/block_service.dart @@ -0,0 +1,56 @@ +import 'package:chat/services/tim_service.dart'; +import 'package:chat/utils/ui_tools.dart'; +import 'package:get/get.dart'; +import 'package:tencent_im_sdk_plugin/manager/v2_tim_friendship_manager.dart'; +import 'package:tencent_im_sdk_plugin/models/v2_tim_friend_info.dart'; + +class TimBlockService extends GetxService { + static TimBlockService get to => Get.find(); + + /// 好友关系 + V2TIMFriendshipManager get friendshipManager => + TimService.to.instance.v2TIMFriendshipManager; + + @override + void onInit() async { + super.onInit(); + await fetchList(); + } + + /// 黑名单列表 + RxList blocks = + List.empty(growable: true).obs; + + /// 拉取黑名单列表 + Future fetchList() async { + var blacklist = await friendshipManager.getBlackList(); + if (blacklist.code == 0) { + blocks.value = blacklist.data!; + } + } + + /// 拉黑某人 + Future add(String userID) async { + var result = await friendshipManager.addToBlackList(userIDList: [userID]); + + if (result.code == 0) { + return result.data!.first.resultCode == 0; + } else { + UiTools.toast(result.desc); + return false; + } + } + + /// 解除拉黑 + Future remove(String userID) async { + var result = + await friendshipManager.deleteFromBlackList(userIDList: [userID]); + + if (result.code == 0) { + return result.data!.first.resultCode == 0; + } else { + UiTools.toast(result.desc); + return false; + } + } +} diff --git a/lib/services/tim/conversation_service.dart b/lib/services/tim/conversation_service.dart new file mode 100644 index 0000000..d479f39 --- /dev/null +++ b/lib/services/tim/conversation_service.dart @@ -0,0 +1,379 @@ +import 'package:chat/models/im/custom_message_model.dart'; +import 'package:chat/models/im/location_model.dart'; +import 'package:chat/services/tim_service.dart'; +import 'package:chat/utils/ui_tools.dart'; +import 'package:get/get.dart'; +import 'package:tencent_im_sdk_plugin/enum/V2TimConversationListener.dart'; +import 'package:tencent_im_sdk_plugin/enum/conversation_type.dart'; +import 'package:tencent_im_sdk_plugin/enum/receive_message_opt_enum.dart'; +import 'package:tencent_im_sdk_plugin/manager/v2_tim_conversation_manager.dart'; +import 'package:tencent_im_sdk_plugin/manager/v2_tim_message_manager.dart'; +import 'package:tencent_im_sdk_plugin/models/v2_tim_callback.dart'; +import 'package:tencent_im_sdk_plugin/models/v2_tim_conversation.dart'; +import 'package:tencent_im_sdk_plugin/models/v2_tim_msg_create_info_result.dart'; +import 'package:wechat_assets_picker/wechat_assets_picker.dart'; + +class TimConversationService extends GetxService { + static TimConversationService get to => Get.find(); + + /// 消息管理实例 + V2TIMMessageManager get messageManager => + TimService.to.instance.v2TIMMessageManager; + + /// 会话管理 + V2TIMConversationManager get conversationManager => + TimService.to.instance.v2ConversationManager; + + @override + void onInit() async { + super.onInit(); + await fetchList(); + _addListener(); + } + + _addListener() { + conversationManager.addConversationListener( + listener: V2TimConversationListener( + + /// 未读消息总数监听 + onTotalUnreadMessageCountChanged: (_) { + unreadCount.value = _; + })); + } + + /// 会话列表 + RxList conversationList = + List.empty(growable: true).obs; + + /// 未读消息总数 + var unreadCount = 0.obs; + + Future fetchList() async { + var data = await conversationManager.getConversationList( + count: 100, + nextSeq: '0', + ); + + if (data.code == 0) { + conversationList.value = data.data!.conversationList!; + } + } + + /// 获取未读消息数 + Future getUnreadCount() async { + var result = await conversationManager.getTotalUnreadMessageCount(); + unreadCount.value = result.data ?? 0; + } + + /// 获取会话信息 + Future getById( + String conversationID, + ) async { + var result = await conversationManager.getConversation( + conversationID: conversationID, + ); + return result.data!; + } + + /// 标记会话已读 + Future markAsRead(V2TimConversation conversation) async { + /// 标记会话内消息已读 + if (conversation.type == ConversationType.V2TIM_GROUP) { + await messageManager.markGroupMessageAsRead( + groupID: conversation.groupID!, + ); + } else { + await messageManager.markC2CMessageAsRead( + userID: conversation.userID!, + ); + } + fetchList(); + } + + /// 从会话列表移除会话 + Future delete(V2TimConversation conversation) async { + await deleteById(conversation.conversationID); + fetchList(); + } + + Future deleteById(String conversationID) async { + await conversationManager.deleteConversation( + conversationID: conversationID, + ); + fetchList(); + } + + /// 清空会话历史消息 + Future clearHistoryMessage(V2TimConversation conversation) async { + if (conversation.type == ConversationType.V2TIM_GROUP) { + await messageManager.clearGroupHistoryMessage( + groupID: conversation.groupID!, + ); + } else { + await messageManager.clearC2CHistoryMessage( + userID: conversation.userID!, + ); + } + fetchList(); + } + + /// 设置会话置顶/取消置顶 + Future setOnTop(V2TimConversation conversation) async { + var result = await conversationManager.pinConversation( + conversationID: conversation.conversationID, + isPinned: !conversation.isPinned!, + ); + fetchList(); + + if (result.code != 0) { + UiTools.toast(result.desc); + } + + return result.code == 0; + } + + /// 开启/关闭消息免打扰 + Future setReceiveOpt( + V2TimConversation conversation, + ) async { + V2TimCallback result; + + if (conversation.type == ConversationType.V2TIM_GROUP) { + result = await messageManager.setGroupReceiveMessageOpt( + groupID: conversation.groupID!, + opt: (conversation.recvOpt == 0) + ? ReceiveMsgOptEnum.V2TIM_NOT_RECEIVE_MESSAGE + : ReceiveMsgOptEnum.V2TIM_RECEIVE_MESSAGE, + ); + } else { + result = await messageManager.setC2CReceiveMessageOpt( + userIDList: [ + conversation.userID!, + ], + opt: (conversation.recvOpt == 0) + ? ReceiveMsgOptEnum.V2TIM_NOT_RECEIVE_MESSAGE + : ReceiveMsgOptEnum.V2TIM_RECEIVE_MESSAGE, + ); + } + + fetchList(); + + return result.code == 0; + } + + /// 发送消息 + Future sendTextMessage( + V2TimConversation conversation, + String text, + ) async { + var msg = await messageManager.createTextAtMessage( + text: text, + atUserList: [], + ); + if (msg.code == 0) { + return await _sendMessage(conversation, msg.data!); + } else { + UiTools.toast(msg.desc); + return false; + } + } + + /// 发送图片消息 + Future sendImageMessage( + V2TimConversation conversation, + AssetEntity asset, + ) async { + var msg = await messageManager.createImageMessage( + imagePath: (await asset.file)!.path, + ); + if (msg.code == 0) { + return await _sendMessage(conversation, msg.data!); + } else { + UiTools.toast(msg.desc); + return false; + } + } + + /// 发送语音消息 + Future sendSoundMessage( + V2TimConversation conversation, + String soundPath, + int duration, + ) async { + var msg = await messageManager.createSoundMessage( + soundPath: soundPath, duration: duration); + if (msg.code == 0) { + return await _sendMessage(conversation, msg.data!); + } else { + UiTools.toast(msg.desc); + return false; + } + } + + /// 发送视频消息 + Future sendVideoMessage( + V2TimConversation conversation, + AssetEntity asset, + ) async { + return false; + // final originFile = await asset.originFile; + // var size = await originFile!.length(); + + // if (size >= 104857600) { + // UiTools.toast('视频文件不能超过100M'); + // return false; + // } + + // final duration = asset.videoDuration.inSeconds; + + // String tempPath = (await getTemporaryDirectory()).path; + // String? thumbnail = await VideoThumbnail.thumbnailFile( + // video: originFile.path, + // thumbnailPath: tempPath, + // imageFormat: ImageFormat.JPEG, + // maxWidth: 256, + // quality: 25, + // ); + + // var msg = await messageManager.createVideoMessage( + // videoFilePath: originFile.path, + // type: asset.mimeType!.replaceFirst('video/', ''), + // duration: duration, + // snapshotPath: thumbnail ?? '', + // ); + // if (msg.code == 0) { + // return await _sendMessage(conversation, msg.data!); + // } else { + // UiTools.toast(msg.desc); + // return false; + // } + } + + /// 发送文件消息 + Future sendFileMessage( + V2TimConversation conversation, + String fileName, + String filePath, + ) async { + var msg = await messageManager.createFileMessage( + fileName: fileName, + filePath: filePath, + ); + if (msg.code == 0) { + return await _sendMessage(conversation, msg.data!); + } else { + UiTools.toast(msg.desc); + return false; + } + } + + /// 发送位置消息 + Future sendLocationMessage( + V2TimConversation conversation, + LocationModel messageModel, + ) async { + var msg = await messageManager.createLocationMessage( + desc: messageModel.name, + latitude: messageModel.latitude, + longitude: messageModel.longitude, + ); + if (msg.code == 0) { + return await _sendMessage(conversation, msg.data!); + } else { + UiTools.toast(msg.desc); + return false; + } + } + + /// 发送表情消息 + Future sendFaceMessage( + V2TimConversation conversation, + ) async { + var msg = await messageManager.createFaceMessage( + data: '', + index: 0, + ); + if (msg.code == 0) { + return await _sendMessage(conversation, msg.data!); + } else { + UiTools.toast(msg.desc); + return false; + } + } + + /// 发送聊天记录消息 + Future sendMergerMessage( + V2TimConversation conversation, + ) async { + var msg = await messageManager.createMergerMessage( + abstractList: [], + compatibleText: '', + msgIDList: [], + title: '', + ); + if (msg.code == 0) { + return await _sendMessage(conversation, msg.data!); + } else { + UiTools.toast(msg.desc); + return false; + } + } + + /// 发送自定义消息 + Future sendCustomMessage( + V2TimConversation conversation, + CustomMessageModel customMessageModel, + String desc, + ) async { + var msg = await messageManager.createCustomMessage( + data: customMessageModel.toJson(), + desc: desc, + ); + if (msg.code == 0) { + return await _sendMessage(conversation, msg.data!); + } else { + UiTools.toast(msg.desc); + return false; + } + } + + Future _sendMessage( + V2TimConversation conversation, + V2TimMsgCreateInfoResult result, + ) async { + var sendMessageRes = await messageManager.sendMessage( + id: result.id!, + receiver: conversation.type == ConversationType.V2TIM_C2C + ? conversation.userID! + : '', + groupID: conversation.type == ConversationType.V2TIM_GROUP + ? conversation.groupID! + : '', + // isExcludedFromUnreadCount: true, + // isExcludedFromLastMessage: true, + // needReadReceipt: true, + ); + + if (sendMessageRes.code == 0) { + // TimMessageService.to + // .add(conversation.conversationID, result.messageInfo!); + // eventBus.fire(result.messageInfo!); + fetchList(); + return true; + } else { + UiTools.toast(sendMessageRes.desc); + return false; + } + } + + /// 设置会话草稿 + Future draft( + V2TimConversation conversation, { + String? draftText = "", + }) async { + await conversationManager.setConversationDraft( + conversationID: conversation.conversationID, + draftText: draftText, + ); + } +} diff --git a/lib/services/tim/friend_service.dart b/lib/services/tim/friend_service.dart new file mode 100644 index 0000000..98e6f0e --- /dev/null +++ b/lib/services/tim/friend_service.dart @@ -0,0 +1,241 @@ +import 'package:azlistview/azlistview.dart'; +import 'package:chat/models/im/contact_info_model.dart'; +import 'package:chat/models/im/search_user_model.dart'; +import 'package:chat/services/tim_service.dart'; +import 'package:chat/utils/im_tools.dart'; +import 'package:chat/utils/request/http.dart'; +import 'package:chat/utils/ui_tools.dart'; +import 'package:get/get.dart'; +import 'package:lpinyin/lpinyin.dart'; +import 'package:tencent_im_sdk_plugin/enum/friend_type_enum.dart'; +import 'package:tencent_im_sdk_plugin/manager/v2_tim_friendship_manager.dart'; +import 'package:tencent_im_sdk_plugin/models/v2_tim_friend_check_result.dart'; +import 'package:tencent_im_sdk_plugin/models/v2_tim_friend_info.dart'; +import 'package:tencent_im_sdk_plugin/models/v2_tim_friend_info_result.dart'; +import 'package:tencent_im_sdk_plugin/models/v2_tim_user_full_info.dart'; + +class TimFriendService extends GetxService { + static TimFriendService get to => Get.find(); + + /// 好友关系 + V2TIMFriendshipManager get friendshipManager => + TimService.to.instance.v2TIMFriendshipManager; + + /// 好友列表 + var friends = List.empty(growable: true).obs; + + /// 格式化后的联系人信息 + var contacts = List.empty(growable: true).obs; + + Future fetchList() async { + var result = await friendshipManager.getFriendList(); + if (result.code == 0) { + friends.value = result.data!; + contacts.clear(); + for (var element in result.data!) { + String name = ImTools.parseNicknameFromInfo(element); + String pinyin = PinyinHelper.getPinyinE(name); + String tag = pinyin.substring(0, 1).toUpperCase(); + + if (!RegExp('[A-Z]').hasMatch(tag)) { + tag = '#'; + } + + contacts.add(ContactInfoModel( + name: name, + userID: element.userID, + tagIndex: tag, + namePinyin: pinyin, + friendInfo: element, + )); + } + + SuspensionUtil.sortListBySuspensionTag(contacts); + SuspensionUtil.setShowSuspensionStatus(contacts); + } + } + + /// 添加好友 + Future add( + String userID, { + String? remark, + String? addWording, + String? addSource, + }) async { + var result = await friendshipManager.addFriend( + userID: userID, + remark: remark, + addWording: addWording, + addSource: addSource, + addType: FriendTypeEnum.V2TIM_FRIEND_TYPE_BOTH, + ); + if (result.code == 0) { + if (result.data!.resultCode == 0) { + fetchList(); + return true; + } else if (result.data!.resultCode == 30539) { + return true; + } else if (result.data!.resultCode == 30010) { + UiTools.toast('好友数量已达上限'); + return false; + } else { + UiTools.toast( + result.data!.resultInfo! + result.data!.resultCode.toString()); + return false; + } + } + UiTools.toast(result.desc); + return false; + } + + /// 删除双向好友 + Future delete(String userID) async { + var result = await friendshipManager.deleteFromFriendList( + deleteType: FriendTypeEnum.V2TIM_FRIEND_TYPE_BOTH, + userIDList: [userID], + ); + + if (result.code == 0) { + fetchList(); + return result.data!.first.resultCode == 0; + } else { + UiTools.toast(result.desc); + return false; + } + } + + /// 检测好友是否有双向(单向)好友关系。 + Future check(String userID) async { + var result = await friendshipManager.checkFriend( + checkType: FriendTypeEnum.V2TIM_FRIEND_TYPE_BOTH, + userIDList: [userID], + ); + + if (result.code == 0) { + return result.data!.first; + } else { + UiTools.toast(result.desc); + return null; + } + } + + /// 获取用户资料 + Future userInfo(String userID) async { + var result = await TimService.to.instance.getUsersInfo( + userIDList: [userID], + ); + + if (result.code == 0) { + return result.data!.first; + } + + UiTools.toast(result.desc); + return null; + } + + /// 获取好友资料 + Future friendInfo(String userID) async { + var result = await friendshipManager.getFriendsInfo(userIDList: [userID]); + + if (result.code == 0) { + if (result.data!.isNotEmpty) { + return result.data!.first; + } else { + return null; + } + } + + UiTools.toast(result.desc); + return null; + } + + /// 修改个人资料 + Future setSelfInfo({ + String? nickname, + String? avatar, + }) async { + var result = await TimService.to.instance.setSelfInfo( + userFullInfo: V2TimUserFullInfo( + nickName: nickname, + faceUrl: avatar, + ), + ); + + if (result.code == 0) { + // TimService.to.fetchSelfInfo(); + return true; + } + UiTools.toast(result.desc); + return false; + } + + /// 设置是否允许陌生人消息 + Future allowStrangerMessage() async { + var result = await TimService.to.instance.setSelfInfo( + userFullInfo: V2TimUserFullInfo( + customInfo: { + 'Stranger': 'TRUE', + }, + ), + ); + + if (result.code == 0) { + return true; + } + UiTools.toast(result.desc); + return false; + } + + /// 禁止陌生人消息 + Future forbidStrangerMessage() async { + var result = await TimService.to.instance.setSelfInfo( + userFullInfo: V2TimUserFullInfo( + customInfo: { + 'Stranger': '', + }, + ), + ); + if (result.code == 0) { + return true; + } + return false; + } + + /// 修改好友备注信息 + Future setFriendRemark( + String userID, + String friendRemark, + ) async { + var result = await friendshipManager.setFriendInfo( + userID: userID, + friendRemark: friendRemark, + ); + + if (result.code == 0) { + return true; + } + UiTools.toast(result.desc); + return false; + } + + /// 从服务器查找用户 + Future?> searchUser(String keyword) async { + try { + var json = await Http.get( + 'user/search', + params: { + 'keyword': keyword, + }, + ); + + return List.from( + json.map( + (x) => SearchUserModel.fromJson(x), + ), + ); + } catch (err) { + UiTools.toast(err.toString()); + } + return null; + } +} diff --git a/lib/services/tim/group_service.dart b/lib/services/tim/group_service.dart new file mode 100644 index 0000000..f613b8b --- /dev/null +++ b/lib/services/tim/group_service.dart @@ -0,0 +1,403 @@ +import 'package:chat/services/tim/conversation_service.dart'; +import 'package:chat/services/tim_service.dart'; +import 'package:chat/utils/ui_tools.dart'; +import 'package:get/get.dart'; +import 'package:tencent_im_sdk_plugin/enum/group_add_opt_enum.dart'; +import 'package:tencent_im_sdk_plugin/enum/group_application_type_enum.dart'; +import 'package:tencent_im_sdk_plugin/enum/group_member_filter_enum.dart'; +import 'package:tencent_im_sdk_plugin/enum/group_member_role_enum.dart'; +import 'package:tencent_im_sdk_plugin/enum/group_type.dart'; +import 'package:tencent_im_sdk_plugin/manager/v2_tim_group_manager.dart'; +import 'package:tencent_im_sdk_plugin/models/v2_tim_conversation.dart'; +import 'package:tencent_im_sdk_plugin/models/v2_tim_friend_info.dart'; +import 'package:tencent_im_sdk_plugin/models/v2_tim_group_application.dart'; +import 'package:tencent_im_sdk_plugin/models/v2_tim_group_info.dart'; +import 'package:tencent_im_sdk_plugin/models/v2_tim_group_member.dart'; +import 'package:tencent_im_sdk_plugin/models/v2_tim_group_member_full_info.dart'; + +class TimGroupService extends GetxService { + static TimGroupService get to => Get.find(); + + /// 群管理实例 + V2TIMGroupManager get groupManager => + TimService.to.instance.v2TIMGroupManager; + + /// 我的群组列表 + RxList groups = + List.empty(growable: true).obs; + + @override + void onInit() async { + super.onInit(); + await fetchList(); + } + + Future fetchList() async { + var result = await groupManager.getJoinedGroupList(); + + if (result.code == 0) { + groups.value = result.data!; + } + } + + /// 创建群 + Future create( + String groupName, + List memberList, { + String? groupType, + }) async { + var result = await groupManager.createGroup( + groupType: groupType ?? GroupType.Public, + groupName: groupName, + notification: '', + introduction: '', + faceUrl: '', + isAllMuted: false, + isSupportTopic: false, + addOpt: GroupAddOptTypeEnum.V2TIM_GROUP_ADD_AUTH, + memberList: memberList.map((e) { + return V2TimGroupMember( + role: GroupMemberRoleTypeEnum.V2TIM_GROUP_MEMBER_ROLE_MEMBER, + userID: e.userID, + ); + }).toList(), + ); + + if (result.code != 0) { + UiTools.toast(result.desc); + } + + await fetchList(); + await TimConversationService.to.fetchList(); + + return result.data; + } + + /// 加群申请列表 + Future?> applies(String groupID) async { + var result = await groupManager.getGroupApplicationList(); + + if (result.code == 0) { + return result.data?.groupApplicationList; + } else { + UiTools.toast(result.desc); + return null; + } + } + + /// 同意加群 + Future accept() async { + await groupManager.acceptGroupApplication( + fromUser: '', + groupID: '', + toUser: '', + ); + } + + /// 拒绝加群请求 + Future refuse() async { + await groupManager.refuseGroupApplication( + addTime: 0, + fromUser: '', + groupID: '', + toUser: '', + type: GroupApplicationTypeEnum.V2TIM_GROUP_APPLICATION_GET_TYPE_JOIN, + ); + } + + /// 加入群组 + Future join( + V2TimGroupInfo group, + String message, + ) async { + var result = await TimService.to.instance.joinGroup( + groupID: group.groupID, + groupType: group.groupType, + message: message, + ); + + if (result.code == 0) { + await fetchList(); + return true; + } else { + UiTools.toast(result.desc); + return false; + } + } + + /// 邀请加群 + Future invite( + V2TimGroupInfo group, + List userList, + ) async { + var result = await groupManager.inviteUserToGroup( + groupID: group.groupID, + userList: userList, + ); + + if (result.code == 0) { + return true; + } else { + UiTools.toast(result.desc); + return false; + } + } + + /// 退出群组 + Future quit( + V2TimGroupInfo group, + ) async { + await TimService.to.instance.quitGroup( + groupID: group.groupID, + ); + await fetchList(); + await TimConversationService.to.deleteById( + 'group_' + group.groupID, + ); + } + + /// 解散群组 + Future dismiss( + V2TimGroupInfo group, + V2TimConversation conversation, + ) async { + var result = await TimService.to.instance.dismissGroup( + groupID: group.groupID, + ); + + if (result.code == 0) { + return true; + } + UiTools.toast(result.desc); + return false; + } + + /// 获取群资料 + Future info(String groupId) async { + var result = await groupManager.getGroupsInfo(groupIDList: [groupId]); + + if (result.code == 0) { + return result.data![0].groupInfo!; + } else { + UiTools.toast(result.desc); + } + return null; + } + + /// 更新群资料 + Future updateName( + V2TimGroupInfo group, + String groupName, + ) async { + var result = await groupManager.setGroupInfo( + info: V2TimGroupInfo.fromJson( + { + 'groupID': group.groupID, + 'groupType': group.groupType, + 'groupName': groupName, + }, + ), + ); + + if (result.code == 0) { + return true; + } + UiTools.toast(result.desc); + return false; + } + + /// 更新群公告 + Future updateNotification( + V2TimGroupInfo group, + String notification, + ) async { + var result = await groupManager.setGroupInfo( + info: V2TimGroupInfo.fromJson( + { + 'groupID': group.groupID, + 'groupType': group.groupType, + 'notification': notification, + }, + ), + ); + + if (result.code == 0) { + await TimConversationService.to.fetchList(); + return true; + } + UiTools.toast(result.desc); + return false; + } + + /// 修改加群方式 + Future updateAddOpt( + V2TimGroupInfo group, + String notification, + ) async { + var result = await groupManager.setGroupInfo( + info: V2TimGroupInfo.fromJson( + { + 'groupID': group.groupID, + 'groupType': group.groupType, + 'groupAddOpt': group.groupAddOpt, + }, + ), + ); + + if (result.code == 0) { + await TimConversationService.to.fetchList(); + return true; + } + UiTools.toast(result.desc); + return false; + } + + /// 获取群组成员 + Future?> members( + String groupID, { + int count = 10, + GroupMemberFilterTypeEnum filter = + GroupMemberFilterTypeEnum.V2TIM_GROUP_MEMBER_FILTER_ALL, + }) async { + var result = await groupManager.getGroupMemberList( + count: count, + filter: filter, + nextSeq: '0', + offset: 0, + groupID: groupID, + ); + + if (result.code == 0) { + return result.data!.memberInfoList!; + } + return null; + } + + /// 获取群成员资料 + Future getMemberInfo( + V2TimGroupInfo group, + String userID, + ) async { + var result = await groupManager.getGroupMembersInfo( + groupID: group.groupID, + memberList: [userID], + ); + + if (result.data != null) { + return result.data!.first; + } else { + return null; + } + } + + /// 设置群成员资料 + Future setMemberInfo( + V2TimGroupInfo group, + String userID, + String nameCard, + ) async { + var result = await groupManager.setGroupMemberInfo( + groupID: group.groupID, + userID: userID, + nameCard: nameCard, + ); + + if (result.code == 0) { + return true; + } + UiTools.toast(result.desc); + return false; + } + + /// 禁言群成员, 时间单位秒,如果是0,应该就解除禁言了 + Future muteMember( + V2TimGroupInfo group, + String userID, { + int seconds = 60, + }) async { + var result = await groupManager.muteGroupMember( + groupID: group.groupID, + userID: userID, + seconds: seconds, + ); + + if (result.code == 0) { + return true; + } + UiTools.toast(result.desc); + return false; + } + + /// 全员禁言/解除全员禁言 + Future mute( + V2TimGroupInfo group, + bool isAllMuted, + ) async { + var result = await groupManager.setGroupInfo( + info: V2TimGroupInfo( + isAllMuted: isAllMuted, + groupID: group.groupID, + groupType: group.groupType, + ), + ); + + if (result.code == 0) { + return true; + } + UiTools.toast(result.desc); + return false; + } + + /// 踢人 + Future kickMember( + V2TimGroupInfo group, + List memberList, + ) async { + var result = await groupManager.kickGroupMember( + groupID: group.groupID, + memberList: memberList, + ); + + if (result.code == 0) { + return true; + } + UiTools.toast(result.desc); + return false; + } + + /// 设置管理员 + Future setMemberRole( + V2TimGroupInfo group, + String userID, + GroupMemberRoleTypeEnum role, + ) async { + var result = await groupManager.setGroupMemberRole( + groupID: group.groupID, + userID: userID, + role: role, + ); + + if (result.code == 0) { + return true; + } + UiTools.toast(result.desc); + return false; + } + + /// 转让群主 + Future transfer( + V2TimGroupInfo group, + String userID, + ) async { + var result = await groupManager.transferGroupOwner( + groupID: group.groupID, + userID: userID, + ); + + if (result.code == 0) { + return true; + } + UiTools.toast(result.desc); + return false; + } +} diff --git a/lib/services/tim/message_service.dart b/lib/services/tim/message_service.dart new file mode 100644 index 0000000..372716b --- /dev/null +++ b/lib/services/tim/message_service.dart @@ -0,0 +1,77 @@ +import 'package:get/get.dart'; +import 'package:scroll_to_index/scroll_to_index.dart'; +import 'package:tencent_im_sdk_plugin/enum/conversation_type.dart'; +import 'package:tencent_im_sdk_plugin/models/v2_tim_conversation.dart'; +import 'package:tencent_im_sdk_plugin/models/v2_tim_message.dart'; +import 'package:tencent_im_sdk_plugin/models/v2_tim_value_callback.dart'; +import 'conversation_service.dart'; + +class LoadMessageResult { + String? lastMsgId; + bool noMore; + + LoadMessageResult({ + this.lastMsgId, + required this.noMore, + }); +} + +class TimMessageService extends GetxService { + static TimMessageService get to => Get.find(); + + RxList messages = List.empty(growable: true).obs; + + var curConversationId = ''; + + Future loadMessagesFromService( + V2TimConversation conversation, + String? lastMsgId, + AutoScrollController _scrollController, + ) async { + if (lastMsgId == null && curConversationId != conversation.conversationID) { + messages.clear(); + curConversationId = conversation.conversationID; + } + + V2TimValueCallback> result; + if (conversation.type == ConversationType.V2TIM_GROUP) { + result = await TimConversationService.to.messageManager + .getGroupHistoryMessageList( + groupID: conversation.groupID!, + count: 20, + lastMsgID: lastMsgId, + ); + } else { + result = await TimConversationService.to.messageManager + .getC2CHistoryMessageList( + userID: conversation.userID!, + count: 20, + lastMsgID: lastMsgId, + ); + } + var list = result.data ?? []; + if (lastMsgId == null) { + messages.value = list.reversed.toList(); + _scrollController.scrollToIndex( + messages.length - 1, + preferPosition: AutoScrollPosition.begin, + ); + } else { + //加载更多 + if (list.isNotEmpty) { + messages.value = messages.reversed.toList(); //正常顺序 + messages.addAll(list); + messages.value = messages.reversed.toList(); //反转一次 + } + } + if (messages.isNotEmpty) { + return messages.first.msgID; + } else { + return ''; + } + } + + addMessage(V2TimMessage message) { + messages.add(message); + } +} diff --git a/lib/services/tim_service.dart b/lib/services/tim_service.dart index ef19c7d..ff34dbf 100644 --- a/lib/services/tim_service.dart +++ b/lib/services/tim_service.dart @@ -1,7 +1,395 @@ +import 'dart:convert'; + +import 'package:chat/controllers/group_controller.dart'; +import 'package:chat/models/im/custom_message_model.dart'; +import 'package:chat/services/auth_service.dart'; +import 'package:chat/services/tim/apply_service.dart'; +import 'package:chat/services/tim/block_service.dart'; +import 'package:chat/services/tim/conversation_service.dart'; +import 'package:chat/services/tim/friend_service.dart'; +import 'package:chat/services/tim/group_service.dart'; +import 'package:chat/services/tim/message_service.dart'; +import 'package:chat/utils/ui_tools.dart'; +import 'package:flutter/material.dart'; import 'package:get/get.dart'; +import 'package:tencent_im_sdk_plugin/enum/V2TimAdvancedMsgListener.dart'; +import 'package:tencent_im_sdk_plugin/enum/V2TimFriendshipListener.dart'; +import 'package:tencent_im_sdk_plugin/enum/V2TimGroupListener.dart'; +import 'package:tencent_im_sdk_plugin/enum/V2TimSDKListener.dart'; +import 'package:tencent_im_sdk_plugin/enum/log_level_enum.dart'; +import 'package:tencent_im_sdk_plugin/enum/message_elem_type.dart'; +import 'package:tencent_im_sdk_plugin/manager/v2_tim_manager.dart'; +import 'package:tencent_im_sdk_plugin/models/v2_tim_friend_application.dart'; +import 'package:tencent_im_sdk_plugin/models/v2_tim_friend_info.dart'; +import 'package:tencent_im_sdk_plugin/models/v2_tim_group_change_info.dart'; +import 'package:tencent_im_sdk_plugin/models/v2_tim_group_member_change_info.dart'; +import 'package:tencent_im_sdk_plugin/models/v2_tim_group_member_info.dart'; +import 'package:tencent_im_sdk_plugin/models/v2_tim_message.dart'; +import 'package:tencent_im_sdk_plugin/models/v2_tim_message_receipt.dart'; +import 'package:tencent_im_sdk_plugin/models/v2_tim_topic_info.dart'; +import 'package:tencent_im_sdk_plugin/models/v2_tim_user_full_info.dart'; +import 'package:tencent_im_sdk_plugin/tencent_im_sdk_plugin.dart'; +import 'package:vibration/vibration.dart'; class TimService extends GetxService { - Future init() async { - return this; + static TimService get to => Get.find(); + + /// 获取实例 + V2TIMManager get instance => _getInstance(); + + /// 获取TIM实例 + V2TIMManager _getInstance() { + return TencentImSDKPlugin.v2TIMManager; + } + + int sdkAppID = 1400719491; + + String get _userSig => AuthService.to.userSig; + + String get _userId => AuthService.to.userId; + + RxString currentConversationId = ''.obs; + + @override + void onInit() { + super.onInit(); + + if (AuthService.to.isLogin.value) { + initSdk(); + } + } + + Future initSdk() async { + /// 初始化TIMSDK + await instance.initSDK( + sdkAppID: sdkAppID, + loglevel: LogLevelEnum.V2TIM_LOG_INFO, + listener: V2TimSDKListener( + onConnectFailed: ( + int code, + String error, + ) {}, + onConnectSuccess: () async {}, + onConnecting: () {}, + onKickedOffline: () { + UiTools.toast('该账号在其他设备登录被迫下线'); + AuthService.to.logout(); + }, + onSelfInfoUpdated: ( + V2TimUserFullInfo info, + ) {}, + onUserSigExpired: () {}, + ), + ); + + /// 登录 + await instance.login( + userID: _userId, + userSig: _userSig, + ); + + Get.put(TimConversationService()); + Get.put(TimFriendService()); + Get.put(TimGroupService()); + Get.put(TimBlockService()); + Get.put(TimApplyService()); + Get.put(TimMessageService()); + + /// 消息监听器 + await TimConversationService.to.messageManager.addAdvancedMsgListener( + listener: V2TimAdvancedMsgListener( + /// 收到新消息 + onRecvNewMessage: ( + V2TimMessage msg, + ) { + onRecvNewMessage(msg); + }, + + /// 收到C2C已读回执 + onRecvC2CReadReceipt: ( + List receiptList, + ) {}, + + /// 消息撤回 + onRecvMessageRevoked: ( + String msgID, + ) {}, + + /// 发送消息进度 + onSendMessageProgress: ( + V2TimMessage message, + int progress, + ) {}, + + /// 消息更改 + onRecvMessageModified: ( + V2TimMessage msg, + ) {}, + + /// 消息已读回执 + onRecvMessageReadReceipts: ( + List receiptList, + ) {}, + ), + ); + + TimFriendService.to.friendshipManager.setFriendListener( + listener: V2TimFriendshipListener( + /// 有新的好友请求 + onFriendApplicationListAdded: ( + List applicationList, + ) { + TimApplyService.to.fetchList(); + }, + onFriendApplicationListDeleted: ( + List userIDList, + ) { + TimApplyService.to.fetchList(); + }, + onFriendApplicationListRead: () { + // UiTools.toast('onFriendApplicationListRead'); + }, + onFriendListAdded: ( + List users, + ) { + TimFriendService.to.fetchList(); + }, + onFriendListDeleted: ( + List userList, + ) { + TimFriendService.to.fetchList(); + }, + + /// 有黑名单 + onBlackListAdd: ( + List infoList, + ) { + TimFriendService.to.fetchList(); + TimBlockService.to.fetchList(); + }, + + /// 移除黑名单 + onBlackListDeleted: ( + List userList, + ) { + TimFriendService.to.fetchList(); + TimBlockService.to.fetchList(); + }, + + /// 好友资料修改 + onFriendInfoChanged: ( + List infoList, + ) { + TimFriendService.to.fetchList(); + }, + ), + ); + + instance.setGroupListener( + listener: V2TimGroupListener( + /// 群组解散 + onGroupDismissed: ( + String groupID, + V2TimGroupMemberInfo opUser, + ) async { + await TimConversationService.to.deleteById( + 'group_$groupID', + ); + await TimGroupService.to.fetchList(); + + /// 如果是在当前的会话中,关闭会话内容,返回主页面 + if (currentConversationId.value == 'group_' + groupID) { + UiTools.toast('群已解散'); + Navigator.popUntil(Get.context!, (route) => route.isFirst); + } + }, + + /// 群资料修改 + onGroupInfoChanged: ( + String groupID, + List changeInfos, + ) { + TimConversationService.to.fetchList(); + TimGroupService.to.fetchList(); + }, + onMemberEnter: ( + String groupID, + List memberList, + ) { + UiTools.toast('onMemberEnter'); + }, + onMemberLeave: ( + String groupID, + V2TimGroupMemberInfo member, + ) { + UiTools.toast('onMemberLeave'); + }, + onMemberInvited: ( + String groupID, + V2TimGroupMemberInfo opUser, + List memberList, + ) { + UiTools.toast('onMemberInvited'); + }, + + /// 有用户被移出群聊,判断是否当前用户,当前会话,关闭会话 + onMemberKicked: ( + String groupID, + V2TimGroupMemberInfo opUser, + List memberList, + ) { + bool isYou = memberList + .where((e) => true + // e.userID == + // UserController.to.userInfo.value!.userId.toString(), + ) + .isNotEmpty; + if (isYou) { + if (currentConversationId.value == 'group_' + groupID) { + UiTools.toast('您已被${opUser.nickName}移出当前群聊'); + Navigator.popUntil(Get.context!, (route) => route.isFirst); + } + TimConversationService.to.deleteById( + 'group_' + groupID, + ); + TimGroupService.to.fetchList(); + } + }, + onMemberInfoChanged: ( + String groupID, + List v2TIMGroupMemberChangeInfoList, + ) { + UiTools.toast('onMemberInfoChanged'); + }, + onGroupRecycled: ( + String groupID, + V2TimGroupMemberInfo opUser, + ) { + UiTools.toast('onGroupRecycled'); + }, + onReceiveJoinApplication: ( + String groupID, + V2TimGroupMemberInfo member, + String opReason, + ) { + UiTools.toast('onReceiveJoinApplication'); + }, + onApplicationProcessed: ( + String groupID, + V2TimGroupMemberInfo opUser, + bool isAgreeJoin, + String opReason, + ) { + UiTools.toast('onApplicationProcessed'); + }, + + /// 有新的管理被授权 + onGrantAdministrator: ( + String groupID, + V2TimGroupMemberInfo opUser, + List memberList, + ) { + if (currentConversationId.value == 'group_' + groupID) { + GroupController.to.fetchGroupMemberList(); + } + }, + + /// 取消管理员 + onRevokeAdministrator: ( + String groupID, + V2TimGroupMemberInfo opUser, + List memberList, + ) { + if (currentConversationId.value == 'group_' + groupID) { + GroupController.to.fetchGroupMemberList(); + } + }, + onQuitFromGroup: ( + String groupID, + ) { + UiTools.toast('onQuitFromGroup'); + }, + onReceiveRESTCustomData: ( + String groupID, + String customData, + ) { + UiTools.toast('onReceiveRESTCustomData'); + }, + onGroupAttributeChanged: ( + String groupID, + Map groupAttributeMap, + ) { + UiTools.toast('onGroupAttributeChanged'); + }, + onTopicCreated: ( + String groupID, + String topicID, + ) { + UiTools.toast('onTopicCreated'); + }, + onTopicDeleted: ( + String groupID, + List topicIDList, + ) { + UiTools.toast('onTopicDeleted'); + }, + onTopicInfoChanged: ( + String groupID, + V2TimTopicInfo topicInfo, + ) { + UiTools.toast('onTopicInfoChanged'); + }, + ), + ); + } + + /// 有新消息的事件处理 + Future onRecvNewMessage(V2TimMessage message) async { + /// 过滤自定义消息中,用户输入状态的消息 + if (message.elemType == MessageElemType.V2TIM_ELEM_TYPE_CUSTOM) { + var msgData = json.decode(message.customElem!.data!); + + if (msgData['businessID'] == CustomMessageType.TYPING_STATUS) { + return; + } + } + + var conversation = await TimConversationService.to.getById( + getConversationIdByMessage(message), + ); + if (conversation.recvOpt == 0) { + /// 振动提醒 + Vibration.vibrate(duration: 100); + } + + if (_isMessageInCurrentConversation(message)) { + TimConversationService.to.markAsRead(conversation); + } else { + await TimConversationService.to.getUnreadCount(); + } + + /// 更新会话列表做了个延迟,要不然列表中的未读消息数量,不正确 + Future.delayed(const Duration(milliseconds: 500), () async { + await TimConversationService.to.fetchList(); + }); + + // eventBus.fire(message); + } + + /// 通过消息判断是否是当前会话 + bool _isMessageInCurrentConversation(V2TimMessage message) { + return getConversationIdByMessage(message) == currentConversationId.value; + } + + /// 通过消息获取会话ID + String getConversationIdByMessage(V2TimMessage message) { + String conId; + if (message.groupID != null) { + conId = 'group_' + message.groupID!; + } else { + conId = 'c2c_' + message.userID!; + } + + return conId; } } diff --git a/lib/utils/convert.dart b/lib/utils/convert.dart new file mode 100644 index 0000000..843f4fe --- /dev/null +++ b/lib/utils/convert.dart @@ -0,0 +1,30 @@ +import 'package:dart_date/dart_date.dart'; + +class Convert { + /// 隐藏字符串中间位 + /// + /// * [str] 要处理的字符串 + /// * [start] 字符串从0开始保留位数 + /// * [end] 末尾保留的长度 + static String hideCenterStr( + String str, { + int start = 8, + int end = 6, + }) { + if (str.length <= start + end) { + return str; + } + return '${str.substring(0, start)}...${str.substring(str.length - end)}'; + } + + /// 时间戳转日期 + /// + /// [timestamp] 要转换的时间戳 + /// [format] 日期格式 + static String timeFormat( + int timestamp, { + String format = 'yyyy-MM-dd HH:mm:ss', + }) { + return DateTime.fromMillisecondsSinceEpoch(timestamp * 1000).format(format); + } +} diff --git a/lib/utils/im_tools.dart b/lib/utils/im_tools.dart new file mode 100644 index 0000000..79fe947 --- /dev/null +++ b/lib/utils/im_tools.dart @@ -0,0 +1,251 @@ +import 'dart:convert'; + +import 'package:azlistview/azlistview.dart'; +import 'package:chat/configs/app_colors.dart'; +import 'package:chat/models/im/custom_message_model.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:tencent_im_sdk_plugin/enum/group_change_info_type.dart'; +import 'package:tencent_im_sdk_plugin/enum/group_tips_elem_type.dart'; +import 'package:tencent_im_sdk_plugin/enum/message_elem_type.dart'; +import 'package:tencent_im_sdk_plugin/models/v2_tim_friend_info.dart'; +import 'package:tencent_im_sdk_plugin/models/v2_tim_friend_info_result.dart'; +import 'package:tencent_im_sdk_plugin/models/v2_tim_message.dart'; + +class ImTools { + static String parseNicknameFromResult(V2TimFriendInfoResult? infoResult) { + if (infoResult == null) { + return ''; + } + + if (infoResult.friendInfo!.friendRemark != null) { + if (infoResult.friendInfo!.friendRemark! == '') { + return infoResult.friendInfo!.userProfile!.nickName!; + } else { + return infoResult.friendInfo!.friendRemark!; + } + } + + return infoResult.friendInfo!.userProfile!.nickName!; + } + + static String parseNicknameFromInfo(V2TimFriendInfo infoResult) { + if (infoResult.friendRemark != '') { + return infoResult.friendRemark!; + } + + return infoResult.userProfile!.nickName!; + } + + static parseMessage(V2TimMessage? message) { + String text = ''; + switch (message?.elemType) { + case MessageElemType.V2TIM_ELEM_TYPE_TEXT: + text = message!.textElem!.text!; + break; + case MessageElemType.V2TIM_ELEM_TYPE_IMAGE: + text = '[图片]'; + break; + case MessageElemType.V2TIM_ELEM_TYPE_SOUND: + text = '[语音]'; + break; + case MessageElemType.V2TIM_ELEM_TYPE_VIDEO: + text = '[视频]'; + break; + case MessageElemType.V2TIM_ELEM_TYPE_FILE: + text = '[文件]'; + break; + case MessageElemType.V2TIM_ELEM_TYPE_LOCATION: + text = '[位置]'; + break; + case MessageElemType.V2TIM_ELEM_TYPE_FACE: + text = '[表情]'; + break; + case MessageElemType.V2TIM_ELEM_TYPE_MERGER: + text = '[聊天记录]'; + break; + case MessageElemType.V2TIM_ELEM_TYPE_CUSTOM: + var data = json.decode(message!.customElem!.data!); + + switch (data['businessID']) { + case CustomMessageType.CALL: + text = '通话'; + break; + case CustomMessageType.DT_TRANSFER: + text = '[转账]'; + break; + case CustomMessageType.NAME_CARD: + text = '[名片]'; + break; + case CustomMessageType.GROUP_CARD: + text = '[群名片]'; + break; + case CustomMessageType.EVALUATION: + text = '[评价]'; + break; + default: + text = '[${data['businessID']}]'; + } + + break; + case MessageElemType.V2TIM_ELEM_TYPE_GROUP_TIPS: + switch (message?.groupTipsElem!.type) { + case GroupTipsElemType.V2TIM_GROUP_TIPS_TYPE_JOIN: + text = '加入群聊'; + break; + case GroupTipsElemType.V2TIM_GROUP_TIPS_TYPE_INVITE: + text = '邀请入群'; + break; + case GroupTipsElemType.V2TIM_GROUP_TIPS_TYPE_QUIT: + text = '退出群聊'; + break; + case GroupTipsElemType.V2TIM_GROUP_TIPS_TYPE_KICKED: + text = '踢出群聊'; + break; + case GroupTipsElemType.V2TIM_GROUP_TIPS_TYPE_SET_ADMIN: + text = '设置管理'; + break; + case GroupTipsElemType.V2TIM_GROUP_TIPS_TYPE_CANCEL_ADMIN: + text = '取消管理'; + break; + case GroupTipsElemType.V2TIM_GROUP_TIPS_TYPE_GROUP_INFO_CHANGE: + // (opMember 修改群资料:groupName & introduction & notification & faceUrl & owner & custom) + switch (message!.groupTipsElem!.groupChangeInfoList![0]!.type) { + case GroupChangeInfoType.V2TIM_GROUP_INFO_CHANGE_TYPE_NAME: + text = '修改群名称'; + break; + case GroupChangeInfoType + .V2TIM_GROUP_INFO_CHANGE_TYPE_INTRODUCTION: + text = '群简介修改'; + break; + case GroupChangeInfoType + .V2TIM_GROUP_INFO_CHANGE_TYPE_NOTIFICATION: + text = '修改群公告'; + break; + case GroupChangeInfoType.V2TIM_GROUP_INFO_CHANGE_TYPE_FACE_URL: + text = '群头像修改'; + break; + case GroupChangeInfoType.V2TIM_GROUP_INFO_CHANGE_TYPE_OWNER: + text = '群主变更'; + break; + case GroupChangeInfoType.V2TIM_GROUP_INFO_CHANGE_TYPE_CUSTOM: + text = '群自定义字段变更'; + break; + default: + text = message.groupTipsElem!.groupChangeInfoList![0]!.type + .toString(); + break; + } + break; + case GroupTipsElemType.V2TIM_GROUP_TIPS_TYPE_MEMBER_INFO_CHANGE: + text = '群成员资料变更'; + break; + default: + text = message!.groupTipsElem!.type.toString(); + } + break; + default: + text = message!.elemType.toString(); + } + + return text; + } + + /// 右侧索引栏样式 + static const IndexBarOptions indexBarOptions = IndexBarOptions( + needRebuild: true, + ignoreDragCancel: true, + downTextStyle: TextStyle( + fontSize: 12, + color: Colors.white, + ), + downItemDecoration: BoxDecoration( + shape: BoxShape.circle, + color: AppColors.primary, + ), + indexHintWidth: 120 / 2, + indexHintHeight: 100 / 2, + indexHintDecoration: BoxDecoration( + image: DecorationImage( + image: AssetImage( + 'assets/chats/index_bar.png', + ), + fit: BoxFit.contain, + ), + ), + indexHintAlignment: Alignment.centerRight, + indexHintChildAlignment: Alignment( + -0.25, + 0.0, + ), + indexHintOffset: Offset( + -20, + 0, + ), + ); + + /// 悬浮导航,显示ABCD~Z + static Widget susItem( + BuildContext context, + String tag, { + double susHeight = 40, + }) { + return Container( + height: susHeight, + width: MediaQuery.of(context).size.width, + padding: const EdgeInsets.only( + left: 16.0, + ), + color: Colors.grey[200], + alignment: Alignment.centerLeft, + child: Text( + tag, + ), + ); + } + + static showTrtcMessage(String userID) { + return showModalBottomSheet( + context: Get.context!, + isScrollControlled: true, + backgroundColor: AppColors.white, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.vertical(top: Radius.circular(8)), + ), + builder: (context) { + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + // PopMenuItem( + // '语音通话', + // onTap: () { + // TimService.to.tuiCalling.call(userID, CallingScenes.Audio); + // Get.back(); + // }, + // ), + // const Divider(height: 0), + // PopMenuItem( + // '视频通话', + // onTap: () { + // TimService.to.tuiCalling.call(userID, CallingScenes.Video); + // Get.back(); + // }, + // ), + const Divider(height: 0), + Container( + height: 8, + color: AppColors.page, + ), + const Divider(height: 0), + // PopMenuItem( + // '取消', + // onTap: () { + // Get.back(); + // }, + // ), + ], + ); + }, + ); + } +} diff --git a/lib/utils/request/http.dart b/lib/utils/request/http.dart new file mode 100644 index 0000000..ab1d11b --- /dev/null +++ b/lib/utils/request/http.dart @@ -0,0 +1,121 @@ +import 'package:chat/utils/request/http_request.dart'; +import 'package:dio/dio.dart'; + +class Http { + /// 取消请求 + static void cancelRequests({ + required CancelToken token, + }) { + HttpRequest().cancelRequests( + token: token, + ); + } + + /// GET 请求 + static Future get( + String path, { + Map? params, + Options? options, + CancelToken? cancelToken, + ProgressCallback? onReceiveProgress, + }) async { + return await HttpRequest().request( + path, + method: HttpMethod.get, + params: params, + options: options, + cancelToken: cancelToken, + ); + } + + /// POST 请求 + static Future post( + String path, { + Map? params, + dynamic data, + Options? options, + CancelToken? cancelToken, + ProgressCallback? onSendProgress, + ProgressCallback? onReceiveProgress, + }) async { + return await HttpRequest().request( + path, + method: HttpMethod.post, + params: params, + data: data, + options: options, + cancelToken: cancelToken, + onReceiveProgress: onReceiveProgress, + ); + } + + /// PUT + static Future put( + String path, { + Map? params, + dynamic data, + Options? options, + CancelToken? cancelToken, + ProgressCallback? onSendProgress, + ProgressCallback? onReceiveProgress, + }) async { + return await HttpRequest().request( + path, + method: HttpMethod.put, + params: params, + data: data, + options: options, + cancelToken: cancelToken, + onSendProgress: onSendProgress, + onReceiveProgress: onReceiveProgress, + ); + } + + /// DELETE + static Future delete( + String path, { + Map? params, + dynamic data, + Options? options, + CancelToken? cancelToken, + ProgressCallback? onSendProgress, + ProgressCallback? onReceiveProgress, + }) async { + return await HttpRequest().request( + path, + method: HttpMethod.delete, + params: params, + data: data, + options: options, + cancelToken: cancelToken, + onSendProgress: onSendProgress, + onReceiveProgress: onReceiveProgress, + ); + } + + /// 上传文件 + static Future upload( + String path, { + required String filePath, + Map? params, + Options? options, + CancelToken? cancelToken, + ProgressCallback? onSendProgress, + ProgressCallback? onReceiveProgress, + }) async { + var formData = FormData.fromMap({ + 'upload': await MultipartFile.fromFile(filePath), + }); + + return await HttpRequest().request( + path, + method: HttpMethod.post, + params: params, + data: formData, + options: options, + cancelToken: cancelToken, + onSendProgress: onSendProgress, + onReceiveProgress: onReceiveProgress, + ); + } +} diff --git a/lib/utils/request/http_interceptor.dart b/lib/utils/request/http_interceptor.dart new file mode 100644 index 0000000..1470efd --- /dev/null +++ b/lib/utils/request/http_interceptor.dart @@ -0,0 +1,97 @@ +import 'package:chat/services/auth_service.dart'; +import 'package:chat/utils/ui_tools.dart'; +import 'package:dio/dio.dart'; + +class HttpInterceptor extends Interceptor { + @override + void onRequest( + RequestOptions options, + RequestInterceptorHandler handler, + ) { + // 头部添加token + // options.headers['Authorization'] = AuthService.to.userToken; + options.headers['Accept'] = 'application/json'; + + super.onRequest( + options, + handler, + ); + } + + @override + void onResponse( + Response response, + ResponseInterceptorHandler handler, + ) { + final apiStatusCode = response.data['status_code']; + + if (apiStatusCode == 200) { + response.data = response.data['data']; + } else if (apiStatusCode == 401) { + throw DioError( + response: response, + error: "登录超时", + requestOptions: response.requestOptions, + ); + } else if (apiStatusCode == 404) { + throw DioError( + response: response, + error: "请求的接口不存在", + requestOptions: response.requestOptions, + ); + } else if (apiStatusCode == 0) { + throw DioError( + response: response, + error: response.data['message'], + requestOptions: response.requestOptions, + ); + } else { + throw DioError( + response: response, + error: response.data['message'], + requestOptions: response.requestOptions, + ); + } + super.onResponse( + response, + handler, + ); + } + + @override + void onError( + DioError err, + ErrorInterceptorHandler handler, + ) { + switch (err.type) { + // 连接服务器超时 + case DioErrorType.connectTimeout: + UiTools.toast('网络传输超时'); + break; + // 响应超时 + case DioErrorType.receiveTimeout: + UiTools.toast('网络传输超时'); + break; + // 发送超时 + case DioErrorType.sendTimeout: + UiTools.toast('网络传输超时'); + break; + // 请求取消 + case DioErrorType.cancel: + break; + // 404/503错误 + case DioErrorType.response: + break; + // other 其他错误类型 + case DioErrorType.other: + if (err.response?.data['status_code'] == 401) { + AuthService.to.logout(); + } + break; + } + super.onError( + err, + handler, + ); + } +} diff --git a/lib/utils/request/http_options.dart b/lib/utils/request/http_options.dart new file mode 100644 index 0000000..54fd532 --- /dev/null +++ b/lib/utils/request/http_options.dart @@ -0,0 +1,5 @@ +class HttpOptions { + static const String baseUrl = 'http://api.gl.shangkelian.cn/api/'; + static const int connectTimeout = 15000; + static const int receiveTimeout = 15000; +} diff --git a/lib/utils/request/http_request.dart b/lib/utils/request/http_request.dart new file mode 100644 index 0000000..5ccfece --- /dev/null +++ b/lib/utils/request/http_request.dart @@ -0,0 +1,91 @@ +import 'package:chat/utils/request/http_interceptor.dart'; +import 'package:chat/utils/request/http_options.dart'; +import 'package:dio/dio.dart'; + +enum HttpMethod { + get, + post, + put, + delete, + patch, + head, +} + +class HttpRequest { + static HttpRequest? _instance; + static Dio _dio = Dio(); + Dio get dio => _dio; + + HttpRequest._internal() { + _instance = this; + _instance!._init(); + } + + factory HttpRequest() => _instance ?? HttpRequest._internal(); + + static HttpRequest? getInstance() { + return _instance ?? HttpRequest._internal(); + } + + /// 取消请求token + final CancelToken _cancelToken = CancelToken(); + + _init() { + BaseOptions options = BaseOptions( + baseUrl: HttpOptions.baseUrl, + connectTimeout: HttpOptions.connectTimeout, + receiveTimeout: HttpOptions.receiveTimeout, + ); + _dio = Dio(options); + + /// 添加拦截器 + _dio.interceptors.add(HttpInterceptor()); + } + + /// 请求 + Future request( + String path, { + HttpMethod method = HttpMethod.get, + Map? params, + dynamic data, + Options? options, + CancelToken? cancelToken, + ProgressCallback? onSendProgress, + ProgressCallback? onReceiveProgress, + }) async { + const methodValues = { + HttpMethod.get: 'get', + HttpMethod.post: 'post', + HttpMethod.put: 'put', + HttpMethod.delete: 'delete', + HttpMethod.patch: 'patch', + HttpMethod.head: 'head' + }; + + options ??= Options(method: methodValues[method]); + + try { + Response response = await _dio.request( + path, + data: data, + queryParameters: params, + cancelToken: cancelToken ?? _cancelToken, + options: options, + onSendProgress: onSendProgress, + onReceiveProgress: onReceiveProgress, + ); + // ignore: avoid_print + print('请求地址:${response.requestOptions.uri}'); + // ignore: avoid_print + print('响应数据:${response.data}'); + return response.data; + } on DioError catch (e) { + throw Exception(e.message); + } + } + + /// 取消网络请求 + void cancelRequests({CancelToken? token}) { + token ?? _cancelToken.cancel('CANCELED'); + } +} diff --git a/lib/views/contact/group/create/index_page.dart b/lib/views/contact/group/create/index_page.dart new file mode 100644 index 0000000..3e1bdaa --- /dev/null +++ b/lib/views/contact/group/create/index_page.dart @@ -0,0 +1,15 @@ +import 'package:flutter/material.dart'; + +class ContactGroupCreatePage extends StatefulWidget { + const ContactGroupCreatePage({Key? key}) : super(key: key); + + @override + _ContactGroupCreatePageState createState() => _ContactGroupCreatePageState(); +} + +class _ContactGroupCreatePageState extends State { + @override + Widget build(BuildContext context) { + return Container(); + } +} diff --git a/lib/views/contact/group/manage/index_page.dart b/lib/views/contact/group/manage/index_page.dart new file mode 100644 index 0000000..074b9a3 --- /dev/null +++ b/lib/views/contact/group/manage/index_page.dart @@ -0,0 +1,15 @@ +import 'package:flutter/material.dart'; + +class ContactGroupManagePage extends StatefulWidget { + const ContactGroupManagePage({Key? key}) : super(key: key); + + @override + State createState() => _ContactGroupManagePageState(); +} + +class _ContactGroupManagePageState extends State { + @override + Widget build(BuildContext context) { + return Container(); + } +} diff --git a/lib/views/contact/group/notification/index_page.dart b/lib/views/contact/group/notification/index_page.dart new file mode 100644 index 0000000..816c532 --- /dev/null +++ b/lib/views/contact/group/notification/index_page.dart @@ -0,0 +1,17 @@ +import 'package:flutter/material.dart'; + +class ContactGroupNotificationPage extends StatefulWidget { + const ContactGroupNotificationPage({Key? key}) : super(key: key); + + @override + _ContactGroupNotificationPageState createState() => + _ContactGroupNotificationPageState(); +} + +class _ContactGroupNotificationPageState + extends State { + @override + Widget build(BuildContext context) { + return Container(); + } +} diff --git a/lib/views/conversation/index_page.dart b/lib/views/conversation/index_page.dart new file mode 100644 index 0000000..f3b43a6 --- /dev/null +++ b/lib/views/conversation/index_page.dart @@ -0,0 +1,82 @@ +import 'package:chat/controllers/group_controller.dart'; +import 'package:chat/controllers/private_controller.dart'; +import 'package:chat/routes/conversation_routes.dart'; +import 'package:chat/services/tim/conversation_service.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:tencent_im_sdk_plugin/enum/conversation_type.dart'; +import 'package:tencent_im_sdk_plugin/models/v2_tim_conversation.dart'; + +class ConversationPage extends StatefulWidget { + const ConversationPage({Key? key}) : super(key: key); + + @override + _ConversationPageState createState() => _ConversationPageState(); +} + +class _ConversationPageState extends State { + late final V2TimConversation conversation; + final _scaffoldKey = GlobalKey(); + final GlobalKey inputextField = GlobalKey(); + + @override + void initState() { + super.initState(); + conversation = Get.arguments['conversation']; + + /// 标记会话内消息已读 + TimConversationService.to.markAsRead(conversation); + if (conversation.type == ConversationType.V2TIM_GROUP) { + GroupController.to.setCurrentGroup(conversation.groupID!); + } else { + PrivateController.to.setCurrentFriend(conversation.userID!); + } + } + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: () { + inputextField.currentState.hideAllPanel(); + }, + child: Scaffold( + key: _scaffoldKey, + appBar: AppBar( + title: conversation.type == ConversationType.V2TIM_GROUP + ? GetX( + builder: (_) { + return Text( + _.currentGroup.value.conversation?.showName ?? '', + ); + }, + ) + : GetX( + builder: (_) { + return Text( + _.currentFriend.value.conversation?.showName ?? '', + ); + }, + ), + actions: [ + _topRightAction(), + ], + ), + ), + ); + } + + Widget _topRightAction() { + return IconButton( + icon: const Icon(Icons.more_horiz), + onPressed: () { + conversation.type == ConversationType.V2TIM_GROUP + ? Get.toNamed( + ConversationRoutes.infoGroup, + ) + : Get.toNamed( + ConversationRoutes.infoPrivate, + ); + }, + ); + } +} diff --git a/lib/views/conversation/info/group_page.dart b/lib/views/conversation/info/group_page.dart new file mode 100644 index 0000000..415a9b4 --- /dev/null +++ b/lib/views/conversation/info/group_page.dart @@ -0,0 +1,235 @@ +import 'package:adaptive_dialog/adaptive_dialog.dart'; +import 'package:chat/configs/app_colors.dart'; +import 'package:chat/controllers/group_controller.dart'; +import 'package:chat/routes/contact_routes.dart'; +import 'package:chat/services/tim/conversation_service.dart'; +import 'package:chat/services/tim/group_service.dart'; +import 'package:chat/views/conversation/info/widgets/group_member_preview.dart'; +import 'package:chat/views/home/widgets/action_button.dart'; +import 'package:chat/views/home/widgets/action_item.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; + +class ConversationInfoGroupPage extends StatelessWidget { + const ConversationInfoGroupPage({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + GroupController.to.fetchGroupMemberList(); + + return GetX(builder: (_) { + var currentGroup = _.currentGroup.value; + var group = _.currentGroup.value.group; + + return Scaffold( + appBar: AppBar( + title: Text('聊天信息(${group?.memberCount})'), + centerTitle: true, + ), + body: SingleChildScrollView( + child: Column( + children: [ + const GroupMemberPreview(), + const SizedBox(height: 8), + Container( + color: AppColors.white, + child: Column( + children: [ + ActionItem( + '群聊名称', + extend: group?.groupName, + onTap: () { + if (currentGroup.isAdmin || currentGroup.isOwner) { + // Get.toNamed( + // ImRoutes.groupName, + // ); + } + }, + ), + const Divider(height: 0, indent: 16), + ActionItem( + '群二维码', + onTap: () { + Get.toNamed( + ContactRoutes.groupQrCode, + ); + }, + ), + const Divider(height: 0, indent: 16), + ActionItem( + '群公告', + bottom: group?.notification, + onTap: () { + Get.toNamed( + ContactRoutes.groupNotification, + ); + }, + ), + Visibility( + visible: currentGroup.isAdmin || currentGroup.isOwner, + child: const Divider( + height: 0, + indent: 16, + ), + ), + Visibility( + visible: currentGroup.isAdmin || currentGroup.isOwner, + child: ActionItem( + '群管理', + onTap: () { + Get.toNamed( + ContactRoutes.groupManage, + ); + }, + ), + ), + Visibility( + visible: currentGroup.isAdmin || currentGroup.isOwner, + child: const Divider( + height: 0, + indent: 16, + ), + ), + Visibility( + visible: currentGroup.isAdmin || currentGroup.isOwner, + child: ActionItem( + '加群申请', + onTap: () { + Get.toNamed( + ContactRoutes.groupApprove, + ); + }, + ), + ), + ], + ), + ), + const SizedBox(height: 8), + Container( + color: AppColors.white, + child: Column( + children: [ + ActionItem( + '查找聊天记录', + onTap: () { + // Get.toNamed( + // ImRoutes.conversationSearch, + // ); + }, + ), + ], + ), + ), + const SizedBox(height: 8), + Container( + color: AppColors.white, + child: Column( + children: [ + ActionItem( + '消息免打扰', + rightWidget: SizedBox( + height: 24, + child: Switch( + value: + _.currentGroup.value.conversation!.recvOpt == 1, + onChanged: (e) async { + _.toggleReceiveOpt(); + }, + materialTapTargetSize: + MaterialTapTargetSize.shrinkWrap, + ), + ), + ), + const Divider( + height: 0, + indent: 16, + ), + ActionItem( + '置顶聊天', + rightWidget: SizedBox( + height: 24, + child: Switch( + value: _.currentGroup.value.conversation!.isPinned!, + onChanged: (e) async { + _.togglePinned(); + }, + materialTapTargetSize: + MaterialTapTargetSize.shrinkWrap, + ), + ), + ), + ], + ), + ), + const SizedBox(height: 8), + Container( + color: AppColors.white, + child: Column( + children: [ + ActionItem( + '我在群里的昵称', + extend: _.currentGroup.value.selfInfo?.nameCard ?? '', + onTap: () { + Get.toNamed( + ContactRoutes.groupNickname, + ); + }, + ), + ], + ), + ), + const SizedBox(height: 8), + Column( + children: [ + ActionButton( + '清空聊天记录', + onTap: () async { + OkCancelResult result = await showOkCancelAlertDialog( + style: AdaptiveStyle.iOS, + context: Get.context!, + title: '系统提示', + message: '将删除该聊天记录,是否继续?', + okLabel: '确定', + cancelLabel: '取消', + defaultType: OkCancelAlertDefaultType.ok, + ); + + if (result == OkCancelResult.ok) { + TimConversationService.to.clearHistoryMessage( + _.currentGroup.value.conversation!, + ); + } + }, + ), + if (!currentGroup.isOwner) const Divider(height: 0), + ActionButton( + '删除并退出', + onTap: () async { + OkCancelResult result = await showOkCancelAlertDialog( + style: AdaptiveStyle.iOS, + context: Get.context!, + title: '系统提示', + message: '将删除并退出该群组,是否继续?', + okLabel: '确定', + cancelLabel: '取消', + defaultType: OkCancelAlertDefaultType.ok, + ); + + if (result == OkCancelResult.ok) { + TimGroupService.to.quit( + group!, + ); + Navigator.popUntil(context, (route) => route.isFirst); + } + }, + ), + ], + ), + const SizedBox(height: 16), + ], + ), + ), + ); + }); + } +} diff --git a/lib/views/conversation/info/private_page.dart b/lib/views/conversation/info/private_page.dart new file mode 100644 index 0000000..98aaef0 --- /dev/null +++ b/lib/views/conversation/info/private_page.dart @@ -0,0 +1,157 @@ +import 'package:adaptive_dialog/adaptive_dialog.dart'; +import 'package:chat/configs/app_colors.dart'; +import 'package:chat/controllers/private_controller.dart'; +import 'package:chat/routes/contact_routes.dart'; +import 'package:chat/services/tim/conversation_service.dart'; +import 'package:chat/views/home/widgets/action_item.dart'; +import 'package:chat/widgets/custom_avatar.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; + +class ConversationInfoPrivatePage extends StatelessWidget { + const ConversationInfoPrivatePage({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return GetX(builder: (_) { + return Scaffold( + appBar: AppBar( + title: const Text('聊天信息'), + ), + body: SingleChildScrollView( + child: Column( + children: [ + Container( + padding: const EdgeInsets.all(16), + decoration: const BoxDecoration(color: AppColors.white), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + InkWell( + onTap: () { + Get.toNamed( + ContactRoutes.friendProfile, + ); + }, + child: Column( + children: [ + CustomAvatar( + _.currentFriend.value.userProfile?.faceUrl, + size: 54, + ), + const SizedBox(height: 4), + Text( + _.currentFriend.value.conversation!.showName!, + style: const TextStyle( + color: AppColors.unactive, + fontSize: 12, + ), + ), + ], + ), + ), + const SizedBox(width: 16), + InkWell( + onTap: () { + Get.toNamed( + ContactRoutes.groupCreate, + ); + }, + child: Container( + width: 54, + height: 54, + decoration: BoxDecoration( + color: AppColors.unactive.withOpacity(0.1), + border: Border.all( + color: AppColors.unactive.withOpacity(0.3), + width: 0.4, + style: BorderStyle.solid, + ), + borderRadius: BorderRadius.circular(8), + ), + child: const Center( + child: Icon( + Icons.add, + color: AppColors.unactive, + ), + ), + ), + ) + ], + ), + ), + const Divider(height: 0), + const SizedBox(height: 8), + const Divider(height: 0), + ActionItem( + '查找聊天记录', + onTap: () { + // Get.toNamed( + // ImRoutes.conversationSearch, + // ); + }, + ), + const Divider(height: 0), + const SizedBox(height: 8), + const Divider(height: 0), + ActionItem( + '消息免打扰', + rightWidget: SizedBox( + height: 24, + child: Switch( + value: _.currentFriend.value.conversation!.recvOpt == 1, + onChanged: (e) async { + _.changeReceiveOpt(); + }, + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + ), + ), + ), + const Divider( + height: 0, + indent: 16, + ), + ActionItem( + '置顶聊天', + rightWidget: SizedBox( + height: 24, + child: Switch( + value: _.currentFriend.value.conversation!.isPinned!, + onChanged: (e) async { + _.togglePinned(); + }, + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + ), + ), + ), + const Divider(height: 0), + const SizedBox(height: 8), + const Divider(height: 0), + ActionItem( + '清空聊天记录', + onTap: () async { + OkCancelResult result = await showOkCancelAlertDialog( + style: AdaptiveStyle.iOS, + context: Get.context!, + title: '系统提示', + message: '将删除该聊天记录,是否继续?', + okLabel: '确定', + cancelLabel: '取消', + defaultType: OkCancelAlertDefaultType.ok, + ); + + if (result == OkCancelResult.ok) { + TimConversationService.to.clearHistoryMessage( + _.currentFriend.value.conversation!, + ); + } + }, + ), + const Divider(height: 0), + ], + ), + ), + ); + }); + } +} diff --git a/lib/views/conversation/info/widgets/group_member_preview.dart b/lib/views/conversation/info/widgets/group_member_preview.dart new file mode 100644 index 0000000..2b9f03f --- /dev/null +++ b/lib/views/conversation/info/widgets/group_member_preview.dart @@ -0,0 +1,209 @@ +import 'dart:math'; + +import 'package:adaptive_dialog/adaptive_dialog.dart'; +import 'package:chat/configs/app_colors.dart'; +import 'package:chat/controllers/group_controller.dart'; +import 'package:chat/controllers/private_controller.dart'; +import 'package:chat/routes/contact_routes.dart'; +import 'package:chat/widgets/custom_avatar.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:tencent_im_sdk_plugin/enum/group_member_role.dart'; +import 'package:tencent_im_sdk_plugin/models/v2_tim_group_member_full_info.dart'; + +class GroupMemberPreview extends StatelessWidget { + const GroupMemberPreview({ + Key? key, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return GetX(builder: (_) { + List items = List.empty(growable: true); + + if (_.currentGroup.value.memberList != null) { + var members = _.currentGroup.value.memberList!; + + if (members.length > 13) { + members = members.sublist(0, 13); + } + + for (var item in members) { + items.add(_memberItem(item!)); + } + } + + /// 因为 Public 类型的群,不支持邀请功能,用户只能主动申请加群 + items.add( + InkWell( + onTap: () async { + OkCancelResult result = await showOkCancelAlertDialog( + style: AdaptiveStyle.iOS, + context: Get.context!, + title: '系统提示', + message: '当前群聊不支持邀请用户,请分享群二维码至您要邀请的好友。', + okLabel: '去分享', + cancelLabel: '取消', + defaultType: OkCancelAlertDefaultType.ok, + ); + + if (result == OkCancelResult.ok) { + Get.toNamed(ContactRoutes.groupQrCode); + } + }, + child: Column( + children: [ + Container( + width: 44, + height: 44, + decoration: BoxDecoration( + color: AppColors.unactive.withOpacity(0.1), + border: Border.all( + color: AppColors.unactive.withOpacity(0.3), + width: 0.4, + style: BorderStyle.solid, + ), + borderRadius: BorderRadius.circular(4), + ), + child: const Center( + child: Icon( + Icons.add, + color: AppColors.unactive, + ), + ), + ), + const Text( + '邀请', + style: TextStyle( + fontSize: 10, + color: AppColors.unactive, + ), + ), + ], + ), + ), + ); + + if (_.currentGroup.value.isAdmin || _.currentGroup.value.isOwner) { + items.add( + InkWell( + onTap: () { + Get.toNamed( + ContactRoutes.groupKick, + ); + }, + child: Column( + children: [ + Container( + width: 44, + height: 44, + decoration: BoxDecoration( + color: AppColors.unactive.withOpacity(0.1), + border: Border.all( + color: AppColors.unactive.withOpacity(0.3), + width: 0.4, + style: BorderStyle.solid, + ), + borderRadius: BorderRadius.circular(4), + ), + child: const Center( + child: Icon( + Icons.remove, + color: AppColors.unactive, + ), + ), + ), + const Text( + '移除', + style: TextStyle( + fontSize: 10, + color: AppColors.unactive, + ), + ), + ], + ), + ), + ); + } + + return Container( + color: AppColors.white, + padding: const EdgeInsets.all(16), + child: GridView.count( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + crossAxisCount: 5, + childAspectRatio: 1 / 1.1, + crossAxisSpacing: 12, + mainAxisSpacing: 12, + children: items, + ), + ); + }); + } + + Widget _memberItem(V2TimGroupMemberFullInfo member) { + double w = 40; + double h = 12; + return InkWell( + onTap: () async { + await PrivateController.to.setCurrentFriend(member.userID); + Get.toNamed( + ContactRoutes.friendProfile, + ); + }, + child: Column( + children: [ + ClipRRect( + child: Stack( + children: [ + CustomAvatar(member.faceUrl), + if (member.role == + GroupMemberRoleType.V2TIM_GROUP_MEMBER_ROLE_ADMIN || + member.role == + GroupMemberRoleType.V2TIM_GROUP_MEMBER_ROLE_OWNER) + Positioned( + left: 0, + top: sqrt(w * w / 2 - sqrt2 * w * h + h * h), + child: Transform.rotate( + angle: -0.25 * pi, + alignment: Alignment.bottomLeft, + child: Container( + color: member.role == + GroupMemberRoleType + .V2TIM_GROUP_MEMBER_ROLE_OWNER + ? AppColors.red + : AppColors.golden, + width: w, + height: h, + alignment: Alignment.center, + child: Text( + member.role == + GroupMemberRoleType + .V2TIM_GROUP_MEMBER_ROLE_OWNER + ? '群主' + : '管理员', + style: const TextStyle( + color: AppColors.white, + fontSize: 8, + ), + ), + ), + ), + ), + ], + ), + ), + const SizedBox(height: 2), + Text( + member.nameCard!.isNotEmpty ? member.nameCard! : member.nickName!, + style: const TextStyle( + fontSize: 10, + color: AppColors.unactive, + ), + ) + ], + ), + ); + } +} diff --git a/lib/views/home/index_page.dart b/lib/views/home/index_page.dart index 4c1edc2..3119cbc 100644 --- a/lib/views/home/index_page.dart +++ b/lib/views/home/index_page.dart @@ -1,19 +1,219 @@ +import 'package:chat/configs/app_colors.dart'; +import 'package:chat/routes/app_routes.dart'; +import 'package:chat/routes/contact_routes.dart'; +import 'package:chat/routes/user_routes.dart'; +import 'package:chat/services/tim/conversation_service.dart'; +import 'package:chat/views/home/widgets/conversation_item.dart'; +import 'package:chat/widgets/custom_easy_refresh.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_easyrefresh/easy_refresh.dart'; +import 'package:get/get.dart'; +import 'package:permission_handler/permission_handler.dart'; -class HomePage extends StatefulWidget { +class HomePage extends StatelessWidget { const HomePage({Key? key}) : super(key: key); - @override - _HomePageState createState() => _HomePageState(); -} - -class _HomePageState extends State { @override Widget build(BuildContext context) { return Scaffold( - appBar: AppBar( - title: const Text('消息'), + backgroundColor: AppColors.white, + appBar: _appBar(), + body: EasyRefresh( + header: CustomEasyRefresh.header, + // firstRefresh: true, + onRefresh: () async { + await TimConversationService.to.fetchList(); + }, + child: GetX( + builder: (_) { + return _.conversationList.isEmpty + ? CustomEasyRefresh.empty() + : ListView.separated( + shrinkWrap: true, + physics: const ClampingScrollPhysics(), + itemCount: _.conversationList.length, + itemBuilder: (context, index) { + return ConversationItem(_.conversationList[index]!); + }, + separatorBuilder: (context, index) { + return const Divider( + height: 0, + indent: 72, + ); + }, + ); + }, + ), ), ); } + + PreferredSizeWidget _appBar() { + return AppBar( + title: const Text('聊聊'), + actions: [ + IconButton( + onPressed: () { + Get.toNamed(AppRoutes.search); + }, + icon: const Icon(Icons.search_outlined), + ), + PopupMenuButton( + onSelected: (String value) { + switch (value) { + case 'A': + Get.toNamed(UserRoutes.qrCode); + break; + case 'B': + Get.toNamed(ContactRoutes.groupCreate); + break; + case 'C': + Get.toNamed(ContactRoutes.friendSearch); + break; + case 'D': + Permission.camera.request().isGranted.then((value) { + if (value) { + Get.toNamed(AppRoutes.scan); + } + }); + break; + } + }, + tooltip: '', + offset: const Offset(0, 56), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + icon: const Icon( + Icons.add, + ), + itemBuilder: (_) { + return [ + _popupMenuItem('我的二维码', 'A', Icons.qr_code_outlined), + _popupMenuItem('发起群聊', 'B', Icons.textsms), + _popupMenuItem('添加朋友', 'C', Icons.person_add_alt), + _popupMenuItem('扫一扫', 'D', Icons.photo_camera), + ]; + }, + ) + ], + ); + } + + /// 右上角弹出菜单 + PopupMenuItem _popupMenuItem( + String text, + String value, + IconData icon, + ) { + return PopupMenuItem( + value: value, + child: Row( + children: [ + Icon( + icon, + color: AppColors.primary, + size: 18, + ), + const SizedBox(width: 8), + Text( + text, + style: const TextStyle( + fontSize: 14, + ), + ), + ], + ), + ); + } + + // /// 左侧抽题 + // Widget _drawer() { + // return Drawer( + // child: ListView( + // children: [ + // GetX(builder: (_) { + // return DrawerHeader( + // child: Column( + // crossAxisAlignment: CrossAxisAlignment.start, + // children: [ + // CustomCircleAvatar( + // _.userInfo.value!.avatar, + // size: 72, + // ), + // const SizedBox(height: 8), + // Text( + // _.userInfo.value!.nickname, + // style: const TextStyle( + // fontSize: 24, + // fontWeight: FontWeight.bold, + // ), + // ), + // ], + // ), + // ); + // }), + // ListTile( + // onTap: () { + // Get.back(); + // Get.toNamed(UserRoutes.info); + // }, + // leading: const Icon(Icons.info_outlined), + // title: const Text('修改资料'), + // ), + // const Divider(height: 0), + // ListTile( + // onTap: () { + // Get.back(); + // Get.toNamed( + // ImRoutes.friend, + // arguments: { + // 'name_card': false, + // }, + // ); + // }, + // leading: const Icon(Icons.person_outlined), + // title: const Text('我的好友'), + // ), + // const Divider(height: 0), + // ListTile( + // onTap: () { + // Get.back(); + // Get.toNamed(ImRoutes.group); + // }, + // leading: const Icon(Icons.group_outlined), + // title: const Text('我的群组'), + // ), + // const Divider(height: 0), + // ListTile( + // onTap: () { + // Get.back(); + // Get.toNamed(ImRoutes.blcok); + // }, + // leading: const Icon(Icons.block_flipped), + // title: const Text('黑名单'), + // ), + // const Divider(height: 0), + // ListTile( + // onTap: () { + // Get.back(); + // Get.toNamed(ImRoutes.friendRequest); + // }, + // leading: const Icon(Icons.person_add_alt_outlined), + // title: const Text('好友申请'), + // ), + // const Divider(height: 0), + // ListTile( + // onTap: () { + // Get.back(); + // Get.toNamed(ImRoutes.setting); + // }, + // leading: const Icon(Icons.settings_outlined), + // title: const Text('消息设置'), + // ), + // const Divider(height: 0), + // ], + // ), + // ); + // } } diff --git a/lib/views/home/widgets/action_button.dart b/lib/views/home/widgets/action_button.dart new file mode 100644 index 0000000..956f852 --- /dev/null +++ b/lib/views/home/widgets/action_button.dart @@ -0,0 +1,39 @@ +import 'package:chat/configs/app_colors.dart'; +import 'package:flutter/material.dart'; + +class ActionButton extends StatelessWidget { + final String text; + final Color color; + final VoidCallback? onTap; + const ActionButton( + this.text, { + this.color = AppColors.red, + this.onTap, + Key? key, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () { + onTap?.call(); + }, + child: Container( + color: AppColors.white, + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Center( + child: Text( + text, + style: TextStyle( + fontWeight: FontWeight.w500, + color: color, + ), + ), + ), + ), + ), + ); + } +} diff --git a/lib/views/home/widgets/action_item.dart b/lib/views/home/widgets/action_item.dart new file mode 100644 index 0000000..e043719 --- /dev/null +++ b/lib/views/home/widgets/action_item.dart @@ -0,0 +1,76 @@ +import 'package:chat/configs/app_colors.dart'; +import 'package:flutter/material.dart'; + +class ActionItem extends StatelessWidget { + final String title; + final String? extend; + final Widget? rightWidget; + final String? bottom; + final VoidCallback? onTap; + + const ActionItem( + this.title, { + this.extend, + this.rightWidget, + this.bottom, + this.onTap, + Key? key, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () { + onTap?.call(); + }, + child: Container( + color: AppColors.white, + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + title, + style: const TextStyle( + fontSize: 16, + ), + ), + Expanded(child: Container()), + if (extend != null) + Text( + extend!, + style: const TextStyle( + color: AppColors.unactive, + ), + ), + rightWidget ?? + const Icon( + Icons.arrow_forward_ios, + size: 16, + color: AppColors.unactive, + ), + ], + ), + if (bottom != null && bottom!.isNotEmpty) + Padding( + padding: const EdgeInsets.only(top: 4), + child: Text( + bottom!, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: const TextStyle( + color: AppColors.unactive, + fontSize: 12, + ), + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/views/home/widgets/conversation_item.dart b/lib/views/home/widgets/conversation_item.dart new file mode 100644 index 0000000..05da61b --- /dev/null +++ b/lib/views/home/widgets/conversation_item.dart @@ -0,0 +1,212 @@ +import 'package:chat/configs/app_colors.dart'; +import 'package:chat/routes/conversation_routes.dart'; +import 'package:chat/services/tim/conversation_service.dart'; +import 'package:chat/utils/convert.dart'; +import 'package:chat/views/home/widgets/group_avatar.dart'; +import 'package:chat/views/home/widgets/message_preview_widget.dart'; +import 'package:chat/views/home/widgets/pop_menu_item.dart'; +import 'package:chat/widgets/custom_avatar.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:tencent_im_sdk_plugin/enum/conversation_type.dart'; +import 'package:tencent_im_sdk_plugin/models/v2_tim_conversation.dart'; + +class ConversationItem extends StatelessWidget { + final V2TimConversation conversation; + const ConversationItem(this.conversation, {Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Container( + height: 68, + decoration: BoxDecoration( + color: conversation.isPinned! ? AppColors.page : null, + ), + padding: const EdgeInsets.only( + left: 16, + right: 16, + top: 12, + bottom: 12, + ), + child: GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () { + Get.toNamed( + ConversationRoutes.index, + arguments: { + 'conversation': conversation, + }, + ); + }, + onLongPress: () async { + await _showLongPressMenu(); + }, + child: Row( + children: [ + Stack( + clipBehavior: Clip.none, + children: [ + conversation.type == ConversationType.V2TIM_C2C + ? CustomAvatar( + conversation.faceUrl, + ) + : GroupAvatar(conversation.groupID!), + Visibility( + visible: conversation.recvOpt == 0 && + conversation.unreadCount! > 0, + child: Positioned( + right: -5, + top: -5, + child: Container( + width: 18, + height: 18, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(18), + color: AppColors.red, + ), + alignment: Alignment.center, + child: Center( + child: Text( + conversation.unreadCount! > 99 + ? '99+' + : conversation.unreadCount.toString(), + style: const TextStyle( + fontSize: 10, + color: AppColors.white, + ), + ), + ), + ), + ), + ), + Visibility( + visible: conversation.recvOpt == 1 && + conversation.unreadCount! > 0, + child: const Positioned( + right: -3, + top: -3, + child: Icon( + Icons.circle_rounded, + color: AppColors.red, + size: 8, + ), + ), + ) + ], + ), + const SizedBox(width: 16), + Expanded( + child: Column( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + conversation.showName!, + style: const TextStyle( + fontSize: 16, + ), + ), + MessagePreviewWidget(conversation.lastMessage), + ], + ), + ), + Column( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Text( + conversation.lastMessage == null + ? '' + : Convert.timeFormat( + conversation.lastMessage!.timestamp!, + format: 'MM/dd HH:mm', + ), + style: const TextStyle( + fontSize: 12, + color: Colors.grey, + ), + ), + if (conversation.recvOpt == 1) + const Icon( + Icons.notifications_off_outlined, + size: 14, + color: AppColors.unactive, + ), + ], + ), + ], + ), + ), + ); + } + + Future _showLongPressMenu() async { + showModalBottomSheet( + context: Get.context!, + isScrollControlled: true, + backgroundColor: AppColors.white, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.vertical(top: Radius.circular(8)), + ), + builder: (context) { + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + PopMenuItem( + conversation.isPinned! ? '取消置顶' : '聊天置顶', + onTap: () { + TimConversationService.to.setOnTop(conversation); + Get.back(); + }, + ), + const Divider(height: 0), + PopMenuItem( + conversation.recvOpt == 1 ? '取消免打扰' : '消息免打扰', + onTap: () { + TimConversationService.to.setReceiveOpt(conversation); + Get.back(); + }, + ), + const Divider(height: 0), + PopMenuItem( + '清空聊天记录', + onTap: () { + TimConversationService.to.clearHistoryMessage(conversation); + Get.back(); + }, + ), + const Divider(height: 0), + PopMenuItem( + '标为已读', + onTap: () { + TimConversationService.to.markAsRead(conversation); + Get.back(); + }, + ), + const Divider(height: 0), + PopMenuItem( + '删除该聊天', + onTap: () { + TimConversationService.to.delete(conversation); + Get.back(); + }, + ), + const Divider(height: 0.4), + Container( + color: AppColors.page, + height: 8, + ), + const Divider(height: 0.4), + PopMenuItem( + '取消', + onTap: () { + Get.back(); + }, + ), + ], + ); + }, + ); + } +} diff --git a/lib/views/home/widgets/friend_selector.dart b/lib/views/home/widgets/friend_selector.dart new file mode 100644 index 0000000..4589fda --- /dev/null +++ b/lib/views/home/widgets/friend_selector.dart @@ -0,0 +1,140 @@ +import 'package:azlistview/azlistview.dart'; +import 'package:chat/configs/app_colors.dart'; +import 'package:chat/models/im/contact_info_model.dart'; +import 'package:chat/services/tim/friend_service.dart'; +import 'package:chat/utils/im_tools.dart'; +import 'package:chat/widgets/custom_avatar.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:tencent_im_sdk_plugin/models/v2_tim_friend_info.dart'; +import 'package:tencent_im_sdk_plugin/models/v2_tim_group_member_full_info.dart'; + +class FriendSelector extends StatefulWidget { + final Function(List) onChanged; + final List? lockedUsers; + const FriendSelector({ + required this.onChanged, + this.lockedUsers, + Key? key, + }) : super(key: key); + + @override + State createState() => _FriendSelectorState(); +} + +class _FriendSelectorState extends State { + /// 选中的好友列表 + List selectList = + List.empty(growable: true); + + /// 选择列表改变的事件,更新选中列表 + _selectListChange(id) { + setState(() { + if (selectList.contains(id)) { + selectList.remove(id); + } else { + selectList.add(id); + } + }); + widget.onChanged(selectList); + } + + @override + Widget build(BuildContext context) { + return GetX( + builder: (_) { + return AzListView( + data: _.contacts, + itemCount: _.contacts.length, + itemBuilder: (BuildContext context, int index) { + ContactInfoModel info = _.contacts[index]; + return _contactItem(info); + }, + susItemBuilder: (BuildContext context, int index) { + ContactInfoModel model = _.contacts[index]; + return ImTools.susItem( + context, + model.getSuspensionTag(), + susHeight: 32, + ); + }, + indexBarData: SuspensionUtil.getTagIndexList(_.contacts).toList(), + indexBarOptions: ImTools.indexBarOptions, + ); + }, + ); + } + + Widget _contactItem( + ContactInfoModel info, + ) { + bool isDisable = widget.lockedUsers == null + ? false + : widget.lockedUsers! + .where( + (e) => e!.userID == info.friendInfo!.userID, + ) + .isNotEmpty; + + return Column( + children: [ + GestureDetector( + onTap: () {}, + child: Container( + color: AppColors.white, + padding: const EdgeInsets.symmetric(vertical: 4), + child: GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: isDisable + ? null + : () { + setState(() { + _selectListChange(info.friendInfo); + }); + }, + child: Row( + children: [ + Radio( + groupValue: + selectList.contains(info.friendInfo) || isDisable + ? info.friendInfo + : null, + onChanged: isDisable + ? null + : (value) { + setState(() { + _selectListChange(info.friendInfo); + }); + }, + value: info.friendInfo!, + toggleable: true, + ), + CustomAvatar( + info.friendInfo!.userProfile!.faceUrl, + size: 32, + radius: 2, + ), + const SizedBox( + width: 16, + ), + Expanded( + child: Text( + info.name, + style: const TextStyle( + fontSize: 16, + ), + ), + ), + ], + ), + ), + ), + ), + const Divider( + height: 0, + indent: 92, + ), + ], + ); + } +} diff --git a/lib/views/home/widgets/group_avatar.dart b/lib/views/home/widgets/group_avatar.dart new file mode 100644 index 0000000..422926b --- /dev/null +++ b/lib/views/home/widgets/group_avatar.dart @@ -0,0 +1,68 @@ +import 'package:chat/configs/app_colors.dart'; +import 'package:chat/services/tim/group_service.dart'; +import 'package:chat/widgets/custom_avatar.dart'; +import 'package:flutter/material.dart'; +import 'package:tencent_im_sdk_plugin/models/v2_tim_group_member_full_info.dart'; + +class GroupAvatar extends StatefulWidget { + final String groupID; + final double size; + const GroupAvatar( + this.groupID, { + this.size = 44, + Key? key, + }) : super(key: key); + + @override + State createState() => _GroupAvatarState(); +} + +class _GroupAvatarState extends State { + List? members; + + @override + void initState() { + super.initState(); + + TimGroupService.to.members(widget.groupID, count: 9).then((value) { + setState(() { + members = value; + }); + }); + } + + @override + Widget build(BuildContext context) { + return Container( + width: widget.size, + height: widget.size, + decoration: BoxDecoration( + border: Border.all( + color: AppColors.border, + width: 0.4, + ), + borderRadius: BorderRadius.circular(4), + ), + child: members == null + ? CustomAvatar('') + : GridView.builder( + padding: const EdgeInsets.all(1), + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: + (members != null && members!.length > 4) ? 3 : 2, + childAspectRatio: 1, + crossAxisSpacing: 1, + mainAxisSpacing: 1, + ), + itemCount: members!.length, + itemBuilder: (context, index) { + return CustomAvatar( + members![index]?.faceUrl, + size: widget.size / 3, + radius: 2, + ); + }, + ), + ); + } +} diff --git a/lib/views/home/widgets/group_user_selector.dart b/lib/views/home/widgets/group_user_selector.dart new file mode 100644 index 0000000..a5a1e00 --- /dev/null +++ b/lib/views/home/widgets/group_user_selector.dart @@ -0,0 +1,111 @@ +import 'package:chat/configs/app_colors.dart'; +import 'package:chat/widgets/custom_avatar.dart'; +import 'package:flutter/material.dart'; +import 'package:tencent_im_sdk_plugin/models/v2_tim_group_member_full_info.dart'; + +class GroupUserSelector extends StatefulWidget { + final Function(List) onChanged; + final List selectedList; + final List originalList; + + const GroupUserSelector({ + required this.onChanged, + required this.selectedList, + required this.originalList, + Key? key, + }) : super(key: key); + + @override + State createState() => _GroupUserSelectorState(); +} + +class _GroupUserSelectorState extends State { + /// 选中的好友列表 + List selectList = + List.empty(growable: true); + + /// 选择列表改变的事件,更新选中列表 + _selectListChange(id) { + setState(() { + if (selectList.contains(id)) { + selectList.remove(id); + } else { + selectList.add(id); + } + }); + widget.onChanged(selectList); + } + + @override + Widget build(BuildContext context) { + return ListView.separated( + itemBuilder: (_, index) { + return _contactItem(widget.originalList[index]!); + }, + separatorBuilder: (_, index) { + return const Divider( + height: 0, + ); + }, + itemCount: widget.originalList.length, + ); + } + + Widget _contactItem( + V2TimGroupMemberFullInfo info, + ) { + return Column( + children: [ + GestureDetector( + onTap: () {}, + child: Container( + color: AppColors.white, + padding: const EdgeInsets.symmetric(vertical: 4), + child: GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () { + setState(() { + _selectListChange(info); + }); + }, + child: Row( + children: [ + Radio( + groupValue: selectList.contains(info) ? info : null, + onChanged: (value) { + setState(() { + _selectListChange(info); + }); + }, + value: info, + toggleable: true, + ), + CustomAvatar( + info.faceUrl, + size: 32, + radius: 2, + ), + const SizedBox( + width: 16, + ), + Expanded( + child: Text( + info.nickName!, + style: const TextStyle( + fontSize: 16, + ), + ), + ), + ], + ), + ), + ), + ), + const Divider( + height: 0, + indent: 92, + ), + ], + ); + } +} diff --git a/lib/views/home/widgets/message_preview_widget.dart b/lib/views/home/widgets/message_preview_widget.dart new file mode 100644 index 0000000..6c22c51 --- /dev/null +++ b/lib/views/home/widgets/message_preview_widget.dart @@ -0,0 +1,26 @@ +import 'package:chat/configs/app_colors.dart'; +import 'package:chat/utils/im_tools.dart'; +import 'package:flutter/material.dart'; +import 'package:tencent_im_sdk_plugin/models/v2_tim_message.dart'; + +class MessagePreviewWidget extends StatelessWidget { + final V2TimMessage? message; + const MessagePreviewWidget(this.message, {Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + if (message == null) { + return const Text(''); + } + + return Text( + ImTools.parseMessage(message), + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: const TextStyle( + color: AppColors.unactive, + fontSize: 14, + ), + ); + } +} diff --git a/lib/views/home/widgets/pop_menu_item.dart b/lib/views/home/widgets/pop_menu_item.dart new file mode 100644 index 0000000..b16e90e --- /dev/null +++ b/lib/views/home/widgets/pop_menu_item.dart @@ -0,0 +1,29 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; + +class PopMenuItem extends StatelessWidget { + final String text; + final VoidCallback? onTap; + + const PopMenuItem( + this.text, { + this.onTap, + Key? key, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () { + onTap?.call(); + }, + child: Container( + height: 52, + width: Get.width, + alignment: Alignment.center, + child: Text(text), + ), + ); + } +} diff --git a/lib/views/public/app_page.dart b/lib/views/public/app_page.dart index 5351231..6a2df01 100644 --- a/lib/views/public/app_page.dart +++ b/lib/views/public/app_page.dart @@ -20,19 +20,23 @@ class AppPage extends StatelessWidget { final List _tabBarList = [ { - 'icon': 'tabBar_03.png', + 'icon': Icons.message_outlined, + 'active_icon': Icons.message, 'label': '消息', }, { - 'icon': 'tabBar_03.png', + 'icon': Icons.contact_page_outlined, + 'active_icon': Icons.contact_page, 'label': '通讯录', }, { - 'icon': 'tabBar_03.png', + 'icon': Icons.explore_outlined, + 'active_icon': Icons.explore, 'label': '发现', }, { - 'icon': 'tabBar_03.png', + 'icon': Icons.person_outline, + 'active_icon': Icons.person, 'label': '我的', }, ]; @@ -47,16 +51,14 @@ class AppPage extends StatelessWidget { }, items: _tabBarList.map((item) { return BottomNavigationBarItem( - icon: Image.asset( - 'assets/icons/${item['icon']}', - width: 20, - height: 20, + icon: Icon( + item['icon'], + size: 24, ), - activeIcon: Image.asset( - 'assets/icons/${item['icon']}', + activeIcon: Icon( + item['active_icon'], + size: 24, color: AppColors.primary, - width: 20, - height: 20, ), label: item['label'], tooltip: '', diff --git a/lib/views/user/qr_code/index_page.dart b/lib/views/user/qr_code/index_page.dart new file mode 100644 index 0000000..5700022 --- /dev/null +++ b/lib/views/user/qr_code/index_page.dart @@ -0,0 +1,15 @@ +import 'package:flutter/material.dart'; + +class UserQrCodePage extends StatefulWidget { + const UserQrCodePage({Key? key}) : super(key: key); + + @override + _UserQrCodePageState createState() => _UserQrCodePageState(); +} + +class _UserQrCodePageState extends State { + @override + Widget build(BuildContext context) { + return Container(); + } +} diff --git a/pubspec.lock b/pubspec.lock index a804644..6c0acb8 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -122,6 +122,13 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "3.0.2" + csslib: + dependency: transitive + description: + name: csslib + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.17.2" cupertino_icons: dependency: "direct main" description: @@ -129,6 +136,13 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "1.0.5" + dart_date: + dependency: "direct main" + description: + name: dart_date + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.1.1" device_info_plus: dependency: "direct main" description: @@ -143,6 +157,20 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "4.0.6" + extended_image: + dependency: transitive + description: + name: extended_image + url: "https://pub.flutter-io.cn" + source: hosted + version: "6.1.0" + extended_image_library: + dependency: transitive + description: + name: extended_image_library + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.2.0" fake_async: dependency: transitive description: @@ -277,6 +305,13 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "0.2.0" + html: + dependency: transitive + description: + name: html + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.15.1" http: dependency: transitive description: @@ -284,6 +319,13 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "0.13.5" + http_client_helper: + dependency: transitive + description: + name: http_client_helper + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.0.3" http_parser: dependency: transitive description: @@ -319,6 +361,13 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "2.0.0" + intl: + dependency: transitive + description: + name: intl + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.17.0" js: dependency: transitive description: @@ -375,6 +424,13 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "1.7.0" + nested: + dependency: transitive + description: + name: nested + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.0.0" octo_image: dependency: transitive description: @@ -473,6 +529,48 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "1.11.1" + permission_handler: + dependency: "direct main" + description: + name: permission_handler + url: "https://pub.flutter-io.cn" + source: hosted + version: "10.2.0" + permission_handler_android: + dependency: transitive + description: + name: permission_handler_android + url: "https://pub.flutter-io.cn" + source: hosted + version: "10.2.0" + permission_handler_apple: + dependency: transitive + description: + name: permission_handler_apple + url: "https://pub.flutter-io.cn" + source: hosted + version: "9.0.7" + permission_handler_platform_interface: + dependency: transitive + description: + name: permission_handler_platform_interface + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.9.0" + permission_handler_windows: + dependency: transitive + description: + name: permission_handler_windows + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.1.2" + photo_manager: + dependency: transitive + description: + name: photo_manager + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.4.1" pinput: dependency: "direct main" description: @@ -515,6 +613,13 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "0.2.4" + provider: + dependency: transitive + description: + name: provider + url: "https://pub.flutter-io.cn" + source: hosted + version: "6.0.4" qr: dependency: transitive description: @@ -543,6 +648,13 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "1.6.0" + scroll_to_index: + dependency: "direct main" + description: + name: scroll_to_index + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.1.1" scrollable_positioned_list: dependency: transitive description: @@ -617,7 +729,14 @@ packages: name: tencent_im_sdk_plugin url: "https://pub.flutter-io.cn" source: hosted - version: "3.5.1" + version: "4.1.9" + tencent_im_sdk_plugin_platform_interface: + dependency: transitive + description: + name: tencent_im_sdk_plugin_platform_interface + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.3.5" term_glyph: dependency: transitive description: @@ -632,6 +751,13 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "0.4.8" + timeago: + dependency: transitive + description: + name: timeago + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.3.0" typed_data: dependency: transitive description: @@ -639,13 +765,6 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "1.3.0" - unorm_dart: - dependency: "direct main" - description: - name: unorm_dart - url: "https://pub.flutter-io.cn" - source: hosted - version: "0.2.0" uuid: dependency: transitive description: @@ -667,6 +786,48 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "1.7.6" + video_player: + dependency: "direct main" + description: + name: video_player + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.4.7" + video_player_android: + dependency: transitive + description: + name: video_player_android + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.3.9" + video_player_avfoundation: + dependency: transitive + description: + name: video_player_avfoundation + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.3.7" + video_player_platform_interface: + dependency: transitive + description: + name: video_player_platform_interface + url: "https://pub.flutter-io.cn" + source: hosted + version: "5.1.4" + video_player_web: + dependency: transitive + description: + name: video_player_web + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.0.12" + wechat_assets_picker: + dependency: "direct main" + description: + name: wechat_assets_picker + url: "https://pub.flutter-io.cn" + source: hosted + version: "7.2.0" win32: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index d2f2c51..602c518 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,7 +1,7 @@ name: chat description: A new Flutter project. publish_to: "none" # Remove this line if you wish to publish to pub.dev -version: 1.0.0+1 +version: 1.0.0 environment: sdk: ">=2.16.2 <3.0.0" dependencies: @@ -15,7 +15,6 @@ dependencies: azlistview: ^2.0.0 lpinyin: ^2.0.3 vibration: ^1.7.6 - tencent_im_sdk_plugin: ^3.5.1 scan: ^1.6.0 package_info_plus: ^0.0.1 device_info_plus: ^0.0.1 @@ -39,7 +38,12 @@ dependencies: ref: master fast_base58: ^0.2.1 hash: ^1.0.4 - unorm_dart: ^0.2.0 + tencent_im_sdk_plugin: ^4.1.9 + wechat_assets_picker: ^7.2.0 + video_player: ^2.4.7 + scroll_to_index: ^2.1.1 + dart_date: ^1.1.1 + permission_handler: ^10.2.0 dev_dependencies: flutter_test: diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index 1e90c87..9825e6b 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -6,9 +6,12 @@ #include "generated_plugin_registrant.h" +#include #include void RegisterPlugins(flutter::PluginRegistry* registry) { + PermissionHandlerWindowsPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("PermissionHandlerWindowsPlugin")); SmartAuthPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("SmartAuthPlugin")); } diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index c9c8861..eb5481e 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -3,6 +3,7 @@ # list(APPEND FLUTTER_PLUGIN_LIST + permission_handler_windows smart_auth )