diff --git a/CHANGELOG.md b/CHANGELOG.md index 8a6acde..86bae52 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,158 @@ +## 2.0.0 + +If you are upgrading from version 1.7.0, please refer to the changelog of all 2.0.0-preview versions, ranging from preview.1 to preview.7. + +The main feature of this new 2.0.0 version is Desktop Support. Tencent Cloud Chat UIKit now supports all platforms, including iOS, Android, Web, Windows, and macOS, which has resulted in significant changes to the codebase. The UI has been improved to adapt to screens of various widths, with different layouts for both wide and narrow screens. + +In addition, there are some significant changes compared to version 2.0.0-preview.7. + +### New Features + +* Added drag and drop support for multiple files in `TIMUIKitChat`, allowing direct sending. +* Introduced functionality to open files or their containing folder (using `Finder` on `macOS` or `Explorer` on `Windows`) for file messages via the message operation tooltip menu on desktop. +* Implemented text selection and copying in messages on desktop. +* Added group joining application processing on Desktop. +* Introduced `isAutoReportRead` to `TIMUIKitChatConfig` for controlling read status reporting. + +### Improvements + +* Enhanced group members selection panel for mentioning someone in a group chat. +* Refined image display ratio on Desktop. +* The Reply or Quote button is now labeled as `Reply` when `isAtWhenReply` is set to true, and `Quote` otherwise. +* @ member tags can now be deleted at once. + +### Bug Fixes + +* Fixed UI layout issue causing the `translate` button to display on two lines. +* Addressed an issue causing the mute status not to change when switching to another conversation. +* Fixed several issues causing bugs when opening files. +* Resolved an issue causing secondary confirmation modal UI layout to be over-width on Desktop. +* Fixed an issue causing UI layout errors on the profile page. +* Addressed an issue where the `chatMessageItemFromSelfBgColor` configuration did not work. +* Fixed an issue preventing files from being opened when the path contained Chinese characters on Windows. +* Resolved an issue preventing images from being pasted and sent directly with Ctrl + V on Windows. +* Fixed an issue causing errors in the muting members list. + +## 2.0.0-preview.7 + +### New Features + +* Added `additionalMessageToolTips` to `ToolTipsConfig`. This new property allows developers to add additional message operation tooltip items, apart from the default ones. The previous `additionalItemBuilder` has been replaced by this new property. With `additionalMessageToolTips`, developers only need to specify the data for the tooltip items, rather than providing a whole widget. This makes usage easier, as you no longer need to worry about the UI display. +* Added `isPreloadMessagesAfterInit` to `TIMUIKitConfig`, allows determines whether TUIKit should preload some messages after initialization for faster message display. + +### Improvements + +* Message operation menu shows when long-pressing messages will not show if nothing operation item includes and do not use message sticker reaction module. +* Renamed `desktopMessageHoverBar` to `additionalDesktopMessageHoverBarItem` in `TIMUIKitChatConfig` to control only the addition of extra operation items displayed on the hover bar of messages on desktop (macOS, Windows, and desktop version of Web), without affecting the default ones. Previously, it controlled the entire message hover bar, including covering the default items. +* Renamed `showWideScreenModalFunc` to `showDesktopModalFunc` in `TIMUIKitConfig` for better clarity. +* Upgraded several dependencies to their latest versions, including `ffi` to ^2.0.1, `file_picker` to ^5.2.9 and `device_info_plus` to ^8.2.0. +* Added support for the new permission authorization schema on Android 13 and `targetSdkVersion` greater than 33. +* Corrected the `textHight` to `textHeight` in `TIMUIKitChatConfig`, and modified the default value to 1.3. + +### Bug Fixes + +* Fixed an issue where the `showVideoCall` and `showVoiceCall` configuration options were not working. +* Fixed potential `Windows` platform deployment prohibition issue. +* Fixed an issue that may cause `setLocalCustomData` to be triggered repeatedly. + +## 2.0.0-preview.6 + +### Improvements + +* Permission requests now feature a gray translucent overlay for secondary confirmations on first-time requests, which was reintroduced after being removed in version 2.0.0-preview.4. Additionally, the overlay can now be successfully hidden once the permission authorization is complete.". +* Time Divider on Message List: The default 12-hour display has been changed to a 24-hour display. +* Message translation now targets the language of TUIKit instead of relying on the system language directly. The language of TUIKit can be set as the system language automatically or defined by the user. For more information, please refer to this documentation: https://www.tencentcloud.com/document/product/1047/52154. +* Optimized the animation for message text input area. + +### Bug Fixes + +* Fixed an issue where the `Voice Call` and `Video Call` buttons were not working in group chat. +* Fixed several null-safety issues. +* Fixed a layout problem for the message operation menu when not using the message sticker reaction module. +* Addressed a problem where the time ago display was not correct on the conversation item. +* Fixed an issue where stickers could not be clicked in some cases. +* Resolved an overflow error that occurred when opening the sticker panel. + +## 2.0.0-preview.5 + +### New Features + +* New Chat Configuration: `isAllowLongPressAvatarToAt`. This option controls whether users are allowed to mention another user in the group by long-pressing on their avatar. + +### Improvements + +* Improved tool bar configuration on desktop: The tool bar can now be customized using `desktopControlBarConfig` for embedded default items and `additionalDesktopControlBarItems` for additional tool items. These configurations come from TIMUIKitChatConfig. +* Renamed the `wideMessageHoverBar` configuration option to `desktopMessageHoverBar` for better clarity. +* Eliminated the dependency on `fluttertoast`. All necessary customer reminders are now triggered through the `onTUIKitCallbackListener` info callback in your project. For more information, please see: https://www.tencentcloud.com/document/product/1047/50054#how-do-i-get-an-api-call-error.2Fflutter-layer-error.2Fpop-up-prompt-message.3F.3Ca-id.3D.22callback.22.3E.3C.2Fa.3E. +* Eliminated other six unnecessary dependency packages to reduce the size and improve performance. +* Improved the clarity of the `sendMessage` function in `TIMUIKitChatController` by replacing the use of `convID` to represent both `userID` and `groupID` with separate parameters. + +### Bug Fixes + +* Fixed an issue where the message operation menu may show inaccurately when the message is too long. +* Fixed a problem where the message operation menu had the potential to be too wide for certain types of messages, causing display issues. +* Corrected an issue where the button to remove group members was not functioning correctly. +* Addressed a problem where the message item could exceed the pixel limit and appear too wide. +* Fixed a bug where certain JSON decoding operations could potentially fail. +* Fixed an issue with sound messages on iOS devices playing only through earpiece instead of speaker by default. + +## 2.0.0-preview.4 + +### New Features + +* New Chat Configuration: `TIMUIKitChatConfig` now includes `offlinePushInfo`, which allows for customization of the entire `offlinePushInfo` for each message. This field has a higher priority than the previous separate configuration fields for this object. +* New Color Configuration: Added `appbarTextColor` and `appbarBgColor` to configure the color for the Appbar. Also added `selectPanelBgColor` and `selectPanelTextIconColor` to configure the color of the messages multi-select panel. + +### Improvements + +* Improved Group Management: Muting members on Work Group is now not allowed. +* Improved Avatar: Ensured that the avatar can be as small as possible while still covering the entire target box. +* Permission Requests: Removed the gray translucent overlay for secondary confirmations on first-time permission requests. + +### Bug Fixes + +* Fixed an issue where the color defined by `chatBgColor` could not cover the entire chat screen when messages did not cover the whole page. +* Fixed an issue where the history message list could not be scrolled in some cases. +* Fixed an issue where the ratio of sending messages was incorrect, resulting in the wrong position of the read status label on the left. +* Fixed an issue where loading messages could fail when the number of messages equaled the specified count. + +## 2.0.0-preview.3 + +### New Features + +* Integrated Callkit: The Calls button no longer needs to be added to `MorePanelConfig`. If `tencent_calls_uikit` is installed, the Video Call and Voice Call buttons will be displayed automatically. +* Paste Images on Desktop: Users can now paste an image on the text field on Desktop to send it. +* Screenshot Capture on Desktop: Users can now capture a screenshot on Desktop and send it. + +### Improvements + +* Improved Compatibility: The TUIKit is now compatible with Flutter versions 3.0.0 to 3.7.7. + +### Bug Fixes + +* Fixed an issue where the `businessID` type may not be correct. +* Fixed an issue where the `chatMessageItemFromSelfBgColor` configuration was not taking effect. + +## 2.0.0-preview.2 + +### New Features + +* Added support for opening files locally from file messages. + +## 2.0.0-preview.1 + +### New Features + +* Desktop Support: Tencent Cloud Chat UIKit now supports all platforms, including iOS, Android, Web, Windows, and macOS, resulting in significant changes to the codebase. The UI has been enhanced to adapt to screens of various widths, with different layouts for both wide and narrow screens. +* Information Copy: The ability to copy information, such as Group ID, from the screen has been added. + +### Improvements + +* Improved group management logic, with non-administrators no longer able to access the management interface. +* Optimized cursor positioning when sending messages. +* Improved and optimized scrollbar functionality. +* Enhanced clickable URL support in messages, with URLs now supporting both with and without the "https://" prefix. + ## 1.7.0+1 * Fix: An issue that caused errors on mentioning all members. diff --git a/LICENSE b/LICENSE index 3e1a561..8482e60 100644 --- a/LICENSE +++ b/LICENSE @@ -1,201 +1,25 @@ -Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ +Copyright © 2013-2023 Tencent Cloud. All Rights Reserved. - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: - 1. Definitions. + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright [2023] [Tencent] - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. \ No newline at end of file +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file diff --git a/README.md b/README.md index 6708341..2a06226 100644 --- a/README.md +++ b/README.md @@ -63,123 +63,167 @@ More languages:
-![](https://qcloudimg.tencent-cloud.cn/raw/193ec650f17da6bb33edf5df5d978091.png) +![](https://qcloudimg.tencent-cloud.cn/image/document/8fd972397eba8f56c8f294c8b042794c.jpg)

TUIKit has Chat SDK, UI components and basic business logic inside. You can choose our pure Chat SDK tencent-cloud-chat-sdk if you tend to build the UI yourself.

- +
-## Experience DEMO +## Preview Version Release Notes -You can experience our Chat and Voice/Video Call modules via the following demos. +This version is a major update with version number 2.0.0-preview series, which is not backward +compatible. Tencent Cloud Chat UIKit has been extended from mobile-only (iOS/Android/mobile web) to +support all platforms, including iOS/Android/Web/Windows/macOS, resulting in significant changes to +the codebase. -**Those following versions of demo has been build by the same Flutter project with our SDKs and extensions.** +Therefore, users should evaluate the compatibility complexity of their business logic before +upgrading, while new users can use this version without any impact. -![](https://qcloudimg.tencent-cloud.cn/raw/89234f5032d4f6f8d89a8b439ca97ca2.png) +The documentation for the new version is still being improved, and users can refer to the sample app +source code at at https://github.com/TencentCloud/chat-demo-flutter. -**Also, taking a look of the screenshots of TUIKit from [here](https://www.tencentcloud.com/document/product/1047/50059?from=pub) are suggested.** +## Check Out Our Sample Apps + +Experience our Chat and Voice/Video Call modules by trying out our sample apps. + +**These apps have been created using the same Flutter project as our SDKs and extensions.** + +| Platform | Link | Remark | +|---------|---------|---------| +| Android / iOS | Tencent Chat Logo | Scan to download app for both Android and iOS. Automatically identifies platform. | +| Web | Tencent Chat Logo | Supports both desktop and mobile browsers and automatically adjusts its layout accordingly. Same website as link below. | +| Web | [Visit Now](https://comm.qq.com/flutter/#/) | Supports both desktop and mobile browsers and automatically adjusts its layout accordingly. Same website as previous QR code. | +| macOS | [Download Now](https://comm.qq.com/im_demo_download/macos_flutter.dmg) | The macOS version of our sample app. Control-click the app icon, then choose "Open" from the shortcut menu. | +| Windows | [Download Now](https://comm.qq.com/im_demo_download/windows_flutter.appx) | The Windows version of our sample app, which is a UWP (Universal Windows Platform) application. | +| Linux | Coming Soon... | Will be available later this year. | + +**Take a look at the screenshots of +TUIKit [here](https://www.tencentcloud.com/document/product/1047/50059?from=pub) to get an idea of +what to expect.** ## Introduction to TUIKit -[TUIKit](https://www.tencentcloud.com/document/product/1047/50059?from=pub) is a set of official UI components for Tencent Cloud Chat SDK, with chat business logic around it. It provides components such as the conversation, chat, relationship chain, and group. +Tencent Cloud Chat SDK comes +with [TUIKit](https://www.tencentcloud.com/document/product/1047/50059?from=pub), which is an +official set of UI components that have chat business logic built-in. TUIKit includes components +like conversation, chat, relationship chain, and group. -You can use these UI components to build your APP with the In-APP chat module quickly and easily. +Developers can use these UI components to quickly and easily add In-APP chat modules to their mobile +applications. ![img](https://qcloudimg.tencent-cloud.cn/raw/f140dd76be01a65abfb7e6ba2bf50ed5.png) -Currently, Flutter [TUIKit](https://www.tencentcloud.com/document/product/1047/50059?from=pub) contains the following main components: +Currently, Flutter [TUIKit](https://www.tencentcloud.com/document/product/1047/50059?from=pub) +contains the following main components: - [TIMUIKitCore](https://comm.qq.com/im/doc/flutter/en/TUIKit/TIMUIKitCore/readme.html): Core entry -- [TIMUIKitConversation](https://www.tencentcloud.com/document/product/1047/50059?from=pub#timuikitconversation): Conversation list -- [TIMUIKitChat](https://www.tencentcloud.com/document/product/1047/50059?from=pub#timuikitchat): Chat module, includes historical message list and message sending area, with some other features like message reaction and URL preview, etc. -- [TIMUIKitContact](https://www.tencentcloud.com/document/product/1047/50059?from=pub#relationship-chain-components): Contacts list -- [TIMUIKitProfile](https://www.tencentcloud.com/document/product/1047/50059?from=pub#timuikitprofile): User profile and relationship management -- [TIMUIKitGroupProfile](https://www.tencentcloud.com/document/product/1047/50059?from=pub#timuikitgroupprofile): Group profile and management -- [TIMUIKitGroup](https://www.tencentcloud.com/document/product/1047/50059?from=pub#relationship-chain-components): The list of group self joined -- [TIMUIKitBlackList](https://www.tencentcloud.com/document/product/1047/50059?from=pub#relationship-chain-components): The list of user been blocked -- [TIMUIKitNewContact](https://www.tencentcloud.com/document/product/1047/50059?from=pub#relationship-chain-components): New contacts application list -- [TIMUIKitSearch](https://www.tencentcloud.com/document/product/1047/50036?from=pub): Search globally -- [TIMUIKitSearchMsgDetail](https://www.tencentcloud.com/document/product/1047/50036?from=pub): Search in specific conversation +- [TIMUIKitConversation](https://www.tencentcloud.com/document/product/1047/50059?from=pub#timuikitconversation): + Conversation list +- [TIMUIKitChat](https://www.tencentcloud.com/document/product/1047/50059?from=pub#timuikitchat): + Chat module, includes historical message list and message sending area, with some other features + like message reaction and URL preview, etc. +- [TIMUIKitContact](https://www.tencentcloud.com/document/product/1047/50059?from=pub#relationship-chain-components): + Contacts list +- [TIMUIKitProfile](https://www.tencentcloud.com/document/product/1047/50059?from=pub#timuikitprofile): + User profile and relationship management +- [TIMUIKitGroupProfile](https://www.tencentcloud.com/document/product/1047/50059?from=pub#timuikitgroupprofile): + Group profile and management +- [TIMUIKitGroup](https://www.tencentcloud.com/document/product/1047/50059?from=pub#relationship-chain-components): + The list of group self joined +- [TIMUIKitBlackList](https://www.tencentcloud.com/document/product/1047/50059?from=pub#relationship-chain-components): + The list of user been blocked +- [TIMUIKitNewContact](https://www.tencentcloud.com/document/product/1047/50059?from=pub#relationship-chain-components): + New contacts application list +- [TIMUIKitSearch](https://www.tencentcloud.com/document/product/1047/50036?from=pub): Search + globally +- [TIMUIKitSearchMsgDetail](https://www.tencentcloud.com/document/product/1047/50036?from=pub): + Search in specific conversation -Also, there are some other useful components and widgets, that can help to build your APP, and meet your business needs, including group entry application list and group member list, etc. +In addition to these components, there are other useful components and widgets available to help +developers meet their business needs, such as group entry application list and group member list. -For the source code of the project in the figure above, see [im-flutter-uikit](https://github.com/TencentCloud/tc-chat-demo-flutter). The project is open source and can be used directly. +For the source code of the project shown in the image above, please refer +to [chat-demo-flutter](https://github.com/TencentCloud/chat-demo-flutter). This project is open +source and can be directly used by developers. -## Supported Platforms +## Compatible Platforms + +The platforms are compatible with the deployment of our Chat UIKit. - Android - iOS -- Web(After version of 0.1.4) +- Web (version 0.1.4 and later) +- Windows (version 2.0.0 and later) +- macOS (version 2.0.0 and later) ## Get Started -**[Please refer this documents](https://www.tencentcloud.com/document/product/1047/45907?from=pub), for a completed and detailed get started guide.** +Please refer to [this document](https://www.tencentcloud.com/document/product/1047/45907?from=pub) for a complete and detailed guide on getting started. ## Directions -The following guide describes how to use Flutter TUIKit to build a simple Chat APP quickly. +The following guide describes how to quickly build a simple chat application using Flutter TUIKit. -**You may refer to the appendix, if willing to know about the detail and parameter for each widgets.** +Refer to the appendix if you want to learn about the details and parameters of each widget. -> If you tend to add this Flutter [TUIKit](https://www.tencentcloud.com/document/product/1047/50059?from=pub) to your existing application directly, you may refer to [this documentation](https://www.tencentcloud.com/document/product/1047/51456). Add the Flutter module to your existing app, coding once and deploying to all platforms. It could reduce your workload, to adding chat and call modules to your existing, to a large extent. +> If you want to directly add Flutter TUIKit to your existing application, refer to [this document](https://www.tencentcloud.com/document/product/1047/51456). You can add the Flutter module to your existing application, code once, and deploy to all platforms. This can significantly reduce the workload of adding chat and call modules to your existing application. -### Step 0. Create two accounts for testing +### Step 0: Create two accounts for testing -[Signed up](https://www.tencentcloud.com/document/product/378/17985?from=pub) and [log in](https://www.tencentcloud.com/document/product/378/36004?from=pub) to the [Tencent IM console](https://console.tencentcloud.com/im?from=pub). +Sign up and log in to the [Tencent Cloud Chat console](https://console.tencentcloud.com/im?from=pub). -[Create an application](https://www.tencentcloud.com/document/product/1047/34577?from=pub) and enter in. +Create an application and enter it. -Select [Auxiliary Tools](https://console.tencentcloud.com/im-detail/tool-usersig?from=pub) > UserSig Generation and Verification on the left sidebar. Generate two pairs of "UserID" and the corresponding "UserSig", and copy the "key" information. [Refer to here.](https://www.tencentcloud.com/document/product/1047/34580?from=pub#usersig-generation-and-verification) +Select Auxiliary Tools > UserSig Generation and Verification on the left sidebar. Generate two pairs of "UserID" and the corresponding "UserSig," and copy the "key" information. Refer to [this document](https://www.tencentcloud.com/document/product/1047/34580?from=pub#usersig-generation-and-verification). -Tips: You may create "user1" and "user2" here. +Tips: You can create "user1" and "user2" here. -> Note: +> Note: > -> The correct `UserSig` distribution method is to integrate the calculation code of `UserSig` into your server and provide an application-oriented API. When `UserSig` is needed, your application can send a request to the business server for a dynamic `UserSig`. For more information, see [How do I calculate UserSig on the server?](https://www.tencentcloud.com/document/product/1047/34385?from=pub). +> The correct way to distribute `UserSig` is to integrate the calculation code for `UserSig` into your server and provide an application-oriented API. When `UserSig` is needed, your application can send a request to the business server for a dynamic `UserSig.` For more information, see [How do I calculate UserSig on the server?](https://www.tencentcloud.com/document/product/1047/34385?from=pub). -### Step 1. Create a Flutter app and add permission configuration +### Step 1: Create a Flutter app and add permission configuration -Quickly create a Flutter APP by referring to [Flutter documentation](https://docs.flutter.dev/get-started/install). +Create a Flutter app quickly by following the [Flutter documentation](https://docs.flutter.dev/get-started/install). -TUIKit needs the permissions of shooting/album/recording/network for basic messaging function. You need to declare in the permission manually to use the relevant capabilities normally. +TUIKit needs the permissions of shooting/album/recording/network for basic messaging functions. You need to declare these permissions manually to use the relevant capabilities normally. #### Android -Open `android/app/src/main/AndroidManifest.xml`, add the following lines between `` and ``. +Open `android/app/src/main/AndroidManifest.xml` and add the following lines between `` and ``. ```xml - - - - - - - - - + + + + + + + + + + + ``` #### iOS -Open `ios/Podfile`, add the following lines to the end of the file. +Open `ios/Podfile` and add the following lines to the end of the file. ```pod post_install do |installer| installer.pods_project.targets.each do |target| flutter_additional_ios_build_settings(target) + target.build_configurations.each do |config| + config.build_settings['EXCLUDED_ARCHS[sdk=iphonesimulator*]'] = 'arm64' + config.build_settings['ENABLE_BITCODE'] = 'NO' + config.build_settings["ONLY_ACTIVE_ARCH"] = "NO" + end target.build_configurations.each do |config| config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] ||= [ '$(inherited)', @@ -192,7 +236,7 @@ post_install do |installer| end ``` -### Step 2. Install dependencies +### Step 2: Install dependencies Add `tencent_cloud_chat_uikit` under `dependencies` in the `pubspec.yaml` file, or run the following command: @@ -200,15 +244,15 @@ Add `tencent_cloud_chat_uikit` under `dependencies` in the `pubspec.yaml` file, flutter pub add tencent_cloud_chat_uikit ``` -It supports Android and iOS as default, if you are also willing to use it on the Web, please refer to the following guide. +It supports Android and iOS by default. If you also want to use it on the web, refer to the following guide. -#### Web Supports +#### Web Support -Version of 0.1.4 or later are required for web supports. +Version 0.1.4 or later is required to support web. -> If your existing Flutter project does not support Web, run `flutter create .` in the project root directory to add web support. +If your existing Flutter project does not support web, run `flutter create .` in the project root directory to add web support. -Install JS dependencies to `web/` by `npm` or `yarn`. +Install JavaScript dependencies to `web/` using `npm` or `yarn`. ```shell cd web @@ -220,7 +264,7 @@ npm i tim-js-sdk npm i tim-upload-plugin ``` -Open `web/index.html` , add the following two lines between `` and `` to import them. +Open `web/index.html` and add the following two lines between `` and `` to import them. ```html @@ -229,35 +273,35 @@ Open `web/index.html` , add the following two lines between `` and ` **You may also better to register a callback function for `onTUIKitCallbackListener` here, please refer to the appendix.** +> **You may also want to register a callback function for `onTUIKitCallbackListener` here. Refer to the appendix.** -### Step 4. Get the signature and log in +### Step 4: Get the signature and log in -Now, you can log in one of the testing accounts, generated on Step 0, to start the IM module. +You can now log in one of the testing accounts generated in Step 0 to start the Tencent Cloud Chat module. -Log in by `_coreInstance.login` . +Log in using `_coreInstance.login`. ```dart /// main.dart @@ -267,16 +311,16 @@ final CoreServicesImpl _coreInstance = TIMUIKitCore.getInstance(); _coreInstance.login(userID: userID, userSig: userSig); ``` -Caveat: Importing UserSig to your application is ONLY for Debugging purposes and cannot be applied for the Release version. Before publishing your app, you should generate your UserSig from your server. Refers to: +Note: Importing UserSig to your application is only for debugging purposes and cannot be used for the release version. Before publishing your app, you should generate your UserSig from your server. Refer to: -### Step 5. Implementing the conversation list page +## Step 5. Implementing the conversation list page -You can take the conversation (channel) list page as the homepage of your Chat module, covering the conversation with all users and groups that have chat records. +You can use the conversation (channel) list page as the homepage of your Chat module, which includes all conversations with users and groups that have chat records. - +```markdown +![Conversation List Page](https://qcloudimg.tencent-cloud.cn/raw/a27b131d555b1158d150bd9b337c1d9d.png) -You can create a `Conversation` class, with `TIMUIKitConversation` on its `body`, to render the conversation list. -The only parameter you need to provide at least is `onTapItem` callback, aimed at navigating to the Chat page for each conversation. The `Chat` class will be introduced in the next step. +You can create a `Conversation` class, with `TIMUIKitConversation` as its body, to render the conversation list. You only need to provide the `onTapItem` callback, which allows users to navigate to the Chat page for each conversation. In the next step, we'll introduce the `Chat` class. ```dart import 'package:flutter/material.dart'; @@ -284,6 +328,7 @@ import 'package:tencent_cloud_chat_uikit/tencent_cloud_chat_uikit.dart'; class Conversation extends StatelessWidget { const Conversation({Key? key}) : super(key: key); + @override Widget build(BuildContext context) { return Scaffold( @@ -298,9 +343,10 @@ class Conversation extends StatelessWidget { Navigator.push( context, MaterialPageRoute( - builder: (context) => Chat( - selectedConversation: selectedConv, - ), + builder: (context) => + Chat( + selectedConversation: selectedConv, + ), )); }, ), @@ -308,15 +354,14 @@ class Conversation extends StatelessWidget { } } ``` +## Step 6. Implementing the chat page -### Step 6. Implementing the chat page +The chat page consists of the main message list and a message sending bar at the bottom. -The chat page is composed of the main historical message list and a message sending bar at the bottom. +```markdown +![Chat Page](https://qcloudimg.tencent-cloud.cn/raw/09b8b9b54fd0caa47069544343eba461.jpg) -![](https://qcloudimg.tencent-cloud.cn/raw/09b8b9b54fd0caa47069544343eba461.jpg) - -You can create a `Chat` class, with `TIMUIKitChat` on its `body`, to render the chat page. -It is recommended to provide a `onTapAvatar` callback function, for navigating to the profile page for current contact, which will be introduced in the next step. +You can create a `Chat` class, with `TIMUIKitChat` as its body, to render the chat page. We recommend providing an `onTapAvatar` callback function to navigate to the profile page for the current contact, which we'll introduce in the next step. ```dart import 'package:flutter/material.dart'; @@ -324,42 +369,43 @@ import 'package:tencent_cloud_chat_uikit/tencent_cloud_chat_uikit.dart'; class Chat extends StatelessWidget { final V2TimConversation selectedConversation; + const Chat({Key? key, required this.selectedConversation}) : super(key: key); + String? _getConvID() { return selectedConversation.type == 1 ? selectedConversation.userID : selectedConversation.groupID; } + @override Widget build(BuildContext context) { return TIMUIKitChat( - conversationID: _getConvID() ?? '', // groupID or UserID - conversationType: selectedConversation.type ?? 1, // Conversation type - conversationShowName: selectedConversation.showName ?? "", // Conversation display name + conversationID: _getConvID() ?? '', + conversationType: selectedConversation.type ?? 1, + conversationShowName: selectedConversation.showName ?? "", onTapAvatar: (_) { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => UserProfile(userID: userID), + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => UserProfile(userID: userID), )); - }, // Callback for the clicking of the message sender profile photo. This callback can be used with `TIMUIKitProfile`. + }, ); -} + } ``` +## Step 7. Implementing the user profile page -### Step 7. Implementing the user profile page +This page shows the profile of a specific user and maintains the relationship between the current logged-in user and the other user. -This page can show the profile of a specific user and maintain the relationship between the current login user and it. +```markdown +![User Profile Page](https://qcloudimg.tencent-cloud.cn/raw/03e88da6f1d63f688d2a8ee446da43ff.png) -![](https://qcloudimg.tencent-cloud.cn/raw/03e88da6f1d63f688d2a8ee446da43ff.png) +You can create a `UserProfile` class, with `TIMUIKitProfile` as its body, to render the user profile page. -Here, you can create a `UserProfile` class, with `TIMUIKitProfile` on its `body`, to render the profile page. +The only parameter you have to provide is `userID`, while this component automatically generates the profile and relationship maintenance page based on the existence of friendship. -The only parameter you have to provide at least is 'userID', while this component can generate the profile and relationship maintenance page based on the existence of friendship automatically. - -> TIPS -> -> Please give priority to use `profileWidgetBuilder`, to customize some profile widgets, with `profileWidgetsOrder`, determine the vertical sequence, if you tend to customize this page. If this method could not meet your business needs, you may consider using `builder` instead. +> **TIP**: Please use `profileWidgetBuilder` first to customize some profile widgets and determine their vertical sequence using `profileWidgetsOrder` if you want to customize this page. If this method cannot meet your business needs, you may consider using `builder` instead. ```dart import 'package:flutter/material.dart'; @@ -367,34 +413,35 @@ import 'package:tencent_cloud_chat_uikit/tencent_cloud_chat_uikit.dart'; class UserProfile extends StatelessWidget { final String userID; + const UserProfile({required this.userID, Key? key}) : super(key: key); @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( - title: const Text( - "Message", - style: TextStyle(color: Colors.black), - ), - ), - body: TIMUIKitProfile( - userID: widget.userID, - ), + title: const Text( + "Message", + style: TextStyle(color: Colors.black), + ), + ), + body: TIMUIKitProfile( + userID: widget.userID, + ), ); } } ``` -Now, your app can send/receive messages, show the conversation list, and deal with the contact friendship. - -You can use others components from TUIKit continually to implement the complete Chat module quickly and easily. +Now your app can send and receive messages, display the conversation list, and manage contact friendships. You can use other components from TUIKit to quickly and easily implement the complete Chat module. ## FAQs ### Do I need to integrate Chat SDK after integrating TUIKit? -No. You don't need to integrate Chat SDK again. If you want to use Chat SDK related APIs, you can get them via `TIMUIKitCore.getSDKInstance()`. This method is recommended to ensure Chat SDK version consistency. +No. You don't need to integrate Chat SDK again. If you want to use Chat SDK related APIs, you can +get them via `TIMUIKitCore.getSDKInstance()`. This method is recommended to ensure Chat SDK version +consistency. ### Why did force quit occur when I sent voice, image, file or other messages? @@ -404,11 +451,13 @@ Refers to Step 1 above. ### What should I do if clicking Build And Run for an Android device triggers an error, stating no available device is found? -Check that the device is not occupied by other resources. Alternatively, click Build to generate an APK package, drag it to the simulator, and run it. +Check that the device is not occupied by other resources. Alternatively, click Build to generate an +APK package, drag it to the simulator, and run it. ### What should I do if an error occurs during the first run for an iOS device? -If an error occurs after the configuration, click **Product > Clean Build Folder** , clean the product, and run `pod install` or `flutter run` again. +If an error occurs after the configuration, click **Product > Clean Build Folder** , clean the +product, and run `pod install` or `flutter run` again. ![](https://qcloudimg.tencent-cloud.cn/raw/d495b2e8be86dac4b430e8f46a15cef4.png) @@ -416,33 +465,33 @@ If an error occurs after the configuration, click **Product > Clean Build Folder ![](https://qcloudimg.tencent-cloud.cn/raw/1ffcfe39a18329c86849d7d3b34b9a0e.png) -Turn on Airplane Mode on your Apple Watch, and go to **Settings > Bluetooth** on your iPhone to turn off Bluetooth. +Turn on Airplane Mode on your Apple Watch, and go to **Settings > Bluetooth** on your iPhone to turn +off Bluetooth. Restart Xcode (if opened) and run `flutter run` again. -### Issue with Flutter environment? - -If you want to check the Flutter environment, run `flutter doctor` to detect whether the Flutter environment is ready. - ### What should I do when an error occurs on an Android device after TUIKit is imported into the application automatically generated by Flutter? ![](https://qcloudimg.tencent-cloud.cn/raw/d95efdd4ae50f13f38f4c383ca755ae7.png) -1. Open `android\app\src\main\AndroidManifest.xml` and complete `xmlns:tools="http://schemas.android.com/tools" / android:label="@string/android_label" / tools:replace="android:label"` as follows. +1. Open `android\app\src\main\AndroidManifest.xml` and + complete `xmlns:tools="http://schemas.android.com/tools" / android:label="@string/android_label" / tools:replace="android:label"` + as follows. ```xml + - + ``` -2. Open `android\app\build.gradle` and complete `minSdkVersion` and `targetSdkVersion` in `defaultConfig`. +2. Open `android\app\build.gradle` and complete `minSdkVersion` and `targetSdkVersion` + in `defaultConfig`. ```gradle defaultConfig { @@ -456,422 +505,8 @@ defaultConfig { ## Contact Us -Please do not hesitate to contact us in the following place, if you have any further questions or tend to learn more about the use cases. - -- Telegram Group: -- WhatsApp Group: -- QQ Group: 788910197, chat in Chinese - -Our Website: - ---- - -## Appendix: Overview for each widgets - -### TIMUIKitCore - -`TIMUIKitCore` provides two static methods, including `getInstance` and `getSDKInstance`。 - -- `getInstance`: Used for get the instance of `CoreServicesImpl`. -- `getSDKInstance`: Used for get the instance of Chat SDK. - -`CoreServicesImpl` is the main class of `TUIKit` , providing the methods includes initialization, logging in and out, getting user information, etc. - -```dart -import 'package:tencent_cloud_chat_uikit/tencent_cloud_chat_uikit.dart'; - -final CoreServicesImpl _coreInstance = TIMUIKitCore.getInstance(); -final V2TIMManager _sdkInstance = TIMUIKitCore.getSDKInstance(); - -// init -_coreInstance.init( - language: LanguageEnum?, // Specify the displaying language among English / Chinese, Traditional / Chinese, Simplified. If this field is not provided, the default is the system language - onTUIKitCallbackListener: ValueChanged, // The callback listener for information from TUIKit, includes errors from SDK API/ the info needs to reminds users/ errors from Flutter. You can reminds users up to your business needs, the description below for details. - sdkAppID: 0, // sdkAppID from Tencent IM console - loglevel: LogLevelEnum.V2TIM_LOG_DEBUG, - listener: V2TimSDKListener()); -// unInit -_coreInstance.unInit(); - -// login -_coreInstance.login( - userID: 0, // user ID - userSig: "" // [How do I calculate UserSig on the server?](https://www.tencentcloud.com/document/product/1047/34385?from=pub) -) - -// logout -_coreInstance.logout(); - -// getUsersInfo -_coreInstance.getUsersInfo(userIDList: ["123", "456"]); - -// setOfflinePushConfig -_coreInstance.setOfflinePushConfig( - businessID: businessID, // The business from Tencent IM console, for each platform of devices - token: token, // The token from manufactors when registering the offline push -) - -// setSelfInfo -_coreInstance.setSelfInfo(userFullInfo: userFullInfo) // set self userinfo - -// setTheme -_coreInstance.setTheme(TUITheme theme: theme) // set theme color -/* - TUITheme( - // Primary color - final Color? primaryColor; - - // Secondary color - final Color? secondaryColor; - - // Info color, for secondary operations or prompts - final Color? infoColor; - - // Weak background color, lighter than the main background color, used to fill gaps or shadows - final Color? weakBackgroundColor; - - // Weak divider line color, for dividing lines or borders - final Color? weakDividerColor; - - // Weak text color - final Color? weakTextColor; - - // Dark text color - final Color? darkTextColor; - - // Used for AppBar or Panels - final Color? lightPrimaryColor; - - // Text color - final Color? textColor; - - // Warning color for dangerous operation - final Color? cautionColor; - - // Group owner identification color - final Color? ownerColor; - - // Group administrator identification color - final Color? adminColor;) -*/ -``` - -#### `onTUIKitCallbackListener` - -This listener is used to get information including: errors form SDK API / errors form Flutter / some remind information that may need to pop up to prompt users. - -Determine the type by `TIMCallbackType`. - -> You may refer to our [DEMO](https://github.com/TencentCloud/tc-chat-demo-flutter/lib/src/pages/app.dart) for the codes in this part, and modifying up to your business needs. - -##### Errors form SDK API(`TIMCallbackType.API_ERROR`) - -In this scenario, SDK API original `errorMsg` and `errorCode` are provided. - -[Error codes listed here](https://www.tencentcloud.com/document/product/1047/34348?from=pub) - -#### Errors form Flutter(`TIMCallbackType.FLUTTER_ERROR`) - -This error is captured by listening Flutter natively throwing an exception, providing `stackTrace` (from `FlutterError.onError`) or `catchError` (from try-catch) when the error occurs. - -#### Remind information(`TIMCallbackType.INFO`) - -It is suggest to pup up to prompt users for this kind of messages. - -Provide the `infoCode` info code to help you determine the current scene, and provide the default prompt recommendation `infoRecommendText`. - -You can directly pop up our recommendations, or you can customize the recommendations according to the scene code. The language of recommendation text is adaptive according to the system language or the language you specified, do not judge the scene according to the recommendation language. - -The rules for info code are as follows: - -The info code consists of seven digits, the first five digits determine the components of the scene, and the last two digits determine the specific performance of the scene. - -| The first five digits | Corresponding widget | -| ---------- | ---------------------- | -| 66601 | `TIMUIKitAddFriend` | -| 66602 | `TIMUIKitAddGroup` | -| 66603 | `TIMUIKitBlackList` | -| 66604 | `TIMUIKitChat` | -| 66605 | `TIMUIKitContact` | -| 66606 | `TIMUIKitConversation` | -| 66607 | `TIMUIKitGroup` | -| 66608 | `TIMUIKitGroupProfile` | -| 66609 | `TIMUIKitNewContact` | -| 66610 | `TIMUIKitGroupProfile` | -| 66611 | `TIMUIKitNewContact` | -| 66612 | `TIMUIKitProfile` | -| 66613 | `TIMUIKitSearch` | -| 66614 | General Widget | - -All info codes are listed below: - -| `infoCode` | Recommendation prompt `infoRecommendText` | Scene description | -| ----------------- | ------------------------------------------------------------ | ------------------------------------------------------------ | -| 6660101 | Contact request sent | User requests to add another user as a contact. | -| 6660102 | This user is your contact. | When a user applies to add another user who is already a contact, the callback of `onTapAlreadyFriendsItem` is triggered. | -| 6660201 | Group request sent | Users apply to join a group chat that requires the approval of the administrator.| -| 6660202 | You are already in this group | When a user applies to join a group, it is determined that the user is already a member of the current group, triggering the callback of `onTapExistGroup`. | -| 6660401 | Failed to locate the original message | When the user needs to jump to the @ message or reference the message, the target message is not found in the message list. | -| 6660402 | Video saved successfully | After clicking on the video message in the message list, and chooses to save the video. | -| 6660403 | Failed to save the video | After clicking on the video message in the message list, and chooses to save the video. | -| 6660404 | Message too short | The user sent an overly short voice message. | -| 6660405 | Sending failed. The video cannot exceed 100 MB. | The user attempted to send a video larger than 100MB. | -| 6660406 | Image saved successfully | After clicking on the image in the message list, the user chooses to save the picture. | -| 6660407 | Failed to save the image | After clicking on the image in the message list, the user chooses to save the picture. | -| 6660408 | Copied | The user chooses to copy the text message in the pop-up window. | -| 6660409 | Not implemented | The user selects a non-implemented function in the pop-up. window | -| 6660410 | You are receiving other files | When the user clicks the download file message, the previous download task has not yet been completed. | -| 6660411 | Receiving | User clicks to download file message. | -| 6660412 | Video is available with .mp4 only | The user sent a video message in non-mp4 format | -| 6660413 | Added to download queue and waiting | Added to the queue to be downloaded, while other files are downloading | -| 6661001 | Modification failed due to network disconnection | When users try to modify group data in a non-network environment. | -| 6661002 | Failed to view the group members due to network disconnection | When users try to modify group data in a non-network environment. | -| 6661003 | Admin role canceled successfully | The user removes the other users from the administrator in the group. | -| 6661201 | Modification failed due to network disconnection | When a user tries to modify his or her contact information without a network environment. | -| 6661202 | Added successfully | Add other users as contact on the profile page and automatically add them successfully without verification. | -| 6661203 | Request sent successfully | Add other users as contact on the profile page, and the other user's settings need to be verified. | -| 6661204 | The user is blocked | Add other users as contacts on the profile page, who are on their own blocklist. | -| 6661205 | Added failed | Add other users as contact on the profile page, but failed, probably because the other party is forbidden to add contact. | -| 6661206 | Deleted successfully | Delete other users as contact on the profile page and succeed. | -| 6661207 | Deleted failed | Delete other users as contact on the profile page. Failed. | -| 6661401 | The input cannot be empty | When the user is entering information, an empty string is entered. | -| 6661402 | Please provide a life cycle hook navigating back to home or other pages. | When users quit the group or dissolve the group, they did not provide a way to return to the home page. | -| 6661403 | Insufficient disk storage space, it is recommended to clean up to obtain a better experience | After the login is successful, the device storage space will be automatically detected. If there is less than 1GB, it will be prompted. | - -### TIMUIKitConversation - -`TIMUIKitConversation` shows the conversation list. - -The corresponding controller: `TIMUIKitConversationController` is also provided. - -```dart -import 'package:tencent_cloud_chat_uikit/tencent_cloud_chat_uikit.dart'; - -final TIMUIKitConversationController _controller = - TIMUIKitConversationController(); - -void _handleOnConvItemTaped(V2TimConversation? selectedConv) { - // You can jump to the chat interface here. -} - -List _itemSlidableBuilder( - V2TimConversation conversationItem) { - return [ - ConversationItemSlidablePanel( - onPressed: (context) { - _clearHistory(conversationItem); - }, - backgroundColor: hexToColor("006EFF"), - foregroundColor: Colors.white, - label: 'Clear conversaation', - autoClose: true, - ), - ConversationItemSlidablePanel( - onPressed: (context) { - _pinConversation(conversationItem); - }, - backgroundColor: hexToColor("FF9C19"), - foregroundColor: Colors.white, - label: conversationItem.isPinned! ? 'unpined' : 'pinned', - ) - ]; - } - -TIMUIKitConversation( - lifeCycle: ConversationLifeCycle(), // The lifecycle hook - onTapItem: _handleOnConvItemTaped, // Callback of clicking conversation, can navigating to chat page - itemSlidableBuilder: _itemSlidableBuilder, // Operation items for conversation Item sliding to the left, conversation topping, etc. - controller: _controller, // Conversation component controller, through which you can get conversation data, set conversation data, pin conversation to top and other operations - itembuilder: (conversationItem) {} // Used to customize the conversation item. Can be combined with TIMUIKitConversationController to implement business logic. - conversationCollector: (conversation) {} // Conversation collector, which can customize whether the conversation is displayed - lastMessageBuilder: (V2TimMessage, List) {} // Customize the second line of the conversation item, which is generally used to show the last message -) -``` - ---- - -### TIMUIKitChat - -`TIMUIKitChat` is the main chat component that provides the display of message list and the ability to send messages. - -It also supports custom display of various message types. - -Additionally, it can be combined with `TIMUIKitChatController` to realize local storage and pre-rendering of messages, etc. - -Currently supported message parsing: - -- Text message. -- Image message. -- Video message. -- Voice message. -- Group message. -- Merge message. -- File message. - -```dart -import 'package:tencent_cloud_chat_uikit/tencent_cloud_chat_uikit.dart'; - -TIMUIKitChat( - lifeCycle: ChatLifeCycle(), // Lifecycle hook for TIMUIKitChat - conversationID: "", // User ID or Group ID - conversationType: ConversationType, // 1 is c2c chat, 2 is group chat - conversationShowName: "", - appBarActions: [], // appBar operation item, which can be used to jump to the page of group details and personal details, etc. - onTapAvatar: _onTapAvatar, // callback function for clicking the avatar, which can be used to jump to the user profile. - messageItemBuilder: (MessageItemBuilder) { - // Message item layout constructor, you can choose to customize part of the message type or message row layout. - }, - extraTipsActionItemBuilder: (message) { - // The configuration for the menu, opend by long pressed messages - }, - morePanelConfig: MorePanelConfig(), // The config for more panel area - appBarConfig: AppBar(), // The config for AppBar - mainHistoryListConfig: ListView(), // Additional config for ListView of historical message list - textFieldHintText: "", // The hint of inputTextField - draftText: "", // The draft of inputting message - initFindingMsg: 0, // The message been jumped - config: TIMUIKitChatConfig(), // The config for the whole TIMUIKitChat - onDealWithGroupApplication: (String groupID){ - // Navigating to the pages for the page of [TIMUIKitGroupApplicationList] or other pages to handle joining group application for specific group - } -) -``` - ---- - -### TIMUIKitProfile - -`TIMUIKitProfile` shows the detail information for a user, and manage the relationship. - -```dart -TIMUIKitProfile( - userID: "", - controller: TIMUIKitProfileController(), // Profile Controller - profileWidgetBuilder: ProfileWidgetBuilder(), // Customized some kinds of item. - profileWidgetsOrder: List, // Determine the vertical sequence for those profile widgets. - builder: ( - BuildContext context, - V2TimFriendInfo friendInfo, - V2TimConversation conversation, - int friendType, - bool isMute) { - // Customized the whole page. `profileWidgetBuilder` and `profileWidgetsOrder` will no longer works if you define this. - }, - lifeCycle: ProfileLifeCycle(),// Lifecycle hook for TIMUIKitProfile -) -``` - ---- - -### TIMUIKitGroupProfile - -`TIMUIKitGroupProfile` shows the details of a group and can manage this group. - -```dart -TIMUIKitGroupProfile( - groupID: "", - profileWidgetBuilder: GroupProfileWidgetBuilder(), // Customized some kinds of item. - profileWidgetsOrder: List, // Determine the vertical sequence for those profile widgets. - builder: (BuildContext context, V2TimGroupInfo groupInfo, List groupMemberList){ - // Customized the whole page. `profileWidgetBuilder` and `profileWidgetsOrder` will no longer works if you define this. - }, - lifeCycle: GroupProfileLifeCycle, // Lifecycle hook for TIMUIKitGroupProfile -) -``` - ---- - -### TIMUIKitBlackList - -`TIMUIKitBlackList` shows the list of blocked users. - -```dart -TIMUIKitBlackList( - onTapItem: (_) {}, // Callback of clicking the item - emptyBuilder: () {} // The builder when no user listed - itemBuilder: () {} // Customized the user item - lifeCycle: BlockListLifeCycle(), // Lifecycle hook for TIMUIKitBlackList -) -``` - ---- - -### TIMUIKitGroup - -`TIMUIKitGroup` shows the list of joined group. - -```dart -TIMUIKitGroup( - onTapItem: (_) {}, // Callback of clicking the item - emptyBuilder: () {} // The builder when no group listed - itemBuilder: () {} // Customized the group item -) -``` - ---- - -### TIMUIKitContact - -`TIMUIKitContact` shows the list of contacts. - -```dart -import 'package:tencent_cloud_chat_uikit/tencent_cloud_chat_uikit.dart'; - -TIMUIKitContact( - lifeCycle: FriendListLifeCycle(), // Lifecycle hook for TIMUIKitContact - topList: [ - TopListItem(name: "New Contact", id: "newContact"), - TopListItem(name: "Group", id: "groupList"), - TopListItem(name: "Blocklist", id: "blackList") - ], // Top list array - topListItemBuilder: _topListBuilder, // The builder for top list array - onTapItem: (item) { }, // Callback of clicking a contact - emptyBuilder: (context) => const Center( - child: Text("No cantact"), - ), // The builder when no contact listed - ); -``` - -### TIMUIKitSearch - -`TIMUIKitSearch` is a global search widget. Global search supports search for "contacts" / "groups" / "chat records". -`TIMUIKitSearchMsgDetail` is an intra-conversation search component that can search for chat records for a specific conversation. - -```dart -import 'package:tencent_cloud_chat_uikit/tencent_cloud_chat_uikit.dart'; - -// Search globally -TIMUIKitSearch( - onTapConversation: _handleOnConvItemTapedWithPlace, // Function(V2TimConversation, V2TimMessage? message), navigating to specific message from specific conversation - onEnterConversation: (V2TimConversation conversation, String initKeyword){}, // Navigating to search in specific conversation. Please navigate to TIMUIKitSearchMsgDetail manually -); - -// Search inside a specific conversation -TIMUIKitSearchMsgDetail( - currentConversation: conversation!, - onTapConversation: onTapConversation, - keyword: initKeyword ?? "", - ); -``` - -## What's more - -In addition to Flutter SDK, we have numerous SDKs that covering all platforms. The following platforms can communicate with each other and provide services across devices and platforms. - -| Platform | Introduction | Demo | Download | UI Components library | -| --- | --- | --- | --- | --- | -| Android | Compatible with JDK 1.6 and Android SDK version 14 and later | [Get](https://www.tencentcloud.com/document/product/1047/34279) | [Get](https://github.com/TencentCloud/TIMSDK/tree/master/Android/IMSDK) | [Get](https://www.tencentcloud.com/document/product/1047/50062) | -| iOS | Compatible with iOS 8.0 and later | [Get](https://www.tencentcloud.com/document/product/1047/34279) | [Get](https://github.com/TencentCloud/TIMSDK/tree/master/iOS/IMSDK) | [Get](https://www.tencentcloud.com/document/product/1047/50062) | -| Mac | Compatible with OS X 10.10 and later | - | [Get](https://github.com/TencentCloud/TIMSDK/tree/master/Mac/IMSDK) | - | -| Windows | C and C++ are included. Compatible with Windows 7, Windows 8 and 8.1, and Windows 10. Both 32-bit and 64-bit programs can be connected | - | [Get](https://github.com/TencentCloud/TIMSDK/tree/master/Windows/IMSDK) | - | -| Web | Supports Internet Explorer 11+, Chrome 7+, Firefox 3.6+, Opera 12+ and Safari 6+ | [Get](https://www.tencentcloud.com/document/product/1047/34279) | [Get](https://www.npmjs.com/package/tim-js-sdk) | [Get](https://www.tencentcloud.com/document/product/1047/50061) | -| Unity | Supports 2020.2.7f1c1 or later | - | [Get](https://www.tencentcloud.com/document/product/1047/46263) | - | -| Flutter | Supports Flutter 2 & dart 2.12 and later, deploying to Android, iOS, Web, macOS and Windows. | [Get](https://www.tencentcloud.com/document/product/1047/34279) | Here | [Get](https://pub.dev/packages/tencent_cloud_chat_uikit) | -| Electron | Electron SDK | - | [Get](https://github.com/tencentyun/im_electron_demo) | - | - -## Contact Us - -Please do not hesitate to contact us in the following place, if you have any further questions or tend to learn more about the use cases. +Please do not hesitate to contact us in the following place, if you have any further questions or +tend to learn more about the use cases. - Telegram Group: - WhatsApp Group: diff --git a/doc/社交场景最佳实践.md b/doc/社交场景最佳实践.md deleted file mode 100644 index 118f1d6..0000000 --- a/doc/社交场景最佳实践.md +++ /dev/null @@ -1,509 +0,0 @@ - -# 社交场景最佳实践方案 - -社交模块是目前主流应用程序最常见的功能之一。有了社交模块,用户在您的应用内,可以自由的交流互动,并添加好友,关注其他用户等等。 - -这可在很大程度上,促进您应用程序的活跃度,吸引用户留存,获取更多新用户,并可拓展您应用的业务范围。让更多用户花更多时间在您的应用程序上。 - -例如, - -- 交友软件,其核心便是社交聊天模块,用于匹配对话及用户关系链维护,让更多的用户相聚与相识; -- 音乐软件,可用社交模块让乐迷及粉丝群体实时沟通,打造音乐社区文化; -- 教育软件,可用社交模块打通 "学校-教师-家长" 循环,促进家校互动,形成家校社三合力,更大程度发挥教育影响作用,保证教育的一致性与连贯性; -- 电子游戏,特别是RPG类型,内置的聊天模块让玩家能在线组局,一起作战,并组建工会,创造游戏内社区,提升游戏社交属性,丰富体验,提升活跃度; -- 医疗软件,聊天及社群模块让患者间得以互助交流,一起战胜病魔,走出病情,让大家看到希望; -- 导航软件,堵车交流群,让归心似箭的旅行者们,交流前方最新情报,不再无聊干等,发发段子调侃一下,为拥堵的出行,带来一些希望与欢乐。 - -因此,社交聊天模块可谓是您应用程序不可或缺的能力。 - -本文将以腾讯云IM为基础,梳理现有应用在接入社交场景过程中常见需求,给出解决实现方案。以及可能遇到的问题、需要注意的细节点等,希望能帮助客户朋友们快速的理解业务、实现需求。 - ->? 本文档主要介绍社交场景的通用SDK实现方案,文中示例截图来自于我们的[Flutter TUIKit](https://cloud.tencent.com/document/product/269/70747),您可根据需要,选用我们提供的全部平台SDK或TUIKit。 - -## 用户 - -腾讯云IM支持托管维护用户信息与用户资料。您可直接将您应用的用户资料存储与我们的服务内,并通过相关API进行读取/更新/维护操作。 - -对于社交场景,常见用户资料可分为基本信息资料和其他信息资料。 - -|基本信息|其他信息| -|---|---| -|用户名,性别,生日,所在地,个性签名,昵称等|其他社交模块内需要的资料| - -### 导入现有用户数据 - -如果您需要给您现有应用,添加社交能力。可直接使用我们的服务端API,快速将您现有用户数据,完整导入至腾讯云IM中。 - -导入完成后,现有用户可直接使用其原有身份数据,和其他用户发起会话,一起聊天,开启社交之旅。 - -[导入多个账号 - 服务端API](https://cloud.tencent.com/document/product/269/4919) - -### 用户在线状态 - -腾讯云IM支持自动上报并让其他用户获取[在线状态信息](https://cloud.tencent.com/document/product/269/3665)。 - -状态包括:前台运行状态 / 后台运行状态 / 未登录状态。 - -利用这一能力,您可让用户看到其他用户的在线状态,增强互动性。 - -此外,您还可使用这一能力,针对您的业务场景,做许多功能拓展。例如,交友软件,能够优先推荐匹配在线的用户,让聊天可进行的更顺畅,缘分更快相聚。 - -| 会话列表用户在线状态 | 通讯录用户在线状态 | -|---------|---------| -| | | - -#### 获取用户在线状态 - -在客户端上, 您可调用 [`getUserStatus`](https://comm.qq.com/im/doc/flutter/zh/SDKAPI/Api/V2TIMManager/getUserStatus.html) 方法,[批量查询其他用户的在线状态](https://cloud.tencent.com/document/product/269/75511#.E6.9F.A5.E8.AF.A2.E7.94.A8.E6.88.B7.E7.8A.B6.E6.80.81)。 - -此外,在服务端上,也可[通过REST API,获取用户状态](https://cloud.tencent.com/document/product/269/2566)。 - -#### 订阅用户在线状态变更 - -其他用户的在线状态总是实时在变化的,您可在客户端上,调用 [`subscribeUserStatus`](https://comm.qq.com/im/doc/flutter/zh/SDKAPI/Api/V2TIMManager/subscribeUserStatus.html) 方法,[批量订阅其他用户的在线状态](https://cloud.tencent.com/document/product/269/75511#.E8.AE.A2.E9.98.85.E7.94.A8.E6.88.B7.E7.8A.B6.E6.80.81)。当发生变化时,将通过回调函数通知您,您可根据其,修改界面UI并完成其他您的业务操作。 - -## 好友 - -好友管理,又称关系链管理,是社交场景的基础。众多会话/聊天特性,都要依赖于好友关系状态。有了好友关系链能力,众多的用户才能得以串联起来,互动形成整体。 - -例如微信/QQ只允许好友间发起一对一单聊;交友软件则常常可在非好友的情况下,进行有限度的聊天;在线娱乐社区软件,则常常不需要好友关系即可会话。 - -因此,您需要根据您的应用使用场景,确定好友及关系链管理的用法。 - -| 通讯录 | -|---------| -| | - -### 好友关系 - -腾讯云IM支持单个用户添加最多3000个好友。 - -#### 好友关系类型 - -好友关系类别包含单向好友和双向好友。 - -- 双向好友:用户 A 的好友表中有用户 B,B 的好友表中也有 A。 -- 单向好友:用户 A 的好友表中有用户 B,但 B 的好友表中却没有 A。 - -#### 添加好友验证方式 - -一回合加好友:如果帐号 A 设置的加好友验证方式是 [AllowType_Type_AllowAny](https://cloud.tencent.com/document/product/269/1500#.E6.A0.87.E9.85.8D.E8.B5.84.E6.96.99.E5.AD.97.E6.AE.B5),那么任何人添加 A 为好友都可直接添加成功,这种一个请求就添加好友成功的场景称作一回合加好友。 - -两回合加好友:如果帐号 A 设置的加好友验证方式是 [AllowType_Type_NeedConfirm](https://cloud.tencent.com/document/product/269/1500#.E6.A0.87.E9.85.8D.E8.B5.84.E6.96.99.E5.AD.97.E6.AE.B5),那么任何人添加 A,A 都会收到一个请求加好友验证消息,这是第一个回合,然后 A 对这个请求加好友验证消息进行同意操作时,这是第二个回合,这种需要验证的加好友场景就被称为两回合加好友。 - -### 非好友发消息 - -对于某些场景,需要非好友关系也能发送消息。例如对于交友软件,常常允许匹配到的陌生人,发送若干条消息打招呼。 - -这需要您在[腾讯云IM的控制台](https://console.cloud.tencent.com/im/login-message),关闭 “好友关系检查” 功能。 - -![](https://qcloudimg.tencent-cloud.cn/raw/a7d79a4f4af1301879683f1ff99dcdd8.png) - -如果您需要针对陌生人发消息的数量加以限制,可在您的业务层实现。发送若干条消息后,不再允许发送即可。 - -## 群组 - -有的时候,仅一对一单聊无法满足您的社交场景要求。 - -例如对于音乐类型app,优质的乐迷群及粉丝群,有助于打造音乐社区文化,吸引用户留存。 - -### 群组类型 - -腾讯云IM支持多种类型的群,为便于理解,在这里以常见的群聊举例。 - -#### 微信群 - 好友工作群 Work - -Work群,类似普通微信群。创建后仅支持已在群内的好友邀请加群,且无需被邀请方同意或群主审批。 - -该群适合用于打造好友间互动拉入,产生的群。 - -#### QQ群 - 陌生人社交群 Public - -Public群,类似QQ群。创建后群主可指定群管理员,用户搜索群 ID 发起加群申请后,需要群主或管理员审批通过(可选)才能入群。 - -该群适合用于打造兴趣社区,用户在您的app中,发现好玩的兴趣群组,可按需主动加入。 - -#### 会议群 - Meeting - -创建后可随意进出,且支持查看入群前消息。 - -适合用于音视频会议场景、在线教育场景等与实时音视频产品结合的场景。 - -#### 直播群 - AVChatRoom - -创建后可随意进出,没有群成员数量上限,但不支持历史消息存储。 - -适合与直播产品结合,用于弹幕聊天场景。 - -#### Discord - 社群 Community - -创建后可随意进出,最多支持10w人,支持历史消息存储,用户搜索群 ID 发起加群申请后,无需管理员审批即可进群。[详情可查看此文档](https://cloud.tencent.com/document/product/269/83870)。 - -### 群资料 - -群资料主要包括 群组本身的资料 和 群成员资料。 - -#### 群组本身的资料 - -群组资料是指单个群组维度的属性,包括群名称、简介、公告、群主等,以及群组维度自定义字段。 - -##### 获取群资料 - -在客户端上,可调用 [`getGroupsInfo`](https://comm.qq.com/im/doc/flutter/zh/SDKAPI/Api/V2TIMGroupManager/getGroupsInfo.html) 方法,获取特定群组资料详情。 - -##### 修改群资料 - -可修改群组名称、群组简介、群组公告、群组头像、群名片,修改加群选项、群纬度自定义字段、用户群内身份、群成员维度自定义字段和接收群消息选项等信息。 - -在客户端上,可调用 [`setGroupInfo`](https://comm.qq.com/im/doc/flutter/zh/SDKAPI/Api/V2TIMGroupManager/setGroupInfo.html) 方法,修改特定群组资料详情。 - -#### 群成员资料 - -群成员资料主要包括,特定成员,在群内的备注名/角色/禁言状态/自定义字段信息。在强社交场景的群内,会非常实用。 - -##### 获取群成员资料 - -可通过客户端 [`getGroupMembersInfo`](https://comm.qq.com/im/doc/flutter/zh/SDKAPI/Api/V2TIMGroupManager/getGroupMembersInfo.html) 方法或 [`getGroupMemberList`](https://comm.qq.com/im/doc/flutter/zh/SDKAPI/Api/V2TIMGroupManager/getGroupMemberList.html) 方法获取此信息。 - -##### 修改群成员资料 - -不同的群成员资料,调用不同的API方法修改。[详情可查看此处](https://cloud.tencent.com/document/product/269/37411#.E7.BE.A4.E6.88.90.E5.91.98)。 - -## 会话 - -一个会话,您可理解为同某个特定用户的单聊,或一个群聊的消息集合。当用户创建了一个单聊或群聊,当其中有消息的收发时,对应的会话就随之创建。 - -在腾讯云IM层面,每个会话都是一个 [`V2TIMConversation`](https://comm.qq.com/im/doc/flutter/zh/SDKAPI/Class/Message/V2TimConversation.html) 类的实例,包括了 `会话类型 / 会话ID / 用户ID / 群ID / 显示名称 / 头像 / 最后一条消息 / 草稿 / 群聊类型 / 消息接收方式 / 群 @ 信息列表 / 是否置顶 / 标记列表 / 所属分组信息 / 自定义数据` 信息。 - -### 会话列表 - -会话列表,您可以理解成微信软件的首页。即,所有会话的集合。方便用户找到目标会话。 - -会话列表功能主要分为获取会话列表、处理会话列表更新。 - -| 会话列表 | -|---------| -| | - -#### 获取会话列表 - -您可在客户端上调用 [`getConversationList`](https://comm.qq.com/im/doc/flutter/zh/SDKAPI/Api/V2TIMConversationManager/getConversationList.html)。该接口获取的是本地缓存的会话,如果服务器会话有更新,SDK 内部会自动同步,然后在 [`V2TIMConversationListener`](https://comm.qq.com/im/doc/flutter/zh/SDKAPI/Class/Listener/V2TimConversationListener.html) 通知回调。 - -如果您的应用场景会产生较多的会话数,考虑到加载效率、网络省流,我们建议您采用分页拉取的方式。每次分页拉取的数量建议不超过 100 个。[具体方案可参考此处。](https://cloud.tencent.com/document/product/269/75366#.E5.88.86.E9.A1.B5.E6.8B.89.E5.8F.96) - -#### 会话列表实时更新 - -当会话信息发生变化,例如收到一条新消息/设置消息草稿/出现一个新的会话,都会导致会话列表发生更新。 - -如果需要实时获取更新信息,需要通过客户端的 [`addConversationListener`](https://comm.qq.com/im/doc/flutter/zh/SDKAPI/Api/V2TIMConversationManager/addConversationListener.html) 方法,添加会话监听器。收到更新触发后,更改UI。 - -### 会话草稿 - -在发送消息时,可能会遇到消息尚未编辑完,就要切换至其它聊天窗口的情况。 - -这些未编辑完的消息可通过 [`setConversationDraft`](https://comm.qq.com/im/doc/flutter/zh/SDKAPI/Api/V2TIMConversationManager/setConversationDraft.html) 接口保存,以便于下次回到这个聊天界面时,通过 [`V2TIMConversation`](https://comm.qq.com/im/doc/flutter/zh/SDKAPI/Class/Message/V2TimConversation.html) 对象的 [`draftText`](https://comm.qq.com/im/doc/flutter/zh/SDKAPI/Class/Message/V2TimConversation.html#drafttext) 字段,获取到尚未编辑完的内容,继续编辑。 - -此类草稿信息,仅保存在本地。 - -### 置顶会话 - -会话置顶指的是把单聊或者群聊会话固定在会话列表的最顶部,不会被其他会话更新挤到底部,方便用户查找。 - -在社交场景中,用户常常需要将一些重要的人或群置顶。这在我们使用微信的过程中,很普遍。 - -置顶状态会存储在服务器,切换终端设备后,置顶状态会同步到新设备上。 - -| 置顶的会话,注意最上方第一条 | -|---------| -| | - -置顶会话,通过客户端 [`pinConversation`](https://comm.qq.com/im/doc/flutter/zh/SDKAPI/Api/V2TIMConversationManager/pinConversation.html) 即可。 - -调用 [`getConversationList`](https://comm.qq.com/im/doc/flutter/zh/SDKAPI/Api/V2TIMConversationManager/getConversationList.html) 获取会话列表时,该接口返回的会话列表中,置顶的会话在前,未置顶的会话在后。您可以通过 [`V2TIMConversation`](https://comm.qq.com/im/doc/flutter/zh/SDKAPI/Class/Message/V2TimConversation.html) 对象的 [`isPinned`](https://comm.qq.com/im/doc/flutter/zh/SDKAPI/Class/Message/V2TimConversation.html#ispinned) 字段,检查会话有没有置顶。 - -### 会话标记 - -在某些社交场景下,您可能需要对会话进行标记,例如 "会话标星"、"会话折叠"、"会话隐藏"、“会话标记未读”。 - -直接在客户端调用 [`markConversation`](https://comm.qq.com/im/doc/flutter/zh/SDKAPI/Api/V2TIMConversationManager/markConversation.html) 方法,即可标记,或取消标记一条会话。 - -标记后,在后续通过 [`getConversationListByFilter`](https://comm.qq.com/im/doc/flutter/zh/SDKAPI/Api/V2TIMConversationManager/getConversationListByFilter.html) 方法请求会话列表时,可按照标记,过滤获取指定会话。 - -## 消息 - -消息是社交模块的灵魂。众多各种类型的消息,组成了一个个会话,使得用户与用户之间,紧密的串联在一起。 - -腾讯云IM中,一对一单聊消息与群聊消息,用法在大部分场景中都类似,下面着重介绍几点。 - -### 消息类型 - -腾讯云IM支持多种类型的消息,如下: - -| 功能类型 | 功能描述 | -| ------------ | ------------------------------------------------------------ | -| 文本消息 | 消息内容是普通文本 | -| 表情消息 | 表情消息为开发者自定义,可传入表情资源路径 | -| 地理位置消息 | 消息内容为地理位置标题、经度、纬度信息 | -| 图片消息 | 消息内容为图片的 URL 地址、尺寸、图片大小等信息,最大支持大小为28M的图片 | -| 语音消息 | 消息内容为语音文件的 URL 地址、大小、时长等信息,最大支持大小为28M的语音文件 | -| 文件消息 | 消息内容为文件的 URL 地址、大小、格式等信息,格式不限,最大支持大小为100M的文件 | -| 短视频消息 | 消息内容为短视频文件的 URL 地址、时长、大小、格式等信息,最大支持大小为100M的短视频文件 | -| 自定义消息 | 开发者自定义的消息类型,例如红包消息、石头剪刀布等形式的消息 | - -### 消息回复 - -回复一条消息,既支持使用文字内容,发一条新消息,引用原消息;也支持使用[Emoji表情回应](https://cloud.tencent.com/document/product/269/85906)。 - -| 引用回复文本 | [表情回应](https://cloud.tencent.com/document/product/269/85906) | -|---------|---------| -| | | - -#### 引用回复文本 - -此方案效果和微信中,长按一条消息,选择 “引用”,效果一致。 - -引用消息,实际上,在腾讯云IM SDK层面,也是一条普通文本消息。文本消息的主体,则是回复的文字内容。 - -为了展示原消息的引用,需要在发送文本消息的时候,将原消息的信息,传入新消息的 [`cloudCustomData`](https://comm.qq.com/im/doc/flutter/zh/SDKAPI/Class/Message/V2TimMessage.html#cloudcustomdata) 字段中。例如,我们的TUIKit,为了实现这个功能,传入了如下JSON。 - -```json -"messageReply": { - "messageID": 原消息的ID, - "messageAbstract": 原消息的描述,用于显示在消息列表气泡中, - "messageSender": 原消息的发送者,建议使用备注名或昵称, - "messageType": 原消息类型, - "version": 协议版本 -} -``` - -在消息列表中展示时,从 [`cloudCustomData`](https://comm.qq.com/im/doc/flutter/zh/SDKAPI/Class/Message/V2TimMessage.html#cloudcustomdata) 字段中,提取出上述JSON信息,直接用于拼接展示 `"${messageSender}: ${messageAbstract}"` 即可。 - -如需支持点击展示引用消息的区域,跳转至被引用原始消息。可根据上述JSON中的 `messageID` 字段,在消息列表中,找到这条消息,跳转即可。如果消息当前不存在于数组中,可直接调用 [`getHistoryMessageList`](https://comm.qq.com/im/doc/flutter/zh/SDKAPI/Api/V2TIMMessageManager/getHistoryMessageList.html),参数传入 `messageID`,获取本条消息及其上下文消息。 - -#### 表情回应 - -在回复特定的单条消息时,你不仅可以直接引用原消息并回复,还可使用Emoji表情回应,高效表达“好的”、“赞同”、“很棒”、“哭笑不得”、“加油”等多种信息,大大降低沟通成本,解决多人聊天中消息冗杂的问题。 - -通常,若干个用户,可对同一条消息,或多条消息,点击一个或若干个回应表情。[在显示上](https://cloud.tencent.com/document/product/269/85906#.E5.B1.95.E7.A4.BA.E8.A1.A8.E6.83.85.E5.9B.9E.E5.BA.94),这些回应信息,常常承载在不同的气泡中,以表情为首,后面跟着若干个名字。如本章节图片所示。 - -这些名称,需要支持点击,并跳转至用户Profile详情页中。若名字过多,还需要加以折叠,[通过新窗口详情页展示](https://cloud.tencent.com/document/product/269/85906#.E5.B1.95.E7.A4.BA.E8.A1.A8.E6.83.85.E5.9B.9E.E5.BA.94)。 - -[发送表情回应](https://cloud.tencent.com/document/product/269/85906#.E5.8F.91.E9.80.81.E8.A1.A8.E6.83.85.E5.9B.9E.E5.BA.94),则可放置于消息的长按菜单中。 - -| 发送表情回应 [TUIKit](https://cloud.tencent.com/document/product/269/85906) | 表情回应详情 [TUIKit](https://cloud.tencent.com/document/product/269/85906) | -|---------|---------| -| | | - -**下面介绍实施细节:** - -表情回应的数据,存储于消息的 [`cloudCustomData`](https://comm.qq.com/im/doc/flutter/zh/SDKAPI/Class/Message/V2TimMessage.html#cloudcustomdata) 字段中,可以如下JSON格式示例。其中的 `key` 字段,如采用Emoji Unicode表情字符,可直接传入 Unicode;若采用图片小表情,可传入路径或文件名。 - -```json -messageReactions: [ - { - key: "表情名称1", - users: ["用户1", "用户2", ...] - }, - { - key: "表情名称2", - users: ["用户1", "用户2", ...] - }, - ... -] -``` - -本部分代码可根据您的需求,加以修改。 - -展示时,渲染遍历上述结构体即可。 - -发送表情回应,则直接在客户端调用 [`modifyMessage`](https://comm.qq.com/im/doc/flutter/zh/SDKAPI/Api/V2TIMMessageManager/modifyMessage.html) 方法,修改消息本身的 [`cloudCustomData`](https://comm.qq.com/im/doc/flutter/zh/SDKAPI/Class/Message/V2TimMessage.html#cloudcustomdata) 字段即可。将当前用户头像,添加或从上述结构体中移除,完成回应或取消回应。 - -调用方法修改后,所有用户的 [`V2TimAdvancedMsgListener`](https://comm.qq.com/im/doc/flutter/zh/SDKAPI/Class/Listener/V2TimAdvancedMsgListener.html) => [`onRecvMessageModified`](https://comm.qq.com/im/doc/flutter/zh/SDKAPI/Callback/OnRecvMessageModified.html) 监听器会触发,您可依此修改消息UI,展示最新表情回应内容。 - -### 删除消息 - -删除消息分为两种:删除本地消息和删除云端消息。 - -删除云端消息会在删除本地消息的基础上,同步删除云端存储的消息,且无法恢复。 - -删除本地消息,在客户端调用 [`deleteMessageFromLocalStorage`](https://comm.qq.com/im/doc/flutter/zh/SDKAPI/Api/V2TIMMessageManager/deleteMessageFromLocalStorage.html) 方法。需要注意的是,如果程序卸载重装,依旧能获取到被删除的消息。 - -删除云端存储的消息,在客户端调用 [`deleteMessages`](https://comm.qq.com/im/doc/flutter/zh/SDKAPI/Api/V2TIMMessageManager/deleteMessages.html) 方法。此方法不支持多端同步,即无法自动删除,其他设备上,已经存在的消息。 - -### 搜索消息 - -搜索能力是社交场景中的重要一环。 - -用户们常常需要,在特定会话中,亦或是全局中,快速准确搜索到某条消息。此外,也可作为社交活动运营工具,增加相关内容的引导,简洁高效。 - -您可在客户端,调用 [`searchLocalMessages`](https://comm.qq.com/im/doc/flutter/zh/SDKAPI/Api/V2TIMMessageManager/searchLocalMessages.html) 方法,并传入以 [`V2TIMMessageSearchParam`](https://comm.qq.com/im/doc/flutter/zh/SDKAPI/Class/Message/V2TimMessageSearchParam.html) 对象封装的关键词信息,即可完成搜索。 - -如果您希望在全部会话范围内搜索,只需要将 [`V2TIMMessageSearchParam`](https://comm.qq.com/im/doc/flutter/zh/SDKAPI/Class/Message/V2TimMessageSearchParam.html) 中的 [`conversationID`](https://comm.qq.com/im/doc/flutter/zh/SDKAPI/Class/Message/V2TimMessageSearchParam.html#conversationid) 设置为空或者不设置即可。 - -| 全局搜索 | 会话内搜索 | -|---------|---------| -| | | - -### 转发消息 - -在日常生活聊天或工作场景中,将一个会话中的消息,合并或逐条转发至另一个会话,是个非常高频且基础的操作。 - -| 合并转发消息 | 合并消息详细内容 | -|---------|---------| -| | | - -逐条转发消息,需要先在客户端调用 [`createForwardMessage`](https://comm.qq.com/im/doc/flutter/zh/SDKAPI/Api/V2TIMMessageManager/createForwardMessage.html) 方法创建一条和原消息内容完全一样的转发消息,再调用 [`sendMessage`](https://comm.qq.com/im/doc/flutter/zh/SDKAPI/Api/V2TIMMessageManager/sendMessage.html) 方法把转发消息发送出去。 - -合并转发消息,同样需要先创建后转发。需要额外注意的是,在客户端调用 [`createMergerMessage`](https://comm.qq.com/im/doc/flutter/zh/SDKAPI/Api/V2TIMMessageManager/createMergerMessage.html) 方法创建一条合并消息时,需要设置原始消息列表,合并消息标题、合并消息摘要等信息。 - -若想转发至多个接收者,遍历调用 [`sendMessage`](https://comm.qq.com/im/doc/flutter/zh/SDKAPI/Api/V2TIMMessageManager/sendMessage.html) 方法即可。 - -对于接收者端,若想展示上方右侧图片的合并消息详情,需要当用户点击合并消息的时候再调用 [`downloadMergerMessage`](https://comm.qq.com/im/doc/flutter/zh/SDKAPI/Api/V2TIMMessageManager/downloadMergerMessage.html) 方法下载合并消息列表 UI 展示。 - -### 撤回消息 - -消息撤回是目前社交软件中必备的功能。 - -发送方可撤回一条已经发送成功的消息。默认情况下,发送者只能撤回 2 分钟以内的消息,此配置可按需修改。 - -撤回方在客户端,调用 [`revokeMessage`](https://comm.qq.com/im/doc/flutter/zh/SDKAPI/Api/V2TIMMessageManager/revokeMessage.html) 方法,接收方会收到消息撤回通知 [`onRecvMessageRevoked`](https://comm.qq.com/im/doc/flutter/zh/SDKAPI/Callback/OnRecvMessageRevokedCallback.html)。通知中包含了撤回消息的 msgID,您可根据这个 msgID 判断 UI 层是哪一条消息撤回了,然后把对应的消息气泡切换成 "消息已被撤回" 状态。 - -### 消息翻译 - -对于国际化的聊天场景,消息翻译功能必不可少,可大大提升跨语言交流效率。社交场景中,大型群聊内,有不同语言的交流存在,是非常之常见的。 - -| 消息翻译 | -|---------| -| | - -对于文本类型的消息,您可在客户端上调用 [`translateText`](https://im.sdk.qcloud.com/doc/zh-cn/classcom_1_1tencent_1_1imsdk_1_1v2_1_1V2TIMMessageManager.html#a1e1806c27bc7b76a3b816492ed9cbe5c) 方法,将待翻译文本列表和目标语言传至我们的服务端。原语言可由您自行判断,也可由我们判断。 - -翻译结果返回后,我们建议您将其通过客户端的 [`setLocalCustomData`](https://comm.qq.com/im/doc/flutter/zh/SDKAPI/Api/V2TIMMessageManager/setLocalCustomData.html) 方法,存放于消息的 [`localCustomData`](https://comm.qq.com/im/doc/flutter/zh/SDKAPI/Class/Message/V2TimMessage.html#localcustomdata) 字段中,以便于后续直接展示,避免用户重复翻译,多次发送翻译请求。 - -我们支持众多语言的互相翻译,[所有支持的语言可查看此处](https://cloud.tencent.com/document/product/269/85380#.E6.96.87.E6.9C.AC.E7.BF.BB.E8.AF.91.E8.AF.AD.E8.A8.80.E6.94.AF.E6.8C.81)。 - -### 消息已读回执 - -单聊和群聊均支持消息已读回执功能,操作步骤一致。 - -| 单聊,[TUIKit](https://cloud.tencent.com/document/product/269/85905)中以文字承载 | 群聊,[TUIKit](https://cloud.tencent.com/document/product/269/85905)中以圆圈承载 | -|---------|---------| -| | | - - -是否启用此功能,可根据您的社交业务需求决定。 - -例如对于类似微信的熟人社交,已读回执的用处可能不是非常大;但是对于陌生人交友场景,已读回执则十分重要,帮助用户来确认,对方是否愿意跟自己聊下去,是否已读不回;对于工作聊天场景,群已读回执还能发挥更大的作用,可便捷看到群内哪些人已读哪些人未读,帮助发送者确认信息传递效率。 - -**具体用法如下:** - -发送端创建消息后,先通过消息对象 [`V2TIMMessage`](https://comm.qq.com/im/doc/flutter/zh/SDKAPI/Class/Message/V2TimMessage.html) 的 [`needReadReceipt`](https://comm.qq.com/im/doc/flutter/zh/SDKAPI/Class/Message/V2TimMessage.html#needreadreceipt) 字段设置这条消息需要已读回执,再发送消息到会话中。 - -接收端收到消息后,根据消息对象 [`V2TIMMessage`](https://comm.qq.com/im/doc/flutter/zh/SDKAPI/Class/Message/V2TimMessage.html) 的 [`needReadReceipt`](https://comm.qq.com/im/doc/flutter/zh/SDKAPI/Class/Message/V2TimMessage.html#needreadreceipt) 字段判断消息是否需要已读回执。如果需要,当用户查看消息后,调用 `sendMessageReadReceipts` 方法发送消息已读回执。 - -接收端发送消息已读回执后,发送端可在 [`V2TIMAdvancedMsgListener`](https://comm.qq.com/im/doc/flutter/zh/SDKAPI/Class/Listener/V2TimAdvancedMsgListener.html) 的 [`onRecvMessageReadReceipts`](https://comm.qq.com/im/doc/flutter/zh/SDKAPI/Callback/OnRecvMessageReadReceipts.html) 中收到已读回执通知,在通知中更新 UI,例如更新某条消息为 “2 人已读”。 - -此外,发送端也可主动请求消息已读回执信息。发送端从其他界面进入消息列表后,先请求获取历史消息,再调用 [`getMessageReadReceipts`](https://comm.qq.com/im/doc/flutter/zh/SDKAPI/Api/V2TIMMessageManager/getMessageReadReceipts.html) 方法请求获取消息已读回执信息。 - -群聊场景的消息已读回执,通常需要能够查看详情,显示群内哪些人已读,哪些人未读。当用户点击已读回执角标时,可调用 [`getGroupMessageReadMemberList`](https://comm.qq.com/im/doc/flutter/zh/SDKAPI/Api/V2TIMMessageManager/getGroupMessageReadMemberList.html) 方法分页拉取消息已读或未读群成员列表。 - -| 已读 群成员 | 未读 群成员 | -|---------|---------| -| | | - -### 群内@消息 - -相信大家已经很熟悉,在群聊交流过程中,如果需要提及或提醒某些群成员,我们可直接 @ 他们。所有的社交聊天软件,都有这个基础功能。 - -当用户输入 @ 字符后,弹出群成员选择界面。选择完需要 @ 的成员后以 “@A @B @C......” 形式显示在输入框,并可继续编辑消息内容,完成消息发送。 - -| 监听 @ 字符选择群成员 | 编辑群 @ 消息发送 | 收到群 @ 消息 | -|---------|---------|---------| -| | | | - ->? 图一:在聊天界面监听到输入框输入 "@" 字符后,可跳转到群成员选择界面,选择需要 @ 的群成员。 ->图二:在群成员选择完成后,重新返回聊天界面,继续编辑群 @ 消息发送。 ->图三:如果有消息 @ 我,自己会收到会话更新,可在会话 Cell 展示 “有人@我” 信息。 - -由于实现方案内容较多,[您可查看此文档](https://cloud.tencent.com/document/product/269/75349),获取详情。 - -### 消息漫游 - -如果用户有多台设备,或者同时使用电脑和手机登录您的应用程序,用户们希望看到,无论在哪一端,历史消息都能尽可能完整。能从提供的历史消息上下文中,快速无障碍的加入聊天,满足社交场景高频率聊天的要求。 - -为了保证交流的连续性与流畅性,我们提供一套消息漫游存储能力,即用户更换终端的情况下,也可以获取到跟其他用户或者某个群的聊天记录,可以达到类似QQ软件的效果。 - -默认情况下,单聊消息和群聊消息有7天漫游,超过漫游时长的消息会被删除。此外,还支持在控制台修改消息漫游时长,延长消息漫游时长是增值服务。 - -以下截图演示了消息在手机和电脑之间漫游。*图片来自Flutter TUIKit,一套代码完成电脑桌面端/Web端/移动端应用的开发。* - -| 电脑端 | 手机移动端 | -|---------|---------| -| | | - -### 更多丰富的消息形态 - -我们默认提供的消息类型,可满足您大部分的聊天场景需求。但是对于社交软件来说,仅有这些还远远不够。 - -红包/送礼物/投票/发送匹配度/闪照等等一系列创新玩法,让您app的社交场景模块变得更加丰富多彩。 - -因此,您可使用我们提供的**自定义消息**能力,来发挥您的想象力,尽情创造激动人心的玩法及贴合您业务需求的功能。 - -发送自定义消息分两步: - -- 调用 [`createCustomMessage`](https://comm.qq.com/im/doc/flutter/zh/SDKAPI/Api/V2TIMMessageManager/createCustomMessage.html) 创建自定义消息,传入消息体。 -- 调用 [`sendMessage`](https://comm.qq.com/im/doc/flutter/zh/SDKAPI/Api/V2TIMMessageManager/sendMessage.html) 发送消息。 - -消息体中,您可以JSON格式,传入任何符合您需求的数据。例如,包含一个字段控制消息形态类型,还包含一个字段控制消息当前数据。 - -如果是投票/红包等类型的消息,如果您想实时更新投票数据/红包领取信息,可将此类信息放在消息体中,在客户端上,通过 [`modifyMessage`](https://comm.qq.com/im/doc/flutter/zh/SDKAPI/Api/V2TIMMessageManager/modifyMessage.html) 方法实时修改。 - -## 更多能力 - -### 内容审核 - -在社交场景中,用户很可能会发送不合规的消息。特别是陌生人交友软件,黄色不良内容消息更是频频出现。诱导性暗示图片/裸聊等令人反感的内容,不仅严重损害了用户们的身心健康,更很有可能违法并导致应用被监管部门查封。 - -我们支持内容审核(反垃圾信息)功能,可针对不安全、不适宜的内容进行自动识别、处理,为您的产品体验和业务安全保驾护航。 - -目前有以下三种内容审核方式。 - -| 方式 | 介绍 | -|---------|---------| -| [本地审核](https://cloud.tencent.com/document/product/269/79139#bdsh) | 在客户端本地检测由即时通信 SDK 发送的文本内容,支持对已配置的敏感词进行拦截或者替换处理。此功能通过在 IM 开启服务并配置词库的方式实现。 | -| [云端审核](https://cloud.tencent.com/document/product/269/79139#ydsh) | 在服务端检测由单聊、群聊、资料场景中产生的文本、图片、音频、视频内容,支持针对不同场景的不同内容分别配置审核策略,并对识别出的不安全内容进行拦截。 | -| [第三方回调服务](https://cloud.tencent.com/document/product/269/79139#dsf) | 如果您已接入第三方内容审核服务,您可以使用 第三方回调配置 来实现。 | - -您可按需使用如上三种内容审核工具,保证业务安全运行。 - -### 离线推送 - -社交场景下,用户需要随时都能够得知最新的消息,以加快聊天效率,促进社交关系的形成。 - -由于移动端设备的性能与电量有限,当 App 处于后台时,为了避免维持长连接而导致的过多资源消耗,我们推荐您使用各厂商提供的系统级推送通道来进行消息通知。系统级的推送通道相比第三方推送拥有更稳定的系统级长连接,可以做到随时接受推送消息,且资源消耗大幅降低。 - -我们目前原生支持的厂商系统有:苹果iOS/Google FCM/OPPO/VIVO/华为/小米/魅族/荣耀。 - -理论上,集成系统原生的离线推送,需要手动对接各个厂商的SDK,手动注册服务/获取Token/承载点击回调页面,非常之复杂。 - -因此,我们提供了如下几款离线推送插件,封装了上述厂商的原生SDK,大大降低了使用上手成本。您可直接按照文档配置。开箱即用。 - -- [Native](https://cloud.tencent.com/document/product/269/74285) -- [Flutter](https://cloud.tencent.com/document/product/269/74605) -- [uni-app](https://cloud.tencent.com/document/product/269/79124) - -![](https://qcloudimg.tencent-cloud.cn/raw/58f36a132a9fa46b84b12070777b8e9b.png) - -### 音视频通话 - -许多时候,仅靠文字和图片还是不足以抒发我们内心的情感,可能打字聊天一小时,也比不是直接打一通视频电话来的爽快。 - -特别是对于社交场景下的用户们,他们一定有很多想法想要交流与诉说。 - -因此,我们也强烈推荐您,再集成我们的音视频通话能力,一步到位,完成一对一或多人群组的音频/视频通话,并且支持离线唤起能力。 - -[详情可查看此文档](https://cloud.tencent.com/document/product/269/84296)。 - -![](https://qcloudimg.tencent-cloud.cn/raw/08f914b45857743fd05dfaa28e2adb72.png) - -以上,就是使用腾讯云IM实现社交场景常见需求的解决方案。快来使用腾讯云IM打造属于您的社交产品吧~ diff --git a/example/.metadata b/example/.metadata index 5a02328..212e4a0 100644 --- a/example/.metadata +++ b/example/.metadata @@ -1,10 +1,36 @@ # This file tracks properties of this Flutter project. # Used by Flutter tool to assess capabilities and perform upgrades etc. # -# This file should be version controlled and should not be manually edited. +# This file should be version controlled. version: - revision: 7e9793dee1b85a243edd0e06cb1658e98b077561 + revision: b8f7f1f9869bb2d116aa6a70dbeac61000b52849 channel: stable project_type: app + +# Tracks metadata for the flutter migrate command +migration: + platforms: + - platform: root + create_revision: b8f7f1f9869bb2d116aa6a70dbeac61000b52849 + base_revision: b8f7f1f9869bb2d116aa6a70dbeac61000b52849 + - platform: linux + create_revision: b8f7f1f9869bb2d116aa6a70dbeac61000b52849 + base_revision: b8f7f1f9869bb2d116aa6a70dbeac61000b52849 + - platform: macos + create_revision: b8f7f1f9869bb2d116aa6a70dbeac61000b52849 + base_revision: b8f7f1f9869bb2d116aa6a70dbeac61000b52849 + - platform: windows + create_revision: b8f7f1f9869bb2d116aa6a70dbeac61000b52849 + base_revision: b8f7f1f9869bb2d116aa6a70dbeac61000b52849 + + # User provided section + + # List of Local paths (relative to this file) that should be + # ignored by the migrate tool. + # + # Files that are not part of the templates will be ignored by default. + unmanaged_files: + - 'lib/main.dart' + - 'ios/Runner.xcodeproj/project.pbxproj' diff --git a/example/1400187352_3130303435333633/im.db b/example/1400187352_3130303435333633/im.db new file mode 100644 index 0000000..84687bc Binary files /dev/null and b/example/1400187352_3130303435333633/im.db differ diff --git a/example/1400187352_3130303435333633/im.db-shm b/example/1400187352_3130303435333633/im.db-shm new file mode 100644 index 0000000..722a46c Binary files /dev/null and b/example/1400187352_3130303435333633/im.db-shm differ diff --git a/example/1400187352_3130303435333633/im.db-wal b/example/1400187352_3130303435333633/im.db-wal new file mode 100644 index 0000000..07d73d7 Binary files /dev/null and b/example/1400187352_3130303435333633/im.db-wal differ diff --git a/example/1400187352_3130303435333633/msg_0.db b/example/1400187352_3130303435333633/msg_0.db new file mode 100644 index 0000000..84687bc Binary files /dev/null and b/example/1400187352_3130303435333633/msg_0.db differ diff --git a/example/1400187352_3130303435333633/msg_0.db-shm b/example/1400187352_3130303435333633/msg_0.db-shm new file mode 100644 index 0000000..015c0c0 Binary files /dev/null and b/example/1400187352_3130303435333633/msg_0.db-shm differ diff --git a/example/1400187352_3130303435333633/msg_0.db-wal b/example/1400187352_3130303435333633/msg_0.db-wal new file mode 100644 index 0000000..c5a8d0a Binary files /dev/null and b/example/1400187352_3130303435333633/msg_0.db-wal differ diff --git a/example/imsdk_C.mmap2 b/example/imsdk_C.mmap2 new file mode 100755 index 0000000..e5b999e Binary files /dev/null and b/example/imsdk_C.mmap2 differ diff --git a/example/imsdk_C_20221229.xlog b/example/imsdk_C_20221229.xlog new file mode 100644 index 0000000..c27d095 Binary files /dev/null and b/example/imsdk_C_20221229.xlog differ diff --git a/example/imsdk_api_report b/example/imsdk_api_report new file mode 100644 index 0000000..3bd4f45 Binary files /dev/null and b/example/imsdk_api_report differ diff --git a/example/imsdk_config_1400187352 b/example/imsdk_config_1400187352 new file mode 100644 index 0000000..a41f4d5 Binary files /dev/null and b/example/imsdk_config_1400187352 differ diff --git a/example/imsdk_sensitive_word_1400187352 b/example/imsdk_sensitive_word_1400187352 new file mode 100644 index 0000000..b8faf10 --- /dev/null +++ b/example/imsdk_sensitive_word_1400187352 @@ -0,0 +1,2 @@ + +Hϼhѽ#%-a \ No newline at end of file diff --git a/example/ios/Podfile b/example/ios/Podfile index 1e8c3c9..313ea4a 100644 --- a/example/ios/Podfile +++ b/example/ios/Podfile @@ -1,5 +1,5 @@ # Uncomment this line to define a global platform for your project -# platform :ios, '9.0' +platform :ios, '11.0' # CocoaPods analytics sends network stats synchronously affecting flutter build latency. ENV['COCOAPODS_DISABLE_STATS'] = 'true' diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index 68bacaa..4bd8450 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -1,17 +1,18 @@ PODS: - - camera (0.0.1): + - audioplayers_darwin (0.0.1): - Flutter - - connectivity_plus (0.0.1): + - camera_avfoundation (0.0.1): - Flutter - - ReachabilitySwift - - DKImagePickerController/Core (4.3.3): + - disk_space (0.0.1): + - Flutter + - DKImagePickerController/Core (4.3.4): - DKImagePickerController/ImageDataManager - DKImagePickerController/Resource - - DKImagePickerController/ImageDataManager (4.3.3) - - DKImagePickerController/PhotoGallery (4.3.3): + - DKImagePickerController/ImageDataManager (4.3.4) + - DKImagePickerController/PhotoGallery (4.3.4): - DKImagePickerController/Core - DKPhotoGallery - - DKImagePickerController/Resource (4.3.3) + - DKImagePickerController/Resource (4.3.4) - DKPhotoGallery (0.0.17): - DKPhotoGallery/Core (= 0.0.17) - DKPhotoGallery/Model (= 0.0.17) @@ -35,11 +36,18 @@ PODS: - DKPhotoGallery/Resource (0.0.17): - SDWebImage - SwiftyGif + - fc_native_video_thumbnail_for_us (0.0.1): + - Flutter - file_picker (0.0.1): - DKImagePickerController/PhotoGallery - Flutter - Flutter (1.0.0) - - flutter_plugin_record (0.0.1): + - flutter_image_compress (1.0.0): + - Flutter + - Mantle + - SDWebImage + - SDWebImageWebPCoder + - flutter_plugin_record_plus (0.0.1): - Flutter - fluttertoast (0.0.2): - Flutter @@ -52,69 +60,98 @@ PODS: - Flutter - image_picker_ios (0.0.1): - Flutter - - libwebp (1.2.1): - - libwebp/demux (= 1.2.1) - - libwebp/mux (= 1.2.1) - - libwebp/webp (= 1.2.1) - - libwebp/demux (1.2.1): + - libwebp (1.2.4): + - libwebp/demux (= 1.2.4) + - libwebp/mux (= 1.2.4) + - libwebp/webp (= 1.2.4) + - libwebp/demux (1.2.4): - libwebp/webp - - libwebp/mux (1.2.1): + - libwebp/mux (1.2.4): - libwebp/demux - - libwebp/webp (1.2.1) - - open_file (0.0.1): - - Flutter + - libwebp/webp (1.2.4) + - Mantle (2.2.0): + - Mantle/extobjc (= 2.2.0) + - Mantle/extobjc (2.2.0) - package_info_plus (0.4.5): - Flutter - - path_provider_ios (0.0.1): + - pasteboard (0.0.1): - Flutter - - "permission_handler (5.1.0+2)": + - path_provider_foundation (0.0.1): + - Flutter + - FlutterMacOS + - permission_handler_apple (9.0.4): - Flutter - photo_manager (2.0.0): - Flutter - FlutterMacOS - - ReachabilitySwift (5.0.0) - - SDWebImage (5.12.5): - - SDWebImage/Core (= 5.12.5) - - SDWebImage/Core (5.12.5) - - shared_preferences_ios (0.0.1): + - ReactiveObjC (3.1.1) + - SDWebImage (5.15.5): + - SDWebImage/Core (= 5.15.5) + - SDWebImage/Core (5.15.5) + - SDWebImageWebPCoder (0.11.0): + - libwebp (~> 1.0) + - SDWebImage/Core (~> 5.15) + - shared_preferences_foundation (0.0.1): - Flutter + - FlutterMacOS - sqflite (0.0.2): - Flutter - FMDB (>= 2.7.5) - - SwiftyGif (5.4.3) - - tencent_im_sdk_plugin (1.0.5): + - SwiftyGif (5.4.4) + - tencent_cloud_chat_sdk (5.1.2): - Flutter - HydraAsync - - TXIMSDK_Plus_iOS (= 6.1.2155.1) + - TXIMSDK_Plus_iOS (= 7.1.3925) + - tencent_cloud_uikit_core (0.0.1): + - Flutter + - TUICore (= 7.1.3925) + - tencent_open_file (0.0.1): + - Flutter - Toast (4.0.0) - - TXIMSDK_Plus_iOS (6.1.2155.1) + - TUICore (7.1.3925): + - ReactiveObjC + - SDWebImage + - TUICore/ImSDK_Plus (= 7.1.3925) + - TUICore/Base (7.1.3925): + - ReactiveObjC + - SDWebImage + - TUICore/ImSDK_Plus (7.1.3925): + - ReactiveObjC + - SDWebImage + - TUICore/Base + - TXIMSDK_Plus_iOS (= 7.1.3925) + - TXIMSDK_Plus_iOS (7.1.3925) + - url_launcher_ios (0.0.1): + - Flutter - video_player_avfoundation (0.0.1): - Flutter - - video_thumbnail (0.0.1): - - Flutter - - libwebp - wakelock (0.0.1): - Flutter DEPENDENCIES: - - camera (from `.symlinks/plugins/camera/ios`) - - connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`) + - audioplayers_darwin (from `.symlinks/plugins/audioplayers_darwin/ios`) + - camera_avfoundation (from `.symlinks/plugins/camera_avfoundation/ios`) + - disk_space (from `.symlinks/plugins/disk_space/ios`) + - fc_native_video_thumbnail_for_us (from `.symlinks/plugins/fc_native_video_thumbnail_for_us/ios`) - file_picker (from `.symlinks/plugins/file_picker/ios`) - Flutter (from `Flutter`) - - flutter_plugin_record (from `.symlinks/plugins/flutter_plugin_record/ios`) + - flutter_image_compress (from `.symlinks/plugins/flutter_image_compress/ios`) + - flutter_plugin_record_plus (from `.symlinks/plugins/flutter_plugin_record_plus/ios`) - fluttertoast (from `.symlinks/plugins/fluttertoast/ios`) - image_gallery_saver (from `.symlinks/plugins/image_gallery_saver/ios`) - image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`) - - open_file (from `.symlinks/plugins/open_file/ios`) - package_info_plus (from `.symlinks/plugins/package_info_plus/ios`) - - path_provider_ios (from `.symlinks/plugins/path_provider_ios/ios`) - - permission_handler (from `.symlinks/plugins/permission_handler/ios`) + - pasteboard (from `.symlinks/plugins/pasteboard/ios`) + - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/ios`) + - permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`) - photo_manager (from `.symlinks/plugins/photo_manager/ios`) - - shared_preferences_ios (from `.symlinks/plugins/shared_preferences_ios/ios`) + - shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/ios`) - sqflite (from `.symlinks/plugins/sqflite/ios`) - - tencent_im_sdk_plugin (from `.symlinks/plugins/tencent_im_sdk_plugin/ios`) + - tencent_cloud_chat_sdk (from `.symlinks/plugins/tencent_cloud_chat_sdk/ios`) + - tencent_cloud_uikit_core (from `.symlinks/plugins/tencent_cloud_uikit_core/ios`) + - tencent_open_file (from `.symlinks/plugins/tencent_open_file/ios`) + - url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`) - video_player_avfoundation (from `.symlinks/plugins/video_player_avfoundation/ios`) - - video_thumbnail (from `.symlinks/plugins/video_thumbnail/ios`) - wakelock (from `.symlinks/plugins/wakelock/ios`) SPEC REPOS: @@ -124,83 +161,104 @@ SPEC REPOS: - FMDB - HydraAsync - libwebp - - ReachabilitySwift + - Mantle + - ReactiveObjC - SDWebImage + - SDWebImageWebPCoder - SwiftyGif - Toast + - TUICore - TXIMSDK_Plus_iOS EXTERNAL SOURCES: - camera: - :path: ".symlinks/plugins/camera/ios" - connectivity_plus: - :path: ".symlinks/plugins/connectivity_plus/ios" + audioplayers_darwin: + :path: ".symlinks/plugins/audioplayers_darwin/ios" + camera_avfoundation: + :path: ".symlinks/plugins/camera_avfoundation/ios" + disk_space: + :path: ".symlinks/plugins/disk_space/ios" + fc_native_video_thumbnail_for_us: + :path: ".symlinks/plugins/fc_native_video_thumbnail_for_us/ios" file_picker: :path: ".symlinks/plugins/file_picker/ios" Flutter: :path: Flutter - flutter_plugin_record: - :path: ".symlinks/plugins/flutter_plugin_record/ios" + flutter_image_compress: + :path: ".symlinks/plugins/flutter_image_compress/ios" + flutter_plugin_record_plus: + :path: ".symlinks/plugins/flutter_plugin_record_plus/ios" fluttertoast: :path: ".symlinks/plugins/fluttertoast/ios" image_gallery_saver: :path: ".symlinks/plugins/image_gallery_saver/ios" image_picker_ios: :path: ".symlinks/plugins/image_picker_ios/ios" - open_file: - :path: ".symlinks/plugins/open_file/ios" package_info_plus: :path: ".symlinks/plugins/package_info_plus/ios" - path_provider_ios: - :path: ".symlinks/plugins/path_provider_ios/ios" - permission_handler: - :path: ".symlinks/plugins/permission_handler/ios" + pasteboard: + :path: ".symlinks/plugins/pasteboard/ios" + path_provider_foundation: + :path: ".symlinks/plugins/path_provider_foundation/ios" + permission_handler_apple: + :path: ".symlinks/plugins/permission_handler_apple/ios" photo_manager: :path: ".symlinks/plugins/photo_manager/ios" - shared_preferences_ios: - :path: ".symlinks/plugins/shared_preferences_ios/ios" + shared_preferences_foundation: + :path: ".symlinks/plugins/shared_preferences_foundation/ios" sqflite: :path: ".symlinks/plugins/sqflite/ios" - tencent_im_sdk_plugin: - :path: ".symlinks/plugins/tencent_im_sdk_plugin/ios" + tencent_cloud_chat_sdk: + :path: ".symlinks/plugins/tencent_cloud_chat_sdk/ios" + tencent_cloud_uikit_core: + :path: ".symlinks/plugins/tencent_cloud_uikit_core/ios" + tencent_open_file: + :path: ".symlinks/plugins/tencent_open_file/ios" + url_launcher_ios: + :path: ".symlinks/plugins/url_launcher_ios/ios" video_player_avfoundation: :path: ".symlinks/plugins/video_player_avfoundation/ios" - video_thumbnail: - :path: ".symlinks/plugins/video_thumbnail/ios" wakelock: :path: ".symlinks/plugins/wakelock/ios" SPEC CHECKSUMS: - camera: 9993f92f2c793e87b65e35f3a23c70582afb05b1 - connectivity_plus: 413a8857dd5d9f1c399a39130850d02fe0feaf7e - DKImagePickerController: 72fd378f244cef3d27288e0aebf217a4467e4012 + audioplayers_darwin: 877d9a4d06331c5c374595e46e16453ac7eafa40 + camera_avfoundation: 07c77549ea54ad95d8581be86617c094a46280d9 + disk_space: e94d34bbdf77954adfb39e60bde9cc5c7233eda6 + DKImagePickerController: b512c28220a2b8ac7419f21c491fc8534b7601ac DKPhotoGallery: fdfad5125a9fdda9cc57df834d49df790dbb4179 - file_picker: 3e6c3790de664ccf9b882732d9db5eaf6b8d4eb1 - Flutter: 50d75fe2f02b26cc09d224853bb45737f8b3214a - flutter_plugin_record: 562ded56f3a109d769e72c3ef52ef20d835493d4 - fluttertoast: 16fbe6039d06a763f3533670197d01fc73459037 + fc_native_video_thumbnail_for_us: 69559e6500bff0f6340f044ec0847366fa6f6233 + file_picker: 817ab1d8cd2da9d2da412a417162deee3500fc95 + Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854 + flutter_image_compress: 5a5e9aee05b6553048b8df1c3bc456d0afaac433 + flutter_plugin_record_plus: 79b8e13ee7ed9a94f6c067018653599528cee1fc + fluttertoast: eb263d302cc92e04176c053d2385237e9f43fad0 FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a HydraAsync: 8d589bd725b0224f899afafc9a396327405f8063 image_gallery_saver: 259eab68fb271cfd57d599904f7acdc7832e7ef2 image_picker_ios: b786a5dcf033a8336a657191401bfdf12017dabb - libwebp: 98a37e597e40bfdb4c911fc98f2c53d0b12d05fc - open_file: 02eb5cb6b21264bd3a696876f5afbfb7ca4f4b7d + libwebp: f62cb61d0a484ba548448a4bd52aabf150ff6eef + Mantle: c5aa8794a29a022dfbbfc9799af95f477a69b62d package_info_plus: 6c92f08e1f853dc01228d6f553146438dafcd14e - path_provider_ios: 14f3d2fd28c4fdb42f44e0f751d12861c43cee02 - permission_handler: ccb20a9fad0ee9b1314a52b70b76b473c5f8dab0 + pasteboard: 982969ebaa7c78af3e6cc7761e8f5e77565d9ce0 + path_provider_foundation: 37748e03f12783f9de2cb2c4eadfaa25fe6d4852 + permission_handler_apple: 44366e37eaf29454a1e7b1b7d736c2cceaeb17ce photo_manager: 4f6810b7dfc4feb03b461ac1a70dacf91fba7604 - ReachabilitySwift: 985039c6f7b23a1da463388634119492ff86c825 - SDWebImage: 0905f1b7760fc8ac4198cae0036600d67478751e - shared_preferences_ios: 548a61f8053b9b8a49ac19c1ffbc8b92c50d68ad + ReactiveObjC: 011caa393aa0383245f2dcf9bf02e86b80b36040 + SDWebImage: fd7e1a22f00303e058058278639bf6196ee431fe + SDWebImageWebPCoder: 295a6573c512f54ad2dd58098e64e17dcf008499 + shared_preferences_foundation: 297b3ebca31b34ec92be11acd7fb0ba932c822ca sqflite: 6d358c025f5b867b29ed92fc697fd34924e11904 - SwiftyGif: 6c3eafd0ce693cad58bb63d2b2fb9bacb8552780 - tencent_im_sdk_plugin: c68993c62fd0198cd47132a055a06c9cef33b8e3 + SwiftyGif: 93a1cc87bf3a51916001cf8f3d63835fb64c819f + tencent_cloud_chat_sdk: 17f2ddd7de43495312603e7c9dac04d76352e246 + tencent_cloud_uikit_core: 2c4ccb41c33b45b5c69750b9774fa389fc20cdb2 + tencent_open_file: 1261db508715b8f43ef3b7e31c90824838038165 Toast: 91b396c56ee72a5790816f40d3a94dd357abc196 - TXIMSDK_Plus_iOS: 2b0e9440eacdb49f385c90a23ad6558013f0cac6 + TUICore: edc9e0911f6a04224620d7098ff9d7f4b1f0291a + TXIMSDK_Plus_iOS: 3edf95acc3dff794287ea858b5205ed6f4dd339f + url_launcher_ios: ae1517e5e344f5544fb090b079e11f399dfbe4d2 video_player_avfoundation: e489aac24ef5cf7af82702979ed16f2a5ef84cff - video_thumbnail: c4e2a3c539e247d4de13cd545344fd2d26ffafd1 wakelock: d0fc7c864128eac40eba1617cb5264d9c940b46f -PODFILE CHECKSUM: aafe91acc616949ddb318b77800a7f51bffa2a4c +PODFILE CHECKSUM: 7368163408c647b7eb699d0d788ba6718e18fb8d -COCOAPODS: 1.11.3 +COCOAPODS: 1.12.0 diff --git a/example/lib/TIMUIKitChatExample.dart b/example/lib/TIMUIKitChatExample.dart index baeed5c..d82bb9f 100644 --- a/example/lib/TIMUIKitChatExample.dart +++ b/example/lib/TIMUIKitChatExample.dart @@ -3,7 +3,6 @@ import 'package:example/TIMUIKitGroupProfileExample.dart'; import 'package:example/TIMUIKitProfileExample.dart'; import 'package:flutter/material.dart'; -import 'package:tencent_cloud_chat_uikit/business_logic/view_models/tui_chat_global_model.dart'; import 'package:tencent_cloud_chat_uikit/tencent_cloud_chat_uikit.dart'; class TIMUIKitChatExample extends StatelessWidget { @@ -12,21 +11,60 @@ class TIMUIKitChatExample extends StatelessWidget { const TIMUIKitChatExample({Key? key, this.selectedConversation}) : super(key: key); - String? _getConversationID() { - if(selectedConversation != null){ - return selectedConversation!.type == 1 - ? selectedConversation!.userID - : selectedConversation!.groupID; - } - return null; + Widget renderCustomStickerPanel({ + sendTextMessage, + sendFaceMessage, + deleteText, + addCustomEmojiText, + addText, + List defaultCustomEmojiStickerList = const [], + double? height, + double? width + }) { + final defaultEmojiList = + defaultCustomEmojiStickerList.map((customEmojiPackage) { + return CustomStickerPackage( + name: customEmojiPackage.name, + baseUrl: "assets/custom_face_resource/${customEmojiPackage.name}", + isEmoji: customEmojiPackage.isEmoji, + isDefaultEmoji: true, + stickerList: customEmojiPackage.list + .asMap() + .keys + .map((idx) => + CustomSticker(index: idx, name: customEmojiPackage.list[idx])) + .toList(), + menuItem: CustomSticker( + index: 0, + name: customEmojiPackage.icon, + )); + }).toList(); + return StickerPanel( + sendTextMsg: sendTextMessage, + sendFaceMsg: (index, data) => + sendFaceMessage(index + 1, (data.split("/")[3]).split("@")[0]), + deleteText: deleteText, + addText: addText, + addCustomEmojiText: addCustomEmojiText, + customStickerPackageList: [ + ...defaultEmojiList, + ]); } @override Widget build(BuildContext context) { return TIMUIKitChat( + conversation: selectedConversation ?? + V2TimConversation( + conversationID: "c2c_10040818", + userID: "10040818", + showName: "Test Chat", + type: 1), + customStickerPanel: renderCustomStickerPanel, config: const TIMUIKitChatConfig( // 仅供演示,非全部配置项,实际使用中,可只传和默认项不同的参数,无需传入所有开关 isAllowClickAvatar: true, + isUseDefaultEmoji: true, isAllowLongPressMessage: true, isShowReadingStatus: true, isShowGroupReadingStatus: true, @@ -38,12 +76,6 @@ class TIMUIKitChatExample extends StatelessWidget { GroupReceiptAllowType.public ], ), - conversation: selectedConversation ?? - V2TimConversation( - conversationID: "c2c_10040818", - userID: "10040818", - showName: "Test Chat", - type: 1), appBarConfig: AppBar( actions: [ IconButton( diff --git a/example/lib/TIMUIKitSearchExample.dart b/example/lib/TIMUIKitSearchExample.dart index d547e53..078e6ae 100644 --- a/example/lib/TIMUIKitSearchExample.dart +++ b/example/lib/TIMUIKitSearchExample.dart @@ -10,8 +10,6 @@ class TIMUIKitSearchExample extends StatelessWidget { Widget build(BuildContext context) { return TIMUIKitSearch( onTapConversation: (conv, message) { - print(conv.toJson()); - print(message!.toJson()); }, onEnterConversation: (V2TimConversation conversation, String keyword) {}, ); diff --git a/example/lib/main.dart b/example/lib/main.dart index 10fee08..eb814ac 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -39,7 +39,7 @@ class MyApp extends StatelessWidget { // is not restarted. primarySwatch: Colors.blue, ), - home: const MyHomePage(title: 'Tencent IM UIKit'), + home: const MyHomePage(title: 'Tencent Cloud Chat UIKit'), ); } } @@ -67,7 +67,7 @@ class _MyHomePageState extends State { } String getUserID() { - return const String.fromEnvironment('LOGINUSERID', defaultValue: ""); + return const String.fromEnvironment('LOGINUSERID', defaultValue: "10045363"); } String getSecret() { diff --git a/example/linux/.gitignore b/example/linux/.gitignore new file mode 100644 index 0000000..d3896c9 --- /dev/null +++ b/example/linux/.gitignore @@ -0,0 +1 @@ +flutter/ephemeral diff --git a/example/linux/CMakeLists.txt b/example/linux/CMakeLists.txt new file mode 100644 index 0000000..74c66dd --- /dev/null +++ b/example/linux/CMakeLists.txt @@ -0,0 +1,138 @@ +# Project-level configuration. +cmake_minimum_required(VERSION 3.10) +project(runner LANGUAGES CXX) + +# The name of the executable created for the application. Change this to change +# the on-disk name of your application. +set(BINARY_NAME "example") +# The unique GTK application identifier for this application. See: +# https://wiki.gnome.org/HowDoI/ChooseApplicationID +set(APPLICATION_ID "com.example.example") + +# Explicitly opt in to modern CMake behaviors to avoid warnings with recent +# versions of CMake. +cmake_policy(SET CMP0063 NEW) + +# Load bundled libraries from the lib/ directory relative to the binary. +set(CMAKE_INSTALL_RPATH "$ORIGIN/lib") + +# Root filesystem for cross-building. +if(FLUTTER_TARGET_PLATFORM_SYSROOT) + set(CMAKE_SYSROOT ${FLUTTER_TARGET_PLATFORM_SYSROOT}) + set(CMAKE_FIND_ROOT_PATH ${CMAKE_SYSROOT}) + set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) + set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY) + set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) + set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) +endif() + +# Define build configuration options. +if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) + set(CMAKE_BUILD_TYPE "Debug" CACHE + STRING "Flutter build mode" FORCE) + set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS + "Debug" "Profile" "Release") +endif() + +# Compilation settings that should be applied to most targets. +# +# Be cautious about adding new options here, as plugins use this function by +# default. In most cases, you should add new options to specific targets instead +# of modifying this function. +function(APPLY_STANDARD_SETTINGS TARGET) + target_compile_features(${TARGET} PUBLIC cxx_std_14) + target_compile_options(${TARGET} PRIVATE -Wall -Werror) + target_compile_options(${TARGET} PRIVATE "$<$>:-O3>") + target_compile_definitions(${TARGET} PRIVATE "$<$>:NDEBUG>") +endfunction() + +# Flutter library and tool build rules. +set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") +add_subdirectory(${FLUTTER_MANAGED_DIR}) + +# System-level dependencies. +find_package(PkgConfig REQUIRED) +pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) + +add_definitions(-DAPPLICATION_ID="${APPLICATION_ID}") + +# Define the application target. To change its name, change BINARY_NAME above, +# not the value here, or `flutter run` will no longer work. +# +# Any new source files that you add to the application should be added here. +add_executable(${BINARY_NAME} + "main.cc" + "my_application.cc" + "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" +) + +# Apply the standard set of build settings. This can be removed for applications +# that need different build settings. +apply_standard_settings(${BINARY_NAME}) + +# Add dependency libraries. Add any application-specific dependencies here. +target_link_libraries(${BINARY_NAME} PRIVATE flutter) +target_link_libraries(${BINARY_NAME} PRIVATE PkgConfig::GTK) + +# Run the Flutter tool portions of the build. This must not be removed. +add_dependencies(${BINARY_NAME} flutter_assemble) + +# Only the install-generated bundle's copy of the executable will launch +# correctly, since the resources must in the right relative locations. To avoid +# people trying to run the unbundled copy, put it in a subdirectory instead of +# the default top-level location. +set_target_properties(${BINARY_NAME} + PROPERTIES + RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/intermediates_do_not_run" +) + +# Generated plugin build rules, which manage building the plugins and adding +# them to the application. +include(flutter/generated_plugins.cmake) + + +# === Installation === +# By default, "installing" just makes a relocatable bundle in the build +# directory. +set(BUILD_BUNDLE_DIR "${PROJECT_BINARY_DIR}/bundle") +if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) + set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) +endif() + +# Start with a clean build bundle directory every time. +install(CODE " + file(REMOVE_RECURSE \"${BUILD_BUNDLE_DIR}/\") + " COMPONENT Runtime) + +set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") +set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}/lib") + +install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +foreach(bundled_library ${PLUGIN_BUNDLED_LIBRARIES}) + install(FILES "${bundled_library}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endforeach(bundled_library) + +# Fully re-copy the assets directory on each build to avoid having stale files +# from a previous install. +set(FLUTTER_ASSET_DIR_NAME "flutter_assets") +install(CODE " + file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\") + " COMPONENT Runtime) +install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}" + DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) + +# Install the AOT library on non-Debug builds only. +if(NOT CMAKE_BUILD_TYPE MATCHES "Debug") + install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endif() diff --git a/example/linux/flutter/CMakeLists.txt b/example/linux/flutter/CMakeLists.txt new file mode 100644 index 0000000..d5bd016 --- /dev/null +++ b/example/linux/flutter/CMakeLists.txt @@ -0,0 +1,88 @@ +# This file controls Flutter-level build steps. It should not be edited. +cmake_minimum_required(VERSION 3.10) + +set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") + +# Configuration provided via flutter tool. +include(${EPHEMERAL_DIR}/generated_config.cmake) + +# TODO: Move the rest of this into files in ephemeral. See +# https://github.com/flutter/flutter/issues/57146. + +# Serves the same purpose as list(TRANSFORM ... PREPEND ...), +# which isn't available in 3.10. +function(list_prepend LIST_NAME PREFIX) + set(NEW_LIST "") + foreach(element ${${LIST_NAME}}) + list(APPEND NEW_LIST "${PREFIX}${element}") + endforeach(element) + set(${LIST_NAME} "${NEW_LIST}" PARENT_SCOPE) +endfunction() + +# === Flutter Library === +# System-level dependencies. +find_package(PkgConfig REQUIRED) +pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) +pkg_check_modules(GLIB REQUIRED IMPORTED_TARGET glib-2.0) +pkg_check_modules(GIO REQUIRED IMPORTED_TARGET gio-2.0) + +set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/libflutter_linux_gtk.so") + +# Published to parent scope for install step. +set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) +set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) +set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) +set(AOT_LIBRARY "${PROJECT_DIR}/build/lib/libapp.so" PARENT_SCOPE) + +list(APPEND FLUTTER_LIBRARY_HEADERS + "fl_basic_message_channel.h" + "fl_binary_codec.h" + "fl_binary_messenger.h" + "fl_dart_project.h" + "fl_engine.h" + "fl_json_message_codec.h" + "fl_json_method_codec.h" + "fl_message_codec.h" + "fl_method_call.h" + "fl_method_channel.h" + "fl_method_codec.h" + "fl_method_response.h" + "fl_plugin_registrar.h" + "fl_plugin_registry.h" + "fl_standard_message_codec.h" + "fl_standard_method_codec.h" + "fl_string_codec.h" + "fl_value.h" + "fl_view.h" + "flutter_linux.h" +) +list_prepend(FLUTTER_LIBRARY_HEADERS "${EPHEMERAL_DIR}/flutter_linux/") +add_library(flutter INTERFACE) +target_include_directories(flutter INTERFACE + "${EPHEMERAL_DIR}" +) +target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}") +target_link_libraries(flutter INTERFACE + PkgConfig::GTK + PkgConfig::GLIB + PkgConfig::GIO +) +add_dependencies(flutter flutter_assemble) + +# === Flutter tool backend === +# _phony_ is a non-existent file to force this command to run every time, +# since currently there's no way to get a full input/output list from the +# flutter tool. +add_custom_command( + OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} + ${CMAKE_CURRENT_BINARY_DIR}/_phony_ + COMMAND ${CMAKE_COMMAND} -E env + ${FLUTTER_TOOL_ENVIRONMENT} + "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.sh" + ${FLUTTER_TARGET_PLATFORM} ${CMAKE_BUILD_TYPE} + VERBATIM +) +add_custom_target(flutter_assemble DEPENDS + "${FLUTTER_LIBRARY}" + ${FLUTTER_LIBRARY_HEADERS} +) diff --git a/example/linux/flutter/generated_plugin_registrant.cc b/example/linux/flutter/generated_plugin_registrant.cc new file mode 100644 index 0000000..2e68889 --- /dev/null +++ b/example/linux/flutter/generated_plugin_registrant.cc @@ -0,0 +1,27 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#include "generated_plugin_registrant.h" + +#include +#include +#include +#include + +void fl_register_plugins(FlPluginRegistry* registry) { + g_autoptr(FlPluginRegistrar) audioplayers_linux_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "AudioplayersLinuxPlugin"); + audioplayers_linux_plugin_register_with_registrar(audioplayers_linux_registrar); + g_autoptr(FlPluginRegistrar) desktop_drop_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "DesktopDropPlugin"); + desktop_drop_plugin_register_with_registrar(desktop_drop_registrar); + g_autoptr(FlPluginRegistrar) pasteboard_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "PasteboardPlugin"); + pasteboard_plugin_register_with_registrar(pasteboard_registrar); + g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin"); + url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar); +} diff --git a/example/linux/flutter/generated_plugin_registrant.h b/example/linux/flutter/generated_plugin_registrant.h new file mode 100644 index 0000000..e0f0a47 --- /dev/null +++ b/example/linux/flutter/generated_plugin_registrant.h @@ -0,0 +1,15 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#ifndef GENERATED_PLUGIN_REGISTRANT_ +#define GENERATED_PLUGIN_REGISTRANT_ + +#include + +// Registers Flutter plugins. +void fl_register_plugins(FlPluginRegistry* registry); + +#endif // GENERATED_PLUGIN_REGISTRANT_ diff --git a/example/linux/flutter/generated_plugins.cmake b/example/linux/flutter/generated_plugins.cmake new file mode 100644 index 0000000..fa2eba0 --- /dev/null +++ b/example/linux/flutter/generated_plugins.cmake @@ -0,0 +1,27 @@ +# +# Generated file, do not edit. +# + +list(APPEND FLUTTER_PLUGIN_LIST + audioplayers_linux + desktop_drop + pasteboard + url_launcher_linux +) + +list(APPEND FLUTTER_FFI_PLUGIN_LIST +) + +set(PLUGIN_BUNDLED_LIBRARIES) + +foreach(plugin ${FLUTTER_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/linux plugins/${plugin}) + target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) + list(APPEND PLUGIN_BUNDLED_LIBRARIES $) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) +endforeach(plugin) + +foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/linux plugins/${ffi_plugin}) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) +endforeach(ffi_plugin) diff --git a/example/linux/main.cc b/example/linux/main.cc new file mode 100644 index 0000000..e7c5c54 --- /dev/null +++ b/example/linux/main.cc @@ -0,0 +1,6 @@ +#include "my_application.h" + +int main(int argc, char** argv) { + g_autoptr(MyApplication) app = my_application_new(); + return g_application_run(G_APPLICATION(app), argc, argv); +} diff --git a/example/linux/my_application.cc b/example/linux/my_application.cc new file mode 100644 index 0000000..0ba8f43 --- /dev/null +++ b/example/linux/my_application.cc @@ -0,0 +1,104 @@ +#include "my_application.h" + +#include +#ifdef GDK_WINDOWING_X11 +#include +#endif + +#include "flutter/generated_plugin_registrant.h" + +struct _MyApplication { + GtkApplication parent_instance; + char** dart_entrypoint_arguments; +}; + +G_DEFINE_TYPE(MyApplication, my_application, GTK_TYPE_APPLICATION) + +// Implements GApplication::activate. +static void my_application_activate(GApplication* application) { + MyApplication* self = MY_APPLICATION(application); + GtkWindow* window = + GTK_WINDOW(gtk_application_window_new(GTK_APPLICATION(application))); + + // Use a header bar when running in GNOME as this is the common style used + // by applications and is the setup most users will be using (e.g. Ubuntu + // desktop). + // If running on X and not using GNOME then just use a traditional title bar + // in case the window manager does more exotic layout, e.g. tiling. + // If running on Wayland assume the header bar will work (may need changing + // if future cases occur). + gboolean use_header_bar = TRUE; +#ifdef GDK_WINDOWING_X11 + GdkScreen* screen = gtk_window_get_screen(window); + if (GDK_IS_X11_SCREEN(screen)) { + const gchar* wm_name = gdk_x11_screen_get_window_manager_name(screen); + if (g_strcmp0(wm_name, "GNOME Shell") != 0) { + use_header_bar = FALSE; + } + } +#endif + if (use_header_bar) { + GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new()); + gtk_widget_show(GTK_WIDGET(header_bar)); + gtk_header_bar_set_title(header_bar, "example"); + gtk_header_bar_set_show_close_button(header_bar, TRUE); + gtk_window_set_titlebar(window, GTK_WIDGET(header_bar)); + } else { + gtk_window_set_title(window, "example"); + } + + gtk_window_set_default_size(window, 1280, 720); + gtk_widget_show(GTK_WIDGET(window)); + + g_autoptr(FlDartProject) project = fl_dart_project_new(); + fl_dart_project_set_dart_entrypoint_arguments(project, self->dart_entrypoint_arguments); + + FlView* view = fl_view_new(project); + gtk_widget_show(GTK_WIDGET(view)); + gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(view)); + + fl_register_plugins(FL_PLUGIN_REGISTRY(view)); + + gtk_widget_grab_focus(GTK_WIDGET(view)); +} + +// Implements GApplication::local_command_line. +static gboolean my_application_local_command_line(GApplication* application, gchar*** arguments, int* exit_status) { + MyApplication* self = MY_APPLICATION(application); + // Strip out the first argument as it is the binary name. + self->dart_entrypoint_arguments = g_strdupv(*arguments + 1); + + g_autoptr(GError) error = nullptr; + if (!g_application_register(application, nullptr, &error)) { + g_warning("Failed to register: %s", error->message); + *exit_status = 1; + return TRUE; + } + + g_application_activate(application); + *exit_status = 0; + + return TRUE; +} + +// Implements GObject::dispose. +static void my_application_dispose(GObject* object) { + MyApplication* self = MY_APPLICATION(object); + g_clear_pointer(&self->dart_entrypoint_arguments, g_strfreev); + G_OBJECT_CLASS(my_application_parent_class)->dispose(object); +} + +static void my_application_class_init(MyApplicationClass* klass) { + G_APPLICATION_CLASS(klass)->activate = my_application_activate; + G_APPLICATION_CLASS(klass)->local_command_line = my_application_local_command_line; + G_OBJECT_CLASS(klass)->dispose = my_application_dispose; +} + +static void my_application_init(MyApplication* self) {} + +MyApplication* my_application_new() { + return MY_APPLICATION(g_object_new(my_application_get_type(), + "application-id", APPLICATION_ID, + "flags", G_APPLICATION_NON_UNIQUE, + nullptr)); +} diff --git a/example/linux/my_application.h b/example/linux/my_application.h new file mode 100644 index 0000000..72271d5 --- /dev/null +++ b/example/linux/my_application.h @@ -0,0 +1,18 @@ +#ifndef FLUTTER_MY_APPLICATION_H_ +#define FLUTTER_MY_APPLICATION_H_ + +#include + +G_DECLARE_FINAL_TYPE(MyApplication, my_application, MY, APPLICATION, + GtkApplication) + +/** + * my_application_new: + * + * Creates a new Flutter-based application. + * + * Returns: a new #MyApplication. + */ +MyApplication* my_application_new(); + +#endif // FLUTTER_MY_APPLICATION_H_ diff --git a/example/macos/.gitignore b/example/macos/.gitignore new file mode 100644 index 0000000..746adbb --- /dev/null +++ b/example/macos/.gitignore @@ -0,0 +1,7 @@ +# Flutter-related +**/Flutter/ephemeral/ +**/Pods/ + +# Xcode-related +**/dgph +**/xcuserdata/ diff --git a/example/macos/Flutter/Flutter-Debug.xcconfig b/example/macos/Flutter/Flutter-Debug.xcconfig new file mode 100644 index 0000000..4b81f9b --- /dev/null +++ b/example/macos/Flutter/Flutter-Debug.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" +#include "ephemeral/Flutter-Generated.xcconfig" diff --git a/example/macos/Flutter/Flutter-Release.xcconfig b/example/macos/Flutter/Flutter-Release.xcconfig new file mode 100644 index 0000000..5caa9d1 --- /dev/null +++ b/example/macos/Flutter/Flutter-Release.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" +#include "ephemeral/Flutter-Generated.xcconfig" diff --git a/example/macos/Flutter/GeneratedPluginRegistrant.swift b/example/macos/Flutter/GeneratedPluginRegistrant.swift new file mode 100644 index 0000000..e63f903 --- /dev/null +++ b/example/macos/Flutter/GeneratedPluginRegistrant.swift @@ -0,0 +1,34 @@ +// +// Generated file. Do not edit. +// + +import FlutterMacOS +import Foundation + +import audioplayers_darwin +import desktop_drop +import device_info_plus_macos +import fc_native_video_thumbnail_for_us +import package_info_plus_macos +import pasteboard +import path_provider_foundation +import photo_manager +import shared_preferences_foundation +import sqflite +import url_launcher_macos +import wakelock_macos + +func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { + AudioplayersDarwinPlugin.register(with: registry.registrar(forPlugin: "AudioplayersDarwinPlugin")) + DesktopDropPlugin.register(with: registry.registrar(forPlugin: "DesktopDropPlugin")) + DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin")) + FcNativeVideoThumbnailPlugin.register(with: registry.registrar(forPlugin: "FcNativeVideoThumbnailPlugin")) + FLTPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FLTPackageInfoPlusPlugin")) + PasteboardPlugin.register(with: registry.registrar(forPlugin: "PasteboardPlugin")) + PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) + PhotoManagerPlugin.register(with: registry.registrar(forPlugin: "PhotoManagerPlugin")) + SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) + SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin")) + UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) + WakelockMacosPlugin.register(with: registry.registrar(forPlugin: "WakelockMacosPlugin")) +} diff --git a/example/macos/Podfile b/example/macos/Podfile new file mode 100644 index 0000000..9ec46f8 --- /dev/null +++ b/example/macos/Podfile @@ -0,0 +1,40 @@ +platform :osx, '10.15' + +# CocoaPods analytics sends network stats synchronously affecting flutter build latency. +ENV['COCOAPODS_DISABLE_STATS'] = 'true' + +project 'Runner', { + 'Debug' => :debug, + 'Profile' => :release, + 'Release' => :release, +} + +def flutter_root + generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'ephemeral', 'Flutter-Generated.xcconfig'), __FILE__) + unless File.exist?(generated_xcode_build_settings_path) + raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure \"flutter pub get\" is executed first" + end + + File.foreach(generated_xcode_build_settings_path) do |line| + matches = line.match(/FLUTTER_ROOT\=(.*)/) + return matches[1].strip if matches + end + raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Flutter-Generated.xcconfig, then run \"flutter pub get\"" +end + +require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) + +flutter_macos_podfile_setup + +target 'Runner' do + use_frameworks! + use_modular_headers! + + flutter_install_all_macos_pods File.dirname(File.realpath(__FILE__)) +end + +post_install do |installer| + installer.pods_project.targets.each do |target| + flutter_additional_macos_build_settings(target) + end +end diff --git a/example/macos/Podfile.lock b/example/macos/Podfile.lock new file mode 100644 index 0000000..4b372e6 --- /dev/null +++ b/example/macos/Podfile.lock @@ -0,0 +1,68 @@ +PODS: + - FlutterMacOS (1.0.0) + - FMDB (2.7.5): + - FMDB/standard (= 2.7.5) + - FMDB/standard (2.7.5) + - package_info_plus_macos (0.0.1): + - FlutterMacOS + - path_provider_macos (0.0.1): + - FlutterMacOS + - photo_manager (2.0.0): + - Flutter + - FlutterMacOS + - shared_preferences_macos (0.0.1): + - FlutterMacOS + - sqflite (0.0.2): + - FlutterMacOS + - FMDB (>= 2.7.5) + - url_launcher_macos (0.0.1): + - FlutterMacOS + - wakelock_macos (0.0.1): + - FlutterMacOS + +DEPENDENCIES: + - FlutterMacOS (from `Flutter/ephemeral`) + - package_info_plus_macos (from `Flutter/ephemeral/.symlinks/plugins/package_info_plus_macos/macos`) + - path_provider_macos (from `Flutter/ephemeral/.symlinks/plugins/path_provider_macos/macos`) + - photo_manager (from `Flutter/ephemeral/.symlinks/plugins/photo_manager/macos`) + - shared_preferences_macos (from `Flutter/ephemeral/.symlinks/plugins/shared_preferences_macos/macos`) + - sqflite (from `Flutter/ephemeral/.symlinks/plugins/sqflite/macos`) + - url_launcher_macos (from `Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos`) + - wakelock_macos (from `Flutter/ephemeral/.symlinks/plugins/wakelock_macos/macos`) + +SPEC REPOS: + trunk: + - FMDB + +EXTERNAL SOURCES: + FlutterMacOS: + :path: Flutter/ephemeral + package_info_plus_macos: + :path: Flutter/ephemeral/.symlinks/plugins/package_info_plus_macos/macos + path_provider_macos: + :path: Flutter/ephemeral/.symlinks/plugins/path_provider_macos/macos + photo_manager: + :path: Flutter/ephemeral/.symlinks/plugins/photo_manager/macos + shared_preferences_macos: + :path: Flutter/ephemeral/.symlinks/plugins/shared_preferences_macos/macos + sqflite: + :path: Flutter/ephemeral/.symlinks/plugins/sqflite/macos + url_launcher_macos: + :path: Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos + wakelock_macos: + :path: Flutter/ephemeral/.symlinks/plugins/wakelock_macos/macos + +SPEC CHECKSUMS: + FlutterMacOS: ae6af50a8ea7d6103d888583d46bd8328a7e9811 + FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a + package_info_plus_macos: f010621b07802a241d96d01876d6705f15e77c1c + path_provider_macos: 3c0c3b4b0d4a76d2bf989a913c2de869c5641a19 + photo_manager: 4f6810b7dfc4feb03b461ac1a70dacf91fba7604 + shared_preferences_macos: a64dc611287ed6cbe28fd1297898db1336975727 + sqflite: a5789cceda41d54d23f31d6de539d65bb14100ea + url_launcher_macos: 597e05b8e514239626bcf4a850fcf9ef5c856ec3 + wakelock_macos: bc3f2a9bd8d2e6c89fee1e1822e7ddac3bd004a9 + +PODFILE CHECKSUM: 0d3963a09fc94f580682bd88480486da345dc3f0 + +COCOAPODS: 1.11.3 diff --git a/example/macos/Runner.xcodeproj/project.pbxproj b/example/macos/Runner.xcodeproj/project.pbxproj new file mode 100644 index 0000000..1dad809 --- /dev/null +++ b/example/macos/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,632 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 51; + objects = { + +/* Begin PBXAggregateTarget section */ + 33CC111A2044C6BA0003C045 /* Flutter Assemble */ = { + isa = PBXAggregateTarget; + buildConfigurationList = 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */; + buildPhases = ( + 33CC111E2044C6BF0003C045 /* ShellScript */, + ); + dependencies = ( + ); + name = "Flutter Assemble"; + productName = FLX; + }; +/* End PBXAggregateTarget section */ + +/* Begin PBXBuildFile section */ + 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */; }; + 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC10F02044A3C60003C045 /* AppDelegate.swift */; }; + 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; }; + 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; }; + 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; }; + BCA39B6D459F9CBDE3F985BB /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6AB45B5BD0777F55845AF08A /* Pods_Runner.framework */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 33CC10E52044A3C60003C045 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 33CC111A2044C6BA0003C045; + remoteInfo = FLX; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 33CC110E2044A8840003C045 /* Bundle Framework */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Bundle Framework"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = ""; }; + 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = ""; }; + 33CC10ED2044A3C60003C045 /* example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = example.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = ""; }; + 33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = ""; }; + 33CC10F72044A3C60003C045 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = Info.plist; path = Runner/Info.plist; sourceTree = ""; }; + 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainFlutterWindow.swift; sourceTree = ""; }; + 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Debug.xcconfig"; sourceTree = ""; }; + 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Release.xcconfig"; sourceTree = ""; }; + 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = "Flutter-Generated.xcconfig"; path = "ephemeral/Flutter-Generated.xcconfig"; sourceTree = ""; }; + 33E51913231747F40026EE4D /* DebugProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugProfile.entitlements; sourceTree = ""; }; + 33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = ""; }; + 33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = ""; }; + 5E1960EFBF841FBF2ED9CB42 /* 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 = ""; }; + 613D89C488B7E8A9AABFE80A /* 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 = ""; }; + 6AB45B5BD0777F55845AF08A /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = ""; }; + 9055C8A896F8B9969E9A9A75 /* 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 = ""; }; + 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 33CC10EA2044A3C60003C045 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + BCA39B6D459F9CBDE3F985BB /* Pods_Runner.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 33BA886A226E78AF003329D5 /* Configs */ = { + isa = PBXGroup; + children = ( + 33E5194F232828860026EE4D /* AppInfo.xcconfig */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 333000ED22D3DE5D00554162 /* Warnings.xcconfig */, + ); + path = Configs; + sourceTree = ""; + }; + 33CC10E42044A3C60003C045 = { + isa = PBXGroup; + children = ( + 33FAB671232836740065AC1E /* Runner */, + 33CEB47122A05771004F2AC0 /* Flutter */, + 33CC10EE2044A3C60003C045 /* Products */, + D73912EC22F37F3D000D13A0 /* Frameworks */, + 35CB9EEBE37DA168A0AC063E /* Pods */, + ); + sourceTree = ""; + }; + 33CC10EE2044A3C60003C045 /* Products */ = { + isa = PBXGroup; + children = ( + 33CC10ED2044A3C60003C045 /* example.app */, + ); + name = Products; + sourceTree = ""; + }; + 33CC11242044D66E0003C045 /* Resources */ = { + isa = PBXGroup; + children = ( + 33CC10F22044A3C60003C045 /* Assets.xcassets */, + 33CC10F42044A3C60003C045 /* MainMenu.xib */, + 33CC10F72044A3C60003C045 /* Info.plist */, + ); + name = Resources; + path = ..; + sourceTree = ""; + }; + 33CEB47122A05771004F2AC0 /* Flutter */ = { + isa = PBXGroup; + children = ( + 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */, + 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */, + 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */, + 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */, + ); + path = Flutter; + sourceTree = ""; + }; + 33FAB671232836740065AC1E /* Runner */ = { + isa = PBXGroup; + children = ( + 33CC10F02044A3C60003C045 /* AppDelegate.swift */, + 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */, + 33E51913231747F40026EE4D /* DebugProfile.entitlements */, + 33E51914231749380026EE4D /* Release.entitlements */, + 33CC11242044D66E0003C045 /* Resources */, + 33BA886A226E78AF003329D5 /* Configs */, + ); + path = Runner; + sourceTree = ""; + }; + 35CB9EEBE37DA168A0AC063E /* Pods */ = { + isa = PBXGroup; + children = ( + 613D89C488B7E8A9AABFE80A /* Pods-Runner.debug.xcconfig */, + 9055C8A896F8B9969E9A9A75 /* Pods-Runner.release.xcconfig */, + 5E1960EFBF841FBF2ED9CB42 /* Pods-Runner.profile.xcconfig */, + ); + name = Pods; + path = Pods; + sourceTree = ""; + }; + D73912EC22F37F3D000D13A0 /* Frameworks */ = { + isa = PBXGroup; + children = ( + 6AB45B5BD0777F55845AF08A /* Pods_Runner.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 33CC10EC2044A3C60003C045 /* Runner */ = { + isa = PBXNativeTarget; + buildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */; + buildPhases = ( + DF30845ABEF3338382F8E5CB /* [CP] Check Pods Manifest.lock */, + 33CC10E92044A3C60003C045 /* Sources */, + 33CC10EA2044A3C60003C045 /* Frameworks */, + 33CC10EB2044A3C60003C045 /* Resources */, + 33CC110E2044A8840003C045 /* Bundle Framework */, + 3399D490228B24CF009A79C7 /* ShellScript */, + 2F97E070B313FE8332FEB41C /* [CP] Embed Pods Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + 33CC11202044C79F0003C045 /* PBXTargetDependency */, + ); + name = Runner; + productName = Runner; + productReference = 33CC10ED2044A3C60003C045 /* example.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 33CC10E52044A3C60003C045 /* Project object */ = { + isa = PBXProject; + attributes = { + LastSwiftUpdateCheck = 0920; + LastUpgradeCheck = 1300; + ORGANIZATIONNAME = ""; + TargetAttributes = { + 33CC10EC2044A3C60003C045 = { + CreatedOnToolsVersion = 9.2; + LastSwiftMigration = 1100; + ProvisioningStyle = Automatic; + SystemCapabilities = { + com.apple.Sandbox = { + enabled = 1; + }; + }; + }; + 33CC111A2044C6BA0003C045 = { + CreatedOnToolsVersion = 9.2; + ProvisioningStyle = Manual; + }; + }; + }; + buildConfigurationList = 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 33CC10E42044A3C60003C045; + productRefGroup = 33CC10EE2044A3C60003C045 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 33CC10EC2044A3C60003C045 /* Runner */, + 33CC111A2044C6BA0003C045 /* Flutter Assemble */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 33CC10EB2044A3C60003C045 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */, + 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 2F97E070B313FE8332FEB41C /* [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; + }; + 3399D490228B24CF009A79C7 /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "echo \"$PRODUCT_NAME.app\" > \"$PROJECT_DIR\"/Flutter/ephemeral/.app_filename && \"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh embed\n"; + }; + 33CC111E2044C6BF0003C045 /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + Flutter/ephemeral/FlutterInputs.xcfilelist, + ); + inputPaths = ( + Flutter/ephemeral/tripwire, + ); + outputFileListPaths = ( + Flutter/ephemeral/FlutterOutputs.xcfilelist, + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh && touch Flutter/ephemeral/tripwire"; + }; + DF30845ABEF3338382F8E5CB /* [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; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 33CC10E92044A3C60003C045 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */, + 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */, + 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 33CC11202044C79F0003C045 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 33CC111A2044C6BA0003C045 /* Flutter Assemble */; + targetProxy = 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + 33CC10F42044A3C60003C045 /* MainMenu.xib */ = { + isa = PBXVariantGroup; + children = ( + 33CC10F52044A3C60003C045 /* Base */, + ); + name = MainMenu.xib; + path = Runner; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 338D0CE9231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.11; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + }; + name = Profile; + }; + 338D0CEA231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_VERSION = 5.0; + }; + name = Profile; + }; + 338D0CEB231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Manual; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Profile; + }; + 33CC10F92044A3C60003C045 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.11; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = macosx; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + 33CC10FA2044A3C60003C045 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.11; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + }; + name = Release; + }; + 33CC10FC2044A3C60003C045 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + }; + name = Debug; + }; + 33CC10FD2044A3C60003C045 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/Release.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_VERSION = 5.0; + }; + name = Release; + }; + 33CC111C2044C6BA0003C045 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Manual; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Debug; + }; + 33CC111D2044C6BA0003C045 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC10F92044A3C60003C045 /* Debug */, + 33CC10FA2044A3C60003C045 /* Release */, + 338D0CE9231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC10FC2044A3C60003C045 /* Debug */, + 33CC10FD2044A3C60003C045 /* Release */, + 338D0CEA231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC111C2044C6BA0003C045 /* Debug */, + 33CC111D2044C6BA0003C045 /* Release */, + 338D0CEB231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 33CC10E52044A3C60003C045 /* Project object */; +} diff --git a/example/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/example/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/example/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 0000000..fb7259e --- /dev/null +++ b/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,87 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/example/macos/Runner.xcworkspace/contents.xcworkspacedata b/example/macos/Runner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..21a3cc1 --- /dev/null +++ b/example/macos/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/example/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/example/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/example/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/example/macos/Runner/AppDelegate.swift b/example/macos/Runner/AppDelegate.swift new file mode 100644 index 0000000..d53ef64 --- /dev/null +++ b/example/macos/Runner/AppDelegate.swift @@ -0,0 +1,9 @@ +import Cocoa +import FlutterMacOS + +@NSApplicationMain +class AppDelegate: FlutterAppDelegate { + override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { + return true + } +} diff --git a/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..a2ec33f --- /dev/null +++ b/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,68 @@ +{ + "images" : [ + { + "size" : "16x16", + "idiom" : "mac", + "filename" : "app_icon_16.png", + "scale" : "1x" + }, + { + "size" : "16x16", + "idiom" : "mac", + "filename" : "app_icon_32.png", + "scale" : "2x" + }, + { + "size" : "32x32", + "idiom" : "mac", + "filename" : "app_icon_32.png", + "scale" : "1x" + }, + { + "size" : "32x32", + "idiom" : "mac", + "filename" : "app_icon_64.png", + "scale" : "2x" + }, + { + "size" : "128x128", + "idiom" : "mac", + "filename" : "app_icon_128.png", + "scale" : "1x" + }, + { + "size" : "128x128", + "idiom" : "mac", + "filename" : "app_icon_256.png", + "scale" : "2x" + }, + { + "size" : "256x256", + "idiom" : "mac", + "filename" : "app_icon_256.png", + "scale" : "1x" + }, + { + "size" : "256x256", + "idiom" : "mac", + "filename" : "app_icon_512.png", + "scale" : "2x" + }, + { + "size" : "512x512", + "idiom" : "mac", + "filename" : "app_icon_512.png", + "scale" : "1x" + }, + { + "size" : "512x512", + "idiom" : "mac", + "filename" : "app_icon_1024.png", + "scale" : "2x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png b/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png new file mode 100644 index 0000000..82b6f9d Binary files /dev/null and b/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png differ diff --git a/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png b/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png new file mode 100644 index 0000000..13b35eb Binary files /dev/null and b/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png differ diff --git a/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png b/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png new file mode 100644 index 0000000..0a3f5fa Binary files /dev/null and b/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png differ diff --git a/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png b/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png new file mode 100644 index 0000000..bdb5722 Binary files /dev/null and b/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png differ diff --git a/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png b/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png new file mode 100644 index 0000000..f083318 Binary files /dev/null and b/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png differ diff --git a/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png b/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png new file mode 100644 index 0000000..326c0e7 Binary files /dev/null and b/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png differ diff --git a/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png b/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png new file mode 100644 index 0000000..2f1632c Binary files /dev/null and b/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png differ diff --git a/example/macos/Runner/Base.lproj/MainMenu.xib b/example/macos/Runner/Base.lproj/MainMenu.xib new file mode 100644 index 0000000..80e867a --- /dev/null +++ b/example/macos/Runner/Base.lproj/MainMenu.xib @@ -0,0 +1,343 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/example/macos/Runner/Configs/AppInfo.xcconfig b/example/macos/Runner/Configs/AppInfo.xcconfig new file mode 100644 index 0000000..8b42559 --- /dev/null +++ b/example/macos/Runner/Configs/AppInfo.xcconfig @@ -0,0 +1,14 @@ +// Application-level settings for the Runner target. +// +// This may be replaced with something auto-generated from metadata (e.g., pubspec.yaml) in the +// future. If not, the values below would default to using the project name when this becomes a +// 'flutter create' template. + +// The application's name. By default this is also the title of the Flutter window. +PRODUCT_NAME = example + +// The application's bundle identifier +PRODUCT_BUNDLE_IDENTIFIER = com.example.example + +// The copyright displayed in application information +PRODUCT_COPYRIGHT = Copyright © 2022 com.example. All rights reserved. diff --git a/example/macos/Runner/Configs/Debug.xcconfig b/example/macos/Runner/Configs/Debug.xcconfig new file mode 100644 index 0000000..36b0fd9 --- /dev/null +++ b/example/macos/Runner/Configs/Debug.xcconfig @@ -0,0 +1,2 @@ +#include "../../Flutter/Flutter-Debug.xcconfig" +#include "Warnings.xcconfig" diff --git a/example/macos/Runner/Configs/Release.xcconfig b/example/macos/Runner/Configs/Release.xcconfig new file mode 100644 index 0000000..dff4f49 --- /dev/null +++ b/example/macos/Runner/Configs/Release.xcconfig @@ -0,0 +1,2 @@ +#include "../../Flutter/Flutter-Release.xcconfig" +#include "Warnings.xcconfig" diff --git a/example/macos/Runner/Configs/Warnings.xcconfig b/example/macos/Runner/Configs/Warnings.xcconfig new file mode 100644 index 0000000..42bcbf4 --- /dev/null +++ b/example/macos/Runner/Configs/Warnings.xcconfig @@ -0,0 +1,13 @@ +WARNING_CFLAGS = -Wall -Wconditional-uninitialized -Wnullable-to-nonnull-conversion -Wmissing-method-return-type -Woverlength-strings +GCC_WARN_UNDECLARED_SELECTOR = YES +CLANG_UNDEFINED_BEHAVIOR_SANITIZER_NULLABILITY = YES +CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE +CLANG_WARN__DUPLICATE_METHOD_MATCH = YES +CLANG_WARN_PRAGMA_PACK = YES +CLANG_WARN_STRICT_PROTOTYPES = YES +CLANG_WARN_COMMA = YES +GCC_WARN_STRICT_SELECTOR_MATCH = YES +CLANG_WARN_OBJC_REPEATED_USE_OF_WEAK = YES +CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES +GCC_WARN_SHADOW = YES +CLANG_WARN_UNREACHABLE_CODE = YES diff --git a/example/macos/Runner/DebugProfile.entitlements b/example/macos/Runner/DebugProfile.entitlements new file mode 100644 index 0000000..9f56413 --- /dev/null +++ b/example/macos/Runner/DebugProfile.entitlements @@ -0,0 +1,12 @@ + + + + + com.apple.security.app-sandbox + + com.apple.security.cs.allow-jit + + com.apple.security.network.server + + + diff --git a/example/macos/Runner/Info.plist b/example/macos/Runner/Info.plist new file mode 100644 index 0000000..4789daa --- /dev/null +++ b/example/macos/Runner/Info.plist @@ -0,0 +1,32 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIconFile + + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSMinimumSystemVersion + $(MACOSX_DEPLOYMENT_TARGET) + NSHumanReadableCopyright + $(PRODUCT_COPYRIGHT) + NSMainNibFile + MainMenu + NSPrincipalClass + NSApplication + + diff --git a/example/macos/Runner/MainFlutterWindow.swift b/example/macos/Runner/MainFlutterWindow.swift new file mode 100644 index 0000000..2722837 --- /dev/null +++ b/example/macos/Runner/MainFlutterWindow.swift @@ -0,0 +1,15 @@ +import Cocoa +import FlutterMacOS + +class MainFlutterWindow: NSWindow { + override func awakeFromNib() { + let flutterViewController = FlutterViewController.init() + let windowFrame = self.frame + self.contentViewController = flutterViewController + self.setFrame(windowFrame, display: true) + + RegisterGeneratedPlugins(registry: flutterViewController) + + super.awakeFromNib() + } +} diff --git a/example/macos/Runner/Release.entitlements b/example/macos/Runner/Release.entitlements new file mode 100644 index 0000000..f7eba17 --- /dev/null +++ b/example/macos/Runner/Release.entitlements @@ -0,0 +1,10 @@ + + + + + com.apple.security.app-sandbox + + com.apple.security.network.client + + + diff --git a/example/pubspec.lock b/example/pubspec.lock index 8f6a22c..0b1b6c2 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -5,10 +5,10 @@ packages: dependency: transitive description: name: _fe_analyzer_shared - sha256: "569ddca58d535e601dd1584afa117710abc999d036c0cd2c51777fb257df78e8" + sha256: "0c80aeab9bc807ab10022cd3b2f4cf2ecdf231949dc1ddd9442406a003f19201" url: "https://pub.dev" source: hosted - version: "53.0.0" + version: "52.0.0" adaptive_action_sheet: dependency: transitive description: @@ -21,10 +21,10 @@ packages: dependency: transitive description: name: analyzer - sha256: "10927c4b7c7c88b1adbca278c3d5531db92e2f4b4abf04e2919a800af965f3f5" + sha256: cd8ee83568a77f3ae6b913a36093a1c9b1264e7cb7f834d9ddd2311dade9c1f4 url: "https://pub.dev" source: hosted - version: "5.5.0" + version: "5.4.0" archive: dependency: "direct main" description: @@ -37,10 +37,10 @@ packages: dependency: transitive description: name: args - sha256: "4cab82a83ffef80b262ddedf47a0a8e56ee6fbf7fe21e6e768b02792034dd440" + sha256: "139d809800a412ebb26a3892da228b2d0ba36f0ef5d9a82166e5e52ec8d61611" url: "https://pub.dev" source: hosted - version: "2.4.0" + version: "2.3.2" async: dependency: transitive description: @@ -49,14 +49,70 @@ packages: url: "https://pub.dev" source: hosted version: "2.10.0" - azlistview: + audioplayers: dependency: transitive description: - name: azlistview - sha256: "93e865f11777a271b439f0d6b00799c0797e9daeec2e082a2e01373809c4b90d" + name: audioplayers + sha256: "16451eab798b23ad9307aef6f9ca62bb8fb06542af8810eead0d236d3fd40a42" + url: "https://pub.dev" + source: hosted + version: "3.0.1" + audioplayers_android: + dependency: transitive + description: + name: audioplayers_android + sha256: b2c833e6f718b6b030454e329931229afafe9327fdb002874dd544dc8bf2484d url: "https://pub.dev" source: hosted version: "2.0.0" + audioplayers_darwin: + dependency: transitive + description: + name: audioplayers_darwin + sha256: e7a3c8759bf11ecfe4b20df338bf9f3d37c7719a5761c46a3833aba0ceeaacff + url: "https://pub.dev" + source: hosted + version: "3.0.1" + audioplayers_linux: + dependency: transitive + description: + name: audioplayers_linux + sha256: e95b65e1f4d4764601dac5e65f8d8186fc29401043ab020f1dacec483d708707 + url: "https://pub.dev" + source: hosted + version: "1.0.4" + audioplayers_platform_interface: + dependency: transitive + description: + name: audioplayers_platform_interface + sha256: "178581a44cb685fd798d2108111d2e98cca3400e30b9c3a05546f124fb37f600" + url: "https://pub.dev" + source: hosted + version: "4.0.0" + audioplayers_web: + dependency: transitive + description: + name: audioplayers_web + sha256: "859ba09be2a57e57a787273f18c8cf0d9b61383870c5ee4b5632fe9adbc37edf" + url: "https://pub.dev" + source: hosted + version: "2.2.0" + audioplayers_windows: + dependency: transitive + description: + name: audioplayers_windows + sha256: "622e01c4c357c2aaf1b956c3a0f89d97c3cb40315c03f16e3b6c2a31ff9c38bc" + url: "https://pub.dev" + source: hosted + version: "1.1.3" + azlistview_all_platforms: + dependency: transitive + description: + name: azlistview_all_platforms + sha256: "47ce2204863e0c3e481ca2a3813096d9818b153f1f677e839503e33d36e97993" + url: "https://pub.dev" + source: hosted + version: "2.1.2" boolean_selector: dependency: transitive description: @@ -109,10 +165,10 @@ packages: dependency: transitive description: name: camera_android - sha256: e491c836147f60dd8a54cf3895fd2960e13b21b78a9d15b563a1b6c70894f142 + sha256: "4cef01e8e78fe27c809a429bf74352ab94ab76b0c980e3ec708f1414614e3d9f" url: "https://pub.dev" source: hosted - version: "0.10.4" + version: "0.10.3" camera_avfoundation: dependency: transitive description: @@ -125,10 +181,10 @@ packages: dependency: transitive description: name: camera_platform_interface - sha256: b632be28e61d00a233f67d98ea90fd7041956f27a1c65500188ee459be60e15f + sha256: "0eedd642d905ca24f1c483fe9ea0d0e7287b86a402845c28d24df28cc7b0ee6e" url: "https://pub.dev" source: hosted - version: "2.4.0" + version: "2.3.4" camera_web: dependency: transitive description: @@ -225,6 +281,70 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.5" + desktop_drop: + dependency: transitive + description: + name: desktop_drop + sha256: "4ca4d960f4b11c032e9adfd2a0a8ac615bc3fddb4cbe73dcf840dd8077582186" + url: "https://pub.dev" + source: hosted + version: "0.4.1" + device_info_plus: + dependency: transitive + description: + name: device_info_plus + sha256: b809c4ed5f7fcdb325ccc70b80ad934677dc4e2aa414bf46859a42bfdfafcbb6 + url: "https://pub.dev" + source: hosted + version: "4.1.3" + device_info_plus_linux: + dependency: transitive + description: + name: device_info_plus_linux + sha256: "77a8b3c4af06bc46507f89304d9f49dfc64b4ae004b994532ed23b34adeae4b3" + url: "https://pub.dev" + source: hosted + version: "3.0.0" + device_info_plus_macos: + dependency: transitive + description: + name: device_info_plus_macos + sha256: "37961762fbd46d3620c7b69ca606671014db55fc1b7a11e696fd90ed2e8fe03d" + url: "https://pub.dev" + source: hosted + version: "3.0.0" + device_info_plus_platform_interface: + dependency: transitive + description: + name: device_info_plus_platform_interface + sha256: "83fdba24fcf6846d3b10f10dfdc8b6c6d7ada5f8ed21d62ea2909c2dfa043773" + url: "https://pub.dev" + source: hosted + version: "3.0.0" + device_info_plus_web: + dependency: transitive + description: + name: device_info_plus_web + sha256: "5890f6094df108181c7a29720bc23d0fd6159f17d82787fac093d1fefcaf6325" + url: "https://pub.dev" + source: hosted + version: "3.0.0" + device_info_plus_windows: + dependency: transitive + description: + name: device_info_plus_windows + sha256: "23a2874af0e23ee6e3a2a0ebcecec3a9da13241f2cb93a93a44c8764df123dd7" + url: "https://pub.dev" + source: hosted + version: "4.1.0" + diff_match_patch: + dependency: transitive + description: + name: diff_match_patch + sha256: "2efc9e6e8f449d0abe15be240e2c2a3bcd977c8d126cfd70598aee60af35c0a4" + url: "https://pub.dev" + source: hosted + version: "0.4.1" disk_space: dependency: transitive description: @@ -245,10 +365,10 @@ packages: dependency: transitive description: name: extended_image - sha256: a6b738d9b8d5513be72c545cc3e9c5c451fbee77c8db3cbec7c32ae85b82fb93 + sha256: "5854d0d05ee0c687d1852af9db05f15cfe058520fa56f417075705c5bce965d4" url: "https://pub.dev" source: hosted - version: "6.4.1" + version: "6.4.0" extended_image_library: dependency: transitive description: @@ -273,14 +393,22 @@ packages: url: "https://pub.dev" source: hosted version: "5.12.6" + fc_native_video_thumbnail_for_us: + dependency: transitive + description: + name: fc_native_video_thumbnail_for_us + sha256: db6fa2998195ef5eadac690ae58d6a909ddb5b0283ebbbf9ae4e0e8f99a54902 + url: "https://pub.dev" + source: hosted + version: "0.4.8+1" ffi: dependency: transitive description: name: ffi - sha256: "13a6ccf6a459a125b3fcdb6ec73bd5ff90822e071207c663bfd1f70062d51d18" + sha256: a38574032c5f1dd06c4aee541789906c12ccaab8ba01446e800d9c5b79c4a978 url: "https://pub.dev" source: hosted - version: "1.2.1" + version: "2.0.1" file: dependency: transitive description: @@ -293,10 +421,10 @@ packages: dependency: transitive description: name: file_picker - sha256: "704259669b5e9cb24e15c11cfcf02caf5f20d30901b3916d60b6d1c2d647035f" + sha256: b85eb92b175767fdaa0c543bf3b0d1f610fe966412ea72845fe5ba7801e763ff url: "https://pub.dev" source: hosted - version: "4.6.1" + version: "5.2.10" file_utils: dependency: transitive description: @@ -350,19 +478,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.4" - flutter_localizations: - dependency: transitive - description: flutter - source: sdk - version: "0.0.0" flutter_markdown: dependency: transitive description: name: flutter_markdown - sha256: "7b25c10de1fea883f3c4f9b8389506b54053cd00807beab69fd65c8653a2711f" + sha256: "818cf6c28377ba2c91ed283c96fd712e9c175dd2d2488eb7fc93b6afb9ad2e08" url: "https://pub.dev" source: hosted - version: "0.6.14" + version: "0.6.13+1" flutter_plugin_android_lifecycle: dependency: transitive description: @@ -387,14 +510,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.4.0" - flutter_spinkit: - dependency: transitive - description: - name: flutter_spinkit - sha256: "77a2117c0517ff909221f3160b8eb20052ab5216107581168af574ac1f05dff8" - url: "https://pub.dev" - source: hosted - version: "5.1.0" flutter_svg: dependency: transitive description: @@ -481,18 +596,18 @@ packages: dependency: transitive description: name: image_picker - sha256: d39cc12402dab8365fe5b5370e64694ae0223d675c36b15ff0490b7cc3d32551 + sha256: f98d76672d309c8b7030c323b3394669e122d52b307d2bbd8d06bd70f5b2aabe url: "https://pub.dev" source: hosted - version: "0.8.6+2" + version: "0.8.6+1" image_picker_android: dependency: transitive description: name: image_picker_android - sha256: "385f12ee9c7288575572c7873a332016ec45ebd092e1c2f6bd421b4a9ad21f1d" + sha256: b1cbfec0f5aef427a18eb573f5445af8c9c568626bf3388553e40c263d3f7368 url: "https://pub.dev" source: hosted - version: "0.8.5+6" + version: "0.8.5+5" image_picker_for_web: dependency: transitive description: @@ -505,10 +620,10 @@ packages: dependency: transitive description: name: image_picker_ios - sha256: "884ed71165bc01ffe1b0b7813e6fa17e1e9442da974656f99b79a292371303d6" + sha256: "39c013200046d14c58b71dc4fa3d00e425fc9c699d589136cd3ca018727c0493" url: "https://pub.dev" source: hosted - version: "0.8.6+8" + version: "0.8.6+6" image_picker_platform_interface: dependency: transitive description: @@ -585,10 +700,10 @@ packages: dependency: transitive description: name: markdown - sha256: b3c60dee8c2af50ad0e6e90cceba98e47718a6ee0a7a6772c77846a0cc21f78b + sha256: c2b81e184067b41d0264d514f7cdaa2c02d38511e39d6521a1ccc238f6d7b3f2 url: "https://pub.dev" source: hosted - version: "7.0.1" + version: "6.0.1" matcher: dependency: transitive description: @@ -649,10 +764,10 @@ packages: dependency: transitive description: name: package_info_plus - sha256: "7a6114becdf042be2b0777d77ace954d4a205644171a1cbd8712976b9bbb837c" + sha256: f62d7253edc197fe3c88d7c2ddab82d68f555e778d55390ccc3537eca8e8d637 url: "https://pub.dev" source: hosted - version: "1.4.2" + version: "1.4.3+1" package_info_plus_linux: dependency: transitive description: @@ -689,10 +804,18 @@ packages: dependency: transitive description: name: package_info_plus_windows - sha256: a6040b8695c82f8dd3c3d4dfab7ef96fbe9c67aac21b9bcf5db272589ef84441 + sha256: "79524f11c42dd9078b96d797b3cf79c0a2883a50c4920dc43da8562c115089bc" url: "https://pub.dev" source: hosted - version: "1.0.5" + version: "2.1.0" + pasteboard: + dependency: transitive + description: + name: pasteboard + sha256: "1c8b6a8b3f1d12e55d4e9404433cda1b4abe66db6b17bc2d2fb5965772c04674" + url: "https://pub.dev" + source: hosted + version: "0.2.0" path: dependency: transitive description: @@ -745,10 +868,10 @@ packages: dependency: transitive description: name: path_provider_linux - sha256: "2e32f1640f07caef0d3cb993680f181c79e54a3827b997d5ee221490d131fbd9" + sha256: ab0987bf95bc591da42dffb38c77398fc43309f0b9b894dcc5d6f40c4b26c379 url: "https://pub.dev" source: hosted - version: "2.1.8" + version: "2.1.7" path_provider_platform_interface: dependency: transitive description: @@ -761,10 +884,10 @@ packages: dependency: transitive description: name: path_provider_windows - sha256: a34ecd7fb548f8e57321fd8e50d865d266941b54e6c3b7758cf8f37c24116905 + sha256: d3f80b32e83ec208ac95253e0cd4d298e104fbc63cb29c5c69edaed43b0c69d6 url: "https://pub.dev" source: hosted - version: "2.0.7" + version: "2.1.6" pedantic: dependency: transitive description: @@ -829,14 +952,6 @@ packages: url: "https://pub.dev" source: hosted version: "2.5.2" - photo_view: - dependency: transitive - description: - name: photo_view - sha256: "8036802a00bae2a78fc197af8a158e3e2f7b500561ed23b4c458107685e645bb" - url: "https://pub.dev" - source: hosted - version: "0.14.0" platform: dependency: transitive description: @@ -909,14 +1024,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.1" - scrollable_positioned_list: + scrollable_positioned_list_for_us: dependency: transitive description: - name: scrollable_positioned_list - sha256: "9566352ab9ba05794ee6c8864f154afba5d36c5637d0e3e32c615ba4ceb92772" + name: scrollable_positioned_list_for_us + sha256: b5bcbb35114902c004a4f98f2dbd5b0a5a7f80a0144a8b46297601e38fa5383d url: "https://pub.dev" source: hosted - version: "0.2.3" + version: "0.4.2" shared_preferences: dependency: transitive description: @@ -1062,33 +1177,41 @@ packages: dependency: transitive description: name: tencent_cloud_chat_sdk - sha256: "32b7e40c5a1682b3ee85daa73504af3d0d4e60a93472b279591e769b15b4861c" + sha256: "765a93262a41080e155ce5b8a6ca20147a81c7d306f7f87444077c5eaae87e08" url: "https://pub.dev" source: hosted - version: "5.0.9" + version: "5.1.5" tencent_cloud_chat_uikit: dependency: "direct main" description: path: ".." relative: true source: path - version: "1.7.0" + version: "2.0.0+1" + tencent_cloud_uikit_core: + dependency: transitive + description: + name: tencent_cloud_uikit_core + sha256: "829dfde0c4fbdae019ba233f7f2c299e7cbd18c3ae20ecfe3ab4a43084a33064" + url: "https://pub.dev" + source: hosted + version: "1.0.2" tencent_extended_text: dependency: transitive description: name: tencent_extended_text - sha256: cf0d283c01a9e63f75666d8b5b1cabd463e18e51802bf1d093d7a65bd369b3d4 + sha256: "27a2f7ee58ada480e295102471f1733a7402178a239d0c80a7aa33a134c641ef" url: "https://pub.dev" source: hosted - version: "1.0.2+1" + version: "1.0.2" tencent_extended_text_field: dependency: transitive description: name: tencent_extended_text_field - sha256: daa10f3775bfac1cc841b34275c2746ced7764f3b77222a93edb4c13bad1209b + sha256: d311c240983dbf78e31b58f91e425920a40d6564942813e692a3419bf5c9deb0 url: "https://pub.dev" source: hosted - version: "1.0.1+2" + version: "1.0.1" tencent_extended_text_library: dependency: transitive description: @@ -1101,26 +1224,34 @@ packages: dependency: transitive description: name: tencent_im_base - sha256: b5917ff0bae7c31d52f54932349fc400d3752719a1c5a2f0209258f85c7a6c07 + sha256: "516356a80f43b94a6c0719b54e4c641cb1f164830b2b3e887d175ae862ebab3f" url: "https://pub.dev" source: hosted - version: "1.0.26" + version: "1.0.51" + tencent_im_sdk_plugin_desktop: + dependency: "direct main" + description: + name: tencent_im_sdk_plugin_desktop + sha256: "5fe5ab0765183185fe4a1f94ce1fdc6ab0a450b8522011806549678edb52130d" + url: "https://pub.dev" + source: hosted + version: "0.1.13" tencent_im_sdk_plugin_platform_interface: dependency: transitive description: name: tencent_im_sdk_plugin_platform_interface - sha256: "7aff6a8495eae3efc3aed1ed944bab7a0ce7eb1035d09b3f3d7974bcb3d8b137" + sha256: "04043582f1af698b4abe12d53cd0f043466228fae712677688988d8ff7bfc1f1" url: "https://pub.dev" source: hosted - version: "0.3.12" + version: "0.3.19" tencent_im_sdk_plugin_web: dependency: "direct main" description: name: tencent_im_sdk_plugin_web - sha256: d83a2370398948bccdc3fa4aae33840bcee62d78faa288823a02d9e0e4ab677f + sha256: "6ddf543c6fb4a2c220c5cec3b9247b4353c3dc0f276e76b81c800ad4c92f9bb5" url: "https://pub.dev" source: hosted - version: "0.3.7" + version: "0.3.9" tencent_keyboard_visibility: dependency: transitive description: @@ -1133,10 +1264,10 @@ packages: dependency: transitive description: name: tencent_open_file - sha256: "98cbffe55e3245a308fbf76e18c9a0f808e534c624bc1a1cc0a00bd63a418290" + sha256: "01f94f618da42e5593bbad0657fcd13cfc1c2360cca805d8cdfefe898cbe5429" url: "https://pub.dev" source: hosted - version: "4.0.9+1" + version: "4.0.10" tencent_super_tooltip: dependency: transitive description: @@ -1149,10 +1280,10 @@ packages: dependency: transitive description: name: tencent_wechat_camera_picker - sha256: "6a6fd12d61ad2ef17273a226a165fe0b5e3ef5c7e49779de38503e4f4b6e3ef1" + sha256: "8f95b435c7a12a9221f00fe4354fb9c0f9313d79cc09ddb5902b7b418185092d" url: "https://pub.dev" source: hosted - version: "3.6.5+2" + version: "3.6.5+1" term_glyph: dependency: transitive description: @@ -1173,18 +1304,18 @@ packages: dependency: transitive description: name: tim_ui_kit_sticker_plugin - sha256: cd5d6e2380deaf73e420602bbfe3da36c0f3253adc531f76e139efec80671175 + sha256: "2a825d33076f319f6c1c87d58e2b0d650c9284ae4afd8efdc206f3e6f3582e64" url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "2.0.1" transparent_image: dependency: transitive description: name: transparent_image - sha256: e8991d955a2094e197ca24c645efec2faf4285772a4746126ca12875e54ca02f + sha256: e566a616922a781489f4d91cc939b1b3203b6e4a093317805f2f82f0bb0f8dec url: "https://pub.dev" source: hosted - version: "2.0.1" + version: "2.0.0" tuple: dependency: transitive description: @@ -1205,26 +1336,26 @@ packages: dependency: transitive description: name: universal_html - sha256: b5061c64c7c863c12e46279e032976f1c274f927fb3589b52b5928dcd2d52f7c + sha256: "5ff50b7c14d201421cf5230ec389a0591c4deb5c817c9d7ccca3b26fe5f31e34" url: "https://pub.dev" source: hosted - version: "2.0.9" + version: "2.0.8" universal_io: dependency: transitive description: name: universal_io - sha256: "06866290206d196064fd61df4c7aea1ffe9a4e7c4ccaa8fcded42dd41948005d" + sha256: "79f78ddad839ee3aae3ec7c01eb4575faf0d5c860f8e5223bc9f9c17f7f03cef" url: "https://pub.dev" source: hosted - version: "2.2.0" + version: "2.0.4" url_launcher: dependency: transitive description: name: url_launcher - sha256: e8f2efc804810c0f2f5b485f49e7942179f56eabcfe81dce3387fec4bb55876b + sha256: "698fa0b4392effdc73e9e184403b627362eb5fbf904483ac9defbb1c2191d809" url: "https://pub.dev" source: hosted - version: "6.1.9" + version: "6.1.8" url_launcher_android: dependency: transitive description: @@ -1237,10 +1368,10 @@ packages: dependency: transitive description: name: url_launcher_ios - sha256: "0a5af0aefdd8cf820dd739886efb1637f1f24489900204f50984634c07a54815" + sha256: bb328b24d3bccc20bdf1024a0990ac4f869d57663660de9c936fb8c043edefe3 url: "https://pub.dev" source: hosted - version: "6.1.0" + version: "6.0.18" url_launcher_linux: dependency: transitive description: @@ -1337,14 +1468,6 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.13" - video_thumbnail: - dependency: transitive - description: - name: video_thumbnail - sha256: "3455c189d3f0bb4e3fc2236475aa84fe598b9b2d0e08f43b9761f5bc44210016" - url: "https://pub.dev" - source: hosted - version: "0.5.3" wakelock: dependency: transitive description: @@ -1381,10 +1504,10 @@ packages: dependency: transitive description: name: wakelock_windows - sha256: "108b1b73711f1664ee462e73af34a9286ff496e27d4d8371e2fb4da8fde4cdac" + sha256: "857f77b3fe6ae82dd045455baa626bc4b93cb9bb6c86bf3f27c182167c3a5567" url: "https://pub.dev" source: hosted - version: "0.2.0" + version: "0.2.1" watcher: dependency: transitive description: @@ -1405,18 +1528,18 @@ packages: dependency: transitive description: name: win32 - sha256: c0e3a4f7be7dae51d8f152230b86627e3397c1ba8c3fa58e63d44a9f3edc9cef + sha256: a6f0236dbda0f63aa9a25ad1ff9a9d8a4eaaa5012da0dc59d21afdb1dc361ca4 url: "https://pub.dev" source: hosted - version: "2.6.1" + version: "3.1.4" xdg_directories: dependency: transitive description: name: xdg_directories - sha256: ee1505df1426458f7f60aac270645098d318a8b4766d85fde75f76f2e21807d1 + sha256: bd512f03919aac5f1313eb8249f223bacf4927031bf60b02601f81f687689e86 url: "https://pub.dev" source: hosted - version: "1.0.0" + version: "0.2.0+3" xml: dependency: transitive description: @@ -1434,5 +1557,5 @@ packages: source: hosted version: "3.1.1" sdks: - dart: ">=2.19.0 <4.0.0" + dart: ">=2.19.0 <3.0.0" flutter: ">=3.7.0" diff --git a/example/pubspec.yaml b/example/pubspec.yaml index 4eecb12..f19a45a 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -36,8 +36,17 @@ dependencies: cupertino_icons: ^1.0.2 tencent_cloud_chat_uikit: path: ../../tim_ui_kit - tencent_im_sdk_plugin_web: ^0.3.2 + tencent_im_sdk_plugin_web: ^0.3.9 archive: ^3.3.0 + tencent_im_sdk_plugin_desktop: ^0.1.13 + +# dependency_overrides: +# tencent_cloud_chat_sdk: +# path: /Users/wangrunlin/Documents/GitHub/im-flutter-plugin/tencent_im_sdk_plugin +# tencent_im_sdk_plugin_desktop: +# path: /Users/wangrunlin/Documents/GitHub/im-flutter-plugin/tencent_im_sdk_plugin_desktop +# tencent_im_sdk_plugin_web: +# path: /Users/wangrunlin/Documents/GitHub/im-flutter-plugin/tencent_im_sdk_plugin_web dev_dependencies: flutter_test: diff --git a/example/windows/flutter/generated_plugin_registrant.cc b/example/windows/flutter/generated_plugin_registrant.cc index a0d0bbe..03ccc3e 100644 --- a/example/windows/flutter/generated_plugin_registrant.cc +++ b/example/windows/flutter/generated_plugin_registrant.cc @@ -6,10 +6,22 @@ #include "generated_plugin_registrant.h" +#include +#include +#include +#include #include #include void RegisterPlugins(flutter::PluginRegistry* registry) { + AudioplayersWindowsPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("AudioplayersWindowsPlugin")); + DesktopDropPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("DesktopDropPlugin")); + FcNativeVideoThumbnailForUsPluginCApiRegisterWithRegistrar( + registry->GetRegistrarForPlugin("FcNativeVideoThumbnailForUsPluginCApi")); + PasteboardPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("PasteboardPlugin")); PermissionHandlerWindowsPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("PermissionHandlerWindowsPlugin")); UrlLauncherWindowsRegisterWithRegistrar( diff --git a/example/windows/flutter/generated_plugins.cmake b/example/windows/flutter/generated_plugins.cmake index c20a586..ad68f93 100644 --- a/example/windows/flutter/generated_plugins.cmake +++ b/example/windows/flutter/generated_plugins.cmake @@ -3,6 +3,10 @@ # list(APPEND FLUTTER_PLUGIN_LIST + audioplayers_windows + desktop_drop + fc_native_video_thumbnail_for_us + pasteboard permission_handler_windows url_launcher_windows ) diff --git a/images/folder_open.png b/images/folder_open.png new file mode 100644 index 0000000..9557b37 Binary files /dev/null and b/images/folder_open.png differ diff --git a/images/open_in_new.png b/images/open_in_new.png new file mode 100644 index 0000000..b2193dd Binary files /dev/null and b/images/open_in_new.png differ diff --git a/images/video_icon.png b/images/video_icon.png new file mode 100644 index 0000000..adeacdd Binary files /dev/null and b/images/video_icon.png differ diff --git a/lib/base_widgets/tim_ui_kit_state.dart b/lib/base_widgets/tim_ui_kit_state.dart index d39284c..4ac9978 100644 --- a/lib/base_widgets/tim_ui_kit_state.dart +++ b/lib/base_widgets/tim_ui_kit_state.dart @@ -8,10 +8,6 @@ import 'package:tencent_cloud_chat_uikit/data_services/services_locatar.dart'; class TIMUIKitState extends TIMState { final CoreServicesImpl _coreServices = serviceLocator(); - @override - initState() { - super.initState(); - } @override void onTIMCallback(TIMCallback callbackValue) { diff --git a/lib/business_logic/separate_models/tui_chat_model_tools.dart b/lib/business_logic/separate_models/tui_chat_model_tools.dart index eee084c..521923e 100644 --- a/lib/business_logic/separate_models/tui_chat_model_tools.dart +++ b/lib/business_logic/separate_models/tui_chat_model_tools.dart @@ -1,13 +1,11 @@ import 'package:tencent_im_base/tencent_im_base.dart'; import 'package:tencent_cloud_chat_uikit/business_logic/view_models/tui_chat_global_model.dart'; import 'package:tencent_cloud_chat_uikit/data_services/core/core_services_implements.dart'; -import 'package:tencent_cloud_chat_uikit/data_services/message/message_services.dart'; import 'package:tencent_cloud_chat_uikit/data_services/services_locatar.dart'; class TUIChatModelTools { final TUIChatGlobalModel globalModel = serviceLocator(); final CoreServicesImpl _coreServices = serviceLocator(); - final MessageService _messageService = serviceLocator(); OfflinePushInfo buildMessagePushInfo( V2TimMessage message, String convID, ConvType convType) { @@ -15,6 +13,14 @@ class TUIChatModelTools { return "{\"conversationID\": \"$convID\"}"; } + if (globalModel.chatConfig.offlinePushInfo != null) { + final customData = + globalModel.chatConfig.offlinePushInfo!(message, convID, convType); + if(customData != null){ + return customData; + } + } + String title = globalModel.chatConfig.notificationTitle; // If user provides null, use default ext. @@ -109,35 +115,9 @@ class TUIChatModelTools { }); if (targetIndex != null && - targetIndex != -1 && + targetIndex > -1 && currentHistoryMsgList.isNotEmpty) { - List response; - if (currentHistoryMsgList.length > targetIndex + 2) { - response = await _messageService.getHistoryMessageList( - count: 1, - getType: HistoryMsgGetTypeEnum.V2TIM_GET_LOCAL_NEWER_MSG, - userID: conversationType == ConvType.c2c ? conversationID : null, - groupID: conversationType == ConvType.group ? conversationID : null, - lastMsgID: currentHistoryMsgList[targetIndex + 1].msgID); - } else { - response = await _messageService.getHistoryMessageList( - count: 5, - getType: HistoryMsgGetTypeEnum.V2TIM_GET_LOCAL_OLDER_MSG, - userID: conversationType == ConvType.c2c ? conversationID : null, - groupID: conversationType == ConvType.group ? conversationID : null, - lastMsgID: currentHistoryMsgList.length - 3 < 0 - ? null - : currentHistoryMsgList[currentHistoryMsgList.length - 3].msgID, - ); - } - - try { - return response.firstWhere((item) { - return item.msgID == msgID; - }); - } catch (e) { - return null; - } + return currentHistoryMsgList[targetIndex]; } else { return null; } diff --git a/lib/business_logic/separate_models/tui_chat_separate_view_model.dart b/lib/business_logic/separate_models/tui_chat_separate_view_model.dart index 9584351..a342a4a 100644 --- a/lib/business_logic/separate_models/tui_chat_separate_view_model.dart +++ b/lib/business_logic/separate_models/tui_chat_separate_view_model.dart @@ -2,10 +2,14 @@ import 'dart:async'; import 'dart:convert'; import 'dart:io'; import 'dart:math'; + import 'package:flutter/cupertino.dart'; + +// ignore: unnecessary_import +import 'package:flutter/foundation.dart'; +import 'package:tencent_cloud_chat_uikit/tencent_cloud_chat_uikit.dart'; import 'package:flutter_image_compress/flutter_image_compress.dart'; import 'package:path_provider/path_provider.dart'; -import 'package:tencent_cloud_chat_uikit/tencent_cloud_chat_uikit.dart'; import 'package:tencent_cloud_chat_uikit/business_logic/life_cycle/chat_life_cycle.dart'; import 'package:tencent_cloud_chat_uikit/business_logic/separate_models/tui_chat_model_tools.dart'; import 'package:tencent_cloud_chat_uikit/business_logic/view_models/tui_chat_global_model.dart'; @@ -17,10 +21,7 @@ import 'package:tencent_cloud_chat_uikit/ui/constants/history_message_constant.d import 'package:tencent_cloud_chat_uikit/ui/utils/platform.dart'; import 'package:uuid/uuid.dart'; -enum LoadDirection { - previous, - latest -} +enum LoadDirection { previous, latest } class TUIChatSeparateViewModel extends ChangeNotifier { final FriendshipServices _friendshipServices = @@ -39,7 +40,7 @@ class TUIChatSeparateViewModel extends ChangeNotifier { ConvType? conversationType; bool haveMoreData = false; bool haveMoreLatestData = false; - String _currentSelectedMsgId = ""; + String _currentPlayedMsgId = ""; String _editRevokedMsg = ""; GroupReceiptAllowType? _groupType; List _multiSelectedMessageList = []; @@ -51,11 +52,29 @@ class TUIChatSeparateViewModel extends ChangeNotifier { TIMUIKitChatConfig chatConfig = const TIMUIKitChatConfig(); ValueChanged? setInputField; String Function(V2TimMessage message)? abstractMessageBuilder; - Function(String userID)? onTapAvatar; + Function(String userID, TapDownDetails tapDetails)? onTapAvatar; V2TimGroupMemberFullInfo? _currentChatUserInfo; V2TimGroupInfo? _groupInfo; String groupMemberListSeq = "0"; List? groupMemberList = []; + double atPositionX = 0.0; + double atPositionY = 0.0; + int _activeAtIndex = -1; + List _showAtMemberList = []; + + int get activeAtIndex => _activeAtIndex; + + set activeAtIndex(int value) { + _activeAtIndex = value; + notifyListeners(); + } + + List get showAtMemberList => _showAtMemberList; + + set showAtMemberList(List value) { + _showAtMemberList = value; + notifyListeners(); + } V2TimGroupInfo? get groupInfo => _groupInfo; @@ -78,10 +97,10 @@ class TUIChatSeparateViewModel extends ChangeNotifier { notifyListeners(); } - String get currentSelectedMsgId => _currentSelectedMsgId; + String get currentPlayedMsgId => _currentPlayedMsgId; - set currentSelectedMsgId(String value) { - _currentSelectedMsgId = value; + set currentPlayedMsgId(String value) { + _currentPlayedMsgId = value; notifyListeners(); } @@ -167,6 +186,12 @@ class TUIChatSeparateViewModel extends ChangeNotifier { globalModel.refreshGroupApplicationList(); getGroupInfo(groupID ?? convID); loadGroupMemberList(groupID: groupID ?? convID); + } else { + _groupType = null; + isGroupExist = true; + _groupInfo = null; + groupMemberList?.clear(); + notifyListeners(); } if (conversationType == ConvType.c2c) { final List? friendRes = @@ -208,36 +233,29 @@ class TUIChatSeparateViewModel extends ChangeNotifier { List msgList = []; haveMoreData = false; - final previousResponse = await _messageService.getHistoryMessageListWithComplete( - count: 20, - getType: HistoryMsgGetTypeEnum.V2TIM_GET_CLOUD_OLDER_MSG, - userID: conversationType == ConvType.c2c ? conversationID : null, - groupID: conversationType == ConvType.group ? conversationID : null, - lastMsgSeq: max(seq, 0) - ); + final previousResponse = + await _messageService.getHistoryMessageListWithComplete( + count: 20, + getType: HistoryMsgGetTypeEnum.V2TIM_GET_CLOUD_OLDER_MSG, + userID: conversationType == ConvType.c2c ? conversationID : null, + groupID: conversationType == ConvType.group ? conversationID : null, + lastMsgSeq: max(seq, 0)); msgList = previousResponse?.messageList ?? []; haveMoreData = !(previousResponse?.isFinished ?? false); - - // final latestResponse = await _messageService.getHistoryMessageListWithComplete( - // count: 20, - // getType: HistoryMsgGetTypeEnum.V2TIM_GET_CLOUD_NEWER_MSG, - // userID: conversationType == ConvType.c2c ? conversationID : null, - // groupID: conversationType == ConvType.group ? conversationID : null, - // lastMsgSeq: max(seq - 1, 0) - // ); - // msgList = [...(latestResponse?.messageList.reversed ?? []), ...msgList]; haveMoreLatestData = true; - globalModel.setMessageListPosition(conversationID, HistoryMessagePosition.notShowLatest); + globalModel.setMessageListPosition( + conversationID, HistoryMessagePosition.notShowLatest); msgList = await lifeCycle?.didGetHistoricalMessageList(msgList) ?? msgList; - msgList.insert(0, V2TimMessage( - userID: '', - isSelf: false, - elemType: 101, - msgID: msgList[0].msgID, - seq: msgList[0].seq, - timestamp: 9999 - )); + msgList.insert( + 0, + V2TimMessage( + userID: '', + isSelf: false, + elemType: 101, + msgID: msgList[0].msgID, + seq: msgList[0].seq, + timestamp: 9999)); globalModel.setMessageList(conversationID, msgList, needResetNewMessageCount: false); @@ -253,99 +271,141 @@ class TUIChatSeparateViewModel extends ChangeNotifier { return haveMoreData; } - Future loadData({ - HistoryMsgGetTypeEnum? getType, - int lastMsgSeq = -1, - required int count, - String? lastMsgID, - LoadDirection direction = LoadDirection.previous, + // 加载聊天记录 + Future loadChatRecord({ + HistoryMsgGetTypeEnum? getType, // 获取聊天记录的方式 + int lastMsgSeq = -1, // 上一条消息的消息序号 + required int count, // 加载的消息数量 + String? lastMsgID, // 最后一条消息的ID + LoadDirection direction = + LoadDirection.previous, // 加载的方向,previous表示向上加载,latest表示向下加载 }) async { - // TODO: 这个函数写的也太复杂了,现在就先不动了,到时候2.0大版本得彻底改造下QAQ - if(direction == LoadDirection.latest){ - haveMoreLatestData = false; - }else{ - haveMoreData = false; - } - final currentHistoryMsgList = globalModel.messageListMap[conversationID]; - final response = await _messageService.getHistoryMessageListWithComplete( + try { + // 根据加载方向设置是否还能继续加载更多消息 + direction == LoadDirection.latest + ? haveMoreLatestData = false + : haveMoreData = false; + + // 获取当前聊天对话的历史消息列表 + final currentRecordList = globalModel.messageListMap[conversationID]; + + // 调用MessageService获取聊天记录 + final response = await _messageService.getHistoryMessageListWithComplete( count: count, - getType: getType ?? (direction == LoadDirection.previous - ? HistoryMsgGetTypeEnum.V2TIM_GET_CLOUD_OLDER_MSG - : HistoryMsgGetTypeEnum.V2TIM_GET_CLOUD_NEWER_MSG), + getType: getType ?? + (direction == LoadDirection.previous + ? HistoryMsgGetTypeEnum.V2TIM_GET_CLOUD_OLDER_MSG + : HistoryMsgGetTypeEnum.V2TIM_GET_CLOUD_NEWER_MSG), userID: conversationType == ConvType.c2c ? conversationID : null, groupID: conversationType == ConvType.group ? conversationID : null, lastMsgID: lastMsgID, - lastMsgSeq: lastMsgSeq); - if (response == null) { - return false; - } + lastMsgSeq: lastMsgSeq, + ); - if (lastMsgID != null && currentHistoryMsgList != null) { - List messageList = response.messageList; - List newList = []; - - if(direction == LoadDirection.latest){ - globalModel.receivedNewMessageCount = globalModel.receivedMessageListCount + messageList.length; - messageList = messageList.reversed.toList(); - newList = [...messageList, ...currentHistoryMsgList]; - }else{ - newList = [...currentHistoryMsgList, ...messageList]; + if (response == null) { + return false; } - final List msgList = - await lifeCycle?.didGetHistoricalMessageList(newList) ?? newList; - globalModel.setMessageList(conversationID, msgList, - needResetNewMessageCount: false); - } else { - List messageList = - await lifeCycle?.didGetHistoricalMessageList(response.messageList) ?? - response.messageList; - if (globalModel.loadingMessage[conversationID] != null) { - if (globalModel.loadingMessage[conversationID]!.isNotEmpty) { - if(direction == LoadDirection.previous){ - messageList = [ - ...?globalModel.loadingMessage[conversationID], - ...messageList - ]; - }else{ - messageList = messageList.reversed.toList(); - messageList = [ - ...messageList, - ...?globalModel.loadingMessage[conversationID], - ]; + // 根据加载方向更新是否还能继续加载更多消息 + if (direction == LoadDirection.latest) { + haveMoreLatestData = !response.isFinished; + } else { + haveMoreData = !response.isFinished; + } + notifyListeners(); + + if (response.messageList.isEmpty) { + return false; + } + + // 根据lastMsgID判断是否为分页加载 + if (lastMsgID != null && currentRecordList != null) { + List messageList = response.messageList; + List newList = []; + + // 根据加载方向拼接消息列表 + if (direction == LoadDirection.latest) { + globalModel.receivedNewMessageCount = + globalModel.receivedMessageListCount + messageList.length; + messageList = messageList.reversed.toList(); + newList = _combineMessageList(messageList, currentRecordList); + } else { + newList = _combineMessageList(currentRecordList, messageList); + } + + // 处理新获取的消息列表后回调 + final List msgList = + await lifeCycle?.didGetHistoricalMessageList(newList) ?? newList; + + // 更新聊天记录到全局model + globalModel.setMessageList( + conversationID, + msgList, + needResetNewMessageCount: false, + ); + } else { + // 处理新获取的消息列表后回调 + List receivedList = await lifeCycle + ?.didGetHistoricalMessageList(response.messageList) ?? + response.messageList; + + // 根据加载方向拼接消息列表 + if (globalModel.loadingMessage[conversationID]?.isNotEmpty ?? false) { + if (direction == LoadDirection.previous) { + receivedList = _combineMessageList( + globalModel.messageListMap[conversationID]!, receivedList); + } else { + receivedList = receivedList.reversed.toList(); + receivedList = _combineMessageList( + receivedList, globalModel.messageListMap[conversationID]!); } } else { globalModel.loadingMessage.remove(conversationID); } - } - globalModel.setMessageList(conversationID, messageList, - needResetNewMessageCount: false); - } - if (chatConfig.isShowGroupReadingStatus && - conversationType == ConvType.group && - response.messageList.isNotEmpty) { - _getMsgReadReceipt(response.messageList); - } - if (chatConfig.isReportGroupReadingStatus && - conversationType == ConvType.group && - response.messageList.isNotEmpty) { - _setMsgReadReceipt(response.messageList); - } - - if(direction == LoadDirection.latest){ - haveMoreLatestData = !response.isFinished; - if(!haveMoreLatestData){ - globalModel.setMessageListPosition(conversationID, HistoryMessagePosition.inTwoScreen); + // 更新聊天记录到全局model + globalModel.setMessageList( + conversationID, + receivedList, + needResetNewMessageCount: false, + ); } - }else{ - haveMoreData = !response.isFinished; + + // 获取已读未读状态 + if (chatConfig.isShowGroupReadingStatus && + conversationType == ConvType.group && + response.messageList.isNotEmpty) { + _getMsgReadReceipt(response.messageList); + } + if (chatConfig.isReportGroupReadingStatus && + conversationType == ConvType.group && + response.messageList.isNotEmpty) { + _setMsgReadReceipt(response.messageList); + } + + // 根据加载方向更新是否还能继续加载更多消息 + if (direction == LoadDirection.latest && !haveMoreLatestData) { + globalModel.setMessageListPosition( + conversationID, HistoryMessagePosition.inTwoScreen); + } + notifyListeners(); + + return haveMoreData; + } catch (e) { + // ignore: avoid_print + print('loadChatRecord error: $e'); + return false; } - return haveMoreData; + } + +// 拼接聊天记录 + List _combineMessageList( + List first, List second) { + return [...first, ...second]; } Future loadDataFromController({int? count}) { - return loadData( + return loadChatRecord( count: count ?? HistoryMessageDartConstant.getCount, //20 ); } @@ -357,7 +417,7 @@ class TUIChatSeparateViewModel extends ChangeNotifier { _getMsgReadReceipt(List message) async { final msgID = message - .where((e) => (e.isSelf ?? false) && (e.needReadReceipt ?? false)) + .where((e) => (e.isSelf ?? true) && (e.needReadReceipt ?? false)) .map((e) => e.msgID ?? '') .toList(); if (msgID.isNotEmpty) { @@ -374,10 +434,26 @@ class TUIChatSeparateViewModel extends ChangeNotifier { } } + translateText(V2TimMessage message) async { + final String originText = message.textElem?.text ?? ""; + final String deviceLocale = TIM_getCurrentDeviceLocale(); + final String targetMessage = deviceLocale.split("-")[0]; + final translatedText = + await _messageService.translateText(originText, targetMessage); + + final LocalCustomDataModel localCustomData = LocalCustomDataModel.fromMap( + json.decode(TencentUtils.checkString(message.localCustomData) ?? "{}")); + localCustomData.translatedText = translatedText; + message.localCustomData = json.encode(localCustomData.toMap()); + globalModel.onMessageModified(message); + TencentImSDKPlugin.v2TIMManager.v2TIMMessageManager.setLocalCustomData( + msgID: message.msgID!, localCustomData: message.localCustomData ?? ""); + } + _setMsgReadReceipt(List message) async { final msgIDList = List.empty(growable: true); for (var item in message) { - final isSelf = item.isSelf ?? false; + final isSelf = item.isSelf ?? true; final needReadReceipt = item.needReadReceipt ?? false; if (!isSelf && needReadReceipt && item.msgID != null) { msgIDList.add(item.msgID!); @@ -397,7 +473,6 @@ class TUIChatSeparateViewModel extends ChangeNotifier { markMessageAsRead() async { globalModel.unreadCountForConversation = 0; - // globalModel.receivedNewMessageCount = 0; if (conversationType == ConvType.c2c) { return _messageService.markC2CMessageAsRead(userID: conversationID); } @@ -472,15 +547,17 @@ class TUIChatSeparateViewModel extends ChangeNotifier { } } - Future updateMessageFromController({required String msgID}) async { - V2TimMessage? newMessage = await tools.getExistingMessageByID( - msgID: msgID, - conversationType: conversationType ?? ConvType.c2c, - conversationID: conversationID); + Future updateMessageFromController( + {required String msgID, V2TimMessage? message}) async { + V2TimMessage? newMessage = message ?? + await tools.getExistingMessageByID( + msgID: msgID, + conversationType: conversationType ?? ConvType.c2c, + conversationID: conversationID); if (newMessage != null) { globalModel.onMessageModified(newMessage, conversationID); } else { - loadData( + loadChatRecord( count: HistoryMessageDartConstant.getCount, ); } @@ -531,7 +608,9 @@ class TUIChatSeparateViewModel extends ChangeNotifier { }) : "", ); - if (isEditStatusMessage == false && globalModel.getMessageListPosition(conversationID) != HistoryMessagePosition.notShowLatest) { + if (isEditStatusMessage == false && + globalModel.getMessageListPosition(conversationID) != + HistoryMessagePosition.notShowLatest) { globalModel.updateMessage( sendMsgRes, convID, id, convType, groupType, setInputField); } @@ -570,7 +649,8 @@ class TUIChatSeparateViewModel extends ChangeNotifier { } } - if(globalModel.getMessageListPosition(conversationID) != HistoryMessagePosition.notShowLatest) { + if (globalModel.getMessageListPosition(conversationID) != + HistoryMessagePosition.notShowLatest) { currentHistoryMsgList = [ lifeCycleMsg ?? messageInfoWithSender, ...currentHistoryMsgList @@ -608,15 +688,16 @@ class TUIChatSeparateViewModel extends ChangeNotifier { } } - if(globalModel.getMessageListPosition(conversationID) != HistoryMessagePosition.notShowLatest) { + if (globalModel.getMessageListPosition(conversationID) != + HistoryMessagePosition.notShowLatest) { currentHistoryMsgList = [ lifeCycleMsg ?? messageInfoWithSender, ...currentHistoryMsgList ]; globalModel.setMessageList(conversationID, currentHistoryMsgList); - notifyListeners(); } + return _sendMessage( convID: convID, id: textATMessageInfo.id as String, @@ -646,7 +727,9 @@ class TUIChatSeparateViewModel extends ChangeNotifier { return null; } } - if(globalModel.getMessageListPosition(conversationID) != HistoryMessagePosition.notShowLatest) { + + if (globalModel.getMessageListPosition(conversationID) != + HistoryMessagePosition.notShowLatest) { currentHistoryMsgList = [ lifeCycleMsg ?? messageInfoWithSender, ...currentHistoryMsgList @@ -654,6 +737,7 @@ class TUIChatSeparateViewModel extends ChangeNotifier { globalModel.setMessageList(conversationID, currentHistoryMsgList); notifyListeners(); } + return _sendMessage( convID: convID, id: textMessageInfo.id as String, @@ -685,15 +769,17 @@ class TUIChatSeparateViewModel extends ChangeNotifier { return null; } } - if(globalModel.getMessageListPosition(conversationID) != HistoryMessagePosition.notShowLatest) { + + if (globalModel.getMessageListPosition(conversationID) != + HistoryMessagePosition.notShowLatest) { currentHistoryMsgList = [ lifeCycleMsg ?? messageInfoWithSender, ...currentHistoryMsgList ]; globalModel.setMessageList(conversationID, currentHistoryMsgList); - notifyListeners(); } + return _sendMessage( convID: convID, id: soundMessageInfo.id as String, @@ -735,16 +821,22 @@ class TUIChatSeparateViewModel extends ChangeNotifier { } } - Future?> sendReplyMessage( - {required String text, - required String convID, - required ConvType convType}) async { + Future?> sendReplyMessage({ + required String text, + required String convID, + required ConvType convType, + List? atUserIDList, + }) async { if (text.isEmpty) { return null; } if (_repliedMessage != null) { - final V2TimMsgCreateInfoResult? textMessageInfo = + V2TimMsgCreateInfoResult? textMessageInfo = await _messageService.createTextMessage(text: text); + if (atUserIDList != null && atUserIDList.isNotEmpty) { + textMessageInfo = await _messageService.createTextAtMessage( + text: text, atUserList: atUserIDList); + } final V2TimMessage? messageInfo = textMessageInfo!.messageInfo; final receiver = convType == ConvType.c2c ? convID : ''; final groupID = convType == ConvType.group ? convID : ''; @@ -753,7 +845,7 @@ class TUIChatSeparateViewModel extends ChangeNotifier { : null; if (messageInfo != null) { V2TimMessage messageInfoWithSender = - tools.setUserInfoForMessage(messageInfo, messageInfo.id!); + tools.setUserInfoForMessage(messageInfo, textMessageInfo.id!); final hasNickName = _repliedMessage?.nickName != null && _repliedMessage?.nickName != ""; final cloudCustomData = { @@ -843,10 +935,8 @@ class TUIChatSeparateViewModel extends ChangeNotifier { format: CompressFormat.jpeg, quality: 85); image = result?.path; } - } catch (e) { - // ignore: avoid_print - print(e); - } + // ignore: empty_catches + } catch (e) {} } final imageMessageInfo = await _messageService.createImageMessage( imagePath: image ?? imagePath, inputElement: inputElement); @@ -863,15 +953,17 @@ class TUIChatSeparateViewModel extends ChangeNotifier { return null; } } - if(globalModel.getMessageListPosition(conversationID) != HistoryMessagePosition.notShowLatest){ - currentHistoryMsgList = [ - lifeCycleMsg ?? messageInfoWithSender, - ...currentHistoryMsgList - ]; - globalModel.setMessageList(conversationID, currentHistoryMsgList); - notifyListeners(); + if (globalModel.getMessageListPosition(conversationID) != + HistoryMessagePosition.notShowLatest) { + currentHistoryMsgList = [ + lifeCycleMsg ?? messageInfoWithSender, + ...currentHistoryMsgList + ]; + globalModel.setMessageList(conversationID, currentHistoryMsgList); + notifyListeners(); } + return _sendMessage( convID: convID, messageInfo: lifeCycleMsg ?? messageInfoWithSender, @@ -894,7 +986,9 @@ class TUIChatSeparateViewModel extends ChangeNotifier { List currentHistoryMsgList = getOriginMessageList(); final videoMessageInfo = await _messageService.createVideoMessage( videoPath: videoPath, - type: 'mp4', + type: videoPath != null + ? videoPath.split(".")[videoPath.split(".").length - 1] + : 'mp4', duration: duration, inputElement: inputElement, snapshotPath: snapshotPath); @@ -909,15 +1003,17 @@ class TUIChatSeparateViewModel extends ChangeNotifier { return null; } } - if(globalModel.getMessageListPosition(conversationID) != HistoryMessagePosition.notShowLatest) { + + if (globalModel.getMessageListPosition(conversationID) != + HistoryMessagePosition.notShowLatest) { currentHistoryMsgList = [ lifeCycleMsg ?? messageInfoWithSender, ...currentHistoryMsgList ]; globalModel.setMessageList(conversationID, currentHistoryMsgList); - notifyListeners(); } + return _sendMessage( convID: convID, messageInfo: lifeCycleMsg ?? messageInfoWithSender, @@ -954,15 +1050,17 @@ class TUIChatSeparateViewModel extends ChangeNotifier { return null; } } - if(globalModel.getMessageListPosition(conversationID) != HistoryMessagePosition.notShowLatest) { + + if (globalModel.getMessageListPosition(conversationID) != + HistoryMessagePosition.notShowLatest) { currentHistoryMsgList = [ lifeCycleMsg ?? messageInfoWithSender, ...currentHistoryMsgList ]; globalModel.setMessageList(conversationID, currentHistoryMsgList); - notifyListeners(); } + return _sendMessage( convID: convID, messageInfo: lifeCycleMsg ?? messageInfoWithSender, @@ -995,13 +1093,14 @@ class TUIChatSeparateViewModel extends ChangeNotifier { return null; } } - if(globalModel.getMessageListPosition(conversationID) != HistoryMessagePosition.notShowLatest) { + + if (globalModel.getMessageListPosition(conversationID) != + HistoryMessagePosition.notShowLatest) { currentHistoryMsgList = [ lifeCycleMsg ?? messageInfoWithSender, ...currentHistoryMsgList ]; globalModel.setMessageList(conversationID, currentHistoryMsgList); - notifyListeners(); } return _sendMessage( @@ -1126,10 +1225,12 @@ class TUIChatSeparateViewModel extends ChangeNotifier { } // 注意重发消息需要先删除之前发送失败的图 - Future?> reSendFailMessage( - {required V2TimMessage message, - required String convID, - required ConvType convType}) async { + Future?> reSendFailMessage({ + required V2TimMessage message, + required String convID, + required ConvType convType, + List? atUserIDList, + }) async { await deleteMsg(message.msgID ?? "", id: message.id, webMessageInstance: message.messageFromWeb); int messageType = message.elemType; @@ -1138,7 +1239,11 @@ class TUIChatSeparateViewModel extends ChangeNotifier { String text = message.textElem!.text!; if (_repliedMessage != null) { res = await sendReplyMessage( - text: text, convID: convID, convType: convType); + text: text, + convID: convID, + convType: convType, + atUserIDList: atUserIDList, + ); } else { res = await sendTextMessage( text: text, convID: convID, convType: convType); @@ -1204,7 +1309,8 @@ class TUIChatSeparateViewModel extends ChangeNotifier { } } - if(globalModel.getMessageListPosition(conversationID) != HistoryMessagePosition.notShowLatest){ + if (globalModel.getMessageListPosition(conversationID) != + HistoryMessagePosition.notShowLatest) { currentHistoryMsgList = [ lifeCycleMsg ?? messageInfoWithSender, ...currentHistoryMsgList @@ -1213,7 +1319,6 @@ class TUIChatSeparateViewModel extends ChangeNotifier { notifyListeners(); } - return _sendMessage( convID: convID, id: textMessageInfo.id as String, @@ -1235,8 +1340,16 @@ class TUIChatSeparateViewModel extends ChangeNotifier { final messageInfoWithSender = messageInfo.sender == null ? tools.setUserInfoForMessage(messageInfo, messageInfo.id!) : messageInfo; - currentHistoryMsgList = [messageInfoWithSender, ...currentHistoryMsgList]; - globalModel.setMessageList(conversationID, currentHistoryMsgList); + + if (globalModel.getMessageListPosition(conversationID) != + HistoryMessagePosition.notShowLatest) { + currentHistoryMsgList = [ + messageInfoWithSender, + ...currentHistoryMsgList + ]; + globalModel.setMessageList(conversationID, currentHistoryMsgList); + } + return _sendMessage( convID: conversationID, id: messageInfo.id as String, @@ -1316,26 +1429,6 @@ class TUIChatSeparateViewModel extends ChangeNotifier { } } - translateText(V2TimMessage message) async { - final String originText = message.textElem?.text ?? ""; - final String deviceLocale = - WidgetsBinding.instance?.window.locale.toLanguageTag() ?? "en"; - final String targetMessage = deviceLocale.split("-")[0]; - final translatedText = - await _messageService.translateText(originText, targetMessage); - - final LocalCustomDataModel localCustomData = LocalCustomDataModel.fromMap( - json.decode(TencentUtils.checkString(message.localCustomData) ?? "{}")); - localCustomData.translatedText = translatedText; - final result = await TencentImSDKPlugin.v2TIMManager.v2TIMMessageManager - .setLocalCustomData( - msgID: message.msgID!, - localCustomData: json.encode(localCustomData.toMap())); - if (result.code == 0 && TencentUtils.checkString(message.msgID) != null) { - updateMessageFromController(msgID: message.msgID!); - } - } - updateMultiSelectStatus(bool isSelect) { _isMultiSelect = isSelect; if (!isSelect) { diff --git a/lib/business_logic/separate_models/tui_group_profile_model.dart b/lib/business_logic/separate_models/tui_group_profile_model.dart index 47c11de..153ba22 100644 --- a/lib/business_logic/separate_models/tui_group_profile_model.dart +++ b/lib/business_logic/separate_models/tui_group_profile_model.dart @@ -26,7 +26,7 @@ class TUIGroupProfileModel extends ChangeNotifier { List? _groupMemberList; String _groupMemberListSeq = "0"; V2TimGroupInfo? _groupInfo; - Function(String userID)? onClickUser; + Function(String userID, TapDownDetails? tapDetails)? onClickUser; GroupProfileLifeCycle? get lifeCycle => _lifeCycle; @@ -180,7 +180,6 @@ class TUIGroupProfileModel extends ChangeNotifier { setGroupNotification(String notification) async { if (_groupInfo != null) { - _groupInfo?.notification = notification; final response = await _groupServices.setGroupInfo( info: V2TimGroupInfo.fromJson({ "groupID": _groupID, @@ -189,6 +188,7 @@ class TUIGroupProfileModel extends ChangeNotifier { })); if (response.code == 0) { notifyListeners(); + _groupInfo?.notification = notification; } } } @@ -327,7 +327,7 @@ class TUIGroupProfileModel extends ChangeNotifier { Future muteGroupMember( String userID, bool isMute, int? serverTime) async { - final muteTime = serverTime != null ? serverTime + 9999 : 51556926 * 10; + const muteTime = 315360000; final res = await _groupServices.muteGroupMember( groupID: _groupID, userID: userID, seconds: isMute ? muteTime : 0); if (res.code == 0) { @@ -335,7 +335,7 @@ class TUIGroupProfileModel extends ChangeNotifier { _groupMemberList!.indexWhere((e) => e!.userID == userID); if (targetIndex != -1) { final targetElem = _groupMemberList![targetIndex]; - targetElem?.muteUntil = isMute ? muteTime : 0; + targetElem?.muteUntil = isMute ? (serverTime ?? 0) + muteTime : 0; _groupMemberList![targetIndex] = targetElem; } notifyListeners(); diff --git a/lib/business_logic/separate_models/tui_profile_view_model.dart b/lib/business_logic/separate_models/tui_profile_view_model.dart index cdb5ee6..6e517e5 100644 --- a/lib/business_logic/separate_models/tui_profile_view_model.dart +++ b/lib/business_logic/separate_models/tui_profile_view_model.dart @@ -31,6 +31,11 @@ class TUIProfileViewModel extends ChangeNotifier { return _userProfile; } + set userProfile(UserProfile? value) { + _userProfile = value; + notifyListeners(); + } + bool? get isDisturb { return _isDisturb; } @@ -48,6 +53,9 @@ class TUIProfileViewModel extends ChangeNotifier { } loadData({required String userID, bool isNeedConversation = true}) async { + if(userID.isEmpty){ + return; + } V2TimFriendInfo? friendUserInfo; V2TimConversation? conversation; final userInfoList = @@ -159,8 +167,6 @@ class TUIProfileViewModel extends ChangeNotifier { if (res.code == 0) { _userProfile?.friendInfo!.userProfile!.allowType = allowType; notifyListeners(); - } else { - print("${res.code},${res.desc}"); } return res; } @@ -175,8 +181,6 @@ class TUIProfileViewModel extends ChangeNotifier { if (res.code == 0) { _userProfile?.friendInfo!.userProfile!.gender = gender; notifyListeners(); - } else { - print("${res.code},${res.desc}"); } return res; @@ -192,8 +196,6 @@ class TUIProfileViewModel extends ChangeNotifier { if (res.code == 0) { _userProfile?.friendInfo!.userProfile!.nickName = nickName; notifyListeners(); - } else { - print("${res.code},${res.desc}"); } return res; @@ -208,8 +210,6 @@ class TUIProfileViewModel extends ChangeNotifier { if (res.code == 0) { _userProfile?.friendInfo!.userProfile!.selfSignature = selfSignature; notifyListeners(); - } else { - print("${res.code},${res.desc}"); } return res; } @@ -297,8 +297,6 @@ class TUIProfileViewModel extends ChangeNotifier { }); notifyListeners(); - } else { - print("${res.code},${res.desc}"); } return res; } diff --git a/lib/business_logic/view_models/tui_chat_global_model.dart b/lib/business_logic/view_models/tui_chat_global_model.dart index fbbd5cf..d75acca 100644 --- a/lib/business_logic/view_models/tui_chat_global_model.dart +++ b/lib/business_logic/view_models/tui_chat_global_model.dart @@ -18,7 +18,12 @@ import 'package:tencent_cloud_chat_uikit/ui/utils/message.dart'; enum ConvType { none, c2c, group } -enum HistoryMessagePosition { bottom, inTwoScreen, awayTwoScreen, notShowLatest } +enum HistoryMessagePosition { + bottom, + inTwoScreen, + awayTwoScreen, + notShowLatest +} class CurrentConversation { final String conversationID; @@ -37,6 +42,7 @@ class TUIChatGlobalModel extends ChangeNotifier with TIMUIKitClass { final Map _preloadImageMap = {}; final Map _historyMessagePositionMap = {}; final List _currentConversationList = []; + Map get preloadImageMap => _preloadImageMap; ChatLifeCycle? _lifeCycle; @@ -49,8 +55,9 @@ class TUIChatGlobalModel extends ChangeNotifier with TIMUIKitClass { late V2TimAdvancedMsgListener advancedMsgListener; int _unreadCountForConversation = 0; - // use for generate a new sliver list to show received message list - int _receivedNewMessageCount = 0; + + // use for generate a new sliver list to show recived messag list + int _recivedNewMessageCount = 0; TIMUIKitChatConfig chatConfig = const TIMUIKitChatConfig(); List? _groupApplicationList; String Function(V2TimMessage message)? _abstractMessageBuilder; @@ -58,7 +65,7 @@ class TUIChatGlobalModel extends ChangeNotifier with TIMUIKitClass { Map.from({}); // 0 normal 1 sending final Map _c2cMessageFromUserActiveMap = Map.from({}); final Map _c2cMessageActiveTimer = Map.from({}); - bool _showC2cMessageEditStatus = true; + bool _showC2cMessageEditStaus = true; final Map _c2cMessageStatusShowTimer = Map.from({}); Map loadingMessage = {}; @@ -73,8 +80,8 @@ class TUIChatGlobalModel extends ChangeNotifier with TIMUIKitClass { onRecvNewMessage: (V2TimMessage newMsg) { _onReceiveNewMsg(newMsg); }, - onSendMessageProgress: (V2TimMessage message, int progress) { - _onSendMessageProgress(message, progress); + onSendMessageProgress: (V2TimMessage messagae, int progress) { + _onSendMessageProgress(messagae, progress); }, onRecvMessageReadReceipts: (List receiptList) { _onReceiveMessageReadReceipts(receiptList); @@ -87,12 +94,14 @@ class TUIChatGlobalModel extends ChangeNotifier with TIMUIKitClass { onMessageDownloadProgressCallback(messageProgress); }, ); - // Future.delayed(const Duration(milliseconds: 200)).then((value) => initMessageMapFromLocal()); } bool get isDownloading => _isDownloading; + bool get hasWaiting => _waitingDownloadList.isNotEmpty; + Map get currentDownLoad => _waitingDownloadList.first; + void addWaitingList(String msgID) { print("add to waiting list success"); bool contains = false; @@ -114,43 +123,28 @@ class TUIChatGlobalModel extends ChangeNotifier with TIMUIKitClass { } downloadFile() async { - bool hasWait = _waitingDownloadList.isNotEmpty; - print(_waitingDownloadList); - print(_isDownloading); - print(!hasWaiting); - if (_isDownloading || !hasWait) { - print("no download return"); + if (_isDownloading || _waitingDownloadList.isEmpty) { return; } - Map nextDownLoad = _waitingDownloadList.first; - String downLoadMsgID = nextDownLoad["msgID"] ?? ""; - if (downLoadMsgID.isEmpty) { + final nextDownload = _waitingDownloadList.first; + final msgID = nextDownload["msgID"] ?? ""; + if (msgID.isEmpty || _messageListProgressMap[msgID] == 100) { return; } - if (_messageListProgressMap[downLoadMsgID] == 100) { - return; - } - // await Dio().download(downLoadMsgUrl, downLoadMsgSavePath, - // onReceiveProgress: (recv, total) { - // if (total != -1) { - // // print((received / total * 100).toStringAsFixed(0) + "%"); - // int progrss = (recv / total * 100).ceil(); - // if (progrss > 1) { - // setMessageProgress(downLoadMsgID, progrss); - // } - // //you can build progressbar feature too - // } - // }); - await _messageService.downloadMessage( - msgID: downLoadMsgID, messageType: 6, imageType: 0, isSnapshot: false); + _isDownloading = true; - // startDownloadProcess(context, theme, received); + await _messageService.downloadMessage( + msgID: msgID, + messageType: 6, + imageType: 0, + isSnapshot: false, + ); + print("start another download"); - // downloadFile(); } - int getReceived(msgID) { + int getRecevied(msgID) { return messageListProgressMap[msgID] ?? 0; } @@ -179,11 +173,11 @@ class TUIChatGlobalModel extends ChangeNotifier with TIMUIKitClass { } int get receivedMessageListCount { - return _receivedNewMessageCount; + return _recivedNewMessageCount; } set receivedNewMessageCount(int value) { - _receivedNewMessageCount = value; + _recivedNewMessageCount = value; } int get unreadCountForConversation => _unreadCountForConversation; @@ -222,7 +216,7 @@ class TUIChatGlobalModel extends ChangeNotifier with TIMUIKitClass { } setShowC2cEditStatus(bool show) { - _showC2cMessageEditStatus = show; + _showC2cMessageEditStaus = show; } /// set edit status from chats @@ -379,14 +373,14 @@ class TUIChatGlobalModel extends ChangeNotifier with TIMUIKitClass { _totalUnreadCount = 0; _groupApplicationList?.clear(); _totalUnreadCount = 0; - _receivedNewMessageCount = 0; + _recivedNewMessageCount = 0; _messageReadReceiptMap.clear(); _messageListProgressMap.clear(); notifyListeners(); } clearRecivedNewMessageCount() { - _receivedNewMessageCount = 0; + _recivedNewMessageCount = 0; } _preLoadImage(List msgList) { @@ -457,17 +451,18 @@ class TUIChatGlobalModel extends ChangeNotifier with TIMUIKitClass { _editStatusCheck(V2TimMessage msg) { bool isStatusMessage = false; - if (msg.customElem != null && msg.groupID == null) { + if (msg.customElem != null && + TencentUtils.checkString(msg.groupID) == null) { V2TimCustomElem customElem = msg.customElem!; String sender = msg.sender ?? ""; if (customElem.data!.isNotEmpty) { try { Map? data = json.decode(customElem.data ?? ""); if (data != null) { - String businessID = data["businessID"]; + var businessID = data["businessID"]; int? userAction = data["userAction"]; String? actionParam = data["actionParam"]; - if (businessID == "user_typing_status") { + if (businessID.toString() == "user_typing_status") { int? typingStatus = data["typingStatus"]; if (sender != "") { if (typingStatus != null) { @@ -501,7 +496,6 @@ class TUIChatGlobalModel extends ChangeNotifier with TIMUIKitClass { try { Map data = json.decode(msg.cloudCustomData ?? ""); Map? messageFeature = data["messageFeature"]; - print(data); if (messageFeature != null) { int needTyping = messageFeature["needTyping"]; if (needTyping == 1) { @@ -532,7 +526,7 @@ class TUIChatGlobalModel extends ChangeNotifier with TIMUIKitClass { } sendEditStatusMessage(bool isEditing, String toUser) async { - if (!_showC2cMessageEditStatus) { + if (!_showC2cMessageEditStaus) { return; } if (!(_c2cMessageFromUserActiveMap[toUser] ?? false)) { @@ -598,19 +592,27 @@ class TUIChatGlobalModel extends ChangeNotifier with TIMUIKitClass { } _checkFromUserisActive(msgComing); - final convID = newMsg.userID ?? newMsg.groupID; + final convID = TencentUtils.checkString(newMsg.userID) ?? newMsg.groupID; + final convType = TencentUtils.checkString(newMsg.groupID) != null + ? ConvType.group + : ConvType.c2c; if (convID != null && convID == currentSelectedConv) { final position = getMessageListPosition(convID); - if(position == HistoryMessagePosition.notShowLatest){ + if (position == HistoryMessagePosition.notShowLatest) { return; } if (position == HistoryMessagePosition.bottom && unreadCountForConversation == 0) { - markMessageAsRead( - convID: convID, - convType: currentSelectedConvType!, - ); - _receivedNewMessageCount = 0; + _unreadCountForConversation = 0; + if (chatConfig.isAutoReportRead) { + Future.delayed(const Duration(seconds: 1), () { + markMessageAsRead( + convID: convID, + convType: convType, + ); + }); + } + _recivedNewMessageCount = 0; final currentMsg = _messageListMap[convID] ?? []; _messageListMap[convID] = [newMsg, ...currentMsg]; notifyListeners(); @@ -619,14 +621,17 @@ class TUIChatGlobalModel extends ChangeNotifier with TIMUIKitClass { if (needReadReceipt && messageID != null && msgComing.groupID != null && - chatConfig.isReportGroupReadingStatus) { + chatConfig.isReportGroupReadingStatus && + chatConfig.isAutoReportRead) { // only group message send message read receipt - sendMessageReadReceipts([messageID]); + Future.delayed(const Duration(seconds: 1), () { + sendMessageReadReceipts([messageID]); + }); } } else { if (convID == currentSelectedConv) { unreadCountForConversation++; - _receivedNewMessageCount++; + _recivedNewMessageCount++; final currentMsg = _messageListMap[convID] ?? []; _messageListMap[convID] = [newMsg, ...currentMsg]; notifyListeners(); @@ -667,7 +672,9 @@ class TUIChatGlobalModel extends ChangeNotifier with TIMUIKitClass { onMessageModified(V2TimMessage modifiedMessage, [String? convID]) async { modifiedMessage.id = DateTime.now().millisecondsSinceEpoch.toString(); - final activeMessageList = _messageListMap[convID ?? currentSelectedConv]; + final String? exactId = TencentUtils.checkString(modifiedMessage.userID) ?? + TencentUtils.checkString(modifiedMessage.groupID); + final activeMessageList = _messageListMap[convID ?? exactId]; if (activeMessageList == null || activeMessageList.isEmpty) { return; } @@ -675,7 +682,7 @@ class TUIChatGlobalModel extends ChangeNotifier with TIMUIKitClass { await _lifeCycle?.modifiedMessageWillMount(modifiedMessage) ?? modifiedMessage; final msgID = newMsg.msgID; - _messageListMap[currentSelectedConv] = activeMessageList.map((item) { + _messageListMap[convID ?? exactId ?? ""] = activeMessageList.map((item) { if (item.msgID == msgID) { return newMsg; } @@ -690,7 +697,7 @@ class TUIChatGlobalModel extends ChangeNotifier with TIMUIKitClass { final isNotEmpty = _messageListMap[convID]?.isNotEmpty; if (isNotEmpty != null && isNotEmpty) { _messageListMap[convID] = _messageListMap[convID]!.map((element) { - final isSelf = element.isSelf ?? false; + final isSelf = element.isSelf ?? true; final isPeerRead = element.isPeerRead ?? false; if (isSelf && !isPeerRead) { element.isPeerRead = true; @@ -711,9 +718,8 @@ class TUIChatGlobalModel extends ChangeNotifier with TIMUIKitClass { } } notifyListeners(); - } catch (e) { - print(e); - } + // ignore: empty_catches + } catch (e) {} } _onSendMessageProgress(V2TimMessage messagae, int progress) { @@ -721,11 +727,23 @@ class TUIChatGlobalModel extends ChangeNotifier with TIMUIKitClass { } onMessageDownloadProgressCallback( - V2TimMessageDownloadProgress messageProgress) { + V2TimMessageDownloadProgress messageProgress) async { if (messageProgress.isFinish) { setMessageProgress(messageProgress.msgID, 100); setFileMessageLocation(messageProgress.msgID, messageProgress.path); downloadFile(); + if (messageProgress.type == 0) { + final messages = await _messageService + .findMessages(messageIDList: [messageProgress.msgID]); + final V2TimMessage? message = messages?.first; + if (message != null) { + updateAsyncMessage( + message, + TencentUtils.checkString(message.userID) ?? + TencentUtils.checkString(message.groupID) ?? + ""); + } + } return; } if (messageProgress.isError) { @@ -761,11 +779,9 @@ class TUIChatGlobalModel extends ChangeNotifier with TIMUIKitClass { required String convID, required ConvType convType, }) async { - _unreadCountForConversation = 0; if (convType == ConvType.c2c) { return _messageService.markC2CMessageAsRead(userID: convID); } - _messageService.markGroupMessageAsRead(groupID: convID); } @@ -888,7 +904,7 @@ class TUIChatGlobalModel extends ChangeNotifier with TIMUIKitClass { {bool needResetNewMessageCount = true}) { _messageListMap[conversationID] = messageList; if (needResetNewMessageCount) { - _receivedNewMessageCount = 0; + _recivedNewMessageCount = 0; } notifyListeners(); } @@ -926,6 +942,7 @@ class TUIChatGlobalModel extends ChangeNotifier with TIMUIKitClass { groupType != null ? GroupReceptAllowType.values[groupType.index] : null; if (chatConfig.isShowGroupReadingStatus && convType == ConvType.group && + sendMsgRes.data?.msgID != null && ((chatConfig.groupReadReceiptPermissionList != null && chatConfig.groupReadReceiptPermissionList! .contains(groupType)) || @@ -967,6 +984,7 @@ class TUIChatGlobalModel extends ChangeNotifier with TIMUIKitClass { if (listWithTimestamp.isEmpty || (listWithTimestamp[listWithTimestamp.length - 1].timestamp != null && + item.timestamp != null && (item.timestamp! - listWithTimestamp[listWithTimestamp.length - 1] .timestamp! > diff --git a/lib/business_logic/view_models/tui_conversation_view_model.dart b/lib/business_logic/view_models/tui_conversation_view_model.dart index 7a7ef52..6d8985f 100644 --- a/lib/business_logic/view_models/tui_conversation_view_model.dart +++ b/lib/business_logic/view_models/tui_conversation_view_model.dart @@ -1,6 +1,7 @@ // ignore_for_file: unnecessary_getters_setters import 'package:flutter/material.dart'; +import 'package:tencent_cloud_chat_uikit/business_logic/view_models/tui_self_info_view_model.dart'; import 'package:tencent_im_base/tencent_im_base.dart'; import 'package:tencent_cloud_chat_uikit/business_logic/life_cycle/conversation_life_cycle.dart'; import 'package:tencent_cloud_chat_uikit/business_logic/view_models/tui_chat_global_model.dart'; @@ -29,6 +30,8 @@ List removeDuplicates( } class TUIConversationViewModel extends ChangeNotifier { + final TUISelfInfoViewModel selfInfoViewModel = + serviceLocator(); final ConversationService _conversationService = serviceLocator(); final FriendshipServices _friendshipServices = @@ -39,23 +42,16 @@ class TUIConversationViewModel extends ChangeNotifier { late V2TimConversationListener _conversationListener; List _conversationList = []; static V2TimConversation? _selectedConversation; + bool _haveMoreData = true; int _totalUnReadCount = 0; String? _scrollToConversation; + final TUIChatGlobalModel globalChatModel = + serviceLocator(); + String _nextSeq = "0"; ConversationLifeCycle? _lifeCycle; - String? get scrollToConversation => _scrollToConversation; - - set scrollToConversation(String? value) { - _scrollToConversation = value; - notifyListeners(); - } - - void clearScrollToConversation(){ - _scrollToConversation = null; - } - List get conversationList { if (PlatformUtils().isWeb) { try { @@ -69,9 +65,8 @@ class TUIConversationViewModel extends ChangeNotifier { .toList(); _conversationList.removeWhere((element) => element?.isPinned == true); _conversationList = [...pinnedConversation, ..._conversationList]; + // ignore: empty_catches } catch (e) { - // ignore: avoid_print - print(e); } } else { _conversationList.sort((a, b) => b!.orderkey!.compareTo(a!.orderkey!)); @@ -79,6 +74,17 @@ class TUIConversationViewModel extends ChangeNotifier { return _conversationList; } + String? get scrollToConversation => _scrollToConversation; + + set scrollToConversation(String? value) { + _scrollToConversation = value; + notifyListeners(); + } + + void clearScrollToConversation() { + _scrollToConversation = null; + } + bool get haveMoreData { return _haveMoreData; } @@ -98,6 +104,11 @@ class TUIConversationViewModel extends ChangeNotifier { notifyListeners(); } + set selectedConversation(V2TimConversation? value) { + _selectedConversation = value; + notifyListeners(); + } + V2TimConversation? get selectedConversation { return _selectedConversation; } @@ -125,7 +136,9 @@ class TUIConversationViewModel extends ChangeNotifier { loadInitConversation() async { await loadData(count: 40); - _chatGlobalModel.initMessageMapFromLocalDatabase(_conversationList); + if (selfInfoViewModel.globalConfig?.isPreloadMessagesAfterInit ?? true) { + _chatGlobalModel.initMessageMapFromLocalDatabase(_conversationList); + } } initConversation() async { @@ -165,6 +178,7 @@ class TUIConversationViewModel extends ChangeNotifier { void setSelectedConversation(V2TimConversation conversation) { _selectedConversation = conversation; + notifyListeners(); } Future pinConversation({ @@ -182,6 +196,9 @@ class TUIConversationViewModel extends ChangeNotifier { false) { return null; } + + globalChatModel.setMessageList(convID, []); + if (convType == 1) { return _messageService.clearC2CHistoryMessage(userID: convID); } else { diff --git a/lib/business_logic/view_models/tui_search_view_model.dart b/lib/business_logic/view_models/tui_search_view_model.dart index 2d90d1a..2bc479d 100644 --- a/lib/business_logic/view_models/tui_search_view_model.dart +++ b/lib/business_logic/view_models/tui_search_view_model.dart @@ -1,5 +1,6 @@ // ignore_for_file: constant_identifier_names +import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:tencent_im_base/tencent_im_base.dart'; import 'package:tencent_cloud_chat_uikit/data_services/friendShip/friendship_services.dart'; @@ -91,10 +92,13 @@ class TUISearchViewModel extends ChangeNotifier { type: KeywordListMatchType.V2TIM_KEYWORD_LIST_MATCH_TYPE_OR.index, )); if (searchResult.code == 0 && searchResult.data != null) { - totalMsgInConversationCount = searchResult.data!.totalCount!; + final messageSearchResultItems = searchResult + .data!.messageSearchResultItems! + .firstWhereOrNull((element) => element.conversationID == conversationId); + totalMsgInConversationCount = messageSearchResultItems?.messageCount ?? 0; currentMsgListForConversation = [ ...currentMsgListForConversation, - ...(searchResult.data!.messageSearchResultItems?[0].messageList ?? []) + ...(messageSearchResultItems?.messageList ?? []) ]; } notifyListeners(); diff --git a/lib/data_services/core/core_services_implements.dart b/lib/data_services/core/core_services_implements.dart index e4f8504..14fd3af 100644 --- a/lib/data_services/core/core_services_implements.dart +++ b/lib/data_services/core/core_services_implements.dart @@ -1,7 +1,6 @@ // ignore_for_file: avoid_print import 'package:flutter/cupertino.dart'; -import 'package:flutter/foundation.dart'; import 'package:tencent_cloud_chat_uikit/business_logic/view_models/tui_setting_model.dart'; import 'package:tencent_im_base/tencent_im_base.dart'; import 'package:tencent_cloud_chat_uikit/business_logic/listener_model/tui_group_listener_model.dart'; @@ -157,7 +156,7 @@ class CoreServicesImpl with CoreServices { onCallback = onTUIKitCallbackListener; } setGlobalConfig(config); - if (PlatformUtils().isIOS || PlatformUtils().isAndroid) { + if (!PlatformUtils().isWeb) { didLoginSuccess(); } } @@ -251,8 +250,8 @@ class CoreServicesImpl with CoreServices { } tuiFriendShipViewModel.userStatusList = currentUserStatusList; + // ignore: empty_catches } catch (e) { - print(e); } } @@ -265,7 +264,7 @@ class CoreServicesImpl with CoreServices { _userSig = userSig; V2TimCallback result = await TencentImSDKPlugin.v2TIMManager .login(userID: userID, userSig: userSig); - if (PlatformUtils().isIOS || PlatformUtils().isAndroid) { + if (!PlatformUtils().isWeb) { didLoginSuccess(); } if (result.code != 0) { @@ -294,7 +293,7 @@ class CoreServicesImpl with CoreServices { } }); - if (!kIsWeb && + if (PlatformUtils().isMobile && selfInfoViewModel.globalConfig?.isCheckDiskStorageSpace == true) { final diskSpace = await DiskSpace.getFreeDiskSpace; if (diskSpace != null && diskSpace < 1024) { diff --git a/lib/data_services/core/tim_uikit_config.dart b/lib/data_services/core/tim_uikit_config.dart index d347edf..cc8f38d 100644 --- a/lib/data_services/core/tim_uikit_config.dart +++ b/lib/data_services/core/tim_uikit_config.dart @@ -1,4 +1,6 @@ import 'package:flutter/cupertino.dart'; +import 'package:tencent_cloud_chat_uikit/data_services/core/tim_uikit_wide_modal_operation_key.dart'; +import 'package:tencent_im_base/theme/tui_theme.dart'; class TIMUIKitConfig { /// Control if show online status of friends or contacts. @@ -18,8 +20,34 @@ class TIMUIKitConfig { /// The configuration of border radius for all the avatar shows in TUIKit. final BorderRadius? defaultAvatarBorderRadius; - const TIMUIKitConfig({ + /// You can use this function to customize the Modal that shows on desktop. + /// Do not specified or return `false` will use our default implementation. + final Future Function( + TUIKitWideModalOperationKey operationKey, + BuildContext context, + Widget Function(VoidCallback closeFunc) child, + TUITheme? theme, + double? width, + double? height, + Offset? offset, + String? initText, + BorderRadius? borderRadius, + bool? isDarkBackground, + String? title, + VoidCallback? onSubmit, + Widget? submitWidget, + VoidCallback? onConfirm, + VoidCallback? onCancel, + )? showDesktopModalFunc; + + /// Determines whether TUIKit should preload some messages after initialization for faster message display, + /// with a default value of `true`, and backward-compatibility. + final bool isPreloadMessagesAfterInit; + + const TIMUIKitConfig( { this.defaultAvatarAssetPath, + this.showDesktopModalFunc, + this.isPreloadMessagesAfterInit = true, this.defaultAvatarBorderRadius, this.isCheckDiskStorageSpace = true, this.isShowOnlineStatus = true, diff --git a/lib/data_services/core/tim_uikit_wide_modal_operation_key.dart b/lib/data_services/core/tim_uikit_wide_modal_operation_key.dart new file mode 100644 index 0000000..98627a4 --- /dev/null +++ b/lib/data_services/core/tim_uikit_wide_modal_operation_key.dart @@ -0,0 +1,43 @@ + +enum TUIKitWideModalOperationKey{ + + /// You could use this value for your own Modal usage. + custom, + + // The following values are used in TUIKit + + conversationSecondaryMenu, + chooseCountry, + beforeSendScreenShot, + showUserProfileFromChat, + addNewContact, + showBlockedUsers, + chooseContacts, + addFriend, + addGroup, + chooseGroupType, + settings, + contactUs, + aboutUs, + showConditionsAndTerms, + secondaryClickUserAvatar, + forward, + messageReadDetails, + mergerMessageList, + chooseMentionedMembers, + chatHistory, + groupAddOpt, + setMute, + setUnmute, + setAdmins, + deleteAdmin, + groupMembersList, + addGroupMembers, + kickOffGroupMembers, + confirmDeleteMessages, + confirmClearChatHistory, + confirmExitGroup, + confirmDisbandGroup, + confirmGeneral, + unableToSendDueToFolders +} \ No newline at end of file diff --git a/lib/data_services/group/group_services_implement.dart b/lib/data_services/group/group_services_implement.dart index 986d58e..89c3b2d 100644 --- a/lib/data_services/group/group_services_implement.dart +++ b/lib/data_services/group/group_services_implement.dart @@ -105,6 +105,7 @@ class GroupServicesImpl extends GroupServices { count: count, offset: offset); if (res.code != 0) { + _coreService.callOnCallback(TIMCallback( type: TIMCallbackType.API_ERROR, errorMsg: res.desc, diff --git a/lib/data_services/message/message_service_implement.dart b/lib/data_services/message/message_service_implement.dart index a1ae652..405567a 100644 --- a/lib/data_services/message/message_service_implement.dart +++ b/lib/data_services/message/message_service_implement.dart @@ -127,14 +127,14 @@ class MessageServiceImpl extends MessageService { lastMsgID: lastMsgID, lastMsgSeq: lastMsgSeq, messageTypeList: messageTypeList); - final reponseMessageList = res.data; + final responseMessageList = res.data; if (res.code != 0) { _coreService.callOnCallback(TIMCallback( type: TIMCallbackType.API_ERROR, errorMsg: res.desc, errorCode: res.code)); } - return reponseMessageList; + return responseMessageList; } @override @@ -812,4 +812,5 @@ class MessageServiceImpl extends MessageService { } return result.data?[text] ?? ""; } + } diff --git a/lib/i18n/strings.i18n.json b/lib/i18n/strings.i18n.json index 689dba7..b441b8d 100644 --- a/lib/i18n/strings.i18n.json +++ b/lib/i18n/strings.i18n.json @@ -1 +1 @@ -{"k_1fdhj9g":"This version does not support the message","k_06pujtm":"Accept all friend requests","k_0gyhkp5":"Require approval for friend requests","k_121ruco":"Reject all friend requests","k_05nspni":"Custom field","k_03fchyy":"Group profile photo","k_03i9mfe":"Group introduction","k_03agq58":"Group name","k_039xqny":"Group notification","k_003tr0a":"Group owner","k_002wddw":"Mute","k_0got6f7":"Unmute","k_1uaqed6":"[Custom]","k_0z2z7rx":"[Voice]","k_0y39ngu":"[Emoji]","k_0y1a2my":"[Image]","k_0z4fib8":"[Video]","k_0y24mcg":"[Location]","k_0pewpd1":"[Chat history]","k_13s8d9p":"Unknown message","k_003qkx2":"Calendar","k_003n2pz":"Camera","k_03idjo0":"Contact","k_003ltgm":"Location","k_02k3k86":"Mic","k_003pm7l":"Album","k_15ao57x":"Album write","k_164m3jd":"Local storage","k_03r6qyx":"We need your approval to get information.","k_02noktt":"Reject","k_00043x4":"Agree","k_003qzac":"Yesterday","k_003r39d":"2 days ago","k_03fqp9o":"Sun","k_03ibg5h":"Mon","k_03i7hu1":"Tue","k_03iaiks":"Wed","k_03el9pa":"Thu","k_03i7ok1":"Fri","k_03efxyg":"Sat","k_003q7ba":"Afternoon","k_003q7bb":"Morning","k_003pu3h":"Now","k_002rflt":"Delete","k_1don84v":"Failed to locate the original message","k_003q5fi":"Copy","k_003prq0":"Forward","k_002r1h2":"Multiple-choice","k_003j708":"Reference","k_003pqpr":"Recall","k_03ezhho":"Copied","k_11ctfsz":"Not implemented","k_1hbjg5g":"[Group system message]","k_03tvswb":"[Unknown message]","k_155cj23":"You've recalled a message.","k_0gapun3":"Edit it again","k_0003z7x":"You","k_002wfe4":"Read","k_002wjlg":"Unread","k_003nevv":"Cancel","k_001nmhu":"Open with another app","k_105682d":"Failed to load the image","k_0pytyeu":"Image saved successfully","k_0akceel":"Failed to save the image","k_003rk1s":"Save","k_04a0awq":"[Voice message]","k_105c3y3":"Failed to load the video","k_176rzr7":"Chat history","k_002r305":"Send","k_003n8b0":"Shoot","k_003tnp0":"File","k_0ylosxn":"Custom message","k_0jhdhtp":"Sending failed. The video cannot exceed 100 MB.","k_0am7r68":"Slide up to cancel","k_13dsw4l":"Release to cancel","k_15jl6qw":"Too short","k_0gx7vl6":"Press and hold to talk","k_15dlafd":"One-by-one forward","k_15dryxy":"Combine and forward","k_1eyhieh":"Are you sure you want to delete the selected message?","k_118prbn":"Search globally","k_003kv3v":"Search","k_17fmlyf":"Clear chat","k_0dhesoz":"Unpin from top","k_002sk7x":"Pin to top","k_003ll77":"Draft","k_003kfai":"Unknown","k_13dq4an":"Automatic approval","k_0l13cde":"Admin approval","k_11y8c6a":"Disallow group joining","k_1kvyskd":"Modification failed due to network disconnection","k_16payqf":"Group joining mode","k_0vzvn8r":"Modify group name","k_003rzap":"OK","k_038lh6u":"Group management","k_0k5wyiy":"Set admin","k_0goiuwk":"Mute all","k_1g889xx":"If you mute all, only the group owner and admin can speak.","k_0wlrefq":"Add group members to mute","k_0goox5g":"Mute","k_08daijh":"Admin role canceled successfully","k_0k5u935":"Add admin","k_003ngex":"Complete","k_03enyx5":"Group member","k_03erpei":"Admin","k_0qi9tno":"Group owner and admin","k_0uj7208":"Failed to view the group members due to network disconnection","k_0ef2a12":"Modify my nickname in group","k_1aajych":"2–20 characters, including digits, letters, and underscores","k_137pab5":"My nickname in group","k_0ivim6d":"No group notice","k_03eq6cn":"Group notice","k_002vxya":"Modify","k_03gu05e":"Chat room","k_03b4f3p":"Meeting group","k_03avj1p":"Public group","k_03asq2g":"Work group","k_03b3hbi":"Unknown group","k_03es1ox":"Group type","k_003mz1i":"Agree","k_003lpre":"Reject","k_003qk66":"Profile photo","k_003lhvk":"Nickname","k_003ps50":"Account","k_15lx52z":"Status","k_003qgkp":"Gender","k_003m6hr":"Date of birth","k_0003v6a":"Male","k_00043x2":"Female","k_03bcjkv":"Not set","k_11s0gdz":"Modify nickname","k_0p3j4sd":"Allows only letters, digits, and underscores","k_15lyvdt":"Modify status","k_0vylzjp":"None","k_1hs7ese":"Modify it later","k_03exjk7":"Remarks","k_0s3skfd":"Add to blocklist","k_17fpl3y":"Pin chat to top","k_0p3b31s":"Modify remarks","k_0003y9x":"None","k_11zgnfs":"Profile","k_1tez2xl":"No status","k_0vjj2kp":"Group chat history","k_003n2rp":"Select","k_1m9exwh":"Recent contacts","k_119nwqr":"The input cannot be empty","k_0pzwbmg":"Video saved successfully","k_0aktupv":"Failed to save the video","k_1yemzyd":"Received a message","k_13sajrj":"Emoji message","k_13sjeb7":"File message","k_0yd2ft8":"Group notification","k_13s7mxn":"Image message","k_13satlt":"Location message","k_00bbtsx":"Combined message","k_13sqwu4":"Voice message","k_13sqjjp":"Video message","k_03iqsh4":" $s to ","k_191t5n4":"$opUserNickName changed ","k_1pg6aoj":"$opUserNickName quit group chat","k_1f6zt3v":"Invite $invitedMemberString to the group","k_0y7zd07":"Remove $invitedMemberString from the group","k_1d5mshh":"User $joinedMemberString joined the group","k_0yenqf0":"$userName was","k_0spotql":"Set $adminMember as admin","k_0pg5zzj":"System message: $operationType","k_1c7z88n":"[File] $fileName","k_1c3us5n":"The current group does not support @all","k_11k579v":"Invalid statements detected","k_0qba4ns":" attempted to access your $yoursItem","k_0oozw9x":"$diffMinutes minutes ago","k_13hzn00":"$yesterday, yesterday","k_0n9pyxz":"The user does not exist","k_1bjwemh":"Search by user ID","k_02owlq8":"My user ID: $userID","k_1wu8h4x":"Me: $showName","k_16758qw":"Add friend","k_1shx4d9":"Status: $selfSignature","k_0i553x0":"Enter verification information","k_031ocwx":"Enter remarks and list","k_003ojje":"Remarks","k_003lsav":"List","k_167bdvq":"My friends","k_156b4ut":"Friend request sent","k_1loix7s":"Group type: $groupType","k_1lqbsib":"The group chat does not exist","k_03h153m":"Search by group ID","k_0oxak3r":"Group request sent","k_1uh417q":"$displayName recalled a message","k_1aszp2k":"Are you sure you want to send the message again?","k_0h1ygf8":"Call initiated","k_0h169j0":"Call canceled","k_0h13jjk":"Call accepted","k_0h19hfx":"Call rejected","k_0obi9lh":"No answer","k_0ohzb9l":"Call duration: $callTime","k_0y9u662":"$appName currently does not support this file type. You can use another app to open and preview the file.","k_1ht1b80":"Receiving","k_0d5z4m5":"Select reminder receiver","k_1665ltg":"Initiate call","k_003kthh":"Photo","k_119ucng":"The image cannot be empty","k_0w9x8gw":"Selected successfully: $successPath","k_1np495n":"$messageString[Someone@me]","k_1m797yi":"$messageString[@all]","k_1uaov41":"Search for chat content","k_0bxm97s":"Admin ($adminNum/10)","k_0jayw3z":"Group members ($groupMemberNum members)","k_0h1svv1":"Delete group member","k_0h1g636":"Add group member","k_01yfa4o":"$memberCount members","k_0hpukyx":"View more group members","k_0qtsar0":"Mute notifications","k_03xd79d":"Status: $signature","k_1m9dftc":"All contacts","k_0em4gyz":"All group chats","k_002twmj":"Group chat","k_09kga0d":"More chat history","k_1ui5lzi":"$count messages are found","k_09khmso":"Related chat records","k_1kevf4k":"Chat history with $receiver","k_03ignw6":"All","k_03icaxo":"Custom","k_1969986":"[Voice Call]:$callingLastMsgShow","k_1960dlr":"[Video Call]:$callingLastMsgShow","k_1qbg9xc":"$option8 to ","k_1wq5ubm":"$option7 changed ","k_0y5pu80":"$option6 quit group chat","k_0nl7cmd":"Invite $option5 to the group","k_1ju5iqw":"Remove $option4 from the group","k_1ovt677":"User $option3 joined the group","k_0k05b8b":"$option2 was ","k_0wm4xeb":"System message: $option2","k_0nbq9v3":"Call duration: $option2","k_0i1kf53":"[File] $option2","k_1gnnby6":" attempted to access your $option2","k_1wh4atg":"$option2 minutes ago","k_07sh7g1":"$option2, yesterday","k_1pj8xzh":"My user ID: $option2","k_0py1evo":"Status: $option2","k_1kvj4i2":"$option2 recalled a message","k_1v0lbpp":"$option2 currently does not support this file type. You can use another app to open and preview the file.","k_0torwfz":"Selected successfully: $option2","k_0i1bjah":"$option1 recalled a message","k_1qzxh9q":"Call duration: $option3","k_0wrgmom":"[Voice Call]:$option1","k_06ix2f0":"[Video Call]:$option2","k_08o3z5w":"[File] $option1","k_0ezbepg":"$option2[Someone@me]","k_1ccnht1":"$option2[@all]","k_1k3arsw":"Admin ($option2/10)","k_1d4golg":"Group members ($option1 members)","k_1bg69nt":"$option1 members","k_00gjqxj":"Status: $option1","k_0c29cxr":"$option1 messages are found","k_1twk5rz":"Chat history with $option1","k_18o68ro":"Allow ","k_1onpf8u":" to access your camera to take photos, record videos, and make video calls.","k_17irga5":" to access your microphone to send voice messages, record videos, and make voice/video calls.","k_0572kc4":" to access your photos to send images and videos.","k_0slykws":" to access your album to save images and videos.","k_119pkcd":" to access your files to view, select and send files in a chat.","k_03c49qt":"Authorize now","k_0nt2uyg":"Back to the bottom","k_04l16at":"$option1 new messages","k_13p3w93":"Someone @ me","k_18w5uk6":"@ all","k_0jmujgh":"You are receiving other files","k_12s5ept":"Message details","k_0mxa4f4":"$option1 read","k_061tue3":"$option2 unread","k_1vn4xq1":"remove $adminMember from admin","k_0e35hsw":"Please allow us to use your camera to capture photos and videos sending to your friends and make video calls.","k_0dj6yr7":"Please allow us to use your microphone for sending voice message, make video/audio calls.","k_003qnsl":"Save","k_0s3rtpw":"Please allow us to access the media and files on your devices, in order to select and send to your friend, or save from them.","k_0tezv85":" Would like to access $option2","k_002rety":" permission. ","k_0gqewd3":"Later","k_03eq4s1":"Authorize Now","k_18qjstb":"Transfer Group","k_0on1aj2":"$option2 messages @ me","k_09j4izl":"[Someone @ me] ","k_1oqtjw0":"[@ all] ","k_1x5a9vb":"This is: $option1","k_14n31e7":"Add Group","k_08nc5j1":"Group type: $option1","k_1josu12":"$option1 group joining request(s)","k_0n2x5s0":"Verification message: $option2","k_03c1nx0":"Agreed","k_03aw9w8":"Rejected","k_038ryos":"Handle now","k_0gw8pum":"Add Group","k_1gcvfrj":"Please fill in the remarks","k_002v9zj":"确认","k_10oqrki":"轻触拍照","k_0f8b3ws":"加载失败","k_11cm5lm":"手动聚焦","k_002uzrd":"预览","k_003qkn3":"录像","k_003k6a7":"拍照","k_0bqpqco":"拍照按钮","k_1626ozl":"停止录像","k_003lvmu":"前置","k_003lued":"后置","k_003lwzh":"外置","k_002qzi3":"关闭","k_003pufb":"自动","k_0apm0ze":"拍照时闪光","k_157zog5":"始终闪光","k_0cfyqhy":"$option1 画面预览","k_0phctlz":"闪光模式: $option2","k_02vfqe0":"切换至 $option3 摄像头","k_0f0y9ex":"说话时间太短","k_0ln70tk":"无法打开URL","k_11a3jdv":"轻触拍照,长按摄像","k_1k18miv":"请传入离开群组生命周期函数,提供返回首页或其他页面的导航方法。","k_1fu9ahv":"全员禁言状态","k_0gmwbnd":"全员禁言中","k_0got2zr":"您被禁言","k_0y9jck8":"你必须自定义search bar,并处理点击跳转","k_0yum3tv":"如使用自定义区域,请在profileWidgetBuilder传入对应组件","k_09kalj0":"清空聊天记录","k_14j5iul":"删除并退出","k_125ru1w":"解散该群","k_0jtutmw":"退出后不会接收到此群聊消息","k_0jtzmqa":"解散后不会接收到此群聊消息","k_0r8fi93":"好友添加成功","k_02qw14e":"好友申请已发出","k_0n3md5x":"当前用户在黑名单","k_094phq4":"好友添加失败","k_129scag":"好友删除成功","k_129uzfn":"好友删除失败","k_1666isy":"清除好友","k_1679vrd":"加为好友","k_1ualc52":"看看对方带来的数据是啥","k_0szluvp":"设置对方在线状态","k_0f4rnf8":"该用户已是好友","k_1tdkom4":"您已是群成员","k_1p2lyuz":"对方正在输入中...","k_1g8wfpy":"...共$option1人","k_12rv9vw":"回应详情","k_0havgi0":"[查看详情 >>](${linkMessage.link})","k_0n9p7g8":"群组不存在","k_1tdh5vn":"您不是群成员","k_0h1q57v":"暂无群成员","k_0y5drq1":"[查看详情 >>]($option1)","k_03pjp61":"[表情消息]","k_1jpvzul":"[自定义消息]","k_03u3bh1":"[文件消息]","k_1odsnsw":"[群消息]","k_03sel4t":"[图片消息]","k_03sfw3r":"[位置消息]","k_03xpuwq":"[合并消息]","k_07ycxwo":"[没有元素]","k_03rc9vz":"[文本消息]","k_046uopf":"[视频消息]","k_0ehmsun":"设备存储空间不足,建议清理,以获得更好使用体验","k_003kmos":"图片","k_002s86q":"视频","k_06bk5ei":"视频消息仅限 mp4 格式","k_13opfxf":"Web网页端不支持搜索","k_1i0o0y2":"暂时仅限 Android/iOS 端","k_045dtzl":"$option1的聊天记录","k_0t0131u":"群资料信息","k_18ok8xz":"消息接收方式","k_03ax3ks":"群资料","k_0sqvoqo":"将 $option1 设置为管理员","k_1gbg1v8":"将 $option1 取消管理员","k_17k64g4":"群聊创建成功!","k_05mn217":"暂未安装表情包插件,如需使用表情相关功能,请根据本文档安装:https://cloud.tencent.com/document/product/269/70746","k_14j17nz":"暂无表情包","k_0fvjexh":"正在下载中","k_1cdagzz":"已加入待下载队列,其他文件下载中","k_0g4vojc":"开始下载","k_1g32es3":"[调皮]@2x.png","k_1g8qorz":"[爱你]@2x.png","k_1g4hmx6":"[爱情]@2x.png","k_1g6b558":"[爱心]@2x.png","k_1g3m4su":"[傲慢]@2x.png","k_1g2jym7":"[白眼]@2x.png","k_0cgkxuw":"[棒棒糖]@2x.png","k_1g48br2":"[抱抱]@2x.png","k_1g49ol8":"[抱拳]@2x.png","k_1g0ras3":"[爆筋]@2x.png","k_1ghy881":"[鄙视]@2x.png","k_1g86bmv":"[闭嘴]@2x.png","k_1g1xs1p":"[鞭炮]@2x.png","k_1g8i6ri":"[便便]@2x.png","k_1g2u5kf":"[擦汗]@2x.png","k_1g60uwh":"[彩带]@2x.png","k_1g1o0d0":"[彩球]@2x.png","k_1g6a6yq":"[菜刀]@2x.png","k_1g6vqo2":"[差劲]@2x.png","k_1g0kvjc":"[钞票]@2x.png","k_1g65x7e":"[车厢]@2x.png","k_0e1tjol":"[打哈欠]@2x.png","k_1g65n58":"[大兵]@2x.png","k_1g7se7o":"[大哭]@2x.png","k_1g03868":"[蛋糕]@2x.png","k_1h8nm66":"[刀]@2x.png","k_1g3dlpi":"[得意]@2x.png","k_1g3u434":"[灯泡]@2x.png","k_1giuqs7":"[凋谢]@2x.png","k_1g8r0r9":"[多云]@2x.png","k_1g7k6i1":"[发呆]@2x.png","k_1g44zsp":"[发抖]@2x.png","k_1g5l96i":"[飞机]@2x.png","k_1g7wsqj":"[飞吻]@2x.png","k_1g49luq":"[奋斗]@2x.png","k_1gixbsm":"[风车]@2x.png","k_1g6cqbq":"[尴尬]@2x.png","k_1g6jbw5":"[勾引]@2x.png","k_1g3lwo1":"[鼓掌]@2x.png","k_1g13nkj":"[害羞]@2x.png","k_1g0mt47":"[憨笑]@2x.png","k_0bxujkf":"[红灯笼]@2x.png","k_0hhaeh8":"[红双喜]@2x.png","k_1g0jnts":"[坏笑]@2x.png","k_1g46g9c":"[挥手]@2x.png","k_1g4vi9g":"[回头]@2x.png","k_1gf7hes":"[饥饿]@2x.png","k_1g6mvsm":"[激动]@2x.png","k_1gku5mf":"[街舞]@2x.png","k_1g4hidg":"[惊恐]@2x.png","k_1gjbrtu":"[惊讶]@2x.png","k_1g6sand":"[咖啡]@2x.png","k_1g4s8rj":"[磕头]@2x.png","k_1g1wn34":"[可爱]@2x.png","k_1g3l0wd":"[可怜]@2x.png","k_1ggaon9":"[抠鼻]@2x.png","k_1ggvcb0":"[骷髅]@2x.png","k_1h8yqjt":"[酷]@2x.png","k_0jac97i":"[快哭了]@2x.png","k_1h8oiby":"[困]@2x.png","k_1g0s5hg":"[蜡烛]@2x.png","k_1g1iuer":"[篮球]@2x.png","k_1g2xjfi":"[冷汗]@2x.png","k_0s5oyqw":"[礼品袋]@2x.png","k_1g1qqvf":"[礼物]@2x.png","k_1g2slew":"[流汗]@2x.png","k_1g3z9xx":"[流泪]@2x.png","k_1g6pabn":"[麻将]@2x.png","k_0pkaxul":"[麦克风]@2x.png","k_1g7m0zj":"[猫咪]@2x.png","k_0ibvtpo":"[么么哒]@2x.png","k_1g1hoh1":"[玫瑰]@2x.png","k_1gfzeow":"[米饭]@2x.png","k_1g5l15p":"[面条]@2x.png","k_1g2hfa6":"[奶瓶]@2x.png","k_1gix9pj":"[难过]@2x.png","k_1giqn6g":"[闹钟]@2x.png","k_1h8kd64":"[怒]@2x.png","k_1g0vui9":"[怄火]@2x.png","k_1g1jsj7":"[皮球]@2x.png","k_1ghdluw":"[啤酒]@2x.png","k_1gl6ec7":"[瓢虫]@2x.png","k_1g7gg5p":"[撇嘴]@2x.png","k_1g8psin":"[乒乓]@2x.png","k_1gjzu3p":"[汽车]@2x.png","k_1h8mr0k":"[强]@2x.png","k_1g45y2n":"[敲打]@2x.png","k_1gkaxsl":"[青蛙]@2x.png","k_0jcfnoo":"[糗大了]@2x.png","k_1g4njy1":"[拳头]@2x.png","k_1h8mqr3":"[弱]@2x.png","k_1h926fg":"[色]@2x.png","k_1g6rtbq":"[沙发]@2x.png","k_1giirh6":"[删除]@2x.png","k_1g14ny9":"[闪电]@2x.png","k_1g6bmsr":"[胜利]@2x.png","k_1g1rytx":"[示爱]@2x.png","k_1g52fbz":"[手枪]@2x.png","k_1h90dam":"[衰]@2x.png","k_1gigiae":"[睡觉]@2x.png","k_1gijchz":"[太阳]@2x.png","k_1g1sgji":"[跳绳]@2x.png","k_1gjwuri":"[跳跳]@2x.png","k_1g0juhk":"[偷笑]@2x.png","k_1h8nzla":"[吐]@2x.png","k_1g6cv0i":"[委屈]@2x.png","k_1g46l5g":"[握手]@2x.png","k_1g2pgkd":"[西瓜]@2x.png","k_1ging9p":"[下雨]@2x.png","k_1h8nzil":"[吓]@2x.png","k_1g7q7wr":"[献吻]@2x.png","k_1gl6uum":"[香蕉]@2x.png","k_1g23fys":"[象棋]@2x.png","k_0j75rdh":"[心碎了]@2x.png","k_1g6ajj2":"[信封]@2x.png","k_1g21prz":"[熊猫]@2x.png","k_1h8octi":"[嘘]@2x.png","k_1h91zox":"[药]@2x.png","k_1ghttfl":"[疑问]@2x.png","k_1ghk7sz":"[阴险]@2x.png","k_0gl37zz":"[右车头]@2x.png","k_0ifkj1p":"[右哼哼]@2x.png","k_0g1yh2e":"[右太极]@2x.png","k_1g9dkfc":"[雨伞]@2x.png","k_1g8jl88":"[月亮]@2x.png","k_1h8lhqj":"[晕]@2x.png","k_1gi9x2q":"[再见]@2x.png","k_1g6dwwv":"[炸弹]@2x.png","k_1fzmkfi":"[折磨]@2x.png","k_1g6jbiw":"[纸巾]@2x.png","k_1ggjnwu":"[咒骂]@2x.png","k_1g4qlq8":"[猪头]@2x.png","k_1g1lqzz":"[抓狂]@2x.png","k_1g80j3u":"[转圈]@2x.png","k_1g0z55s":"[龇牙]@2x.png","k_1g3ju6v":"[钻戒]@2x.png","k_0gl51l6":"[左车头]@2x.png","k_0iflllk":"[左哼哼]@2x.png","k_0g1y3ir":"[左太极]@2x.png","k_026hiq5":"消息列表加载中","k_003tu8k":"爱你","k_003myvp":"傲慢","k_003kddw":"白眼","k_039yfhv":"棒棒糖","k_003nu3p":"抱抱","k_003nijr":"抱拳","k_003mg88":"爆筋","k_002v17e":"鄙视","k_003qhy4":"闭嘴","k_003l5fq":"鞭炮","k_003uacl":"便便","k_003oq1g":"擦汗","k_003qvey":"彩带","k_003jci7":"彩球","k_003pyu1":"菜刀","k_003q97d":"差劲","k_003po5d":"车厢","k_03eadb2":"打哈欠","k_003pnuf":"大兵","k_003kg57":"蛋糕","k_003mxkt":"得意","k_003onu3":"灯泡","k_002uv8s":"凋谢","k_003kqy0":"调皮","k_003tyum":"多云","k_003pv9u":"发呆","k_036o6mu":"发抖t","k_003nogx":"飞机","k_003q7wg":"飞吻","k_003m0jd":"奋斗","k_002ult9":"风车","k_003r8gt":"尴尬","k_003qy4u":"勾引","k_003mnoa":"鼓掌","k_003lmw8":"害羞","k_003mb30":"憨笑","k_03bj41g":"红灯笼","k_03dxw2f":"红双喜","k_003mk57":"坏笑","k_003nmvf":"挥手","k_003r2i7":"回头","k_002s6f3":"饥饿","k_003qd0t":"激动","k_002vgi4":"街舞","k_003nz33":"惊恐","k_002wh4p":"惊讶","k_003ozpu":"咖啡","k_003qvs4":"磕头","k_003l3wb":"可爱","k_003nuwm":"可怜","k_002rw1q":"抠鼻","k_002tujb":"骷髅","k_00030eq":"酷","k_03i8ath":"快哭了","k_000421h":"困","k_003l5i7":"蜡烛","k_003j72g":"篮球","k_003ofwl":"冷汗","k_02mw65v":"礼品袋","k_003ku40":"礼物","k_003ookz":"流汗","k_003on72":"流泪","k_003rjy0":"麻将","k_003q2f8":"猫咪","k_03et393":"么么哒","k_003j7j2":"玫瑰","k_002sr0b":"米饭","k_003nnza":"面条","k_003jef9":"奶瓶","k_002umn0":"难过","k_002rjib":"闹钟","k_0003zcn":"怒","k_003jzwq":"怄火","k_003j4js":"皮球","k_002r5ir":"啤酒","k_002ubu4":"瓢虫","k_003ppo6":"撇嘴","k_003ty3o":"乒乓","k_002vxwe":"汽车","k_00043hb":"强","k_003nmbo":"敲打","k_002tfhq":"青蛙","k_03i7lrn":"糗大了","k_003r03m":"拳头","k_00043h0":"弱","k_000345z":"色","k_003qmp9":"沙发","k_003it8a":"闪电","k_003pxow":"胜利","k_003kw8e":"示爱","k_003n99g":"手枪","k_00035cl":"衰","k_002vl3h":"睡觉","k_002rgqk":"太阳","k_003m9d1":"跳绳","k_002vobp":"跳跳","k_003mkoz":"偷笑","k_00041px":"吐","k_003rjh5":"委屈","k_003j36u":"西瓜","k_002re92":"下雨","k_00041py":"吓","k_003q06o":"献吻","k_002ubjp":"香蕉","k_003o2tr":"象棋","k_03ie6pa":"心碎了","k_003rao5":"信封","k_003l3us":"熊猫","k_000424d":"嘘","k_00033yi":"药","k_002qtyy":"疑问","k_002qe0o":"阴险","k_03gu7us":"右车头","k_03ere8m":"右哼哼","k_003uqk3":"雨伞","k_003tzdv":"月亮","k_0003z00":"晕","k_002vdrd":"再见","k_003ra1w":"炸弹","k_003lcad":"折磨","k_003q7sz":"纸巾","k_002thn9":"咒骂","k_003qx7f":"猪头","k_003l044":"抓狂","k_003qg4h":"转圈","k_003kb97":"龇牙","k_03gu53l":"左车头","k_03erd1f":"左哼哼","k_003nyvl":"爱情","k_003r85z":"爱心","k_003mk8j":"钞票","k_003pwfj":"大哭","k_00042w5":"刀","k_003nmtr":"握手","k_03c529p":"右太极","k_003n4mk":"钻戒","k_03c5488":"左太极","k_1llp7tu":"该用户不存在","k_0tbyqyb":"加载中…","k_0td1p3f":"保存中…","k_1klqdh1":"仅限汉字、英文、数字和下划线","k_03el5lp":"未填写","k_1ui0gai":"搜索指定内容","k_003nvk2":"消息","k_03agld7":"群提示","k_002wkr3":"翻译","k_13g4hxv":"翻译完成","k_1qqgjra":"$option3条未读消息","k_0uubyjr":"以下为未读消息"} \ No newline at end of file +{"k_1fdhj9g":"This version does not support the message","k_06pujtm":"Accept all friend requests","k_0gyhkp5":"Require approval for friend requests","k_121ruco":"Reject all friend requests","k_05nspni":"Custom field","k_03fchyy":"Group profile photo","k_03i9mfe":"Group introduction","k_03agq58":"Group name","k_039xqny":"Group notification","k_003tr0a":"Group owner","k_002wddw":"Mute","k_0got6f7":"Unmute","k_1uaqed6":"[Custom]","k_0z2z7rx":"[Voice]","k_0y39ngu":"[Emoji]","k_0y1a2my":"[Image]","k_0z4fib8":"[Video]","k_0y24mcg":"[Location]","k_0pewpd1":"[Chat history]","k_13s8d9p":"Unknown message","k_003qkx2":"Calendar","k_003n2pz":"Camera","k_03idjo0":"Contact","k_003ltgm":"Location","k_02k3k86":"Mic","k_003pm7l":"Album","k_15ao57x":"Album write","k_164m3jd":"Local storage","k_03r6qyx":"We need your approval to get information.","k_02noktt":"Reject","k_00043x4":"Agree","k_003qzac":"Yesterday","k_003r39d":"2 days ago","k_03fqp9o":"Sun","k_03ibg5h":"Mon","k_03i7hu1":"Tue","k_03iaiks":"Wed","k_03el9pa":"Thu","k_03i7ok1":"Fri","k_03efxyg":"Sat","k_003q7ba":"Afternoon","k_003q7bb":"Morning","k_003pu3h":"Now","k_002rflt":"Delete","k_1don84v":"Failed to locate the original message","k_003q5fi":"Copy","k_003prq0":"Forward","k_002r1h2":"Multiple-choice","k_003j708":"Reference","k_003pqpr":"Recall","k_03ezhho":"Copied","k_11ctfsz":"Not implemented","k_1hbjg5g":"[Group system message]","k_03tvswb":"[Unknown message]","k_155cj23":"You've recalled a message.","k_0gapun3":"Edit it again","k_0003z7x":"You","k_002wfe4":"Read","k_002wjlg":"Unread","k_003nevv":"Cancel","k_001nmhu":"Open with another app","k_105682d":"Failed to load the image","k_0pytyeu":"Image saved successfully","k_0akceel":"Failed to save the image","k_003rk1s":"Save","k_04a0awq":"[Voice message]","k_105c3y3":"Failed to load the video","k_176rzr7":"Chat history","k_002r305":"Send","k_003n8b0":"Shoot","k_003tnp0":"File","k_0ylosxn":"Custom message","k_0jhdhtp":"Sending failed. The video cannot exceed 100 MB.","k_0am7r68":"Slide up to cancel","k_13dsw4l":"Release to cancel","k_15jl6qw":"Too short","k_0gx7vl6":"Press and hold to talk","k_15dlafd":"One-by-one forward","k_15dryxy":"Combine and forward","k_1eyhieh":"Are you sure you want to delete the selected message?","k_118prbn":"Search globally","k_003kv3v":"Search","k_17fmlyf":"Clear chat","k_0dhesoz":"Unpin from top","k_002sk7x":"Pin to top","k_003ll77":"Draft","k_003kfai":"Unknown","k_13dq4an":"Automatic approval","k_0l13cde":"Admin approval","k_11y8c6a":"Disallow group joining","k_1kvyskd":"Modification failed due to network disconnection","k_16payqf":"Group joining mode","k_0vzvn8r":"Modify group name","k_003rzap":"OK","k_038lh6u":"Group management","k_0k5wyiy":"Set admin","k_0goiuwk":"Mute all","k_1g889xx":"If you mute all, only the group owner and admin can speak.","k_0wlrefq":"Add group members to mute","k_0goox5g":"Mute","k_08daijh":"Admin role canceled successfully","k_0k5u935":"Add admin","k_003ngex":"Complete","k_03enyx5":"Group member","k_03erpei":"Admin","k_0qi9tno":"Group owner and admin","k_0uj7208":"Failed to view the group members due to network disconnection","k_0ef2a12":"Modify my nickname in group","k_1aajych":"2–20 characters, including digits, letters, and underscores","k_137pab5":"My nickname in group","k_0ivim6d":"No group notice","k_03eq6cn":"Group notice","k_002vxya":"Modify","k_03gu05e":"Chat room","k_03b4f3p":"Meeting group","k_03avj1p":"Public group","k_03asq2g":"Work group","k_03b3hbi":"Unknown group","k_03es1ox":"Group type","k_003mz1i":"Agree","k_003lpre":"Reject","k_003qk66":"Profile photo","k_003lhvk":"Nickname","k_003ps50":"Account","k_15lx52z":"Status","k_003qgkp":"Gender","k_003m6hr":"Date of birth","k_0003v6a":"Male","k_00043x2":"Female","k_03bcjkv":"Not set","k_11s0gdz":"Modify nickname","k_0p3j4sd":"Allows only letters, digits, and underscores","k_15lyvdt":"Modify status","k_0vylzjp":"None","k_1hs7ese":"Modify it later","k_03exjk7":"Remarks","k_0s3skfd":"Add to blocklist","k_17fpl3y":"Pin chat to top","k_0p3b31s":"Modify remarks","k_0003y9x":"None","k_11zgnfs":"Profile","k_1tez2xl":"No status","k_0vjj2kp":"Group chat history","k_003n2rp":"Select","k_1m9exwh":"Recent contacts","k_119nwqr":"The input cannot be empty","k_0pzwbmg":"Video saved successfully","k_0aktupv":"Failed to save the video","k_1yemzyd":"Received a message","k_13sajrj":"Emoji message","k_13sjeb7":"File message","k_0yd2ft8":"Group notification","k_13s7mxn":"Image message","k_13satlt":"Location message","k_00bbtsx":"Combined message","k_13sqwu4":"Voice message","k_13sqjjp":"Video message","k_03iqsh4":" $s to ","k_191t5n4":"$opUserNickName changed ","k_1pg6aoj":"$opUserNickName quit group chat","k_1f6zt3v":"Invite $invitedMemberString to the group","k_0y7zd07":"Remove $invitedMemberString from the group","k_1d5mshh":"User $joinedMemberString joined the group","k_0yenqf0":"$userName was","k_0spotql":"Set $adminMember as admin","k_0pg5zzj":"System message: $operationType","k_1c7z88n":"[File] $fileName","k_1c3us5n":"The current group does not support @all","k_11k579v":"Invalid statements detected","k_0qba4ns":" attempted to access your $yoursItem","k_0oozw9x":"$diffMinutes minutes ago","k_13hzn00":"$yesterday, yesterday","k_0n9pyxz":"The user does not exist","k_1bjwemh":"Search by user ID","k_02owlq8":"My user ID: $userID","k_1wu8h4x":"Me: $showName","k_16758qw":"Add friend","k_1shx4d9":"Status: $selfSignature","k_0i553x0":"Enter verification information","k_031ocwx":"Enter remarks and list","k_003ojje":"Remarks","k_003lsav":"List","k_167bdvq":"My friends","k_156b4ut":"Friend request sent","k_1loix7s":"Group type: $groupType","k_1lqbsib":"The group chat does not exist","k_03h153m":"Search by group ID","k_0oxak3r":"Group request sent","k_1uh417q":"$displayName recalled a message","k_1aszp2k":"Are you sure you want to send the message again?","k_0h1ygf8":"Call initiated","k_0h169j0":"Call canceled","k_0h13jjk":"Call accepted","k_0h19hfx":"Call rejected","k_0obi9lh":"No answer","k_0ohzb9l":"Call duration: $callTime","k_0y9u662":"$appName currently does not support this file type. You can use another app to open and preview the file.","k_1ht1b80":"Receiving","k_0d5z4m5":"Select reminder receiver","k_1665ltg":"Initiate call","k_003kthh":"Photo","k_119ucng":"The image cannot be empty","k_0w9x8gw":"Selected successfully: $successPath","k_1np495n":"$messageString[Someone@me]","k_1m797yi":"$messageString[@all]","k_1uaov41":"Search for chat content","k_0bxm97s":"Admin ($adminNum/10)","k_0jayw3z":"Group members ($groupMemberNum members)","k_0h1svv1":"Delete group member","k_0h1g636":"Add group member","k_01yfa4o":"$memberCount members","k_0hpukyx":"View more group members","k_0qtsar0":"Mute notifications","k_03xd79d":"Status: $signature","k_1m9dftc":"All contacts","k_0em4gyz":"All group chats","k_002twmj":"Group chat","k_09kga0d":"More chat history","k_1ui5lzi":"$count messages are found","k_09khmso":"Related chat records","k_1kevf4k":"Chat history with $receiver","k_03ignw6":"All","k_03icaxo":"Custom","k_1969986":"[Voice Call]:$callingLastMsgShow","k_1960dlr":"[Video Call]:$callingLastMsgShow","k_1qbg9xc":"$option8 to ","k_1wq5ubm":"$option7 changed ","k_0y5pu80":"$option6 quit group chat","k_0nl7cmd":"Invite $option5 to the group","k_1ju5iqw":"Remove $option4 from the group","k_1ovt677":"User $option3 joined the group","k_0k05b8b":"$option2 was ","k_0wm4xeb":"System message: $option2","k_0nbq9v3":"Call duration: $option2","k_0i1kf53":"[File] $option2","k_1gnnby6":" attempted to access your $option2","k_1wh4atg":"$option2 minutes ago","k_07sh7g1":"$option2, yesterday","k_1pj8xzh":"My user ID: $option2","k_0py1evo":"Status: $option2","k_1kvj4i2":"$option2 recalled a message","k_1v0lbpp":"$option2 currently does not support this file type. You can use another app to open and preview the file.","k_0torwfz":"Selected successfully: $option2","k_0i1bjah":"$option1 recalled a message","k_1qzxh9q":"Call duration: $option3","k_0wrgmom":"[Voice Call]:$option1","k_06ix2f0":"[Video Call]:$option2","k_08o3z5w":"[File] $option1","k_0ezbepg":"$option2[Someone@me]","k_1ccnht1":"$option2[@all]","k_1k3arsw":"Admin ($option2/10)","k_1d4golg":"Group members ($option1 members)","k_1bg69nt":"$option1 members","k_00gjqxj":"Status: $option1","k_0c29cxr":"$option1 messages are found","k_1twk5rz":"Chat history with $option1","k_18o68ro":"Allow ","k_1onpf8u":" to access your camera to take photos, record videos, and make video calls.","k_17irga5":" to access your microphone to send voice messages, record videos, and make voice/video calls.","k_0572kc4":" to access your photos to send images and videos.","k_0slykws":" to access your album to save images and videos.","k_119pkcd":" to access your files to view, select and send files in a chat.","k_03c49qt":"Authorize now","k_0nt2uyg":"Back to the bottom","k_04l16at":"$option1 new messages","k_13p3w93":"Someone @ me","k_18w5uk6":"@ all","k_0jmujgh":"You are receiving other files","k_12s5ept":"Message details","k_0mxa4f4":"$option1 read","k_061tue3":"$option2 unread","k_1vn4xq1":"remove $adminMember from admin","k_0e35hsw":"Please allow us to use your camera to capture photos and videos sending to your friends and make video calls.","k_0dj6yr7":"Please allow us to use your microphone for sending voice message, make video/audio calls.","k_003qnsl":"Save","k_0s3rtpw":"Please allow us to access the media and files on your devices, in order to select and send to your friend, or save from them.","k_0tezv85":" Would like to access $option2","k_002rety":" permission. ","k_0gqewd3":"Later","k_03eq4s1":"Authorize Now","k_18qjstb":"Transfer Group","k_0on1aj2":"$option2 messages @ me","k_09j4izl":"[Someone @ me] ","k_1oqtjw0":"[@ all] ","k_1x5a9vb":"This is: $option1","k_14n31e7":"Add Group","k_08nc5j1":"Group type: $option1","k_1josu12":"$option1 group joining request(s)","k_0n2x5s0":"Verification message: $option2","k_03c1nx0":"Agreed","k_03aw9w8":"Rejected","k_038ryos":"Handle now","k_0gw8pum":"Add Group","k_1gcvfrj":"Please fill in the remarks","k_002v9zj":"确认","k_10oqrki":"轻触拍照","k_0f8b3ws":"加载失败","k_11cm5lm":"手动聚焦","k_002uzrd":"预览","k_003qkn3":"录像","k_003k6a7":"拍照","k_0bqpqco":"拍照按钮","k_1626ozl":"停止录像","k_003lvmu":"前置","k_003lued":"后置","k_003lwzh":"外置","k_002qzi3":"关闭","k_003pufb":"自动","k_0apm0ze":"拍照时闪光","k_157zog5":"始终闪光","k_0cfyqhy":"$option1 画面预览","k_0phctlz":"闪光模式: $option2","k_02vfqe0":"切换至 $option3 摄像头","k_0f0y9ex":"说话时间太短","k_0ln70tk":"无法打开URL","k_11a3jdv":"轻触拍照,长按摄像","k_1k18miv":"请传入离开群组生命周期函数,提供返回首页或其他页面的导航方法。","k_1fu9ahv":"全员禁言状态","k_0gmwbnd":"全员禁言中","k_0got2zr":"您被禁言","k_0y9jck8":"你必须自定义search bar,并处理点击跳转","k_0yum3tv":"如使用自定义区域,请在profileWidgetBuilder传入对应组件","k_09kalj0":"清空聊天记录","k_14j5iul":"删除并退出","k_125ru1w":"解散该群","k_0jtutmw":"退出后不会接收到此群聊消息","k_0jtzmqa":"解散后不会接收到此群聊消息","k_0r8fi93":"好友添加成功","k_02qw14e":"好友申请已发出","k_0n3md5x":"当前用户在黑名单","k_094phq4":"好友添加失败","k_129scag":"好友删除成功","k_129uzfn":"好友删除失败","k_1666isy":"清除好友","k_1679vrd":"加为好友","k_1ualc52":"看看对方带来的数据是啥","k_0szluvp":"设置对方在线状态","k_0f4rnf8":"该用户已是好友","k_1tdkom4":"您已是群成员","k_1p2lyuz":"对方正在输入中...","k_1g8wfpy":"...共$option1人","k_12rv9vw":"回应详情","k_0havgi0":"[查看详情 >>](${linkMessage.link})","k_0n9p7g8":"群组不存在","k_1tdh5vn":"您不是群成员","k_0h1q57v":"暂无群成员","k_0y5drq1":"[查看详情 >>]($option1)","k_03pjp61":"[表情消息]","k_1jpvzul":"[自定义消息]","k_03u3bh1":"[文件消息]","k_1odsnsw":"[群消息]","k_03sel4t":"[图片消息]","k_03sfw3r":"[位置消息]","k_03xpuwq":"[合并消息]","k_07ycxwo":"[没有元素]","k_03rc9vz":"[文本消息]","k_046uopf":"[视频消息]","k_0ehmsun":"设备存储空间不足,建议清理,以获得更好使用体验","k_003kmos":"图片","k_002s86q":"视频","k_06bk5ei":"视频消息仅限 mp4 格式","k_13opfxf":"Web网页端不支持搜索","k_1i0o0y2":"暂时仅限 Android/iOS 端","k_045dtzl":"$option1的聊天记录","k_0t0131u":"群资料信息","k_18ok8xz":"消息接收方式","k_03ax3ks":"群资料","k_0sqvoqo":"将 $option1 设置为管理员","k_1gbg1v8":"将 $option1 取消管理员","k_17k64g4":"群聊创建成功!","k_05mn217":"暂未安装表情包插件,如需使用表情相关功能,请根据本文档安装:https://cloud.tencent.com/document/product/269/70746","k_14j17nz":"暂无表情包","k_0fvjexh":"正在下载中","k_1cdagzz":"已加入待下载队列,其他文件下载中","k_0g4vojc":"开始下载","k_1g32es3":"[调皮]@2x.png","k_1g8qorz":"[爱你]@2x.png","k_1g4hmx6":"[爱情]@2x.png","k_1g6b558":"[爱心]@2x.png","k_1g3m4su":"[傲慢]@2x.png","k_1g2jym7":"[白眼]@2x.png","k_0cgkxuw":"[棒棒糖]@2x.png","k_1g48br2":"[抱抱]@2x.png","k_1g49ol8":"[抱拳]@2x.png","k_1g0ras3":"[爆筋]@2x.png","k_1ghy881":"[鄙视]@2x.png","k_1g86bmv":"[闭嘴]@2x.png","k_1g1xs1p":"[鞭炮]@2x.png","k_1g8i6ri":"[便便]@2x.png","k_1g2u5kf":"[擦汗]@2x.png","k_1g60uwh":"[彩带]@2x.png","k_1g1o0d0":"[彩球]@2x.png","k_1g6a6yq":"[菜刀]@2x.png","k_1g6vqo2":"[差劲]@2x.png","k_1g0kvjc":"[钞票]@2x.png","k_1g65x7e":"[车厢]@2x.png","k_0e1tjol":"[打哈欠]@2x.png","k_1g65n58":"[大兵]@2x.png","k_1g7se7o":"[大哭]@2x.png","k_1g03868":"[蛋糕]@2x.png","k_1h8nm66":"[刀]@2x.png","k_1g3dlpi":"[得意]@2x.png","k_1g3u434":"[灯泡]@2x.png","k_1giuqs7":"[凋谢]@2x.png","k_1g8r0r9":"[多云]@2x.png","k_1g7k6i1":"[发呆]@2x.png","k_1g44zsp":"[发抖]@2x.png","k_1g5l96i":"[飞机]@2x.png","k_1g7wsqj":"[飞吻]@2x.png","k_1g49luq":"[奋斗]@2x.png","k_1gixbsm":"[风车]@2x.png","k_1g6cqbq":"[尴尬]@2x.png","k_1g6jbw5":"[勾引]@2x.png","k_1g3lwo1":"[鼓掌]@2x.png","k_1g13nkj":"[害羞]@2x.png","k_1g0mt47":"[憨笑]@2x.png","k_0bxujkf":"[红灯笼]@2x.png","k_0hhaeh8":"[红双喜]@2x.png","k_1g0jnts":"[坏笑]@2x.png","k_1g46g9c":"[挥手]@2x.png","k_1g4vi9g":"[回头]@2x.png","k_1gf7hes":"[饥饿]@2x.png","k_1g6mvsm":"[激动]@2x.png","k_1gku5mf":"[街舞]@2x.png","k_1g4hidg":"[惊恐]@2x.png","k_1gjbrtu":"[惊讶]@2x.png","k_1g6sand":"[咖啡]@2x.png","k_1g4s8rj":"[磕头]@2x.png","k_1g1wn34":"[可爱]@2x.png","k_1g3l0wd":"[可怜]@2x.png","k_1ggaon9":"[抠鼻]@2x.png","k_1ggvcb0":"[骷髅]@2x.png","k_1h8yqjt":"[酷]@2x.png","k_0jac97i":"[快哭了]@2x.png","k_1h8oiby":"[困]@2x.png","k_1g0s5hg":"[蜡烛]@2x.png","k_1g1iuer":"[篮球]@2x.png","k_1g2xjfi":"[冷汗]@2x.png","k_0s5oyqw":"[礼品袋]@2x.png","k_1g1qqvf":"[礼物]@2x.png","k_1g2slew":"[流汗]@2x.png","k_1g3z9xx":"[流泪]@2x.png","k_1g6pabn":"[麻将]@2x.png","k_0pkaxul":"[麦克风]@2x.png","k_1g7m0zj":"[猫咪]@2x.png","k_0ibvtpo":"[么么哒]@2x.png","k_1g1hoh1":"[玫瑰]@2x.png","k_1gfzeow":"[米饭]@2x.png","k_1g5l15p":"[面条]@2x.png","k_1g2hfa6":"[奶瓶]@2x.png","k_1gix9pj":"[难过]@2x.png","k_1giqn6g":"[闹钟]@2x.png","k_1h8kd64":"[怒]@2x.png","k_1g0vui9":"[怄火]@2x.png","k_1g1jsj7":"[皮球]@2x.png","k_1ghdluw":"[啤酒]@2x.png","k_1gl6ec7":"[瓢虫]@2x.png","k_1g7gg5p":"[撇嘴]@2x.png","k_1g8psin":"[乒乓]@2x.png","k_1gjzu3p":"[汽车]@2x.png","k_1h8mr0k":"[强]@2x.png","k_1g45y2n":"[敲打]@2x.png","k_1gkaxsl":"[青蛙]@2x.png","k_0jcfnoo":"[糗大了]@2x.png","k_1g4njy1":"[拳头]@2x.png","k_1h8mqr3":"[弱]@2x.png","k_1h926fg":"[色]@2x.png","k_1g6rtbq":"[沙发]@2x.png","k_1giirh6":"[删除]@2x.png","k_1g14ny9":"[闪电]@2x.png","k_1g6bmsr":"[胜利]@2x.png","k_1g1rytx":"[示爱]@2x.png","k_1g52fbz":"[手枪]@2x.png","k_1h90dam":"[衰]@2x.png","k_1gigiae":"[睡觉]@2x.png","k_1gijchz":"[太阳]@2x.png","k_1g1sgji":"[跳绳]@2x.png","k_1gjwuri":"[跳跳]@2x.png","k_1g0juhk":"[偷笑]@2x.png","k_1h8nzla":"[吐]@2x.png","k_1g6cv0i":"[委屈]@2x.png","k_1g46l5g":"[握手]@2x.png","k_1g2pgkd":"[西瓜]@2x.png","k_1ging9p":"[下雨]@2x.png","k_1h8nzil":"[吓]@2x.png","k_1g7q7wr":"[献吻]@2x.png","k_1gl6uum":"[香蕉]@2x.png","k_1g23fys":"[象棋]@2x.png","k_0j75rdh":"[心碎了]@2x.png","k_1g6ajj2":"[信封]@2x.png","k_1g21prz":"[熊猫]@2x.png","k_1h8octi":"[嘘]@2x.png","k_1h91zox":"[药]@2x.png","k_1ghttfl":"[疑问]@2x.png","k_1ghk7sz":"[阴险]@2x.png","k_0gl37zz":"[右车头]@2x.png","k_0ifkj1p":"[右哼哼]@2x.png","k_0g1yh2e":"[右太极]@2x.png","k_1g9dkfc":"[雨伞]@2x.png","k_1g8jl88":"[月亮]@2x.png","k_1h8lhqj":"[晕]@2x.png","k_1gi9x2q":"[再见]@2x.png","k_1g6dwwv":"[炸弹]@2x.png","k_1fzmkfi":"[折磨]@2x.png","k_1g6jbiw":"[纸巾]@2x.png","k_1ggjnwu":"[咒骂]@2x.png","k_1g4qlq8":"[猪头]@2x.png","k_1g1lqzz":"[抓狂]@2x.png","k_1g80j3u":"[转圈]@2x.png","k_1g0z55s":"[龇牙]@2x.png","k_1g3ju6v":"[钻戒]@2x.png","k_0gl51l6":"[左车头]@2x.png","k_0iflllk":"[左哼哼]@2x.png","k_0g1y3ir":"[左太极]@2x.png","k_026hiq5":"消息列表加载中","k_003tu8k":"爱你","k_003myvp":"傲慢","k_003kddw":"白眼","k_039yfhv":"棒棒糖","k_003nu3p":"抱抱","k_003nijr":"抱拳","k_003mg88":"爆筋","k_002v17e":"鄙视","k_003qhy4":"闭嘴","k_003l5fq":"鞭炮","k_003uacl":"便便","k_003oq1g":"擦汗","k_003qvey":"彩带","k_003jci7":"彩球","k_003pyu1":"菜刀","k_003q97d":"差劲","k_003po5d":"车厢","k_03eadb2":"打哈欠","k_003pnuf":"大兵","k_003kg57":"蛋糕","k_003mxkt":"得意","k_003onu3":"灯泡","k_002uv8s":"凋谢","k_003kqy0":"调皮","k_003tyum":"多云","k_003pv9u":"发呆","k_036o6mu":"发抖t","k_003nogx":"飞机","k_003q7wg":"飞吻","k_003m0jd":"奋斗","k_002ult9":"风车","k_003r8gt":"尴尬","k_003qy4u":"勾引","k_003mnoa":"鼓掌","k_003lmw8":"害羞","k_003mb30":"憨笑","k_03bj41g":"红灯笼","k_03dxw2f":"红双喜","k_003mk57":"坏笑","k_003nmvf":"挥手","k_003r2i7":"回头","k_002s6f3":"饥饿","k_003qd0t":"激动","k_002vgi4":"街舞","k_003nz33":"惊恐","k_002wh4p":"惊讶","k_003ozpu":"咖啡","k_003qvs4":"磕头","k_003l3wb":"可爱","k_003nuwm":"可怜","k_002rw1q":"抠鼻","k_002tujb":"骷髅","k_00030eq":"酷","k_03i8ath":"快哭了","k_000421h":"困","k_003l5i7":"蜡烛","k_003j72g":"篮球","k_003ofwl":"冷汗","k_02mw65v":"礼品袋","k_003ku40":"礼物","k_003ookz":"流汗","k_003on72":"流泪","k_003rjy0":"麻将","k_003q2f8":"猫咪","k_03et393":"么么哒","k_003j7j2":"玫瑰","k_002sr0b":"米饭","k_003nnza":"面条","k_003jef9":"奶瓶","k_002umn0":"难过","k_002rjib":"闹钟","k_0003zcn":"怒","k_003jzwq":"怄火","k_003j4js":"皮球","k_002r5ir":"啤酒","k_002ubu4":"瓢虫","k_003ppo6":"撇嘴","k_003ty3o":"乒乓","k_002vxwe":"汽车","k_00043hb":"强","k_003nmbo":"敲打","k_002tfhq":"青蛙","k_03i7lrn":"糗大了","k_003r03m":"拳头","k_00043h0":"弱","k_000345z":"色","k_003qmp9":"沙发","k_003it8a":"闪电","k_003pxow":"胜利","k_003kw8e":"示爱","k_003n99g":"手枪","k_00035cl":"衰","k_002vl3h":"睡觉","k_002rgqk":"太阳","k_003m9d1":"跳绳","k_002vobp":"跳跳","k_003mkoz":"偷笑","k_00041px":"吐","k_003rjh5":"委屈","k_003j36u":"西瓜","k_002re92":"下雨","k_00041py":"吓","k_003q06o":"献吻","k_002ubjp":"香蕉","k_003o2tr":"象棋","k_03ie6pa":"心碎了","k_003rao5":"信封","k_003l3us":"熊猫","k_000424d":"嘘","k_00033yi":"药","k_002qtyy":"疑问","k_002qe0o":"阴险","k_03gu7us":"右车头","k_03ere8m":"右哼哼","k_003uqk3":"雨伞","k_003tzdv":"月亮","k_0003z00":"晕","k_002vdrd":"再见","k_003ra1w":"炸弹","k_003lcad":"折磨","k_003q7sz":"纸巾","k_002thn9":"咒骂","k_003qx7f":"猪头","k_003l044":"抓狂","k_003qg4h":"转圈","k_003kb97":"龇牙","k_03gu53l":"左车头","k_03erd1f":"左哼哼","k_003nyvl":"爱情","k_003r85z":"爱心","k_003mk8j":"钞票","k_003pwfj":"大哭","k_00042w5":"刀","k_003nmtr":"握手","k_03c529p":"右太极","k_003n4mk":"钻戒","k_03c5488":"左太极","k_1llp7tu":"该用户不存在","k_0tbyqyb":"加载中…","k_0td1p3f":"保存中…","k_1klqdh1":"仅限汉字、英文、数字和下划线","k_03el5lp":"未填写","k_1ui0gai":"搜索指定内容","k_003nvk2":"消息","k_03agld7":"群提示","k_0elt0kw":"添加群聊","k_0s3sgel":"移出黑名单","k_1qqgjra":"$option3条未读消息","k_0uubyjr":"以下为未读消息","k_16as7eq":"表情回应","k_003s12u":"回复","k_003s38r":"更多","k_002wkr3":"翻译","k_13g4hxv":"翻译完成","k_003molk":"表情","k_165bbw6":"消息历史","k_13sqc0z":"清除消息","k_0glns86":"删除会话","k_13s99rx":"清空消息","k_11vsa3j":"退出群组","k_11vvszp":"解散群组","k_15i9w72":"群管理员","k_0p3espj":"设置备注名","k_118sw9v":"立即搜索","k_0h20hg5":"视频通话","k_0h22snw":"语音通话","k_003lz6t":"对方","k_1xf4yre":"发送给$option1","k_003por5":"截图","k_1rw7s82":" 访问相册中视频权限,以正常使用发送视频等功能。","k_003rcwm":"打开","k_1698c42":"在访达中打开","k_066fxsz":"查看文件夹","k_0k432y2":"无法发送,包含文件夹","k_002wb4y":"会话"} \ No newline at end of file diff --git a/lib/i18n/strings_zh-Hans.i18n.json b/lib/i18n/strings_zh-Hans.i18n.json index 72463e1..5a79486 100644 --- a/lib/i18n/strings_zh-Hans.i18n.json +++ b/lib/i18n/strings_zh-Hans.i18n.json @@ -659,8 +659,34 @@ "k_1ui0gai": "搜索指定内容", "k_003nvk2": "消息", "k_03agld7": "群提示", + "k_0elt0kw": "添加群聊", + "k_0s3sgel": "移出黑名单", + "k_1qqgjra": "$option3条未读消息", + "k_0uubyjr": "以下为未读消息", + "k_16as7eq": "表情回应", + "k_003s12u": "回复", + "k_003s38r": "更多", "k_002wkr3": "翻译", "k_13g4hxv": "翻译完成", - "k_1qqgjra": "$option3条未读消息", - "k_0uubyjr": "以下为未读消息" + "k_003molk": "表情", + "k_165bbw6": "消息历史", + "k_13sqc0z": "清除消息", + "k_0glns86": "删除会话", + "k_13s99rx": "清空消息", + "k_11vsa3j": "退出群组", + "k_11vvszp": "解散群组", + "k_15i9w72": "群管理员", + "k_0p3espj": "设置备注名", + "k_118sw9v": "立即搜索", + "k_0h20hg5": "视频通话", + "k_0h22snw": "语音通话", + "k_003lz6t": "对方", + "k_1xf4yre": "发送给$option1", + "k_003por5": "截图", + "k_1rw7s82": " 访问相册中视频权限,以正常使用发送视频等功能。", + "k_003rcwm": "打开", + "k_1698c42": "在访达中打开", + "k_066fxsz": "查看文件夹", + "k_0k432y2": "无法发送,包含文件夹", + "k_002wb4y": "会话" } \ No newline at end of file diff --git a/lib/i18n/strings_zh-Hant.i18n.json b/lib/i18n/strings_zh-Hant.i18n.json index d36d77a..0c8ea1c 100644 --- a/lib/i18n/strings_zh-Hant.i18n.json +++ b/lib/i18n/strings_zh-Hant.i18n.json @@ -1 +1 @@ -{"k_1yemzyd":"收到一條訊息","k_0ylosxn":"自定義訊息","k_13sajrj":"貼圖訊息","k_13sjeb7":"檔案訊息","k_0yd2ft8":"群提示訊息","k_13s7mxn":"圖片訊息","k_13satlt":"位置訊息","k_00bbtsx":"合並轉發訊息","k_13sqwu4":"語音訊息","k_13sqjjp":"影片","k_1fdhj9g":"該版本不支持此訊息","k_06pujtm":"同意任何用戶添加好友","k_0gyhkp5":"需要驗證","k_121ruco":"拒絕任何人加好友","k_05nspni":"自定義字段","k_03fchyy":"群頭像","k_03i9mfe":"群簡介","k_03agq58":"群名稱","k_039xqny":"群通知","k_003tr0a":"群主","k_03iqsh4":"$s為 ","k_191t5n4":"$opUserNickName修改","k_1pg6aoj":"$opUserNickName退出群組","k_1f6zt3v":"邀請$invitedMemberString加入群組","k_0y7zd07":"將$invitedMemberString踢出群組","k_03c49qt":"去授權","k_1d5mshh":"用戶$joinedMemberString加入了群組","k_002wddw":"禁言","k_0got6f7":"解除禁言","k_0yenqf0":"$userName 被","k_0spotql":"將 $adminMember 設置為管理員","k_0pg5zzj":"系統訊息 $operationType","k_0ohzb9l":"通話時間:$callTime","k_1uaqed6":"[自定義]","k_0z2z7rx":"[語音]","k_0y39ngu":"[貼圖]","k_1c7z88n":"[檔案] $fileName","k_0y1a2my":"[圖片]","k_0z4fib8":"[影片]","k_0y24mcg":"[位置]","k_0pewpd1":"[聊天記錄]","k_13s8d9p":"未知訊息","k_1c3us5n":"當前群組不支持@全體成員","k_11k579v":"發言中有非法語句","k_003qkx2":"日歷","k_003n2pz":"相機","k_03idjo0":"聯絡人","k_003ltgm":"位置","k_02k3k86":"咪高風","k_003pm7l":"相冊","k_15ao57x":"相冊寫入","k_164m3jd":"本地存儲","k_0qba4ns":"想訪問您的$yoursItem","k_03r6qyx":"我們需要您的同意才能獲取信息","k_02noktt":"不允許","k_00043x4":"好","k_003qzac":"昨天","k_003r39d":"前天","k_03fqp9o":"星期天","k_03ibg5h":"星期一","k_03i7hu1":"星期二","k_03iaiks":"星期三","k_03el9pa":"星期四","k_03i7ok1":"星期五","k_03efxyg":"星期六","k_0oozw9x":"$diffMinutes 分鐘前","k_003q7ba":"下午","k_003q7bb":"上午","k_003pu3h":"現在","k_13hzn00":"昨天 $yesterday","k_0n9pyxz":"用戶不存在","k_1bjwemh":"搜尋用戶 ID","k_003kv3v":"搜尋","k_02owlq8":"我的用戶ID: $userID","k_1wu8h4x":"我是: $showName","k_16758qw":"添加好友","k_1shx4d9":"個性簽名: $selfSignature","k_0i553x0":"填寫驗證信息","k_031ocwx":"請填寫備註和分組","k_003ojje":"備註","k_003lsav":"分組","k_167bdvq":"我的好友","k_156b4ut":"好友申請已發送","k_002r305":"發送","k_03gu05e":"聊天室","k_03b4f3p":"會議群","k_03avj1p":"公開群","k_03asq2g":"工作群","k_03b3hbi":"未知群","k_1loix7s":"群類型: $groupType","k_1lqbsib":"該群組不存在","k_03h153m":"搜尋群ID","k_0oxak3r":"群申請已發送","k_002rflt":"刪除","k_1don84v":"無法定位到原訊息","k_003q5fi":"復製","k_003prq0":"轉發","k_002r1h2":"多選","k_003j708":"引用","k_003pqpr":"回收","k_03ezhho":"已復製","k_11ctfsz":"暫未實現","k_1hbjg5g":"[群系統訊息]","k_03tvswb":"[未知訊息]","k_155cj23":"您回收了一條訊息,","k_0gapun3":"重新編輯","k_1uh417q":"$displayName回收了一條訊息","k_1aszp2k":"您確定要重發這條訊息麽?","k_003rzap":"確定","k_003nevv":"取消","k_0003z7x":"您","k_002wfe4":"已讀","k_002wjlg":"未讀","k_0h1ygf8":"發起通話","k_0h169j0":"取消通話","k_0h13jjk":"接受通話","k_0h19hfx":"拒絕通話","k_0obi9lh":"超時未接聽","k_0y9u662":"「$appName」暫不可以開啟此類檔案,你可以使用其他應用開啟並預覽","k_001nmhu":"用其他應用開啟","k_1ht1b80":"正在接收中","k_105682d":"圖片載入失敗","k_0pytyeu":"圖片保存成功","k_0akceel":"圖片保存失敗","k_003rk1s":"保存","k_04a0awq":"[語音訊息]","k_105c3y3":"影片載入失敗","k_176rzr7":"聊天記錄","k_0d5z4m5":"選擇提醒人","k_003ngex":"完成","k_1665ltg":"發起呼叫","k_003n8b0":"拍攝","k_003kthh":"照片","k_003tnp0":"檔案","k_0jhdhtp":"發送失敗,影片不能大於100MB","k_119ucng":"圖片不能為空","k_0w9x8gw":"選擇成功$successPath","k_13dsw4l":"松開取消","k_0am7r68":"手指上滑,取消發送","k_15jl6qw":"說話時間太短!","k_0gx7vl6":"按住說話","k_15dlafd":"逐條轉發","k_15dryxy":"合並轉發","k_1eyhieh":"確定刪除已選訊息","k_17fmlyf":"清除聊天","k_0dhesoz":"取消置頂","k_002sk7x":"置頂","k_003ll77":"草稿","k_03icaxo":"自定義","k_1969986":"[語音通話]:$callingLastMsgShow","k_1960dlr":"[視訊通話]:$callingLastMsgShow","k_1np495n":"$messageString[有人@我]","k_1m797yi":"$messageString[@所有人]","k_1uaov41":"查找聊天內容","k_003kfai":"未知","k_13dq4an":"自動審批","k_0l13cde":"管理員審批","k_11y8c6a":"禁止加群","k_1kvyskd":"無網絡連接,無法修改","k_16payqf":"加群方式","k_0vzvn8r":"修改群名稱","k_038lh6u":"群管理","k_0k5wyiy":"設置管理員","k_0goiuwk":"全員禁言","k_1g889xx":"全員禁言開啟後,只允許群主和管理員發言。","k_0wlrefq":"添加需要禁言的群成員","k_0goox5g":"設置禁言","k_08daijh":"成功取消管理員身份","k_0bxm97s":"管理員 ($adminNum/10)","k_0k5u935":"添加管理員","k_03enyx5":"群成員","k_0jayw3z":"群成員($groupMemberNum人)","k_0h1svv1":"刪除群成員","k_0h1g636":"添加群成員","k_0uj7208":"無網絡連接,無法查看群成員","k_01yfa4o":"$memberCount人","k_0hpukyx":"查看更多群成員","k_0qtsar0":"訊息免打擾","k_0ef2a12":"修改我的群昵稱","k_1aajych":"僅限中文、字母、數字和下劃線,2-20個字","k_137pab5":"我的群昵稱","k_0ivim6d":"暫無群公告","k_03eq6cn":"群公告","k_002vxya":"編輯","k_17fpl3y":"置頂聊天","k_03es1ox":"群類型","k_003mz1i":"同意","k_003lpre":"拒絕","k_003qk66":"頭像","k_003lhvk":"昵稱","k_003ps50":"賬號","k_15lx52z":"個性簽名","k_003qgkp":"性別","k_003m6hr":"生日","k_0003v6a":"男","k_00043x2":"女","k_03bcjkv":"未設置","k_11s0gdz":"修改昵稱","k_0p3j4sd":"僅限中字、字母、數字和下劃線","k_15lyvdt":"修改簽名","k_0vylzjp":"這個人很懶,什麽也沒寫","k_1hs7ese":"等上線再改這個","k_03exjk7":"備註名","k_0s3skfd":"加入黑名單","k_0p3b31s":"修改備註名","k_0003y9x":"無","k_11zgnfs":"個人資料","k_03xd79d":"個性簽名: $signature","k_1tez2xl":"暫無個性簽名","k_118prbn":"全局搜尋","k_1m9dftc":"全部聯絡人","k_0em4gyz":"全部群組","k_002twmj":"群組","k_09kga0d":"更多聊天記錄","k_1ui5lzi":"$count條相關聊天記錄","k_09khmso":"相關聊天記錄","k_1kevf4k":"與$receiver的聊天記錄","k_0vjj2kp":"群組的聊天記錄","k_003n2rp":"選擇","k_03ignw6":"所有人","k_03erpei":"管理員","k_0qi9tno":"群主、管理員","k_1m9exwh":"最近聯絡人","k_119nwqr":"輸入不能為空","k_0pzwbmg":"影片保存成功","k_0aktupv":"影片保存失敗","k_1qbg9xc":"$option8為 ","k_1wq5ubm":"$option7修改","k_0y5pu80":"$option6退出群組","k_0nl7cmd":"邀請$option5加入群組","k_1ju5iqw":"將$option4踢出群組","k_1ovt677":"用戶$option3加入了群組","k_0k05b8b":"$option2 被","k_0wm4xeb":"系統訊息 $option2","k_0nbq9v3":"通話時間:$option2","k_0i1kf53":"[檔案] $option2","k_1gnnby6":"想訪問您的$option2","k_1wh4atg":"$option2 分鐘前","k_07sh7g1":"昨天 $option2","k_1pj8xzh":"我的用戶ID: $option2","k_0py1evo":"個性簽名: $option2","k_1kvj4i2":"$option2回收了一條訊息","k_1v0lbpp":"「$option2」暫不可以開啟此類檔案,你可以使用其他應用開啟並預覽","k_0torwfz":"選擇成功$option2","k_0i1bjah":"$option1回收了一條訊息","k_1qzxh9q":"通話時間:$option3","k_0wrgmom":"[語音通話]:$option1","k_06ix2f0":"[視訊通話]:$option2","k_08o3z5w":"[檔案] $option1","k_0ezbepg":"$option2[有人@我]","k_1ccnht1":"$option2[@所有人]","k_1k3arsw":"管理員 ($option2/10)","k_1d4golg":"群成員($option1人)","k_1bg69nt":"$option1人","k_00gjqxj":"個性簽名: $option1","k_0c29cxr":"$option1條相關聊天記錄","k_1twk5rz":"與$option1的聊天記錄","k_1vn4xq1":"將 $adminMember 取消管理員","k_0e35hsw":"為方便您將所拍攝的照片或影片發送給朋友,以及進行視訊通話,請允許我們訪問攝像頭進行拍攝照片和影片。","k_0dj6yr7":"為方便您發送語音訊息、拍攝影片以及音視訊通話,請允許我們使用咪高風進行錄音。","k_003qnsl":"存儲","k_0s3rtpw":"為方便您查看和選擇相冊裏的圖片影片發送給朋友,以及保存內容到設備,請允許我們訪問您設備上的照片、媒體內容。","k_0tezv85":" 申請獲取$option2","k_002rety":"權限","k_18o68ro":"需要授予","k_1onpf8u":" 相機權限,以正常使用拍攝圖片/影片、視訊通話等功能。","k_17irga5":" 咪高風權限,以正常使用發送語音訊息、拍攝影片、音視訊通話等功能。","k_0572kc4":" 訪問照片權限,以正常使用發送圖片、影片等功能。","k_0slykws":" 訪問相冊寫入權限,以正常使用存儲圖片、影片等功能。","k_119pkcd":" 檔案讀寫權限,以正常使用在聊天功能中的圖片查看、選擇能力和發送檔案的能力。","k_0gqewd3":"以後再說","k_03eq4s1":"去開啟","k_0nt2uyg":"回到最新位置","k_04l16at":"$option1條新訊息","k_13p3w93":"有人@我","k_18w5uk6":"@所有人","k_0jmujgh":"其他檔案正在接收中","k_12s5ept":"訊息詳情","k_0mxa4f4":"$option1人已讀","k_061tue3":"$option2人未讀","k_18qjstb":"轉讓群主","k_0on1aj2":"有$option2條@我訊息","k_09j4izl":"[有人@我] ","k_1oqtjw0":"[@所有人] ","k_1x5a9vb":"我是: $option1","k_14n31e7":"進群請求","k_08nc5j1":"群類型: $option1","k_1josu12":"$option1 條入群請求","k_0n2x5s0":"驗證消息: $option2","k_03c1nx0":"已同意","k_03aw9w8":"已拒絕","k_038ryos":"去處理","k_0gw8pum":"進群申請","k_1gcvfrj":"請填寫備註名","k_002v9zj":"确认","k_10oqrki":"轻触拍照","k_0f8b3ws":"加载失败","k_11cm5lm":"手动聚焦","k_002uzrd":"预览","k_003qkn3":"录像","k_003k6a7":"拍照","k_0bqpqco":"拍照按钮","k_1626ozl":"停止录像","k_003lvmu":"前置","k_003lued":"后置","k_003lwzh":"外置","k_002qzi3":"关闭","k_003pufb":"自动","k_0apm0ze":"拍照时闪光","k_157zog5":"始终闪光","k_0cfyqhy":"$option1 画面预览","k_0phctlz":"闪光模式: $option2","k_02vfqe0":"切换至 $option3 摄像头","k_0f0y9ex":"说话时间太短","k_0ln70tk":"无法打开URL","k_11a3jdv":"轻触拍照,长按摄像","k_1k18miv":"请传入离开群组生命周期函数,提供返回首页或其他页面的导航方法。","k_1fu9ahv":"全员禁言状态","k_0gmwbnd":"全员禁言中","k_0got2zr":"您被禁言","k_0y9jck8":"你必须自定义search bar,并处理点击跳转","k_0yum3tv":"如使用自定义区域,请在profileWidgetBuilder传入对应组件","k_09kalj0":"清空聊天记录","k_14j5iul":"删除并退出","k_125ru1w":"解散该群","k_0jtutmw":"退出后不会接收到此群聊消息","k_0jtzmqa":"解散后不会接收到此群聊消息","k_0r8fi93":"好友添加成功","k_02qw14e":"好友申请已发出","k_0n3md5x":"当前用户在黑名单","k_094phq4":"好友添加失败","k_129scag":"好友删除成功","k_129uzfn":"好友删除失败","k_1666isy":"清除好友","k_1679vrd":"加为好友","k_1ualc52":"看看对方带来的数据是啥","k_0szluvp":"设置对方在线状态","k_0f4rnf8":"该用户已是好友","k_1tdkom4":"您已是群成员","k_1p2lyuz":"对方正在输入中...","k_1g8wfpy":"...共$option1人","k_12rv9vw":"回应详情","k_0havgi0":"[查看详情 >>](${linkMessage.link})","k_0n9p7g8":"群组不存在","k_1tdh5vn":"您不是群成员","k_0h1q57v":"暂无群成员","k_0y5drq1":"[查看详情 >>]($option1)","k_03pjp61":"[表情消息]","k_1jpvzul":"[自定义消息]","k_03u3bh1":"[文件消息]","k_1odsnsw":"[群消息]","k_03sel4t":"[图片消息]","k_03sfw3r":"[位置消息]","k_03xpuwq":"[合并消息]","k_07ycxwo":"[没有元素]","k_03rc9vz":"[文本消息]","k_046uopf":"[视频消息]","k_0ehmsun":"设备存储空间不足,建议清理,以获得更好使用体验","k_003kmos":"图片","k_002s86q":"视频","k_06bk5ei":"视频消息仅限 mp4 格式","k_13opfxf":"Web网页端不支持搜索","k_1i0o0y2":"暂时仅限 Android/iOS 端","k_045dtzl":"$option1的聊天记录","k_0t0131u":"群资料信息","k_18ok8xz":"消息接收方式","k_03ax3ks":"群资料","k_0sqvoqo":"将 $option1 设置为管理员","k_1gbg1v8":"将 $option1 取消管理员","k_17k64g4":"群聊创建成功!","k_05mn217":"暂未安装表情包插件,如需使用表情相关功能,请根据本文档安装:https://cloud.tencent.com/document/product/269/70746","k_14j17nz":"暂无表情包","k_0fvjexh":"正在下载中","k_1cdagzz":"已加入待下载队列,其他文件下载中","k_0g4vojc":"开始下载","k_1g32es3":"[调皮]@2x.png","k_1g8qorz":"[爱你]@2x.png","k_1g4hmx6":"[爱情]@2x.png","k_1g6b558":"[爱心]@2x.png","k_1g3m4su":"[傲慢]@2x.png","k_1g2jym7":"[白眼]@2x.png","k_0cgkxuw":"[棒棒糖]@2x.png","k_1g48br2":"[抱抱]@2x.png","k_1g49ol8":"[抱拳]@2x.png","k_1g0ras3":"[爆筋]@2x.png","k_1ghy881":"[鄙视]@2x.png","k_1g86bmv":"[闭嘴]@2x.png","k_1g1xs1p":"[鞭炮]@2x.png","k_1g8i6ri":"[便便]@2x.png","k_1g2u5kf":"[擦汗]@2x.png","k_1g60uwh":"[彩带]@2x.png","k_1g1o0d0":"[彩球]@2x.png","k_1g6a6yq":"[菜刀]@2x.png","k_1g6vqo2":"[差劲]@2x.png","k_1g0kvjc":"[钞票]@2x.png","k_1g65x7e":"[车厢]@2x.png","k_0e1tjol":"[打哈欠]@2x.png","k_1g65n58":"[大兵]@2x.png","k_1g7se7o":"[大哭]@2x.png","k_1g03868":"[蛋糕]@2x.png","k_1h8nm66":"[刀]@2x.png","k_1g3dlpi":"[得意]@2x.png","k_1g3u434":"[灯泡]@2x.png","k_1giuqs7":"[凋谢]@2x.png","k_1g8r0r9":"[多云]@2x.png","k_1g7k6i1":"[发呆]@2x.png","k_1g44zsp":"[发抖]@2x.png","k_1g5l96i":"[飞机]@2x.png","k_1g7wsqj":"[飞吻]@2x.png","k_1g49luq":"[奋斗]@2x.png","k_1gixbsm":"[风车]@2x.png","k_1g6cqbq":"[尴尬]@2x.png","k_1g6jbw5":"[勾引]@2x.png","k_1g3lwo1":"[鼓掌]@2x.png","k_1g13nkj":"[害羞]@2x.png","k_1g0mt47":"[憨笑]@2x.png","k_0bxujkf":"[红灯笼]@2x.png","k_0hhaeh8":"[红双喜]@2x.png","k_1g0jnts":"[坏笑]@2x.png","k_1g46g9c":"[挥手]@2x.png","k_1g4vi9g":"[回头]@2x.png","k_1gf7hes":"[饥饿]@2x.png","k_1g6mvsm":"[激动]@2x.png","k_1gku5mf":"[街舞]@2x.png","k_1g4hidg":"[惊恐]@2x.png","k_1gjbrtu":"[惊讶]@2x.png","k_1g6sand":"[咖啡]@2x.png","k_1g4s8rj":"[磕头]@2x.png","k_1g1wn34":"[可爱]@2x.png","k_1g3l0wd":"[可怜]@2x.png","k_1ggaon9":"[抠鼻]@2x.png","k_1ggvcb0":"[骷髅]@2x.png","k_1h8yqjt":"[酷]@2x.png","k_0jac97i":"[快哭了]@2x.png","k_1h8oiby":"[困]@2x.png","k_1g0s5hg":"[蜡烛]@2x.png","k_1g1iuer":"[篮球]@2x.png","k_1g2xjfi":"[冷汗]@2x.png","k_0s5oyqw":"[礼品袋]@2x.png","k_1g1qqvf":"[礼物]@2x.png","k_1g2slew":"[流汗]@2x.png","k_1g3z9xx":"[流泪]@2x.png","k_1g6pabn":"[麻将]@2x.png","k_0pkaxul":"[麦克风]@2x.png","k_1g7m0zj":"[猫咪]@2x.png","k_0ibvtpo":"[么么哒]@2x.png","k_1g1hoh1":"[玫瑰]@2x.png","k_1gfzeow":"[米饭]@2x.png","k_1g5l15p":"[面条]@2x.png","k_1g2hfa6":"[奶瓶]@2x.png","k_1gix9pj":"[难过]@2x.png","k_1giqn6g":"[闹钟]@2x.png","k_1h8kd64":"[怒]@2x.png","k_1g0vui9":"[怄火]@2x.png","k_1g1jsj7":"[皮球]@2x.png","k_1ghdluw":"[啤酒]@2x.png","k_1gl6ec7":"[瓢虫]@2x.png","k_1g7gg5p":"[撇嘴]@2x.png","k_1g8psin":"[乒乓]@2x.png","k_1gjzu3p":"[汽车]@2x.png","k_1h8mr0k":"[强]@2x.png","k_1g45y2n":"[敲打]@2x.png","k_1gkaxsl":"[青蛙]@2x.png","k_0jcfnoo":"[糗大了]@2x.png","k_1g4njy1":"[拳头]@2x.png","k_1h8mqr3":"[弱]@2x.png","k_1h926fg":"[色]@2x.png","k_1g6rtbq":"[沙发]@2x.png","k_1giirh6":"[删除]@2x.png","k_1g14ny9":"[闪电]@2x.png","k_1g6bmsr":"[胜利]@2x.png","k_1g1rytx":"[示爱]@2x.png","k_1g52fbz":"[手枪]@2x.png","k_1h90dam":"[衰]@2x.png","k_1gigiae":"[睡觉]@2x.png","k_1gijchz":"[太阳]@2x.png","k_1g1sgji":"[跳绳]@2x.png","k_1gjwuri":"[跳跳]@2x.png","k_1g0juhk":"[偷笑]@2x.png","k_1h8nzla":"[吐]@2x.png","k_1g6cv0i":"[委屈]@2x.png","k_1g46l5g":"[握手]@2x.png","k_1g2pgkd":"[西瓜]@2x.png","k_1ging9p":"[下雨]@2x.png","k_1h8nzil":"[吓]@2x.png","k_1g7q7wr":"[献吻]@2x.png","k_1gl6uum":"[香蕉]@2x.png","k_1g23fys":"[象棋]@2x.png","k_0j75rdh":"[心碎了]@2x.png","k_1g6ajj2":"[信封]@2x.png","k_1g21prz":"[熊猫]@2x.png","k_1h8octi":"[嘘]@2x.png","k_1h91zox":"[药]@2x.png","k_1ghttfl":"[疑问]@2x.png","k_1ghk7sz":"[阴险]@2x.png","k_0gl37zz":"[右车头]@2x.png","k_0ifkj1p":"[右哼哼]@2x.png","k_0g1yh2e":"[右太极]@2x.png","k_1g9dkfc":"[雨伞]@2x.png","k_1g8jl88":"[月亮]@2x.png","k_1h8lhqj":"[晕]@2x.png","k_1gi9x2q":"[再见]@2x.png","k_1g6dwwv":"[炸弹]@2x.png","k_1fzmkfi":"[折磨]@2x.png","k_1g6jbiw":"[纸巾]@2x.png","k_1ggjnwu":"[咒骂]@2x.png","k_1g4qlq8":"[猪头]@2x.png","k_1g1lqzz":"[抓狂]@2x.png","k_1g80j3u":"[转圈]@2x.png","k_1g0z55s":"[龇牙]@2x.png","k_1g3ju6v":"[钻戒]@2x.png","k_0gl51l6":"[左车头]@2x.png","k_0iflllk":"[左哼哼]@2x.png","k_0g1y3ir":"[左太极]@2x.png","k_026hiq5":"消息列表加载中","k_003tu8k":"爱你","k_003myvp":"傲慢","k_003kddw":"白眼","k_039yfhv":"棒棒糖","k_003nu3p":"抱抱","k_003nijr":"抱拳","k_003mg88":"爆筋","k_002v17e":"鄙视","k_003qhy4":"闭嘴","k_003l5fq":"鞭炮","k_003uacl":"便便","k_003oq1g":"擦汗","k_003qvey":"彩带","k_003jci7":"彩球","k_003pyu1":"菜刀","k_003q97d":"差劲","k_003po5d":"车厢","k_03eadb2":"打哈欠","k_003pnuf":"大兵","k_003kg57":"蛋糕","k_003mxkt":"得意","k_003onu3":"灯泡","k_002uv8s":"凋谢","k_003kqy0":"调皮","k_003tyum":"多云","k_003pv9u":"发呆","k_036o6mu":"发抖t","k_003nogx":"飞机","k_003q7wg":"飞吻","k_003m0jd":"奋斗","k_002ult9":"风车","k_003r8gt":"尴尬","k_003qy4u":"勾引","k_003mnoa":"鼓掌","k_003lmw8":"害羞","k_003mb30":"憨笑","k_03bj41g":"红灯笼","k_03dxw2f":"红双喜","k_003mk57":"坏笑","k_003nmvf":"挥手","k_003r2i7":"回头","k_002s6f3":"饥饿","k_003qd0t":"激动","k_002vgi4":"街舞","k_003nz33":"惊恐","k_002wh4p":"惊讶","k_003ozpu":"咖啡","k_003qvs4":"磕头","k_003l3wb":"可爱","k_003nuwm":"可怜","k_002rw1q":"抠鼻","k_002tujb":"骷髅","k_00030eq":"酷","k_03i8ath":"快哭了","k_000421h":"困","k_003l5i7":"蜡烛","k_003j72g":"篮球","k_003ofwl":"冷汗","k_02mw65v":"礼品袋","k_003ku40":"礼物","k_003ookz":"流汗","k_003on72":"流泪","k_003rjy0":"麻将","k_003q2f8":"猫咪","k_03et393":"么么哒","k_003j7j2":"玫瑰","k_002sr0b":"米饭","k_003nnza":"面条","k_003jef9":"奶瓶","k_002umn0":"难过","k_002rjib":"闹钟","k_0003zcn":"怒","k_003jzwq":"怄火","k_003j4js":"皮球","k_002r5ir":"啤酒","k_002ubu4":"瓢虫","k_003ppo6":"撇嘴","k_003ty3o":"乒乓","k_002vxwe":"汽车","k_00043hb":"强","k_003nmbo":"敲打","k_002tfhq":"青蛙","k_03i7lrn":"糗大了","k_003r03m":"拳头","k_00043h0":"弱","k_000345z":"色","k_003qmp9":"沙发","k_003it8a":"闪电","k_003pxow":"胜利","k_003kw8e":"示爱","k_003n99g":"手枪","k_00035cl":"衰","k_002vl3h":"睡觉","k_002rgqk":"太阳","k_003m9d1":"跳绳","k_002vobp":"跳跳","k_003mkoz":"偷笑","k_00041px":"吐","k_003rjh5":"委屈","k_003j36u":"西瓜","k_002re92":"下雨","k_00041py":"吓","k_003q06o":"献吻","k_002ubjp":"香蕉","k_003o2tr":"象棋","k_03ie6pa":"心碎了","k_003rao5":"信封","k_003l3us":"熊猫","k_000424d":"嘘","k_00033yi":"药","k_002qtyy":"疑问","k_002qe0o":"阴险","k_03gu7us":"右车头","k_03ere8m":"右哼哼","k_003uqk3":"雨伞","k_003tzdv":"月亮","k_0003z00":"晕","k_002vdrd":"再见","k_003ra1w":"炸弹","k_003lcad":"折磨","k_003q7sz":"纸巾","k_002thn9":"咒骂","k_003qx7f":"猪头","k_003l044":"抓狂","k_003qg4h":"转圈","k_003kb97":"龇牙","k_03gu53l":"左车头","k_03erd1f":"左哼哼","k_003nyvl":"爱情","k_003r85z":"爱心","k_003mk8j":"钞票","k_003pwfj":"大哭","k_00042w5":"刀","k_003nmtr":"握手","k_03c529p":"右太极","k_003n4mk":"钻戒","k_03c5488":"左太极","k_1llp7tu":"该用户不存在","k_0tbyqyb":"加载中…","k_0td1p3f":"保存中…","k_1klqdh1":"仅限汉字、英文、数字和下划线","k_03el5lp":"未填写","k_1ui0gai":"搜索指定内容","k_003nvk2":"消息","k_03agld7":"群提示","k_002wkr3":"翻译","k_13g4hxv":"翻译完成","k_1qqgjra":"$option3条未读消息","k_0uubyjr":"以下为未读消息"} \ No newline at end of file +{"k_1yemzyd":"收到一條訊息","k_0ylosxn":"自定義訊息","k_13sajrj":"貼圖訊息","k_13sjeb7":"檔案訊息","k_0yd2ft8":"群提示訊息","k_13s7mxn":"圖片訊息","k_13satlt":"位置訊息","k_00bbtsx":"合並轉發訊息","k_13sqwu4":"語音訊息","k_13sqjjp":"影片","k_1fdhj9g":"該版本不支持此訊息","k_06pujtm":"同意任何用戶添加好友","k_0gyhkp5":"需要驗證","k_121ruco":"拒絕任何人加好友","k_05nspni":"自定義字段","k_03fchyy":"群頭像","k_03i9mfe":"群簡介","k_03agq58":"群名稱","k_039xqny":"群通知","k_003tr0a":"群主","k_03iqsh4":"$s為 ","k_191t5n4":"$opUserNickName修改","k_1pg6aoj":"$opUserNickName退出群組","k_1f6zt3v":"邀請$invitedMemberString加入群組","k_0y7zd07":"將$invitedMemberString踢出群組","k_03c49qt":"去授權","k_1d5mshh":"用戶$joinedMemberString加入了群組","k_002wddw":"禁言","k_0got6f7":"解除禁言","k_0yenqf0":"$userName 被","k_0spotql":"將 $adminMember 設置為管理員","k_0pg5zzj":"系統訊息 $operationType","k_0ohzb9l":"通話時間:$callTime","k_1uaqed6":"[自定義]","k_0z2z7rx":"[語音]","k_0y39ngu":"[貼圖]","k_1c7z88n":"[檔案] $fileName","k_0y1a2my":"[圖片]","k_0z4fib8":"[影片]","k_0y24mcg":"[位置]","k_0pewpd1":"[聊天記錄]","k_13s8d9p":"未知訊息","k_1c3us5n":"當前群組不支持@全體成員","k_11k579v":"發言中有非法語句","k_003qkx2":"日歷","k_003n2pz":"相機","k_03idjo0":"聯絡人","k_003ltgm":"位置","k_02k3k86":"咪高風","k_003pm7l":"相冊","k_15ao57x":"相冊寫入","k_164m3jd":"本地存儲","k_0qba4ns":"想訪問您的$yoursItem","k_03r6qyx":"我們需要您的同意才能獲取信息","k_02noktt":"不允許","k_00043x4":"好","k_003qzac":"昨天","k_003r39d":"前天","k_03fqp9o":"星期天","k_03ibg5h":"星期一","k_03i7hu1":"星期二","k_03iaiks":"星期三","k_03el9pa":"星期四","k_03i7ok1":"星期五","k_03efxyg":"星期六","k_0oozw9x":"$diffMinutes 分鐘前","k_003q7ba":"下午","k_003q7bb":"上午","k_003pu3h":"現在","k_13hzn00":"昨天 $yesterday","k_0n9pyxz":"用戶不存在","k_1bjwemh":"搜尋用戶 ID","k_003kv3v":"搜尋","k_02owlq8":"我的用戶ID: $userID","k_1wu8h4x":"我是: $showName","k_16758qw":"添加好友","k_1shx4d9":"個性簽名: $selfSignature","k_0i553x0":"填寫驗證信息","k_031ocwx":"請填寫備註和分組","k_003ojje":"備註","k_003lsav":"分組","k_167bdvq":"我的好友","k_156b4ut":"好友申請已發送","k_002r305":"發送","k_03gu05e":"聊天室","k_03b4f3p":"會議群","k_03avj1p":"公開群","k_03asq2g":"工作群","k_03b3hbi":"未知群","k_1loix7s":"群類型: $groupType","k_1lqbsib":"該群組不存在","k_03h153m":"搜尋群ID","k_0oxak3r":"群申請已發送","k_002rflt":"刪除","k_1don84v":"無法定位到原訊息","k_003q5fi":"復製","k_003prq0":"轉發","k_002r1h2":"多選","k_003j708":"引用","k_003pqpr":"回收","k_03ezhho":"已復製","k_11ctfsz":"暫未實現","k_1hbjg5g":"[群系統訊息]","k_03tvswb":"[未知訊息]","k_155cj23":"您回收了一條訊息,","k_0gapun3":"重新編輯","k_1uh417q":"$displayName回收了一條訊息","k_1aszp2k":"您確定要重發這條訊息麽?","k_003rzap":"確定","k_003nevv":"取消","k_0003z7x":"您","k_002wfe4":"已讀","k_002wjlg":"未讀","k_0h1ygf8":"發起通話","k_0h169j0":"取消通話","k_0h13jjk":"接受通話","k_0h19hfx":"拒絕通話","k_0obi9lh":"超時未接聽","k_0y9u662":"「$appName」暫不可以開啟此類檔案,你可以使用其他應用開啟並預覽","k_001nmhu":"用其他應用開啟","k_1ht1b80":"正在接收中","k_105682d":"圖片載入失敗","k_0pytyeu":"圖片保存成功","k_0akceel":"圖片保存失敗","k_003rk1s":"保存","k_04a0awq":"[語音訊息]","k_105c3y3":"影片載入失敗","k_176rzr7":"聊天記錄","k_0d5z4m5":"選擇提醒人","k_003ngex":"完成","k_1665ltg":"發起呼叫","k_003n8b0":"拍攝","k_003kthh":"照片","k_003tnp0":"檔案","k_0jhdhtp":"發送失敗,影片不能大於100MB","k_119ucng":"圖片不能為空","k_0w9x8gw":"選擇成功$successPath","k_13dsw4l":"松開取消","k_0am7r68":"手指上滑,取消發送","k_15jl6qw":"說話時間太短!","k_0gx7vl6":"按住說話","k_15dlafd":"逐條轉發","k_15dryxy":"合並轉發","k_1eyhieh":"確定刪除已選訊息","k_17fmlyf":"清除聊天","k_0dhesoz":"取消置頂","k_002sk7x":"置頂","k_003ll77":"草稿","k_03icaxo":"自定義","k_1969986":"[語音通話]:$callingLastMsgShow","k_1960dlr":"[視訊通話]:$callingLastMsgShow","k_1np495n":"$messageString[有人@我]","k_1m797yi":"$messageString[@所有人]","k_1uaov41":"查找聊天內容","k_003kfai":"未知","k_13dq4an":"自動審批","k_0l13cde":"管理員審批","k_11y8c6a":"禁止加群","k_1kvyskd":"無網絡連接,無法修改","k_16payqf":"加群方式","k_0vzvn8r":"修改群名稱","k_038lh6u":"群管理","k_0k5wyiy":"設置管理員","k_0goiuwk":"全員禁言","k_1g889xx":"全員禁言開啟後,只允許群主和管理員發言。","k_0wlrefq":"添加需要禁言的群成員","k_0goox5g":"設置禁言","k_08daijh":"成功取消管理員身份","k_0bxm97s":"管理員 ($adminNum/10)","k_0k5u935":"添加管理員","k_03enyx5":"群成員","k_0jayw3z":"群成員($groupMemberNum人)","k_0h1svv1":"刪除群成員","k_0h1g636":"添加群成員","k_0uj7208":"無網絡連接,無法查看群成員","k_01yfa4o":"$memberCount人","k_0hpukyx":"查看更多群成員","k_0qtsar0":"訊息免打擾","k_0ef2a12":"修改我的群昵稱","k_1aajych":"僅限中文、字母、數字和下劃線,2-20個字","k_137pab5":"我的群昵稱","k_0ivim6d":"暫無群公告","k_03eq6cn":"群公告","k_002vxya":"編輯","k_17fpl3y":"置頂聊天","k_03es1ox":"群類型","k_003mz1i":"同意","k_003lpre":"拒絕","k_003qk66":"頭像","k_003lhvk":"昵稱","k_003ps50":"賬號","k_15lx52z":"個性簽名","k_003qgkp":"性別","k_003m6hr":"生日","k_0003v6a":"男","k_00043x2":"女","k_03bcjkv":"未設置","k_11s0gdz":"修改昵稱","k_0p3j4sd":"僅限中字、字母、數字和下劃線","k_15lyvdt":"修改簽名","k_0vylzjp":"這個人很懶,什麽也沒寫","k_1hs7ese":"等上線再改這個","k_03exjk7":"備註名","k_0s3skfd":"加入黑名單","k_0p3b31s":"修改備註名","k_0003y9x":"無","k_11zgnfs":"個人資料","k_03xd79d":"個性簽名: $signature","k_1tez2xl":"暫無個性簽名","k_118prbn":"全局搜尋","k_1m9dftc":"全部聯絡人","k_0em4gyz":"全部群組","k_002twmj":"群組","k_09kga0d":"更多聊天記錄","k_1ui5lzi":"$count條相關聊天記錄","k_09khmso":"相關聊天記錄","k_1kevf4k":"與$receiver的聊天記錄","k_0vjj2kp":"群組的聊天記錄","k_003n2rp":"選擇","k_03ignw6":"所有人","k_03erpei":"管理員","k_0qi9tno":"群主、管理員","k_1m9exwh":"最近聯絡人","k_119nwqr":"輸入不能為空","k_0pzwbmg":"影片保存成功","k_0aktupv":"影片保存失敗","k_1qbg9xc":"$option8為 ","k_1wq5ubm":"$option7修改","k_0y5pu80":"$option6退出群組","k_0nl7cmd":"邀請$option5加入群組","k_1ju5iqw":"將$option4踢出群組","k_1ovt677":"用戶$option3加入了群組","k_0k05b8b":"$option2 被","k_0wm4xeb":"系統訊息 $option2","k_0nbq9v3":"通話時間:$option2","k_0i1kf53":"[檔案] $option2","k_1gnnby6":"想訪問您的$option2","k_1wh4atg":"$option2 分鐘前","k_07sh7g1":"昨天 $option2","k_1pj8xzh":"我的用戶ID: $option2","k_0py1evo":"個性簽名: $option2","k_1kvj4i2":"$option2回收了一條訊息","k_1v0lbpp":"「$option2」暫不可以開啟此類檔案,你可以使用其他應用開啟並預覽","k_0torwfz":"選擇成功$option2","k_0i1bjah":"$option1回收了一條訊息","k_1qzxh9q":"通話時間:$option3","k_0wrgmom":"[語音通話]:$option1","k_06ix2f0":"[視訊通話]:$option2","k_08o3z5w":"[檔案] $option1","k_0ezbepg":"$option2[有人@我]","k_1ccnht1":"$option2[@所有人]","k_1k3arsw":"管理員 ($option2/10)","k_1d4golg":"群成員($option1人)","k_1bg69nt":"$option1人","k_00gjqxj":"個性簽名: $option1","k_0c29cxr":"$option1條相關聊天記錄","k_1twk5rz":"與$option1的聊天記錄","k_1vn4xq1":"將 $adminMember 取消管理員","k_0e35hsw":"為方便您將所拍攝的照片或影片發送給朋友,以及進行視訊通話,請允許我們訪問攝像頭進行拍攝照片和影片。","k_0dj6yr7":"為方便您發送語音訊息、拍攝影片以及音視訊通話,請允許我們使用咪高風進行錄音。","k_003qnsl":"存儲","k_0s3rtpw":"為方便您查看和選擇相冊裏的圖片影片發送給朋友,以及保存內容到設備,請允許我們訪問您設備上的照片、媒體內容。","k_0tezv85":" 申請獲取$option2","k_002rety":"權限","k_18o68ro":"需要授予","k_1onpf8u":" 相機權限,以正常使用拍攝圖片/影片、視訊通話等功能。","k_17irga5":" 咪高風權限,以正常使用發送語音訊息、拍攝影片、音視訊通話等功能。","k_0572kc4":" 訪問照片權限,以正常使用發送圖片、影片等功能。","k_0slykws":" 訪問相冊寫入權限,以正常使用存儲圖片、影片等功能。","k_119pkcd":" 檔案讀寫權限,以正常使用在聊天功能中的圖片查看、選擇能力和發送檔案的能力。","k_0gqewd3":"以後再說","k_03eq4s1":"去開啟","k_0nt2uyg":"回到最新位置","k_04l16at":"$option1條新訊息","k_13p3w93":"有人@我","k_18w5uk6":"@所有人","k_0jmujgh":"其他檔案正在接收中","k_12s5ept":"訊息詳情","k_0mxa4f4":"$option1人已讀","k_061tue3":"$option2人未讀","k_18qjstb":"轉讓群主","k_0on1aj2":"有$option2條@我訊息","k_09j4izl":"[有人@我] ","k_1oqtjw0":"[@所有人] ","k_1x5a9vb":"我是: $option1","k_14n31e7":"進群請求","k_08nc5j1":"群類型: $option1","k_1josu12":"$option1 條入群請求","k_0n2x5s0":"驗證消息: $option2","k_03c1nx0":"已同意","k_03aw9w8":"已拒絕","k_038ryos":"去處理","k_0gw8pum":"進群申請","k_1gcvfrj":"請填寫備註名","k_002v9zj":"确认","k_10oqrki":"轻触拍照","k_0f8b3ws":"加载失败","k_11cm5lm":"手动聚焦","k_002uzrd":"预览","k_003qkn3":"录像","k_003k6a7":"拍照","k_0bqpqco":"拍照按钮","k_1626ozl":"停止录像","k_003lvmu":"前置","k_003lued":"后置","k_003lwzh":"外置","k_002qzi3":"关闭","k_003pufb":"自动","k_0apm0ze":"拍照时闪光","k_157zog5":"始终闪光","k_0cfyqhy":"$option1 画面预览","k_0phctlz":"闪光模式: $option2","k_02vfqe0":"切换至 $option3 摄像头","k_0f0y9ex":"说话时间太短","k_0ln70tk":"无法打开URL","k_11a3jdv":"轻触拍照,长按摄像","k_1k18miv":"请传入离开群组生命周期函数,提供返回首页或其他页面的导航方法。","k_1fu9ahv":"全员禁言状态","k_0gmwbnd":"全员禁言中","k_0got2zr":"您被禁言","k_0y9jck8":"你必须自定义search bar,并处理点击跳转","k_0yum3tv":"如使用自定义区域,请在profileWidgetBuilder传入对应组件","k_09kalj0":"清空聊天记录","k_14j5iul":"删除并退出","k_125ru1w":"解散该群","k_0jtutmw":"退出后不会接收到此群聊消息","k_0jtzmqa":"解散后不会接收到此群聊消息","k_0r8fi93":"好友添加成功","k_02qw14e":"好友申请已发出","k_0n3md5x":"当前用户在黑名单","k_094phq4":"好友添加失败","k_129scag":"好友删除成功","k_129uzfn":"好友删除失败","k_1666isy":"清除好友","k_1679vrd":"加为好友","k_1ualc52":"看看对方带来的数据是啥","k_0szluvp":"设置对方在线状态","k_0f4rnf8":"该用户已是好友","k_1tdkom4":"您已是群成员","k_1p2lyuz":"对方正在输入中...","k_1g8wfpy":"...共$option1人","k_12rv9vw":"回应详情","k_0havgi0":"[查看详情 >>](${linkMessage.link})","k_0n9p7g8":"群组不存在","k_1tdh5vn":"您不是群成员","k_0h1q57v":"暂无群成员","k_0y5drq1":"[查看详情 >>]($option1)","k_03pjp61":"[表情消息]","k_1jpvzul":"[自定义消息]","k_03u3bh1":"[文件消息]","k_1odsnsw":"[群消息]","k_03sel4t":"[图片消息]","k_03sfw3r":"[位置消息]","k_03xpuwq":"[合并消息]","k_07ycxwo":"[没有元素]","k_03rc9vz":"[文本消息]","k_046uopf":"[视频消息]","k_0ehmsun":"设备存储空间不足,建议清理,以获得更好使用体验","k_003kmos":"图片","k_002s86q":"视频","k_06bk5ei":"视频消息仅限 mp4 格式","k_13opfxf":"Web网页端不支持搜索","k_1i0o0y2":"暂时仅限 Android/iOS 端","k_045dtzl":"$option1的聊天记录","k_0t0131u":"群资料信息","k_18ok8xz":"消息接收方式","k_03ax3ks":"群资料","k_0sqvoqo":"将 $option1 设置为管理员","k_1gbg1v8":"将 $option1 取消管理员","k_17k64g4":"群聊创建成功!","k_05mn217":"暂未安装表情包插件,如需使用表情相关功能,请根据本文档安装:https://cloud.tencent.com/document/product/269/70746","k_14j17nz":"暂无表情包","k_0fvjexh":"正在下载中","k_1cdagzz":"已加入待下载队列,其他文件下载中","k_0g4vojc":"开始下载","k_1g32es3":"[调皮]@2x.png","k_1g8qorz":"[爱你]@2x.png","k_1g4hmx6":"[爱情]@2x.png","k_1g6b558":"[爱心]@2x.png","k_1g3m4su":"[傲慢]@2x.png","k_1g2jym7":"[白眼]@2x.png","k_0cgkxuw":"[棒棒糖]@2x.png","k_1g48br2":"[抱抱]@2x.png","k_1g49ol8":"[抱拳]@2x.png","k_1g0ras3":"[爆筋]@2x.png","k_1ghy881":"[鄙视]@2x.png","k_1g86bmv":"[闭嘴]@2x.png","k_1g1xs1p":"[鞭炮]@2x.png","k_1g8i6ri":"[便便]@2x.png","k_1g2u5kf":"[擦汗]@2x.png","k_1g60uwh":"[彩带]@2x.png","k_1g1o0d0":"[彩球]@2x.png","k_1g6a6yq":"[菜刀]@2x.png","k_1g6vqo2":"[差劲]@2x.png","k_1g0kvjc":"[钞票]@2x.png","k_1g65x7e":"[车厢]@2x.png","k_0e1tjol":"[打哈欠]@2x.png","k_1g65n58":"[大兵]@2x.png","k_1g7se7o":"[大哭]@2x.png","k_1g03868":"[蛋糕]@2x.png","k_1h8nm66":"[刀]@2x.png","k_1g3dlpi":"[得意]@2x.png","k_1g3u434":"[灯泡]@2x.png","k_1giuqs7":"[凋谢]@2x.png","k_1g8r0r9":"[多云]@2x.png","k_1g7k6i1":"[发呆]@2x.png","k_1g44zsp":"[发抖]@2x.png","k_1g5l96i":"[飞机]@2x.png","k_1g7wsqj":"[飞吻]@2x.png","k_1g49luq":"[奋斗]@2x.png","k_1gixbsm":"[风车]@2x.png","k_1g6cqbq":"[尴尬]@2x.png","k_1g6jbw5":"[勾引]@2x.png","k_1g3lwo1":"[鼓掌]@2x.png","k_1g13nkj":"[害羞]@2x.png","k_1g0mt47":"[憨笑]@2x.png","k_0bxujkf":"[红灯笼]@2x.png","k_0hhaeh8":"[红双喜]@2x.png","k_1g0jnts":"[坏笑]@2x.png","k_1g46g9c":"[挥手]@2x.png","k_1g4vi9g":"[回头]@2x.png","k_1gf7hes":"[饥饿]@2x.png","k_1g6mvsm":"[激动]@2x.png","k_1gku5mf":"[街舞]@2x.png","k_1g4hidg":"[惊恐]@2x.png","k_1gjbrtu":"[惊讶]@2x.png","k_1g6sand":"[咖啡]@2x.png","k_1g4s8rj":"[磕头]@2x.png","k_1g1wn34":"[可爱]@2x.png","k_1g3l0wd":"[可怜]@2x.png","k_1ggaon9":"[抠鼻]@2x.png","k_1ggvcb0":"[骷髅]@2x.png","k_1h8yqjt":"[酷]@2x.png","k_0jac97i":"[快哭了]@2x.png","k_1h8oiby":"[困]@2x.png","k_1g0s5hg":"[蜡烛]@2x.png","k_1g1iuer":"[篮球]@2x.png","k_1g2xjfi":"[冷汗]@2x.png","k_0s5oyqw":"[礼品袋]@2x.png","k_1g1qqvf":"[礼物]@2x.png","k_1g2slew":"[流汗]@2x.png","k_1g3z9xx":"[流泪]@2x.png","k_1g6pabn":"[麻将]@2x.png","k_0pkaxul":"[麦克风]@2x.png","k_1g7m0zj":"[猫咪]@2x.png","k_0ibvtpo":"[么么哒]@2x.png","k_1g1hoh1":"[玫瑰]@2x.png","k_1gfzeow":"[米饭]@2x.png","k_1g5l15p":"[面条]@2x.png","k_1g2hfa6":"[奶瓶]@2x.png","k_1gix9pj":"[难过]@2x.png","k_1giqn6g":"[闹钟]@2x.png","k_1h8kd64":"[怒]@2x.png","k_1g0vui9":"[怄火]@2x.png","k_1g1jsj7":"[皮球]@2x.png","k_1ghdluw":"[啤酒]@2x.png","k_1gl6ec7":"[瓢虫]@2x.png","k_1g7gg5p":"[撇嘴]@2x.png","k_1g8psin":"[乒乓]@2x.png","k_1gjzu3p":"[汽车]@2x.png","k_1h8mr0k":"[强]@2x.png","k_1g45y2n":"[敲打]@2x.png","k_1gkaxsl":"[青蛙]@2x.png","k_0jcfnoo":"[糗大了]@2x.png","k_1g4njy1":"[拳头]@2x.png","k_1h8mqr3":"[弱]@2x.png","k_1h926fg":"[色]@2x.png","k_1g6rtbq":"[沙发]@2x.png","k_1giirh6":"[删除]@2x.png","k_1g14ny9":"[闪电]@2x.png","k_1g6bmsr":"[胜利]@2x.png","k_1g1rytx":"[示爱]@2x.png","k_1g52fbz":"[手枪]@2x.png","k_1h90dam":"[衰]@2x.png","k_1gigiae":"[睡觉]@2x.png","k_1gijchz":"[太阳]@2x.png","k_1g1sgji":"[跳绳]@2x.png","k_1gjwuri":"[跳跳]@2x.png","k_1g0juhk":"[偷笑]@2x.png","k_1h8nzla":"[吐]@2x.png","k_1g6cv0i":"[委屈]@2x.png","k_1g46l5g":"[握手]@2x.png","k_1g2pgkd":"[西瓜]@2x.png","k_1ging9p":"[下雨]@2x.png","k_1h8nzil":"[吓]@2x.png","k_1g7q7wr":"[献吻]@2x.png","k_1gl6uum":"[香蕉]@2x.png","k_1g23fys":"[象棋]@2x.png","k_0j75rdh":"[心碎了]@2x.png","k_1g6ajj2":"[信封]@2x.png","k_1g21prz":"[熊猫]@2x.png","k_1h8octi":"[嘘]@2x.png","k_1h91zox":"[药]@2x.png","k_1ghttfl":"[疑问]@2x.png","k_1ghk7sz":"[阴险]@2x.png","k_0gl37zz":"[右车头]@2x.png","k_0ifkj1p":"[右哼哼]@2x.png","k_0g1yh2e":"[右太极]@2x.png","k_1g9dkfc":"[雨伞]@2x.png","k_1g8jl88":"[月亮]@2x.png","k_1h8lhqj":"[晕]@2x.png","k_1gi9x2q":"[再见]@2x.png","k_1g6dwwv":"[炸弹]@2x.png","k_1fzmkfi":"[折磨]@2x.png","k_1g6jbiw":"[纸巾]@2x.png","k_1ggjnwu":"[咒骂]@2x.png","k_1g4qlq8":"[猪头]@2x.png","k_1g1lqzz":"[抓狂]@2x.png","k_1g80j3u":"[转圈]@2x.png","k_1g0z55s":"[龇牙]@2x.png","k_1g3ju6v":"[钻戒]@2x.png","k_0gl51l6":"[左车头]@2x.png","k_0iflllk":"[左哼哼]@2x.png","k_0g1y3ir":"[左太极]@2x.png","k_026hiq5":"消息列表加载中","k_003tu8k":"爱你","k_003myvp":"傲慢","k_003kddw":"白眼","k_039yfhv":"棒棒糖","k_003nu3p":"抱抱","k_003nijr":"抱拳","k_003mg88":"爆筋","k_002v17e":"鄙视","k_003qhy4":"闭嘴","k_003l5fq":"鞭炮","k_003uacl":"便便","k_003oq1g":"擦汗","k_003qvey":"彩带","k_003jci7":"彩球","k_003pyu1":"菜刀","k_003q97d":"差劲","k_003po5d":"车厢","k_03eadb2":"打哈欠","k_003pnuf":"大兵","k_003kg57":"蛋糕","k_003mxkt":"得意","k_003onu3":"灯泡","k_002uv8s":"凋谢","k_003kqy0":"调皮","k_003tyum":"多云","k_003pv9u":"发呆","k_036o6mu":"发抖t","k_003nogx":"飞机","k_003q7wg":"飞吻","k_003m0jd":"奋斗","k_002ult9":"风车","k_003r8gt":"尴尬","k_003qy4u":"勾引","k_003mnoa":"鼓掌","k_003lmw8":"害羞","k_003mb30":"憨笑","k_03bj41g":"红灯笼","k_03dxw2f":"红双喜","k_003mk57":"坏笑","k_003nmvf":"挥手","k_003r2i7":"回头","k_002s6f3":"饥饿","k_003qd0t":"激动","k_002vgi4":"街舞","k_003nz33":"惊恐","k_002wh4p":"惊讶","k_003ozpu":"咖啡","k_003qvs4":"磕头","k_003l3wb":"可爱","k_003nuwm":"可怜","k_002rw1q":"抠鼻","k_002tujb":"骷髅","k_00030eq":"酷","k_03i8ath":"快哭了","k_000421h":"困","k_003l5i7":"蜡烛","k_003j72g":"篮球","k_003ofwl":"冷汗","k_02mw65v":"礼品袋","k_003ku40":"礼物","k_003ookz":"流汗","k_003on72":"流泪","k_003rjy0":"麻将","k_003q2f8":"猫咪","k_03et393":"么么哒","k_003j7j2":"玫瑰","k_002sr0b":"米饭","k_003nnza":"面条","k_003jef9":"奶瓶","k_002umn0":"难过","k_002rjib":"闹钟","k_0003zcn":"怒","k_003jzwq":"怄火","k_003j4js":"皮球","k_002r5ir":"啤酒","k_002ubu4":"瓢虫","k_003ppo6":"撇嘴","k_003ty3o":"乒乓","k_002vxwe":"汽车","k_00043hb":"强","k_003nmbo":"敲打","k_002tfhq":"青蛙","k_03i7lrn":"糗大了","k_003r03m":"拳头","k_00043h0":"弱","k_000345z":"色","k_003qmp9":"沙发","k_003it8a":"闪电","k_003pxow":"胜利","k_003kw8e":"示爱","k_003n99g":"手枪","k_00035cl":"衰","k_002vl3h":"睡觉","k_002rgqk":"太阳","k_003m9d1":"跳绳","k_002vobp":"跳跳","k_003mkoz":"偷笑","k_00041px":"吐","k_003rjh5":"委屈","k_003j36u":"西瓜","k_002re92":"下雨","k_00041py":"吓","k_003q06o":"献吻","k_002ubjp":"香蕉","k_003o2tr":"象棋","k_03ie6pa":"心碎了","k_003rao5":"信封","k_003l3us":"熊猫","k_000424d":"嘘","k_00033yi":"药","k_002qtyy":"疑问","k_002qe0o":"阴险","k_03gu7us":"右车头","k_03ere8m":"右哼哼","k_003uqk3":"雨伞","k_003tzdv":"月亮","k_0003z00":"晕","k_002vdrd":"再见","k_003ra1w":"炸弹","k_003lcad":"折磨","k_003q7sz":"纸巾","k_002thn9":"咒骂","k_003qx7f":"猪头","k_003l044":"抓狂","k_003qg4h":"转圈","k_003kb97":"龇牙","k_03gu53l":"左车头","k_03erd1f":"左哼哼","k_003nyvl":"爱情","k_003r85z":"爱心","k_003mk8j":"钞票","k_003pwfj":"大哭","k_00042w5":"刀","k_003nmtr":"握手","k_03c529p":"右太极","k_003n4mk":"钻戒","k_03c5488":"左太极","k_1llp7tu":"该用户不存在","k_0tbyqyb":"加载中…","k_0td1p3f":"保存中…","k_1klqdh1":"仅限汉字、英文、数字和下划线","k_03el5lp":"未填写","k_1ui0gai":"搜索指定内容","k_003nvk2":"消息","k_03agld7":"群提示","k_0elt0kw":"添加群聊","k_0s3sgel":"移出黑名单","k_1qqgjra":"$option3条未读消息","k_0uubyjr":"以下为未读消息","k_16as7eq":"表情回应","k_003s12u":"回复","k_003s38r":"更多","k_002wkr3":"翻译","k_13g4hxv":"翻译完成","k_003molk":"表情","k_165bbw6":"消息历史","k_13sqc0z":"清除消息","k_0glns86":"删除会话","k_13s99rx":"清空消息","k_11vsa3j":"退出群组","k_11vvszp":"解散群组","k_15i9w72":"群管理员","k_0p3espj":"设置备注名","k_118sw9v":"立即搜索","k_0h20hg5":"视频通话","k_0h22snw":"语音通话","k_003lz6t":"对方","k_1xf4yre":"发送给$option1","k_003por5":"截图","k_1rw7s82":" 访问相册中视频权限,以正常使用发送视频等功能。","k_003rcwm":"打开","k_1698c42":"在访达中打开","k_066fxsz":"查看文件夹","k_0k432y2":"无法发送,包含文件夹","k_002wb4y":"会话"} \ No newline at end of file diff --git a/lib/tencent_cloud_chat_uikit.dart b/lib/tencent_cloud_chat_uikit.dart index 4c8ec65..90cc3dd 100644 --- a/lib/tencent_cloud_chat_uikit.dart +++ b/lib/tencent_cloud_chat_uikit.dart @@ -32,12 +32,12 @@ export 'package:tencent_cloud_chat_uikit/ui/views/TIMUIKitSearch/tim_uikit_searc export 'package:tencent_cloud_chat_uikit/ui/views/TIMUIKitChat/TIMUIKitTextField/tim_uikit_text_field_controller.dart'; export 'package:tencent_cloud_chat_uikit/ui/views/TIMUIKitChat/TIMUIKitAppBar/tim_uikit_appbar.dart'; export 'package:tencent_cloud_chat_uikit/ui/views/TIMUIKitChat/TIMUIKItMessageList/tim_uikit_chat_history_message_list.dart'; -export 'package:tencent_cloud_chat_uikit/ui/views/TIMUIKitChat/TIMUIKItMessageList/tim_uikit_chat_history_message_list_item.dart'; export 'package:tencent_cloud_chat_uikit/ui/views/TIMUIKitChat/TIMUIKitTextField/tim_uikit_text_field.dart'; export 'package:tencent_cloud_chat_uikit/ui/views/TIMUIKitGroup/tim_uikit_group_application_list.dart'; export 'package:tencent_im_base/tencent_im_base.dart'; export 'package:tencent_cloud_chat_uikit/ui/widgets/link_preview/models/link_preview_content.dart'; -export 'package:tencent_cloud_chat_uikit/ui/views/TIMUIKitProfile/widget/tim_uikit_profile_userinfo_card.dart'; +export 'package:tencent_cloud_chat_uikit/ui/widgets/column_menu.dart'; +export 'package:tencent_cloud_chat_uikit/ui/views/TIMUIKitProfile/widget/tim_uikit_profile_userinfo_card/tim_uikit_profile_userinfo_card.dart'; export 'package:tencent_cloud_chat_uikit/ui/views/TIMUIKitConversation/tim_ui_kit_conversation_total_unread.dart'; // Enum @@ -52,6 +52,7 @@ export 'package:permission_handler/permission_handler.dart'; // Utils export 'package:tencent_cloud_chat_uikit/ui/utils/common_utils.dart'; +export 'package:tencent_cloud_uikit_core/tencent_cloud_uikit_core.dart'; class TIMUIKitCore { static CoreServicesImpl getInstance() { diff --git a/lib/ui/controller/tim_uikit_chat_controller.dart b/lib/ui/controller/tim_uikit_chat_controller.dart index 2152058..a2fadb7 100644 --- a/lib/ui/controller/tim_uikit_chat_controller.dart +++ b/lib/ui/controller/tim_uikit_chat_controller.dart @@ -17,17 +17,16 @@ class TIMUIKitChatController { } } - Future loadHistoryMessageList({ - HistoryMsgGetTypeEnum getType = - HistoryMsgGetTypeEnum.V2TIM_GET_CLOUD_OLDER_MSG, - String? userID, - String? groupID, - int lastMsgSeq = -1, - required int count, - String? lastMsgID, - LoadDirection direction = LoadDirection.previous - }) async { - return await model?.loadData( + Future loadHistoryMessageList( + {HistoryMsgGetTypeEnum getType = + HistoryMsgGetTypeEnum.V2TIM_GET_CLOUD_OLDER_MSG, + String? userID, + String? groupID, + int lastMsgSeq = -1, + required int count, + String? lastMsgID, + LoadDirection direction = LoadDirection.previous}) async { + return await model?.loadChatRecord( count: count, getType: getType, lastMsgID: lastMsgID, @@ -70,7 +69,6 @@ class TIMUIKitChatController { /// Please provide `convID`, if you use `TIMUIKitChatController` without specifying to a `TIMUIKitChat`. Future updateMessage( { - /// The ID of the target conversation String? convID, @@ -88,32 +86,47 @@ class TIMUIKitChatController { return; } - /// Send message; - /// 发送消息 - /// Please provide `convType` and `convID`, if you use `TIMUIKitChatController` without specifying to a `TIMUIKitChat`. + /// Sends a message to the specified conversation, or to the current conversation specified on `TIMUIKitChat`. + /// 发送消息到指定的对话,或者发送到 `TIMUIKitChat` 中指定的当前对话。 + /// You must provide `convType` and either `userID` or `groupID`, only if you use `TIMUIKitChat` without specifying a `TIMUIKitChatController`, you must provide these parameters. + /// 您需要提供 `convType` 和 `userID` 或 `groupID`, 只有在如果您使用 `TIMUIKitChat` 而没有指定 `TIMUIKitChatController`,则必须提供这些参数。 Future?>? sendMessage({ required V2TimMessage? messageInfo, - /// The type of the target conversation + /// The type of the target conversation: either ConvType.group or ConvType.c2c. Required if using `TIMUIKitChat` without specifying a `TIMUIKitChatController`. + /// 目标对话的类型:ConvType.group 或 ConvType.c2c。只有在如果您使用 `TIMUIKitChat` 而没有指定 `TIMUIKitChatController`,则必须提供此参数。 ConvType? convType, - /// The ID of the target conversation - String? convID, + /// The user ID of the target one-to-one conversation. Required if convType is ConvType.c2c. + /// 目标一对一对话的用户 ID。如果 convType 是 ConvType.c2c,则必填。 + String? userID, - /// The method for updating the input field when message sending failed + /// The target group ID. Required if convType is ConvType.group. + /// 目标群组的 ID。如果 convType 是 ConvType.group,则必填。 + String? groupID, + + /// A callback function to update the input field when message sending fails. + /// 当消息发送失败时,用于更新输入框的回调函数。 ValueChanged? setInputField, - /// Offline push info + /// Offline push information. + /// 离线推送信息。 OfflinePushInfo? offlinePushInfo, }) { - if (convID != null && convType != null) { + if (convType != null) { + /// Sends a message to the specified conversation. 发送消息到指定的对话。 + assert((groupID == null) != (userID == null)); + assert(groupID != null || convType != ConvType.group); + assert(userID != null || convType != ConvType.c2c); + return globalChatModel.sendMessageFromController( messageInfo: messageInfo, convType: convType, - convID: convID, + convID: (convType == ConvType.group ? groupID : userID) ?? "", setInputField: setInputField, offlinePushInfo: offlinePushInfo); } else if (model != null) { + /// Sends a message to the current conversation specified on `TIMUIKitChat`. 发送到 `TIMUIKitChat` 中指定的当前对话。 return model!.sendMessageFromController( messageInfo: messageInfo, offlinePushInfo: offlinePushInfo); } diff --git a/lib/ui/controller/tim_uikit_conversation_controller.dart b/lib/ui/controller/tim_uikit_conversation_controller.dart index 9979cb2..37e7b0c 100644 --- a/lib/ui/controller/tim_uikit_conversation_controller.dart +++ b/lib/ui/controller/tim_uikit_conversation_controller.dart @@ -9,6 +9,11 @@ class TIMUIKitConversationController { return model.selectedConversation; } + /// Set the selected conversation currently + set selectedConversation(V2TimConversation? conversation) { + model.selectedConversation = conversation; + } + /// Get the conversation list List get conversationList { return model.conversationList; diff --git a/lib/ui/controller/tim_uikit_profile_controller.dart b/lib/ui/controller/tim_uikit_profile_controller.dart index 9fbf53c..f2fe078 100644 --- a/lib/ui/controller/tim_uikit_profile_controller.dart +++ b/lib/ui/controller/tim_uikit_profile_controller.dart @@ -47,15 +47,23 @@ class TIMUIKitProfileController { } /// Show the text input bottom sheet - showTextInputBottomSheet( - BuildContext context, - String title, - String tips, - void Function(String) onSubmitted, - TUITheme theme - ) { + showTextInputBottomSheet({ + required BuildContext context, + required String title, + required String tips, + required Function(String text) onSubmitted, + required TUITheme theme, + Offset? initOffset, + String? initText, + }) { TextInputBottomSheet.showTextInputBottomSheet( - context, title, tips, onSubmitted, theme); + context: context, + title: title, + tips: tips, + onSubmitted: onSubmitted, + theme: theme, + initOffset: initOffset, + initText: initText); } /// Load the profile data diff --git a/lib/ui/utils/common_utils.dart b/lib/ui/utils/common_utils.dart index 5487769..1da0c53 100644 --- a/lib/ui/utils/common_utils.dart +++ b/lib/ui/utils/common_utils.dart @@ -6,4 +6,153 @@ class TencentUtils{ static String? checkString(String? text){ return (text != null && text.isEmpty) ? null : text; } + + static String? checkStringWithoutSpace(String? text){ + if (text == null || text.trim().isEmpty || text.contains(' ')) { + return null; + } + return text; + } + + static String getFileType(String fileType) { + switch (fileType) { + case "3gp": + return "video/3gpp"; + case "torrent": + return "application/x-bittorrent"; + case "kml": + return "application/vnd.google-earth.kml+xml"; + case "gpx": + return "application/gpx+xml"; + case "asf": + return "video/x-ms-asf"; + case "avi": + return "video/x-msvideo"; + case "bin": + case "class": + case "exe": + return "application/octet-stream"; + case "bmp": + return "image/bmp"; + case "c": + return "text/plain"; + case "conf": + return "text/plain"; + case "cpp": + return "text/plain"; + case "doc": + return "application/msword"; + case "docx": + return "application/vnd.openxmlformats-officedocument.wordprocessingml.document"; + case "xls": + case "csv": + return "application/vnd.ms-excel"; + case "xlsx": + return "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"; + case "gif": + return "image/gif"; + case "gtar": + return "application/x-gtar"; + case "gz": + return "application/x-gzip"; + case "h": + return "text/plain"; + case "htm": + return "text/html"; + case "html": + return "text/html"; + case "jar": + return "application/java-archive"; + case "java": + return "text/plain"; + case "jpeg": + return "image/jpeg"; + case "jpg": + return "image/jpeg"; + case "js": + return "application/x-javascript"; + case "log": + return "text/plain"; + case "m3u": + return "audio/x-mpegurl"; + case "m4a": + return "audio/mp4a-latm"; + case "m4b": + return "audio/mp4a-latm"; + case "m4p": + return "audio/mp4a-latm"; + case "m4u": + return "video/vnd.mpegurl"; + case "m4v": + return "video/x-m4v"; + case "mov": + return "video/quicktime"; + case "mp2": + return "audio/x-mpeg"; + case "mp3": + return "audio/x-mpeg"; + case "mp4": + return "video/mp4"; + case "mpc": + return "application/vnd.mpohun.certificate"; + case "mpe": + return "video/mpeg"; + case "mpeg": + return "video/mpeg"; + case "mpg": + return "video/mpeg"; + case "mpg4": + return "video/mp4"; + case "mpga": + return "audio/mpeg"; + case "msg": + return "application/vnd.ms-outlook"; + case "ogg": + return "audio/ogg"; + case "pdf": + return "application/pdf"; + case "png": + return "image/png"; + case "pps": + return "application/vnd.ms-powerpoint"; + case "ppt": + return "application/vnd.ms-powerpoint"; + case "pptx": + return "application/vnd.openxmlformats-officedocument.presentationml.presentation"; + case "prop": + return "text/plain"; + case "rc": + return "text/plain"; + case "rmvb": + return "audio/x-pn-realaudio"; + case "rtf": + return "application/rtf"; + case "sh": + return "text/plain"; + case "tar": + return "application/x-tar"; + case "tgz": + return "application/x-compressed"; + case "txt": + return "text/plain"; + case "wav": + return "audio/x-wav"; + case "wma": + return "audio/x-ms-wma"; + case "wmv": + return "audio/x-ms-wmv"; + case "wps": + return "application/vnd.ms-works"; + case "xml": + return "text/plain"; + case "z": + return "application/x-compress"; + case "zip": + return "application/x-zip-compressed"; + default: + return "*/*"; + } + } + + } \ No newline at end of file diff --git a/lib/ui/utils/message.dart b/lib/ui/utils/message.dart index 0a28527..32575ef 100644 --- a/lib/ui/utils/message.dart +++ b/lib/ui/utils/message.dart @@ -1,13 +1,12 @@ // ignore_for_file: unrelated_type_equality_checks, avoid_print import 'dart:convert'; - -import 'package:collection/collection.dart'; import 'package:flutter/cupertino.dart'; -import 'package:tencent_cloud_chat_uikit/ui/utils/common_utils.dart'; import 'package:tencent_im_base/tencent_im_base.dart'; import 'package:tencent_cloud_chat_uikit/ui/constants/history_message_constant.dart'; import 'package:tencent_cloud_chat_uikit/ui/constants/time.dart'; +import 'package:collection/collection.dart'; +import 'package:tencent_cloud_chat_uikit/ui/utils/common_utils.dart'; class MessageUtils { // 判断CallingData的方式和Trtc的方法一致 @@ -101,9 +100,12 @@ class MessageUtils { } static String? _getOpUserNick(V2TimGroupMemberInfo? opUser) { - return TencentUtils.checkString(opUser?.friendRemark) ?? - TencentUtils.checkString(opUser?.nickName) ?? - TencentUtils.checkString(opUser?.userID) ?? ""; + if(opUser == null){ + return ""; + } + return TencentUtils.checkString(opUser.friendRemark) ?? + TencentUtils.checkString(opUser.nickName) ?? + TencentUtils.checkString(opUser.userID); } static String? _getMemberNickName(V2TimGroupMemberInfo e) { @@ -155,21 +157,21 @@ class MessageUtils { break; case GroupTipsElemType.V2TIM_GROUP_TIPS_TYPE_INVITE: final option5 = - memberList!.map((e) => _getMemberNickName(e!).toString()).join("、"); + memberList!.map((e) => _getMemberNickName(e!).toString()).join("、"); final inviteUser = _getOpUserNick(operationMember); displayMessage = '$inviteUser' + TIM_t_para("邀请{{option5}}加入群组", "邀请$option5加入群组")(option5: option5); break; case GroupTipsElemType.V2TIM_GROUP_TIPS_TYPE_KICKED: final option4 = - memberList!.map((e) => _getMemberNickName(e!).toString()).join("、"); + memberList!.map((e) => _getMemberNickName(e!).toString()).join("、"); final kickUser = _getOpUserNick(operationMember); displayMessage = '$kickUser' + TIM_t_para("将{{option4}}踢出群组", "将$option4踢出群组")(option4: option4); break; case GroupTipsElemType.V2TIM_GROUP_TIPS_TYPE_JOIN: final option3 = - memberList!.map((e) => _getMemberNickName(e!).toString()).join("、"); + memberList!.map((e) => _getMemberNickName(e!).toString()).join("、"); displayMessage = TIM_t_para("用户{{option3}}加入了群聊", "用户$option3加入了群聊")( option3: option3); break; @@ -186,7 +188,7 @@ class MessageUtils { break; case GroupTipsElemType.V2TIM_GROUP_TIPS_TYPE_SET_ADMIN: final adminMember = - memberList!.map((e) => _getMemberNickName(e!).toString()).join("、"); + memberList!.map((e) => _getMemberNickName(e!).toString()).join("、"); final opMember = _getOpUserNick(operationMember); final option1 = adminMember; displayMessage = '$opMember' + @@ -195,7 +197,7 @@ class MessageUtils { break; case GroupTipsElemType.V2TIM_GROUP_TIPS_TYPE_CANCEL_ADMIN: final adminMember = - memberList!.map((e) => _getMemberNickName(e!).toString()).join("、"); + memberList!.map((e) => _getMemberNickName(e!).toString()).join("、"); final opMember = _getOpUserNick(operationMember); final option1 = adminMember; displayMessage = '$opMember' + @@ -276,46 +278,14 @@ class MessageUtils { } } - static Future getAbstractMessage(V2TimMessage message, - List groupMemberList) async { - final msgType = message.elemType; - switch (msgType) { - case MessageElemType.V2TIM_ELEM_TYPE_CUSTOM: - return handleCustomMessageString(message); - case MessageElemType.V2TIM_ELEM_TYPE_SOUND: - return TIM_t("[语音]"); - case MessageElemType.V2TIM_ELEM_TYPE_TEXT: - return message.textElem!.text as String; - case MessageElemType.V2TIM_ELEM_TYPE_FACE: - return TIM_t("[表情]"); - case MessageElemType.V2TIM_ELEM_TYPE_FILE: - final String? option2 = message.fileElem!.fileName ?? ""; - return TIM_t_para("[文件] {{option2}}", "[文件] $option2")( - option2: option2); - case MessageElemType.V2TIM_ELEM_TYPE_GROUP_TIPS: - return await MessageUtils.groupTipsMessageAbstract( - message.groupTipsElem!, groupMemberList); - case MessageElemType.V2TIM_ELEM_TYPE_IMAGE: - return TIM_t("[图片]"); - case MessageElemType.V2TIM_ELEM_TYPE_VIDEO: - return TIM_t("[视频]"); - case MessageElemType.V2TIM_ELEM_TYPE_LOCATION: - return TIM_t("[位置]"); - case MessageElemType.V2TIM_ELEM_TYPE_MERGER: - return TIM_t("[聊天记录]"); - default: - return TIM_t("未知消息"); - } - } - static V2TimImage? getImageFromImgList( List? list, List order) { V2TimImage? img; try { for (String type in order) { img = list?.firstWhere( - (e) => - e?.type == HistoryMessageDartConstant.V2_TIM_IMAGE_TYPES[type], + (e) => + e?.type == HistoryMessageDartConstant.V2_TIM_IMAGE_TYPES[type], orElse: () => null); } } catch (e) { @@ -332,10 +302,10 @@ class MessageUtils { final displayName = friendRemark.isNotEmpty ? friendRemark : nameCard.isNotEmpty - ? nameCard - : nickName.isNotEmpty - ? nickName - : sender; + ? nameCard + : nickName.isNotEmpty + ? nickName + : sender; return displayName.toString(); } diff --git a/lib/ui/utils/permission.dart b/lib/ui/utils/permission.dart index 74b1e29..675d05b 100644 --- a/lib/ui/utils/permission.dart +++ b/lib/ui/utils/permission.dart @@ -1,6 +1,7 @@ // ignore_for_file: unused_import import 'dart:io'; + import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:package_info_plus/package_info_plus.dart'; @@ -11,6 +12,7 @@ import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_base.dart'; import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_state.dart'; import 'package:tencent_cloud_chat_uikit/ui/utils/platform.dart'; + class PermissionRequestInfo extends StatefulWidget { final Function removeOverLay; final int permissionType; @@ -32,14 +34,14 @@ class _PermissionRequestInfo extends TIMUIKitState @override void initState() { super.initState(); - WidgetsBinding.instance?.addObserver(this); + WidgetsBinding.instance.addObserver(this); } @override void dispose() { widget.removeOverLay(); super.dispose(); - WidgetsBinding.instance?.removeObserver(this); + WidgetsBinding.instance.removeObserver(this); } @override @@ -65,11 +67,21 @@ class _PermissionRequestInfo extends TIMUIKitState "icon": "images/chat_permission_icon_mic.png", "text": TIM_t("为方便您发送语音消息、拍摄视频以及音视频通话,请允许我们使用麦克风进行录音。") }, + 9: { + "name": TIM_t("相册"), + "icon": "images/chat_permission_icon_file.png", + "text": TIM_t("为方便您查看和选择相册里的图片视频发送给朋友,以及保存内容到设备,请允许我们访问您设备上的照片、媒体内容。") + }, 15: { "name": TIM_t("存储"), "icon": "images/chat_permission_icon_file.png", "text": TIM_t("为方便您查看和选择相册里的图片视频发送给朋友,以及保存内容到设备,请允许我们访问您设备上的照片、媒体内容。") }, + 32: { + "name": TIM_t("相册"), + "icon": "images/chat_permission_icon_file.png", + "text": TIM_t("为方便您查看和选择相册里的图片视频发送给朋友,以及保存内容到设备,请允许我们访问您设备上的照片、媒体内容。") + }, }[widget.permissionType]; final option2 = permission?["name"] ?? ""; return Stack( @@ -128,6 +140,9 @@ class _PermissionRequestInfo extends TIMUIKitState } class Permissions { + + static OverlayEntry? _entry; + static List _names(BuildContext context) { return [ TIM_t("日历"), @@ -161,6 +176,10 @@ class Permissions { 'bluetoothScan', 'bluetoothAdvertise', 'bluetoothConnect', + 'nearbyWifiDevices', + TIM_t("视频"), + 'audio', + 'scheduleExactAlarm' ]; } @@ -199,6 +218,10 @@ class Permissions { 'bluetoothScan', 'bluetoothAdvertise', 'bluetoothConnect', + 'nearbyWifiDevices', + TIM_t(" 访问相册中视频权限,以正常使用发送视频等功能。"), + 'audio', + 'scheduleExactAlarm' ]; return _prefix + appName + _postfixList[value]; } @@ -220,7 +243,10 @@ class Permissions { isShowPermissionPage, ); if (shouldRequestPermission != null && shouldRequestPermission) { - return await Permission.byValue(value).request().isGranted; + final isGranted = await Permission.byValue(value).request().isGranted; + _entry?.remove(); + _entry = null; + return isGranted; } return shouldRequestPermission ?? false; } @@ -241,11 +267,13 @@ class Permissions { static showPermissionRequestInfoDialog(BuildContext context, value) async { PackageInfo packageInfo = await PackageInfo.fromPlatform(); String appName = packageInfo.appName; - OverlayEntry? _entry; final entry = OverlayEntry(builder: (context) { return PermissionRequestInfo( appName: appName, - removeOverLay: () => _entry?.remove(), + removeOverLay: () { + _entry?.remove(); + _entry = null; + }, permissionType: value, ); }); @@ -269,6 +297,7 @@ class Permissions { String appName = packageInfo.appName; final option2 = _names(context)[value]; final permissionText = _permissionText(context, appName, value); + void closeDialog() { Navigator.of(context).pop(false); } diff --git a/lib/ui/utils/platform.dart b/lib/ui/utils/platform.dart index 3aa485e..4272f2d 100644 --- a/lib/ui/utils/platform.dart +++ b/lib/ui/utils/platform.dart @@ -5,13 +5,23 @@ class PlatformUtils { PlatformUtils._internal(); static late bool _isAndroid; static late bool _isIos; + static late bool _isMobile; static late bool _isWeb; + static late bool _isWindows; + static late bool _isMacOS; + static late bool _isLinux; + static late bool _isDesktop; static bool _isInstantiation = false; factory PlatformUtils() { if (!_isInstantiation) { _isAndroid = !kIsWeb && Platform.isAndroid; _isIos = !kIsWeb && Platform.isIOS; + _isMobile = _isAndroid || _isIos; + _isWindows = !kIsWeb && Platform.isWindows; + _isMacOS = !kIsWeb && Platform.isMacOS; + _isLinux = !kIsWeb && Platform.isLinux; + _isDesktop = _isMacOS || _isWindows || _isLinux; _isWeb = kIsWeb; _isInstantiation = true; } @@ -32,4 +42,20 @@ class PlatformUtils { get isIOS { return _isIos; } + + get isWindows { + return _isWindows; + } + + get isMacOS { + return _isMacOS; + } + + get isMobile { + return _isMobile; + } + + bool get isDesktop => _isDesktop; + + bool get isLinux => _isLinux; } diff --git a/lib/ui/utils/screen_shot.dart b/lib/ui/utils/screen_shot.dart new file mode 100644 index 0000000..00e7894 --- /dev/null +++ b/lib/ui/utils/screen_shot.dart @@ -0,0 +1,104 @@ +import 'dart:async'; +import 'dart:io'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:package_info_plus/package_info_plus.dart'; +import 'package:path/path.dart' as p; +import 'package:path_provider/path_provider.dart'; +import 'package:tencent_cloud_chat_uikit/ui/utils/platform.dart'; +import 'package:uuid/uuid.dart'; + +class ScreenshotHelper { + static Future captureScreen() async { + await requestScreenRecordingPermission(); + String directory; + + if(PlatformUtils().isWindows){ + final String documentsDirectoryPath = + "${Platform.environment['USERPROFILE']}"; + PackageInfo packageInfo = await PackageInfo.fromPlatform(); + String pkgName = packageInfo.packageName; + directory = p.join(documentsDirectoryPath, "Documents", ".TencentCloudChat", + pkgName, "screenshots"); + }else{ + final dic = await getApplicationSupportDirectory(); + directory = dic.path; + } + + const uuid = Uuid(); + final fileName = 'screenshot_${uuid.v4()}.png'; + final filePath = '$directory/$fileName'; + if (Platform.isMacOS) { + // 在macOS平台上使用screencapture工具 + final result = await Process.run( + 'screencapture', + ['-i', '-s', '-o', filePath], + ); + if (result.exitCode == 0) { + return filePath; + } else { + return null; + } + } else if (Platform.isWindows) { + // 在Windows平台上使用snippingtool工具 + final result = await Process.run( + 'snippingtool', + ['/clip', filePath], + ); + if (result.exitCode == 0) { + return filePath; + } else { + return null; + } + } else { + // 不支持的平台 + return null; + } + } + + static Future requestScreenRecordingPermission() async { + if (Platform.isMacOS) { + final result = await Process.run( + 'sh', ['-c', 'echo ${Platform.environment['USER']}']); + final username = result.stdout.toString().trim(); + const script = + 'tell application "System Events" to return exists (processes where name is "ControlCenter")'; + final process = await Process.run('osascript', ['-e', script]); + final isControlCenterRunning = process.stdout.toString().trim() == 'true'; + + if (!isControlCenterRunning) { + await Process.run('open', ['-a', 'ControlCenter']); + await Future.delayed(const Duration(seconds: 1)); + } + + final script2 = 'tell application "ControlCenter" to activate\n' + 'tell application "System Events"\n' + ' tell process "ControlCenter"\n' + ' click menu item "Screen Recording" of menu "File" of menu bar 1\n' + ' delay 0.5\n' + ' keystroke "$username" & return\n' + ' end tell\n' + 'end tell\n'; + await Process.run('osascript', ['-e', script2]); + + await Future.delayed(const Duration(seconds: 1)); + final isScreenRecordingEnabled = await SystemChannels.platform + .invokeMethod('isScreenRecordingEnabled'); + return isScreenRecordingEnabled ?? false; + } else { + return true; + } + } + + static Future getImageSize(String imagePath) async { + final bytes = await File(imagePath).readAsBytes(); + final completer = Completer(); + final imageStream = + Image.memory(bytes).image.resolve(ImageConfiguration.empty); + imageStream.addListener(ImageStreamListener((imageInfo, _) { + completer.complete(Size( + imageInfo.image.width.toDouble(), imageInfo.image.height.toDouble())); + })); + return completer.future; + } +} diff --git a/lib/ui/utils/screen_utils.dart b/lib/ui/utils/screen_utils.dart index 922a9e7..73b839c 100644 --- a/lib/ui/utils/screen_utils.dart +++ b/lib/ui/utils/screen_utils.dart @@ -2,20 +2,40 @@ import 'package:flutter/cupertino.dart'; -enum ScreenType { Desktop, Tablet, Handset, Watch } +enum DeviceType { Desktop, Mobile } class FormFactor { static double desktop = 900; - static double tablet = 600; static double handset = 300; } -class ScreenUtils { - static ScreenType getFormFactor(BuildContext context) { - double deviceWidth = MediaQuery.of(context).size.shortestSide; - if (deviceWidth > FormFactor.desktop) return ScreenType.Desktop; - if (deviceWidth > FormFactor.tablet) return ScreenType.Tablet; - if (deviceWidth > FormFactor.handset) return ScreenType.Handset; - return ScreenType.Watch; +class TUIKitScreenUtils { + static DeviceType? deviceType; + + static DeviceType getFormFactor([BuildContext? context]) { + if (deviceType != null) return deviceType!; + + if(context != null){ + double deviceWidth = MediaQuery.of(context).size.width; + double deviceHeight = MediaQuery.of(context).size.height; + + if (deviceWidth > FormFactor.desktop || deviceWidth > deviceHeight * 1.1) { + deviceType = DeviceType.Desktop; + } else if (deviceWidth > FormFactor.handset) { + deviceType = DeviceType.Mobile; + } + return deviceType ?? DeviceType.Mobile; + }else{ + return DeviceType.Mobile; + } + } + + static Widget getDeviceWidget({ + required Widget defaultWidget, + Widget? desktopWidget, + Widget? mobileWidget, + }) { + if (deviceType == DeviceType.Desktop) return desktopWidget ?? defaultWidget; + return mobileWidget ?? defaultWidget; } } diff --git a/lib/ui/utils/sound_record.dart b/lib/ui/utils/sound_record.dart index c04402e..78ec249 100644 --- a/lib/ui/utils/sound_record.dart +++ b/lib/ui/utils/sound_record.dart @@ -1,5 +1,5 @@ import 'dart:async'; - +import 'package:audioplayers/audioplayers.dart'; import 'package:flutter_plugin_record_plus/const/play_state.dart'; import 'package:flutter_plugin_record_plus/const/response.dart'; import 'package:flutter_plugin_record_plus/index.dart'; @@ -14,33 +14,37 @@ class SoundPlayer { static final FlutterPluginRecord _recorder = FlutterPluginRecord(); static SoundInterruptListener? _soundInterruptListener; static bool isInited = false; + static final AudioPlayer _audioPlayer = AudioPlayer(); static initSoundPlayer() { if (!isInited) { _recorder.init(); + AudioPlayer.global.setGlobalAudioContext(const AudioContext()); isInited = true; } } - static play({required String url}) { - _recorder.stopPlay(); + static Future play({required String url}) async { + _audioPlayer.stop(); if (_soundInterruptListener != null) { _soundInterruptListener!(); } - _recorder.playByPath(url, 'url'); + await _audioPlayer.play(UrlSource(url)); } static stop() { - _recorder.stopPlay(); + _audioPlayer.stop(); } static dispose() { + _audioPlayer.dispose(); _recorder.dispose(); } - static StreamSubscription playStateListener( - {required PlayStateListener listener}) => - _recorder.responsePlayStateController.listen(listener); + static StreamSubscription playStateListener( + {required void Function(PlayerState)? listener}) => + _audioPlayer.onPlayerStateChanged.listen(listener); + static setSoundInterruptListener(SoundInterruptListener listener) { _soundInterruptListener = listener; diff --git a/lib/ui/utils/time_ago.dart b/lib/ui/utils/time_ago.dart index dbb2ac7..95cdbcc 100644 --- a/lib/ui/utils/time_ago.dart +++ b/lib/ui/utils/time_ago.dart @@ -11,18 +11,6 @@ class TimeAgo { ]; } - List daysMap() { - return [ - TIM_t("星期天"), - TIM_t("星期一"), - TIM_t("星期二"), - TIM_t("星期三"), - TIM_t("星期四"), - TIM_t("星期五"), - TIM_t("星期六") - ]; - } - List weekdayMap() { return [ '', @@ -36,7 +24,7 @@ class TimeAgo { ]; } - String getYearMounthDate(DateTime dateTime) { + String getYearMonthDate(DateTime dateTime) { String month = dateTime.month.toString(); String date = dateTime.day.toString(); return dateTime.year.toString() + @@ -48,7 +36,7 @@ class TimeAgo { date; } - String getMounthDate(DateTime dateTime) { + String getMonthDate(DateTime dateTime) { String month = dateTime.month.toString(); String date = dateTime.day.toString(); return (month.length == 1 ? '0' : '') + @@ -59,39 +47,33 @@ class TimeAgo { } String getTimeStringForChat(int timeStamp) { - final formatedTimeStamp = timeStamp * 1000; - final DateTime date = - DateTime.fromMillisecondsSinceEpoch(formatedTimeStamp); - final currentTimeStamp = DateTime.now().millisecondsSinceEpoch; + final DateTime date = DateTime.fromMillisecondsSinceEpoch(timeStamp * 1000); final Duration duration = DateTime.now().difference(date); + final int diffDays = duration.inDays + + (duration.inMinutes > + DateTime.now() + .difference(DateTime(DateTime.now().year, + DateTime.now().month, DateTime.now().day)) + .inMinutes + ? 1 + : 0); + final int diffMinutes = duration.inMinutes; - int diffDays = duration.inDays; - final diffMinutes = duration.inMinutes; var res; // 一个礼拜之内 if (diffDays > 0 && diffDays < 7) { - final String formatTodayZero = - DateFormat('yyyyMMdd').format(DateTime.now()); - final todayZero = DateTime.parse(formatTodayZero).millisecondsSinceEpoch; - final todayDiff = currentTimeStamp - todayZero; - - final isTwoDay = todayDiff < (diffMinutes - diffDays * 1440) * 60000; - if (isTwoDay) { - diffDays = diffDays + 1; - } - if (diffDays <= 2) { res = dayMap()[diffDays - 1]; } else { - res = daysMap()[date.weekday]; + res = weekdayMap()[date.weekday]; } } else if (diffDays >= 7) { //当年内 if (date.year == DateTime.now().year) { - res = getMounthDate(date); + res = getMonthDate(date); } else { - res = getYearMounthDate(date); + res = getYearMonthDate(date); } } else { if (diffMinutes > 1) { @@ -99,9 +81,9 @@ class TimeAgo { final String option2 = diffMinutes.toString(); res = TIM_t_para("{{option2}} 分钟前", "$option2 分钟前")(option2: option2); } else { - final prefix = date.hour > 12 ? TIM_t("下午") : TIM_t("上午"); - final timeStr = DateFormat('hh:mm').format(date); - res = "$prefix $timeStr"; + res = + "${date.hour}:${date.minute < 10 ? date.minute.toString() + "0" : date.minute}"; + // res = "$prefix $timeStr"; } } else { res = TIM_t("现在"); @@ -115,26 +97,27 @@ class TimeAgo { var nowTime = DateTime.now(); nowTime = DateTime(nowTime.year, nowTime.month, nowTime.day); var ftime = DateTime.fromMillisecondsSinceEpoch(timeStamp * 1000); - var preFix = ftime.hour >= 12 ? TIM_t("下午") : TIM_t("上午"); - final timeStr = DateFormat('hh:mm').format(ftime); - // 一年外 年月日 + 上/下午 + 时间 (12小时制) + // var preFix = ftime.hour >= 12 ? TIM_t("下午") : TIM_t("上午"); + final timeStr = + DateFormat('HH:mm').format(ftime); // Use 'HH:mm' for 24-hour format + // 一年外 年月日 + 时间 (24小时制) if (nowTime.year != ftime.year) { - return '${DateFormat('yyyy-MM-dd').format(ftime)} $preFix $timeStr'; + return '${DateFormat('yyyy-MM-dd').format(ftime)} $timeStr'; } - // 一年内一周外 月日 + 上/下午 + 时间 (12小时制) + // 一年内一周外 月日 + 时间 (24小时制) if (ftime.isBefore(nowTime.subtract(const Duration(days: 6)))) { - return '${DateFormat('MM-dd').format(ftime)} $preFix $timeStr'; + return '${DateFormat('MM-dd').format(ftime)} $timeStr'; } - // 一周内一天外 星期 + 上/下午 + 时间 (12小时制) + // 一周内一天外 星期 + 时间 (24小时制) if (ftime.isBefore(nowTime.subtract(const Duration(days: 1)))) { - return '${weekdayMap()[ftime.weekday]} $preFix $timeStr'; + return '${weekdayMap()[ftime.weekday]} $timeStr'; } - // 昨日 昨天 + 上/下午 + 时间 (12小时制) + // 昨日 昨天 + 时间 (24小时制) if (nowTime.day != ftime.day) { - String option2 = '$preFix $timeStr'; + String option2 = timeStr; return TIM_t_para("昨天 {{option2}}", "昨天 $option2")(option2: option2); } - // 同年月日 上/下午 + 时间 (12小时制) - return '$preFix $timeStr'; + // 同年月日 时间 (24小时制) + return timeStr; } } diff --git a/lib/ui/views/TIMUIKitAddFriend/tim_uikit_add_friend.dart b/lib/ui/views/TIMUIKitAddFriend/tim_uikit_add_friend.dart index 3ba37fc..aef87b5 100644 --- a/lib/ui/views/TIMUIKitAddFriend/tim_uikit_add_friend.dart +++ b/lib/ui/views/TIMUIKitAddFriend/tim_uikit_add_friend.dart @@ -1,5 +1,8 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; +import 'package:tencent_cloud_chat_uikit/data_services/core/tim_uikit_wide_modal_operation_key.dart'; +import 'package:tencent_cloud_chat_uikit/ui/utils/screen_utils.dart'; +import 'package:tencent_cloud_chat_uikit/ui/widgets/wide_popup.dart'; import 'package:tencent_im_base/tencent_im_base.dart'; import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_base.dart'; import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_state.dart'; @@ -20,11 +23,15 @@ class TIMUIKitAddFriend extends StatefulWidget { /// The life cycle hooks for adding friends and contact business logic final AddFriendLifeCycle? lifeCycle; + + final VoidCallback? closeFunc; + const TIMUIKitAddFriend( {Key? key, this.isShowDefaultGroup = false, this.lifeCycle, - required this.onTapAlreadyFriendsItem}) + required this.onTapAlreadyFriendsItem, + this.closeFunc}) : super(key: key); @override @@ -45,6 +52,9 @@ class _TIMUIKitAddFriendState extends TIMUIKitState { Widget _searchResultItemBuilder( V2TimUserFullInfo friendInfo, TUITheme theme) { + final isDesktopScreen = + TUIKitScreenUtils.getFormFactor(context) == DeviceType.Desktop; + final faceUrl = friendInfo.faceUrl ?? ""; final userID = friendInfo.userID ?? ""; final String showName = @@ -74,14 +84,32 @@ class _TIMUIKitAddFriendState extends TIMUIKitState { return; } - Navigator.pushReplacement( - context, - MaterialPageRoute( - builder: (context) => SendApplication( - lifeCycle: widget.lifeCycle, - isShowDefaultGroup: widget.isShowDefaultGroup ?? false, - friendInfo: friendInfo, - model: _selfInfoViewModel))); + if (isDesktopScreen) { + if (widget.closeFunc != null) { + widget.closeFunc!(); + } + TUIKitWidePopup.showPopupWindow( + operationKey: TUIKitWideModalOperationKey.addFriend, + context: context, + width: MediaQuery.of(context).size.width * 0.3, + height: MediaQuery.of(context).size.width * 0.4, + title: TIM_t("添加好友"), + child: (closeFuncSendApplication) => SendApplication( + lifeCycle: widget.lifeCycle, + isShowDefaultGroup: widget.isShowDefaultGroup ?? false, + friendInfo: friendInfo, + model: _selfInfoViewModel), + ); + } else { + Navigator.pushReplacement( + context, + MaterialPageRoute( + builder: (context) => SendApplication( + lifeCycle: widget.lifeCycle, + isShowDefaultGroup: widget.isShowDefaultGroup ?? false, + friendInfo: friendInfo, + model: _selfInfoViewModel))); + } }, child: Container( padding: const EdgeInsets.symmetric(vertical: 12), @@ -89,9 +117,9 @@ class _TIMUIKitAddFriendState extends TIMUIKitState { // crossAxisAlignment: CrossAxisAlignment.start, children: [ Container( - width: 48, - height: 48, - margin: const EdgeInsets.only(right: 12), + width: isDesktopScreen ? 38 : 48, + height: isDesktopScreen ? 38 : 48, + margin: const EdgeInsets.only(right: 16), child: Avatar(faceUrl: faceUrl, showName: showName), ), Column( @@ -99,7 +127,9 @@ class _TIMUIKitAddFriendState extends TIMUIKitState { children: [ Text( showName, - style: TextStyle(color: theme.darkTextColor, fontSize: 18), + style: TextStyle( + color: theme.darkTextColor, + fontSize: isDesktopScreen ? 16 : 18), ), const SizedBox( height: 4, diff --git a/lib/ui/views/TIMUIKitAddFriend/tim_uikit_send_application.dart b/lib/ui/views/TIMUIKitAddFriend/tim_uikit_send_application.dart index 883dbc7..6b0515b 100644 --- a/lib/ui/views/TIMUIKitAddFriend/tim_uikit_send_application.dart +++ b/lib/ui/views/TIMUIKitAddFriend/tim_uikit_send_application.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; -import 'package:tencent_im_base/tencent_im_base.dart'; +import 'package:tencent_cloud_chat_uikit/tencent_cloud_chat_uikit.dart'; +import 'package:tencent_cloud_chat_uikit/ui/utils/screen_utils.dart'; import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_state.dart'; import 'package:tencent_cloud_chat_uikit/business_logic/life_cycle/add_friend_life_cycle.dart'; import 'package:tencent_cloud_chat_uikit/business_logic/view_models/tui_self_info_view_model.dart'; @@ -53,26 +54,9 @@ class _SendApplicationState extends TIMUIKitState { : userID) ?? ""; final option2 = widget.friendInfo.selfSignature ?? ""; - return Scaffold( - appBar: AppBar( - title: Text( - TIM_t("添加好友"), - style: TextStyle(color: theme.white, fontSize: 17), - ), - shadowColor: theme.white, - flexibleSpace: Container( - decoration: BoxDecoration( - gradient: LinearGradient(colors: [ - theme.lightPrimaryColor ?? CommonColor.lightPrimaryColor, - theme.primaryColor ?? CommonColor.primaryColor - ]), - ), - ), - iconTheme: IconThemeData( - color: theme.white, - ), - ), - body: SingleChildScrollView( + + Widget sendApplicationBody(){ + return SingleChildScrollView( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -95,7 +79,7 @@ class _SendApplicationState extends TIMUIKitState { Text( showName, style: - TextStyle(color: theme.darkTextColor, fontSize: 18), + TextStyle(color: theme.darkTextColor, fontSize: 18), ), const SizedBox( height: 4, @@ -103,16 +87,16 @@ class _SendApplicationState extends TIMUIKitState { Text( "ID: $userID", style: - TextStyle(fontSize: 13, color: theme.weakTextColor), + TextStyle(fontSize: 13, color: theme.weakTextColor), ), const SizedBox( height: 4, ), - Text( + if(TencentUtils.checkString(option2) != null)Text( TIM_t_para("个性签名: {{option2}}", "个性签名: $option2")( option2: option2), style: - TextStyle(fontSize: 13, color: theme.weakTextColor), + TextStyle(fontSize: 13, color: theme.weakTextColor), ), ], ) @@ -184,19 +168,19 @@ class _SendApplicationState extends TIMUIKitState { Container( color: theme.white, padding: - const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + const EdgeInsets.symmetric(horizontal: 16, vertical: 12), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( TIM_t("分组"), style: - TextStyle(color: theme.darkTextColor, fontSize: 16), + TextStyle(color: theme.darkTextColor, fontSize: 16), ), Text( TIM_t("我的好友"), style: - TextStyle(color: theme.darkTextColor, fontSize: 16), + TextStyle(color: theme.darkTextColor, fontSize: 16), ) ], ), @@ -213,7 +197,7 @@ class _SendApplicationState extends TIMUIKitState { if (widget.lifeCycle?.shouldAddFriend != null && await widget.lifeCycle!.shouldAddFriend(userID, remark, - friendGroup, addWording, context) == + friendGroup, addWording, context) == false) { return; } @@ -246,7 +230,29 @@ class _SendApplicationState extends TIMUIKitState { ) ], ), + ); + } + + return TUIKitScreenUtils.getDeviceWidget( + desktopWidget: Container( + padding: const EdgeInsets.only(top: 10), + color: theme.weakBackgroundColor, + child: sendApplicationBody(), ), - ); + defaultWidget: Scaffold( + appBar: AppBar( + title: Text( + TIM_t("添加好友"), + style: TextStyle(color: theme.appbarTextColor, fontSize: 17), + ), + shadowColor: theme.white, + backgroundColor: theme.appbarBgColor ?? + theme.primaryColor, + iconTheme: IconThemeData( + color: theme.appbarTextColor, + ), + ), + body: sendApplicationBody(), + )); } } diff --git a/lib/ui/views/TIMUIKitAddGroup/tim_uikit_add_group.dart b/lib/ui/views/TIMUIKitAddGroup/tim_uikit_add_group.dart index 4a96136..23d4798 100644 --- a/lib/ui/views/TIMUIKitAddGroup/tim_uikit_add_group.dart +++ b/lib/ui/views/TIMUIKitAddGroup/tim_uikit_add_group.dart @@ -1,4 +1,7 @@ import 'package:flutter/material.dart'; +import 'package:tencent_cloud_chat_uikit/data_services/core/tim_uikit_wide_modal_operation_key.dart'; +import 'package:tencent_cloud_chat_uikit/ui/utils/screen_utils.dart'; +import 'package:tencent_cloud_chat_uikit/ui/widgets/wide_popup.dart'; import 'package:tencent_im_base/tencent_im_base.dart'; import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_state.dart'; import 'package:tencent_cloud_chat_uikit/business_logic/life_cycle/add_group_life_cycle.dart'; @@ -19,8 +22,10 @@ class TIMUIKitAddGroup extends StatefulWidget { final Function(String groupID, V2TimConversation conversation) onTapExistGroup; + final VoidCallback? closeFunc; + const TIMUIKitAddGroup( - {Key? key, this.lifeCycle, required this.onTapExistGroup}) + {Key? key, this.lifeCycle, required this.onTapExistGroup, this.closeFunc}) : super(key: key); @override @@ -67,6 +72,8 @@ class _TIMUIKitAddGroupState extends TIMUIKitState { final groupID = groupInfo.groupID; final showName = groupInfo.groupName ?? groupID; final groupType = _getGroupType(groupInfo.groupType); + final isDesktopScreen = + TUIKitScreenUtils.getFormFactor(context) == DeviceType.Desktop; return InkWell( onTap: () async { final V2TimConversation? groupConversation = @@ -76,16 +83,38 @@ class _TIMUIKitAddGroupState extends TIMUIKitState { type: TIMCallbackType.INFO, infoRecommendText: TIM_t("您已是群成员"), infoCode: 6660202)); + if (widget.closeFunc != null) { + widget.closeFunc!(); + } widget.onTapExistGroup(groupID, groupConversation); return; } - Navigator.pushReplacement( - context, - MaterialPageRoute( - builder: (context) => SendJoinGroupApplication( - lifeCycle: widget.lifeCycle, - groupInfo: groupInfo, - ))); + + if(isDesktopScreen){ + if (widget.closeFunc != null) { + widget.closeFunc!(); + } + TUIKitWidePopup.showPopupWindow( + operationKey: TUIKitWideModalOperationKey.addGroup, + context: context, + width: MediaQuery.of(context).size.width * 0.3, + height: MediaQuery.of(context).size.width * 0.4, + title: TIM_t("添加群聊"), + child: (closeFuncSendApplication) => SendJoinGroupApplication( + lifeCycle: widget.lifeCycle, + groupInfo: groupInfo, + ), + ); + }else{ + Navigator.pushReplacement( + context, + MaterialPageRoute( + builder: (context) => SendJoinGroupApplication( + lifeCycle: widget.lifeCycle, + groupInfo: groupInfo, + ))); + } + }, child: Container( padding: const EdgeInsets.symmetric(vertical: 12), @@ -93,9 +122,9 @@ class _TIMUIKitAddGroupState extends TIMUIKitState { // crossAxisAlignment: CrossAxisAlignment.start, children: [ Container( - width: 48, - height: 48, - margin: const EdgeInsets.only(right: 12), + width: isDesktopScreen ? 38 : 48, + height: isDesktopScreen ? 38 : 48, + margin: const EdgeInsets.only(right: 16), child: Avatar(faceUrl: faceUrl, showName: showName), ), Column( @@ -103,7 +132,7 @@ class _TIMUIKitAddGroupState extends TIMUIKitState { children: [ Text( showName, - style: const TextStyle(fontSize: 18), + style: TextStyle(fontSize: isDesktopScreen ? 16 : 18), ), Text( "ID: $groupID", diff --git a/lib/ui/views/TIMUIKitAddGroup/tim_uikit_send_application.dart b/lib/ui/views/TIMUIKitAddGroup/tim_uikit_send_application.dart index 178c192..1c64605 100644 --- a/lib/ui/views/TIMUIKitAddGroup/tim_uikit_send_application.dart +++ b/lib/ui/views/TIMUIKitAddGroup/tim_uikit_send_application.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:tencent_cloud_chat_uikit/ui/utils/screen_utils.dart'; import 'package:tencent_im_base/tencent_im_base.dart'; import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_state.dart'; import 'package:tencent_cloud_chat_uikit/business_logic/life_cycle/add_group_life_cycle.dart'; @@ -74,26 +75,9 @@ class _SendJoinGroupApplicationState final groupID = widget.groupInfo.groupID; final showName = widget.groupInfo.groupName ?? groupID; final option1 = _getGroupType(widget.groupInfo.groupType); - return Scaffold( - appBar: AppBar( - title: Text( - TIM_t("进群申请"), - style: TextStyle(color: theme.white, fontSize: 17), - ), - shadowColor: theme.white, - flexibleSpace: Container( - decoration: BoxDecoration( - gradient: LinearGradient(colors: [ - theme.lightPrimaryColor ?? CommonColor.lightPrimaryColor, - theme.primaryColor ?? CommonColor.primaryColor - ]), - ), - ), - iconTheme: IconThemeData( - color: theme.white, - ), - ), - body: SingleChildScrollView( + + Widget sendGroupApplicationBody(){ + return SingleChildScrollView( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -116,7 +100,7 @@ class _SendJoinGroupApplicationState Text( showName, style: - TextStyle(color: theme.darkTextColor, fontSize: 18), + TextStyle(color: theme.darkTextColor, fontSize: 18), ), const SizedBox( height: 4, @@ -124,7 +108,7 @@ class _SendJoinGroupApplicationState Text( "ID: $groupID", style: - TextStyle(fontSize: 13, color: theme.weakTextColor), + TextStyle(fontSize: 13, color: theme.weakTextColor), ), const SizedBox( height: 4, @@ -133,7 +117,7 @@ class _SendJoinGroupApplicationState TIM_t_para("群类型: {{option1}}", "群类型: $option1")( option1: option1), style: - TextStyle(fontSize: 12, color: theme.weakTextColor), + TextStyle(fontSize: 12, color: theme.weakTextColor), ), ], ) @@ -181,7 +165,26 @@ class _SendJoinGroupApplicationState ) ], ), + ); + } + + + return TUIKitScreenUtils.getDeviceWidget( + desktopWidget: sendGroupApplicationBody(), + defaultWidget: Scaffold( + appBar: AppBar( + title: Text( + TIM_t("进群申请"), + style: TextStyle(color: theme.appbarTextColor, fontSize: 17), + ), + shadowColor: theme.white, + backgroundColor: theme.appbarBgColor ?? + theme.primaryColor, + iconTheme: IconThemeData( + color: theme.appbarTextColor, + ), ), - ); + body: sendGroupApplicationBody(), + )); } } diff --git a/lib/ui/views/TIMUIKitBlackList/tim_uikit_black_list.dart b/lib/ui/views/TIMUIKitBlackList/tim_uikit_black_list.dart index bb97ccd..8c21604 100644 --- a/lib/ui/views/TIMUIKitBlackList/tim_uikit_black_list.dart +++ b/lib/ui/views/TIMUIKitBlackList/tim_uikit_black_list.dart @@ -7,6 +7,7 @@ import 'package:tencent_cloud_chat_uikit/business_logic/view_models/tui_friendsh import 'package:tencent_cloud_chat_uikit/data_services/services_locatar.dart'; import 'package:tencent_cloud_chat_uikit/tencent_cloud_chat_uikit.dart'; +import 'package:tencent_cloud_chat_uikit/ui/utils/screen_utils.dart'; import 'package:tencent_cloud_chat_uikit/ui/widgets/avatar.dart'; import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_base.dart'; @@ -50,35 +51,33 @@ class _TIMUIKitBlackListState extends TIMUIKitState { final theme = Provider.of(context).theme; final showName = _getShowName(friendInfo); final faceUrl = friendInfo.userProfile?.faceUrl ?? ""; - return Slidable( - endActionPane: ActionPane(motion: const DrawerMotion(), children: [ - SlidableAction( - onPressed: (context) async { - await _friendshipViewModel - .deleteFromBlockList([friendInfo.userID]); - }, - backgroundColor: theme.cautionColor ?? CommonColor.cautionColor, - foregroundColor: theme.white, - label: TIM_t("删除"), - autoClose: true, - ) - ]), - child: GestureDetector( + final isDesktopScreen = + TUIKitScreenUtils.getFormFactor(context) == DeviceType.Desktop; + + Widget itemWidget() { + return Material( + color: theme.wideBackgroundColor, + child: InkWell( onTap: () { if (widget.onTapItem != null) { widget.onTapItem!(friendInfo); } }, child: Container( - padding: const EdgeInsets.only(top: 10, left: 16), + padding: const EdgeInsets.only(top: 10, left: 16, right: 16), + decoration: BoxDecoration( + border: Border( + bottom: BorderSide( + color: theme.weakDividerColor ?? + CommonColor.weakDividerColor))), child: Row( children: [ Container( padding: const EdgeInsets.only(bottom: 12), margin: const EdgeInsets.only(right: 12), child: SizedBox( - height: 40, - width: 40, + height: isDesktopScreen ? 30 : 40, + width: isDesktopScreen ? 30 : 40, child: Avatar(faceUrl: faceUrl, showName: showName), ), ), @@ -86,19 +85,45 @@ class _TIMUIKitBlackListState extends TIMUIKitState { child: Container( alignment: Alignment.centerLeft, padding: const EdgeInsets.only(top: 10, bottom: 20), - decoration: BoxDecoration( - border: Border( - bottom: BorderSide( - color: theme.weakDividerColor ?? - CommonColor.weakDividerColor))), child: Text( showName, - style: TextStyle(color: theme.black, fontSize: 18), + style: TextStyle( + color: theme.black, fontSize: isDesktopScreen ? 14 : 18), ), - )) + )), + if (isDesktopScreen) + OutlinedButton( + onPressed: () { + _friendshipViewModel + .deleteFromBlockList([friendInfo.userID]); + }, + child: Text( + TIM_t("移出黑名单"), + style: TextStyle(color: theme.primaryColor), + )) ], ), ), + ), + ); + } + + return TUIKitScreenUtils.getDeviceWidget( + desktopWidget: itemWidget(), + defaultWidget: Slidable( + endActionPane: ActionPane(motion: const DrawerMotion(), children: [ + SlidableAction( + onPressed: (context) async { + await _friendshipViewModel + .deleteFromBlockList([friendInfo.userID]); + }, + backgroundColor: theme.cautionColor ?? CommonColor.cautionColor, + foregroundColor: theme.white, + label: TIM_t("删除"), + autoClose: true, + ) + ]), + child: itemWidget(), )); } @@ -106,10 +131,6 @@ class _TIMUIKitBlackListState extends TIMUIKitState { return widget.itemBuilder ?? _itemBuilder; } - @override - void initState() { - super.initState(); - } @override Widget tuiBuild(BuildContext context, TUIKitBuildValue value) { diff --git a/lib/ui/views/TIMUIKitChat/TIMUIKItMessageList/TIMUIKitTongue/tim_uikit_chat_history_message_list_tongue.dart b/lib/ui/views/TIMUIKitChat/TIMUIKItMessageList/TIMUIKitTongue/tim_uikit_chat_history_message_list_tongue.dart index 56babcc..6b876bc 100644 --- a/lib/ui/views/TIMUIKitChat/TIMUIKItMessageList/TIMUIKitTongue/tim_uikit_chat_history_message_list_tongue.dart +++ b/lib/ui/views/TIMUIKitChat/TIMUIKItMessageList/TIMUIKitTongue/tim_uikit_chat_history_message_list_tongue.dart @@ -8,8 +8,8 @@ enum MessageListTongueType { none, toLatest, showUnread, - showPrevious, atMe, + showPrevious, atAll, } @@ -36,9 +36,9 @@ class TIMUIKitHistoryMessageListTongue extends TIMUIKitStatelessWidget { TIMUIKitHistoryMessageListTongue({ Key? key, - required this.previousCount, required this.valueType, required this.onClick, + required this.previousCount, required this.unreadCount, this.tongueItemBuilder, this.atNum = "", diff --git a/lib/ui/views/TIMUIKitChat/TIMUIKItMessageList/TIMUIKitTongue/tim_uikit_chat_history_message_list_tongue_container.dart b/lib/ui/views/TIMUIKitChat/TIMUIKItMessageList/TIMUIKitTongue/tim_uikit_chat_history_message_list_tongue_container.dart index e03eea7..321c8f9 100644 --- a/lib/ui/views/TIMUIKitChat/TIMUIKItMessageList/TIMUIKitTongue/tim_uikit_chat_history_message_list_tongue_container.dart +++ b/lib/ui/views/TIMUIKitChat/TIMUIKItMessageList/TIMUIKitTongue/tim_uikit_chat_history_message_list_tongue_container.dart @@ -1,7 +1,7 @@ import 'package:flutter/cupertino.dart'; import 'package:provider/provider.dart'; import 'package:scroll_to_index/scroll_to_index.dart'; -import 'package:tencent_cloud_chat_uikit/tencent_cloud_chat_uikit.dart'; +import 'package:tencent_cloud_chat_uikit/ui/utils/common_utils.dart'; import 'package:tencent_im_base/tencent_im_base.dart'; import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_state.dart'; import 'package:tencent_cloud_chat_uikit/business_logic/separate_models/tui_chat_separate_view_model.dart'; @@ -61,20 +61,22 @@ class _TIMUIKitHistoryMessageListTongueContainerState final screenHeight = MediaQuery.of(context).size.height; final offset = widget.scrollController.offset; final conversationUnreadCount = widget.model.getConversationUnreadCount(); - if (offset <= 0.0 && - conversationUnreadCount != 0) { + if (offset <= 0.0 && conversationUnreadCount != 0) { widget.model.showLatestUnread(); } if (widget.scrollController.offset <= widget.scrollController.position.minScrollExtent && - !widget.scrollController.position.outOfRange && !widget.model.haveMoreLatestData) { + !widget.scrollController.position.outOfRange && + !widget.model.haveMoreLatestData) { changePositionState(HistoryMessagePosition.bottom); } else if (widget.scrollController.offset <= screenHeight * 1.6 && widget.scrollController.offset > 0 && - !widget.scrollController.position.outOfRange && !widget.model.haveMoreLatestData) { + !widget.scrollController.position.outOfRange && + !widget.model.haveMoreLatestData) { changePositionState(HistoryMessagePosition.inTwoScreen); } else if (widget.scrollController.offset > screenHeight * 1.6 && - !widget.scrollController.position.outOfRange && !widget.model.haveMoreLatestData) { + !widget.scrollController.position.outOfRange && + !widget.model.haveMoreLatestData) { changePositionState(HistoryMessagePosition.awayTwoScreen); } } @@ -85,9 +87,8 @@ class _TIMUIKitHistoryMessageListTongueContainerState MessageListTongueType _getTongueValueType( List? groupAtInfoList) { - - if(globalModel.getMessageListPosition(widget.model.conversationID) == - HistoryMessagePosition.notShowLatest){ + if (globalModel.getMessageListPosition(widget.model.conversationID) == + HistoryMessagePosition.notShowLatest) { return MessageListTongueType.none; } if (groupAtInfoList != null && @@ -152,22 +153,24 @@ class _TIMUIKitHistoryMessageListTongueContainerState } else { widget.scrollToIndexBySeq(groupAtInfoList!.removeAt(0)!.seq); } - } else if ((widget.conversation.unreadCount ?? 0) > 20 && !isClickShowPrevious) { - try{ + } else if ((widget.conversation.unreadCount ?? 0) > 20 && + !isClickShowPrevious) { + try { isClickShowPrevious = true; final String? lastSeqString = widget.conversation.lastMessage?.seq; final int? lastSeq = - TencentUtils.checkString(lastSeqString) != null - ? int.parse(lastSeqString!) - : null; + TencentUtils.checkString(lastSeqString) != null + ? int.parse(lastSeqString!) + : null; final int? previousCount = widget.conversation.unreadCount; if (lastSeq != null && previousCount != null) { final targetSeq = lastSeq - previousCount; - await widget.model.loadListForSpecificMessage(seq: targetSeq); + await widget.model + .loadListForSpecificMessage(seq: targetSeq); // widget.scrollToIndexBySeq((targetSeq + 1).toString()); } - }catch(e){ + } catch (e) { // TODO: 这里后续加个弹窗提示客户,找消息失败了 } // widget.model.loadListForSpecificMessage(seq: count); diff --git a/lib/ui/views/TIMUIKitChat/TIMUIKItMessageList/TIMUIKitTongue/tim_uikit_tongue_item.dart b/lib/ui/views/TIMUIKitChat/TIMUIKItMessageList/TIMUIKitTongue/tim_uikit_tongue_item.dart index 6c83348..5f0b663 100644 --- a/lib/ui/views/TIMUIKitChat/TIMUIKItMessageList/TIMUIKitTongue/tim_uikit_tongue_item.dart +++ b/lib/ui/views/TIMUIKitChat/TIMUIKItMessageList/TIMUIKitTongue/tim_uikit_tongue_item.dart @@ -21,7 +21,7 @@ class TIMUIKitTongueItem extends TIMUIKitStatelessWidget { final int previousCount; - TIMUIKitTongueItem( { + TIMUIKitTongueItem({ Key? key, required this.onClick, required this.valueType, diff --git a/lib/ui/views/TIMUIKitChat/TIMUIKItMessageList/tim_uikit_chat_history_message_list.dart b/lib/ui/views/TIMUIKitChat/TIMUIKItMessageList/tim_uikit_chat_history_message_list.dart index f5f0dbb..b9239b5 100644 --- a/lib/ui/views/TIMUIKitChat/TIMUIKItMessageList/tim_uikit_chat_history_message_list.dart +++ b/lib/ui/views/TIMUIKitChat/TIMUIKItMessageList/tim_uikit_chat_history_message_list.dart @@ -8,17 +8,14 @@ import 'package:scroll_to_index/scroll_to_index.dart'; import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_base.dart'; import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_state.dart'; import 'package:tencent_im_base/tencent_im_base.dart'; - import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_statelesswidget.dart'; import 'package:tencent_cloud_chat_uikit/business_logic/separate_models/tui_chat_separate_view_model.dart'; import 'package:tencent_cloud_chat_uikit/business_logic/view_models/tui_chat_global_model.dart'; - // ignore: unused_import import 'package:tencent_cloud_chat_uikit/ui/utils/optimize_utils.dart'; import 'package:tencent_cloud_chat_uikit/ui/views/TIMUIKitChat/TIMUIKItMessageList/tim_uikit_chat_history_message_list_config.dart'; import 'package:tencent_cloud_chat_uikit/ui/views/TIMUIKitChat/TIMUIKItMessageList/utils.dart'; import 'package:tencent_cloud_chat_uikit/ui/widgets/keepalive_wrapper.dart'; - import 'TIMUIKitTongue/tim_uikit_chat_history_message_list_tongue.dart'; import 'TIMUIKitTongue/tim_uikit_chat_history_message_list_tongue_container.dart'; @@ -76,7 +73,8 @@ class TIMUIKitHistoryMessageList extends StatefulWidget { final V2TimMessage? initFindingMsg; /// use for load more message - final Future Function(String?, LoadDirection direction, [int?]) onLoadMore; + final Future Function(String?, LoadDirection direction, [int?]) + onLoadMore; /// configuration for list view final TIMUIKitHistoryMessageListConfig? mainHistoryListConfig; @@ -99,7 +97,7 @@ class TIMUIKitHistoryMessageList extends StatefulWidget { this.initFindingMsg, this.isAllowScroll = true, this.mainHistoryListConfig, - required this.conversation}) + required this.conversation}) : super(key: key); @override @@ -272,8 +270,8 @@ class _TIMUIKitHistoryMessageListState } else { if (widget.model.haveMoreData) { findingSeq = targetSeq; - widget.onLoadMore( - _getMessageId(widget.messageList.length - 1), LoadDirection.previous, singleLoadAmount); + widget.onLoadMore(_getMessageId(widget.messageList.length - 1), + LoadDirection.previous, singleLoadAmount); } else { showCantFindMsg(); } @@ -308,15 +306,15 @@ class _TIMUIKitHistoryMessageListState } } - List _getRecivedMessageList(int recivedMessageListCount) { - if (recivedMessageListCount == 0) { + List _getReceivedMessageList(int receivedMessageListCount) { + if (receivedMessageListCount == 0) { return []; } final haveTimeStampMessage = - widget.messageList[recivedMessageListCount]?.elemType == 11; + widget.messageList[receivedMessageListCount]?.elemType == 11; final endPoint = haveTimeStampMessage - ? recivedMessageListCount + 1 - : recivedMessageListCount; + ? receivedMessageListCount + 1 + : receivedMessageListCount; return widget.messageList.sublist(0, endPoint).reversed.toList(); } @@ -334,22 +332,23 @@ class _TIMUIKitHistoryMessageListState final globalModel = context.read(); final receivedNewMessageList = globalModel.receivedMessageListCount; final shouldShowUnreadMessage = receivedNewMessageList > 0; - final unreadMessageList = _getRecivedMessageList(receivedNewMessageList); + final unreadMessageList = _getReceivedMessageList(receivedNewMessageList); final readMessageList = messageList .sublist(unreadMessageList.length, messageList.length) .toList(); - final throteFunction = OptimizeUtils.multiThrottle((index, LoadDirection direction) async { + final throttleFunction = + OptimizeUtils.multiThrottle((index, LoadDirection direction) async { final msgID = TIMUIKitChatUtils.getMessageIDWithinIndex(readMessageList, index); await widget.onLoadMore(msgID, direction); }, 20); - final throteFunctionWithMsgID = OptimizeUtils.multiThrottle((msgID, LoadDirection direction) async { + final throttleFunctionWithMsgID = + OptimizeUtils.multiThrottle((msgID, LoadDirection direction) async { await widget.onLoadMore(msgID, direction); }, 200); - if (findingMsg != null) { _onScrollToIndex(findingMsg!); } else if (findingSeq != "") { @@ -360,167 +359,187 @@ class _TIMUIKitHistoryMessageListState return "${message?.msgID} - ${message?.timestamp} - ${message?.seq} -${message?.id}"; } - return Container( - color: theme.chatBgColor, - child: Stack( - alignment: Alignment.topCenter, - children: [ - CustomScrollView( - center: shouldShowUnreadMessage ? centerKey : null, - key: widget.mainHistoryListConfig?.key, - primary: widget.mainHistoryListConfig?.primary, - physics: (widget.isAllowScroll == false) - ? const NeverScrollableScrollPhysics() - : widget.mainHistoryListConfig?.physics, - // padding: widget.mainHistoryListConfig?.padding ?? EdgeInsets.zero, - // itemExtent: widget.mainHistoryListConfig?.itemExtent, - // prototypeItem: widget.mainHistoryListConfig?.prototypeItem, - cacheExtent: widget.mainHistoryListConfig?.cacheExtent ?? 1500, - semanticChildCount: - widget.mainHistoryListConfig?.semanticChildCount, - dragStartBehavior: - widget.mainHistoryListConfig?.dragStartBehavior ?? - DragStartBehavior.start, - keyboardDismissBehavior: - widget.mainHistoryListConfig?.keyboardDismissBehavior ?? - ScrollViewKeyboardDismissBehavior.manual, - restorationId: widget.mainHistoryListConfig?.restorationId, - clipBehavior: - widget.mainHistoryListConfig?.clipBehavior ?? Clip.hardEdge, - reverse: true, - shrinkWrap: !shouldShowUnreadMessage, + return Stack( + alignment: Alignment.topCenter, + children: [ + Scrollbar( controller: _autoScrollController, - slivers: [ - SliverPadding( - padding: - widget.mainHistoryListConfig?.padding ?? EdgeInsets.zero, - sliver: SliverList( - delegate: SliverChildBuilderDelegate( - (BuildContext context, int index) { - final messageItem = unreadMessageList[index]; - if(index == unreadMessageList.length - 1 && widget.model.haveMoreLatestData == true){ - throteFunctionWithMsgID(messageItem?.msgID ?? "", LoadDirection.latest); - } - return AutoScrollTag( - controller: _autoScrollController, - index: -index, - key: ValueKey( - getMessageIdentifier(messageItem, index)), - highlightColor: Colors.black.withOpacity(0.1), - child: KeepAliveWrapper( - keepAlive: messageItem?.elemType == - MessageElemType.V2TIM_ELEM_TYPE_SOUND, - child: Container( - padding: const EdgeInsets.symmetric( - horizontal: 16), - child: - _getMessageItemBuilder(messageItem))), - ); - }, - childCount: unreadMessageList.length, - findChildIndexCallback: (Key key) { - final ValueKey valueKey = - key as ValueKey; - final String data = valueKey.value; - final int index = unreadMessageList.indexWhere( - (element) => - getMessageIdentifier(element, 0) == data); - return index != -1 ? index : null; - })), - ), - SliverPadding( - padding: EdgeInsets.zero, - key: centerKey, - ), - SliverPadding( - padding: - widget.mainHistoryListConfig?.padding ?? EdgeInsets.zero, - sliver: SliverList( - delegate: SliverChildBuilderDelegate( - (BuildContext context, int index) { - final messageItem = readMessageList[index]; - if (index == readMessageList.length - 1) { - if (widget.model.haveMoreData) { - throteFunction(index, LoadDirection.previous); - return Column( - children: [ - LoadingAnimationWidget.staggeredDotsWave( - color: theme.weakTextColor ?? Colors.grey, - size: 28, - ), - AutoScrollTag( - controller: _autoScrollController, - index: -index, - key: ValueKey(getMessageIdentifier( - messageItem, index)), - highlightColor: - Colors.black.withOpacity(0.1), - child: KeepAliveWrapper( + child: CustomScrollView( + center: shouldShowUnreadMessage ? centerKey : null, + key: widget.mainHistoryListConfig?.key, + primary: widget.mainHistoryListConfig?.primary, + physics: (widget.isAllowScroll == false) + ? const NeverScrollableScrollPhysics() + : widget.mainHistoryListConfig?.physics, + // padding: widget.mainHistoryListConfig?.padding ?? EdgeInsets.zero, + // itemExtent: widget.mainHistoryListConfig?.itemExtent, + // prototypeItem: widget.mainHistoryListConfig?.prototypeItem, + cacheExtent: widget.mainHistoryListConfig?.cacheExtent ?? 1500, + semanticChildCount: + widget.mainHistoryListConfig?.semanticChildCount, + dragStartBehavior: + widget.mainHistoryListConfig?.dragStartBehavior ?? + DragStartBehavior.start, + keyboardDismissBehavior: + widget.mainHistoryListConfig?.keyboardDismissBehavior ?? + ScrollViewKeyboardDismissBehavior.manual, + restorationId: widget.mainHistoryListConfig?.restorationId, + clipBehavior: + widget.mainHistoryListConfig?.clipBehavior ?? Clip.hardEdge, + reverse: true, + shrinkWrap: !shouldShowUnreadMessage, + controller: _autoScrollController, + slivers: [ + SliverPadding( + padding: + widget.mainHistoryListConfig?.padding ?? EdgeInsets.zero, + sliver: SliverList( + delegate: SliverChildBuilderDelegate( + (BuildContext context, int index) { + final messageItem = unreadMessageList[index]; + if (index == unreadMessageList.length - 1 && + widget.model.haveMoreLatestData == true) { + throttleFunctionWithMsgID( + messageItem?.msgID ?? "", + LoadDirection.latest); + } + return AutoScrollTag( + controller: _autoScrollController, + index: -index, + key: ValueKey( + getMessageIdentifier(messageItem, index)), + highlightColor: Colors.black.withOpacity(0.1), + child: KeepAliveWrapper( + keepAlive: messageItem?.elemType == + MessageElemType.V2TIM_ELEM_TYPE_SOUND, + child: Container( + padding: const EdgeInsets.symmetric( + horizontal: 16), + child: + _getMessageItemBuilder(messageItem))), + ); + }, + childCount: unreadMessageList.length, + findChildIndexCallback: (Key key) { + final ValueKey valueKey = + key as ValueKey; + final String data = valueKey.value; + final int index = unreadMessageList.indexWhere( + (element) => + getMessageIdentifier(element, 0) == data); + return index != -1 ? index : null; + })), + ), + SliverPadding( + padding: EdgeInsets.zero, + key: centerKey, + ), + SliverPadding( + padding: + widget.mainHistoryListConfig?.padding ?? EdgeInsets.zero, + sliver: Selector( + selector: (context, model) { + return model.haveMoreData; + }, + shouldRebuild: (previous, next) { + return previous != next; + }, + builder: (context, haveMoreData, child) { + return SliverList( + delegate: SliverChildBuilderDelegate( + (BuildContext context, int index) { + final messageItem = readMessageList[index]; + final isSelf = messageItem?.isSelf ?? true; + if (index == readMessageList.length - 1) { + if (haveMoreData) { + throttleFunction( + index, LoadDirection.previous); + return Column( + children: [ + LoadingAnimationWidget + .staggeredDotsWave( + color: theme.weakTextColor ?? + Colors.grey, + size: 28, + ), + AutoScrollTag( + controller: _autoScrollController, + index: -index, + key: ValueKey(getMessageIdentifier( + messageItem, index)), + highlightColor: + Colors.black.withOpacity(0.1), + child: KeepAliveWrapper( + keepAlive: messageItem + ?.elemType == + MessageElemType + .V2TIM_ELEM_TYPE_SOUND, + child: Container( + padding: const EdgeInsets + .symmetric( + horizontal: 16), + child: _getMessageItemBuilder( + messageItem))), + ), + ], + ); + } + } + if (index == 0 && + widget.model.haveMoreLatestData == true && + globalModel.receivedMessageListCount < 10) { + throttleFunction(index, LoadDirection.latest); + } + return AutoScrollTag( + controller: _autoScrollController, + index: -index, + key: ValueKey( + getMessageIdentifier(messageItem, index)), + highlightColor: Colors.black.withOpacity(0.1), + child: KeepAliveWrapper( keepAlive: messageItem?.elemType == MessageElemType.V2TIM_ELEM_TYPE_SOUND, child: Container( - padding: const EdgeInsets.symmetric( - horizontal: 16), + padding: EdgeInsets.only( + left: isSelf ? 0 : 16, + right: isSelf ? 16 : 0), child: _getMessageItemBuilder( - messageItem)), - ), - ), - ], - ); - } - } - if(index == 0 && widget.model.haveMoreLatestData == true && globalModel.receivedMessageListCount < 10){ - throteFunction(index, LoadDirection.latest); - } - return AutoScrollTag( - controller: _autoScrollController, - index: -index, - key: ValueKey( - getMessageIdentifier(messageItem, index)), - highlightColor: Colors.black.withOpacity(0.1), - child: KeepAliveWrapper( - keepAlive: messageItem?.elemType == - MessageElemType.V2TIM_ELEM_TYPE_SOUND, - child: Container( - padding: const EdgeInsets.symmetric( - horizontal: 16), - child: - _getMessageItemBuilder(messageItem))), - ); - }, - addAutomaticKeepAlives: true, - childCount: readMessageList.length, - findChildIndexCallback: (Key key) { - final ValueKey valueKey = - key as ValueKey; - final String data = valueKey.value; - final int index = readMessageList.indexWhere( - (element) => - getMessageIdentifier(element, 0) == data); - return index > -1 ? index : null; - } - )), - ), - ], - ), - TIMUIKitHistoryMessageListTongueContainer( - conversation: widget.conversation, - model: widget.model, - scrollController: _autoScrollController, - scrollToIndexBySeq: _onScrollToIndexBySeq, - groupAtInfoList: widget.groupAtInfoList, - tongueItemBuilder: widget.tongueItemBuilder, - ), - if (loadingPlace == LoadingPlace.top) - Positioned( - top: 8, - child: LoadingAnimationWidget.staggeredDotsWave( - color: theme.weakTextColor ?? Colors.grey, - size: 28, - ), + messageItem))), + ); + }, + childCount: readMessageList.length, + findChildIndexCallback: (Key key) { + final ValueKey valueKey = + key as ValueKey; + final String data = valueKey.value; + final int index = readMessageList.indexWhere( + (element) => + getMessageIdentifier(element, 0) == + data); + return index > -1 ? index : null; + })); + }, + ), + ), + ], + )), + TIMUIKitHistoryMessageListTongueContainer( + conversation: widget.conversation, + model: widget.model, + scrollController: _autoScrollController, + scrollToIndexBySeq: _onScrollToIndexBySeq, + groupAtInfoList: widget.groupAtInfoList, + tongueItemBuilder: widget.tongueItemBuilder, + ), + if (loadingPlace == LoadingPlace.top) + Positioned( + top: 8, + child: LoadingAnimationWidget.staggeredDotsWave( + color: theme.weakTextColor ?? Colors.grey, + size: 28, ), - ], - ), + ), + ], ); } } diff --git a/lib/ui/views/TIMUIKitChat/TIMUIKItMessageList/tim_uikit_chat_history_message_list_item.dart b/lib/ui/views/TIMUIKitChat/TIMUIKItMessageList/tim_uikit_chat_history_message_list_item.dart index d626b6f..6383cab 100644 --- a/lib/ui/views/TIMUIKitChat/TIMUIKItMessageList/tim_uikit_chat_history_message_list_item.dart +++ b/lib/ui/views/TIMUIKitChat/TIMUIKItMessageList/tim_uikit_chat_history_message_list_item.dart @@ -1,20 +1,22 @@ -// ignore_for_file: non_constant_identifier_names - import 'dart:convert'; +import 'dart:math'; import 'package:flutter/cupertino.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:loading_animation_widget/loading_animation_widget.dart'; import 'package:provider/provider.dart'; +import 'package:tencent_cloud_chat_uikit/data_services/core/tim_uikit_wide_modal_operation_key.dart'; +import 'package:tencent_cloud_chat_uikit/ui/utils/screen_utils.dart'; import 'package:tencent_cloud_chat_uikit/ui/views/TIMUIKitChat/TIMUIKitMessageItem/tim_uikit_chat_text_translate_elem.dart'; +import 'package:tencent_cloud_chat_uikit/ui/widgets/forward_message_screen.dart'; +import 'package:tencent_cloud_chat_uikit/ui/widgets/wide_popup.dart'; import 'package:tencent_super_tooltip/tencent_super_tooltip.dart'; import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_state.dart'; import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_statelesswidget.dart'; import 'package:tencent_cloud_chat_uikit/business_logic/separate_models/tui_chat_separate_view_model.dart'; import 'package:tencent_cloud_chat_uikit/business_logic/view_models/tui_chat_global_model.dart'; import 'package:tencent_cloud_chat_uikit/business_logic/view_models/tui_self_info_view_model.dart'; - import 'package:tencent_cloud_chat_uikit/data_services/message/message_services.dart'; import 'package:tencent_cloud_chat_uikit/data_services/services_locatar.dart'; import 'package:tencent_cloud_chat_uikit/tencent_cloud_chat_uikit.dart'; @@ -30,9 +32,7 @@ import 'package:tencent_cloud_chat_uikit/ui/views/TIMUIKitChat/TIMUIKitMessageIt import 'package:tencent_cloud_chat_uikit/ui/views/TIMUIKitChat/TIMUIKitMessageItem/tim_uikit_chat_face_elem.dart'; import 'package:tencent_cloud_chat_uikit/ui/views/TIMUIKitChat/tim_uikit_cloud_custom_data.dart'; import 'package:tencent_cloud_chat_uikit/ui/widgets/avatar.dart'; -import 'package:tencent_cloud_chat_uikit/ui/widgets/loading.dart'; import 'package:tencent_cloud_chat_uikit/ui/widgets/radio_button.dart'; - import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_base.dart'; import '../TIMUIKitMessageItem/TIMUIKitMessageReaction/tim_uikit_message_reaction_select_emoji.dart'; @@ -66,6 +66,15 @@ typedef MessageItemContent = Widget? Function( VoidCallback clearJump, ); +class MessageHoverControlItem { + String name; + Widget icon; + ValueChanged onClick; + + MessageHoverControlItem( + {required this.name, required this.icon, required this.onClick}); +} + class MessageItemBuilder { /// text message builder final MessageItemContent? textMessageItemBuilder; @@ -125,6 +134,19 @@ class MessageItemBuilder { }); } +class MessageToolTipItem { + final String label; + final String id; + final String iconImageAsset; + final VoidCallback onClick; + + MessageToolTipItem( + {required this.label, + required this.id, + required this.iconImageAsset, + required this.onClick}); +} + class ToolTipsConfig { final bool showReplyMessage; final bool showMultipleChoiceMessage; @@ -135,6 +157,8 @@ class ToolTipsConfig { final bool showTranslation; final Widget? Function(V2TimMessage message, Function() closeTooltip, [Key? key, BuildContext? context])? additionalItemBuilder; + List Function( + V2TimMessage message, Function() closeTooltip)? additionalMessageToolTips; ToolTipsConfig( {this.showDeleteMessage = true, @@ -144,7 +168,9 @@ class ToolTipsConfig { this.showTranslation = true, this.showCopyMessage = true, this.showForwardMessage = true, - this.additionalItemBuilder}); + this.additionalMessageToolTips, + @Deprecated("Please use `additionalMessageToolTips` instead. You are now only expected to specify the data, rather than providing a whole widget. This makes usage easier, as you no longer need to worry about the UI display.") + this.additionalItemBuilder}); } class TIMUIKitHistoryMessageListItem extends StatefulWidget { @@ -152,7 +178,8 @@ class TIMUIKitHistoryMessageListItem extends StatefulWidget { final V2TimMessage message; /// tap remote user avatar callback function - final void Function(String userID)? onTapForOthersPortrait; + final void Function(String userID, TapDownDetails tapDetails)? + onTapForOthersPortrait; /// the function use for reply message, when click replied message can scroll to it. final Function? onScrollToIndex; @@ -167,7 +194,7 @@ class TIMUIKitHistoryMessageListItem extends StatefulWidget { /// message item builder, works for customize all message types and row layout. final MessageItemBuilder? messageItemBuilder; - /// controll avatart hide or show + /// Control avatar hide or show final bool showAvatar; /// message sending status @@ -185,7 +212,7 @@ class TIMUIKitHistoryMessageListItem extends StatefulWidget { /// allow avatar can tap final bool allowAvatarTap; - /// allow notifi user when send reply message + /// Auto mention user when send reply message final bool allowAtUserWhenReply; @Deprecated( @@ -260,7 +287,7 @@ class TIMUIKitHistoryMessageListItem extends StatefulWidget { this.bottomRowBuilder, this.isUseDefaultEmoji = false, this.customEmojiStickerList = const [], - this.textFieldController}) + this.textFieldController}) : super(key: key); @override @@ -306,13 +333,17 @@ class _TIMUIKItHistoryMessageListItemState extends TIMUIKitState with TickerProviderStateMixin { SuperTooltip? tooltip; + // ignore: unused_field final MessageService _messageService = serviceLocator(); final TUISelfInfoViewModel selfInfoModel = serviceLocator(); final TUIThemeViewModel themeModel = serviceLocator(); + // bool isChecked = false; final GlobalKey _key = GlobalKey(); + bool isShowWideToolTip = false; + TapDownDetails? _tapDetails; closeTooltip() { tooltip?.close(); @@ -323,8 +354,11 @@ class _TIMUIKItHistoryMessageListItemState message.cloudCustomData != null && message.cloudCustomData != ""; if (hasCustomData) { try { - final CloudCustomData messageCloudCustomData = - CloudCustomData.fromJson(json.decode(message.cloudCustomData!)); + final CloudCustomData messageCloudCustomData = CloudCustomData.fromJson( + json.decode( + TencentUtils.checkString(message.cloudCustomData) != null + ? message.cloudCustomData! + : "{}")); if (messageCloudCustomData.messageReply != null) { MessageRepliedData.fromJson(messageCloudCustomData.messageReply!); return true; @@ -343,7 +377,7 @@ class _TIMUIKItHistoryMessageListItemState final isShowJump = (model.jumpMsgID == messageItem.msgID) && (messageItem.msgID?.isNotEmpty ?? false); final MessageItemBuilder? messageItemBuilder = widget.messageItemBuilder; - final isFromSelf = messageItem.isSelf ?? false; + final isFromSelf = messageItem.isSelf ?? true; void clearJump() { Future.delayed(const Duration(milliseconds: 100), () { model.jumpMsgID = ""; @@ -382,7 +416,7 @@ class _TIMUIKItHistoryMessageListItemState message: messageItem, soundElem: messageItem.soundElem!, msgID: messageItem.msgID ?? "", - isFromSelf: messageItem.isSelf ?? false, + isFromSelf: messageItem.isSelf ?? true, clearJump: clearJump, isShowJump: isShowJump, localCustomInt: messageItem.localCustomInt, @@ -410,6 +444,8 @@ class _TIMUIKItHistoryMessageListItemState fontStyle: widget.themeData?.messageTextStyle, backgroundColor: widget.themeData?.messageBackgroundColor, textPadding: widget.textPadding, + isUseDefaultEmoji: widget.isUseDefaultEmoji, + customEmojiStickerList: widget.customEmojiStickerList, chatModel: model, isShowMessageReaction: widget.isUseMessageReaction, ); @@ -424,7 +460,7 @@ class _TIMUIKItHistoryMessageListItemState return TIMUIKitTextElem( chatModel: model, message: messageItem, - isFromSelf: messageItem.isSelf ?? false, + isFromSelf: messageItem.isSelf ?? true, clearJump: clearJump, isShowJump: isShowJump, borderRadius: widget.themeData?.messageBorderRadius, @@ -464,7 +500,7 @@ class _TIMUIKItHistoryMessageListItemState message: messageItem, messageID: messageItem.msgID, fileElem: messageItem.fileElem, - isSelf: messageItem.isSelf ?? false, + isSelf: messageItem.isSelf ?? true, clearJump: clearJump, isShowJump: isShowJump, isShowMessageReaction: widget.isUseMessageReaction, @@ -527,7 +563,7 @@ class _TIMUIKItHistoryMessageListItemState )!; } return TIMUIKitMergerElem( - messageItemBuilder: messageItemBuilder, + messageItemBuilder: messageItemBuilder, model: model, isShowJump: isShowJump, clearJump: clearJump, @@ -535,7 +571,7 @@ class _TIMUIKItHistoryMessageListItemState isShowMessageReaction: widget.isUseMessageReaction, mergerElem: messageItem.mergerElem!, messageID: messageItem.msgID ?? "", - isSelf: messageItem.isSelf ?? false); + isSelf: messageItem.isSelf ?? true); default: return Text(TIM_t("[未知消息]")); } @@ -545,8 +581,9 @@ class _TIMUIKItHistoryMessageListItemState final messageItem = widget.message; return Container( padding: const EdgeInsets.only(bottom: 20), - child: - TIMUIKitGroupTipsElem(groupTipsElem: messageItem.groupTipsElem!, groupMemberList: model.groupMemberList ?? [])); + child: TIMUIKitGroupTipsElem( + groupTipsElem: messageItem.groupTipsElem!, + groupMemberList: model.groupMemberList ?? [])); } Widget _selfRevokeEditMessageBuilder(theme, model) { @@ -611,12 +648,11 @@ class _TIMUIKItHistoryMessageListItemState width: 100, child: Container( decoration: BoxDecoration( - gradient: LinearGradient(colors: [ - const Color(0x00C0E1FF), - theme.primaryColor ?? CommonColor.lightPrimaryColor - ]), - ) - ), + gradient: LinearGradient(colors: [ + const Color(0x00C0E1FF), + theme.primaryColor ?? CommonColor.lightPrimaryColor + ]), + )), ), ), Text( @@ -635,12 +671,11 @@ class _TIMUIKItHistoryMessageListItemState width: 100, child: Container( decoration: BoxDecoration( - gradient: LinearGradient(colors: [ - theme.primaryColor ?? CommonColor.primaryColor, - const Color(0x00C0E1FF), - ]), - ) - ), + gradient: LinearGradient(colors: [ + theme.primaryColor ?? CommonColor.primaryColor, + const Color(0x00C0E1FF), + ]), + )), ), ), ], @@ -648,15 +683,17 @@ class _TIMUIKItHistoryMessageListItemState ); } - bool isRevokable(int timestamp) => + bool isRevocable(int timestamp) => (DateTime.now().millisecondsSinceEpoch / 1000).ceil() - timestamp < 120; - _onLongPress( + _onOpenToolTip( c, V2TimMessage message, TUIChatSeparateViewModel model, TUITheme theme, TapDownDetails? details, + bool? isFromWideTooltip, + bool? isShowMoreSticker, ) { if (tooltip != null && tooltip!.isOpen) { tooltip!.close(); @@ -665,29 +702,40 @@ class _TIMUIKItHistoryMessageListItemState tooltip = null; final screenHeight = MediaQuery.of(context).size.height; - if (context.size!.height + 180 > screenHeight && !PlatformUtils().isWeb) { - initTools( - context: c, - isLongMessage: true, - model: model, - details: details, - theme: theme, - ); - if (widget.onScrollToIndexBegin != null) { - widget.onScrollToIndexBegin!(message); - } - Future.delayed(const Duration(milliseconds: 500), () { - tooltip!.show(c); - }); - } else { - initTools( + final screenWidth = MediaQuery.of(context).size.width; + final isLongMessage = + context.size!.height + 350 > screenHeight && PlatformUtils().isMobile; + final isDesktopScreen = + TUIKitScreenUtils.getFormFactor(context) == DeviceType.Desktop; + final tapDetails = + (isDesktopScreen || isLongMessage) ? (details ?? _tapDetails) : details; + final isSelf = message.isSelf ?? true; + + final targetWidth = + min(MediaQuery.of(context).size.width * 0.84, 350).toDouble(); + final double dx = !isSelf + ? min(tapDetails?.globalPosition.dx ?? targetWidth, + screenWidth - targetWidth) + : max(tapDetails?.globalPosition.dx ?? targetWidth, targetWidth) + .toDouble(); + final double dy = min( + tapDetails?.globalPosition.dy ?? MediaQuery.of(context).size.height, + MediaQuery.of(context).size.height - 320) + .toDouble(); + final finalTapDetail = tapDetails != null + ? TapDownDetails( + globalPosition: Offset(dx, dy), + ) + : null; + + initTools( context: c, model: model, - details: details, + isShowMoreSticker: isShowMoreSticker, + details: finalTapDetail, theme: theme, - ); - tooltip!.show(c, targetCenter: details?.globalPosition); - } + isFromWideToolTip: isFromWideTooltip); + tooltip!.show(c, targetCenter: finalTapDetail?.globalPosition); } _clickOnCurrentSticker(int sticker) async { @@ -711,8 +759,15 @@ class _TIMUIKItHistoryMessageListItemState bool isLongMessage = false, required TUIChatSeparateViewModel model, TUITheme? theme, - TapDownDetails? details}) { - final isSelf = widget.message.isSelf ?? false; + bool? isShowMoreSticker, + TapDownDetails? details, + bool? isFromWideToolTip}) { + final isUseMessageReaction = widget.message.elemType == 2 + ? false + : model.chatConfig.isUseMessageReaction; + final isDesktopScreen = + TUIKitScreenUtils.getFormFactor(context) == DeviceType.Desktop; + final isSelf = widget.message.isSelf ?? true; double arrowTipDistance = 30; double arrowBaseWidth = 10; double arrowLength = 10; @@ -727,12 +782,12 @@ class _TIMUIKItHistoryMessageListItemState if (details != null && box != null) { double screenWidth = MediaQuery.of(context).size.width; final mousePosition = details.globalPosition; - hasArrow = false; + hasArrow = isDesktopScreen ? false : true; arrowTipDistance = 0; arrowBaseWidth = 0; arrowLength = 0; popupDirection = TooltipDirection.down; - if (isSelf) { + if (isSelf || (isFromWideToolTip ?? false)) { right = screenWidth - mousePosition.dx; } else { left = mousePosition.dx; @@ -744,14 +799,16 @@ class _TIMUIKItHistoryMessageListItemState Offset offset = box.localToGlobal(Offset.zero); double boxWidth = box.size.width; if (isSelf) { - right = screenWidth - offset.dx - boxWidth; + right = screenWidth - + offset.dx - + ((isUseMessageReaction) ? boxWidth : (boxWidth / 1.3)); } else { left = offset.dx; } if (offset.dy < 300 && !isLongMessage && viewInsetsBottom == 0) { selectEmojiPanelPosition = SelectEmojiPanelPosition.up; popupDirection = TooltipDirection.down; - } else if(viewInsetsBottom != 0 && offset.dy < 220){ + } else if (viewInsetsBottom != 0 && offset.dy < 220) { selectEmojiPanelPosition = SelectEmojiPanelPosition.up; popupDirection = TooltipDirection.down; } @@ -773,16 +830,15 @@ class _TIMUIKItHistoryMessageListItemState borderColor: theme?.white ?? Colors.white, backgroundColor: theme?.white ?? Colors.white, shadowColor: Colors.black26, - hasShadow: true, + hasShadow: isDesktopScreen ? false : true, borderWidth: 1.0, showCloseButton: ShowCloseButton.none, touchThroughAreaShape: ClipAreaShape.rectangle, content: TIMUIKitMessageTooltip( model: model, + isShowMoreSticker: isShowMoreSticker ?? false, toolTipsConfig: widget.toolTipsConfig, - isUseMessageReaction: widget.message.elemType == 2 - ? false - : model.chatConfig.isUseMessageReaction, + isUseMessageReaction: isUseMessageReaction, message: widget.message, allowAtUserWhenReply: widget.allowAtUserWhenReply, onLongPressForOthersHeadPortrait: @@ -797,12 +853,6 @@ class _TIMUIKItHistoryMessageListItemState ); } - double getMaxWidth(isSelect) { - final size = MediaQuery.of(context).size; - final width = size.width; - return width - (isSelect ? 180 : 150); - } - Widget _getMessageItemBuilder(V2TimMessage message, int? messageStatues, TUIChatSeparateViewModel model) { final messageBuilder = _messageItemBuilder; @@ -811,7 +861,7 @@ class _TIMUIKItHistoryMessageListItemState } // 弹出对话框 - Future showResendMsgFailDialg(BuildContext context) { + Future showResendMsgFailDialog(BuildContext context) { return showDialog( context: context, builder: (context) { @@ -837,6 +887,117 @@ class _TIMUIKItHistoryMessageListItemState ); } + @override + void dispose() { + super.dispose(); + if (tooltip?.isOpen ?? false) { + tooltip?.close(); + } + } + + bool isVoteMessage(V2TimMessage message) { + bool isvote = false; + V2TimCustomElem? custom = message.customElem; + + if (custom != null) { + String? data = custom.data; + if (data != null && data.isNotEmpty) { + try { + Map mapData = json.decode(data); + if (mapData["businessID"] == "group_poll") { + isvote = true; + } + } catch (err) { + // err + } + } + } + return isvote; + } + + List getMessageHoverControlBar( + TUIChatSeparateViewModel model, TUITheme theme) { + return [ + if (widget.isUseMessageReaction ?? false) + MessageHoverControlItem( + name: TIM_t("表情回应"), + icon: Icon( + Icons.emoji_emotions, + size: 13, + color: hexToColor("8f959e"), + ), + onClick: (details) { + _onOpenToolTip( + context, widget.message, model, theme, details, true, true); + }, + ), + if (widget.toolTipsConfig?.showReplyMessage ?? true) + MessageHoverControlItem( + name: TIM_t("回复"), + icon: Icon( + Icons.message, + size: 13, + color: hexToColor("8f959e"), + ), + onClick: (_) { + model.repliedMessage = widget.message; + if (widget.allowAtUserWhenReply && + widget.onLongPressForOthersHeadPortrait != null && + !(widget.message.isSelf ?? true)) { + widget.onLongPressForOthersHeadPortrait!( + widget.message.sender, widget.message.nickName); + } + }, + ), + if ((widget.toolTipsConfig?.showForwardMessage ?? true) && + !isVoteMessage(widget.message)) + MessageHoverControlItem( + name: TIM_t("转发"), + icon: Icon( + Icons.send, + size: 13, + color: hexToColor("8f959e"), + ), + onClick: (_) { + model.addToMultiSelectedMessageList(widget.message); + TUIKitWidePopup.showPopupWindow( + operationKey: TUIKitWideModalOperationKey.forward, + context: context, + title: TIM_t("转发"), + submitWidget: Text(TIM_t("发送")), + width: MediaQuery.of(context).size.width * 0.5, + height: MediaQuery.of(context).size.height * 0.8, + onSubmit: () { + forwardMessageScreenKey.currentState?.handleForwardMessage(); + }, + child: (onClose) => Container( + padding: const EdgeInsets.symmetric(horizontal: 10), + child: ForwardMessageScreen( + conversationType: ConvType.c2c, + key: forwardMessageScreenKey, + onClose: onClose, + model: model, + ), + ), + theme: theme); + }, + ), + MessageHoverControlItem( + name: TIM_t("更多"), + icon: Icon( + Icons.more_horiz, + size: 13, + color: hexToColor("8f959e"), + ), + onClick: (details) { + _onOpenToolTip( + context, widget.message, model, theme, details, true, false); + }, + ), + ...?model.chatConfig.additionalDesktopMessageHoverBarItem + ]; + } + _onMsgSendFailIconTap(V2TimMessage message, TUIChatSeparateViewModel model) { final convID = model.conversationID; final convType = model.conversationType; @@ -848,12 +1009,99 @@ class _TIMUIKItHistoryMessageListItemState context); } - @override - void dispose() { - super.dispose(); - if (tooltip?.isOpen ?? false) { - tooltip?.close(); - } + Widget renderHoverTipAndReadStatus(TUIChatSeparateViewModel model, + bool isSelf, V2TimMessage message, bool isPeerRead, TUITheme theme) { + final isDesktopScreen = + TUIKitScreenUtils.getFormFactor(context) == DeviceType.Desktop; + final wideHoverTipList = getMessageHoverControlBar(model, theme); + final lastItemName = wideHoverTipList.last.name; + return Column( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + if (isDesktopScreen && isShowWideToolTip) + Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(4), + border: Border.all(color: hexToColor("d9dde0"), width: 1)), + margin: const EdgeInsets.symmetric(horizontal: 4), + child: Row( + children: wideHoverTipList + .map((e) => Tooltip( + message: e.name, + preferBelow: false, + textStyle: TextStyle(fontSize: 12, color: theme.white), + child: Row( + children: [ + InkWell( + onTapDown: e.onClick, + child: SizedBox( + width: 22, + height: 22, + child: e.icon, + ), + ), + if (lastItemName != e.name) + SizedBox( + width: 1, + height: 22, + child: Container( + color: theme.weakDividerColor, + ), + ) + ], + ), + )) + .toList(), + ), + ), + if (!isDesktopScreen || !isShowWideToolTip) + const SizedBox( + height: 24, + ), + if (isSelf && + message.status == MessageStatus.V2TIM_MSG_STATUS_SEND_FAIL) + Container( + padding: const EdgeInsets.only(bottom: 3), + margin: const EdgeInsets.only(right: 6), + child: GestureDetector( + onTap: () async { + final reSend = await showResendMsgFailDialog(context); + if (reSend != null) { + _onMsgSendFailIconTap(message, model); + } + }, + child: Icon(Icons.error, color: theme.cautionColor, size: 18), + )), + if (model.chatConfig.isShowReadingStatus && + widget.showMessageReadRecipt && + model.conversationType == ConvType.c2c && + isSelf && + (message.status == MessageStatus.V2TIM_MSG_STATUS_SEND_SUCC || + message.status == MessageStatus.V2TIM_MSG_STATUS_SENDING)) + Container( + padding: const EdgeInsets.only(bottom: 3), + margin: const EdgeInsets.only(right: 6), + child: Text( + isPeerRead ? TIM_t("已读") : TIM_t("未读"), + style: TextStyle( + color: theme.chatMessageItemUnreadStatusTextColor, + fontSize: 12), + ), + ), + if (model.chatConfig.isShowGroupReadingStatus && + model.chatConfig.isShowGroupMessageReadReceipt && + model.conversationType == ConvType.group && + isSelf && + (message.status == MessageStatus.V2TIM_MSG_STATUS_SEND_SUCC || + message.status == MessageStatus.V2TIM_MSG_STATUS_SENDING)) + TIMUIKitMessageReadReceipt( + messageItem: widget.message, + onTapAvatar: widget.onTapForOthersPortrait, + ), + ], + ); } @override @@ -865,7 +1113,7 @@ class _TIMUIKItHistoryMessageListItemState final TUITheme theme = value.theme; final message = widget.message; final msgType = message.elemType; - final isSelf = message.isSelf ?? false; + final isSelf = message.isSelf ?? true; final msgStatus = message.status; final isGroupTipsMsg = msgType == MessageElemType.V2TIM_ELEM_TYPE_GROUP_TIPS; @@ -880,10 +1128,12 @@ class _TIMUIKItHistoryMessageListItemState isGroupMessage && model.chatConfig.isShowSelfNameInGroup; final isShowNickNameForOthers = isGroupMessage && model.chatConfig.isShowOthersNameInGroup; + final isDesktopScreen = + TUIKitScreenUtils.getFormFactor(context) == DeviceType.Desktop; if (isTimeDivider) { return _timeDividerBuilder(theme, message.timestamp ?? 0, model); } - if(isLatestDivider){ + if (isLatestDivider) { return _latestDividerBuilder(theme); } void clearJump() { @@ -908,7 +1158,7 @@ class _TIMUIKItHistoryMessageListItemState if (isRevokedMsg) { final displayName = isSelf ? TIM_t("您") : message.nickName ?? message.sender; - return isSelf && isRevokeEditable && isRevokable(message.timestamp!) + return isSelf && isRevokeEditable && isRevocable(message.timestamp!) ? _selfRevokeEditMessageBuilder(theme, model) : _revokedMessageBuilder(theme, displayName ?? ""); } @@ -938,7 +1188,7 @@ class _TIMUIKItHistoryMessageListItemState children: [ if (model.isMultiSelect) Container( - margin: const EdgeInsets.only(right: 12, top: 10), + margin: EdgeInsets.only(right: 12, top: 10, left: isSelf ? 16 : 0), child: CheckBoxButton( isChecked: model.multiSelectedMessageList.contains(message), onChanged: (value) { @@ -951,260 +1201,250 @@ class _TIMUIKItHistoryMessageListItemState ), ), Expanded( - child: GestureDetector( - behavior: - model.isMultiSelect ? HitTestBehavior.translucent : null, - onTap: () { - if (model.isMultiSelect) { - final checked = - model.multiSelectedMessageList.contains(message); - if (checked) { - model.removeFromMultiSelectedMessageList(message); - } else { - model.addToMultiSelectedMessageList(message); - } + child: MouseRegion( + onEnter: (_) { + if (isDesktopScreen) { + setState(() { + isShowWideToolTip = true; + }); } }, - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: - isSelf ? MainAxisAlignment.end : MainAxisAlignment.start, - children: [ - if (!isSelf && widget.showAvatar) - GestureDetector( - onTap: () { - if (widget.onTapForOthersPortrait != null && - widget.allowAvatarTap) { - widget - .onTapForOthersPortrait!(message.sender ?? ""); - } - }, - onLongPress: () { - if (widget.onLongPressForOthersHeadPortrait != - null) {} - widget.onLongPressForOthersHeadPortrait!( - message.sender, message.nickName); - }, - child: widget.userAvatarBuilder != null - ? widget.userAvatarBuilder!(context, message) - : Container( - margin: (isSelf && isShowNickNameForSelf) || - (!isSelf && isShowNickNameForOthers) - ? const EdgeInsets.only(top: 2) - : null, - child: SizedBox( - width: 40, - height: 40, - child: Avatar( - faceUrl: message.faceUrl ?? "", - showName: - MessageUtils.getDisplayName(message), - ), - ), - ), - ), - Container( - margin: widget.showAvatar - ? (isSelf - ? const EdgeInsets.only(right: 13) - : const EdgeInsets.only(left: 13)) - : null, - child: Column( - crossAxisAlignment: isSelf - ? CrossAxisAlignment.end - : CrossAxisAlignment.start, - children: [ - if ((isSelf && isShowNickNameForSelf) || - (!isSelf && isShowNickNameForOthers)) - widget.topRowBuilder != null - ? widget.topRowBuilder!(context, message) - : Container( - margin: const EdgeInsets.only(bottom: 4), - child: ConstrainedBox( - constraints: BoxConstraints( - maxWidth: MediaQuery.of(context) - .size - .width / - 1.7), - child: Text( - MessageUtils.getDisplayName(message), - overflow: TextOverflow.ellipsis, - style: widget - .themeData?.nickNameTextStyle ?? - TextStyle( - fontSize: 12, - color: theme.weakTextColor), - ), - )), - Row( - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - if (model.chatConfig.isShowReadingStatus && - widget.showMessageReadRecipt && - model.conversationType == ConvType.c2c && - isSelf && - message.status == - MessageStatus.V2TIM_MSG_STATUS_SEND_SUCC) - Container( - padding: const EdgeInsets.only(bottom: 3), - margin: const EdgeInsets.only(right: 6), - child: Text( - isPeerRead ? TIM_t("已读") : TIM_t("未读"), - style: TextStyle( - color: theme - .chatMessageItemUnreadStatusTextColor, - fontSize: 12), - ), - ), - if (model.chatConfig.isShowGroupReadingStatus && - model.chatConfig - .isShowGroupMessageReadReceipt && - model.conversationType == ConvType.group && - isSelf && - message.status == - MessageStatus.V2TIM_MSG_STATUS_SEND_SUCC) - TIMUIKitMessageReadReceipt( - messageItem: widget.message, - onTapAvatar: widget.onTapForOthersPortrait, - ), - if (widget.showMessageSending && - isSelf && - message.status == - MessageStatus.V2TIM_MSG_STATUS_SENDING) - Container( - padding: const EdgeInsets.only(bottom: 3), - margin: const EdgeInsets.only(right: 6), - child: const Loading(), - ), - if (isSelf && - message.status == - MessageStatus.V2TIM_MSG_STATUS_SEND_FAIL) - Container( - padding: const EdgeInsets.only(bottom: 3), - margin: const EdgeInsets.only(right: 6), - child: GestureDetector( - onTap: () async { - final reSend = - await showResendMsgFailDialg( - context); - if (reSend != null) { - _onMsgSendFailIconTap(message, model); - } - }, - child: Icon(Icons.error, - color: theme.cautionColor, size: 18), - )), - Container( - constraints: BoxConstraints( - // maxWidth: getMaxWidth(false), - // maxWidth: getMaxWidth(false), - maxWidth: constraints.maxWidth * 0.8, - ), - child: Builder(builder: (context) { - return Column( - crossAxisAlignment: - (message.isSelf ?? false) - ? CrossAxisAlignment.end - : CrossAxisAlignment.start, - children: [ - GestureDetector( - child: IgnorePointer( - ignoring: model.isMultiSelect, - child: _getMessageItemBuilder( - message, message.status, model)), - onSecondaryTapDown: (details) { - if (PlatformUtils().isWeb) { - if (widget.allowLongPress) { - _onLongPress( - context, - message, - model, - theme, - details, - ); - } - if (widget.onLongPress != null) { - widget.onLongPress!(context, message); - } - } - }, - onLongPress: () { - if (widget.allowLongPress) { - _onLongPress( - context, - message, - model, - theme, - null, - ); - } - if (widget.onLongPress != null) { - widget.onLongPress!(context, message); - } - }, - ), - Container( - color: theme.white, - child: TIMUIKitTextTranslationElem( - message: message, - isUseDefaultEmoji: widget.isUseDefaultEmoji, - customEmojiStickerList: widget.customEmojiStickerList, - isFromSelf: message.isSelf ?? false, - isShowJump: false, - clearJump: () {}, - chatModel: model), - ) - ], - ); - }), - ), - if (!isSelf && - message.elemType == - MessageElemType.V2TIM_ELEM_TYPE_SOUND && - message.localCustomInt != null && - message.localCustomInt != - HistoryMessageDartConstant.read) - Padding( - padding: const EdgeInsets.only( - left: 5, bottom: 12), - child: Icon(Icons.circle, - color: theme.cautionColor, size: 10)), - ], - ), - if (widget.bottomRowBuilder != null) - widget.bottomRowBuilder!(context, message) - ], - ), - ), - if (widget.message.elemType == 6 && isDownloadWaiting) - Container( - margin: const EdgeInsets.only(top: 24, left: 6), - child: LoadingAnimationWidget.threeArchedCircle( - color: theme.weakTextColor ?? Colors.grey, - size: 20, - ), - ), - if (isSelf && widget.showAvatar) - widget.userAvatarBuilder != null - ? widget.userAvatarBuilder!(context, message) - : SizedBox( - width: 40, - height: 40, - child: GestureDetector( - onTap: () { + onExit: (_) { + if (isDesktopScreen) { + setState(() { + isShowWideToolTip = false; + }); + } + }, + child: GestureDetector( + behavior: + model.isMultiSelect ? HitTestBehavior.translucent : null, + onTap: () { + if (model.isMultiSelect) { + final checked = + model.multiSelectedMessageList.contains(message); + if (checked) { + model.removeFromMultiSelectedMessageList(message); + } else { + model.addToMultiSelectedMessageList(message); + } + } + }, + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: isSelf + ? MainAxisAlignment.end + : MainAxisAlignment.start, + children: [ + if (!isSelf && widget.showAvatar) + InkWell( + onLongPress: () { + if (widget.onLongPressForOthersHeadPortrait != + null) {} + if (model.chatConfig.isAllowLongPressAvatarToAt) { + widget.onLongPressForOthersHeadPortrait!( + message.sender, message.nickName); + } + }, + onTapDown: isDesktopScreen + ? (details) { if (widget.onTapForOthersPortrait != null && widget.allowAvatarTap) { widget.onTapForOthersPortrait!( - message.sender ?? ""); + message.sender ?? "", details); + } + } + : null, + onTap: isDesktopScreen + ? null + : () { + if (widget.onTapForOthersPortrait != null && + widget.allowAvatarTap) { + widget.onTapForOthersPortrait!( + message.sender ?? "", TapDownDetails()); } }, - child: Avatar( - faceUrl: message.faceUrl ?? "", - showName: - MessageUtils.getDisplayName(message)), - ), + child: widget.userAvatarBuilder != null + ? widget.userAvatarBuilder!(context, message) + : Container( + margin: (isSelf && isShowNickNameForSelf) || + (!isSelf && isShowNickNameForOthers) + ? const EdgeInsets.only(top: 2) + : null, + child: SizedBox( + width: 40, + height: 40, + child: Avatar( + faceUrl: message.faceUrl ?? "", + showName: + MessageUtils.getDisplayName(message), + ), + ), + ), + ), + Container( + margin: widget.showAvatar + ? (isSelf + ? const EdgeInsets.only(right: 13) + : const EdgeInsets.only(left: 13)) + : null, + child: Column( + crossAxisAlignment: isSelf + ? CrossAxisAlignment.end + : CrossAxisAlignment.start, + children: [ + if ((isSelf && isShowNickNameForSelf) || + (!isSelf && isShowNickNameForOthers)) + widget.topRowBuilder != null + ? widget.topRowBuilder!(context, message) + : Container( + margin: const EdgeInsets.only(bottom: 4), + child: ConstrainedBox( + constraints: BoxConstraints( + maxWidth: MediaQuery.of(context) + .size + .width / + 1.7), + child: Text( + MessageUtils.getDisplayName(message), + overflow: TextOverflow.ellipsis, + style: widget.themeData + ?.nickNameTextStyle ?? + TextStyle( + fontSize: 12, + color: theme.weakTextColor), + ), + )), + Row( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + if (isSelf) + renderHoverTipAndReadStatus(model, isSelf, + message, isPeerRead, theme), + Container( + constraints: BoxConstraints( + maxWidth: constraints.maxWidth * 0.77, + ), + child: Builder(builder: (context) { + return Column( + crossAxisAlignment: + (message.isSelf ?? true) + ? CrossAxisAlignment.end + : CrossAxisAlignment.start, + children: [ + GestureDetector( + child: IgnorePointer( + ignoring: model.isMultiSelect, + child: _getMessageItemBuilder( + message, + message.status, + model)), + onSecondaryTapDown: (details) { + if (widget.onLongPress != null) { + widget.onLongPress!( + context, message); + return; + } + if (!PlatformUtils().isMobile) { + if (widget.allowLongPress) { + _onOpenToolTip( + context, + message, + model, + theme, + details, + false, + false); + } + } + }, + onLongPress: () { + if (widget.onLongPress != null) { + widget.onLongPress!( + context, message); + return; + } + if (widget.allowLongPress && + PlatformUtils().isMobile) { + _onOpenToolTip( + context, + message, + model, + theme, + null, + false, + false); + } + }, + onTapDown: (details) { + _tapDetails = details; + }, + ), + TIMUIKitTextTranslationElem( + message: message, + isUseDefaultEmoji: + widget.isUseDefaultEmoji, + customEmojiStickerList: + widget.customEmojiStickerList, + isFromSelf: message.isSelf ?? true, + isShowJump: false, + clearJump: () {}, + chatModel: model) + ], + ); + }), + ), + if (!isSelf && + message.elemType == + MessageElemType.V2TIM_ELEM_TYPE_SOUND && + message.localCustomInt != null && + message.localCustomInt != + HistoryMessageDartConstant.read) + Padding( + padding: const EdgeInsets.only( + left: 5, bottom: 12), + child: Icon(Icons.circle, + color: theme.cautionColor, size: 10)), + if (!isSelf) + renderHoverTipAndReadStatus(model, isSelf, + message, isPeerRead, theme), + ], ), - ], + if (widget.bottomRowBuilder != null) + widget.bottomRowBuilder!(context, message) + ], + ), + ), + if (widget.message.elemType == 6 && isDownloadWaiting) + Container( + margin: const EdgeInsets.only(top: 24, left: 6), + child: LoadingAnimationWidget.threeArchedCircle( + color: theme.weakTextColor ?? Colors.grey, + size: 20, + ), + ), + if (isSelf && widget.showAvatar) + widget.userAvatarBuilder != null + ? widget.userAvatarBuilder!(context, message) + : SizedBox( + width: 40, + height: 40, + child: InkWell( + onTapDown: (details) { + if (widget.onTapForOthersPortrait != null && + widget.allowAvatarTap) { + widget.onTapForOthersPortrait!( + message.sender ?? "", details); + } + }, + child: Avatar( + faceUrl: message.faceUrl ?? "", + showName: + MessageUtils.getDisplayName(message)), + ), + ), + ], + ), ), ), ), diff --git a/lib/ui/views/TIMUIKitChat/TIMUIKItMessageList/tim_uikit_chat_message_tooltip.dart b/lib/ui/views/TIMUIKitChat/TIMUIKItMessageList/tim_uikit_chat_message_tooltip.dart index 00bc57d..d7f6016 100644 --- a/lib/ui/views/TIMUIKitChat/TIMUIKItMessageList/tim_uikit_chat_message_tooltip.dart +++ b/lib/ui/views/TIMUIKitChat/TIMUIKItMessageList/tim_uikit_chat_message_tooltip.dart @@ -1,11 +1,15 @@ // ignore_for_file: non_constant_identifier_names, avoid_print +import 'dart:convert'; +import 'dart:io'; import 'dart:math'; - import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:provider/provider.dart'; +import 'package:tencent_cloud_chat_uikit/data_services/services_locatar.dart'; +import 'package:tencent_cloud_chat_uikit/ui/utils/common_utils.dart'; import 'package:tencent_cloud_chat_uikit/ui/utils/message.dart'; +import 'package:tencent_cloud_chat_uikit/ui/views/TIMUIKitChat/TIMUIKitMessageItem/TIMUIKitMessageReaction/tim_uikit_message_reaction_select_emoji.dart'; import 'package:tencent_im_base/tencent_im_base.dart'; import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_base.dart'; import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_state.dart'; @@ -13,11 +17,11 @@ import 'package:tencent_cloud_chat_uikit/business_logic/separate_models/tui_chat import 'package:tencent_cloud_chat_uikit/business_logic/view_models/tui_chat_global_model.dart'; import 'package:tencent_cloud_chat_uikit/ui/utils/platform.dart'; import 'package:tencent_cloud_chat_uikit/ui/utils/screen_utils.dart'; - import 'package:tencent_cloud_chat_uikit/ui/views/TIMUIKitChat/TIMUIKItMessageList/tim_uikit_chat_history_message_list_item.dart'; import 'package:tencent_cloud_chat_uikit/ui/widgets/forward_message_screen.dart'; - -import '../TIMUIKitMessageItem/TIMUIKitMessageReaction/tim_uikit_message_reaction_select_emoji.dart'; +import 'package:tencent_open_file/tencent_open_file.dart'; +import 'package:url_launcher/url_launcher.dart'; +import 'package:path/path.dart' as path; class TIMUIKitMessageTooltip extends StatefulWidget { /// tool tips panel configuration, long press message will show tool tips panel @@ -46,6 +50,8 @@ class TIMUIKitMessageTooltip extends StatefulWidget { final TUIChatSeparateViewModel model; + final bool isShowMoreSticker; + const TIMUIKitMessageTooltip( {Key? key, this.toolTipsConfig, @@ -56,7 +62,8 @@ class TIMUIKitMessageTooltip extends StatefulWidget { this.onLongPressForOthersHeadPortrait, required this.selectEmojiPanelPosition, required this.onCloseTooltip, - required this.onSelectSticker}) + required this.onSelectSticker, + this.isShowMoreSticker = false}) : super(key: key); @override @@ -65,7 +72,52 @@ class TIMUIKitMessageTooltip extends StatefulWidget { class TIMUIKitMessageTooltipState extends TIMUIKitState { + final TUIChatGlobalModel globalModal = serviceLocator(); bool isShowMoreSticker = false; + bool isShowOpenFile = false; + String filePath = ""; + + @override + void initState() { + super.initState(); + hasFile(); + isShowMoreSticker = widget.isShowMoreSticker; + } + + hasFile() { + if (PlatformUtils().isMobile || widget.message.fileElem == null) { + isShowOpenFile = false; + return; + } + if (PlatformUtils().isWeb) { + isShowOpenFile = true; + return; + } + if (PlatformUtils().isDesktop) { + if (globalModal.getMessageProgress(widget.message.msgID) == 100) { + String savePath = + TencentUtils.checkString(widget.message.fileElem!.localUrl) ?? + globalModal.getFileMessageLocation(widget.message.msgID); + File f = File(savePath); + if (f.existsSync() && widget.message.msgID != null) { + filePath = savePath; + isShowOpenFile = true; + return; + } + isShowOpenFile = false; + return; + } + String savePath = widget.message.fileElem!.localUrl ?? ''; + File f = File(savePath); + if (f.existsSync() && widget.message.msgID != null) { + filePath = savePath; + globalModal.setMessageProgress(widget.message.msgID!, 100); + isShowOpenFile = true; + return; + } + } + isShowOpenFile = false; + } bool isRevocable(int timestamp, int upperTimeLimit) => (DateTime.now().millisecondsSinceEpoch / 1000).ceil() - timestamp < @@ -73,10 +125,10 @@ class TIMUIKitMessageTooltipState Widget ItemInkWell({ Widget? child, - GestureTapCallback? onTap + GestureTapCallback? onTap, }) { return SizedBox( - width: 40, + width: 44, child: InkWell( onTap: onTap, splashColor: Colors.white, @@ -88,69 +140,105 @@ class TIMUIKitMessageTooltipState ); } - _buildLongPressTipItem(TUITheme theme, TUIChatSeparateViewModel model) { + bool isVoteMessage(V2TimMessage message) { + bool isvote = false; + V2TimCustomElem? custom = message.customElem; + + if (custom != null) { + String? data = custom.data; + if (data != null && data.isNotEmpty) { + try { + Map mapData = json.decode(data); + if (mapData["businessID"] == "group_poll") { + isvote = true; + } + } catch (err) { + // err + } + } + } + return isvote; + } + + _buildLongPressTipItem( + TUITheme theme, TUIChatSeparateViewModel model, V2TimMessage message) { + final isDesktopScreen = + TUIKitScreenUtils.getFormFactor(context) == DeviceType.Desktop; final isCanRevoke = isRevocable( widget.message.timestamp!, model.chatConfig.upperRecallTime); final shouldShowRevokeAction = isCanRevoke && - (widget.message.isSelf ?? false) && + (widget.message.isSelf ?? true) && widget.message.status != MessageStatus.V2TIM_MSG_STATUS_SEND_FAIL; final shouldShowReplyAction = !(widget.message.customElem?.data != null && MessageUtils.isCallingData(widget.message.customElem!.data!)); final shouldShowForwardAction = !(widget.message.customElem?.data != null && MessageUtils.isCallingData(widget.message.customElem!.data!)); final tooltipsConfig = widget.toolTipsConfig; - final defaultTipsList = [ - { - "label": TIM_t("复制"), - "id": "copyMessage", - "icon": "images/copy_message.png" - }, - if(shouldShowForwardAction) - { - "label": TIM_t("转发"), - "id": "forwardMessage", - "icon": "images/forward_message.png" - }, - { - "label": TIM_t("多选"), - "id": "multiSelect", - "icon": "images/multi_message.png" - }, + final List defaultTipsList = [ + if (isShowOpenFile) + MessageToolTipItem( + label: TIM_t("打开"), + id: "open", + iconImageAsset: "images/open_in_new.png", + onClick: () => _onTap("open", model)), + if (isShowOpenFile && PlatformUtils().isDesktop) + MessageToolTipItem( + label: PlatformUtils().isMacOS ? TIM_t("在访达中打开") : TIM_t("查看文件夹"), + id: "finder", + iconImageAsset: "images/folder_open.png", + onClick: () => _onTap("finder", model)), + MessageToolTipItem( + label: TIM_t("复制"), + id: "copyMessage", + iconImageAsset: "images/copy_message.png", + onClick: () => _onTap("copyMessage", model)), + if (shouldShowForwardAction && !isVoteMessage(widget.message)) + MessageToolTipItem( + label: TIM_t("转发"), + id: "forwardMessage", + iconImageAsset: "images/forward_message.png", + onClick: () => _onTap("forwardMessage", model)), + MessageToolTipItem( + label: TIM_t("多选"), + id: "multiSelect", + iconImageAsset: "images/multi_message.png", + onClick: () => _onTap("multiSelect", model)), if (shouldShowReplyAction) - { - "label": TIM_t("引用"), - "id": "replyMessage", - "icon": "images/reply_message.png" - }, - { - "label": TIM_t("删除"), - "id": "delete", - "icon": "images/delete_message.png" - }, - { - "label": TIM_t("翻译"), - "id": "translate", - "icon": "images/translate.png" - }, + MessageToolTipItem( + label: TIM_t(model.chatConfig.isAtWhenReply ? "回复" : "引用"), + id: "replyMessage", + iconImageAsset: "images/reply_message.png", + onClick: () => _onTap("replyMessage", model)), + MessageToolTipItem( + label: TIM_t("删除"), + id: "delete", + iconImageAsset: "images/delete_message.png", + onClick: () => _onTap("delete", model)), + MessageToolTipItem( + label: TIM_t("翻译"), + id: "translate", + iconImageAsset: "images/translate.png", + onClick: () => _onTap("translate", model)), if (shouldShowRevokeAction) - { - "label": TIM_t("撤回"), - "id": "revoke", - "icon": "images/revoke_message.png" - } + MessageToolTipItem( + label: TIM_t("撤回"), + id: "revoke", + iconImageAsset: "images/revoke_message.png", + onClick: () => _onTap("revoke", model)), ]; - List formatedTipsList = defaultTipsList; + List defaultFormattedTipsList = defaultTipsList; if (tooltipsConfig != null) { - formatedTipsList = defaultTipsList.where((element) { - final type = element["id"]; + defaultFormattedTipsList = defaultTipsList.where((element) { + final type = element.id; if (type == "copyMessage") { - return tooltipsConfig.showCopyMessage && widget.message.elemType == MessageElemType.V2TIM_ELEM_TYPE_TEXT; + return tooltipsConfig.showCopyMessage && + widget.message.elemType == MessageElemType.V2TIM_ELEM_TYPE_TEXT; } if (type == "forwardMessage") { - return tooltipsConfig.showForwardMessage; + return tooltipsConfig.showForwardMessage && !isDesktopScreen; } if (type == "replyMessage") { - return tooltipsConfig.showReplyMessage; + return tooltipsConfig.showReplyMessage && !isDesktopScreen; } if (type == "delete") { return (!PlatformUtils().isWeb) && tooltipsConfig.showDeleteMessage; @@ -158,67 +246,158 @@ class TIMUIKitMessageTooltipState if (type == "multiSelect") { return tooltipsConfig.showMultipleChoiceMessage; } - if (type == "translate") { - return tooltipsConfig.showTranslation && widget.message.elemType == MessageElemType.V2TIM_ELEM_TYPE_TEXT; - } + if (type == "revoke") { return tooltipsConfig.showRecallMessage; } + if (type == "translate") { + return tooltipsConfig.showTranslation && + widget.message.elemType == MessageElemType.V2TIM_ELEM_TYPE_TEXT; + } return true; }).toList(); } - return formatedTipsList - .map( - (item) => Material( - color: Colors.white, - child: ItemInkWell( - onTap: () { - _onTap(item["id"]!, model); - }, - child: Column( - children: [ - Image.asset( - item["icon"]!, - package: 'tencent_cloud_chat_uikit', - width: 20, - height: 20, + + final List? customList = + widget.toolTipsConfig?.additionalMessageToolTips != null + ? (widget.toolTipsConfig?.additionalMessageToolTips!( + message, widget.onCloseTooltip)) + : []; + + List formattedTipsList = [ + ...defaultFormattedTipsList, + ...?customList, + ]; + + List widgetList = []; + if (isDesktopScreen) { + widgetList = formattedTipsList + .map( + (item) => Material( + color: Colors.white, + child: InkWell( + onTap: () { + item.onClick(); + }, + child: Container( + padding: const EdgeInsets.all(6), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Image.asset( + item.iconImageAsset, + package: 'tencent_cloud_chat_uikit', + width: 20, + height: 20, + ), + const SizedBox( + height: 4, + width: 8, + ), + Text( + item.label, + style: TextStyle( + decoration: TextDecoration.none, + color: theme.darkTextColor, + fontSize: 12, + ), + ) + ], ), - const SizedBox( - height: 4, - ), - Text( - item["label"]!, - style: TextStyle( - decoration: TextDecoration.none, - color: theme.darkTextColor, - fontSize: 10, - ), - ) - ], + ), ), ), - ), - ) - .toList(); + ) + .toList(); + } else { + widgetList = formattedTipsList + .map( + (item) => Material( + color: Colors.white, + child: ItemInkWell( + onTap: () { + item.onClick(); + }, + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Image.asset( + item.iconImageAsset, + package: 'tencent_cloud_chat_uikit', + width: 20, + height: 20, + ), + const SizedBox( + height: 4, + width: 60, + ), + Text( + item.label, + style: TextStyle( + decoration: TextDecoration.none, + color: theme.darkTextColor, + fontSize: 10, + ), + ) + ], + ), + ), + ), + ) + .toList(); + } + if (widgetList.isEmpty && widget.isUseMessageReaction == false) { + widget.onCloseTooltip(); + } + + return widgetList; } _onTap(String operation, TUIChatSeparateViewModel model) async { final messageItem = widget.message; final msgID = messageItem.msgID as String; switch (operation) { + case "open": + if (PlatformUtils().isDesktop) { + final String savePath = + TencentUtils.checkString(widget.message.fileElem!.localUrl) ?? + globalModal.getFileMessageLocation(widget.message.msgID); + launchUrl(Uri.file(savePath)); + } else { + if (PlatformUtils().isWindows) { + OpenFile.open(widget.message.fileElem?.path ?? ""); + } else { + launchUrl( + Uri.parse(widget.message.fileElem?.path ?? ""), + mode: LaunchMode.externalApplication, + ); + } + } + break; + case "finder": + final String savePath = + TencentUtils.checkString(widget.message.fileElem!.localUrl) ?? + globalModal.getFileMessageLocation(widget.message.msgID); + final String fileDir = path.dirname(savePath); + if (PlatformUtils().isWindows) { + OpenFile.open(fileDir); + } else { + launchUrl(Uri.file(fileDir)); + } + break; case "delete": model.deleteMsg(msgID, webMessageInstance: messageItem.messageFromWeb); break; case "revoke": model.revokeMsg(msgID, messageItem.messageFromWeb); break; + case 'translate': + model.translateText(widget.message); + break; case "multiSelect": model.updateMultiSelectStatus(true); model.addToMultiSelectedMessageList(widget.message); break; - case 'translate': - model.translateText(widget.message); - break; case "forwardMessage": model.addToMultiSelectedMessageList(widget.message); Navigator.push( @@ -238,16 +417,15 @@ class TIMUIKitMessageTooltipState type: TIMCallbackType.INFO, infoRecommendText: TIM_t("已复制"), infoCode: 6660408)); - } catch (e) { - print(e); - } + // ignore: empty_catches + } catch (e) {} } break; case "replyMessage": model.repliedMessage = widget.message; if (widget.allowAtUserWhenReply && widget.onLongPressForOthersHeadPortrait != null && - !(widget.message.isSelf ?? false)) { + !(widget.message.isSelf ?? true)) { widget.onLongPressForOthersHeadPortrait!( widget.message.sender, widget.message.nickName); } @@ -264,7 +442,8 @@ class TIMUIKitMessageTooltipState @override Widget tuiBuild(BuildContext context, TUIKitBuildValue value) { final TUITheme theme = value.theme; - + final isDesktopScreen = + TUIKitScreenUtils.getFormFactor(context) == DeviceType.Desktop; return MultiProvider( providers: [ ChangeNotifierProvider.value(value: widget.model), @@ -278,16 +457,38 @@ class TIMUIKitMessageTooltipState ? widget.toolTipsConfig!.additionalItemBuilder!( widget.message, widget.onCloseTooltip, null, context) : null; + final message = widget.message; return Container( - padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + decoration: isDesktopScreen + ? BoxDecoration( + boxShadow: const [ + BoxShadow( + color: Color(0xCCbebebe), + offset: Offset(2, 2), + blurRadius: 10, + spreadRadius: 1, + ), + ], + border: Border.all( + width: 1, + color: hexToColor("dee0e3"), + ), + color: Colors.white, + borderRadius: const BorderRadius.all(Radius.circular(10)), + ) + : null, + color: isDesktopScreen ? null : Colors.white, + padding: EdgeInsets.symmetric( + horizontal: 8, vertical: isDesktopScreen ? 8 : 4), child: ConstrainedBox( constraints: BoxConstraints( - maxWidth: min(MediaQuery.of(context).size.width * 0.7, 350), + maxWidth: min(MediaQuery.of(context).size.width * 0.75, 350), ), child: Column( mainAxisSize: MainAxisSize.min, children: [ - if (widget.isUseMessageReaction && + if ((!isDesktopScreen || widget.isShowMoreSticker) && + widget.isUseMessageReaction && widget.selectEmojiPanelPosition == SelectEmojiPanelPosition.up) TIMUIKitMessageReactionEmojiSelectPanel( @@ -299,7 +500,8 @@ class TIMUIKitMessageTooltipState }); }, ), - if (widget.isUseMessageReaction && + if (!isDesktopScreen && + widget.isUseMessageReaction && widget.selectEmojiPanelPosition == SelectEmojiPanelPosition.up && isShowMoreSticker == false) @@ -314,40 +516,58 @@ class TIMUIKitMessageTooltipState Row( mainAxisSize: MainAxisSize.min, children: [ - if (widget.isUseMessageReaction) + if (!isDesktopScreen && widget.isUseMessageReaction) Expanded( child: Wrap( direction: Axis.horizontal, - alignment: ScreenUtils.getFormFactor(context) == - ScreenType.Handset - ? WrapAlignment.spaceBetween - : WrapAlignment.start, + alignment: + TUIKitScreenUtils.getFormFactor(context) == + DeviceType.Mobile + ? WrapAlignment.spaceBetween + : WrapAlignment.start, spacing: 4, runSpacing: 8, children: [ - ..._buildLongPressTipItem(theme, model), + ..._buildLongPressTipItem(theme, model, message), if (extraTipsActionItem != null) extraTipsActionItem ], )), - if (!widget.isUseMessageReaction) - Wrap( - direction: Axis.horizontal, - alignment: ScreenUtils.getFormFactor(context) == - ScreenType.Handset - ? WrapAlignment.spaceAround - : WrapAlignment.start, - spacing: 4, - runSpacing: 16, - children: [ - ..._buildLongPressTipItem(theme, model), - if (extraTipsActionItem != null) - extraTipsActionItem - ], - ) + if (!isDesktopScreen && !widget.isUseMessageReaction) + ConstrainedBox( + constraints: BoxConstraints( + maxWidth: min( + MediaQuery.of(context).size.width * 0.75, + 350), + ), + child: Wrap( + direction: Axis.horizontal, + alignment: + TUIKitScreenUtils.getFormFactor(context) == + DeviceType.Mobile + ? WrapAlignment.spaceBetween + : WrapAlignment.start, + spacing: 4, + runSpacing: 8, + children: [ + ..._buildLongPressTipItem( + theme, model, message), + if (extraTipsActionItem != null) + extraTipsActionItem + ], + ), + ), + if (isDesktopScreen) + Table(columnWidths: const { + 0: IntrinsicColumnWidth(), + }, children: [ + ..._buildLongPressTipItem(theme, model, message) + .map((e) => TableRow(children: [e])) + ]) ], ), - if (widget.isUseMessageReaction && + if (!isDesktopScreen && + widget.isUseMessageReaction && widget.selectEmojiPanelPosition == SelectEmojiPanelPosition.down && isShowMoreSticker == false) @@ -358,7 +578,8 @@ class TIMUIKitMessageTooltipState indent: 0, // endIndent: 10, color: Colors.black12)), - if (widget.isUseMessageReaction && + if ((!isDesktopScreen || widget.isShowMoreSticker) && + widget.isUseMessageReaction && widget.selectEmojiPanelPosition == SelectEmojiPanelPosition.down) TIMUIKitMessageReactionEmojiSelectPanel( diff --git a/lib/ui/views/TIMUIKitChat/TIMUIKItMessageList/tim_uikit_history_message_list_container.dart b/lib/ui/views/TIMUIKitChat/TIMUIKItMessageList/tim_uikit_history_message_list_container.dart index 838b933..6ed275f 100644 --- a/lib/ui/views/TIMUIKitChat/TIMUIKItMessageList/tim_uikit_history_message_list_container.dart +++ b/lib/ui/views/TIMUIKitChat/TIMUIKItMessageList/tim_uikit_history_message_list_container.dart @@ -1,3 +1,5 @@ +// ignore_for_file: deprecated_member_use_from_same_package + import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; @@ -33,6 +35,9 @@ class TIMUIKitHistoryMessageListContainer extends StatefulWidget { /// message item builder, works for customize all message types and row layout. final MessageItemBuilder? messageItemBuilder; + /// The controller for text field. + final TIMUIKitInputTextFieldController? textFieldController; + /// the builder for avatar final Widget Function(BuildContext context, V2TimMessage message)? userAvatarBuilder; @@ -46,7 +51,7 @@ class TIMUIKitHistoryMessageListContainer extends StatefulWidget { /// conversation type final ConvType conversationType; - final void Function(String userID)? onTapAvatar; + final void Function(String userID, TapDownDetails tapDetails)? onTapAvatar; @Deprecated( "Nickname will not show in one-to-one chat, if you tend to control it in group chat, please use `isShowSelfNameInGroup` and `isShowOthersNameInGroup` from `config: TIMUIKitChatConfig` instead") @@ -62,9 +67,6 @@ class TIMUIKitHistoryMessageListContainer extends StatefulWidget { final List customEmojiStickerList; - /// The controller for text field. - final TIMUIKitInputTextFieldController? textFieldController; - final bool isAllowScroll; final V2TimConversation conversation; @@ -105,12 +107,13 @@ class _TIMUIKitHistoryMessageListContainerState List historyMessageList = []; - Future requestForData(String? lastMsgID, LoadDirection direction, TUIChatSeparateViewModel model, + Future requestForData(String? lastMsgID, LoadDirection direction, + TUIChatSeparateViewModel model, [int? count]) async { - print("requestForData $lastMsgID $direction"); - if ((direction == LoadDirection.previous && model.haveMoreData) || (direction == LoadDirection.latest && model.haveMoreLatestData)) { - await model.loadData( - direction: direction, + if ((direction == LoadDirection.previous && model.haveMoreData) || + (direction == LoadDirection.latest && model.haveMoreLatestData)) { + await model.loadChatRecord( + direction: direction, count: count ?? (kIsWeb ? 15 : HistoryMessageDartConstant.getCount), lastMsgID: lastMsgID); } @@ -152,6 +155,7 @@ class _TIMUIKitHistoryMessageListContainerState mainHistoryListConfig: widget.mainHistoryListConfig, itemBuilder: (context, message) { return TIMUIKitHistoryMessageListItem( + textFieldController: widget.textFieldController, userAvatarBuilder: widget.userAvatarBuilder, customEmojiStickerList: widget.customEmojiStickerList, isUseDefaultEmoji: widget.isUseDefaultEmoji, @@ -166,7 +170,6 @@ class _TIMUIKitHistoryMessageListContainerState message: message!, showAvatar: chatConfig.isShowAvatar, onTapForOthersPortrait: widget.onTapAvatar, - textFieldController: widget.textFieldController, messageItemBuilder: widget.messageItemBuilder, onLongPressForOthersHeadPortrait: widget.onLongPressForOthersHeadPortrait, diff --git a/lib/ui/views/TIMUIKitChat/TIMUIKItMessageList/tim_uikit_message_read_receipt.dart b/lib/ui/views/TIMUIKitChat/TIMUIKItMessageList/tim_uikit_message_read_receipt.dart index 5c9a738..2b1c04a 100644 --- a/lib/ui/views/TIMUIKitChat/TIMUIKItMessageList/tim_uikit_message_read_receipt.dart +++ b/lib/ui/views/TIMUIKitChat/TIMUIKItMessageList/tim_uikit_message_read_receipt.dart @@ -3,13 +3,16 @@ import 'package:provider/provider.dart'; import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_statelesswidget.dart'; import 'package:tencent_cloud_chat_uikit/business_logic/separate_models/tui_chat_separate_view_model.dart'; import 'package:tencent_cloud_chat_uikit/business_logic/view_models/tui_chat_global_model.dart'; +import 'package:tencent_cloud_chat_uikit/data_services/core/tim_uikit_wide_modal_operation_key.dart'; import 'package:tencent_cloud_chat_uikit/tencent_cloud_chat_uikit.dart'; +import 'package:tencent_cloud_chat_uikit/ui/utils/screen_utils.dart'; import 'package:tencent_cloud_chat_uikit/ui/widgets/message_read_receipt.dart'; import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_base.dart'; +import 'package:tencent_cloud_chat_uikit/ui/widgets/wide_popup.dart'; class TIMUIKitMessageReadReceipt extends TIMUIKitStatelessWidget { final V2TimMessage messageItem; - final void Function(String)? onTapAvatar; + final void Function(String, TapDownDetails tapDetails)? onTapAvatar; TIMUIKitMessageReadReceipt( {Key? key, this.onTapAvatar, required this.messageItem}) @@ -20,31 +23,48 @@ class TIMUIKitMessageReadReceipt extends TIMUIKitStatelessWidget { final TUITheme theme = value.theme; final TUIChatSeparateViewModel model = Provider.of(context, listen: false); + final isDesktopScreen = TUIKitScreenUtils.getFormFactor(context) == DeviceType.Desktop; return Selector( builder: (context, value, child) { - if (value == null || value.unreadCount == 0 && value.readCount == 0) { - return Container(); - } + // if (value == null || value.unreadCount == 0 && value.readCount == 0) { + // return Container(); + // } return GestureDetector( behavior: HitTestBehavior.opaque, onTap: () { - if ((value.readCount ?? 0) > 0) { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => MessageReadReceipt( - model: model, - onTapAvatar: onTapAvatar, - messageItem: messageItem, - unreadCount: value.unreadCount ?? 0, - readCount: value.readCount ?? 0))); + if ((value?.readCount ?? 0) > 0) { + if(isDesktopScreen){ + TUIKitWidePopup.showPopupWindow( + operationKey: TUIKitWideModalOperationKey.messageReadDetails, + context: context, + width: MediaQuery.of(context).size.width * 0.5, + height: MediaQuery.of(context).size.height * 0.8, + title: TIM_t("消息详情"), + child: (onClose) => MessageReadReceipt( + model: model, + onTapAvatar: onTapAvatar, + messageItem: messageItem, + unreadCount: value?.unreadCount ?? 0, + readCount: value?.readCount ?? 0) + ); + }else{ + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => MessageReadReceipt( + model: model, + onTapAvatar: onTapAvatar, + messageItem: messageItem, + unreadCount: value?.unreadCount ?? 0, + readCount: value?.readCount ?? 0))); + } } }, child: Container( - padding: - const EdgeInsets.only(bottom: 3, right: 6, left: 6, top: 6), - child: (value.unreadCount == 0 && (value.readCount ?? 0) > 0) + padding: EdgeInsets.only( + bottom: 3, right: 6, left: 6, top: isDesktopScreen ? 2 : 6), + child: ((value?.unreadCount ?? 0) == 0 && (value?.readCount ?? 0) > 0) ? Icon( Icons.check_circle_outline, size: 18, @@ -58,12 +78,12 @@ class TIMUIKitMessageReadReceipt extends TIMUIKitStatelessWidget { shape: BoxShape.circle, border: Border.all( width: 1.3, - color: (value.readCount ?? 0) > 0 + color: (value?.readCount ?? 0) > 0 ? theme.primaryColor! : theme.weakTextColor!)), - child: (value.readCount ?? 0) > 0 + child: (value?.readCount ?? 0) > 0 ? Text( - '${value.readCount}', + '${value?.readCount ?? 0}', textAlign: TextAlign.center, style: TextStyle( fontSize: 8, color: theme.primaryColor), diff --git a/lib/ui/views/TIMUIKitChat/TIMUIKitAppBar/tim_uikit_appbar.dart b/lib/ui/views/TIMUIKitChat/TIMUIKitAppBar/tim_uikit_appbar.dart index 6690568..396a60b 100644 --- a/lib/ui/views/TIMUIKitChat/TIMUIKitAppBar/tim_uikit_appbar.dart +++ b/lib/ui/views/TIMUIKitChat/TIMUIKitAppBar/tim_uikit_appbar.dart @@ -9,6 +9,7 @@ import 'package:tencent_cloud_chat_uikit/data_services/friendShip/friendship_ser import 'package:tencent_cloud_chat_uikit/data_services/group/group_services.dart'; import 'package:tencent_cloud_chat_uikit/data_services/services_locatar.dart'; import 'package:tencent_cloud_chat_uikit/tencent_cloud_chat_uikit.dart'; +import 'package:tencent_cloud_chat_uikit/ui/utils/screen_utils.dart'; import 'package:tencent_cloud_chat_uikit/ui/views/TIMUIKitChat/TIMUIKitAppBar/tim_uikit_appbar_title.dart'; import 'package:tuple/tuple.dart'; @@ -30,7 +31,9 @@ class TIMUIKitAppBar extends StatefulWidget implements PreferredSizeWidget { /// If allow update the conversation show name automatically. final bool isConversationShowNameFixed; - final bool showC2cMessageEditStaus; + final bool showC2cMessageEditStatus; + + final GestureTapDownCallback? onClickTitle; const TIMUIKitAppBar({ Key? key, @@ -39,7 +42,8 @@ class TIMUIKitAppBar extends StatefulWidget implements PreferredSizeWidget { this.showTotalUnReadCount = true, this.conversationID = "", this.conversationShowName = "", - this.showC2cMessageEditStaus = true, + this.showC2cMessageEditStatus = true, + this.onClickTitle, }) : super(key: key); @override @@ -76,9 +80,8 @@ class _TIMUIKitAppBarState extends TIMUIKitState { changedInfo.userProfile?.userID) ?? ""; } + // ignore: empty_catches } catch (e) { - // ignore: avoid_print - print(e); } }, ); @@ -100,9 +103,8 @@ class _TIMUIKitAppBarState extends TIMUIKitState { setState(() {}); } } + // ignore: empty_catches } catch (e) { - // ignore: avoid_print - print(e); } }, ); @@ -159,9 +161,8 @@ class _TIMUIKitAppBarState extends TIMUIKitState { ""; }); } + // ignore: empty_catches } catch (e) { - // ignore: avoid_print - print(e); } } @@ -172,11 +173,16 @@ class _TIMUIKitAppBarState extends TIMUIKitState { final setAppbar = widget.config; final TUIChatSeparateViewModel chatVM = Provider.of(context); + final isDesktopScreen = + TUIKitScreenUtils.getFormFactor(context) == DeviceType.Desktop; return AppBar( - backgroundColor: setAppbar?.backgroundColor ?? theme.chatHeaderBgColor, + backgroundColor: setAppbar?.backgroundColor ?? + theme.chatHeaderBgColor ?? + theme.appbarBgColor ?? + theme.primaryColor, actionsIconTheme: setAppbar?.actionsIconTheme, foregroundColor: setAppbar?.foregroundColor, - elevation: setAppbar?.elevation, + elevation: setAppbar?.elevation ?? (isDesktopScreen ? 0 : 1), bottom: setAppbar?.bottom, bottomOpacity: setAppbar?.bottomOpacity ?? 1.0, titleSpacing: setAppbar?.titleSpacing, @@ -188,46 +194,28 @@ class _TIMUIKitAppBarState extends TIMUIKitState { toolbarOpacity: setAppbar?.toolbarOpacity ?? 1.0, toolbarTextStyle: setAppbar?.toolbarTextStyle, textTheme: setAppbar?.textTheme, - flexibleSpace: setAppbar?.backgroundColor == null - ? theme.chatHeaderBgColor != null - ? setAppbar?.flexibleSpace ?? - Container( - decoration: BoxDecoration( - gradient: LinearGradient(colors: [ - theme.lightPrimaryColor ?? - CommonColor.lightPrimaryColor, - theme.primaryColor ?? CommonColor.primaryColor - ]), - ), - ) - : null - : null, iconTheme: setAppbar?.iconTheme ?? const IconThemeData( color: Colors.white, ), - // title: setAppbar?.title ?? - // Text( - // _conversationShowName, - // style: const TextStyle( - // color: Colors.white, - // fontSize: 17, - // ), - // ), title: TIMUIKitAppBarTitle( title: setAppbar?.title, - textStyle: setAppbar?.textTheme?.titleMedium, + onClick: widget.onClickTitle, + textStyle: setAppbar?.textTheme?.titleMedium ?? + TextStyle( + color: theme.appbarTextColor ?? hexToColor("010000"), + fontSize: 16), conversationShowName: _conversationShowName, - showC2cMessageEditStaus: widget.showC2cMessageEditStaus, + showC2cMessageEditStatus: widget.showC2cMessageEditStatus, fromUser: widget.conversationID, ), - centerTitle: setAppbar?.centerTitle ?? true, - leadingWidth: setAppbar?.leadingWidth ?? 70, + centerTitle: setAppbar?.centerTitle ?? (isDesktopScreen ? false : true), + leadingWidth: setAppbar?.leadingWidth ?? (isDesktopScreen ? 8 : 70), leading: Selector>( builder: (context, data, _) { final isMultiSelect = data.item1; final unReadCount = data.item2; - return isMultiSelect + return (!isDesktopScreen && isMultiSelect) ? TextButton( onPressed: () { chatVM.updateMultiSelectStatus(false); @@ -235,42 +223,49 @@ class _TIMUIKitAppBarState extends TIMUIKitState { child: Text( TIM_t('取消'), style: setAppbar?.textTheme?.titleMedium ?? - const TextStyle( - color: Colors.white, + TextStyle( + color: + theme.appbarTextColor ?? hexToColor("010000"), fontSize: 16, ), ), ) : setAppbar?.leading ?? - Row( - children: [ - IconButton( - padding: const EdgeInsets.only(left: 16), - constraints: const BoxConstraints(), - icon: Icon( - Icons.arrow_back_ios, - color: setAppbar?.textTheme?.titleMedium?.color ?? - Colors.white, - size: 17, - ), - onPressed: () async { - chatVM.repliedMessage = null; - Navigator.pop(context); - }, - ), - if (widget.showTotalUnReadCount && unReadCount > 0) - Container( - width: 22, - height: 22, - alignment: Alignment.center, - decoration: BoxDecoration( - shape: BoxShape.circle, - color: theme.cautionColor, - ), - child: Text(_getTotalUnReadCount(unReadCount)), - ), - ], - ); + (isDesktopScreen + ? Container() + : Row( + children: [ + IconButton( + padding: const EdgeInsets.only(left: 16), + constraints: const BoxConstraints(), + icon: Icon( + Icons.arrow_back_ios, + color: setAppbar + ?.textTheme?.titleMedium?.color ?? + theme.appbarTextColor ?? + hexToColor("010000"), + size: 17, + ), + onPressed: () async { + chatVM.repliedMessage = null; + Navigator.pop(context); + }, + ), + if (widget.showTotalUnReadCount && + unReadCount > 0) + Container( + width: 22, + height: 22, + alignment: Alignment.center, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: theme.cautionColor, + ), + child: + Text(_getTotalUnReadCount(unReadCount)), + ), + ], + )); }, shouldRebuild: (prev, next) => prev.item1 != next.item1 || prev.item2 != next.item2, diff --git a/lib/ui/views/TIMUIKitChat/TIMUIKitAppBar/tim_uikit_appbar_title.dart b/lib/ui/views/TIMUIKitChat/TIMUIKitAppBar/tim_uikit_appbar_title.dart index 08b8450..92126d2 100644 --- a/lib/ui/views/TIMUIKitChat/TIMUIKitAppBar/tim_uikit_appbar_title.dart +++ b/lib/ui/views/TIMUIKitChat/TIMUIKitAppBar/tim_uikit_appbar_title.dart @@ -6,8 +6,9 @@ import 'package:tencent_cloud_chat_uikit/business_logic/view_models/tui_chat_glo class TIMUIKitAppBarTitle extends StatelessWidget { final Widget? title; final String conversationShowName; - final bool showC2cMessageEditStaus; + final bool showC2cMessageEditStatus; final String fromUser; + final GestureTapDownCallback? onClick; final TextStyle? textStyle; const TIMUIKitAppBarTitle( @@ -15,10 +16,24 @@ class TIMUIKitAppBarTitle extends StatelessWidget { this.title, this.textStyle, required this.conversationShowName, - required this.showC2cMessageEditStaus, - required this.fromUser}) + required this.showC2cMessageEditStatus, + required this.fromUser, this.onClick}) : super(key: key); + Widget titleText(String text){ + return InkWell( + onTapDown: onClick, + child: Text( + text, + style: textStyle ?? + const TextStyle( + color: Colors.white, + fontSize: 17, + ), + ), + ); + } + // String conversationShowName; @override Widget build(BuildContext context) { @@ -28,36 +43,19 @@ class TIMUIKitAppBarTitle extends StatelessWidget { if (title != null) { return title!; } - return Text( - conversationShowName, - style: textStyle ?? - const TextStyle( - color: Colors.white, - fontSize: 17, - ), - ); + return titleText(conversationShowName,); } else { - if (showC2cMessageEditStaus) { - return Text( - TIM_t("对方正在输入中..."), - style: textStyle ?? - const TextStyle( - color: Colors.white, - fontSize: 17, - ), - ); + if (showC2cMessageEditStatus) { + return titleText( + TIM_t("对方正在输入中..."),); + } else { if (title != null) { return title!; } - return Text( - conversationShowName, - style: textStyle ?? - const TextStyle( - color: Colors.white, - fontSize: 17, - ), - ); + return titleText( + conversationShowName,); + } } } diff --git a/lib/ui/views/TIMUIKitChat/TIMUIKitMessageItem/TIMUIKitMessageReaction/tim_uikit_message_reaction_detail.dart b/lib/ui/views/TIMUIKitChat/TIMUIKitMessageItem/TIMUIKitMessageReaction/tim_uikit_message_reaction_detail.dart index 4b73b59..d69f4b5 100644 --- a/lib/ui/views/TIMUIKitChat/TIMUIKitMessageItem/TIMUIKitMessageReaction/tim_uikit_message_reaction_detail.dart +++ b/lib/ui/views/TIMUIKitChat/TIMUIKitMessageItem/TIMUIKitMessageReaction/tim_uikit_message_reaction_detail.dart @@ -20,7 +20,7 @@ class TIMUIKitMessageReactionDetail extends StatefulWidget { /// the sticker list from message reaction final List stickerList; - final Function(String userID)? onTapAvatar; + final Function(String userID, TapDownDetails tapDetails)? onTapAvatar; const TIMUIKitMessageReactionDetail( {required this.currentStickerIndex, @@ -42,7 +42,7 @@ class TIMUIKitMessageReactionDetailState serviceLocator(); Widget getUserItem( - String userID, TUITheme theme, Function(String userID)? onTapAvatar) { + String userID, TUITheme theme, Function(String userID, TapDownDetails tapDetails)? onTapAvatar) { V2TimGroupMemberFullInfo? memberInfo; String showName = userID; try { @@ -66,11 +66,11 @@ class TIMUIKitMessageReactionDetailState // e } - return GestureDetector( - onTap: () { + return InkWell( + onTapDown: (tapDetails) { if (onTapAvatar != null) { if (userID != selfInfoModel.loginInfo?.userID) { - onTapAvatar(userID); + onTapAvatar(userID, tapDetails); } } }, @@ -137,7 +137,7 @@ class TIMUIKitMessageReactionDetailState } Widget getStickerNameList( - int sticker, TUITheme theme, Function(String userID)? onTapAvatar) { + int sticker, TUITheme theme, Function(String userID, TapDownDetails tapDetails)? onTapAvatar) { final nameList = widget.messageReaction[sticker.toString()]; return SingleChildScrollView( child: Column( diff --git a/lib/ui/views/TIMUIKitChat/TIMUIKitMessageItem/TIMUIKitMessageReaction/tim_uikit_message_reaction_select_emoji.dart b/lib/ui/views/TIMUIKitChat/TIMUIKitMessageItem/TIMUIKitMessageReaction/tim_uikit_message_reaction_select_emoji.dart index 9adbd8b..323ee05 100644 --- a/lib/ui/views/TIMUIKitChat/TIMUIKitMessageItem/TIMUIKitMessageReaction/tim_uikit_message_reaction_select_emoji.dart +++ b/lib/ui/views/TIMUIKitChat/TIMUIKitMessageItem/TIMUIKitMessageReaction/tim_uikit_message_reaction_select_emoji.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_base.dart'; import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_state.dart'; import 'package:tencent_cloud_chat_uikit/tencent_cloud_chat_uikit.dart'; +import 'package:tencent_cloud_chat_uikit/ui/utils/screen_utils.dart'; import 'package:tencent_cloud_chat_uikit/ui/views/TIMUIKitChat/TIMUIKitMessageItem/TIMUIKitMessageReaction/message_reaction_emoji.dart'; import 'package:tencent_cloud_chat_uikit/ui/views/TIMUIKitChat/TIMUIKitTextField/tim_uikit_emoji_panel.dart' as emoji; @@ -33,6 +34,7 @@ class TIMUIKitMessageReactionEmojiSelectPanelState _buildSimplePanel(TUITheme theme) { final List> emojiData = messageReactionEmojiData; + final isDesktopScreen = TUIKitScreenUtils.getFormFactor() == DeviceType.Desktop; return Material( color: Colors.white, child: ExtendedWrap( @@ -41,7 +43,8 @@ class TIMUIKitMessageReactionEmojiSelectPanelState crossAxisAlignment: WrapCrossAlignment.center, runSpacing: 24, children: [ - GestureDetector( + if(!isDesktopScreen) + GestureDetector( onTap: () { widget.onClickShowMore(!widget.isShowMoreSticker); }, diff --git a/lib/ui/views/TIMUIKitChat/TIMUIKitMessageItem/TIMUIKitMessageReaction/tim_uikit_message_reaction_show_item.dart b/lib/ui/views/TIMUIKitChat/TIMUIKitMessageItem/TIMUIKitMessageReaction/tim_uikit_message_reaction_show_item.dart index 89ec53f..2506e71 100644 --- a/lib/ui/views/TIMUIKitChat/TIMUIKitMessageItem/TIMUIKitMessageReaction/tim_uikit_message_reaction_show_item.dart +++ b/lib/ui/views/TIMUIKitChat/TIMUIKitMessageItem/TIMUIKitMessageReaction/tim_uikit_message_reaction_show_item.dart @@ -78,7 +78,7 @@ class TIMUIKitMessageReactionShowItem extends TIMUIKitStatelessWidget { mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.center, children: [ - GestureDetector( + InkWell( onTap: clickOnCurrentSticker, child: Container( margin: EdgeInsets.only( @@ -87,8 +87,8 @@ class TIMUIKitMessageReactionShowItem extends TIMUIKitStatelessWidget { child: Text( String.fromCharCode(sticker), style: TextStyle( - fontSize: (!PlatformUtils().isIOS) ? 12 : 16, - ), + fontSize: (!PlatformUtils().isIOS) ? 12 : 16, + color: hexToColor("f9453d")), ), ), ), @@ -145,11 +145,11 @@ class TIMUIKitMessageReactionShowItem extends TIMUIKitStatelessWidget { // e } } - return GestureDetector( - onTap: () { + return InkWell( + onTapDown: (tapDetails) { if (model.onTapAvatar != null) { if (e != selfInfoModel.loginInfo?.userID) { - model.onTapAvatar!(e); + model.onTapAvatar!(e, tapDetails); } } }, diff --git a/lib/ui/views/TIMUIKitChat/TIMUIKitMessageItem/TIMUIKitMessageReaction/tim_uikit_message_reaction_utils.dart b/lib/ui/views/TIMUIKitChat/TIMUIKitMessageItem/TIMUIKitMessageReaction/tim_uikit_message_reaction_utils.dart index ee82e42..041dc9f 100644 --- a/lib/ui/views/TIMUIKitChat/TIMUIKitMessageItem/TIMUIKitMessageReaction/tim_uikit_message_reaction_utils.dart +++ b/lib/ui/views/TIMUIKitChat/TIMUIKitMessageItem/TIMUIKitMessageReaction/tim_uikit_message_reaction_utils.dart @@ -1,6 +1,6 @@ import 'dart:convert'; -import 'package:tencent_im_base/tencent_im_base.dart'; +import 'package:tencent_cloud_chat_uikit/tencent_cloud_chat_uikit.dart'; import 'package:tencent_cloud_chat_uikit/business_logic/view_models/tui_self_info_view_model.dart'; import 'package:tencent_cloud_chat_uikit/data_services/message/message_services.dart'; import 'package:tencent_cloud_chat_uikit/data_services/services_locatar.dart'; @@ -16,8 +16,10 @@ class MessageReactionUtils { static CloudCustomData getCloudCustomData(V2TimMessage message) { CloudCustomData messageCloudCustomData; try { - messageCloudCustomData = - CloudCustomData.fromJson(json.decode(message.cloudCustomData!)); + messageCloudCustomData = CloudCustomData.fromJson(json.decode( + TencentUtils.checkString(message.cloudCustomData) != null + ? message.cloudCustomData! + : "{}")); } catch (e) { messageCloudCustomData = CloudCustomData(); } diff --git a/lib/ui/views/TIMUIKitChat/TIMUIKitMessageItem/TIMUIKitMessageReaction/tim_uikit_message_reaction_wrapper.dart b/lib/ui/views/TIMUIKitChat/TIMUIKitMessageItem/TIMUIKitMessageReaction/tim_uikit_message_reaction_wrapper.dart index da94359..3387b40 100644 --- a/lib/ui/views/TIMUIKitChat/TIMUIKitMessageItem/TIMUIKitMessageReaction/tim_uikit_message_reaction_wrapper.dart +++ b/lib/ui/views/TIMUIKitChat/TIMUIKitMessageItem/TIMUIKitMessageReaction/tim_uikit_message_reaction_wrapper.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'dart:convert'; import 'package:flutter/material.dart'; +import 'package:tencent_cloud_chat_uikit/ui/utils/common_utils.dart'; import 'package:tencent_im_base/tencent_im_base.dart'; import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_base.dart'; import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_state.dart'; @@ -26,7 +27,7 @@ class TIMUIKitMessageReactionWrapper extends StatefulWidget { this.clearJump, required this.isFromSelf, this.backgroundColor, - required this.chatModel, + required this.chatModel, required this.message, this.borderRadius, required this.child, @@ -76,8 +77,10 @@ class _TIMUIKitMessageReactionWrapperState Map messageReaction = {}; CloudCustomData messageCloudCustomData; try { - messageCloudCustomData = CloudCustomData.fromJson( - json.decode(widget.message.cloudCustomData!)); + messageCloudCustomData = CloudCustomData.fromJson(json.decode( + TencentUtils.checkString(widget.message.cloudCustomData) != null + ? widget.message.cloudCustomData! + : "{}")); } catch (e) { messageCloudCustomData = CloudCustomData(); } @@ -134,7 +137,7 @@ class _TIMUIKitMessageReactionWrapperState } else { if ((widget.chatModel.jumpMsgID == widget.message.msgID) && (widget.message.msgID?.isNotEmpty ?? false)) { - if (widget.clearJump != null) { + if(widget.clearJump != null){ widget.clearJump!(); } } diff --git a/lib/ui/views/TIMUIKitChat/TIMUIKitMessageItem/tim_uikit_chat_face_elem.dart b/lib/ui/views/TIMUIKitChat/TIMUIKitMessageItem/tim_uikit_chat_face_elem.dart index fed5487..fd10412 100644 --- a/lib/ui/views/TIMUIKitChat/TIMUIKitMessageItem/tim_uikit_chat_face_elem.dart +++ b/lib/ui/views/TIMUIKitChat/TIMUIKitMessageItem/tim_uikit_chat_face_elem.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:tencent_cloud_chat_uikit/ui/utils/screen_utils.dart'; import 'package:tencent_im_base/tencent_im_base.dart'; import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_base.dart'; import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_state.dart'; @@ -28,10 +29,6 @@ class TIMUIKitFaceElem extends StatefulWidget { } class _TIMUIKitTextElemState extends TIMUIKitState { - @override - void initState() { - super.initState(); - } bool isFromNetwork() { return widget.path.startsWith('http'); @@ -51,8 +48,9 @@ class _TIMUIKitTextElemState extends TIMUIKitState { @override Widget tuiBuild(BuildContext context, TUIKitBuildValue value) { + final isDesktopScreen = TUIKitScreenUtils.getFormFactor(context) == DeviceType.Desktop; return TIMUIKitMessageReactionWrapper( - chatModel: widget.model, + chatModel: widget.model, isShowJump: widget.isShowJump, isFromSelf: widget.message.isSelf ?? true, clearJump: widget.clearJump, @@ -61,7 +59,7 @@ class _TIMUIKitTextElemState extends TIMUIKitState { child: Container( padding: const EdgeInsets.all(10), constraints: - BoxConstraints(maxWidth: MediaQuery.of(context).size.width * 0.3), + BoxConstraints(maxWidth: MediaQuery.of(context).size.width * (isDesktopScreen ? 0.1 : 0.3)), child: isFromNetwork() ? Image.network(widget.path) : Image.asset(createPathFromNative(widget.path)), diff --git a/lib/ui/views/TIMUIKitChat/TIMUIKitMessageItem/tim_uikit_chat_file_elem.dart b/lib/ui/views/TIMUIKitChat/TIMUIKitMessageItem/tim_uikit_chat_file_elem.dart index 19b8474..f121465 100644 --- a/lib/ui/views/TIMUIKitChat/TIMUIKitMessageItem/tim_uikit_chat_file_elem.dart +++ b/lib/ui/views/TIMUIKitChat/TIMUIKitMessageItem/tim_uikit_chat_file_elem.dart @@ -3,17 +3,18 @@ import 'dart:io'; import 'dart:math'; +import 'package:device_info_plus/device_info_plus.dart'; import 'package:flutter/material.dart'; +import 'package:tencent_cloud_chat_uikit/tencent_cloud_chat_uikit.dart'; import 'package:tencent_cloud_chat_uikit/ui/utils/permission.dart'; import 'package:tencent_open_file/tencent_open_file.dart'; -import 'package:permission_handler/permission_handler.dart'; import 'package:provider/provider.dart'; -import 'package:tencent_im_base/tencent_im_base.dart'; import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_base.dart'; import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_state.dart'; import 'package:tencent_cloud_chat_uikit/business_logic/separate_models/tui_chat_separate_view_model.dart'; import 'package:tencent_cloud_chat_uikit/business_logic/view_models/tui_chat_global_model.dart'; import 'package:tencent_cloud_chat_uikit/data_services/services_locatar.dart'; + import 'package:tencent_cloud_chat_uikit/ui/utils/platform.dart'; import 'package:tencent_cloud_chat_uikit/ui/views/TIMUIKitChat/TIMUIKitMessageItem/TIMUIKitMessageReaction/tim_uikit_message_reaction_wrapper.dart'; import 'package:tencent_cloud_chat_uikit/ui/views/TIMUIKitChat/TIMUIKitMessageItem/tim_uikit_chat_file_icon.dart'; @@ -75,23 +76,20 @@ class _TIMUIKitFileElemState extends TIMUIKitState { } if (model.getMessageProgress(widget.messageID) == 100) { - String savePath = widget.message.fileElem!.localUrl ?? + String savePath = TencentUtils.checkString(widget.message.fileElem!.localUrl) ?? model.getFileMessageLocation(widget.messageID); File f = File(savePath); if (f.existsSync() && widget.messageID != null) { filePath = savePath; - // model.setFileMessageLocation(widget.messageID!, filePath); return true; } return false; } - // String savePath = await getSavePath(); String savePath = widget.message.fileElem!.localUrl ?? ''; File f = File(savePath); if (f.existsSync() && widget.messageID != null) { filePath = savePath; model.setMessageProgress(widget.messageID!, 100); - // model.setFileMessageLocation(widget.messageID!, filePath); return true; } return false; @@ -110,8 +108,10 @@ class _TIMUIKitFileElemState extends TIMUIKitState { } addUrlToWaitingPath() async { - model.addWaitingList(widget.messageID!); - print("add path success"); + if(widget.messageID !=null ){ + model.addWaitingList(widget.messageID!); + print("add path success"); + } } checkIsWaiting() { @@ -127,22 +127,59 @@ class _TIMUIKitFileElemState extends TIMUIKitState { } downloadFile(TUITheme theme) async { - if (!await Permissions.checkPermission( - context, Permission.storage.value, theme)) { - return; + if(PlatformUtils().isMobile){ + if (PlatformUtils().isIOS) { + if (!await Permissions.checkPermission( + context, Permission.photosAddOnly.value, theme, false)) { + return; + } + } else { + final DeviceInfoPlugin deviceInfo = DeviceInfoPlugin(); + AndroidDeviceInfo androidInfo = await deviceInfo.androidInfo; + if ((androidInfo.version.sdkInt ?? 0) >= 33) { + } else { + var storage = await Permissions.checkPermission( + context, Permission.storage.value, + ); + if(!storage){ + return; + } + } + } } await model.downloadFile(); } tryOpenFile(context, theme) async { - if (!await Permissions.checkPermission( - context, Permission.storage.value, theme)) { - return; + if(PlatformUtils().isMobile){ + if (PlatformUtils().isIOS) { + if (!await Permissions.checkPermission( + context, Permission.photosAddOnly.value, theme!, false)) { + return; + } + } else { + final DeviceInfoPlugin deviceInfo = DeviceInfoPlugin(); + AndroidDeviceInfo androidInfo = await deviceInfo.androidInfo; + if ((androidInfo.version.sdkInt ?? 0) >= 33) { + } else { + var storage = await Permissions.checkPermission( + context, Permission.storage.value, + ); + if(!storage){ + return; + } + } + } } + try { - OpenFile.open(filePath); + if(PlatformUtils().isDesktop && !PlatformUtils().isWindows){ + launchUrl(Uri.file(filePath)); + }else{ + OpenFile.open(filePath); + } + // ignore: empty_catches } catch (e) { - print(e); } } @@ -181,7 +218,7 @@ class _TIMUIKitFileElemState extends TIMUIKitState { fileFormat = fileName.split(".")[max(fileName.split(".").length - 1, 0)]; } - return GestureDetector( + return InkWell( onTap: () async { if (PlatformUtils().isWeb) { launchUrl( diff --git a/lib/ui/views/TIMUIKitChat/TIMUIKitMessageItem/tim_uikit_chat_file_icon.dart b/lib/ui/views/TIMUIKitChat/TIMUIKitMessageItem/tim_uikit_chat_file_icon.dart index ebf586a..e2853cb 100644 --- a/lib/ui/views/TIMUIKitChat/TIMUIKitMessageItem/tim_uikit_chat_file_icon.dart +++ b/lib/ui/views/TIMUIKitChat/TIMUIKitMessageItem/tim_uikit_chat_file_icon.dart @@ -7,8 +7,9 @@ import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_statelesswidget class TIMUIKitFileIcon extends TIMUIKitStatelessWidget { final String? fileFormat; + final double? size; - TIMUIKitFileIcon({this.fileFormat, Key? key}) : super(key: key); + TIMUIKitFileIcon( {this.size, this.fileFormat, Key? key}) : super(key: key); Map fileMap = { "doc": "images/word.png", @@ -34,6 +35,11 @@ class TIMUIKitFileIcon extends TIMUIKitStatelessWidget { "tif": "images/image_icon.png", "wmf": "images/image_icon.png", "dib": "images/image_icon.png", + "mp4": "images/video_icon.png", + "avi": "images/video_icon.png", + "mov": "images/video_icon.png", + "wmv": "images/video_icon.png", + "flv": "images/video_icon.png", }; Widget _getFileIcon() { @@ -46,8 +52,8 @@ class TIMUIKitFileIcon extends TIMUIKitStatelessWidget { @override Widget tuiBuild(BuildContext context, TUIKitBuildValue value) { return SizedBox( - height: 50, - width: 50, + height: size ?? 50, + width: size ?? 50, child: Container(padding: const EdgeInsets.all(4), child: _getFileIcon()), ); } diff --git a/lib/ui/views/TIMUIKitChat/TIMUIKitMessageItem/tim_uikit_chat_image_elem.dart b/lib/ui/views/TIMUIKitChat/TIMUIKitMessageItem/tim_uikit_chat_image_elem.dart index 0b44e04..2b1eed1 100644 --- a/lib/ui/views/TIMUIKitChat/TIMUIKitMessageItem/tim_uikit_chat_image_elem.dart +++ b/lib/ui/views/TIMUIKitChat/TIMUIKitMessageItem/tim_uikit_chat_image_elem.dart @@ -3,24 +3,24 @@ import 'dart:async'; import 'dart:convert'; import 'package:crypto/crypto.dart'; +import 'package:device_info_plus/device_info_plus.dart'; import 'package:http/http.dart' as http; import 'package:tencent_cloud_chat_uikit/business_logic/separate_models/tui_chat_separate_view_model.dart'; import 'package:tencent_cloud_chat_uikit/data_services/message/message_services.dart'; +import 'package:tencent_cloud_chat_uikit/ui/utils/screen_utils.dart'; +import 'package:tencent_open_file/tencent_open_file.dart'; import 'package:universal_html/html.dart' as html; import 'dart:io'; import 'dart:math'; import 'dart:typed_data'; import 'package:cached_network_image/cached_network_image.dart'; - import 'package:flutter/material.dart'; import 'package:loading_animation_widget/loading_animation_widget.dart'; import 'package:path_provider/path_provider.dart'; import 'package:provider/provider.dart'; import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_base.dart'; import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_state.dart'; - import 'package:tencent_cloud_chat_uikit/business_logic/view_models/tui_chat_global_model.dart'; - import 'package:tencent_cloud_chat_uikit/data_services/services_locatar.dart'; import 'package:tencent_cloud_chat_uikit/tencent_cloud_chat_uikit.dart'; import 'package:tencent_cloud_chat_uikit/ui/constants/history_message_constant.dart'; @@ -31,6 +31,7 @@ import 'package:tencent_cloud_chat_uikit/ui/views/TIMUIKitChat/TIMUIKitMessageIt import 'package:tencent_cloud_chat_uikit/ui/widgets/image_screen.dart'; import 'package:transparent_image/transparent_image.dart'; import 'package:image_gallery_saver/image_gallery_saver.dart'; +import 'package:url_launcher/url_launcher.dart'; class TIMUIKitImageElem extends StatefulWidget { final V2TimMessage message; @@ -79,10 +80,11 @@ class _TIMUIKitImageElem extends TIMUIKitState { decoration: BoxDecoration( borderRadius: const BorderRadius.all(Radius.circular(5)), border: Border.all( - width: 1, - color: theme?.black ?? Colors.white, + width: 2, + color: theme?.weakDividerColor ?? Colors.grey, )), - height: 100, + height: 170, + width: 170, child: Center( child: Row( mainAxisAlignment: MainAxisAlignment.center, @@ -135,9 +137,27 @@ class _TIMUIKitImageElem extends TIMUIKitState { return; } } else { - if (!await Permissions.checkPermission( - context, Permission.storage.value, theme!)) { - return; + final DeviceInfoPlugin deviceInfo = DeviceInfoPlugin(); + AndroidDeviceInfo androidInfo = await deviceInfo.androidInfo; + if (PlatformUtils().isMobile) { + if ((androidInfo.version.sdkInt ?? 0) >= 33) { + final photos = await Permissions.checkPermission( + context, + Permission.photos.value, + theme, + ); + if (!photos) { + return; + } + } else { + final storage = await Permissions.checkPermission( + context, + Permission.storage.value, + ); + if (!storage) { + return; + } + } } } @@ -233,7 +253,8 @@ class _TIMUIKitImageElem extends TIMUIKitState { if (path != null && PlatformUtils().isWeb ? true : File(path!).existsSync()) { - return await _saveImageToLocal(context, path, isAsset: true, theme: theme); + return await _saveImageToLocal(context, path, + isAsset: true, theme: theme); } else { String imgUrl = getBigPicUrl(); if (widget.message.imageElem!.imageList![0]!.localUrl != '' && @@ -288,18 +309,22 @@ class _TIMUIKitImageElem extends TIMUIKitState { child: errorDisplay(context, theme), )); - Widget _renderLocalImage( - String imgPath, dynamic heroTag, double positionRadio, TUITheme? theme) { - double? currentPositionRadio; + Widget _renderLocalImage(String smallImage, dynamic heroTag, + double? positionRadio, TUITheme? theme, String? originImage) { + double? currentPositionRadio = positionRadio; + File imgF = File(smallImage); - File imgF = File(imgPath); bool isExist = imgF.existsSync(); - if (!isExist) { return errorDisplay(context, theme); } + Image image = Image.file(imgF); + String showImage = (originImage != null && File(originImage).existsSync()) + ? originImage + : smallImage; + image.image .resolve(const ImageConfiguration()) .addListener(ImageStreamListener((image, synchronousCall) { @@ -311,30 +336,42 @@ class _TIMUIKitImageElem extends TIMUIKitState { final preloadImage = model.preloadImageMap[ message.seq! + message.timestamp.toString() + (message.msgID ?? "")]; + final isDesktopScreen = + TUIKitScreenUtils.getFormFactor(context) == DeviceType.Desktop; + return Stack( alignment: AlignmentDirectional.topStart, children: [ - AspectRatio( - aspectRatio: currentPositionRadio ?? positionRadio, - child: Container( - decoration: const BoxDecoration(color: Colors.transparent), + if (!isDesktopScreen && currentPositionRadio != null) + AspectRatio( + aspectRatio: currentPositionRadio!, + child: Container( + decoration: const BoxDecoration(color: Colors.transparent), + ), ), - ), getImage( - GestureDetector( + InkWell( onTap: () { - Navigator.of(context).push( - PageRouteBuilder( - opaque: false, // set to false - pageBuilder: (_, __, ___) => ImageScreen( - imageProvider: FileImage(File(imgPath)), - heroTag: heroTag, - messageID: widget.message.msgID, - downloadFn: () async { - return await _saveImg(theme!); - }), - ), - ); + if (PlatformUtils().isDesktop) { + if(PlatformUtils().isWindows){ + OpenFile.open(showImage); + } else{ + launchUrl(Uri.file(showImage)); + } + } else { + Navigator.of(context).push( + PageRouteBuilder( + opaque: false, // set to false + pageBuilder: (_, __, ___) => ImageScreen( + imageProvider: FileImage(File(showImage)), + heroTag: heroTag, + messageID: widget.message.msgID, + downloadFn: () async { + return await _saveImg(theme!); + }), + ), + ); + } }, child: Container( constraints: @@ -344,13 +381,11 @@ class _TIMUIKitImageElem extends TIMUIKitState { child: preloadImage != null ? RawImage( image: preloadImage, - fit: BoxFit.fitWidth, - width: double.infinity, + fit: BoxFit.contain, ) : Image.file( - File(imgPath), - fit: BoxFit.fitWidth, - width: double.infinity, + File(smallImage), + fit: BoxFit.contain, ), ), )), @@ -406,35 +441,45 @@ class _TIMUIKitImageElem extends TIMUIKitState { } Widget _renderNetworkImage( - dynamic heroTag, double positionRadio, TUITheme? theme, + dynamic heroTag, double? positionRadio, TUITheme? theme, {String? path, V2TimImage? originalImg, V2TimImage? smallImg}) { try { + final isDesktopScreen = + TUIKitScreenUtils.getFormFactor(context) == DeviceType.Desktop; String bigImgUrl = originalImg?.url ?? getBigPicUrl(); if (bigImgUrl.isEmpty && smallImg?.url != null) { bigImgUrl = smallImg!.url!; } return Stack( - alignment: widget.message.isSelf ?? false + alignment: widget.message.isSelf ?? true ? AlignmentDirectional.topEnd : AlignmentDirectional.topStart, children: [ getImage( GestureDetector( onTap: () { - Navigator.of(context).push( - PageRouteBuilder( - opaque: false, // set to false - pageBuilder: (_, __, ___) => ImageScreen( - imageProvider: CachedNetworkImageProvider( - path ?? bigImgUrl, - cacheKey: widget.message.msgID, - ), - heroTag: heroTag, - messageID: widget.message.msgID, - downloadFn: () async { - return await _saveImg(theme!); - })), - ); + if (isDesktopScreen) { + onTIMCallback(TIMCallback( + infoCode: 6660414, + infoRecommendText: TIM_t("正在下载中"), + type: TIMCallbackType.INFO + )); + } else { + Navigator.of(context).push( + PageRouteBuilder( + opaque: false, // set to false + pageBuilder: (_, __, ___) => ImageScreen( + imageProvider: CachedNetworkImageProvider( + path ?? bigImgUrl, + cacheKey: widget.message.msgID, + ), + heroTag: heroTag, + messageID: widget.message.msgID, + downloadFn: () async { + return await _saveImg(theme!); + })), + ); + } }, child: Container( constraints: @@ -474,13 +519,13 @@ class _TIMUIKitImageElem extends TIMUIKitState { bool isNeedShowLocalPath() { final current = (DateTime.now().millisecondsSinceEpoch / 1000).ceil(); final timeStamp = widget.message.timestamp ?? current; - return (widget.message.isSelf ?? false) && + return (widget.message.isSelf ?? true) && (isSent || current - timeStamp < 300); } Widget? _renderImage(dynamic heroTag, TUITheme? theme, {V2TimImage? originalImg, V2TimImage? smallImg}) { - double positionRadio = 1.0; + double? positionRadio; if (smallImg?.width != null && smallImg?.height != null && smallImg?.width != 0 && @@ -501,8 +546,12 @@ class _TIMUIKitImageElem extends TIMUIKitState { widget.message.imageElem!.path != null && widget.message.imageElem!.path!.isNotEmpty && File(widget.message.imageElem!.path!).existsSync())) { - return _renderLocalImage(widget.message.imageElem!.path!, heroTag, - networkImagePositionRadio ?? positionRadio, theme); + return _renderLocalImage( + widget.message.imageElem!.path!, + heroTag, + networkImagePositionRadio ?? positionRadio, + theme, + widget.message.imageElem!.path!); } } catch (e) { // ignore: avoid_print @@ -513,15 +562,18 @@ class _TIMUIKitImageElem extends TIMUIKitState { if (smallImg?.localUrl != null && smallImg?.localUrl != "" && File((smallImg?.localUrl!)!).existsSync()) { - return _renderLocalImage(smallImg!.localUrl!, heroTag, - networkImagePositionRadio ?? positionRadio, theme); + return _renderLocalImage(smallImg!.localUrl!, heroTag, positionRadio, + theme, originalImg?.localUrl); } } catch (e) { // ignore: avoid_print print(e); + return _renderNetworkImage(heroTag, positionRadio, theme, + smallImg: smallImg, originalImg: originalImg); } - if ((smallImg?.url ?? originalImg?.url) != null && + if ( + (smallImg?.url ?? originalImg?.url) != null && (smallImg?.url ?? originalImg?.url)!.isNotEmpty) { return _renderNetworkImage(heroTag, positionRadio, theme, smallImg: smallImg, originalImg: originalImg); diff --git a/lib/ui/views/TIMUIKitChat/TIMUIKitMessageItem/tim_uikit_chat_reply_elem.dart b/lib/ui/views/TIMUIKitChat/TIMUIKitMessageItem/tim_uikit_chat_reply_elem.dart index 4b3336b..fb8c83a 100644 --- a/lib/ui/views/TIMUIKitChat/TIMUIKitMessageItem/tim_uikit_chat_reply_elem.dart +++ b/lib/ui/views/TIMUIKitChat/TIMUIKitMessageItem/tim_uikit_chat_reply_elem.dart @@ -2,22 +2,20 @@ import 'dart:async'; import 'dart:convert'; - import 'package:flutter/material.dart'; +import 'package:tencent_cloud_chat_uikit/ui/utils/common_utils.dart'; +import 'package:tencent_cloud_chat_uikit/ui/utils/screen_utils.dart'; +import 'package:tencent_cloud_chat_uikit/ui/views/TIMUIKitChat/TIMUIKitTextField/special_text/DefaultSpecialTextSpanBuilder.dart'; +import 'package:tencent_extended_text/extended_text.dart'; import 'package:tencent_im_base/tencent_im_base.dart'; import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_base.dart'; import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_state.dart'; import 'package:tencent_cloud_chat_uikit/business_logic/separate_models/tui_chat_separate_view_model.dart'; import 'package:tencent_cloud_chat_uikit/business_logic/view_models/tui_chat_global_model.dart'; - import 'package:tencent_cloud_chat_uikit/data_services/services_locatar.dart'; - - import 'package:tencent_cloud_chat_uikit/ui/views/TIMUIKitChat/TIMUIKitMessageItem/TIMUIKitMessageReaction/tim_uikit_message_reaction_show_panel.dart'; import 'package:tencent_cloud_chat_uikit/ui/views/TIMUIKitChat/TIMUIKitMessageItem/main.dart'; import 'package:tencent_cloud_chat_uikit/ui/views/TIMUIKitChat/TIMUIKitMessageItem/tim_uikit_chat_face_elem.dart'; - - import 'package:tencent_cloud_chat_uikit/ui/views/TIMUIKitChat/tim_uikit_chat_config.dart'; import 'package:tencent_cloud_chat_uikit/ui/views/TIMUIKitChat/tim_uikit_cloud_custom_data.dart'; import 'package:tencent_cloud_chat_uikit/ui/widgets/link_preview/link_preview_entry.dart'; @@ -35,6 +33,8 @@ class TIMUIKitReplyElem extends StatefulWidget { final EdgeInsetsGeometry? textPadding; final TUIChatSeparateViewModel chatModel; final bool? isShowMessageReaction; + final bool isUseDefaultEmoji; + final List customEmojiStickerList; const TIMUIKitReplyElem({ Key? key, @@ -47,6 +47,8 @@ class TIMUIKitReplyElem extends StatefulWidget { this.isShowMessageReaction, this.backgroundColor, this.textPadding, + this.isUseDefaultEmoji = false, + this.customEmojiStickerList = const [], required this.chatModel, }) : super(key: key); @@ -63,7 +65,10 @@ class _TIMUIKitReplyElemState extends TIMUIKitState { MessageRepliedData? _getRepliedMessage() { try { final CloudCustomData messageCloudCustomData = CloudCustomData.fromJson( - json.decode(widget.message.cloudCustomData!)); + json.decode( + TencentUtils.checkString(widget.message.cloudCustomData) != null + ? widget.message.cloudCustomData! + : "{}")); if (messageCloudCustomData.messageReply != null) { final MessageRepliedData repliedMessage = MessageRepliedData.fromJson(messageCloudCustomData.messageReply!); @@ -81,14 +86,14 @@ class _TIMUIKitReplyElemState extends TIMUIKitState { final messageID = cloudCustomData.messageID; final message = await widget.chatModel.findMessage(messageID); if (message != null) { - if(mounted){ + if (mounted) { setState(() { rawMessage = message; }); } } } - if(mounted){ + if (mounted) { setState(() { repliedMessage = cloudCustomData; }); @@ -104,17 +109,17 @@ class _TIMUIKitReplyElemState extends TIMUIKitState { } _rawMessageBuilder(V2TimMessage? message, TUITheme? theme) { - if(repliedMessage == null){ + if (repliedMessage == null) { return const SizedBox(width: 0, height: 12); } if (message == null) { - if(repliedMessage?.messageAbstract != null){ + if (repliedMessage?.messageAbstract != null) { return _defaultRawMessageText(repliedMessage!.messageAbstract, theme); } return const SizedBox(width: 0, height: 12); } final messageType = message.elemType; - final isSelf = message.isSelf ?? false; + final isSelf = message.isSelf ?? true; if (widget.chatModel.abstractMessageBuilder != null) { return _defaultRawMessageText( widget.chatModel.abstractMessageBuilder!(message), @@ -148,11 +153,14 @@ class _TIMUIKitReplyElemState extends TIMUIKitState { case MessageElemType.V2TIM_ELEM_TYPE_IMAGE: return TIMUIKitImageElem( chatModel: widget.chatModel, - message: message, isFrom: "reply", isShowMessageReaction: false); + message: message, + isFrom: "reply", + isShowMessageReaction: false); case MessageElemType.V2TIM_ELEM_TYPE_VIDEO: return TIMUIKitVideoElem(message, chatModel: widget.chatModel, - isFrom: "reply", isShowMessageReaction: false); + isFrom: "reply", + isShowMessageReaction: false); case MessageElemType.V2TIM_ELEM_TYPE_LOCATION: return _defaultRawMessageText(TIM_t("[位置]"), theme); case MessageElemType.V2TIM_ELEM_TYPE_MERGER: @@ -177,7 +185,7 @@ class _TIMUIKitReplyElemState extends TIMUIKitState { @override void didUpdateWidget(covariant TIMUIKitReplyElem oldWidget) { - WidgetsBinding.instance?.addPostFrameCallback((mag) { + WidgetsBinding.instance.addPostFrameCallback((mag) { super.didUpdateWidget(oldWidget); _getMessageByMessageID(); }); @@ -219,44 +227,39 @@ class _TIMUIKitReplyElemState extends TIMUIKitState { } } - Widget _renderPreviewWidget() { + Widget? _renderPreviewWidget() { // If the link preview info from [localCustomData] is available, use it to render the preview card. // Otherwise, it will returns null. if (widget.message.localCustomData != null && widget.message.localCustomData!.isNotEmpty) { - final String localJSON = widget.message.localCustomData!; - final LocalCustomDataModel? localPreviewInfo = - LocalCustomDataModel.fromMap(json.decode(localJSON)); - if (localPreviewInfo != null && !localPreviewInfo.isLinkPreviewEmpty()) { - return Container( - margin: const EdgeInsets.only(top: 8), - child: - // You can use this default widget [LinkPreviewWidget] to render preview card, or you can use custom widget. - LinkPreviewWidget(linkPreview: localPreviewInfo), - ); - } else { - return Text(widget.message.textElem?.text ?? "", - softWrap: true, - style: widget.fontStyle ?? - TextStyle( - fontSize: 16, - height: widget.chatModel.chatConfig.textHight, - )); + try { + final String localJSON = widget.message.localCustomData!; + final LocalCustomDataModel? localPreviewInfo = + LocalCustomDataModel.fromMap(json.decode(localJSON)); + if (localPreviewInfo != null && + !localPreviewInfo.isLinkPreviewEmpty()) { + return Container( + margin: const EdgeInsets.only(top: 8), + child: + // You can use this default widget [LinkPreviewWidget] to render preview card, or you can use custom widget. + LinkPreviewWidget(linkPreview: localPreviewInfo), + ); + } else { + return null; + } + } catch (e) { + return null; } } else { - return Text(widget.message.textElem?.text ?? "", - softWrap: true, - style: widget.fontStyle ?? - TextStyle( - fontSize: 16, - height: widget.chatModel.chatConfig.textHight, - )); + return null; } } @override Widget tuiBuild(BuildContext context, TUIKitBuildValue value) { final theme = value.theme; + final isDesktopScreen = + TUIKitScreenUtils.getFormFactor(context) == DeviceType.Desktop; if (widget.isShowJump) { if (!isShining) { Future.delayed(Duration.zero, () { @@ -269,13 +272,17 @@ class _TIMUIKitReplyElemState extends TIMUIKitState { } } } - final defaultStyle = (widget.message.isSelf ?? false) - ? theme.lightPrimaryMaterialColor.shade50 - : theme.weakBackgroundColor; + + final isFromSelf = widget.message.isSelf ?? true; + + final defaultStyle = isFromSelf + ? (theme.chatMessageItemFromSelfBgColor ?? + theme.lightPrimaryMaterialColor.shade50) + : (theme.chatMessageItemFromOthersBgColor); + final backgroundColor = isShowJumpState ? const Color.fromRGBO(245, 166, 35, 1) - : (widget.backgroundColor ?? defaultStyle); - final isFromSelf = widget.message.isSelf ?? false; + : (defaultStyle ?? widget.backgroundColor); final borderRadius = isFromSelf ? const BorderRadius.only( @@ -291,9 +298,13 @@ class _TIMUIKitReplyElemState extends TIMUIKitState { final textWithLink = LinkPreviewEntry.getHyperlinksText( widget.message.textElem?.text ?? "", widget.chatModel.chatConfig.isSupportMarkdownForTextMessage, - widget.chatModel.chatConfig.onTapLink); + onLinkTap: widget.chatModel.chatConfig.onTapLink, + isUseDefaultEmoji: widget.isUseDefaultEmoji, + customEmojiStickerList: widget.customEmojiStickerList, + isEnableTextSelection: + widget.chatModel.chatConfig.isEnableTextSelection); return Container( - padding: widget.textPadding ?? const EdgeInsets.all(10), + padding: widget.textPadding ?? EdgeInsets.all(isDesktopScreen ? 12 : 10), decoration: BoxDecoration( color: backgroundColor, borderRadius: widget.borderRadius ?? borderRadius, @@ -318,7 +329,9 @@ class _TIMUIKitReplyElemState extends TIMUIKitState { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - repliedMessage != null ? "${repliedMessage!.messageSender}:" : "", + repliedMessage != null + ? "${repliedMessage!.messageSender}:" + : "", style: TextStyle( fontSize: 12, color: theme.weakTextColor, @@ -334,27 +347,31 @@ class _TIMUIKitReplyElemState extends TIMUIKitState { const SizedBox( height: 12, ), - if (widget.chatModel.chatConfig.urlPreviewType == - UrlPreviewType.none) - Text(widget.message.textElem?.text ?? "", - softWrap: true, - style: widget.fontStyle ?? - TextStyle( - fontSize: 16, - height: widget.chatModel.chatConfig.textHight, - )), - if (widget.chatModel.chatConfig.urlPreviewType == - UrlPreviewType.onlyHyperlink) - textWithLink!( - style: widget.fontStyle ?? - TextStyle( - fontSize: 16, - height: widget.chatModel.chatConfig.textHight, - )), + // If the [elemType] is text message, it will not be null here. + // You can render the widget from extension directly, with a [TextStyle] optionally. + widget.chatModel.chatConfig.urlPreviewType != UrlPreviewType.none + ? textWithLink!( + style: widget.fontStyle ?? + TextStyle( + fontSize: isDesktopScreen ? 14 : 16, + textBaseline: TextBaseline.ideographic, + height: widget.chatModel.chatConfig.textHeight)) + : ExtendedText(widget.message.textElem?.text ?? "", + softWrap: true, + style: widget.fontStyle ?? + TextStyle( + fontSize: isDesktopScreen ? 14 : 16, + height: widget.chatModel.chatConfig.textHeight), + specialTextSpanBuilder: DefaultSpecialTextSpanBuilder( + isUseDefaultEmoji: widget.isUseDefaultEmoji, + customEmojiStickerList: widget.customEmojiStickerList, + showAtBackground: true, + )), // If the link preview info is available, render the preview card. - if (widget.chatModel.chatConfig.urlPreviewType == - UrlPreviewType.previewCardAndHyperlink) - _renderPreviewWidget(), + if (_renderPreviewWidget() != null && + widget.chatModel.chatConfig.urlPreviewType == + UrlPreviewType.previewCardAndHyperlink) + _renderPreviewWidget()!, if (widget.isShowMessageReaction ?? true) TIMUIKitMessageReactionShowPanel(message: widget.message) ], diff --git a/lib/ui/views/TIMUIKitChat/TIMUIKitMessageItem/tim_uikit_chat_sound_elem.dart b/lib/ui/views/TIMUIKitChat/TIMUIKitMessageItem/tim_uikit_chat_sound_elem.dart index 26d15c7..d3ab96e 100644 --- a/lib/ui/views/TIMUIKitChat/TIMUIKitMessageItem/tim_uikit_chat_sound_elem.dart +++ b/lib/ui/views/TIMUIKitChat/TIMUIKitMessageItem/tim_uikit_chat_sound_elem.dart @@ -1,8 +1,8 @@ + import 'dart:async'; import 'dart:math'; - +import 'package:audioplayers/audioplayers.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_plugin_record_plus/const/play_state.dart'; import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_base.dart'; import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_state.dart'; import 'package:tencent_cloud_chat_uikit/business_logic/separate_models/tui_chat_separate_view_model.dart'; @@ -10,11 +10,9 @@ import 'package:tencent_cloud_chat_uikit/business_logic/view_models/tui_chat_glo import 'package:tencent_cloud_chat_uikit/data_services/message/message_services.dart'; import 'package:tencent_cloud_chat_uikit/data_services/services_locatar.dart'; import 'package:tencent_cloud_chat_uikit/ui/constants/history_message_constant.dart'; - import 'package:tencent_cloud_chat_uikit/ui/utils/platform.dart'; import 'package:tencent_im_base/tencent_im_base.dart'; import 'package:tencent_cloud_chat_uikit/ui/utils/sound_record.dart'; - import 'TIMUIKitMessageReaction/tim_uikit_message_reaction_show_panel.dart'; class TIMUIKitSoundElem extends StatefulWidget { @@ -65,13 +63,6 @@ class _TIMUIKitSoundElemState extends TIMUIKitState { _playSound() async { if (!SoundPlayer.isInited) { - // bool hasMicrophonePermission = await Permissions.checkPermission( - // context, Permission.microphone.value); - // bool hasStoragePermission = isIosDevice || - // await Permissions.checkPermission(context, Permission.storage.value); - // if (!hasMicrophonePermission || !hasStoragePermission) { - // return; - // } SoundPlayer.initSoundPlayer(); } if (widget.localCustomInt == null || @@ -81,15 +72,10 @@ class _TIMUIKitSoundElemState extends TIMUIKitState { } if (isPlaying) { SoundPlayer.stop(); - widget.chatModel.currentSelectedMsgId = ""; + widget.chatModel.currentPlayedMsgId = ""; } else { SoundPlayer.play(url: stateElement.url!); - widget.chatModel.currentSelectedMsgId = widget.msgID; - // SoundPlayer.setSoundInterruptListener(() { - // // setState(() { - // isPlaying = false; - // // }); - // }); + widget.chatModel.currentPlayedMsgId = widget.msgID; } } @@ -123,20 +109,22 @@ class _TIMUIKitSoundElemState extends TIMUIKitState { void didUpdateWidget(oldWidget) { super.didUpdateWidget(oldWidget); setState(() { - isPlaying = widget.chatModel.currentSelectedMsgId != '' && - widget.chatModel.currentSelectedMsgId == widget.msgID; + isPlaying = widget.chatModel.currentPlayedMsgId != '' && + widget.chatModel.currentPlayedMsgId == widget.msgID; }); } @override void initState() { super.initState(); - subscription = SoundPlayer.playStateListener(listener: (PlayState data) { - if (data.playState == 'complete') { - widget.chatModel.currentSelectedMsgId = ""; - // SoundPlayer.removeSoundInterruptListener(); + + subscription = + SoundPlayer.playStateListener(listener: (PlayerState state) { + if(state == PlayerState.completed){ + widget.chatModel.currentPlayedMsgId = ""; } }); + downloadMessageDetailAndSave(); } @@ -144,7 +132,7 @@ class _TIMUIKitSoundElemState extends TIMUIKitState { void dispose() { if (isPlaying) { SoundPlayer.stop(); - widget.chatModel.currentSelectedMsgId = ""; + widget.chatModel.currentPlayedMsgId = ""; } subscription?.cancel(); super.dispose(); @@ -195,9 +183,12 @@ class _TIMUIKitSoundElemState extends TIMUIKitState { @override Widget tuiBuild(BuildContext context, TUIKitBuildValue value) { final theme = value.theme; + final backgroundColor = widget.isFromSelf - ? theme.lightPrimaryMaterialColor.shade50 - : theme.weakBackgroundColor ?? CommonColor.weakBackgroundColor; + ? (theme.chatMessageItemFromSelfBgColor ?? + theme.lightPrimaryMaterialColor.shade50) + : (theme.chatMessageItemFromOthersBgColor); + final borderRadius = widget.isFromSelf ? const BorderRadius.only( topLeft: Radius.circular(10), diff --git a/lib/ui/views/TIMUIKitChat/TIMUIKitMessageItem/tim_uikit_chat_text_elem.dart b/lib/ui/views/TIMUIKitChat/TIMUIKitMessageItem/tim_uikit_chat_text_elem.dart index 29bf559..e1bed03 100644 --- a/lib/ui/views/TIMUIKitChat/TIMUIKitMessageItem/tim_uikit_chat_text_elem.dart +++ b/lib/ui/views/TIMUIKitChat/TIMUIKitMessageItem/tim_uikit_chat_text_elem.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'dart:convert'; +import 'package:tencent_cloud_chat_uikit/ui/utils/screen_utils.dart'; import 'package:tencent_extended_text/extended_text.dart'; import 'package:flutter/material.dart'; import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_base.dart'; @@ -123,9 +124,9 @@ class _TIMUIKitTextElemState extends TIMUIKitState { // The `onUpdateMessage` can use the `updateMessage()` from the [TIMUIKitChatController] directly. LinkPreviewEntry.getFirstLinkPreviewContent( message: widget.message, - onUpdateMessage: () { - widget.chatModel - .updateMessageFromController(msgID: widget.message.msgID!); + onUpdateMessage: (message) { + widget.chatModel.updateMessageFromController( + msgID: widget.message.msgID!, message: message); }); } @@ -138,7 +139,8 @@ class _TIMUIKitTextElemState extends TIMUIKitState { final String localJSON = widget.message.localCustomData!; final LocalCustomDataModel? localPreviewInfo = LocalCustomDataModel.fromMap(json.decode(localJSON)); - if (localPreviewInfo != null && !localPreviewInfo.isLinkPreviewEmpty()) { + if (localPreviewInfo != null && + !localPreviewInfo.isLinkPreviewEmpty()) { return Container( margin: const EdgeInsets.only(top: 8), child: @@ -159,13 +161,16 @@ class _TIMUIKitTextElemState extends TIMUIKitState { @override Widget tuiBuild(BuildContext context, TUIKitBuildValue value) { final theme = value.theme; + final isDesktopScreen = + TUIKitScreenUtils.getFormFactor(context) == DeviceType.Desktop; final textWithLink = LinkPreviewEntry.getHyperlinksText( - widget.message.textElem?.text ?? "", - widget.chatModel.chatConfig.isSupportMarkdownForTextMessage, - widget.chatModel.chatConfig.onTapLink, - widget.isUseDefaultEmoji, - widget.customEmojiStickerList, - ); + widget.message.textElem?.text ?? "", + widget.chatModel.chatConfig.isSupportMarkdownForTextMessage, + onLinkTap: widget.chatModel.chatConfig.onTapLink, + isUseDefaultEmoji: widget.isUseDefaultEmoji, + customEmojiStickerList: widget.customEmojiStickerList, + isEnableTextSelection: + widget.chatModel.chatConfig.isEnableTextSelection); final borderRadius = widget.isFromSelf ? const BorderRadius.only( topLeft: Radius.circular(10), @@ -190,14 +195,18 @@ class _TIMUIKitTextElemState extends TIMUIKitState { } } } + final defaultStyle = widget.isFromSelf - ? theme.lightPrimaryMaterialColor.shade50 - : theme.chatMessageItemFromOthersBgColor; + ? (theme.chatMessageItemFromSelfBgColor ?? + theme.lightPrimaryMaterialColor.shade50) + : (theme.chatMessageItemFromOthersBgColor); + final backgroundColor = isShowJumpState ? const Color.fromRGBO(245, 166, 35, 1) - : (widget.backgroundColor ?? defaultStyle); + : (defaultStyle ?? widget.backgroundColor); + return Container( - padding: widget.textPadding ?? const EdgeInsets.all(10), + padding: widget.textPadding ?? EdgeInsets.all(isDesktopScreen ? 12 : 10), decoration: BoxDecoration( color: backgroundColor, borderRadius: widget.borderRadius ?? borderRadius, @@ -213,15 +222,15 @@ class _TIMUIKitTextElemState extends TIMUIKitState { ? textWithLink!( style: widget.fontStyle ?? TextStyle( - fontSize: 16, + fontSize: isDesktopScreen ? 14 : 16, textBaseline: TextBaseline.ideographic, - height: widget.chatModel.chatConfig.textHight)) + height: widget.chatModel.chatConfig.textHeight)) : ExtendedText(widget.message.textElem?.text ?? "", softWrap: true, style: widget.fontStyle ?? TextStyle( - fontSize: 16, - height: widget.chatModel.chatConfig.textHight), + fontSize: isDesktopScreen ? 14 : 16, + height: widget.chatModel.chatConfig.textHeight), specialTextSpanBuilder: DefaultSpecialTextSpanBuilder( isUseDefaultEmoji: widget.isUseDefaultEmoji, customEmojiStickerList: widget.customEmojiStickerList, diff --git a/lib/ui/views/TIMUIKitChat/TIMUIKitMessageItem/tim_uikit_chat_text_translate_elem.dart b/lib/ui/views/TIMUIKitChat/TIMUIKitMessageItem/tim_uikit_chat_text_translate_elem.dart index 32ac021..a703ee5 100644 --- a/lib/ui/views/TIMUIKitChat/TIMUIKitMessageItem/tim_uikit_chat_text_translate_elem.dart +++ b/lib/ui/views/TIMUIKitChat/TIMUIKitMessageItem/tim_uikit_chat_text_translate_elem.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'dart:convert'; +import 'package:tencent_cloud_chat_uikit/ui/utils/screen_utils.dart'; import 'package:tencent_extended_text/extended_text.dart'; import 'package:flutter/material.dart'; import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_base.dart'; @@ -9,8 +10,6 @@ import 'package:tencent_cloud_chat_uikit/business_logic/separate_models/tui_chat import 'package:tencent_cloud_chat_uikit/tencent_cloud_chat_uikit.dart'; import 'package:tencent_cloud_chat_uikit/ui/views/TIMUIKitChat/TIMUIKitTextField/special_text/DefaultSpecialTextSpanBuilder.dart'; import 'package:tencent_cloud_chat_uikit/ui/widgets/link_preview/link_preview_entry.dart'; -import 'package:tencent_cloud_chat_uikit/ui/widgets/link_preview/widgets/link_preview.dart'; -import 'TIMUIKitMessageReaction/tim_uikit_message_reaction_show_panel.dart'; class TIMUIKitTextTranslationElem extends StatefulWidget { final V2TimMessage message; @@ -46,25 +45,11 @@ class TIMUIKitTextTranslationElem extends StatefulWidget { State createState() => _TIMUIKitTextTranslationElemState(); } -class _TIMUIKitTextTranslationElemState extends TIMUIKitState { +class _TIMUIKitTextTranslationElemState + extends TIMUIKitState { bool isShowJumpState = false; bool isShining = false; - @override - void initState() { - super.initState(); - // get the link preview info - _getLinkPreview(); - } - - @override - void didUpdateWidget(TIMUIKitTextTranslationElem oldWidget) { - super.didUpdateWidget(oldWidget); - if (oldWidget.message.msgID == null && widget.message.msgID != null) { - _getLinkPreview(); - } - } - _showJumpColor() { if ((widget.chatModel.jumpMsgID != widget.message.msgID) && (widget.message.msgID?.isNotEmpty ?? true)) { @@ -92,73 +77,11 @@ class _TIMUIKitTextTranslationElemState extends TIMUIKitState { final TUIChatGlobalModel globalModel = serviceLocator(); final MessageService _messageService = serviceLocator(); late V2TimVideoElem stateElement = widget.message.videoElem!; + Widget errorDisplay(TUITheme? theme) { return Container( decoration: BoxDecoration( @@ -87,8 +88,7 @@ class _TIMUIKitVideoElemState extends TIMUIKitState { } } - if ((stateElement.snapshotUrl == null || - stateElement.snapshotUrl == '') && + if ((stateElement.snapshotUrl == null || stateElement.snapshotUrl == '') && (stateElement.snapshotPath == null || stateElement.snapshotPath == '')) { return Container( @@ -115,8 +115,7 @@ class _TIMUIKitVideoElemState extends TIMUIKitState { return (!kIsWeb && stateElement.snapshotUrl == null || widget.message.status == MessageStatus.V2TIM_MSG_STATUS_SENDING) ? (stateElement.snapshotPath!.isNotEmpty - ? Image.file(File(stateElement.snapshotPath!), - fit: BoxFit.fitWidth) + ? Image.file(File(stateElement.snapshotPath!), fit: BoxFit.fitWidth) : Image.file(File(stateElement.localSnapshotUrl!), fit: BoxFit.fitWidth)) : (kIsWeb || @@ -133,7 +132,7 @@ class _TIMUIKitVideoElemState extends TIMUIKitState { widget.message.videoElem!.videoUrl == '') { final response = await _messageService.getMessageOnlineUrl( msgID: widget.message.msgID!); - if(response.data != null){ + if (response.data != null) { widget.message.videoElem = response.data!.videoElem; Future.delayed(const Duration(microseconds: 10), () { setState(() => stateElement = response.data!.videoElem!); @@ -173,18 +172,47 @@ class _TIMUIKitVideoElemState extends TIMUIKitState { final heroTag = "${widget.message.msgID ?? widget.message.id ?? widget.message.timestamp ?? DateTime.now().millisecondsSinceEpoch}${widget.isFrom}"; - return GestureDetector( + return InkWell( onTap: () { - Navigator.of(context).push( - PageRouteBuilder( - opaque: false, // set to false - pageBuilder: (_, __, ___) => VideoScreen( - message: widget.message, - heroTag: heroTag, - videoElement: stateElement, + if (PlatformUtils().isDesktop) { + final videoElem = widget.message.videoElem; + if (videoElem != null) { + final localVideoUrl = TencentUtils.checkString(videoElem.localVideoUrl); + final videoPath = TencentUtils.checkString(videoElem.videoPath); + final videoUrl = videoElem.videoUrl; + + if (localVideoUrl != null) { + if(PlatformUtils().isWindows){ + OpenFile.open(localVideoUrl); + } else{ + launchUrl(Uri.file(localVideoUrl)); + } + } else if (videoPath != null) { + if(PlatformUtils().isWindows){ + OpenFile.open(videoPath); + } else{ + launchUrl(Uri.file(videoPath)); + } + } else if (TencentUtils.isTextNotEmpty(videoUrl)) { + onTIMCallback(TIMCallback( + infoCode: 6660414, + infoRecommendText: TIM_t("正在下载中"), + type: TIMCallbackType.INFO + )); + } + } + } else { + Navigator.of(context).push( + PageRouteBuilder( + opaque: false, // set to false + pageBuilder: (_, __, ___) => VideoScreen( + message: widget.message, + heroTag: heroTag, + videoElement: stateElement, + ), ), - ), - ); + ); + } }, child: Hero( tag: heroTag, @@ -220,22 +248,28 @@ class _TIMUIKitVideoElemState extends TIMUIKitState { aspectRatio: positionRadio, child: Stack( children: [ + if (stateElement.snapshotUrl != null || + stateElement.snapshotUrl != null) + AspectRatio( + aspectRatio: positionRadio, + child: Container( + decoration: const BoxDecoration( + color: Colors.transparent), + ), + ), Row( children: [ Expanded( - child: generateSnapshot( - theme, - stateElement.snapshotHeight ?? - 100)) + child: generateSnapshot(theme, + stateElement.snapshotHeight ?? 100)) ], ), if (widget.message.status != - MessageStatus - .V2TIM_MSG_STATUS_SENDING && - (stateElement.snapshotUrl != null || - stateElement.snapshotPath != - null) && - stateElement.videoPath != null || + MessageStatus + .V2TIM_MSG_STATUS_SENDING && + (stateElement.snapshotUrl != null || + stateElement.snapshotPath != null) && + stateElement.videoPath != null || stateElement.videoUrl != null) Positioned.fill( // alignment: Alignment.center, @@ -249,18 +283,14 @@ class _TIMUIKitVideoElemState extends TIMUIKitState { bottom: 10, child: Text( MessageUtils.formatVideoTime(widget - .message - .videoElem - ?.duration ?? - 0) + .message.videoElem?.duration ?? + 0) .toString(), style: const TextStyle( - color: Colors.white, - fontSize: 12))), + color: Colors.white, fontSize: 12))), ], ), - ) - ); + )); }), ))), ); diff --git a/lib/ui/views/TIMUIKitChat/TIMUIKitMessageItem/tim_uikit_merger_message_elem.dart b/lib/ui/views/TIMUIKitChat/TIMUIKitMessageItem/tim_uikit_merger_message_elem.dart index e318731..498f145 100644 --- a/lib/ui/views/TIMUIKitChat/TIMUIKitMessageItem/tim_uikit_merger_message_elem.dart +++ b/lib/ui/views/TIMUIKitChat/TIMUIKitMessageItem/tim_uikit_merger_message_elem.dart @@ -3,14 +3,13 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_state.dart'; import 'package:tencent_cloud_chat_uikit/business_logic/separate_models/tui_chat_separate_view_model.dart'; +import 'package:tencent_cloud_chat_uikit/data_services/core/tim_uikit_wide_modal_operation_key.dart'; +import 'package:tencent_cloud_chat_uikit/ui/utils/screen_utils.dart'; import 'package:tencent_cloud_chat_uikit/ui/views/TIMUIKitChat/TIMUIKItMessageList/tim_uikit_chat_history_message_list_item.dart'; - - import 'package:tencent_cloud_chat_uikit/ui/widgets/merger_message_screen.dart'; - import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_base.dart'; +import 'package:tencent_cloud_chat_uikit/ui/widgets/wide_popup.dart'; import 'package:tencent_im_base/tencent_im_base.dart'; - import 'TIMUIKitMessageReaction/tim_uikit_message_reaction_show_panel.dart'; class TIMUIKitMergerElem extends StatefulWidget { @@ -33,7 +32,8 @@ class TIMUIKitMergerElem extends StatefulWidget { this.isShowMessageReaction, required this.messageID, required this.isShowJump, - this.clearJump, this.messageItemBuilder}) + this.clearJump, + this.messageItemBuilder}) : super(key: key); @override @@ -42,6 +42,13 @@ class TIMUIKitMergerElem extends StatefulWidget { class TIMUIKitMergerElemState extends TIMUIKitState { bool isShowJumpState = false; + late ScrollController _scrollController; + + @override + void initState() { + super.initState(); + _scrollController = ScrollController(); + } _showJumpColor() { int shineAmount = 6; @@ -69,13 +76,34 @@ class TIMUIKitMergerElemState extends TIMUIKitState { _handleTap(BuildContext context, TUIChatSeparateViewModel model) async { try { if (widget.messageID != "") { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => MergerMessageScreen( - messageItemBuilder: widget.messageItemBuilder, - model: model, msgID: widget.messageID), - )); + final isDesktopScreen = + TUIKitScreenUtils.getFormFactor(context) == DeviceType.Desktop; + + if (isDesktopScreen) { + TUIKitWidePopup.showPopupWindow( + operationKey: TUIKitWideModalOperationKey.mergerMessageList, + context: context, + width: MediaQuery.of(context).size.width * 0.7, + title: TIM_t("聊天记录"), + height: MediaQuery.of(context).size.height * 0.7, + child: (onClose) => Scrollbar( + controller: _scrollController, + child: MergerMessageScreen( + messageItemBuilder: widget.messageItemBuilder, + model: model, + msgID: widget.messageID), + ), + ); + } else { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => MergerMessageScreen( + messageItemBuilder: widget.messageItemBuilder, + model: model, + msgID: widget.messageID), + )); + } } } catch (e) { onTIMCallback(TIMCallback( @@ -101,9 +129,12 @@ class TIMUIKitMergerElemState extends TIMUIKitState { _showJumpColor(); }); } + final isDesktopScreen = + TUIKitScreenUtils.getFormFactor(context) == DeviceType.Desktop; return Container( - constraints: - BoxConstraints(maxWidth: MediaQuery.of(context).size.width * 0.6), + constraints: BoxConstraints( + maxWidth: + MediaQuery.of(context).size.width * (isDesktopScreen ? 0.3 : 0.6)), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.only( diff --git a/lib/ui/views/TIMUIKitChat/TIMUIKitTextField/at_member_panel.dart b/lib/ui/views/TIMUIKitChat/TIMUIKitTextField/at_member_panel.dart new file mode 100644 index 0000000..e133996 --- /dev/null +++ b/lib/ui/views/TIMUIKitChat/TIMUIKitTextField/at_member_panel.dart @@ -0,0 +1,136 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:scroll_to_index/scroll_to_index.dart'; +import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_base.dart'; +import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_state.dart'; +import 'package:tencent_cloud_chat_uikit/business_logic/separate_models/tui_chat_separate_view_model.dart'; + +import 'package:tencent_cloud_chat_uikit/tencent_cloud_chat_uikit.dart'; +import 'package:tencent_cloud_chat_uikit/ui/widgets/avatar.dart'; + +class AtMemberPanel extends StatefulWidget { + /// messageList widget scroll controller + final AutoScrollController atMemberPanelScroll; + + final ValueChanged onSelectMember; + + // final TextFieldWebController textFieldWebController; + const AtMemberPanel( + // this.textFieldWebController, + {Key? key, + required this.atMemberPanelScroll, + required this.onSelectMember}) + : super(key: key); + + @override + State createState() { + return _AtMemberPanelState(); + } +} + +_getShowName(V2TimGroupMemberFullInfo? item) { + return TencentUtils.checkStringWithoutSpace(item?.nameCard) ?? + TencentUtils.checkStringWithoutSpace(item?.nickName) ?? + TencentUtils.checkStringWithoutSpace(item?.userID); +} + +class _AtMemberPanelState extends TIMUIKitState { + @override + Widget tuiBuild(BuildContext context, TUIKitBuildValue value) { + final theme = value.theme; + final chatModal = Provider.of(context); + final List groupMemberList = + chatModal.showAtMemberList; + final double positionX = chatModal.atPositionX; + final double positionY = chatModal.atPositionY; + final int activeIndex = chatModal.activeAtIndex; + + if (groupMemberList.isEmpty) { + return Container(); + } + return Positioned( + left: positionX, + bottom: positionY, + child: Container( + constraints: const BoxConstraints(maxHeight: 170, maxWidth: 170), + padding: const EdgeInsets.symmetric(vertical: 5), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: const BorderRadius.all(Radius.circular(8)), + border: Border.all(color: const Color(0xFFE5E6E9))), + child: Scrollbar( + controller: widget.atMemberPanelScroll, + child: ListView.builder( + shrinkWrap: true, + itemCount: groupMemberList.length, + controller: widget.atMemberPanelScroll, + itemBuilder: ((context, index) { + final memberItem = groupMemberList[index]; + if (memberItem == null) { + return AutoScrollTag( + key: ValueKey(index), + controller: widget.atMemberPanelScroll, + index: index); + } + final showName = _getShowName(memberItem); + final isAtAll = memberItem.userID == "__kImSDK_MesssageAtALL__"; + return AutoScrollTag( + key: ValueKey(index), + controller: widget.atMemberPanelScroll, + index: index, + child: Material( + color: theme.wideBackgroundColor, + child: InkWell( + onTap: () { + chatModal.activeAtIndex = index; + widget.onSelectMember(memberItem); + }, + child: Container( + padding: const EdgeInsets.symmetric( + horizontal: 8, vertical: 4), + color: activeIndex == index + ? theme.weakBackgroundColor + : theme.wideBackgroundColor, + child: Row( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + SizedBox( + height: 24, + width: 24, + child: Avatar( + faceUrl: memberItem.faceUrl ?? "", + showName: showName), + ), + const SizedBox( + width: 8, + ), + Expanded(child: Text( + isAtAll + ? "$showName(${groupMemberList.length - 1})" + : showName, + softWrap: false, + style: TextStyle( + fontSize: 14, + overflow: TextOverflow.ellipsis, + fontWeight: memberItem.role == 400 || + memberItem.role == 300 + ? FontWeight.w500 + : FontWeight.normal, + color: memberItem.role == 400 || + memberItem.role == 300 + ? theme.primaryColor + : theme.darkTextColor), + )) + ], + ), + ), + ), + ), + ); + })), + ), + ), + ); + } +} diff --git a/lib/ui/views/TIMUIKitChat/TIMUIKitTextField/tim_uikit_at_text.dart b/lib/ui/views/TIMUIKitChat/TIMUIKitTextField/tim_uikit_at_text.dart index 7257e91..d1a27eb 100644 --- a/lib/ui/views/TIMUIKitChat/TIMUIKitTextField/tim_uikit_at_text.dart +++ b/lib/ui/views/TIMUIKitChat/TIMUIKitTextField/tim_uikit_at_text.dart @@ -4,6 +4,7 @@ import 'package:tencent_cloud_chat_uikit/data_services/group/group_services.dart import 'package:tencent_cloud_chat_uikit/data_services/services_locatar.dart'; import 'package:tencent_cloud_chat_uikit/ui/utils/platform.dart'; +import 'package:tencent_cloud_chat_uikit/ui/utils/screen_utils.dart'; import 'package:tencent_im_base/tencent_im_base.dart'; import 'package:tencent_cloud_chat_uikit/ui/views/TIMUIKitGroupProfile/widgets/tim_ui_group_member_search.dart'; @@ -15,14 +16,22 @@ class AtText extends StatefulWidget { final String? groupID; final V2TimGroupInfo? groupInfo; final List? groupMemberList; + final VoidCallback? closeFunc; + final Function( + V2TimGroupMemberFullInfo memberInfo, TapDownDetails? tapDetails)? + onChooseMember; + // some Group type cant @all final String? groupType; + const AtText({ this.groupID, this.groupType, Key? key, this.groupInfo, this.groupMemberList, + this.closeFunc, + this.onChooseMember, }) : super(key: key); @override @@ -47,8 +56,17 @@ class _AtTextState extends TIMUIKitState { super.dispose(); } - _onTapMemberItem(V2TimGroupMemberFullInfo memberInfo) { - Navigator.pop(context, memberInfo); + _onTapMemberItem( + V2TimGroupMemberFullInfo memberInfo, TapDownDetails? tapDetails) { + if (widget.closeFunc != null) { + widget.closeFunc!(); + } + + if (widget.onChooseMember != null) { + widget.onChooseMember!(memberInfo, tapDetails); + } else { + Navigator.pop(context, memberInfo); + } } Future> searchGroupMember( @@ -93,61 +111,62 @@ class _AtTextState extends TIMUIKitState { Widget tuiBuild(BuildContext context, TUIKitBuildValue value) { final TUITheme theme = value.theme; - return Scaffold( - appBar: AppBar( - shadowColor: theme.weakBackgroundColor, - iconTheme: const IconThemeData( - color: Colors.white, - ), - flexibleSpace: Container( - decoration: BoxDecoration( - gradient: LinearGradient(colors: [ - theme.lightPrimaryColor ?? CommonColor.lightPrimaryColor, - theme.primaryColor ?? CommonColor.primaryColor - ]), - ), - ), - leading: Row( - children: [ - IconButton( - padding: const EdgeInsets.only(left: 16), - constraints: const BoxConstraints(), - icon: Image.asset( - 'images/arrow_back.png', - package: 'tencent_cloud_chat_uikit', - height: 34, - width: 34, - ), - onPressed: () async { - Navigator.pop(context); - }, + Widget mentionedMembersBody() { + return GroupProfileMemberList( + groupType: widget.groupType ?? "", + memberList: searchMemberList ?? [], + onTapMemberItem: _onTapMemberItem, + canAtAll: true, + canSlideDelete: false, + touchBottomCallBack: () { + // Get all by once, unnecessary to load more + }, + customTopArea: PlatformUtils().isWeb + ? null + : GroupMemberSearchTextField( + onTextChange: (text) => + handleSearchGroupMembers(text, context), + )); + } + + return TUIKitScreenUtils.getDeviceWidget( + desktopWidget: mentionedMembersBody(), + defaultWidget: Scaffold( + appBar: AppBar( + shadowColor: theme.weakBackgroundColor, + iconTheme: IconThemeData( + color: theme.appbarTextColor, + ), + backgroundColor: theme.appbarBgColor ?? + theme.primaryColor, + leading: Row( + children: [ + IconButton( + padding: const EdgeInsets.only(left: 16), + constraints: const BoxConstraints(), + icon: Image.asset( + 'images/arrow_back.png', + package: 'tencent_cloud_chat_uikit', + height: 34, + width: 34, + color: theme.appbarTextColor, + ), + onPressed: () async { + Navigator.pop(context); + }, + ), + ], + ), + centerTitle: true, + leadingWidth: 100, + title: Text( + TIM_t("选择提醒人"), + style: TextStyle( + color: theme.appbarTextColor, + fontSize: 17, + ), ), - ], - ), - centerTitle: true, - leadingWidth: 100, - title: Text( - TIM_t("选择提醒人"), - style: const TextStyle( - color: Colors.white, - fontSize: 17, ), - ), - ), - body: GroupProfileMemberList( - groupType: widget.groupType ?? "", - memberList: searchMemberList ?? [], - onTapMemberItem: _onTapMemberItem, - canAtAll: true, - canSlideDelete: false, - touchBottomCallBack: () { - // Get all by once, unnecessary to load more - }, - customTopArea: PlatformUtils().isWeb - ? null - : GroupMemberSearchTextField( - onTextChange: (text) => - handleSearchGroupMembers(text, context), - ))); + body: mentionedMembersBody())); } } diff --git a/lib/ui/views/TIMUIKitChat/TIMUIKitTextField/tim_uikit_call_invite_list.dart b/lib/ui/views/TIMUIKitChat/TIMUIKitTextField/tim_uikit_call_invite_list.dart index 54afc63..2229fc8 100644 --- a/lib/ui/views/TIMUIKitChat/TIMUIKitTextField/tim_uikit_call_invite_list.dart +++ b/lib/ui/views/TIMUIKitChat/TIMUIKitTextField/tim_uikit_call_invite_list.dart @@ -135,26 +135,20 @@ class _SelectCallInviterState extends TIMUIKitState { return Scaffold( appBar: AppBar( shadowColor: theme.weakBackgroundColor, - iconTheme: const IconThemeData( - color: Colors.white, - ), - flexibleSpace: Container( - decoration: BoxDecoration( - gradient: LinearGradient(colors: [ - theme.lightPrimaryColor ?? CommonColor.lightPrimaryColor, - theme.primaryColor ?? CommonColor.primaryColor - ]), - ), + iconTheme: IconThemeData( + color: theme.appbarTextColor, ), + backgroundColor: theme.appbarBgColor ?? + theme.primaryColor, leading: TextButton( onPressed: () { Navigator.pop(context); }, child: Text( TIM_t("取消"), - style: const TextStyle( - color: Colors.white, - fontSize: 16, + style: TextStyle( + color: theme.appbarTextColor, + fontSize: 14, ), ), ), @@ -167,19 +161,19 @@ class _SelectCallInviterState extends TIMUIKitState { }, child: Text( TIM_t("完成"), - style: const TextStyle( - color: Colors.white, - fontSize: 16, + style: TextStyle( + color: theme.appbarTextColor, + fontSize: 14, ), ), ) ], centerTitle: true, - leadingWidth: 100, + leadingWidth: 80, title: Text( TIM_t("发起呼叫"), - style: const TextStyle( - color: Colors.white, + style: TextStyle( + color: theme.appbarTextColor, fontSize: 17, ), ), diff --git a/lib/ui/views/TIMUIKitChat/TIMUIKitTextField/tim_uikit_emoji_panel.dart b/lib/ui/views/TIMUIKitChat/TIMUIKitTextField/tim_uikit_emoji_panel.dart index bafa2b0..a1df2dd 100644 --- a/lib/ui/views/TIMUIKitChat/TIMUIKitTextField/tim_uikit_emoji_panel.dart +++ b/lib/ui/views/TIMUIKitChat/TIMUIKitTextField/tim_uikit_emoji_panel.dart @@ -77,6 +77,7 @@ class EmojiItem extends TIMUIKitStatelessWidget { return DefaultTextStyle( style: TextStyle( fontSize: (PlatformUtils().isAndroid) ? 20 : 26, + color: hexToColor("f9453d") ), child: Text( String.fromCharCode(unicode), diff --git a/lib/ui/views/TIMUIKitChat/TIMUIKitTextField/tim_uikit_more_panel.dart b/lib/ui/views/TIMUIKitChat/TIMUIKitTextField/tim_uikit_more_panel.dart index 8d53497..78c2d89 100644 --- a/lib/ui/views/TIMUIKitChat/TIMUIKitTextField/tim_uikit_more_panel.dart +++ b/lib/ui/views/TIMUIKitChat/TIMUIKitTextField/tim_uikit_more_panel.dart @@ -1,35 +1,33 @@ // ignore_for_file: unused_field, avoid_print, unused_import import 'dart:io'; - +import 'package:device_info_plus/device_info_plus.dart'; +import 'package:fc_native_video_thumbnail_for_us/fc_native_video_thumbnail_for_us.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_svg/svg.dart'; import 'package:image_picker/image_picker.dart'; import 'package:file_picker/file_picker.dart'; import 'package:path_provider/path_provider.dart'; -import 'package:permission_handler/permission_handler.dart'; import 'package:provider/provider.dart'; -import 'package:tencent_im_base/tencent_im_base.dart'; +import 'package:tencent_cloud_chat_uikit/tencent_cloud_chat_uikit.dart'; +import 'package:tencent_cloud_chat_uikit/ui/views/TIMUIKitChat/TIMUIKitTextField/tim_uikit_call_invite_list.dart'; import 'package:tencent_wechat_camera_picker/tencent_wechat_camera_picker.dart'; import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_state.dart'; import 'package:tencent_cloud_chat_uikit/business_logic/separate_models/tui_chat_separate_view_model.dart'; import 'package:tencent_cloud_chat_uikit/business_logic/view_models/tui_chat_global_model.dart'; import 'package:tencent_cloud_chat_uikit/business_logic/view_models/tui_self_info_view_model.dart'; - +import 'package:path/path.dart' as p; import 'package:tencent_cloud_chat_uikit/data_services/services_locatar.dart'; - import 'package:tencent_cloud_chat_uikit/ui/utils/message.dart'; import 'package:tencent_cloud_chat_uikit/ui/utils/permission.dart'; import 'package:tencent_cloud_chat_uikit/ui/utils/platform.dart'; - import 'package:tencent_cloud_chat_uikit/ui/views/TIMUIKitChat/TIMUIKitTextField/intl_camer_picker.dart'; -import 'package:video_thumbnail/video_thumbnail.dart' as video_thumbnail; import 'package:wechat_assets_picker/wechat_assets_picker.dart'; import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_base.dart'; + // ignore: unnecessary_import import 'dart:typed_data'; - import 'package:universal_html/html.dart' as html; class MorePanelConfig { @@ -38,6 +36,8 @@ class MorePanelConfig { final bool showFilePickAction; final bool showWebImagePickAction; final bool showWebVideoPickAction; + final bool showVoiceCall; + final bool showVideoCall; final List? extraAction; final Widget Function(MorePanelItem item)? actionBuilder; @@ -47,6 +47,8 @@ class MorePanelConfig { this.showCameraAction = true, this.showWebImagePickAction = true, this.showWebVideoPickAction = true, + this.showVoiceCall = true, + this.showVideoCall = true, this.extraAction, this.actionBuilder, }); @@ -77,6 +79,7 @@ class MorePanel extends StatefulWidget { Key? key, this.morePanelConfig}) : super(key: key); + @override State createState() => _MorePanelState(); } @@ -88,11 +91,28 @@ class _MorePanelState extends TIMUIKitState { Uint8List? fileContent; String? fileName; File? tempFile; + final _tUICore = TUICore(); + final _tUILogin = TUILogin(); + bool isInstallCallkit = false; + final ScrollController _scrollController = ScrollController(); + final DeviceInfoPlugin deviceInfo = DeviceInfoPlugin(); + + @override + void initState() { + super.initState(); + if (PlatformUtils().isMobile) { + _tUICore.getService(TUICALLKIT_SERVICE_NAME).then((value) { + setState(() { + isInstallCallkit = value; + }); + }); + } + } List itemList(TUIChatSeparateViewModel model, TUITheme theme) { final config = widget.morePanelConfig ?? MorePanelConfig(); return [ - if (!PlatformUtils().isWeb) + if (PlatformUtils().isMobile) MorePanelItem( id: "screen", title: TIM_t("拍摄"), @@ -212,6 +232,58 @@ class _MorePanelState extends TIMUIKitState { width: 64, ), )), + if (isInstallCallkit && PlatformUtils().isMobile) + MorePanelItem( + id: "videoCall", + title: TIM_t("视频通话"), + onTap: (c) { + _onFeatureTap( + "videoCall", + c, + model, + theme, + ); + }, + icon: Container( + height: 64, + width: 64, + margin: const EdgeInsets.only(bottom: 4), + decoration: const BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.all(Radius.circular(5))), + child: SvgPicture.asset( + "images/video-call.svg", + package: 'tencent_cloud_chat_uikit', + height: 64, + width: 64, + ), + )), + if (isInstallCallkit && PlatformUtils().isMobile) + MorePanelItem( + id: "voiceCall", + title: TIM_t("语音通话"), + onTap: (c) { + _onFeatureTap( + "voiceCall", + c, + model, + theme, + ); + }, + icon: Container( + height: 64, + width: 64, + margin: const EdgeInsets.only(bottom: 4), + decoration: const BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.all(Radius.circular(5))), + child: SvgPicture.asset( + "images/voice-call.svg", + package: 'tencent_cloud_chat_uikit', + height: 64, + width: 64, + ), + )), if (config.extraAction != null) ...?config.extraAction, ].where((element) { if (element.id == "screen") { @@ -233,11 +305,18 @@ class _MorePanelState extends TIMUIKitState { if (element.id == "video") { return config.showWebVideoPickAction; } + if (element.id == "voiceCall") { + return config.showVoiceCall; + } + if (element.id == "videoCall") { + return config.showVideoCall; + } return true; }).toList(); } _sendVideoMessage(AssetEntity asset, TUIChatSeparateViewModel model) async { + final plugin = FcNativeVideoThumbnail(); final originFile = await asset.originFile; final size = await originFile!.length(); if (size >= 104857600) { @@ -253,21 +332,24 @@ class _MorePanelState extends TIMUIKitState { final convID = widget.conversationID; final convType = widget.conversationType; - String tempPath = (await getTemporaryDirectory()).path; + String tempPath = (await getTemporaryDirectory()).path + + p.basename(originFile.path) + + ".jpeg"; - String? thumbnail = await video_thumbnail.VideoThumbnail.thumbnailFile( - video: originFile.path, - thumbnailPath: tempPath, - imageFormat: video_thumbnail.ImageFormat.JPEG, - maxWidth: - 128, // specify the width of the thumbnail, let the height auto-scaled to keep the source aspect ratio - quality: 25, + await plugin.getVideoThumbnail( + srcFile: originFile.path, + keepAspectRatio: true, + destFile: tempPath, + format: 'jpeg', + width: 128, + quality: 100, + height: 128, ); MessageUtils.handleMessageError( model.sendVideoMessage( videoPath: filePath, duration: duration, - snapshotPath: thumbnail ?? '', + snapshotPath: tempPath, convID: convID, convType: convType), context); @@ -275,36 +357,92 @@ class _MorePanelState extends TIMUIKitState { _sendImageMessage(TUIChatSeparateViewModel model, TUITheme theme) async { try { - final bool isAndroid = PlatformUtils().isAndroid; - if (!PlatformUtils().isWeb && - !await Permissions.checkPermission( - context, - isAndroid ? Permission.storage.value : Permission.photos.value, - theme, - )) { - return; - } - final convID = widget.conversationID; - final convType = widget.conversationType; - final pickedAssets = await AssetPicker.pickAssets(context); - - if (pickedAssets != null) { - for (var asset in pickedAssets) { - final originFile = await asset.originFile; - final filePath = originFile?.path; - final type = asset.type; - if (filePath != null) { - if (type == AssetType.image) { - MessageUtils.handleMessageError( - model.sendImageMessage( - imagePath: filePath, convID: convID, convType: convType), - context); + if (PlatformUtils().isMobile){ + if(PlatformUtils().isAndroid){ + AndroidDeviceInfo androidInfo = await deviceInfo.androidInfo; + if ((androidInfo.version.sdkInt ?? 0) >= 33) { + final videos = await Permissions.checkPermission( + context,Permission.videos.value, + theme, + ); + final photos = await Permissions.checkPermission( + context,Permission.photos.value, + theme, + ); + if(!videos && !photos){ + return; } - - if (type == AssetType.video) { - _sendVideoMessage(asset, model); + } else { + final storage = await Permissions.checkPermission( + context, Permission.storage.value, + theme, + ); + if(!storage){ + return; } } + }else{ + final photos = await Permissions.checkPermission( + context, + Permission.photos.value, + theme, + ); + if(!photos){ + return; + } + } + } + + final convID = widget.conversationID; + final convType = widget.conversationType; + + if (PlatformUtils().isMobile) { + final pickedAssets = await AssetPicker.pickAssets(context); + + if (pickedAssets != null) { + for (var asset in pickedAssets) { + final originFile = await asset.originFile; + final filePath = originFile?.path; + final type = asset.type; + if (filePath != null) { + if (type == AssetType.image) { + MessageUtils.handleMessageError( + model.sendImageMessage( + imagePath: filePath, + convID: convID, + convType: convType), + context); + } + + if (type == AssetType.video) { + _sendVideoMessage(asset, model); + } + } + } + } + } else { + FilePickerResult? result = + await FilePicker.platform.pickFiles(type: FileType.media); + if (result != null && result.files.isNotEmpty) { + File file = File(result.files.single.path!); + final String savePath = file.path; + final String type = TencentUtils.getFileType( + savePath.split(".")[savePath.split(".").length - 1]) + .split("/")[0]; + + if (type == "image") { + MessageUtils.handleMessageError( + model.sendImageMessage( + imagePath: savePath, convID: convID, convType: convType), + context); + } else if (type == "video") { + MessageUtils.handleMessageError( + model.sendVideoMessage( + videoPath: savePath, convID: convID, convType: convType), + context); + } + } else { + throw TypeError(); } } } catch (err) { @@ -317,14 +455,18 @@ class _MorePanelState extends TIMUIKitState { TUITheme theme, ) async { try { - if (PlatformUtils().isIOS && - !await Permissions.checkPermission( + if (!await Permissions.checkPermission( context, Permission.camera.value, theme, )) { return; } + await Permissions.checkPermission( + context, + Permission.microphone.value, + theme, + ); final convID = widget.conversationID; final convType = widget.conversationType; final pickedFile = await CameraPicker.pickFromCamera(context, @@ -417,14 +559,6 @@ class _MorePanelState extends TIMUIKitState { TUIChatSeparateViewModel model, TUITheme theme, ) async { - if (!kIsWeb && - !await Permissions.checkPermission( - context, - Permission.storage.value, - theme, - )) { - return; - } try { final convID = widget.conversationID; final convType = widget.conversationType; @@ -462,7 +596,7 @@ class _MorePanelState extends TIMUIKitState { convType: convType), context); } else { - throw NullThrownError(); + throw TypeError(); } } catch (e) { print("_sendFileErr: ${e.toString()}"); @@ -493,6 +627,53 @@ class _MorePanelState extends TIMUIKitState { // only for web _sendVideoFileOnWeb(model); break; + case "voiceCall": + _goToVideoUI(TYPE_AUDIO); + break; + case "videoCall": + _goToVideoUI(TYPE_VIDEO); + break; + } + } + + _goToVideoUI(String type) async { + if (!PlatformUtils().isWeb) { + final hasCameraPermission = type == TYPE_VIDEO + ? await Permissions.checkPermission(context, Permission.camera.value) + : true; + final hasMicphonePermission = await Permissions.checkPermission( + context, Permission.microphone.value); + if (!hasCameraPermission || !hasMicphonePermission) { + return; + } + } + + final isGroup = widget.conversationType == ConvType.group; + if (isGroup) { + List? selectedMember = await Navigator.push( + context, + MaterialPageRoute( + builder: (context) => SelectCallInviter( + groupID: widget.conversationID, + ), + ), + ); + if (selectedMember != null) { + final inviteMember = selectedMember.map((e) => e.userID).toList(); + _tUICore.callService(TUICALLKIT_SERVICE_NAME, METHOD_NAME_CALL, { + PARAM_NAME_TYPE: type, + PARAM_NAME_USERIDS: inviteMember, + PARAM_NAME_GROUPID: widget.conversationType == ConvType.group + ? widget.conversationID + : "" + }); + } + } else { + _tUICore.callService(TUICALLKIT_SERVICE_NAME, METHOD_NAME_CALL, { + PARAM_NAME_TYPE: type, + PARAM_NAME_USERIDS: [widget.conversationID], + PARAM_NAME_GROUPID: "" + }); } } @@ -512,42 +693,46 @@ class _MorePanelState extends TIMUIKitState { ), padding: const EdgeInsets.only(top: 20, left: 20, right: 20), width: screenWidth, - child: SingleChildScrollView( - child: Wrap( - spacing: (screenWidth - (23 * 2) - 64 * 4) / 3, - runSpacing: 20, - children: itemList(model, theme) - .map((item) => InkWell( - onTap: () { - if (item.onTap != null) { - item.onTap!(context); - } - }, - child: widget.morePanelConfig?.actionBuilder != null - ? widget.morePanelConfig?.actionBuilder!(item) - : SizedBox( - height: 94, - width: 64, - child: Column( - children: [ - Container( - height: 64, - width: 64, - margin: const EdgeInsets.only(bottom: 4), - decoration: const BoxDecoration( - borderRadius: - BorderRadius.all(Radius.circular(5))), - child: item.icon, - ), - Text( - item.title, - style: TextStyle( - fontSize: 12, color: theme.darkTextColor), - ) - ], - ), - ))) - .toList(), + child: Scrollbar( + controller: _scrollController, + child: SingleChildScrollView( + controller: _scrollController, + child: Wrap( + spacing: (screenWidth - (23 * 2) - 64 * 4) / 3, + runSpacing: 20, + children: itemList(model, theme) + .map((item) => InkWell( + onTap: () { + if (item.onTap != null) { + item.onTap!(context); + } + }, + child: widget.morePanelConfig?.actionBuilder != null + ? widget.morePanelConfig?.actionBuilder!(item) + : SizedBox( + height: 94, + width: 64, + child: Column( + children: [ + Container( + height: 64, + width: 64, + margin: const EdgeInsets.only(bottom: 4), + decoration: const BoxDecoration( + borderRadius: + BorderRadius.all(Radius.circular(5))), + child: item.icon, + ), + Text( + item.title, + style: TextStyle( + fontSize: 12, color: theme.darkTextColor), + ) + ], + ), + ))) + .toList(), + ), ), ), ); diff --git a/lib/ui/views/TIMUIKitChat/TIMUIKitTextField/tim_uikit_send_sound_message.dart b/lib/ui/views/TIMUIKitChat/TIMUIKitTextField/tim_uikit_send_sound_message.dart index faae46b..bcd1a2e 100644 --- a/lib/ui/views/TIMUIKitChat/TIMUIKitTextField/tim_uikit_send_sound_message.dart +++ b/lib/ui/views/TIMUIKitChat/TIMUIKitTextField/tim_uikit_send_sound_message.dart @@ -129,7 +129,7 @@ class _SendSoundMessageState extends TIMUIKitState { ), ); }); - Overlay.of(context)!.insert(overlayEntry!); + Overlay.of(context)?.insert(overlayEntry!); } } diff --git a/lib/ui/views/TIMUIKitChat/TIMUIKitTextField/tim_uikit_text_field.dart b/lib/ui/views/TIMUIKitChat/TIMUIKitTextField/tim_uikit_text_field.dart index 93b23eb..5c5d473 100644 --- a/lib/ui/views/TIMUIKitChat/TIMUIKitTextField/tim_uikit_text_field.dart +++ b/lib/ui/views/TIMUIKitChat/TIMUIKitTextField/tim_uikit_text_field.dart @@ -1,11 +1,11 @@ import 'dart:async'; import 'dart:math'; +import 'package:diff_match_patch/diff_match_patch.dart'; import 'package:tencent_cloud_chat_uikit/business_logic/view_models/tui_setting_model.dart'; -import 'package:tencent_cloud_chat_uikit/ui/utils/optimize_utils.dart'; -import 'package:tencent_extended_text_field/extended_text_field.dart'; +import 'package:tencent_cloud_chat_uikit/ui/utils/screen_utils.dart'; +import 'package:tencent_cloud_chat_uikit/ui/views/TIMUIKitChat/TIMUIKitTextField/tim_uikit_text_field_layout/narrow.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import 'package:flutter_svg/svg.dart'; import 'package:provider/provider.dart'; import 'package:scroll_to_index/scroll_to_index.dart'; import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_state.dart'; @@ -18,15 +18,28 @@ import 'package:tencent_cloud_chat_uikit/tencent_cloud_chat_uikit.dart'; import 'package:tencent_cloud_chat_uikit/ui/utils/message.dart'; import 'package:tencent_cloud_chat_uikit/ui/utils/platform.dart'; import 'package:tencent_cloud_chat_uikit/ui/views/TIMUIKitChat/TIMUIKitTextField/tim_uikit_at_text.dart'; -import 'package:tencent_cloud_chat_uikit/ui/views/TIMUIKitChat/TIMUIKitTextField/tim_uikit_emoji_panel.dart'; import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_base.dart'; -import 'package:tencent_cloud_chat_uikit/ui/views/TIMUIKitChat/TIMUIKitTextField/tim_uikit_send_sound_message.dart'; -import 'package:tencent_cloud_chat_uikit/ui/utils/permission.dart'; -import 'package:tencent_keyboard_visibility/tencent_keyboard_visibility.dart'; -import 'special_text/DefaultSpecialTextSpanBuilder.dart'; +import 'package:tencent_cloud_chat_uikit/ui/views/TIMUIKitChat/TIMUIKitTextField/tim_uikit_text_field_layout/wide.dart'; enum MuteStatus { none, me, all } +typedef CustomStickerPanel = Widget Function({ + void Function() sendTextMessage, + void Function(int index, String data) sendFaceMessage, + void Function() deleteText, + void Function(int unicode) addText, + void Function(String singleEmojiName) addCustomEmojiText, + List defaultCustomEmojiStickerList, + + /// If non-null, requires the child to have exactly this width. + double? width, + + /// If non-null, requires the child to have exactly this height. + double? height, +}); + +GlobalKey<_InputTextFieldState> inputTextFieldState = GlobalKey(); + class TIMUIKitInputTextField extends StatefulWidget { /// conversation id final String conversationID; @@ -40,6 +53,9 @@ class TIMUIKitInputTextField extends StatefulWidget { /// messageList widget scroll controller final AutoScrollController? scrollController; + /// messageList widget scroll controller + final AutoScrollController? atMemberPanelScroll; + /// hint text for textField widget final String? hintText; @@ -71,17 +87,13 @@ class TIMUIKitInputTextField extends StatefulWidget { final List customEmojiStickerList; - final String? groupType; - /// sticker panel customization - final Widget Function( - {void Function() sendTextMessage, - void Function(int index, String data) sendFaceMessage, - void Function() deleteText, - void Function(int unicode) addText, - void Function(String singleEmojiName) addCustomEmojiText, - List defaultCustomEmojiStickerList})? - customStickerPanel; + final CustomStickerPanel? customStickerPanel; + + /// Conversation need search + final V2TimConversation currentConversation; + + final String? groupType; const TIMUIKitInputTextField( {Key? key, @@ -100,7 +112,10 @@ class TIMUIKitInputTextField extends StatefulWidget { this.onChanged, this.isUseDefaultEmoji = false, this.customEmojiStickerList = const [], - required this.model, this.groupType}) + required this.model, + required this.currentConversation, + this.groupType, + this.atMemberPanelScroll}) : super(key: key); @override @@ -110,18 +125,14 @@ class TIMUIKitInputTextField extends StatefulWidget { class _InputTextFieldState extends TIMUIKitState { final TUIChatGlobalModel globalModel = serviceLocator(); final TUISettingModel settingModel = serviceLocator(); - bool showMore = false; - bool showMoreButton = true; - bool showSendSoundText = false; - bool showEmojiPanel = false; - bool showKeyboard = false; + final RegExp atTextReg = RegExp(r'@([^@\s]*)'); late FocusNode focusNode; String zeroWidthSpace = '\ufeff'; String lastText = ""; String languageType = ""; - double? bottomPadding; - Function? setKeyboardHeight; int? currentCursor; + bool isAddingAtSearchWords = false; + double inputWidth = 900; Map memberInfoMap = {}; @@ -133,56 +144,8 @@ class _InputTextFieldState extends TIMUIKitState { int latestSendEditStatusTime = DateTime.now().millisecondsSinceEpoch; - Widget _getBottomContainer() { - if (showEmojiPanel) { - return widget.customStickerPanel != null - ? widget.customStickerPanel!( - sendTextMessage: () { - onEmojiSubmitted(); - }, - sendFaceMessage: onCustomEmojiFaceSubmitted, - deleteText: () { - backSpaceText(); - }, - addText: (int unicode) { - final newText = String.fromCharCode(unicode); - addStickerToText(newText); - // handleSetDraftText(); - }, - addCustomEmojiText: ((String singleEmojiName) { - String? emojiName = singleEmojiName.split('.png')[0]; - if (widget.isUseDefaultEmoji && - languageType == 'zh' && - ConstData.emojiMapList[emojiName] != null && - ConstData.emojiMapList[emojiName] != '') { - emojiName = ConstData.emojiMapList[emojiName]; - } - final newText = '[$emojiName]'; - addStickerToText(newText); - setSendButton(); - }), - defaultCustomEmojiStickerList: - widget.isUseDefaultEmoji ? ConstData.emojiList : []) - : EmojiPanel(onTapEmoji: (unicode) { - final newText = String.fromCharCode(unicode); - addStickerToText(newText); - setSendButton(); - // handleSetDraftText(); - }, onSubmitted: () { - onEmojiSubmitted(); - }, delete: () { - backSpaceText(); - }); - } - - if (showMore) { - return MorePanel( - morePanelConfig: widget.morePanelConfig, - conversationID: widget.conversationID, - conversationType: widget.conversationType); - } - - return const SizedBox(height: 0); + setCurrentCursor(int? value) { + currentCursor = value; } void addStickerToText(String sticker) { @@ -195,59 +158,10 @@ class _InputTextFieldState extends TIMUIKitState { } else { textEditingController.text = "$oldText$sticker"; } - } - double _getBottomHeight() { - if (showKeyboard) { - final currentKeyboardHeight = MediaQuery.of(context).viewInsets.bottom; - double originHeight = settingModel.keyboardHeight; - if (currentKeyboardHeight != 0) { - if (currentKeyboardHeight >= originHeight) { - originHeight = currentKeyboardHeight; - } - if (setKeyboardHeight != null) { - setKeyboardHeight!(currentKeyboardHeight); - } - } - final height = originHeight != 0 ? originHeight : currentKeyboardHeight; - return height; - } else if (showMore || showEmojiPanel) { - return 248.0 + (bottomPadding ?? 0.0); - } else if (textEditingController.text.length >= 46 && - showKeyboard == false) { - return 25 + (bottomPadding ?? 0.0); - } else { - return bottomPadding ?? 0; - } - } - - _openMore() { - if (!showMore) { - focusNode.unfocus(); - currentCursor = null; - } - setState(() { - showKeyboard = false; - showEmojiPanel = false; - showSendSoundText = false; - showMore = !showMore; - }); - } - - _openEmojiPanel() { - _onCursorChange(); - showKeyboard = showEmojiPanel; - if (showEmojiPanel) { - focusNode.requestFocus(); - } else { + if (TUIKitScreenUtils.getFormFactor(context) == DeviceType.Desktop) { focusNode.unfocus(); } - - setState(() { - showMore = false; - showSendSoundText = false; - showEmojiPanel = !showEmojiPanel; - }); } String _filterU200b(String text) { @@ -255,18 +169,17 @@ class _InputTextFieldState extends TIMUIKitState { } getShowName(message) { - return message.friendRemark == null || message.friendRemark == '' - ? message.nickName == null || message.nickName == '' - ? message.sender - : message.nickName - : message.friendRemark; + return TencentUtils.checkStringWithoutSpace(message?.friendRemark) ?? + TencentUtils.checkStringWithoutSpace(message?.nickName) ?? + TencentUtils.checkStringWithoutSpace(message?.userID); } - handleSetDraftText() async { - String convID = widget.conversationID; - String conversationID = widget.conversationType == ConvType.c2c - ? "c2c_$convID" - : "group_$convID"; + handleSetDraftText([String? id, ConvType? convType]) async { + String convID = id ?? widget.conversationID; + String conversationID = + (convType ?? widget.conversationType) == ConvType.c2c + ? "c2c_$convID" + : "group_$convID"; String text = textEditingController.text; String? draftText = _filterU200b(text); @@ -277,59 +190,6 @@ class _InputTextFieldState extends TIMUIKitState { conversationID: conversationID, draftText: draftText); } - _buildRepliedMessage(V2TimMessage? repliedMessage) { - final haveRepliedMessage = repliedMessage != null; - if (haveRepliedMessage) { - final text = - "${MessageUtils.getDisplayName(widget.model.repliedMessage!)}:${widget.model.abstractMessageBuilder != null ? widget.model.abstractMessageBuilder!(widget.model.repliedMessage!) : MessageUtils.getAbstractMessageAsync(widget.model.repliedMessage!, widget.model.groupMemberList ?? [])}"; - return Container( - color: widget.backgroundColor ?? hexToColor("f5f5f6"), - alignment: Alignment.centerLeft, - padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 16), - child: Row( - crossAxisAlignment: CrossAxisAlignment.center, - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Expanded( - child: Text( - text, - softWrap: true, - maxLines: 3, - overflow: TextOverflow.ellipsis, - style: TextStyle(color: hexToColor("8f959e"), fontSize: 14), - ), - ), - const SizedBox( - width: 16, - ), - InkWell( - onTap: () { - widget.model.repliedMessage = null; - }, - child: Icon(Icons.clear, color: hexToColor("8f959e"), size: 18), - ) - ], - ), - ); - } - return Container(); - } - - void setSendButton() { - final value = textEditingController.text; - if (isWebDevice() || isAndroidDevice()) { - if (value.isEmpty && showMoreButton != true) { - setState(() { - showMoreButton = true; - }); - } else if (value.isNotEmpty && showMoreButton == true) { - setState(() { - showMoreButton = false; - }); - } - } - } - backSpaceText() { String originalText = textEditingController.text; dynamic text; @@ -339,32 +199,38 @@ class _InputTextFieldState extends TIMUIKitState { // _addDeleteTag(); } else { text = originalText.characters.skipLast(1); - textEditingController.text = "$text"; + textEditingController.text = text; // handleSetDraftText(); } - setSendButton(); } // 和onSubmitted一样,只是保持焦点的不同 onEmojiSubmitted() { + lastText = ""; final text = textEditingController.text.trim(); final convType = widget.conversationType; if (text.isNotEmpty && text != zeroWidthSpace) { if (widget.model.repliedMessage != null) { MessageUtils.handleMessageError( widget.model.sendReplyMessage( - text: text, convID: widget.conversationID, convType: convType), + text: text, + convID: widget.conversationID, + convType: convType, + atUserIDList: getUserIdFromMemberInfoMap(), + ), context); } else { MessageUtils.handleMessageError( widget.model.sendTextMessage( - text: text, convID: widget.conversationID, convType: convType), + text: text, + convID: widget.conversationID, + convType: convType, + ), context); } textEditingController.clear(); goDownBottom(); } - setSendButton(); currentCursor = null; } @@ -400,13 +266,17 @@ class _InputTextFieldState extends TIMUIKitState { } onSubmitted() async { + lastText = ""; final text = textEditingController.text.trim(); final convType = widget.conversationType; if (text.isNotEmpty && text != zeroWidthSpace) { if (widget.model.repliedMessage != null) { MessageUtils.handleMessageError( widget.model.sendReplyMessage( - text: text, convID: widget.conversationID, convType: convType), + text: text, + convID: widget.conversationID, + convType: convType, + atUserIDList: getUserIdFromMemberInfoMap()), context); } else if (memberInfoMap.isNotEmpty) { widget.model.sendTextAtMessage( @@ -422,23 +292,17 @@ class _InputTextFieldState extends TIMUIKitState { } textEditingController.clear(); currentCursor = null; - if (showKeyboard) { - focusNode.requestFocus(); - } lastText = ""; memberInfoMap = {}; - setState(() { - if (textEditingController.text.isEmpty) { - showMoreButton = true; - } - }); + goDownBottom(); _handleSendEditStatus("", false); } } void goDownBottom() { - if(globalModel.getMessageListPosition(widget.conversationID) == HistoryMessagePosition.notShowLatest){ + if (globalModel.getMessageListPosition(widget.conversationID) == + HistoryMessagePosition.notShowLatest) { return; } Future.delayed(const Duration(milliseconds: 50), () { @@ -450,35 +314,20 @@ class _InputTextFieldState extends TIMUIKitState { curve: Curves.ease, ); } - } catch (e) { - // ignore: avoid_print - print(e); - } + // ignore: empty_catches + } catch (e) {} }); } - _hideAllPanel() { - focusNode.unfocus(); - currentCursor == null; - if (showKeyboard != false || showMore != false || showEmojiPanel != false) { - setState(() { - showKeyboard = false; - showMore = false; - showEmojiPanel = false; - }); - } - } - void onModelChanged() { if (widget.model.repliedMessage != null) { - showKeyboard = true; + narrowTextFieldKey.currentState?.showKeyboard = true; focusNode.requestFocus(); _addDeleteTag(); } else {} if (widget.model.editRevokedMsg != "") { - showKeyboard = true; + narrowTextFieldKey.currentState?.showKeyboard = true; focusNode.requestFocus(); - textEditingController.clear(); textEditingController.text = widget.model.editRevokedMsg; textEditingController.selection = TextSelection.fromPosition(TextPosition( affinity: TextAffinity.downstream, @@ -505,15 +354,11 @@ class _InputTextFieldState extends TIMUIKitState { } } - _getShowName(V2TimGroupMemberFullInfo? item) { - final nameCard = item?.nameCard ?? ""; - final nickName = item?.nickName ?? ""; - final userID = item?.userID ?? ""; - return nameCard.isNotEmpty - ? nameCard - : nickName.isNotEmpty - ? nickName - : userID; + String _getShowName(V2TimGroupMemberFullInfo? item) { + return TencentUtils.checkStringWithoutSpace(item?.nameCard) ?? + TencentUtils.checkStringWithoutSpace(item?.nickName) ?? + TencentUtils.checkStringWithoutSpace(item?.userID) ?? + ""; } _longPressToAt(String? userID, String? nickName) { @@ -523,47 +368,145 @@ class _InputTextFieldState extends TIMUIKitState { ); final showName = _getShowName(memberInfo); memberInfoMap["@$showName"] = memberInfo; - String text = "$lastText@$showName "; + String text = "${textEditingController.text}@$showName "; //please do not delete space + focusNode.requestFocus(); textEditingController.text = text; textEditingController.selection = - TextSelection.fromPosition(TextPosition(offset: text.length)); + TextSelection.fromPosition(TextPosition(offset: text.length - 1)); lastText = text; } + bool shouldRemoveAtTag(String atTag, String deletedChar) { + final atMemberArray = []; + memberInfoMap.forEach((key, value) { + atMemberArray.add(key); + }); + for (String member in atMemberArray) { + if (atTag == member && member.contains(deletedChar)) { + return true; + } + } + return false; + } + + Offset getAtPosition(String text, int atPlace) { + final textBeforeAt = text.substring(0, atPlace + 1); + final textPainter = TextPainter( + text: TextSpan(text: textBeforeAt, style: const TextStyle(fontSize: 14)), + textDirection: TextDirection.ltr, + maxLines: null, + ); + textPainter.layout(maxWidth: inputWidth); + final TextPosition lastLineOffset = textPainter + .getPositionForOffset(Offset(textPainter.width, textPainter.height)); + final Offset caretPosition = + textPainter.getOffsetForCaret(lastLineOffset, Rect.zero); + final dx = min(inputWidth - 180, caretPosition.dx + 16); + final dy = max(24, 110 - caretPosition.dy).toDouble(); + + return Offset(dx, dy); + } + + calculateRemoveRemainAt(String text) { + Map map = {}; + Iterable matches = atTextReg.allMatches(text); + List parseAtList = []; + for (final item in matches) { + final str = item.group(0); + parseAtList.add(str); + } + for (String? key in parseAtList) { + if (key != null && memberInfoMap[key] != null) { + map[key] = memberInfoMap[key]!; + } + } + memberInfoMap = map; + } + _handleAtText(String text, TUIChatSeparateViewModel model) async { + final text = textEditingController.text; String? groupID = widget.conversationType == ConvType.group ? widget.conversationID : null; + final isDesktopScreen = + TUIKitScreenUtils.getFormFactor(context) == DeviceType.Desktop; if (groupID == null) { lastText = text; return; } - RegExp atTextReg = RegExp(r'@([^@\s]*)'); - int textLength = text.length; // 删除的话 - if (lastText.length > textLength && text != "@") { - Map map = {}; - Iterable matches = atTextReg.allMatches(text); - List parseAtList = []; - for (final item in matches) { - final str = item.group(0); - parseAtList.add(str); - } - for (String? key in parseAtList) { - if (key != null && memberInfoMap[key] != null) { - map[key] = memberInfoMap[key]!; + if (lastText.length > textLength) { + final List differencesList = diff(lastText, text); + final diffIndex = differencesList.first.text.length - 1; + int atIndex = lastText.lastIndexOf('@', diffIndex); + int spaceIndex = lastText.indexOf(' ', diffIndex); + if (diffIndex < 0 || atIndex < 0 || spaceIndex <= atIndex) { + lastText = text; + } else { + String atTag = lastText.substring(atIndex, spaceIndex); + String deletedChar = lastText[diffIndex]; + if (shouldRemoveAtTag(atTag, deletedChar)) { + final newText = lastText.substring(0, atIndex) + + lastText.substring(spaceIndex + 1); + textEditingController.text = newText; + textEditingController.selection = + TextSelection.collapsed(offset: atIndex); + lastText = newText; + Map map = {}; + Iterable matches = atTextReg.allMatches(text); + List parseAtList = []; + for (final item in matches) { + final str = item.group(0); + parseAtList.add(str); + } + for (String? key in parseAtList) { + if (key != null && memberInfoMap[key] != null) { + map[key] = memberInfoMap[key]!; + } + } + memberInfoMap = map; + return; } } - memberInfoMap = map; } - // 有@的情况并且是文本新增的时候 - if (textLength > 0 && + + if (isDesktopScreen) { + final atPlace = text.lastIndexOf("@"); + final keyword = text.substring(atPlace + 1); + if (atPlace >= 0) { + if (text[textLength - 1] == "@") { + final atPosition = getAtPosition(text, atPlace); + model.atPositionX = atPosition.dx; + model.atPositionY = atPosition.dy; + isAddingAtSearchWords = true; + } + final List showAtMemberList = + (model.groupMemberList ?? []).where((element) { + final friendRemark = element?.friendRemark ?? ""; + final nickName = element?.nickName ?? ""; + final showName = TencentUtils.checkString(friendRemark) ?? + TencentUtils.checkString(nickName) ?? + TencentUtils.checkString(element?.userID) ?? + ""; + return showName.contains(keyword); + }).toList(); + + model.activeAtIndex = 0; + model.showAtMemberList = showAtMemberList; + + isAddingAtSearchWords = showAtMemberList.isNotEmpty; + } else { + model.activeAtIndex = -1; + model.showAtMemberList = []; + isAddingAtSearchWords = false; + } + } else if (textLength > 0 && text[textLength - 1] == "@" && - (lastText.length < textLength || text == "@")) { + lastText.length < textLength) { V2TimGroupMemberFullInfo? memberInfo = await Navigator.push( context, MaterialPageRoute( @@ -577,38 +520,104 @@ class _InputTextFieldState extends TIMUIKitState { final showName = _getShowName(memberInfo); if (memberInfo != null) { memberInfoMap["@$showName"] = memberInfo; - //please don not delete space textEditingController.text = "$text$showName "; + lastText = "$text$showName "; } } - lastText = text; + lastText = textEditingController.text; } - _debounce( - Function(String text) fun, [ - Duration delay = const Duration(milliseconds: 30), - ]) { - Timer? timer; - return (String text) { - if (timer != null) { - timer?.cancel(); - } + void replaceAtTag(String selectedMember) { + int cursorPosition = textEditingController.selection.baseOffset; + int atIndex = + textEditingController.text.lastIndexOf('@', cursorPosition - 1); + if (atIndex >= 0) { + String beforeAt = textEditingController.text.substring(0, atIndex); + String afterAt = textEditingController.text.substring(cursorPosition); + textEditingController.text = + beforeAt + '@' + selectedMember + ' ' + afterAt; + textEditingController.selection = + TextSelection.collapsed(offset: atIndex + selectedMember.length + 2); + lastText = beforeAt + '@' + selectedMember + ' ' + afterAt; + } + } - timer = Timer(delay, () { - fun(text); - }); - }; + void handleAtMember( + {V2TimGroupMemberFullInfo? memberInfo, + bool? isAddToCursorPosition = false}) { + if (memberInfo != null) { + final String showName = _getShowName(memberInfo); + memberInfoMap["@$showName"] = memberInfo; + replaceAtTag(showName); + widget.model.showAtMemberList = []; + widget.model.activeAtIndex = -1; + } } @override void initState() { super.initState(); - if (PlatformUtils().isWeb) { + if (PlatformUtils().isWeb || PlatformUtils().isDesktop) { focusNode = FocusNode( onKey: (node, event) { - if (event.isKeyPressed(LogicalKeyboardKey.enter)) { - return KeyEventResult.handled; + final activeIndex = widget.model.activeAtIndex; + final showMemberList = widget.model.showAtMemberList; + if (event.runtimeType == RawKeyDownEvent) { + if (event.physicalKey == PhysicalKeyboardKey.backspace) { + if (textEditingController.text.isEmpty && lastText.isEmpty) { + widget.model.repliedMessage = null; + return KeyEventResult.handled; + } + } else if ((event.isShiftPressed || + event.isAltPressed || + event.isControlPressed || + event.isMetaPressed) && + event.physicalKey == PhysicalKeyboardKey.enter) { + final offset = textEditingController.selection.baseOffset; + textEditingController.text = + '${lastText.substring(0, offset)}\n${lastText.substring(offset)}'; + textEditingController.selection = + TextSelection.fromPosition(TextPosition(offset: offset + 1)); + lastText = textEditingController.text; + + return KeyEventResult.handled; + } else if (event.physicalKey == PhysicalKeyboardKey.enter) { + if (!isAddingAtSearchWords || + widget.model.showAtMemberList.isEmpty) { + onSubmitted(); + } else { + isAddingAtSearchWords = false; + final V2TimGroupMemberFullInfo? memberInfo = + showMemberList[activeIndex]; + if (memberInfo != null) { + handleAtMember( + memberInfo: memberInfo, isAddToCursorPosition: true); + } + } + return KeyEventResult.handled; + } + + if (event.isKeyPressed(LogicalKeyboardKey.arrowUp) && + isAddingAtSearchWords && + showMemberList.isNotEmpty) { + final newIndex = max(activeIndex - 1, 0); + widget.model.activeAtIndex = newIndex; + widget.atMemberPanelScroll?.scrollToIndex(newIndex, + preferPosition: AutoScrollPosition.middle); + return KeyEventResult.handled; + } + + if (event.isKeyPressed(LogicalKeyboardKey.arrowDown) && + isAddingAtSearchWords && + showMemberList.isNotEmpty) { + final newIndex = min(activeIndex + 1, showMemberList.length - 1); + widget.model.activeAtIndex = newIndex; + widget.atMemberPanelScroll?.scrollToIndex(newIndex, + preferPosition: AutoScrollPosition.middle); + return KeyEventResult.handled; + } } + return KeyEventResult.ignored; }, ); @@ -620,9 +629,6 @@ class _InputTextFieldState extends TIMUIKitState { if (widget.controller != null) { widget.controller?.addListener(() { final actionType = widget.controller?.actionType; - if (actionType == ActionType.hideAllPanel) { - _hideAllPanel(); - } if (actionType == ActionType.longPressToAt) { _longPressToAt( widget.controller?.atUserID, widget.controller?.atUserName); @@ -633,13 +639,26 @@ class _InputTextFieldState extends TIMUIKitState { if (widget.initText != null) { textEditingController.text = widget.initText!; } - final AppLocale appLocale = I18nUtils.findDeviceLocale(); + final AppLocale appLocale = I18nUtils.findDeviceLocale(null); languageType = (appLocale == AppLocale.zhHans || appLocale == AppLocale.zhHant) ? 'zh' : 'en'; } + @override + void didUpdateWidget(TIMUIKitInputTextField oldWidget) { + super.didUpdateWidget(oldWidget); + if (widget.conversationID != oldWidget.conversationID) { + handleSetDraftText(oldWidget.conversationID, oldWidget.conversationType); + if (oldWidget.initText != widget.initText) { + textEditingController.text = widget.initText ?? ""; + } else { + textEditingController.clear(); + } + } + } + @override void dispose() { handleSetDraftText(); @@ -663,6 +682,9 @@ class _InputTextFieldState extends TIMUIKitState { } _getMuteType(TUIChatSeparateViewModel model) async { + if (!mounted) { + return; + } if (widget.conversationType == ConvType.group) { if ((model.groupInfo?.isAllMuted ?? false) && muteStatus != MuteStatus.all) { @@ -689,6 +711,12 @@ class _InputTextFieldState extends TIMUIKitState { }); }); } + } else { + Future.delayed(const Duration(seconds: 0), () { + setState(() { + muteStatus = MuteStatus.none; + }); + }); } } @@ -710,45 +738,11 @@ class _InputTextFieldState extends TIMUIKitState { @override Widget tuiBuild(BuildContext context, TUIKitBuildValue value) { - final theme = value.theme; final TUIChatSeparateViewModel model = Provider.of(context); - setKeyboardHeight ??= OptimizeUtils.debounce((height) { - settingModel.keyboardHeight = height; - }, const Duration(seconds: 1)); - _getMuteType(model); - final debounceFunc = _debounce((value) { - if (isWebDevice() || isAndroidDevice()) { - if (value.isEmpty && showMoreButton != true) { - setState(() { - showMoreButton = true; - }); - } else if (value.isNotEmpty && showMoreButton == true) { - setState(() { - showMoreButton = false; - }); - } - } - if (widget.onChanged != null) { - widget.onChanged!(value); - } - _handleAtText(value, model); - _handleSendEditStatus(value, true); - final isEmpty = value.isEmpty; - if (isEmpty) { - _handleSoftKeyBoardDelete(); - } - }, const Duration(milliseconds: 80)); - - final MediaQueryData data = MediaQuery.of(context); - EdgeInsets padding = data.padding; - if (bottomPadding == null || padding.bottom > bottomPadding!) { - bottomPadding = padding.bottom; - } - return Selector( builder: ((context, value, child) { String? getForbiddenText() { @@ -765,208 +759,80 @@ class _InputTextFieldState extends TIMUIKitState { } final forbiddenText = getForbiddenText(); - return Column( - children: [ - _buildRepliedMessage(value), - Container( - color: widget.backgroundColor ?? hexToColor("f5f5f6"), - child: Column( - children: [ - Container( - padding: const EdgeInsets.symmetric( - vertical: 8, horizontal: 16), - constraints: const BoxConstraints(minHeight: 50), - child: Row( - children: [ - if (forbiddenText != null) - Expanded( - child: Container( - height: 35, - color: theme.weakBackgroundColor, - alignment: Alignment.center, - child: Text( - TIM_t(forbiddenText), - textAlign: TextAlign.center, - style: TextStyle( - fontWeight: FontWeight.bold, - fontSize: 16, - color: theme.weakTextColor, - ), - ), - )), - if (!PlatformUtils().isWeb && - widget.showSendAudio && - forbiddenText == null) - InkWell( - onTap: () async { - showKeyboard = showSendSoundText; - if (showSendSoundText) { - focusNode.requestFocus(); - } - if (await Permissions.checkPermission( - context, - Permission.microphone.value, - theme, - )) { - setState(() { - showEmojiPanel = false; - showMore = false; - showSendSoundText = !showSendSoundText; - }); - } - }, - child: SvgPicture.asset( - showSendSoundText - ? 'images/keyboard.svg' - : 'images/voice.svg', - package: 'tencent_cloud_chat_uikit', - color: const Color.fromRGBO(68, 68, 68, 1), - height: 28, - width: 28, - ), - ), - if (forbiddenText == null) - const SizedBox( - width: 10, - ), - if (forbiddenText == null) - Expanded( - child: showSendSoundText - ? SendSoundMessage( - onDownBottom: goDownBottom, - conversationID: widget.conversationID, - conversationType: widget.conversationType) - : KeyboardVisibility( - child: ExtendedTextField( - maxLines: 4, - minLines: 1, - focusNode: focusNode, - onChanged: debounceFunc, - onTap: () { - showKeyboard = true; - goDownBottom(); - setState(() { - showEmojiPanel = false; - showMore = false; - }); - }, - keyboardType: TextInputType.multiline, - textInputAction: - PlatformUtils().isAndroid - ? TextInputAction.newline - : TextInputAction.send, - onEditingComplete: onSubmitted, - textAlignVertical: - TextAlignVertical.top, - decoration: InputDecoration( - border: InputBorder.none, - hintStyle: const TextStyle( - // fontSize: 10, - color: Color(0xffAEA4A3), - ), - fillColor: Colors.white, - filled: true, - isDense: true, - hintText: widget.hintText ?? ''), - controller: textEditingController, - specialTextSpanBuilder: PlatformUtils() - .isWeb - ? null - : DefaultSpecialTextSpanBuilder( - isUseDefaultEmoji: - widget.isUseDefaultEmoji, - customEmojiStickerList: widget - .customEmojiStickerList, - showAtBackground: true, - )), - onChanged: (bool visibility) { - if (showKeyboard != visibility) { - setState(() { - showKeyboard = visibility; - }); - } - }), - ), - if (forbiddenText == null) - const SizedBox( - width: 10, - ), - if (widget.showSendEmoji && forbiddenText == null) - InkWell( - onTap: () { - _openEmojiPanel(); - goDownBottom(); - }, - child: PlatformUtils().isWeb - ? Icon( - showEmojiPanel - ? Icons.keyboard_alt_outlined - : Icons.mood_outlined, - color: hexToColor("5c6168"), - size: 32) - : SvgPicture.asset( - showEmojiPanel - ? 'images/keyboard.svg' - : 'images/face.svg', - package: 'tencent_cloud_chat_uikit', - color: - const Color.fromRGBO(68, 68, 68, 1), - height: 28, - width: 28, - ), - ), - if (forbiddenText == null) - const SizedBox( - width: 10, - ), - if (widget.showMorePanel && - forbiddenText == null && - showMoreButton) - InkWell( - onTap: () { - // model.sendCustomMessage(data: "a", convID: model.currentSelectedConv, convType: model.currentSelectedConvType == 1 ? ConvType.c2c : ConvType.group); - _openMore(); - goDownBottom(); - }, - child: PlatformUtils().isWeb - ? Icon(Icons.add_circle_outline_outlined, - color: hexToColor("5c6168"), size: 32) - : SvgPicture.asset( - 'images/add.svg', - package: 'tencent_cloud_chat_uikit', - color: - const Color.fromRGBO(68, 68, 68, 1), - height: 28, - width: 28, - ), - ), - if ((isAndroidDevice() || isWebDevice()) && - !showMoreButton) - SizedBox( - height: 32.0, - child: ElevatedButton( - onPressed: onSubmitted, - child: Text(TIM_t("发送")), - ), - ), - ], - ), - ), - AnimatedContainer( - duration: Duration( - milliseconds: - (showKeyboard && PlatformUtils().isAndroid) - ? 200 - : 340), - curve: Curves.fastOutSlowIn, - height: max(_getBottomHeight(), 0.0), - child: _getBottomContainer(), - ), - ], - ), - ) - ], - ); + return LayoutBuilder( + builder: (BuildContext context, BoxConstraints constraints) { + inputWidth = constraints.maxWidth; + return TUIKitScreenUtils.getDeviceWidget( + defaultWidget: TIMUIKitTextFieldLayoutNarrow( + onEmojiSubmitted: onEmojiSubmitted, + onCustomEmojiFaceSubmitted: onCustomEmojiFaceSubmitted, + backSpaceText: backSpaceText, + addStickerToText: addStickerToText, + customStickerPanel: widget.customStickerPanel, + forbiddenText: forbiddenText, + onChanged: widget.onChanged, + backgroundColor: widget.backgroundColor, + morePanelConfig: widget.morePanelConfig, + repliedMessage: value, + currentCursor: currentCursor, + hintText: widget.hintText, + isUseDefaultEmoji: widget.isUseDefaultEmoji, + languageType: languageType, + textEditingController: textEditingController, + conversationID: widget.conversationID, + conversationType: widget.conversationType, + focusNode: focusNode, + controller: widget.controller, + setCurrentCursor: setCurrentCursor, + onCursorChange: _onCursorChange, + model: model, + handleSendEditStatus: _handleSendEditStatus, + handleAtText: (text) { + _handleAtText(text, model); + }, + handleSoftKeyBoardDelete: _handleSoftKeyBoardDelete, + onSubmitted: onSubmitted, + goDownBottom: goDownBottom, + showSendAudio: widget.showSendAudio, + showSendEmoji: widget.showSendEmoji, + showMorePanel: widget.showMorePanel, + customEmojiStickerList: widget.customEmojiStickerList), + desktopWidget: TIMUIKitTextFieldLayoutWide( + currentConversation: widget.currentConversation, + onEmojiSubmitted: onEmojiSubmitted, + onCustomEmojiFaceSubmitted: onCustomEmojiFaceSubmitted, + backSpaceText: backSpaceText, + addStickerToText: addStickerToText, + customStickerPanel: widget.customStickerPanel, + forbiddenText: forbiddenText, + onChanged: widget.onChanged, + backgroundColor: widget.backgroundColor, + morePanelConfig: widget.morePanelConfig, + repliedMessage: value, + currentCursor: currentCursor, + hintText: widget.hintText, + isUseDefaultEmoji: widget.isUseDefaultEmoji, + languageType: languageType, + textEditingController: textEditingController, + conversationID: widget.conversationID, + conversationType: widget.conversationType, + focusNode: focusNode, + controller: widget.controller, + setCurrentCursor: setCurrentCursor, + onCursorChange: _onCursorChange, + model: model, + handleSendEditStatus: _handleSendEditStatus, + handleAtText: (text) { + _handleAtText(text, model); + }, + handleSoftKeyBoardDelete: _handleSoftKeyBoardDelete, + onSubmitted: onSubmitted, + goDownBottom: goDownBottom, + showSendAudio: widget.showSendAudio, + showSendEmoji: widget.showSendEmoji, + showMorePanel: widget.showMorePanel, + customEmojiStickerList: widget.customEmojiStickerList)); + }); }), selector: (c, model) => model.repliedMessage); } diff --git a/lib/ui/views/TIMUIKitChat/TIMUIKitTextField/tim_uikit_text_field_layout/narrow.dart b/lib/ui/views/TIMUIKitChat/TIMUIKitTextField/tim_uikit_text_field_layout/narrow.dart new file mode 100644 index 0000000..40679d4 --- /dev/null +++ b/lib/ui/views/TIMUIKitChat/TIMUIKitTextField/tim_uikit_text_field_layout/narrow.dart @@ -0,0 +1,611 @@ +import 'dart:async'; +import 'dart:math'; + +import 'package:flutter/material.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_base.dart'; +import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_state.dart'; +import 'package:tencent_cloud_chat_uikit/business_logic/separate_models/tui_chat_separate_view_model.dart'; +import 'package:tencent_cloud_chat_uikit/business_logic/view_models/tui_chat_global_model.dart'; +import 'package:tencent_cloud_chat_uikit/business_logic/view_models/tui_setting_model.dart'; +import 'package:tencent_cloud_chat_uikit/data_services/services_locatar.dart'; +import 'package:tencent_cloud_chat_uikit/tencent_cloud_chat_uikit.dart'; +import 'package:tencent_cloud_chat_uikit/ui/utils/message.dart'; +import 'package:tencent_cloud_chat_uikit/ui/utils/optimize_utils.dart'; +import 'package:tencent_cloud_chat_uikit/ui/utils/permission.dart'; +import 'package:tencent_cloud_chat_uikit/ui/utils/platform.dart'; +import 'package:tencent_cloud_chat_uikit/ui/views/TIMUIKitChat/TIMUIKitTextField/special_text/DefaultSpecialTextSpanBuilder.dart'; +import 'package:tencent_cloud_chat_uikit/ui/views/TIMUIKitChat/TIMUIKitTextField/tim_uikit_emoji_panel.dart'; +import 'package:tencent_cloud_chat_uikit/ui/views/TIMUIKitChat/TIMUIKitTextField/tim_uikit_send_sound_message.dart'; +import 'package:tencent_extended_text_field/extended_text_field.dart'; +import 'package:tencent_keyboard_visibility/tencent_keyboard_visibility.dart'; + +GlobalKey<_TIMUIKitTextFieldLayoutNarrowState> narrowTextFieldKey = GlobalKey(); + +class TIMUIKitTextFieldLayoutNarrow extends StatefulWidget { + /// sticker panel customization + final CustomStickerPanel? customStickerPanel; + + final VoidCallback onEmojiSubmitted; + final Function(int, String) onCustomEmojiFaceSubmitted; + final Function(String, bool) handleSendEditStatus; + final VoidCallback backSpaceText; + final ValueChanged addStickerToText; + + final ValueChanged handleAtText; + + /// Whether to use the default emoji + final bool isUseDefaultEmoji; + + final TUIChatSeparateViewModel model; + + /// background color + final Color? backgroundColor; + + /// control input field behavior + final TIMUIKitInputTextFieldController? controller; + + /// config for more panel + final MorePanelConfig? morePanelConfig; + + final String languageType; + + final TextEditingController textEditingController; + + /// conversation id + final String conversationID; + + /// conversation type + final ConvType conversationType; + + final FocusNode focusNode; + + /// show more panel + final bool showMorePanel; + + /// hint text for textField widget + final String? hintText; + + final int? currentCursor; + + final ValueChanged setCurrentCursor; + + final VoidCallback onCursorChange; + + /// show send audio icon + final bool showSendAudio; + + final VoidCallback handleSoftKeyBoardDelete; + + /// on text changed + final void Function(String)? onChanged; + + final V2TimMessage? repliedMessage; + + /// show send emoji icon + final bool showSendEmoji; + + final String? forbiddenText; + + final VoidCallback onSubmitted; + + final VoidCallback goDownBottom; + + final List customEmojiStickerList; + + const TIMUIKitTextFieldLayoutNarrow( + {Key? key, + this.customStickerPanel, + required this.onEmojiSubmitted, + required this.onCustomEmojiFaceSubmitted, + required this.backSpaceText, + required this.addStickerToText, + required this.isUseDefaultEmoji, + required this.languageType, + required this.textEditingController, + this.morePanelConfig, + required this.conversationID, + required this.conversationType, + required this.focusNode, + this.currentCursor, + required this.setCurrentCursor, + required this.onCursorChange, + required this.model, + this.backgroundColor, + this.onChanged, + required this.handleSendEditStatus, + required this.handleAtText, + required this.handleSoftKeyBoardDelete, + this.repliedMessage, + this.forbiddenText, + required this.onSubmitted, + required this.goDownBottom, + required this.showSendAudio, + required this.showSendEmoji, + required this.showMorePanel, + this.hintText, + required this.customEmojiStickerList, + this.controller}) + : super(key: key); + + @override + State createState() => + _TIMUIKitTextFieldLayoutNarrowState(); +} + +class _TIMUIKitTextFieldLayoutNarrowState + extends TIMUIKitState { + final TUISettingModel settingModel = serviceLocator(); + + bool showMore = false; + bool showMoreButton = true; + bool showSendSoundText = false; + bool showEmojiPanel = false; + bool showKeyboard = false; + Function? setKeyboardHeight; + double? bottomPadding; + + @override + void initState() { + super.initState(); + if (widget.controller != null) { + widget.controller?.addListener(() { + final actionType = widget.controller?.actionType; + if (actionType == ActionType.hideAllPanel) { + hideAllPanel(); + } + }); + } + } + + void setSendButton() { + final value = widget.textEditingController.text; + if (isWebDevice() || isAndroidDevice()) { + if (value.isEmpty && showMoreButton != true) { + setState(() { + showMoreButton = true; + }); + } else if (value.isNotEmpty && showMoreButton == true) { + setState(() { + showMoreButton = false; + }); + } + } + } + + hideAllPanel() { + widget.focusNode.unfocus(); + widget.currentCursor == null; + if (showKeyboard != false || showMore != false || showEmojiPanel != false) { + setState(() { + showKeyboard = false; + showMore = false; + showEmojiPanel = false; + }); + } + } + + Widget _getBottomContainer() { + if (showEmojiPanel) { + return widget.customStickerPanel != null + ? widget.customStickerPanel!( + sendTextMessage: () { + widget.onEmojiSubmitted(); + setSendButton(); + }, + sendFaceMessage: widget.onCustomEmojiFaceSubmitted, + deleteText: () { + widget.backSpaceText(); + setSendButton(); + }, + addText: (int unicode) { + final newText = String.fromCharCode(unicode); + widget.addStickerToText(newText); + // handleSetDraftText(); + }, + addCustomEmojiText: ((String singleEmojiName) { + String? emojiName = singleEmojiName.split('.png')[0]; + if (widget.isUseDefaultEmoji && + widget.languageType == 'zh' && + ConstData.emojiMapList[emojiName] != null && + ConstData.emojiMapList[emojiName] != '') { + emojiName = ConstData.emojiMapList[emojiName]; + } + final newText = '[$emojiName]'; + widget.addStickerToText(newText); + setSendButton(); + }), + defaultCustomEmojiStickerList: + widget.isUseDefaultEmoji ? ConstData.emojiList : []) + : EmojiPanel(onTapEmoji: (unicode) { + final newText = String.fromCharCode(unicode); + widget.addStickerToText(newText); + setSendButton(); + // handleSetDraftText(); + }, onSubmitted: () { + widget.onEmojiSubmitted(); + setSendButton(); + }, delete: () { + widget.backSpaceText(); + setSendButton(); + }); + } + + if (showMore) { + return MorePanel( + morePanelConfig: widget.morePanelConfig, + conversationID: widget.conversationID, + conversationType: widget.conversationType); + } + + return const SizedBox(height: 0); + } + + double _getBottomHeight() { + if (showKeyboard) { + final currentKeyboardHeight = MediaQuery.of(context).viewInsets.bottom; + double originHeight = settingModel.keyboardHeight; + if (currentKeyboardHeight != 0) { + if (currentKeyboardHeight >= originHeight) { + originHeight = currentKeyboardHeight; + } + if (setKeyboardHeight != null) { + setKeyboardHeight!(currentKeyboardHeight); + } + } + final height = originHeight != 0 ? originHeight : currentKeyboardHeight; + return height; + } else if (showMore || showEmojiPanel) { + return 248.0 + (bottomPadding ?? 0.0); + } else if (widget.textEditingController.text.length >= 46 && + showKeyboard == false) { + return 25 + (bottomPadding ?? 0.0); + } else { + return bottomPadding ?? 0; + } + } + + _openMore() { + if (!showMore) { + widget.focusNode.unfocus(); + widget.setCurrentCursor(null); + } + setState(() { + showKeyboard = false; + showEmojiPanel = false; + showSendSoundText = false; + showMore = !showMore; + }); + } + + _openEmojiPanel() { + widget.onCursorChange(); + showKeyboard = showEmojiPanel; + if (showEmojiPanel) { + widget.focusNode.requestFocus(); + } else { + widget.focusNode.unfocus(); + } + + setState(() { + showMore = false; + showSendSoundText = false; + showEmojiPanel = !showEmojiPanel; + }); + } + + _debounce( + Function(String text) fun, [ + Duration delay = const Duration(milliseconds: 30), + ]) { + Timer? timer; + return (String text) { + if (timer != null) { + timer?.cancel(); + } + + timer = Timer(delay, () { + fun(text); + }); + }; + } + + _buildRepliedMessage(V2TimMessage? repliedMessage) { + final haveRepliedMessage = repliedMessage != null; + if (haveRepliedMessage) { + final text = + "${MessageUtils.getDisplayName(widget.model.repliedMessage!)}:${widget.model.abstractMessageBuilder != null ? widget.model.abstractMessageBuilder!(widget.model.repliedMessage!) : MessageUtils.getAbstractMessageAsync(widget.model.repliedMessage!, widget.model.groupMemberList ?? [])}"; + return Container( + color: widget.backgroundColor ?? hexToColor("f5f5f6"), + alignment: Alignment.centerLeft, + padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 16), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Text( + text, + softWrap: true, + maxLines: 3, + overflow: TextOverflow.ellipsis, + style: TextStyle(color: hexToColor("8f959e"), fontSize: 14), + ), + ), + const SizedBox( + width: 16, + ), + InkWell( + onTap: () { + widget.model.repliedMessage = null; + }, + child: Icon(Icons.clear, color: hexToColor("8f959e"), size: 18), + ) + ], + ), + ); + } + return Container(); + } + + @override + Widget tuiBuild(BuildContext context, TUIKitBuildValue value) { + final theme = value.theme; + + setKeyboardHeight ??= OptimizeUtils.debounce((height) { + settingModel.keyboardHeight = height; + }, const Duration(seconds: 1)); + + final debounceFunc = _debounce((value) { + if (isWebDevice() || isAndroidDevice()) { + if (value.isEmpty && showMoreButton != true) { + setState(() { + showMoreButton = true; + }); + } else if (value.isNotEmpty && showMoreButton == true) { + setState(() { + showMoreButton = false; + }); + } + } + if (widget.onChanged != null) { + widget.onChanged!(value); + } + widget.handleAtText(value); + widget.handleSendEditStatus(value, true); + final isEmpty = value.isEmpty; + if (isEmpty) { + widget.handleSoftKeyBoardDelete(); + } + }, const Duration(milliseconds: 80)); + + final MediaQueryData data = MediaQuery.of(context); + EdgeInsets padding = data.padding; + if (bottomPadding == null || padding.bottom > bottomPadding!) { + bottomPadding = padding.bottom; + } + + return Column( + children: [ + _buildRepliedMessage(widget.repliedMessage), + Container( + color: widget.backgroundColor ?? hexToColor("f5f5f6"), + child: Column( + children: [ + Container( + padding: + const EdgeInsets.symmetric(vertical: 8, horizontal: 16), + constraints: const BoxConstraints(minHeight: 50), + child: Row( + children: [ + if (widget.forbiddenText != null) + Expanded( + child: Container( + height: 35, + color: theme.weakBackgroundColor, + alignment: Alignment.center, + child: Text( + TIM_t(widget.forbiddenText!), + textAlign: TextAlign.center, + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 16, + color: theme.weakTextColor, + ), + ), + )), + if (PlatformUtils().isMobile && + widget.showSendAudio && + widget.forbiddenText == null) + InkWell( + onTap: () async { + showKeyboard = showSendSoundText; + if (showSendSoundText) { + widget.focusNode.requestFocus(); + } + if (await Permissions.checkPermission( + context, + Permission.microphone.value, + theme, + )) { + setState(() { + showEmojiPanel = false; + showMore = false; + showSendSoundText = !showSendSoundText; + }); + } + }, + child: SvgPicture.asset( + showSendSoundText + ? 'images/keyboard.svg' + : 'images/voice.svg', + package: 'tencent_cloud_chat_uikit', + color: const Color.fromRGBO(68, 68, 68, 1), + height: 28, + width: 28, + ), + ), + if (widget.forbiddenText == null) + const SizedBox( + width: 10, + ), + if (widget.forbiddenText == null) + Expanded( + child: showSendSoundText + ? SendSoundMessage( + onDownBottom: widget.goDownBottom, + conversationID: widget.conversationID, + conversationType: widget.conversationType) + : KeyboardVisibility( + child: ExtendedTextField( + maxLines: 4, + minLines: 1, + focusNode: widget.focusNode, + onChanged: debounceFunc, + onTap: () { + showKeyboard = true; + widget.goDownBottom(); + setState(() { + showEmojiPanel = false; + showMore = false; + }); + }, + keyboardType: TextInputType.multiline, + textInputAction: PlatformUtils().isAndroid + ? TextInputAction.newline + : TextInputAction.send, + onEditingComplete: () { + widget.onSubmitted(); + if (showKeyboard) { + widget.focusNode.requestFocus(); + } + setState(() { + if (widget.textEditingController.text + .isEmpty) { + showMoreButton = true; + } + }); + }, + textAlignVertical: TextAlignVertical.top, + decoration: InputDecoration( + border: InputBorder.none, + hintStyle: const TextStyle( + // fontSize: 10, + color: Color(0xffAEA4A3), + ), + fillColor: Colors.white, + filled: true, + isDense: true, + hintText: widget.hintText ?? ''), + controller: widget.textEditingController, + specialTextSpanBuilder: PlatformUtils() + .isWeb + ? null + : DefaultSpecialTextSpanBuilder( + isUseDefaultEmoji: + widget.isUseDefaultEmoji, + customEmojiStickerList: + widget.customEmojiStickerList, + showAtBackground: true, + )), + onChanged: (bool visibility) { + if (showKeyboard != visibility) { + setState(() { + showKeyboard = visibility; + }); + } + }), + ), + if (widget.forbiddenText == null) + const SizedBox( + width: 10, + ), + if (widget.showSendEmoji && widget.forbiddenText == null) + InkWell( + onTap: () { + _openEmojiPanel(); + widget.goDownBottom(); + }, + child: PlatformUtils().isWeb + ? Icon( + showEmojiPanel + ? Icons.keyboard_alt_outlined + : Icons.mood_outlined, + color: hexToColor("5c6168"), + size: 32) + : SvgPicture.asset( + showEmojiPanel + ? 'images/keyboard.svg' + : 'images/face.svg', + package: 'tencent_cloud_chat_uikit', + color: const Color.fromRGBO(68, 68, 68, 1), + height: 28, + width: 28, + ), + ), + if (widget.forbiddenText == null) + const SizedBox( + width: 10, + ), + if (widget.showMorePanel && + widget.forbiddenText == null && + showMoreButton) + InkWell( + onTap: () { + // model.sendCustomMessage(data: "a", convID: model.currentSelectedConv, convType: model.currentSelectedConvType == 1 ? ConvType.c2c : ConvType.group); + _openMore(); + widget.goDownBottom(); + }, + child: PlatformUtils().isWeb + ? Icon(Icons.add_circle_outline_outlined, + color: hexToColor("5c6168"), size: 32) + : SvgPicture.asset( + 'images/add.svg', + package: 'tencent_cloud_chat_uikit', + color: const Color.fromRGBO(68, 68, 68, 1), + height: 28, + width: 28, + ), + ), + if ((isAndroidDevice() || isWebDevice()) && !showMoreButton) + SizedBox( + height: 32.0, + child: ElevatedButton( + onPressed: () { + widget.onSubmitted(); + if (showKeyboard) { + widget.focusNode.requestFocus(); + } + setState(() { + if (widget.textEditingController.text.isEmpty) { + showMoreButton = true; + } + }); + }, + child: Text(TIM_t("发送")), + ), + ), + ], + ), + ), + AnimatedContainer( + duration: Duration( + milliseconds: (showKeyboard && PlatformUtils().isAndroid) + ? 200 + : 340), + curve: Curves.fastOutSlowIn, + height: max(_getBottomHeight(), 0.0), + child: ListView( + physics: const NeverScrollableScrollPhysics(), + children: [ + _getBottomContainer() + ], + ), + ), + ], + ), + ) + ], + ); + } +} diff --git a/lib/ui/views/TIMUIKitChat/TIMUIKitTextField/tim_uikit_text_field_layout/wide.dart b/lib/ui/views/TIMUIKitChat/TIMUIKitTextField/tim_uikit_text_field_layout/wide.dart new file mode 100644 index 0000000..ef8f10d --- /dev/null +++ b/lib/ui/views/TIMUIKitChat/TIMUIKitTextField/tim_uikit_text_field_layout/wide.dart @@ -0,0 +1,963 @@ +import 'dart:async'; +import 'dart:io'; +import 'dart:math'; +import 'package:fc_native_video_thumbnail_for_us/fc_native_video_thumbnail_for_us.dart'; +import 'package:flutter/services.dart'; +import 'package:package_info_plus/package_info_plus.dart'; +import 'package:pasteboard/pasteboard.dart'; +import 'package:path/path.dart' as p; +import 'package:file_picker/file_picker.dart'; +import 'package:flutter/material.dart'; +import 'package:image_picker/image_picker.dart'; +import 'package:path_provider/path_provider.dart'; +import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_base.dart'; +import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_state.dart'; +import 'package:tencent_cloud_chat_uikit/business_logic/separate_models/tui_chat_separate_view_model.dart'; +import 'package:tencent_cloud_chat_uikit/business_logic/view_models/tui_chat_global_model.dart'; +import 'package:tencent_cloud_chat_uikit/business_logic/view_models/tui_setting_model.dart'; +import 'package:tencent_cloud_chat_uikit/data_services/core/tim_uikit_wide_modal_operation_key.dart'; +import 'package:tencent_cloud_chat_uikit/data_services/services_locatar.dart'; +import 'package:tencent_cloud_chat_uikit/tencent_cloud_chat_uikit.dart'; +import 'package:tencent_cloud_chat_uikit/ui/utils/message.dart'; +import 'package:tencent_cloud_chat_uikit/ui/utils/optimize_utils.dart'; +import 'package:tencent_cloud_chat_uikit/ui/utils/screen_shot.dart'; +import 'package:tencent_cloud_chat_uikit/ui/widgets/wide_popup.dart'; +import 'package:tencent_cloud_chat_uikit/ui/utils/platform.dart'; +import 'package:tencent_cloud_chat_uikit/ui/views/TIMUIKitChat/TIMUIKitTextField/special_text/DefaultSpecialTextSpanBuilder.dart'; +import 'package:tencent_cloud_chat_uikit/ui/views/TIMUIKitChat/TIMUIKitTextField/tim_uikit_emoji_panel.dart'; +import 'package:tencent_cloud_chat_uikit/ui/widgets/drag_widget.dart'; +import 'package:tencent_extended_text_field/extended_text_field.dart'; +import 'package:universal_html/html.dart' as html; +import 'package:url_launcher/url_launcher.dart'; +import 'package:uuid/uuid.dart'; +import 'package:wechat_assets_picker/wechat_assets_picker.dart'; + +// ignore: unnecessary_import +import 'dart:typed_data'; + +class DesktopControlBarItem { + final String item; + final IconData? icon; + final String? imgPath; + final Color? color; + final ValueChanged onClick; + final String? showName; + final double? size; + + DesktopControlBarItem( + {required this.item, + this.icon, + this.color, + this.imgPath, + required this.onClick, + this.showName, + this.size}) + : assert(icon != null || imgPath != null); +} + +class DesktopControlBarConfig { + final bool showStickerPanel; + final bool showScreenshotButton; + final bool showSendFileButton; + final bool showSendImageButton; + final bool showSendVideoButton; + final bool showMessageHistoryButton; + + DesktopControlBarConfig({ + this.showStickerPanel = true, + this.showScreenshotButton = true, + this.showSendFileButton = true, + this.showSendImageButton = true, + this.showSendVideoButton = true, + this.showMessageHistoryButton = true, + }); +} + +class TIMUIKitTextFieldLayoutWide extends StatefulWidget { + /// sticker panel customization + final CustomStickerPanel? customStickerPanel; + final VoidCallback onEmojiSubmitted; + final Function(int, String) onCustomEmojiFaceSubmitted; + final Function(String, bool) handleSendEditStatus; + final VoidCallback backSpaceText; + final ValueChanged addStickerToText; + + final ValueChanged handleAtText; + + /// Whether to use the default emoji + final bool isUseDefaultEmoji; + + final TUIChatSeparateViewModel model; + + /// background color + final Color? backgroundColor; + + /// control input field behavior + final TIMUIKitInputTextFieldController? controller; + + /// config for more panel + final MorePanelConfig? morePanelConfig; + + final String languageType; + + final TextEditingController textEditingController; + + /// conversation id + final String conversationID; + + /// conversation type + final ConvType conversationType; + + final FocusNode focusNode; + + /// show more panel + final bool showMorePanel; + + /// hint text for textField widget + final String? hintText; + + final int? currentCursor; + + final ValueChanged setCurrentCursor; + + final VoidCallback onCursorChange; + + /// show send audio icon + final bool showSendAudio; + + final VoidCallback handleSoftKeyBoardDelete; + + /// on text changed + final void Function(String)? onChanged; + + final V2TimMessage? repliedMessage; + + /// show send emoji icon + final bool showSendEmoji; + + final String? forbiddenText; + + final VoidCallback onSubmitted; + + final VoidCallback goDownBottom; + + final List customEmojiStickerList; + + /// Conversation need search + final V2TimConversation currentConversation; + + const TIMUIKitTextFieldLayoutWide( + {Key? key, + this.customStickerPanel, + required this.onEmojiSubmitted, + required this.onCustomEmojiFaceSubmitted, + required this.backSpaceText, + required this.addStickerToText, + required this.isUseDefaultEmoji, + required this.languageType, + required this.textEditingController, + this.morePanelConfig, + required this.conversationID, + required this.conversationType, + required this.focusNode, + this.currentCursor, + required this.setCurrentCursor, + required this.onCursorChange, + required this.model, + this.backgroundColor, + this.onChanged, + required this.handleSendEditStatus, + required this.handleAtText, + required this.handleSoftKeyBoardDelete, + this.repliedMessage, + this.forbiddenText, + required this.onSubmitted, + required this.goDownBottom, + required this.showSendAudio, + required this.showSendEmoji, + required this.showMorePanel, + this.hintText, + required this.customEmojiStickerList, + this.controller, + required this.currentConversation}) + : super(key: key); + + @override + State createState() => + _TIMUIKitTextFieldLayoutWideState(); +} + +class _TIMUIKitTextFieldLayoutWideState + extends TIMUIKitState { + final TUISettingModel settingModel = serviceLocator(); + OverlayEntry? entry; + final ImagePicker _picker = ImagePicker(); + Uint8List? fileContent; + String? fileName; + File? tempFile; + Function? setKeyboardHeight; + double? bottomPadding; + late ScrollController _scrollController; + late FocusNode textFocusNode; + + @override + void initState() { + super.initState(); + if (widget.controller != null) { + widget.controller?.addListener(() { + final actionType = widget.controller?.actionType; + if (actionType == ActionType.hideAllPanel) { + hideAllPanel(); + } + }); + } + textFocusNode = FocusNode(); + widget.focusNode.requestFocus(); + _scrollController = ScrollController(); + } + + hideAllPanel() { + widget.focusNode.unfocus(); + widget.currentCursor == null; + } + + _debounce( + Function(String text) fun, [ + Duration delay = const Duration(milliseconds: 30), + ]) { + Timer? timer; + return (String text) { + if (timer != null) { + timer?.cancel(); + } + + timer = Timer(delay, () { + fun(text); + }); + }; + } + + _buildRepliedMessage(V2TimMessage? repliedMessage) { + final haveRepliedMessage = repliedMessage != null; + if (haveRepliedMessage) { + final text = + "${MessageUtils.getDisplayName(widget.model.repliedMessage!)}:${widget.model.abstractMessageBuilder != null ? widget.model.abstractMessageBuilder!(widget.model.repliedMessage!) : MessageUtils.getAbstractMessageAsync(widget.model.repliedMessage!, widget.model.groupMemberList ?? [])}"; + return Container( + color: widget.backgroundColor ?? hexToColor("f5f5f6"), + alignment: Alignment.centerLeft, + padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 16), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Text( + text, + softWrap: true, + maxLines: 3, + overflow: TextOverflow.ellipsis, + style: TextStyle(color: hexToColor("8f959e"), fontSize: 14), + ), + ), + const SizedBox( + width: 16, + ), + InkWell( + onTap: () { + widget.model.repliedMessage = null; + }, + child: Icon(Icons.clear, color: hexToColor("8f959e"), size: 18), + ) + ], + ), + ); + } + return Container(); + } + + _sendEmoji(Offset? offset, TUITheme theme) { + widget.onCursorChange(); + if (entry != null) { + entry?.remove(); + entry = null; + } else { + entry = OverlayEntry(builder: (BuildContext context) { + return TUIKitDragArea( + closeFun: () { + if (entry != null) { + entry?.remove(); + entry = null; + } + }, + initOffset: offset ?? + Offset(MediaQuery.of(context).size.height * 0.5 + 20, + MediaQuery.of(context).size.height * 0.5 - 100), + child: Container( + decoration: BoxDecoration( + borderRadius: const BorderRadius.all(Radius.circular(8)), + color: theme.wideBackgroundColor, + border: Border.all( + width: 2, + color: theme.weakBackgroundColor ?? const Color(0xFFbebebe), + ), + boxShadow: const [ + BoxShadow( + color: Color(0xFFbebebe), + offset: Offset(5, 5), + blurRadius: 10, + spreadRadius: 1, + ), + ], + ), + child: Container( + child: widget.customStickerPanel != null + ? widget.customStickerPanel!( + height: 400, + width: 350, + sendTextMessage: () { + widget.onEmojiSubmitted(); + }, + sendFaceMessage: widget.onCustomEmojiFaceSubmitted, + deleteText: () { + widget.backSpaceText(); + }, + addText: (int unicode) { + final newText = String.fromCharCode(unicode); + widget.addStickerToText(newText); + }, + addCustomEmojiText: ((String singleEmojiName) { + String? emojiName = singleEmojiName.split('.png')[0]; + if (widget.isUseDefaultEmoji && + widget.languageType == 'zh' && + ConstData.emojiMapList[emojiName] != null && + ConstData.emojiMapList[emojiName] != '') { + emojiName = ConstData.emojiMapList[emojiName]; + } + final newText = '[$emojiName]'; + widget.addStickerToText(newText); + }), + defaultCustomEmojiStickerList: + widget.isUseDefaultEmoji ? ConstData.emojiList : []) + : EmojiPanel(onTapEmoji: (unicode) { + final newText = String.fromCharCode(unicode); + widget.addStickerToText(newText); + }, onSubmitted: () { + widget.onEmojiSubmitted(); + }, delete: () { + widget.backSpaceText(); + }), + ), + )); + }); + Overlay.of(context)?.insert(entry!); + } + } + + _addGreyOverlay() { + if (entry != null) { + _removeOverlay(); + return; + } else { + entry = OverlayEntry(builder: (BuildContext context) { + return Container( + color: const Color(0x7F000000), + ); + }); + Overlay.of(context)?.insert(entry!); + } + } + + _removeOverlay() { + entry!.remove(); + entry = null; + } + + _sendFile( + TUIChatSeparateViewModel model, + TUITheme theme, + ) async { + if (!PlatformUtils().isWeb) { + _addGreyOverlay(); + } + try { + final convID = widget.conversationID; + final convType = widget.conversationType; + FilePickerResult? result = await FilePicker.platform.pickFiles(); + _removeOverlay(); + if (result != null && result.files.isNotEmpty) { + if (PlatformUtils().isWeb) { + html.Node? inputElem; + inputElem = html.document + .getElementById("__file_picker_web-file-input") + ?.querySelector("input"); + fileName = result.files.single.name; + + MessageUtils.handleMessageError( + model.sendFileMessage( + inputElement: inputElem, + fileName: fileName, + convID: convID, + convType: convType), + context); + return; + } + + File file = File(result.files.single.path!); + final int size = file.lengthSync(); + final String savePath = file.path; + + MessageUtils.handleMessageError( + model.sendFileMessage( + filePath: savePath, + size: size, + convID: convID, + convType: convType), + context); + } else { + throw TypeError(); + } + } catch (e) { + // ignore: avoid_print + print("_sendFileErr: ${e.toString()}"); + } + } + + List generateBarIcons( + List items, TUITheme theme) { + return items.map((e) { + final GlobalKey key = GlobalKey(); + return Container( + margin: const EdgeInsets.only(right: 6), + child: InkWell( + onTap: () { + final alignBox = + key.currentContext?.findRenderObject() as RenderBox?; + var offset = alignBox?.localToGlobal(Offset.zero); + final double? dx = (offset?.dx != null) ? offset!.dx : null; + final double? dy = + (offset?.dy != null && alignBox?.size.height != null) + ? offset!.dy - 420 + : null; + e.onClick((dx != null && dy != null) ? Offset(dx, dy) : null); + }, + child: Tooltip( + preferBelow: false, + textStyle: TextStyle(fontSize: 12, color: theme.white), + message: e.showName, + child: Container( + decoration: + BoxDecoration(borderRadius: BorderRadius.circular(2)), + padding: const EdgeInsets.all(4), + child: e.imgPath != null + ? Image.asset( + e.imgPath!, + key: key, + width: e.size ?? 20, + height: e.size ?? 20, + ) + : Icon(e.icon, + key: key, + color: e.color ?? hexToColor("646a73"), + size: e.size ?? 20)), + ), + ), + ); + }).toList(); + } + + _sendImageFileOnWeb(TUIChatSeparateViewModel model) async { + try { + final pickedFile = await _picker.pickImage(source: ImageSource.gallery); + final imageContent = await pickedFile!.readAsBytes(); + fileName = pickedFile.name; + tempFile = File(pickedFile.path); + fileContent = imageContent; + + html.Node? inputElem; + inputElem = html.document + .getElementById("__image_picker_web-file-input") + ?.querySelector("input"); + final convID = widget.conversationID; + final convType = widget.conversationType; + MessageUtils.handleMessageError( + model.sendImageMessage( + inputElement: inputElem, + imagePath: tempFile?.path, + convID: convID, + convType: convType), + context); + } catch (e) { + // ignore: avoid_print + print("_sendFileErr: ${e.toString()}"); + } + } + + _sendVideoFileOnWeb(TUIChatSeparateViewModel model) async { + try { + final pickedFile = await _picker.pickVideo(source: ImageSource.gallery); + final videoContent = await pickedFile!.readAsBytes(); + fileName = pickedFile.name; + tempFile = File(pickedFile.path); + fileContent = videoContent; + + if (fileName!.split(".")[fileName!.split(".").length - 1] != "mp4") { + onTIMCallback(TIMCallback( + type: TIMCallbackType.INFO, + infoRecommendText: TIM_t("视频消息仅限 mp4 格式"), + infoCode: 6660412)); + return; + } + + html.Node? inputElem; + inputElem = html.document + .getElementById("__image_picker_web-file-input") + ?.querySelector("input"); + final convID = widget.conversationID; + final convType = widget.conversationType; + MessageUtils.handleMessageError( + model.sendVideoMessage( + inputElement: inputElem, + videoPath: tempFile?.path, + convID: convID, + convType: convType), + context); + } catch (e) { + // ignore: avoid_print + print("_sendFileErr: ${e.toString()}"); + } + } + + _sendVideoMessage(AssetEntity asset, TUIChatSeparateViewModel model) async { + try { + final plugin = FcNativeVideoThumbnail(); + final originFile = await asset.originFile; + final size = await originFile!.length(); + if (size >= 104857600) { + onTIMCallback(TIMCallback( + type: TIMCallbackType.INFO, + infoRecommendText: TIM_t("发送失败,视频不能大于100MB"), + infoCode: 6660405)); + return; + } + + final duration = asset.videoDuration.inSeconds; + final filePath = originFile.path; + final convID = widget.conversationID; + final convType = widget.conversationType; + + String tempPath = (await getTemporaryDirectory()).path + + p.extension(originFile.path, 3) + + ".jpeg"; + + await plugin.getVideoThumbnail( + srcFile: originFile.path, + keepAspectRatio: true, + destFile: tempPath, + format: 'jpeg', + width: 128, + quality: 100, + height: 128, + ); + MessageUtils.handleMessageError( + model.sendVideoMessage( + videoPath: filePath, + duration: duration, + snapshotPath: tempPath, + convID: convID, + convType: convType), + context); + } catch (e) { + onTIMCallback(TIMCallback( + type: TIMCallbackType.INFO, + infoRecommendText: TIM_t("视频文件异常"), + infoCode: 6660415)); + } + } + + _sendMediaMessage( + TUIChatSeparateViewModel model, TUITheme theme, FileType fileType) async { + try { + final convID = widget.conversationID; + final convType = widget.conversationType; + + if (PlatformUtils().isMobile) { + final pickedAssets = await AssetPicker.pickAssets(context); + + if (pickedAssets != null) { + for (var asset in pickedAssets) { + final originFile = await asset.originFile; + final filePath = originFile?.path; + final type = asset.type; + if (filePath != null) { + if (type == AssetType.image) { + MessageUtils.handleMessageError( + model.sendImageMessage( + imagePath: filePath, + convID: convID, + convType: convType), + context); + } + + if (type == AssetType.video) { + _sendVideoMessage(asset, model); + } + } + } + } + } else { + final plugin = FcNativeVideoThumbnail(); + _addGreyOverlay(); + FilePickerResult? result = + await FilePicker.platform.pickFiles(type: fileType); + _removeOverlay(); + if (result != null && result.files.isNotEmpty) { + File file = File(result.files.single.path!); + final String savePath = file.path; + final String type = TencentUtils.getFileType( + (savePath.split(".")[savePath.split(".").length - 1]) + .toLowerCase()) + .split("/")[0]; + + if (type == "image") { + MessageUtils.handleMessageError( + model.sendImageMessage( + imagePath: savePath, convID: convID, convType: convType), + context); + } else if (type == "video") { + String tempPath = (await getTemporaryDirectory()).path + + p.basename(savePath) + + ".jpeg"; + await plugin.getVideoThumbnail( + srcFile: savePath, + keepAspectRatio: true, + destFile: tempPath, + format: 'jpeg', + width: 128, + quality: 100, + height: 128, + ); + MessageUtils.handleMessageError( + model.sendVideoMessage( + videoPath: savePath, + convID: convID, + convType: convType, + snapshotPath: tempPath), + context); + } + } else { + throw TypeError(); + } + } + } catch (err) { + // ignore: avoid_print + print("send media err: $err"); + onTIMCallback(TIMCallback( + type: TIMCallbackType.INFO, + infoRecommendText: TIM_t("视频文件异常"), + infoCode: 6660415)); + } + } + + _sendImageWithConfirmation(String file) async { + final option1 = widget.currentConversation.showName ?? + (widget.conversationType == ConvType.group ? TIM_t("群聊") : TIM_t("对方")); + final size = await ScreenshotHelper.getImageSize(file); + + TUIKitWidePopup.showPopupWindow( + operationKey: TUIKitWideModalOperationKey.beforeSendScreenShot, + context: context, + isDarkBackground: false, + width: 500, + height: min(500, size.height / 2 + 140), + title: TIM_t_para("发送给{{option1}}", "发送给$option1")(option1: option1), + child: (closeFunc) => Container( + padding: const EdgeInsets.all(16), + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + SizedBox( + height: min(360, size.height / 2), + child: InkWell( + onTap: () { + launchUrl(Uri.file(file)); + }, + child: Image.file( + File(file), + height: min(360, size.height / 2), + ), + ), + ), + Row( + mainAxisAlignment: MainAxisAlignment.end, + mainAxisSize: MainAxisSize.max, + children: [ + OutlinedButton( + onPressed: () { + closeFunc(); + }, + child: Text(TIM_t("取消"))), + const SizedBox( + width: 20, + ), + ElevatedButton( + onPressed: () { + MessageUtils.handleMessageError( + widget.model.sendImageMessage( + imagePath: file, + convID: widget.conversationID, + convType: widget.conversationType), + context); + closeFunc(); + }, + child: Text(TIM_t("发送"))) + ], + ) + ], + ), + )); + } + + _sendScreenShot() async { + final file = await ScreenshotHelper.captureScreen(); + if (file != null) { + _sendImageWithConfirmation(file); + } else {} + } + + List generateControlBar( + TUIChatSeparateViewModel model, TUITheme theme) { + final DesktopControlBarConfig config = + widget.model.chatConfig.desktopControlBarConfig ?? + DesktopControlBarConfig(); + final List itemsList = [ + if (config.showStickerPanel) + DesktopControlBarItem( + item: "face", + showName: TIM_t("表情"), + onClick: (offset) { + _sendEmoji(offset, theme); + }, + icon: Icons.mood), + if (config.showScreenshotButton && PlatformUtils().isDesktop) + DesktopControlBarItem( + item: "screenShot", + showName: TIM_t("截图"), + onClick: (offset) { + _sendScreenShot(); + }, + icon: Icons.cut_outlined), + if (config.showSendFileButton) + DesktopControlBarItem( + item: "file", + showName: TIM_t("文件"), + onClick: (offset) { + _sendFile(widget.model, theme); + }, + icon: Icons.file_copy_outlined), + if (config.showSendImageButton) + DesktopControlBarItem( + item: "photo", + showName: TIM_t("图片"), + onClick: (offset) { + if (PlatformUtils().isWeb) { + _sendImageFileOnWeb(widget.model); + } else { + _sendMediaMessage(widget.model, theme, FileType.image); + } + }, + icon: Icons.image_outlined), + if (config.showSendVideoButton) + DesktopControlBarItem( + item: "video", + showName: TIM_t("视频"), + onClick: (offset) { + if (PlatformUtils().isWeb) { + _sendVideoFileOnWeb(widget.model); + } else { + _sendMediaMessage(widget.model, theme, FileType.video); + } + }, + icon: Icons.video_library_outlined), + if (config.showMessageHistoryButton) + DesktopControlBarItem( + item: "history", + showName: TIM_t("消息历史"), + onClick: (offset) { + TUIKitWidePopup.showPopupWindow( + operationKey: TUIKitWideModalOperationKey.chatHistory, + context: context, + width: MediaQuery.of(context).size.width * 0.5, + height: MediaQuery.of(context).size.width * 0.5, + child: (onClose) => TIMUIKitSearchMsgDetail( + currentConversation: widget.currentConversation, + keyword: '', + initMessageList: widget.model + .getOriginMessageList() + .getRange( + 0, + min(widget.model.getOriginMessageList().length, + 100)) + .toList(), + onTapConversation: (V2TimConversation conversation, + V2TimMessage? message) {}, + ), + theme: theme); + }, + icon: Icons.chat_outlined), + ...(widget.model.chatConfig.additionalDesktopControlBarItems ?? []) + ]; + + return generateBarIcons(itemsList, theme); + } + + Future _handleKeyEvent(RawKeyEvent event) async { + if ((event.isKeyPressed(LogicalKeyboardKey.controlLeft) && + event.logicalKey == LogicalKeyboardKey.keyV) || + (event.isMetaPressed && event.logicalKey == LogicalKeyboardKey.keyV)) { + final bytes = await Pasteboard.image; + if (bytes != null) { + String directory; + if (PlatformUtils().isWindows) { + final String documentsDirectoryPath = + "${Platform.environment['USERPROFILE']}"; + PackageInfo packageInfo = await PackageInfo.fromPlatform(); + String pkgName = packageInfo.packageName; + directory = p.join(documentsDirectoryPath, "Documents", + ".TencentCloudChat", pkgName, "screenshots"); + } else { + final dic = await getApplicationSupportDirectory(); + directory = dic.path; + } + const uuid = Uuid(); + final fileName = 'paste_image_${uuid.v4()}.png'; + final scDirectory = Directory(directory); + final filePath = + '${scDirectory.path}${PlatformUtils().isWindows ? "\\" : "/"}$fileName'; + final file = File(filePath); + if (!await scDirectory.exists()) { + await scDirectory.create(recursive: true); + } + await file.writeAsBytes(bytes.toList()); + _sendImageWithConfirmation(filePath); + } + } + } + + @override + Widget tuiBuild(BuildContext context, TUIKitBuildValue value) { + final theme = value.theme; + + setKeyboardHeight ??= OptimizeUtils.debounce((height) { + settingModel.keyboardHeight = height; + }, const Duration(seconds: 1)); + + final debounceFunc = _debounce((value) { + if (widget.onChanged != null) { + widget.onChanged!(value); + } + widget.handleAtText(value); + widget.handleSendEditStatus(value, true); + final isEmpty = value.isEmpty; + if (isEmpty) { + widget.handleSoftKeyBoardDelete(); + } + }, const Duration(milliseconds: 80)); + + final MediaQueryData data = MediaQuery.of(context); + EdgeInsets padding = data.padding; + if (bottomPadding == null || padding.bottom > bottomPadding!) { + bottomPadding = padding.bottom; + } + + return RawKeyboardListener( + focusNode: textFocusNode, + onKey: _handleKeyEvent, + child: Container( + color: widget.backgroundColor, + child: Column( + children: [ + SizedBox( + height: 1, + child: Container( + color: theme.weakDividerColor ?? Colors.black12)), + if (widget.forbiddenText == null) + Container( + padding: + const EdgeInsets.symmetric(vertical: 4, horizontal: 12), + child: Row( + mainAxisAlignment: MainAxisAlignment.start, + children: generateControlBar(widget.model, theme), + ), + ), + _buildRepliedMessage(widget.repliedMessage), + Container( + padding: const EdgeInsets.symmetric(vertical: 6, horizontal: 6), + constraints: const BoxConstraints(minHeight: 50), + child: Row( + children: [ + if (widget.forbiddenText != null) + Expanded( + child: Container( + height: 35, + color: theme.weakBackgroundColor, + alignment: Alignment.center, + child: Text( + TIM_t(widget.forbiddenText!), + textAlign: TextAlign.center, + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 16, + color: theme.weakTextColor, + ), + ), + )), + if (widget.forbiddenText == null) + Expanded( + child: Scrollbar( + controller: _scrollController, + child: ExtendedTextField( + autofocus: true, + maxLines: 6, + minLines: 6, + focusNode: widget.focusNode, + onChanged: debounceFunc, + keyboardType: TextInputType.multiline, + textInputAction: PlatformUtils().isAndroid + ? TextInputAction.newline + : TextInputAction.send, + onEditingComplete: () { + widget.onSubmitted(); + }, + textAlignVertical: TextAlignVertical.top, + style: const TextStyle(fontSize: 14), + decoration: InputDecoration( + hoverColor: hexToColor("fafafa"), + border: InputBorder.none, + hintStyle: const TextStyle( + color: Color(0xffAEA4A3), + ), + fillColor: hexToColor("fafafa"), + filled: true, + isDense: true, + hintText: widget.hintText ?? '', + ), + controller: widget.textEditingController, + specialTextSpanBuilder: PlatformUtils().isWeb + ? null + : DefaultSpecialTextSpanBuilder( + isUseDefaultEmoji: + widget.isUseDefaultEmoji, + customEmojiStickerList: + widget.customEmojiStickerList, + showAtBackground: true, + )), + ), + ), + ], + ), + ), + ], + ), + )); + } +} diff --git a/lib/ui/views/TIMUIKitChat/tim_uikit_chat.dart b/lib/ui/views/TIMUIKitChat/tim_uikit_chat.dart index 27d3158..a294e8f 100644 --- a/lib/ui/views/TIMUIKitChat/tim_uikit_chat.dart +++ b/lib/ui/views/TIMUIKitChat/tim_uikit_chat.dart @@ -1,5 +1,6 @@ // ignore_for_file: must_be_immutable, avoid_print +import 'package:desktop_drop/desktop_drop.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; @@ -16,7 +17,9 @@ import 'package:tencent_cloud_chat_uikit/ui/constants/history_message_constant.d import 'package:tencent_cloud_chat_uikit/ui/controller/tim_uikit_chat_controller.dart'; import 'package:tencent_cloud_chat_uikit/ui/utils/frame.dart'; import 'package:tencent_cloud_chat_uikit/ui/utils/optimize_utils.dart'; +import 'package:tencent_cloud_chat_uikit/ui/views/TIMUIKitChat/TIMUIKitTextField/at_member_panel.dart'; import 'package:tencent_cloud_chat_uikit/ui/views/TIMUIKitChat/tim_uikit_multi_select_panel.dart'; +import 'package:tencent_cloud_chat_uikit/ui/views/TIMUIKitChat/tim_uikit_send_file.dart'; import 'TIMUIKItMessageList/TIMUIKitTongue/tim_uikit_chat_history_message_list_tongue.dart'; import 'TIMUIKItMessageList/tim_uikit_chat_history_message_list_config.dart'; import 'TIMUIKItMessageList/tim_uikit_history_message_list_container.dart'; @@ -27,7 +30,7 @@ class TIMUIKitChat extends StatefulWidget { int endTime = 0; /// The chat controller you tend to used. - /// You have to provide this before using it after tencent_cloud_chat_uikit 0.1.4. + /// You have to provide this before using it since tencent_cloud_chat_uikit 0.1.4. final TIMUIKitChatController? controller; /// [Update] It is suggested to provide the `V2TimConversation` once directly, since tencent_cloud_chat_uikit 1.5.0. @@ -41,6 +44,7 @@ class TIMUIKitChat extends StatefulWidget { /// This field is not necessary to be provided, when `conversation` is provided, unless you want to cover this field manually. final String? conversationID; + /// Conversation type. /// This field is not necessary to be provided, when `conversation` is provided, unless you want to cover this field manually. final ConvType? conversationType; @@ -48,11 +52,12 @@ class TIMUIKitChat extends StatefulWidget { final Widget Function(BuildContext context, V2TimMessage message)? userAvatarBuilder; + /// Use for show conversation name. /// This field is not necessary to be provided, when `conversation` is provided, unless you want to cover this field manually. final String? conversationShowName; /// Avatar and name in message reaction tap callback. - final void Function(String userID)? onTapAvatar; + final void Function(String userID, TapDownDetails tapDetails)? onTapAvatar; @Deprecated( "Nickname will not shows in one-to-one chat, if you tend to control it in group chat, please use `isShowSelfNameInGroup` and `isShowOthersNameInGroup` from `config: TIMUIKitChatConfig` instead") @@ -123,15 +128,10 @@ class TIMUIKitChat extends StatefulWidget { final List customEmojiStickerList; + final Widget? customAppBar; + /// Custom emoji panel. - final Widget Function( - {void Function() sendTextMessage, - void Function(int index, String data) sendFaceMessage, - void Function() deleteText, - void Function(int unicode) addText, - void Function(String singleEmojiName) addCustomEmojiText, - List defaultCustomEmojiStickerList})? - customStickerPanel; + final CustomStickerPanel? customStickerPanel; /// Custom text field final Widget Function(BuildContext context)? textFieldBuilder; @@ -139,6 +139,7 @@ class TIMUIKitChat extends StatefulWidget { TIMUIKitChat( {Key? key, this.groupID, + required this.conversation, this.conversationID, this.conversationType, this.conversationShowName, @@ -169,7 +170,7 @@ class TIMUIKitChat extends StatefulWidget { this.topFixWidget = const SizedBox(), this.textFieldBuilder, this.customEmojiStickerList = const [], - required this.conversation}) + this.customAppBar}) : super(key: key) { startTime = DateTime.now().millisecondsSinceEpoch; } @@ -179,11 +180,14 @@ class TIMUIKitChat extends StatefulWidget { } class _TUIChatState extends TIMUIKitState { - final TUIChatSeparateViewModel model = TUIChatSeparateViewModel(); + TUIChatSeparateViewModel model = TUIChatSeparateViewModel(); final TUIThemeViewModel themeViewModel = serviceLocator(); final TIMUIKitInputTextFieldController textFieldController = TIMUIKitInputTextFieldController(); bool isInit = false; + final TUIChatGlobalModel chatGlobalModel = + serviceLocator(); + bool _dragging = false; final GlobalKey alignKey = GlobalKey(); final GlobalKey listContainerKey = GlobalKey(); @@ -194,23 +198,11 @@ class _TUIChatState extends TIMUIKitState { axis: Axis.vertical, ); - String _getTitle() { - return TencentUtils.checkString(widget.conversationShowName) ?? - widget.conversation.showName ?? - "Chat"; - } - - String _getConvID() { - return TencentUtils.checkString(widget.conversationID) ?? - (widget.conversation.type == 1 - ? widget.conversation.userID - : widget.conversation.groupID) ?? - ""; - } - - ConvType _getConvType() { - return widget.conversation.type == 1 ? ConvType.c2c : ConvType.group; - } + late AutoScrollController atMemberPanelScroll = AutoScrollController( + viewportBoundaryGetter: () => + Rect.fromLTRB(0, 0, 0, MediaQuery.of(context).padding.bottom), + axis: Axis.vertical, + ); @override void initState() { @@ -220,7 +212,7 @@ class _TUIChatState extends TIMUIKitState { } model.abstractMessageBuilder = widget.abstractMessageBuilder; model.onTapAvatar = widget.onTapAvatar; - WidgetsBinding.instance?.addPostFrameCallback((_) async { + WidgetsBinding.instance.addPostFrameCallback((_) async { if (kProfileMode) { widget.endTime = DateTime.now().millisecondsSinceEpoch; int timeSpend = widget.endTime - widget.startTime; @@ -238,6 +230,29 @@ class _TUIChatState extends TIMUIKitState { model.dispose(); } + @override + void didUpdateWidget(TIMUIKitChat oldWidget) { + super.didUpdateWidget(oldWidget); + if (widget.conversationID != oldWidget.conversationID) { + isInit = false; + chatGlobalModel.clearCurrentConversation(); + model = TUIChatSeparateViewModel(); + model.abstractMessageBuilder = widget.abstractMessageBuilder; + model.onTapAvatar = widget.onTapAvatar; + Future.delayed(const Duration(milliseconds: 50), () { + try { + autoController.jumpTo( + autoController.position.minScrollExtent, + ); + autoController.jumpTo( + autoController.position.minScrollExtent, + ); + // ignore: empty_catches + } catch (e) {} + }); + } + } + Widget _renderJoinGroupApplication(int amount, TUITheme theme) { String option1 = amount.toString(); return Container( @@ -273,6 +288,24 @@ class _TUIChatState extends TIMUIKitState { ); } + String _getTitle() { + return TencentUtils.checkString(widget.conversationShowName) ?? + widget.conversation.showName ?? + "Chat"; + } + + String _getConvID() { + return TencentUtils.checkString(widget.conversationID) ?? + (widget.conversation.type == 1 + ? widget.conversation.userID + : widget.conversation.groupID) ?? + ""; + } + + ConvType _getConvType() { + return widget.conversation.type == 1 ? ConvType.c2c : ConvType.group; + } + @override Widget tuiBuild(BuildContext context, TUIKitBuildValue value) { final TUITheme theme = value.theme; @@ -299,11 +332,11 @@ class _TUIChatState extends TIMUIKitState { widget.controller?.model = model; List filteredApplicationList = []; - if (_getConvType() == ConvType.group && + if (widget.conversationType == ConvType.group && widget.onDealWithGroupApplication != null) { filteredApplicationList = chatGlobalModel.groupApplicationList.where((item) { - return (item.groupID == _getConvID()) && + return (item.groupID == widget.conversationID) && item.handleStatus == 0; }).toList(); } @@ -312,7 +345,7 @@ class _TUIChatState extends TIMUIKitState { Provider.of(context, listen: true); final NeedUpdate? needUpdate = groupListenerModel.needUpdate; if (needUpdate != null && - needUpdate.groupID == _getConvID()) { + needUpdate.groupID == widget.conversationID) { groupListenerModel.needUpdate = null; switch (needUpdate.updateType) { case UpdateType.groupInfo: @@ -327,116 +360,160 @@ class _TUIChatState extends TIMUIKitState { } } - final alignBox = - alignKey.currentContext?.findRenderObject() as RenderBox?; - final alignHeight = alignBox?.size.height ?? 0; - - final listContainerBox = - listContainerKey.currentContext?.findRenderObject() as RenderBox?; - final listContainerBoxHeight = listContainerBox?.size.height ?? 0; - return GestureDetector( onTap: () { textFieldController.hideAllPanel(); }, child: Scaffold( resizeToAvoidBottomInset: false, - appBar: TIMUIKitAppBar( - showTotalUnReadCount: widget.showTotalUnReadCount, - config: widget.appBarConfig, - conversationShowName: _getTitle(), - conversationID: _getConvID(), - showC2cMessageEditStaus: - widget.config?.showC2cMessageEditStaus ?? true, - ), - body: Column( - children: [ - if (filteredApplicationList.isNotEmpty) - _renderJoinGroupApplication( - filteredApplicationList.length, theme), - if (widget.topFixWidget != null) widget.topFixWidget!, - Expanded( - child: Align( - key: alignKey, - alignment: Alignment.topCenter, - child: Listener( - onPointerMove: closePanel, - child: TIMUIKitHistoryMessageListContainer( - conversation: widget.conversation, - textFieldController: textFieldController, - customEmojiStickerList: - widget.customEmojiStickerList, - isUseDefaultEmoji: - widget.config!.isUseDefaultEmoji, - key: listContainerKey, - isAllowScroll: - (!(listContainerBoxHeight < alignHeight)), - userAvatarBuilder: widget.userAvatarBuilder, - toolTipsConfig: widget.toolTipsConfig, - groupAtInfoList: widget.groupAtInfoList, - tongueItemBuilder: widget.tongueItemBuilder, - onLongPressForOthersHeadPortrait: - (String? userId, String? nickName) { - if (_getConvType() != ConvType.c2c) { - textFieldController.longPressToAt( - nickName, userId); - } - }, - mainHistoryListConfig: - widget.mainHistoryListConfig, - initFindingMsg: widget.initFindingMsg, - extraTipsActionItemBuilder: - widget.extraTipsActionItemBuilder ?? - widget.exteraTipsActionItemBuilder, - conversationType: _getConvType(), - scrollController: autoController, - onTapAvatar: widget.onTapAvatar, - // ignore: deprecated_member_use_from_same_package - showNickName: widget.showNickName, - messageItemBuilder: widget.messageItemBuilder, - conversationID: _getConvID(), - ), - ))), - Selector( - builder: (context, value, child) { - return value - ? MultiSelectPanel( - conversationType: _getConvType(), - ) - : (widget.textFieldBuilder != null - ? widget.textFieldBuilder!(context) - : TIMUIKitInputTextField( - groupType: widget.conversation.groupType, - model: model, - controller: textFieldController, + appBar: (widget.customAppBar == null) + ? TIMUIKitAppBar( + showTotalUnReadCount: widget.showTotalUnReadCount, + config: widget.appBarConfig, + conversationShowName: _getTitle(), + conversationID: _getConvID(), + showC2cMessageEditStatus: + widget.config?.showC2cMessageEditStatus ?? true, + ) + : null, + body: DropTarget( + onDragDone: (detail) { + setState(() { + _dragging = false; + sendFileWithConfirmation( + files: detail.files, + conversation: widget.conversation, + conversationType: _getConvType(), + model: model, + theme: theme, + context: context); + }); + }, + onDragEntered: (detail) { + setState(() { + _dragging = true; + }); + }, + onDragExited: (detail) { + setState(() { + _dragging = false; + }); + }, + child: Stack( + children: [ + Column( + children: [ + if (widget.customAppBar != null) widget.customAppBar!, + if (filteredApplicationList.isNotEmpty) + _renderJoinGroupApplication( + filteredApplicationList.length, theme), + if (widget.topFixWidget != null) widget.topFixWidget!, + Expanded( + child: Container( + color: theme.chatBgColor, + child: Align( + key: alignKey, + alignment: Alignment.topCenter, + child: Listener( + onPointerMove: closePanel, + child: TIMUIKitHistoryMessageListContainer( + conversation: widget.conversation, + textFieldController: textFieldController, customEmojiStickerList: widget.customEmojiStickerList, isUseDefaultEmoji: widget.config!.isUseDefaultEmoji, - customStickerPanel: - widget.customStickerPanel, - morePanelConfig: widget.morePanelConfig, - scrollController: autoController, - conversationID: _getConvID(), + key: listContainerKey, + isAllowScroll: true, + userAvatarBuilder: widget.userAvatarBuilder, + toolTipsConfig: widget.toolTipsConfig, + groupAtInfoList: widget.groupAtInfoList, + tongueItemBuilder: widget.tongueItemBuilder, + onLongPressForOthersHeadPortrait: + (String? userId, String? nickName) { + if (widget.conversationType != + ConvType.c2c) { + textFieldController.longPressToAt( + nickName, userId); + } + }, + mainHistoryListConfig: + widget.mainHistoryListConfig, + initFindingMsg: widget.initFindingMsg, + extraTipsActionItemBuilder: + widget.extraTipsActionItemBuilder ?? + widget.exteraTipsActionItemBuilder, conversationType: _getConvType(), - initText: widget.draftText, - hintText: widget.textFieldHintText, - showMorePanel: - widget.config?.isAllowShowMorePanel ?? - true, - showSendAudio: - widget.config?.isAllowSoundMessage ?? - true, - showSendEmoji: - widget.config?.isAllowEmojiPanel ?? - true, - )); - }, - selector: (c, model) { - return model.isMultiSelect; - }, - ) - ], + scrollController: autoController, + onTapAvatar: widget.onTapAvatar, + // ignore: deprecated_member_use_from_same_package + showNickName: widget.showNickName, + messageItemBuilder: + widget.messageItemBuilder, + conversationID: _getConvID(), + ), + )), + )), + Selector( + builder: (context, value, child) { + return value + ? MultiSelectPanel( + conversationType: _getConvType(), + ) + : (widget.textFieldBuilder != null + ? widget.textFieldBuilder!(context) + : TIMUIKitInputTextField( + key: inputTextFieldState, + atMemberPanelScroll: + atMemberPanelScroll, + groupType: + widget.conversation.groupType, + currentConversation: + widget.conversation, + model: model, + controller: textFieldController, + customEmojiStickerList: + widget.customEmojiStickerList, + isUseDefaultEmoji: + widget.config!.isUseDefaultEmoji, + customStickerPanel: + widget.customStickerPanel, + morePanelConfig: + widget.morePanelConfig, + scrollController: autoController, + conversationID: _getConvID(), + conversationType: _getConvType(), + initText: widget.draftText, + hintText: widget.textFieldHintText, + showMorePanel: widget.config + ?.isAllowShowMorePanel ?? + true, + showSendAudio: widget.config + ?.isAllowSoundMessage ?? + true, + showSendEmoji: widget + .config?.isAllowEmojiPanel ?? + true, + )); + }, + selector: (c, model) { + return model.isMultiSelect; + }, + ) + ], + ), + if (_dragging) + TIMUIKitSendFile( + conversation: widget.conversation, + ), + AtMemberPanel( + atMemberPanelScroll: atMemberPanelScroll, + onSelectMember: (member) => inputTextFieldState + .currentState + ?.handleAtMember(memberInfo: member), + ) + ], + ), )), ); }); @@ -511,14 +588,15 @@ class TIMUIKitChatProviderScope extends StatelessWidget { groupID: groupID, ); model?.showC2cMessageEditStatus = (conversationType == ConvType.c2c - ? config?.showC2cMessageEditStaus ?? true + ? config?.showC2cMessageEditStatus ?? true : false); loadData(); } loadData() { // if (model!.haveMoreData) { - model!.loadData(count: kIsWeb ? 15 : HistoryMessageDartConstant.getCount); + model!.loadChatRecord( + count: kIsWeb ? 15 : HistoryMessageDartConstant.getCount); // } } diff --git a/lib/ui/views/TIMUIKitChat/tim_uikit_chat_config.dart b/lib/ui/views/TIMUIKitChat/tim_uikit_chat_config.dart index be2bef1..7096a47 100644 --- a/lib/ui/views/TIMUIKitChat/tim_uikit_chat_config.dart +++ b/lib/ui/views/TIMUIKitChat/tim_uikit_chat_config.dart @@ -1,6 +1,8 @@ import 'package:tencent_cloud_chat_uikit/business_logic/view_models/tui_chat_global_model.dart'; import 'package:tencent_cloud_chat_uikit/tencent_cloud_chat_uikit.dart'; +import 'TIMUIKitTextField/tim_uikit_text_field_layout/wide.dart'; + enum GroupReceptAllowType { work, public, meeting } enum GroupReceiptAllowType { work, public, meeting } @@ -36,7 +38,7 @@ class TIMUIKitChatConfig { /// [Default]: true. final bool isReportGroupReadingStatus; - /// Control if allowed to show the menu after long pressing message. + /// Control if allowed to show the message operation menu after long pressing message. /// [Default]: true. final bool isAllowLongPressMessage; @@ -77,6 +79,11 @@ class TIMUIKitChatConfig { /// [Default]: true. final bool isShowOthersNameInGroup; + /// Configuration for offline push. + /// If this field is specified, `notificationTitle`, `notificationOPPOChannelID`, `notificationIOSSound`, `notificationAndroidSound`, `notificationBody` and `notificationExt` will not work. + final OfflinePushInfo? Function( + V2TimMessage message, String convID, ConvType convType)? offlinePushInfo; + /// The title shows in push notification final String notificationTitle; @@ -91,7 +98,7 @@ class TIMUIKitChatConfig { final String notificationAndroidSound; ///Used to set the line height of text messages - final double textHight; + final double textHeight; /// The body content shows in push notification. /// Returning `null` means using default body in this case. @@ -109,7 +116,7 @@ class TIMUIKitChatConfig { /// Whether to display the sending status of c2c messages /// [Default]: true. - final bool showC2cMessageEditStaus; + final bool showC2cMessageEditStatus; /// Control if take emoji stickers as message reaction. /// [Default]: true. @@ -127,7 +134,9 @@ class TIMUIKitChatConfig { /// The suffix of face sticker URI. final String Function(String data)? faceURISuffix; - /// Control if text and replied messages can be show with markdown. + /// Controls whether text and replied messages can be displayed with Markdown formatting. + /// When enabled, small image stickers, including QQ stickers, will not work in message items. + /// Also, when enabled, `isEnableTextSelection` will not works. /// [Default]: false. final bool isSupportMarkdownForTextMessage; @@ -142,12 +151,40 @@ class TIMUIKitChatConfig { /// [Default]: true. final bool isShowAvatar; + /// This list contains additional operation items that are displayed on the hover bar + /// of a message on desktop (macOS, Windows, and desktop version of Web). These items + /// are in addition to the default ones and do not affect them. + final List? additionalDesktopMessageHoverBarItem; + + /// This list contains additional items that are displayed + /// on the control bar on desktop (macOS, Windows, and desktop version of Web). + /// Use `desktopControlBarConfig` to configure whether or not to show the default control items. + final List? additionalDesktopControlBarItems; + + /// This configuration is used for the control bar + /// on desktop (macOS, Windows, and desktop version of Web). + /// Use `desktopControlBarConfig` to add additional items to the desktop control bar, in addition to the default ones. + final DesktopControlBarConfig? desktopControlBarConfig; + + /// Controls whether users are allowed to mention another user in the group by long-pressing on their avatar. + /// [Default]: true. + final bool isAllowLongPressAvatarToAt; + + /// Controls whether auto report message read status when new messages come. + /// [Default]: true. + final bool isAutoReportRead; + + /// Controls whether enable text selection. + /// [Default]: true on Desktop while false on Mobile. + final bool? isEnableTextSelection; + const TIMUIKitChatConfig( {this.onTapLink, this.timeDividerConfig, + this.isAutoReportRead = true, this.faceURIPrefix, this.faceURISuffix, - this.textHight = 1.0, + this.textHeight = 1.3, this.isAtWhenReply = true, this.notificationAndroidSound = "", this.isSupportMarkdownForTextMessage = false, @@ -155,6 +192,7 @@ class TIMUIKitChatConfig { this.isUseMessageReaction = true, this.isShowAvatar = true, this.isShowSelfNameInGroup = false, + this.offlinePushInfo, @Deprecated("Please use [isShowGroupReadingStatus] instead") this.isShowGroupMessageReadReceipt = true, this.upperRecallTime = 120, @@ -171,10 +209,15 @@ class TIMUIKitChatConfig { this.isAllowEmojiPanel = true, this.isAllowShowMorePanel = true, this.isShowReadingStatus = true, + this.desktopControlBarConfig, this.isAllowLongPressMessage = true, this.isAllowClickAvatar = true, + this.isEnableTextSelection, + this.additionalDesktopMessageHoverBarItem, this.isShowGroupReadingStatus = true, this.isReportGroupReadingStatus = true, - this.showC2cMessageEditStaus = true, + this.showC2cMessageEditStatus = true, + this.additionalDesktopControlBarItems, + this.isAllowLongPressAvatarToAt = true, this.isUseDefaultEmoji = false}); } diff --git a/lib/ui/views/TIMUIKitChat/tim_uikit_multi_select_panel.dart b/lib/ui/views/TIMUIKitChat/tim_uikit_multi_select_panel.dart index 02dfa76..93f2be6 100644 --- a/lib/ui/views/TIMUIKitChat/tim_uikit_multi_select_panel.dart +++ b/lib/ui/views/TIMUIKitChat/tim_uikit_multi_select_panel.dart @@ -4,8 +4,11 @@ import 'package:provider/provider.dart'; import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_statelesswidget.dart'; import 'package:tencent_cloud_chat_uikit/business_logic/separate_models/tui_chat_separate_view_model.dart'; import 'package:tencent_cloud_chat_uikit/business_logic/view_models/tui_chat_global_model.dart'; +import 'package:tencent_cloud_chat_uikit/data_services/core/tim_uikit_wide_modal_operation_key.dart'; +import 'package:tencent_cloud_chat_uikit/ui/utils/screen_utils.dart'; import 'package:tencent_cloud_chat_uikit/ui/widgets/forward_message_screen.dart'; +import 'package:tencent_cloud_chat_uikit/ui/widgets/wide_popup.dart'; import 'package:tencent_im_base/tencent_im_base.dart'; import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_base.dart'; @@ -28,102 +31,221 @@ class MultiSelectPanel extends TIMUIKitStatelessWidget { ))); } + _handleForwardMessageWide(BuildContext context, bool isMergerForward, + TUIChatSeparateViewModel model) { + TUIKitWidePopup.showPopupWindow( + operationKey: TUIKitWideModalOperationKey.forward, + context: context, + isDarkBackground: false, + title: TIM_t("转发"), + submitWidget: Text(TIM_t("发送")), + width: MediaQuery.of(context).size.width * 0.5, + height: MediaQuery.of(context).size.height * 0.8, + onSubmit: (){ + forwardMessageScreenKey.currentState?.handleForwardMessage(); + }, + child: (onClose) => Container( + padding: const EdgeInsets.symmetric( horizontal: 10), + child: ForwardMessageScreen( + model: model, + key: forwardMessageScreenKey, + onClose: onClose, + isMergerForward: isMergerForward, + conversationType: conversationType, + ), + ) + ); + } + @override Widget tuiBuild(BuildContext context, TUIKitBuildValue value) { final TUITheme theme = value.theme; final TUIChatSeparateViewModel model = Provider.of(context); - return Container( - decoration: BoxDecoration( - gradient: LinearGradient(colors: [ - theme.lightPrimaryColor ?? CommonColor.lightPrimaryColor, - theme.primaryColor ?? CommonColor.primaryColor - ]), - ), - padding: const EdgeInsets.only(top: 12, bottom: 48), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - Column( - children: [ - IconButton( - icon: Image.asset('images/forward.png', - package: 'tencent_cloud_chat_uikit', color: theme.white), - iconSize: 40, - onPressed: () { - _handleForwardMessage(context, false, model); - }, - ), - Text(TIM_t("逐条转发"), - style: TextStyle(color: theme.white, fontSize: 12)) - ], + return TUIKitScreenUtils.getDeviceWidget( + desktopWidget: Container( + decoration: BoxDecoration( + color: theme.selectPanelBgColor ?? theme.primaryColor, + border: Border( + top: BorderSide( + color: theme.weakDividerColor ?? Colors.grey, + width: 1.0, + ), ), - Column( - children: [ - IconButton( - icon: Image.asset('images/merge_forward.png', - package: 'tencent_cloud_chat_uikit', color: theme.white), - iconSize: 40, - onPressed: () { - _handleForwardMessage(context, true, model); - }, - ), - Text( - TIM_t("合并转发"), - style: TextStyle(color: theme.white, fontSize: 12), - ) - ], - ), - Column( - children: [ - IconButton( - icon: Image.asset('images/delete.png', - package: 'tencent_cloud_chat_uikit', color: theme.white), - iconSize: 40, - onPressed: () { - showCupertinoModalPopup( - context: context, - builder: (BuildContext context) { - return CupertinoActionSheet( - title: Text(TIM_t("确定删除已选消息")), - cancelButton: CupertinoActionSheetAction( - onPressed: () { - Navigator.pop( - context, - "cancel", - ); - }, - child: Text(TIM_t("取消")), - isDefaultAction: false, - ), - actions: [ - CupertinoActionSheetAction( - onPressed: () { + ), + padding: const EdgeInsets.symmetric(vertical: 32), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Expanded( + child: Wrap( + crossAxisAlignment: WrapCrossAlignment.center, + alignment: WrapAlignment.center, + spacing: 64, + children: [ + Column( + children: [ + IconButton( + icon: Image.asset('images/forward.png', + package: 'tencent_cloud_chat_uikit', + color: theme.selectPanelTextIconColor), + iconSize: 30, + onPressed: () { + _handleForwardMessageWide(context, false, model); + }, + ), + Text(TIM_t("逐条转发"), + style: TextStyle( + color: hexToColor("646a73"), fontSize: 12)) + ], + ), + Column( + children: [ + IconButton( + icon: Image.asset('images/merge_forward.png', + package: 'tencent_cloud_chat_uikit', + color: theme.selectPanelTextIconColor), + iconSize: 30, + onPressed: () { + _handleForwardMessageWide(context, true, model); + }, + ), + Text( + TIM_t("合并转发"), + style: + TextStyle(color: theme.selectPanelTextIconColor, fontSize: 12), + ) + ], + ), + Column( + children: [ + IconButton( + icon: Image.asset('images/delete.png', + package: 'tencent_cloud_chat_uikit', + color: theme.selectPanelTextIconColor), + iconSize: 30, + onPressed: () { + TUIKitWidePopup.showSecondaryConfirmDialog( + operationKey: TUIKitWideModalOperationKey.confirmDeleteMessages, + context: context, + text: TIM_t("确定删除已选消息"), + theme: theme, + onCancel: () {}, + onConfirm: () async { model.deleteSelectedMsg(); model.updateMultiSelectStatus(false); + }); + }, + ), + Text(TIM_t("删除"), + style: TextStyle( + color: theme.selectPanelTextIconColor, fontSize: 12)) + ], + ), + InkWell( + onTap: (){ + model.updateMultiSelectStatus(false); + }, + child: Icon(Icons.close, color: theme.darkTextColor,), + ) + ], + )) + ], + ), + ), + defaultWidget: Container( + decoration: BoxDecoration( + border: Border( + top: BorderSide( + color: theme.weakDividerColor ?? + CommonColor.weakDividerColor)), + color: theme.selectPanelBgColor ?? theme.primaryColor, + ), + padding: const EdgeInsets.only(top: 12, bottom: 48), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + Column( + children: [ + IconButton( + icon: Image.asset('images/forward.png', + package: 'tencent_cloud_chat_uikit', color: theme.selectPanelTextIconColor), + iconSize: 40, + onPressed: () { + _handleForwardMessage(context, false, model); + }, + ), + Text(TIM_t("逐条转发"), + style: TextStyle(color: theme.selectPanelTextIconColor, fontSize: 12)) + ], + ), + Column( + children: [ + IconButton( + icon: Image.asset('images/merge_forward.png', + package: 'tencent_cloud_chat_uikit', color: theme.selectPanelTextIconColor), + iconSize: 40, + onPressed: () { + _handleForwardMessage(context, true, model); + }, + ), + Text( + TIM_t("合并转发"), + style: TextStyle(color: theme.selectPanelTextIconColor, fontSize: 12), + ) + ], + ), + Column( + children: [ + IconButton( + icon: Image.asset('images/delete.png', + package: 'tencent_cloud_chat_uikit', color: theme.selectPanelTextIconColor), + iconSize: 40, + onPressed: () { + showCupertinoModalPopup( + context: context, + builder: (BuildContext context) { + return CupertinoActionSheet( + title: Text(TIM_t("确定删除已选消息")), + cancelButton: CupertinoActionSheetAction( + onPressed: () { Navigator.pop( context, "cancel", ); }, - child: Text( - TIM_t("删除"), - style: TextStyle(color: theme.cautionColor), - ), + child: Text(TIM_t("取消")), isDefaultAction: false, - ) - ], - ); - }, - ); - }, - ), - Text(TIM_t("删除"), - style: TextStyle(color: theme.white, fontSize: 12)) - ], - ) - ], + ), + actions: [ + CupertinoActionSheetAction( + onPressed: () { + model.deleteSelectedMsg(); + model.updateMultiSelectStatus(false); + Navigator.pop( + context, + "cancel", + ); + }, + child: Text( + TIM_t("删除"), + style: TextStyle(color: theme.cautionColor), + ), + isDefaultAction: false, + ) + ], + ); + }, + ); + }, + ), + Text(TIM_t("删除"), + style: TextStyle(color: theme.selectPanelTextIconColor, fontSize: 12)) + ], + ) + ], + ), ), ); } diff --git a/lib/ui/views/TIMUIKitChat/tim_uikit_send_file.dart b/lib/ui/views/TIMUIKitChat/tim_uikit_send_file.dart new file mode 100644 index 0000000..60bcbb0 --- /dev/null +++ b/lib/ui/views/TIMUIKitChat/tim_uikit_send_file.dart @@ -0,0 +1,227 @@ +import 'dart:io'; +import 'package:dotted_border/dotted_border.dart'; +import 'package:flutter/material.dart'; +import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_base.dart'; +import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_statelesswidget.dart'; +import 'package:tencent_cloud_chat_uikit/business_logic/separate_models/tui_chat_separate_view_model.dart'; +import 'package:tencent_cloud_chat_uikit/business_logic/view_models/tui_chat_global_model.dart'; +import 'package:tencent_cloud_chat_uikit/data_services/core/tim_uikit_wide_modal_operation_key.dart'; +import 'package:tencent_cloud_chat_uikit/tencent_cloud_chat_uikit.dart'; +import 'package:cross_file/cross_file.dart'; +import 'package:tencent_cloud_chat_uikit/ui/utils/message.dart'; +import 'package:tencent_cloud_chat_uikit/ui/widgets/wide_popup.dart'; +import 'package:path/path.dart' as path; +import 'package:url_launcher/url_launcher.dart'; + +import 'TIMUIKitMessageItem/tim_uikit_chat_file_icon.dart'; + +String _getConvID(V2TimConversation conversation) { + return (conversation.type == 1 + ? conversation.userID + : conversation.groupID) ?? + ""; +} + +sendFileWithConfirmation( + {required List files, + required V2TimConversation conversation, + required ConvType conversationType, + required TUIChatSeparateViewModel model, + required TUITheme theme, + required BuildContext context}) async { + bool isCanSend = true; + + files.map((e) => e.path).any((filePath) { + final directory = Directory(filePath); + final isDirectoryExists = directory.existsSync(); + if (isDirectoryExists) { + isCanSend = false; + return false; + } + return true; + }); + + if (!isCanSend) { + TUIKitWidePopup.showSecondaryConfirmDialog( + text: TIM_t("无法发送,包含文件夹"), + onConfirm: () {}, + operationKey: TUIKitWideModalOperationKey.unableToSendDueToFolders, + context: context, + theme: theme); + return; + } + + final option1 = conversation.showName ?? + (conversationType == ConvType.group ? TIM_t("群聊") : TIM_t("对方")); + TUIKitWidePopup.showPopupWindow( + operationKey: TUIKitWideModalOperationKey.beforeSendScreenShot, + context: context, + isDarkBackground: false, + width: 600, + height: files.length < 4 ? 300 : 500, + title: TIM_t_para("发送给{{option1}}", "发送给$option1")(option1: option1), + child: (closeFunc) => Container( + padding: const EdgeInsets.only(bottom: 16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Scrollbar( + child: ListView.separated( + itemBuilder: (BuildContext context, int index) { + final file = files[index]; + return Material( + color: theme.wideBackgroundColor, + child: InkWell( + onTap: () { + launchUrl(Uri.file(file.path)); + }, + child: Padding( + padding: const EdgeInsets.symmetric( + vertical: 6, horizontal: 20), + child: Row( + children: [ + TIMUIKitFileIcon( + size: 44, + fileFormat: path + .extension(file.path) + .split(".")[path + .extension(file.path) + .split(".") + .length - + 1], + ), + const SizedBox(width: 16), + Expanded( + child: Text( + path.basename(file.path), + style: TextStyle( + fontSize: 16, + color: theme.darkTextColor), + ), + ), + ], + ), + ), + ), + ); + }, + separatorBuilder: (BuildContext context, int index) { + return Divider( + height: 1, + thickness: 1, + color: theme.weakDividerColor, + ); + }, + itemCount: files.length, + ), + ), + ), + Padding( + padding: const EdgeInsets.only(right: 16, top: 16), + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + mainAxisSize: MainAxisSize.max, + children: [ + OutlinedButton( + onPressed: () { + closeFunc(); + }, + child: Text(TIM_t("取消"))), + const SizedBox( + width: 20, + ), + ElevatedButton( + onPressed: () { + sendFiles(files, model, conversation, + conversationType, context); + closeFunc(); + }, + child: Text(TIM_t("发送"))) + ], + ), + ) + ], + ), + )); +} + +Future sendFiles( + List files, + TUIChatSeparateViewModel model, + V2TimConversation conversation, + ConvType conversationType, + BuildContext context) async { + for (final file in files) { + await MessageUtils.handleMessageError( + model.sendFileMessage( + filePath: file.path, + convID: _getConvID(conversation), + convType: conversationType), + context); + await Future.delayed(const Duration(microseconds: 300)); + } +} + +class TIMUIKitSendFile extends TIMUIKitStatelessWidget { + final V2TimConversation conversation; + + TIMUIKitSendFile({required this.conversation, Key? key}) : super(key: key); + + @override + Widget tuiBuild(BuildContext context, TUIKitBuildValue value) { + final theme = value.theme; + final conversationType = conversation.type; + final option1 = conversation.showName ?? + (conversationType == 2 ? TIM_t("群聊") : TIM_t("会话")); + + return Row( + mainAxisSize: MainAxisSize.max, + children: [ + Expanded( + child: Opacity( + opacity: 0.85, + child: Container( + color: theme.wideBackgroundColor, + padding: const EdgeInsets.all(40), + child: DottedBorder( + borderType: BorderType.RRect, + radius: const Radius.circular(20), + color: theme.primaryColor ?? theme.weakTextColor!, + dashPattern: const [6, 3], + child: Row( + children: [ + Expanded( + child: Column( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Icon( + Icons.file_copy_outlined, + size: 60, + color: theme.primaryColor, + ), + const SizedBox( + height: 40, + ), + Text( + TIM_t_para("发送给{{option1}}", "发送给$option1")( + option1: option1), + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + color: theme.darkTextColor), + ) + ], + )) + ], + ), + ), + ), + )) + ], + ); + } +} diff --git a/lib/ui/views/TIMUIKitContact/tim_uikit_contact.dart b/lib/ui/views/TIMUIKitContact/tim_uikit_contact.dart index 2e1651e..9e8318b 100644 --- a/lib/ui/views/TIMUIKitContact/tim_uikit_contact.dart +++ b/lib/ui/views/TIMUIKitContact/tim_uikit_contact.dart @@ -8,6 +8,7 @@ import 'package:tencent_cloud_chat_uikit/tencent_cloud_chat_uikit.dart'; export 'package:tencent_cloud_chat_uikit/ui/widgets/contact_list.dart'; import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_base.dart'; +import 'package:tencent_cloud_chat_uikit/ui/utils/screen_utils.dart'; class TIMUIKitContact extends StatefulWidget { /// the callback after clicking contact item @@ -44,11 +45,8 @@ class TIMUIKitContact extends StatefulWidget { class _TIMUIKitContactState extends TIMUIKitState { final TUIFriendShipViewModel model = serviceLocator(); + String currentItem = ""; - @override - void initState() { - super.initState(); - } @override void dispose() { @@ -57,6 +55,8 @@ class _TIMUIKitContactState extends TIMUIKitState { @override Widget tuiBuild(BuildContext context, TUIKitBuildValue value) { + final theme = value.theme; + final isDesktopScreen = TUIKitScreenUtils.getFormFactor(context) == DeviceType.Desktop; return MultiProvider( providers: [ ChangeNotifierProvider.value(value: model), @@ -67,10 +67,21 @@ class _TIMUIKitContactState extends TIMUIKitState { final memberList = model.friendList ?? []; return ContactList( + currentItem: currentItem, emptyBuilder: widget.emptyBuilder, isShowOnlineStatus: widget.isShowOnlineStatus, contactList: memberList, - onTapItem: widget.onTapItem, + onTapItem: (item){ + if(isDesktopScreen){ + setState(() { + currentItem = item.userID; + }); + } + if(widget.onTapItem != null){ + widget.onTapItem!(item); + } + }, + bgColor: isDesktopScreen ? theme.wideBackgroundColor : null, topList: widget.topList, topListItemBuilder: widget.topListItemBuilder, ); diff --git a/lib/ui/views/TIMUIKitConversation/tim_uikit_conversation.dart b/lib/ui/views/TIMUIKitConversation/tim_uikit_conversation.dart index 08fbb03..5ec34e0 100644 --- a/lib/ui/views/TIMUIKitConversation/tim_uikit_conversation.dart +++ b/lib/ui/views/TIMUIKitConversation/tim_uikit_conversation.dart @@ -1,3 +1,5 @@ +import 'dart:math'; + import 'package:flutter/material.dart'; import 'package:flutter_easyrefresh/easy_refresh.dart'; import 'package:flutter_slidable_for_tencent_im/flutter_slidable.dart'; @@ -8,24 +10,27 @@ import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_statelesswidget import 'package:tencent_cloud_chat_uikit/business_logic/life_cycle/conversation_life_cycle.dart'; import 'package:tencent_cloud_chat_uikit/business_logic/view_models/tui_conversation_view_model.dart'; import 'package:tencent_cloud_chat_uikit/business_logic/view_models/tui_friendship_view_model.dart'; - +import 'package:tencent_cloud_chat_uikit/data_services/core/tim_uikit_wide_modal_operation_key.dart'; import 'package:tencent_cloud_chat_uikit/data_services/services_locatar.dart'; import 'package:tencent_cloud_chat_uikit/tencent_cloud_chat_uikit.dart'; import 'package:tencent_cloud_chat_uikit/ui/controller/tim_uikit_conversation_controller.dart'; - import 'package:tencent_cloud_chat_uikit/ui/utils/platform.dart'; +import 'package:tencent_cloud_chat_uikit/ui/utils/screen_utils.dart'; import 'package:tencent_cloud_chat_uikit/ui/views/TIMUIKitConversation/tim_uikit_conversation_item.dart'; import 'package:tencent_cloud_chat_uikit/ui/widgets/customize_ball_pulse_header.dart'; import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_base.dart'; -import 'package:tencent_im_base/tencent_im_base.dart'; +import 'package:tencent_cloud_chat_uikit/ui/widgets/wide_popup.dart'; typedef ConversationItemBuilder = Widget Function( V2TimConversation conversationItem, [V2TimUserStatus? onlineStatus]); -typedef ConversationItemSlidableBuilder = List +typedef ConversationItemSlideBuilder = List Function(V2TimConversation conversationItem); +typedef ConversationItemSecondaryMenuBuilder = Widget Function( + V2TimConversation conversationItem, VoidCallback onClose); + class TIMUIKitConversation extends StatefulWidget { /// the callback after clicking conversation item final ValueChanged? onTapItem; @@ -34,10 +39,13 @@ class TIMUIKitConversation extends StatefulWidget { final TIMUIKitConversationController? controller; /// the builder for conversation item - final ConversationItemBuilder? itembuilder; + final ConversationItemBuilder? itemBuilder; - /// the builder for slidable item for each conversation item - final ConversationItemSlidableBuilder? itemSlidableBuilder; + /// the builder for Slidable item for each conversation item, shows on narrow screens. + final ConversationItemSlideBuilder? itemSlideBuilder; + + /// the widget of secondary tap menu for each conversation item, shows on wide screens. + final ConversationItemSecondaryMenuBuilder? itemSecondaryMenuBuilder; /// the widget shows when no conversation exists final Widget Function()? emptyBuilder; @@ -65,9 +73,10 @@ class TIMUIKitConversation extends StatefulWidget { this.lifeCycle, this.onTapItem, this.controller, - this.itembuilder, + this.itemSecondaryMenuBuilder, + this.itemBuilder, this.isShowDraft = true, - this.itemSlidableBuilder, + this.itemSlideBuilder, this.conversationCollector, this.emptyBuilder, this.lastMessageBuilder, @@ -80,8 +89,8 @@ class TIMUIKitConversation extends StatefulWidget { } } -class ConversationItemSlidablePanel extends TIMUIKitStatelessWidget { - ConversationItemSlidablePanel({ +class ConversationItemSlidePanel extends TIMUIKitStatelessWidget { + ConversationItemSlidePanel({ Key? key, this.flex = 1, this.backgroundColor = Colors.white, @@ -150,7 +159,7 @@ class _TIMUIKitConversationState extends TIMUIKitState { final controller = getController(); _timuiKitConversationController = controller; _timuiKitConversationController.model = model; - _autoScrollController = AutoScrollController(); + _autoScrollController = AutoScrollController(); } TIMUIKitConversationController getController() { @@ -180,46 +189,17 @@ class _TIMUIKitConversationState extends TIMUIKitState { conversationID: conversation.conversationID); } - List _defaultSlidableBuilder( - V2TimConversation conversationItem, - ) { - final theme = themeViewModel.theme; - return [ - if (!PlatformUtils().isWeb) - ConversationItemSlidablePanel( - onPressed: (context) { - _clearHistory(conversationItem); - }, - backgroundColor: theme.conversationItemSliderClearBgColor ?? - CommonColor.primaryColor, - foregroundColor: theme.conversationItemSliderTextColor, - label: TIM_t("清除聊天"), - spacing: 0, - autoClose: true, - ), - ConversationItemSlidablePanel( - onPressed: (context) { - _pinConversation(conversationItem); - }, - backgroundColor: - theme.conversationItemSliderPinBgColor ?? CommonColor.infoColor, - foregroundColor: theme.conversationItemSliderTextColor, - label: conversationItem.isPinned! ? TIM_t("取消置顶") : TIM_t("置顶"), - ), - ConversationItemSlidablePanel( - onPressed: (context) { - _deleteConversation(conversationItem); - }, - backgroundColor: - theme.conversationItemSliderDeleteBgColor ?? Colors.red, - foregroundColor: theme.conversationItemSliderTextColor, - label: TIM_t("删除"), - ) - ]; - } - - ConversationItemSlidableBuilder _getSlidableBuilder() { - return widget.itemSlidableBuilder ?? _defaultSlidableBuilder; + List getFilteredConversation() { + List filteredConversationList = model.conversationList + .where( + (element) => (element?.groupID != null || element?.userID != null)) + .toList(); + if (widget.conversationCollector != null) { + filteredConversationList = filteredConversationList + .where(widget.conversationCollector!) + .toList(); + } + return filteredConversationList; } _onScrollToConversation(String conversationID) { @@ -234,6 +214,7 @@ class _TIMUIKitConversationState extends TIMUIKitState { break; } } + if (isFound) { _autoScrollController.scrollToIndex( targetIndex, @@ -242,29 +223,98 @@ class _TIMUIKitConversationState extends TIMUIKitState { } } + Widget _defaultSecondaryMenu( + V2TimConversation conversationItem, VoidCallback onClose) { + return TUIKitColumnMenu(data: [ + if (!PlatformUtils().isWeb) + ColumnMenuItem( + label: TIM_t("清除消息"), + icon: const Icon(Icons.clear_all, size: 16), + onClick: () { + onClose(); + _clearHistory(conversationItem); + }), + ColumnMenuItem( + label: conversationItem.isPinned! ? TIM_t("取消置顶") : TIM_t("置顶"), + icon: Icon( + conversationItem.isPinned! + ? Icons.vertical_align_bottom + : Icons.vertical_align_top, + size: 16), + onClick: () { + onClose(); + _pinConversation(conversationItem); + }), + ColumnMenuItem( + label: TIM_t("删除会话"), + icon: const Icon(Icons.delete_outline, size: 16), + onClick: () { + onClose(); + _deleteConversation(conversationItem); + }), + ]); + } + + List _defaultSlideBuilder( + V2TimConversation conversationItem, + ) { + final theme = themeViewModel.theme; + return [ + if (!PlatformUtils().isWeb) + ConversationItemSlidePanel( + onPressed: (context) { + _clearHistory(conversationItem); + }, + backgroundColor: theme.conversationItemSliderClearBgColor ?? + CommonColor.primaryColor, + foregroundColor: theme.conversationItemSliderTextColor, + label: TIM_t("清除聊天"), + spacing: 0, + autoClose: true, + ), + ConversationItemSlidePanel( + onPressed: (context) { + _pinConversation(conversationItem); + }, + backgroundColor: + theme.conversationItemSliderPinBgColor ?? CommonColor.infoColor, + foregroundColor: theme.conversationItemSliderTextColor, + label: conversationItem.isPinned! ? TIM_t("取消置顶") : TIM_t("置顶"), + ), + ConversationItemSlidePanel( + onPressed: (context) { + _deleteConversation(conversationItem); + }, + backgroundColor: + theme.conversationItemSliderDeleteBgColor ?? Colors.red, + foregroundColor: theme.conversationItemSliderTextColor, + label: TIM_t("删除"), + ) + ]; + } + + Widget _getSecondaryMenu( + V2TimConversation conversation, VoidCallback onClose) { + if (widget.itemSecondaryMenuBuilder != null) { + return widget.itemSecondaryMenuBuilder!(conversation, onClose); + } + return _defaultSecondaryMenu(conversation, onClose); + } + + ConversationItemSlideBuilder _getSlideBuilder() { + return widget.itemSlideBuilder ?? _defaultSlideBuilder; + } + @override void dispose() { super.dispose(); - // model.dispose(); - } - - List getFilteredConversation(){ - List filteredConversationList = model - .conversationList - .where((element) => - (element?.groupID != null || element?.userID != null)) - .toList(); - if (widget.conversationCollector != null) { - filteredConversationList = filteredConversationList - .where(widget.conversationCollector!) - .toList(); - } - return filteredConversationList; } @override Widget tuiBuild(BuildContext context, TUIKitBuildValue value) { final theme = value.theme; + final isDesktopScreen = + TUIKitScreenUtils.getFormFactor(context) == DeviceType.Desktop; return MultiProvider( providers: [ ChangeNotifierProvider.value(value: model), @@ -277,89 +327,141 @@ class _TIMUIKitConversationState extends TIMUIKitState { Provider.of(context); _model.lifeCycle = widget.lifeCycle; - List filteredConversationList = getFilteredConversation(); + List filteredConversationList = + getFilteredConversation(); - if(TencentUtils.checkString(_model.scrollToConversation) != null){ + if (TencentUtils.checkString(_model.scrollToConversation) != null) { _onScrollToConversation(_model.scrollToConversation!); _model.clearScrollToConversation(); } - return SlidableAutoCloseBehavior( - child: EasyRefresh( - header: CustomizeBallPulseHeader(color: theme.primaryColor), - onRefresh: () async { - model.refresh(); - }, - child: filteredConversationList.isNotEmpty - ? ListView.builder( - controller: _autoScrollController, - shrinkWrap: true, - itemCount: filteredConversationList.length, - itemBuilder: (context, index) { - if (index == filteredConversationList.length - 1) { - if (haveMoreData) { - _timuiKitConversationController.loadData(); - } + Widget conversationList() { + return filteredConversationList.isNotEmpty + ? ListView.builder( + controller: _autoScrollController, + shrinkWrap: true, + itemCount: filteredConversationList.length, + itemBuilder: (context, index) { + if (index == filteredConversationList.length - 1) { + if (haveMoreData) { + _timuiKitConversationController.loadData(); } + } - final conversationItem = - filteredConversationList[index]; + final conversationItem = filteredConversationList[index]; - final V2TimUserStatus? onlineStatus = - _friendShipViewModel.userStatusList.firstWhere( - (item) => - item.userID == conversationItem?.userID, - orElse: () => V2TimUserStatus(statusType: 0)); + final V2TimUserStatus? onlineStatus = + _friendShipViewModel.userStatusList.firstWhere( + (item) => item.userID == conversationItem?.userID, + orElse: () => V2TimUserStatus(statusType: 0)); - if (widget.itembuilder != null) { - return widget.itembuilder!( - conversationItem!, onlineStatus); - } + if (widget.itemBuilder != null) { + return widget.itemBuilder!( + conversationItem!, onlineStatus); + } - final slidableChildren = - _getSlidableBuilder()(conversationItem!); - return AutoScrollTag( + final slideChildren = + _getSlideBuilder()(conversationItem!); + + final isCurrent = conversationItem.conversationID == + model.selectedConversation?.conversationID; + + final isPined = conversationItem.isPinned ?? false; + + Widget conversationLineItem() { + return Material( + color: (isCurrent && isDesktopScreen) + ? theme.conversationItemChooseBgColor + : isPined + ? theme.conversationItemPinedBgColor + : theme.conversationItemBgColor, + child: InkWell( + child: TIMUIKitConversationItem( + isCurrent: isCurrent, + isShowDraft: widget.isShowDraft, + lastMessageBuilder: widget.lastMessageBuilder, + faceUrl: conversationItem.faceUrl ?? "", + nickName: conversationItem.showName ?? "", + isDisturb: conversationItem.recvOpt != 0, + lastMsg: conversationItem.lastMessage, + isPined: isPined, + groupAtInfoList: + conversationItem.groupAtInfoList ?? [], + unreadCount: conversationItem.unreadCount ?? 0, + draftText: conversationItem.draftText, + onlineStatus: (widget.isShowOnlineStatus && + conversationItem.userID != null && + conversationItem.userID!.isNotEmpty) + ? onlineStatus + : null, + draftTimestamp: conversationItem.draftTimestamp, + convType: conversationItem.type), + onTap: () => onTapConvItem(conversationItem), + ), + ); + } + + return TUIKitScreenUtils.getDeviceWidget( + desktopWidget: AutoScrollTag( key: ValueKey(conversationItem.conversationID), controller: _autoScrollController, index: index, - child: Slidable( - groupTag: 'conversation-list', - child: InkWell( - child: TIMUIKitConversationItem( - isShowDraft: widget.isShowDraft, - lastMessageBuilder: widget.lastMessageBuilder, - faceUrl: conversationItem.faceUrl ?? "", - nickName: conversationItem.showName ?? "", - isDisturb: conversationItem.recvOpt != 0, - lastMsg: conversationItem.lastMessage, - isPined: conversationItem.isPinned ?? false, - groupAtInfoList: - conversationItem.groupAtInfoList ?? [], - unreadCount: - conversationItem.unreadCount ?? 0, - draftText: conversationItem.draftText, - onlineStatus: (widget.isShowOnlineStatus && - conversationItem.userID != null && - conversationItem.userID!.isNotEmpty) - ? onlineStatus - : null, - draftTimestamp: - conversationItem.draftTimestamp, - convType: conversationItem.type), - onTap: () => onTapConvItem(conversationItem), - ), - endActionPane: ActionPane( - extentRatio: - slidableChildren.length > 2 ? 0.77 : 0.5, - motion: const DrawerMotion(), - children: slidableChildren)), - ); - }) - : (widget.emptyBuilder != null - ? widget.emptyBuilder!() - : Container()), - ), - ); + child: GestureDetector( + onSecondaryTapDown: (details) { + TUIKitWidePopup.showPopupWindow( + operationKey: TUIKitWideModalOperationKey + .conversationSecondaryMenu, + isDarkBackground: false, + borderRadius: const BorderRadius.all( + Radius.circular(4)), + context: context, + offset: Offset( + min( + details.globalPosition.dx, + MediaQuery.of(context).size.width - + 80), + min( + details.globalPosition.dy, + MediaQuery.of(context).size.height - + 130)), + child: (onClose) => _getSecondaryMenu( + conversationItem, onClose)); + }, + child: conversationLineItem(), + ), + ), + defaultWidget: AutoScrollTag( + key: ValueKey(conversationItem.conversationID), + controller: _autoScrollController, + index: index, + child: Slidable( + groupTag: 'conversation-list', + child: conversationLineItem(), + endActionPane: ActionPane( + extentRatio: + slideChildren.length > 2 ? 0.77 : 0.5, + motion: const DrawerMotion(), + children: slideChildren)), + )); + }) + : (widget.emptyBuilder != null + ? widget.emptyBuilder!() + : Container()); + } + + return TUIKitScreenUtils.getDeviceWidget( + defaultWidget: SlidableAutoCloseBehavior( + child: EasyRefresh( + header: CustomizeBallPulseHeader(color: theme.primaryColor), + onRefresh: () async { + model.refresh(); + }, + child: conversationList(), + ), + ), + desktopWidget: Scrollbar( + controller: _autoScrollController, + child: conversationList())); }); } } diff --git a/lib/ui/views/TIMUIKitConversation/tim_uikit_conversation_draft_text.dart b/lib/ui/views/TIMUIKitConversation/tim_uikit_conversation_draft_text.dart index e7b7d2f..ff1dc94 100644 --- a/lib/ui/views/TIMUIKitConversation/tim_uikit_conversation_draft_text.dart +++ b/lib/ui/views/TIMUIKitConversation/tim_uikit_conversation_draft_text.dart @@ -7,9 +7,11 @@ import 'package:tencent_im_base/tencent_im_base.dart'; class TIMUIKitDraftText extends TIMUIKitStatelessWidget { final BuildContext context; final String draftText; + final double fontSize; TIMUIKitDraftText({ Key? key, + this.fontSize = 14.0, required this.context, required this.draftText, }) : super(key: key); @@ -37,7 +39,7 @@ class TIMUIKitDraftText extends TIMUIKitStatelessWidget { style: TextStyle( height: 1.5, color: theme.conversationItemLastMessageTextColor, - fontSize: 14), + fontSize: fontSize), )), ]); } diff --git a/lib/ui/views/TIMUIKitConversation/tim_uikit_conversation_item.dart b/lib/ui/views/TIMUIKitConversation/tim_uikit_conversation_item.dart index ae07634..0a1c692 100644 --- a/lib/ui/views/TIMUIKitConversation/tim_uikit_conversation_item.dart +++ b/lib/ui/views/TIMUIKitConversation/tim_uikit_conversation_item.dart @@ -1,6 +1,7 @@ -// ignore_for_file: avoid_print, empty_catches +// ignore_for_file: empty_catches import 'package:flutter/material.dart'; +import 'package:tencent_cloud_chat_uikit/ui/utils/screen_utils.dart'; import 'package:tencent_im_base/tencent_im_base.dart'; import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_statelesswidget.dart'; @@ -28,6 +29,7 @@ class TIMUIKitConversationItem extends TIMUIKitStatelessWidget { final LastMessageBuilder? lastMessageBuilder; final V2TimUserStatus? onlineStatus; final int? convType; + final bool isCurrent; /// Control if shows the identifier that the conversation has a draft text, inputted in previous. /// Also, you'd better specifying the `draftText` field for `TIMUIKitChat`, from the `draftText` in `V2TimConversation`, @@ -42,6 +44,7 @@ class TIMUIKitConversationItem extends TIMUIKitStatelessWidget { required this.lastMsg, this.onlineStatus, required this.isPined, + this.isCurrent = false, required this.unreadCount, required this.groupAtInfoList, required this.isDisturb, @@ -52,10 +55,12 @@ class TIMUIKitConversationItem extends TIMUIKitStatelessWidget { }) : super(key: key); Widget _getShowMsgWidget(BuildContext context) { + final isDesktopScreen = TUIKitScreenUtils.getFormFactor(context) == DeviceType.Desktop; if (isShowDraft && draftText != null && draftText != "") { return TIMUIKitDraftText( context: context, draftText: draftText ?? "", + fontSize: isDesktopScreen ? 12 : 14, ); } else if (lastMsg != null) { if (lastMessageBuilder != null && @@ -63,6 +68,7 @@ class TIMUIKitConversationItem extends TIMUIKitStatelessWidget { return lastMessageBuilder!(lastMsg, groupAtInfoList)!; } return TIMUIKitLastMsg( + fontSize: isDesktopScreen ? 12 : 14, groupAtInfoList: groupAtInfoList, lastMsg: lastMsg, context: context, @@ -90,7 +96,7 @@ class TIMUIKitConversationItem extends TIMUIKitStatelessWidget { } else if (lastMsg != null) { return Text(TimeAgo().getTimeStringForChat(lastMsg!.timestamp as int), style: TextStyle( - fontSize: 12, + fontSize: 11, color: theme.conversationItemTitmeTextColor, )); } @@ -102,12 +108,10 @@ class TIMUIKitConversationItem extends TIMUIKitStatelessWidget { @override Widget tuiBuild(BuildContext context, TUIKitBuildValue value) { final TUITheme theme = value.theme; + final isDesktopScreen = TUIKitScreenUtils.getFormFactor(context) == DeviceType.Desktop; return Container( padding: const EdgeInsets.only(top: 6, bottom: 6, left: 16, right: 16), decoration: BoxDecoration( - color: isPined - ? theme.conversationItemPinedBgColor - : theme.conversationItemBgColor, border: Border( bottom: BorderSide( color: theme.conversationItemBorderColor ?? @@ -122,8 +126,8 @@ class TIMUIKitConversationItem extends TIMUIKitStatelessWidget { Container( padding: const EdgeInsets.only(top: 0, bottom: 2, right: 0), child: SizedBox( - width: 44, - height: 44, + width: isDesktopScreen ? 40 : 44, + height: isDesktopScreen ? 40 : 44, child: Stack( fit: StackFit.expand, clipBehavior: Clip.none, @@ -151,7 +155,7 @@ class TIMUIKitConversationItem extends TIMUIKitStatelessWidget { Expanded( child: Container( height: 60, - margin: const EdgeInsets.only(left: 12), + margin: EdgeInsets.only(left: isDesktopScreen ? 10 : 12), padding: const EdgeInsets.only(top: 0, bottom: 0), child: Column( mainAxisAlignment: MainAxisAlignment.center, @@ -169,7 +173,7 @@ class TIMUIKitConversationItem extends TIMUIKitStatelessWidget { style: TextStyle( height: 1, color: theme.conversationItemTitleTextColor, - fontSize: 18, + fontSize: isDesktopScreen ? 14 : 18, fontWeight: FontWeight.w400, ), )), @@ -190,7 +194,7 @@ class TIMUIKitConversationItem extends TIMUIKitStatelessWidget { child: Icon( Icons.notifications_off, color: theme.conversationItemNoNotificationIconColor, - size: 16.0, + size: isDesktopScreen ? 14 : 16.0, ), ) ], diff --git a/lib/ui/views/TIMUIKitConversation/tim_uikit_conversation_last_msg.dart b/lib/ui/views/TIMUIKitConversation/tim_uikit_conversation_last_msg.dart index f79e6eb..09a335a 100644 --- a/lib/ui/views/TIMUIKitConversation/tim_uikit_conversation_last_msg.dart +++ b/lib/ui/views/TIMUIKitConversation/tim_uikit_conversation_last_msg.dart @@ -13,12 +13,14 @@ class TIMUIKitLastMsg extends StatefulWidget { final V2TimMessage? lastMsg; final List groupAtInfoList; final BuildContext context; + final double fontSize; const TIMUIKitLastMsg( {Key? key, this.lastMsg, required this.groupAtInfoList, - required this.context}) + required this.context, + this.fontSize = 14.0}) : super(key: key); @override @@ -46,11 +48,11 @@ class _TIMUIKitLastMsgState extends TIMUIKitState { void _getMsgElem() async { final isRevokedMessage = widget.lastMsg!.status == 6; if (isRevokedMessage) { - final isSelf = widget.lastMsg!.isSelf ?? false; + final isSelf = widget.lastMsg!.isSelf ?? true; final option1 = isSelf ? TIM_t("您") : widget.lastMsg!.nickName ?? widget.lastMsg?.sender; - if(mounted){ + if (mounted) { setState(() { groupTipsAbstractText = TIM_t_para( "{{option1}}撤回了一条消息", "$option1撤回了一条消息")(option1: option1); @@ -58,7 +60,7 @@ class _TIMUIKitLastMsgState extends TIMUIKitState { } } else { final newText = await _getLastMsgShowText(widget.lastMsg, widget.context); - if(mounted){ + if (mounted) { setState(() { groupTipsAbstractText = newText; }); @@ -75,7 +77,7 @@ class _TIMUIKitLastMsgState extends TIMUIKitState { case MessageElemType.V2TIM_ELEM_TYPE_SOUND: return TIM_t("[语音]"); case MessageElemType.V2TIM_ELEM_TYPE_TEXT: - return widget.lastMsg?.textElem?.text ?? ""; + return (widget.lastMsg?.textElem?.text)?.trim() ?? ""; case MessageElemType.V2TIM_ELEM_TYPE_FACE: return TIM_t("[表情]"); case MessageElemType.V2TIM_ELEM_TYPE_FILE: @@ -134,14 +136,16 @@ class _TIMUIKitLastMsgState extends TIMUIKitState { ), if (widget.groupAtInfoList.isNotEmpty) Text(_getAtMessage(), - style: TextStyle(color: theme.cautionColor, fontSize: 14)), + style: TextStyle( + color: theme.cautionColor, fontSize: widget.fontSize)), Expanded( child: Text( groupTipsAbstractText, softWrap: true, maxLines: 1, overflow: TextOverflow.ellipsis, - style: TextStyle(height: 1, color: theme.weakTextColor, fontSize: 14), + style: TextStyle( + height: 1, color: theme.weakTextColor, fontSize: widget.fontSize), )), ]); } diff --git a/lib/ui/views/TIMUIKitGroup/tim_uikit_group.dart b/lib/ui/views/TIMUIKitGroup/tim_uikit_group.dart index eb04886..e58a9f9 100644 --- a/lib/ui/views/TIMUIKitGroup/tim_uikit_group.dart +++ b/lib/ui/views/TIMUIKitGroup/tim_uikit_group.dart @@ -1,7 +1,8 @@ -import 'package:azlistview/azlistview.dart'; +import 'package:azlistview_all_platforms/azlistview_all_platforms.dart'; import 'package:flutter/material.dart'; import 'package:lpinyin/lpinyin.dart'; import 'package:provider/provider.dart'; +import 'package:tencent_cloud_chat_uikit/ui/utils/screen_utils.dart'; import 'package:tencent_im_base/tencent_im_base.dart'; import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_state.dart'; import 'package:tencent_cloud_chat_uikit/business_logic/listener_model/tui_group_listener_model.dart'; @@ -17,7 +18,8 @@ typedef GroupItemBuilder = Widget Function( BuildContext context, V2TimGroupInfo groupInfo); class TIMUIKitGroup extends StatefulWidget { - final void Function(V2TimGroupInfo groupInfo)? onTapItem; + final void Function(V2TimGroupInfo groupInfo, V2TimConversation conversation)? + onTapItem; final Widget Function(BuildContext context)? emptyBuilder; final GroupItemBuilder? itemBuilder; @@ -68,44 +70,67 @@ class _TIMUIKitGroupState extends TIMUIKitState { final theme = Provider.of(context).theme; final showName = groupInfo.groupName ?? groupInfo.groupID; final faceUrl = groupInfo.faceUrl ?? ""; + final isDesktopScreen = + TUIKitScreenUtils.getFormFactor(context) == DeviceType.Desktop; return Container( - padding: const EdgeInsets.only(top: 10, left: 16), decoration: BoxDecoration( border: Border( bottom: BorderSide( color: theme.weakDividerColor ?? CommonColor.weakDividerColor))), - child: InkWell( - onTap: (() { - if (widget.onTapItem != null) { - widget.onTapItem!(groupInfo); - } - }), - child: Row( - children: [ - Container( - padding: const EdgeInsets.only(bottom: 12), - margin: const EdgeInsets.only(right: 12), - child: SizedBox( - height: 40, - width: 40, - child: Avatar( - faceUrl: faceUrl, - showName: showName, - type: 2, + child: Material( + color: isDesktopScreen ? theme.wideBackgroundColor : null, + child: InkWell( + onTap: (() async { + if (widget.onTapItem != null) { + V2TimConversation conversation = V2TimConversation( + conversationID: "group_${groupInfo.groupID}", + groupID: groupInfo.groupID, + type: 2, + showName: groupInfo.groupName, + groupType: groupInfo.groupType, + faceUrl: groupInfo.faceUrl, + ); + final res = await TencentImSDKPlugin + .v2TIMManager.v2ConversationManager + .getConversation( + conversationID: "group_${groupInfo.groupID}"); + if (res.code == 0 && res.data != null) { + conversation = res.data!; + } + widget.onTapItem!(groupInfo, conversation); + } + }), + child: Container( + padding: const EdgeInsets.only(top: 10, left: 16), + child: Row( + children: [ + Container( + padding: const EdgeInsets.only(bottom: 12), + margin: const EdgeInsets.only(right: 12), + child: SizedBox( + height: isDesktopScreen ? 30 : 40, + width: isDesktopScreen ? 30 : 40, + child: Avatar( + faceUrl: faceUrl, + showName: showName, + type: 2, + ), + ), ), - ), + Expanded( + child: Container( + alignment: Alignment.centerLeft, + padding: const EdgeInsets.only(top: 10, bottom: 20), + child: Text( + showName, + style: TextStyle( + color: Colors.black, fontSize: isDesktopScreen ? 14 : 18), + ), + )) + ], ), - Expanded( - child: Container( - alignment: Alignment.centerLeft, - padding: const EdgeInsets.only(top: 10, bottom: 20), - child: Text( - showName, - style: const TextStyle(color: Colors.black, fontSize: 18), - ), - )) - ], + ), ), ), ); diff --git a/lib/ui/views/TIMUIKitGroup/tim_uikit_group_application_list.dart b/lib/ui/views/TIMUIKitGroup/tim_uikit_group_application_list.dart index 69bf2bb..63850cd 100644 --- a/lib/ui/views/TIMUIKitGroup/tim_uikit_group_application_list.dart +++ b/lib/ui/views/TIMUIKitGroup/tim_uikit_group_application_list.dart @@ -6,6 +6,7 @@ import 'package:tencent_cloud_chat_uikit/business_logic/view_models/tui_chat_glo import 'package:tencent_cloud_chat_uikit/data_services/group/group_services.dart'; import 'package:tencent_cloud_chat_uikit/data_services/services_locatar.dart'; +import 'package:tencent_cloud_chat_uikit/ui/utils/screen_utils.dart'; import 'package:tencent_cloud_chat_uikit/ui/widgets/avatar.dart'; @@ -74,7 +75,7 @@ class TIMUIKitGroupApplicationListState } String _getRequestMessage() { - String option2 = applicationInfo.requestMsg!; + String option2 = applicationInfo.requestMsg ?? ""; return TIM_t_para("验证消息: {{option2}}", "验证消息: $option2")( option2: option2); } @@ -227,8 +228,10 @@ class TIMUIKitGroupApplicationListState return MultiProvider( providers: [ChangeNotifierProvider.value(value: model)], builder: (context, w) { + final isDesktopScreen = + TUIKitScreenUtils.getFormFactor(context) == DeviceType.Desktop; return Container( - decoration: BoxDecoration(color: theme.weakBackgroundColor), + decoration: isDesktopScreen ? null : BoxDecoration(color: theme.weakBackgroundColor), child: ListView.builder( shrinkWrap: true, itemCount: groupApplicationList.length, diff --git a/lib/ui/views/TIMUIKitGroup/tim_uikit_group_application_list_item.dart b/lib/ui/views/TIMUIKitGroup/tim_uikit_group_application_list_item.dart index 749474e..acd0060 100644 --- a/lib/ui/views/TIMUIKitGroup/tim_uikit_group_application_list_item.dart +++ b/lib/ui/views/TIMUIKitGroup/tim_uikit_group_application_list_item.dart @@ -27,10 +27,6 @@ class TIMUIKitGroupApplicationListItemState extends TIMUIKitState { ApplicationStatus applicationStatus = ApplicationStatus.none; - @override - void initState() { - super.initState(); - } String _getUserName() { if (widget.applicationInfo.fromUserNickName != null && diff --git a/lib/ui/views/TIMUIKitGroupProfile/group_member/tui_add_group_member.dart b/lib/ui/views/TIMUIKitGroupProfile/group_member/tui_add_group_member.dart index e89bf65..583a8fc 100644 --- a/lib/ui/views/TIMUIKitGroupProfile/group_member/tui_add_group_member.dart +++ b/lib/ui/views/TIMUIKitGroupProfile/group_member/tui_add_group_member.dart @@ -1,12 +1,14 @@ import 'package:flutter/material.dart'; +import 'package:tencent_cloud_chat_uikit/ui/utils/screen_utils.dart'; import 'package:tencent_im_base/tencent_im_base.dart'; import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_base.dart'; import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_state.dart'; import 'package:tencent_cloud_chat_uikit/business_logic/separate_models/tui_group_profile_model.dart'; - import 'package:tencent_cloud_chat_uikit/ui/widgets/contact_list.dart'; +GlobalKey<_AddGroupMemberPageState> addGroupMemberKey = GlobalKey(); + class AddGroupMemberPage extends StatefulWidget { final TUIGroupProfileModel model; @@ -17,56 +19,66 @@ class AddGroupMemberPage extends StatefulWidget { } class _AddGroupMemberPageState extends TIMUIKitState { - List selectedContacter = []; + List selectedContacts = []; + + void submitAdd() async { + if (selectedContacts.isNotEmpty) { + final userIDs = selectedContacts.map((e) => e.userID).toList(); + await widget.model.inviteUserToGroup(userIDs); + Navigator.pop(context); + } + } @override Widget tuiBuild(BuildContext context, TUIKitBuildValue value) { final TUITheme theme = value.theme; - return Scaffold( - appBar: AppBar( - title: Text( - TIM_t("添加群成员"), - style: const TextStyle(color: Colors.white, fontSize: 17), - ), - actions: [ - TextButton( - onPressed: () async { - if (selectedContacter.isNotEmpty) { - final userIDs = - selectedContacter.map((e) => e.userID).toList(); - await widget.model.inviteUserToGroup(userIDs); - Navigator.pop(context); - } - }, - child: Text( - TIM_t("确定"), - style: const TextStyle( - color: Colors.white, - fontSize: 16, - ), + return TUIKitScreenUtils.getDeviceWidget( + desktopWidget: Container( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: ContactList( + bgColor: theme.wideBackgroundColor, + groupMemberList: widget.model.groupMemberList, + contactList: widget.model.contactList, + isCanSelectMemberItem: true, + onSelectedMemberItemChange: (selectedMember) { + selectedContacts = selectedMember; + }, + ), + ), + defaultWidget: Scaffold( + appBar: AppBar( + title: Text( + TIM_t("添加群成员"), + style: TextStyle(color: theme.appbarTextColor, fontSize: 17), ), - ) - ], - shadowColor: theme.weakDividerColor, - flexibleSpace: Container( - decoration: BoxDecoration( - gradient: LinearGradient(colors: [ - theme.lightPrimaryColor ?? CommonColor.lightPrimaryColor, - theme.primaryColor ?? CommonColor.primaryColor - ]), - ), - ), - iconTheme: const IconThemeData( - color: Colors.white, - )), - body: ContactList( - groupMemberList: widget.model.groupMemberList, - contactList: widget.model.contactList, - isCanSelectMemberItem: true, - onSelectedMemberItemChange: (selectedMember) { - selectedContacter = selectedMember; - }, - )); + actions: [ + TextButton( + onPressed: () async { + submitAdd(); + }, + child: Text( + TIM_t("确定"), + style: TextStyle( + color: theme.appbarTextColor, + fontSize: 16, + ), + ), + ) + ], + shadowColor: theme.weakDividerColor, + backgroundColor: theme.appbarBgColor ?? + theme.primaryColor, + iconTheme: IconThemeData( + color: theme.appbarTextColor, + )), + body: ContactList( + groupMemberList: widget.model.groupMemberList, + contactList: widget.model.contactList, + isCanSelectMemberItem: true, + onSelectedMemberItemChange: (selectedMember) { + selectedContacts = selectedMember; + }, + ))); } } diff --git a/lib/ui/views/TIMUIKitGroupProfile/group_member/tui_delete_group_member.dart b/lib/ui/views/TIMUIKitGroupProfile/group_member/tui_delete_group_member.dart index a23b7f7..2e693e4 100644 --- a/lib/ui/views/TIMUIKitGroupProfile/group_member/tui_delete_group_member.dart +++ b/lib/ui/views/TIMUIKitGroupProfile/group_member/tui_delete_group_member.dart @@ -1,13 +1,15 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; +import 'package:tencent_cloud_chat_uikit/ui/utils/screen_utils.dart'; import 'package:tencent_im_base/tencent_im_base.dart'; import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_base.dart'; import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_state.dart'; import 'package:tencent_cloud_chat_uikit/business_logic/separate_models/tui_group_profile_model.dart'; - import 'package:tencent_cloud_chat_uikit/ui/widgets/group_member_list.dart'; +GlobalKey<_DeleteGroupMemberPageState> deleteGroupMemberKey = GlobalKey(); + class DeleteGroupMemberPage extends StatefulWidget { final TUIGroupProfileModel model; @@ -67,56 +69,65 @@ class _DeleteGroupMemberPageState extends TIMUIKitState { []; } + void submitDelete() async { + if (selectedGroupMember.isNotEmpty) { + final userIDs = selectedGroupMember.map((e) => e.userID).toList(); + widget.model.kickOffMember(userIDs); + Navigator.pop(context); + } + } + @override Widget tuiBuild(BuildContext context, TUIKitBuildValue value) { final TUITheme theme = value.theme; - return Scaffold( - appBar: AppBar( - title: Text( - TIM_t("删除群成员"), - style: const TextStyle(color: Colors.white, fontSize: 17), - ), - actions: [ - TextButton( - onPressed: () async { - if (selectedGroupMember.isNotEmpty) { - final userIDs = - selectedGroupMember.map((e) => e.userID).toList(); - widget.model.kickOffMember(userIDs); - Navigator.pop(context); - } - }, - child: Text( - TIM_t("确定"), - style: const TextStyle( - color: Colors.white, - fontSize: 16, - ), + return TUIKitScreenUtils.getDeviceWidget( + desktopWidget: Container( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: GroupProfileMemberList( + memberList: + handleRole(searchMemberList ?? widget.model.groupMemberList), + canSelectMember: true, + canSlideDelete: false, + onSelectedMemberChange: (selectedMember) { + selectedGroupMember = selectedMember; + }, + touchBottomCallBack: () {}, + ), + ), + defaultWidget: Scaffold( + appBar: AppBar( + title: Text( + TIM_t("删除群成员"), + style: TextStyle(color: theme.appbarTextColor, fontSize: 17), ), - ) - ], - shadowColor: theme.weakBackgroundColor, - flexibleSpace: Container( - decoration: BoxDecoration( - gradient: LinearGradient(colors: [ - theme.lightPrimaryColor ?? CommonColor.lightPrimaryColor, - theme.primaryColor ?? CommonColor.primaryColor - ]), - ), - ), - iconTheme: const IconThemeData( - color: Colors.white, - )), - body: GroupProfileMemberList( - memberList: - handleRole(searchMemberList ?? widget.model.groupMemberList), - canSelectMember: true, - canSlideDelete: false, - onSelectedMemberChange: (selectedMember) { - selectedGroupMember = selectedMember; - }, - touchBottomCallBack: () {}, - )); + actions: [ + TextButton( + onPressed: submitDelete, + child: Text( + TIM_t("确定"), + style: TextStyle( + color: theme.appbarTextColor, + fontSize: 16, + ), + ), + ) + ], + shadowColor: theme.weakBackgroundColor, + backgroundColor: theme.appbarBgColor ?? + theme.primaryColor, + iconTheme: IconThemeData( + color: theme.appbarTextColor, + )), + body: GroupProfileMemberList( + memberList: + handleRole(searchMemberList ?? widget.model.groupMemberList), + canSelectMember: true, + canSlideDelete: false, + onSelectedMemberChange: (selectedMember) { + selectedGroupMember = selectedMember; + }, + touchBottomCallBack: () {}, + ))); } } diff --git a/lib/ui/views/TIMUIKitGroupProfile/group_member/tui_group_member_list.dart b/lib/ui/views/TIMUIKitGroupProfile/group_member/tui_group_member_list.dart index 0aaab63..a397b31 100644 --- a/lib/ui/views/TIMUIKitGroupProfile/group_member/tui_group_member_list.dart +++ b/lib/ui/views/TIMUIKitGroupProfile/group_member/tui_group_member_list.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; +import 'package:tencent_cloud_chat_uikit/ui/utils/screen_utils.dart'; import 'package:tencent_im_base/tencent_im_base.dart'; import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_base.dart'; import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_state.dart'; @@ -81,6 +82,8 @@ class GroupProfileMemberListPageState @override Widget tuiBuild(BuildContext context, TUIKitBuildValue value) { final TUITheme theme = value.theme; + final isDesktopScreen = + TUIKitScreenUtils.getFormFactor() == DeviceType.Desktop; return MultiProvider( providers: [ ChangeNotifierProvider.value(value: widget.model), @@ -90,24 +93,36 @@ class GroupProfileMemberListPageState Provider.of(context); String option1 = groupProfileModel.groupInfo?.memberCount.toString() ?? widget.memberList.length.toString(); + if(isDesktopScreen){ + return GroupProfileMemberList( + customTopArea: PlatformUtils().isWeb + ? null + : GroupMemberSearchTextField( + onTextChange: (text) => + handleSearchGroupMembers(text, context), + ), + memberList: searchMemberList ?? groupProfileModel.groupMemberList, + removeMember: _kickedOffMember, + touchBottomCallBack: () {}, + onTapMemberItem: (friendInfo, details) { + if (widget.model.onClickUser != null) { + widget.model.onClickUser!(friendInfo.userID, details); + } + }, + ); + } return Scaffold( appBar: AppBar( title: Text( TIM_t_para("群成员({{option1}}人)", "群成员($option1人)")( option1: option1), - style: const TextStyle(color: Colors.white, fontSize: 17), + style: TextStyle(color: theme.appbarTextColor, fontSize: 17), ), shadowColor: theme.weakBackgroundColor, - flexibleSpace: Container( - decoration: BoxDecoration( - gradient: LinearGradient(colors: [ - theme.lightPrimaryColor ?? CommonColor.lightPrimaryColor, - theme.primaryColor ?? CommonColor.primaryColor - ]), - ), - ), - iconTheme: const IconThemeData( - color: Colors.white, + backgroundColor: theme.appbarBgColor ?? + theme.primaryColor, + iconTheme: IconThemeData( + color: theme.appbarTextColor, )), body: GroupProfileMemberList( customTopArea: PlatformUtils().isWeb @@ -119,12 +134,13 @@ class GroupProfileMemberListPageState memberList: searchMemberList ?? groupProfileModel.groupMemberList, removeMember: _kickedOffMember, touchBottomCallBack: () {}, - onTapMemberItem: (friendInfo) { + onTapMemberItem: (friendInfo, details) { if (widget.model.onClickUser != null) { - widget.model.onClickUser!(friendInfo.userID); + widget.model.onClickUser!(friendInfo.userID, details); } }, - )); + ) + ); }, ); } diff --git a/lib/ui/views/TIMUIKitGroupProfile/tim_uikit_group_profile.dart b/lib/ui/views/TIMUIKitGroupProfile/tim_uikit_group_profile.dart index 751c440..d04fb83 100644 --- a/lib/ui/views/TIMUIKitGroupProfile/tim_uikit_group_profile.dart +++ b/lib/ui/views/TIMUIKitGroupProfile/tim_uikit_group_profile.dart @@ -8,6 +8,7 @@ import 'package:tencent_cloud_chat_uikit/business_logic/separate_models/tui_grou import 'package:tencent_cloud_chat_uikit/data_services/services_locatar.dart'; import 'package:tencent_cloud_chat_uikit/tencent_cloud_chat_uikit.dart'; import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_base.dart'; +import 'package:tencent_cloud_chat_uikit/ui/utils/screen_utils.dart'; import 'package:tencent_cloud_chat_uikit/ui/views/TIMUIKitGroupProfile/group_profile_widget.dart'; import 'package:tencent_cloud_chat_uikit/ui/views/TIMUIKitGroupProfile/widgets/tim_ui_group_profile_widget.dart'; import 'package:tencent_cloud_chat_uikit/ui/views/TIMUIKitGroupProfile/widgets/tim_uikit_group_button_area.dart'; @@ -54,7 +55,7 @@ class TIMUIKitGroupProfile extends StatefulWidget { /// The callback after user clicking a user, /// you may navigating to the specific profile page, or anywhere you want. - final Function(String userID)? onClickUser; + final Function(String userID, TapDownDetails? tapDetails)? onClickUser; const TIMUIKitGroupProfile( {Key? key, @@ -96,7 +97,7 @@ class _TIMUIKitGroupProfileState extends TIMUIKitState { @override void didUpdateWidget(covariant TIMUIKitGroupProfile oldWidget) { super.didUpdateWidget(oldWidget); - if(oldWidget.groupID != widget.groupID){ + if (oldWidget.groupID != widget.groupID) { model.loadData(widget.groupID); } } @@ -135,6 +136,8 @@ class _TIMUIKitGroupProfileState extends TIMUIKitState { model.lifeCycle = widget.lifeCycle; final V2TimGroupInfo? groupInfo = model.groupInfo; final memberList = model.groupMemberList; + final isDesktopScreen = + TUIKitScreenUtils.getFormFactor(context) == DeviceType.Desktop; if (groupInfo == null) { return Center( child: LoadingAnimationWidget.staggeredDotsWave( @@ -170,7 +173,10 @@ class _TIMUIKitGroupProfileState extends TIMUIKitState { Widget groupProfilePage({required Widget child}) { return SingleChildScrollView( child: Container( - color: widget.backGroundColor ?? theme.weakBackgroundColor, + color: widget.backGroundColor ?? + (isDesktopScreen + ? theme.wideBackgroundColor + : theme.weakBackgroundColor), child: child, ), ); @@ -205,6 +211,7 @@ class _TIMUIKitGroupProfileState extends TIMUIKitState { ? customBuilder?.detailCard!( groupInfo, model.setGroupName) : TIMUIKitGroupProfileWidget.detailCard( + isHavePermission: isAdmin || isGroupOwner, groupInfo: groupInfo))!; case GroupProfileWidgetEnum.memberListTile: return (customBuilder?.memberListTile != null @@ -216,7 +223,8 @@ class _TIMUIKitGroupProfileState extends TIMUIKitState { groupInfo.notification ?? "", toDefaultNoticePage, model.setGroupNotification) - : TIMUIKitGroupProfileWidget.groupNotification())!; + : TIMUIKitGroupProfileWidget.groupNotification( + isHavePermission: isAdmin || isGroupOwner))!; case GroupProfileWidgetEnum.groupManage: if (isAdmin || isGroupOwner) { return (customBuilder?.groupManage != null @@ -232,14 +240,15 @@ class _TIMUIKitGroupProfileState extends TIMUIKitState { case GroupProfileWidgetEnum.operationDivider: return (customBuilder?.operationDivider != null ? customBuilder?.operationDivider!() - : TIMUIKitGroupProfileWidget.operationDivider())!; + : TIMUIKitGroupProfileWidget.operationDivider(theme))!; case GroupProfileWidgetEnum.groupTypeBar: return (customBuilder?.groupTypeBar != null ? customBuilder?.groupTypeBar!(groupInfo.groupType) : TIMUIKitGroupProfileWidget.groupType())!; case GroupProfileWidgetEnum.groupJoiningModeBar: final String groupType = groupInfo.groupType; - if (groupType == "Work" || + if (!(isGroupOwner || isAdmin) || + groupType == "Work" || groupType == "Meeting" || groupType == "AVChatRoom") { return Container(); diff --git a/lib/ui/views/TIMUIKitGroupProfile/widgets/tim_ui_group_member_search.dart b/lib/ui/views/TIMUIKitGroupProfile/widgets/tim_ui_group_member_search.dart index ccfbaee..9af7999 100644 --- a/lib/ui/views/TIMUIKitGroupProfile/widgets/tim_ui_group_member_search.dart +++ b/lib/ui/views/TIMUIKitGroupProfile/widgets/tim_ui_group_member_search.dart @@ -4,6 +4,8 @@ import 'package:tencent_cloud_chat_uikit/ui/utils/optimize_utils.dart'; import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_base.dart'; +import 'package:tencent_cloud_chat_uikit/ui/utils/screen_utils.dart'; +import 'package:tencent_cloud_chat_uikit/ui/views/TIMUIKitSearch/pureUI/tim_uikit_search_input.dart'; import 'package:tencent_im_base/tencent_im_base.dart'; class GroupMemberSearchTextField extends TIMUIKitStatelessWidget { @@ -14,13 +16,17 @@ class GroupMemberSearchTextField extends TIMUIKitStatelessWidget { @override Widget tuiBuild(BuildContext context, TUIKitBuildValue value) { final TUITheme theme = value.theme; + final isDesktopScreen = + TUIKitScreenUtils.getFormFactor(context) == DeviceType.Desktop; + final FocusNode focusNode = FocusNode(); var debounceFunc = OptimizeUtils.debounce( (text) => onTextChange(text), const Duration(milliseconds: 300)); + return Container( color: Colors.white, child: Column(children: [ - Container( + if(!isDesktopScreen) Container( decoration: BoxDecoration( borderRadius: const BorderRadius.all(Radius.circular(5.0)), border: Border.all(color: theme.weakBackgroundColor!, width: 12)), @@ -32,6 +38,16 @@ class GroupMemberSearchTextField extends TIMUIKitStatelessWidget { ), ), ), + if(isDesktopScreen) TIMUIKitSearchInput(prefixIcon: Icon( + Icons.search, + size: 16, + color: hexToColor("979797"), + ), + onChange: (text){ + focusNode.requestFocus(); + debounceFunc(text); + }, focusNode: focusNode, + ), Divider( thickness: 1, indent: 74, diff --git a/lib/ui/views/TIMUIKitGroupProfile/widgets/tim_ui_group_profile_widget.dart b/lib/ui/views/TIMUIKitGroupProfile/widgets/tim_ui_group_profile_widget.dart index 291ee98..4753ad5 100644 --- a/lib/ui/views/TIMUIKitGroupProfile/widgets/tim_ui_group_profile_widget.dart +++ b/lib/ui/views/TIMUIKitGroupProfile/widgets/tim_ui_group_profile_widget.dart @@ -1,4 +1,5 @@ import 'package:flutter/cupertino.dart'; +import 'package:tencent_cloud_chat_uikit/ui/utils/screen_utils.dart'; import 'package:tencent_im_base/tencent_im_base.dart'; import 'package:tencent_cloud_chat_uikit/ui/views/TIMUIKitGroupProfile/widgets/tim_ui_group_search_msg.dart'; import 'package:tencent_cloud_chat_uikit/ui/views/TIMUIKitGroupProfile/widgets/tim_uikit_group_add_opt.dart'; @@ -14,11 +15,13 @@ import 'package:tencent_cloud_chat_uikit/ui/views/TIMUIKitGroupProfile/widgets/t class TIMUIKitGroupProfileWidget { static Widget detailCard( {required V2TimGroupInfo groupInfo, + bool isHavePermission = false, /// You can deal with updating group name manually, or UIKIt do it automatically. Function(String updateGroupName)? updateGroupName}) { return GroupProfileDetailCard( groupInfo: groupInfo, + isHavePermission: isHavePermission, updateGroupName: updateGroupName, ); } @@ -27,21 +30,27 @@ class TIMUIKitGroupProfileWidget { return GroupMemberTile(); } - static Widget groupNotification() { - return GroupProfileNotification(); + static Widget groupNotification({ + bool isHavePermission = false, + }) { + return GroupProfileNotification( + isHavePermission: isHavePermission, + ); } static Widget groupManage() { - return GroupProfileGroupManage(); + return const GroupProfileGroupManage(); } static Widget searchMessage(Function(V2TimConversation?) onJumpToSearch) { return GroupProfileGroupSearch(onJumpToSearch: onJumpToSearch); } - static Widget operationDivider() { - return const SizedBox( - height: 10, + static Widget operationDivider(TUITheme theme) { + final isDesktopScreen = TUIKitScreenUtils.getFormFactor() == DeviceType.Desktop; + return Container( + color: theme.weakDividerColor, + height: isDesktopScreen ? 1 : 10, ); } @@ -54,7 +63,7 @@ class TIMUIKitGroupProfileWidget { } static Widget nameCard() { - return GroupProfileNameCard(); + return const GroupProfileNameCard(); } static Widget messageDisturb() { diff --git a/lib/ui/views/TIMUIKitGroupProfile/widgets/tim_ui_group_search_msg.dart b/lib/ui/views/TIMUIKitGroupProfile/widgets/tim_ui_group_search_msg.dart index 23f1033..e70f63a 100644 --- a/lib/ui/views/TIMUIKitGroupProfile/widgets/tim_ui_group_search_msg.dart +++ b/lib/ui/views/TIMUIKitGroupProfile/widgets/tim_ui_group_search_msg.dart @@ -39,7 +39,8 @@ class GroupProfileGroupSearch extends TIMUIKitStatelessWidget { } }, child: Container( - padding: const EdgeInsets.only(top: 12, left: 16, bottom: 12), + padding: const EdgeInsets.symmetric( + vertical: 14, horizontal: 16), decoration: BoxDecoration( color: Colors.white, border: Border( diff --git a/lib/ui/views/TIMUIKitGroupProfile/widgets/tim_uikit_group_add_opt.dart b/lib/ui/views/TIMUIKitGroupProfile/widgets/tim_uikit_group_add_opt.dart index 5484a73..816ac67 100644 --- a/lib/ui/views/TIMUIKitGroupProfile/widgets/tim_uikit_group_add_opt.dart +++ b/lib/ui/views/TIMUIKitGroupProfile/widgets/tim_uikit_group_add_opt.dart @@ -1,5 +1,11 @@ +import 'dart:math'; + import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; +import 'package:tencent_cloud_chat_uikit/data_services/core/tim_uikit_wide_modal_operation_key.dart'; +import 'package:tencent_cloud_chat_uikit/ui/utils/screen_utils.dart'; +import 'package:tencent_cloud_chat_uikit/ui/widgets/column_menu.dart'; +import 'package:tencent_cloud_chat_uikit/ui/widgets/wide_popup.dart'; import 'package:tencent_im_base/tencent_im_base.dart'; import 'package:provider/provider.dart'; import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_statelesswidget.dart'; @@ -16,6 +22,8 @@ class GroupProfileAddOpt extends TIMUIKitStatelessWidget { Widget tuiBuild(BuildContext context, TUIKitBuildValue value) { final TUITheme theme = value.theme; final model = Provider.of(context); + final isDesktopScreen = + TUIKitScreenUtils.getFormFactor(context) == DeviceType.Desktop; String addOpt = TIM_t("未知"); @@ -40,65 +48,87 @@ class GroupProfileAddOpt extends TIMUIKitStatelessWidget { _handleActionTap(int addOpt) async { model.setGroupAddOpt(addOpt).then((res) {}); - Navigator.pop( - context, - "cancel", - ); } return Container( - padding: const EdgeInsets.only(top: 12, left: 16, bottom: 12), + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), decoration: BoxDecoration( color: Colors.white, - border: Border( + border: isDesktopScreen ? null : Border( bottom: BorderSide( color: theme.weakDividerColor ?? CommonColor.weakDividerColor))), child: InkWell( - onTap: () async { - showCupertinoModalPopup( - context: context, - builder: (BuildContext context) { - return CupertinoActionSheet( - title: Text(TIM_t("加群方式")), - cancelButton: CupertinoActionSheetAction( - onPressed: () { - Navigator.pop( - context, - "cancel", - ); - }, - child: Text(TIM_t("取消")), - isDefaultAction: false, - ), - actions: actionList - .map((e) => CupertinoActionSheetAction( - onPressed: () { - _handleActionTap(e["id"] as int); - }, - child: Text( - e["label"] as String, - style: TextStyle(color: theme.primaryColor), - ), - isDefaultAction: false, - )) - .toList(), - ); - }, - ); + onTapDown: (details) async { + if(isDesktopScreen){ + TUIKitWidePopup.showPopupWindow( + operationKey: TUIKitWideModalOperationKey.groupAddOpt, + isDarkBackground: false, + borderRadius: const BorderRadius.all(Radius.circular(4)), + context: context, + offset: Offset(min(details.globalPosition.dx, + MediaQuery.of(context).size.width - 186), details.globalPosition.dy), + child: (onClose) => TUIKitColumnMenu( + data: [ + ...actionList + .map((e){ + return ColumnMenuItem(label: e["label"] as String, onClick: (){ + _handleActionTap(e["id"] as int); + onClose(); + }); + }), + ], + ) + ); + }else{ + showCupertinoModalPopup( + context: context, + builder: (BuildContext context) { + return CupertinoActionSheet( + title: Text(TIM_t("加群方式")), + cancelButton: CupertinoActionSheetAction( + onPressed: () { + Navigator.pop( + context, + "cancel", + ); + }, + child: Text(TIM_t("取消")), + isDefaultAction: false, + ), + actions: actionList + .map((e) => CupertinoActionSheetAction( + onPressed: () { + _handleActionTap(e["id"] as int); + Navigator.pop( + context, + "cancel", + ); + }, + child: Text( + e["label"] as String, + style: TextStyle(color: theme.primaryColor), + ), + isDefaultAction: false, + )) + .toList(), + ); + }, + ); + } }, child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( TIM_t("加群方式"), - style: TextStyle(fontSize: 16, color: theme.darkTextColor), + style: TextStyle(fontSize: isDesktopScreen ? 14 : 16, color: theme.darkTextColor), ), Row( children: [ Text( addOpt, - style: const TextStyle(fontSize: 16, color: Colors.black), + style: TextStyle(fontSize: isDesktopScreen ? 14 : 16, color: Colors.black), ), Icon(Icons.keyboard_arrow_right, color: theme.weakTextColor) ], diff --git a/lib/ui/views/TIMUIKitGroupProfile/widgets/tim_uikit_group_button_area.dart b/lib/ui/views/TIMUIKitGroupProfile/widgets/tim_uikit_group_button_area.dart index 16d00b5..88f5bfd 100644 --- a/lib/ui/views/TIMUIKitGroupProfile/widgets/tim_uikit_group_button_area.dart +++ b/lib/ui/views/TIMUIKitGroupProfile/widgets/tim_uikit_group_button_area.dart @@ -3,10 +3,13 @@ import 'package:flutter/material.dart'; import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_base.dart'; import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_statelesswidget.dart'; import 'package:tencent_cloud_chat_uikit/business_logic/separate_models/tui_group_profile_model.dart'; +import 'package:tencent_cloud_chat_uikit/data_services/core/tim_uikit_wide_modal_operation_key.dart'; import 'package:tencent_cloud_chat_uikit/tencent_cloud_chat_uikit.dart'; import 'package:tencent_cloud_chat_uikit/ui/controller/tim_uikit_chat_controller.dart'; import 'package:tencent_cloud_chat_uikit/ui/utils/platform.dart'; +import 'package:tencent_cloud_chat_uikit/ui/utils/screen_utils.dart'; +import 'package:tencent_cloud_chat_uikit/ui/widgets/wide_popup.dart'; class GroupProfileButtonArea extends TIMUIKitStatelessWidget { final String groupID; @@ -20,164 +23,268 @@ class GroupProfileButtonArea extends TIMUIKitStatelessWidget { : super(key: key); final _operationList = [ - {"label": TIM_t("清空聊天记录"), "id": "clearHistory"}, + {"label": TIM_t("清空消息"), "id": "clearHistory"}, {"label": TIM_t("转让群主"), "id": "transimitOwner"}, - {"label": TIM_t("删除并退出"), "id": "quitGroup"}, - {"label": TIM_t("解散该群"), "id": "dismissGroup"} + {"label": TIM_t("退出群组"), "id": "quitGroup"}, + {"label": TIM_t("解散群组"), "id": "dismissGroup"} ]; _clearHistory(BuildContext context, theme) async { - showCupertinoModalPopup( - context: context, - builder: (BuildContext context) { - return CupertinoActionSheet( - cancelButton: CupertinoActionSheetAction( - onPressed: () { - Navigator.pop( - context, - ); - }, - child: Text(TIM_t("取消")), - isDefaultAction: false, - ), - actions: [ - CupertinoActionSheetAction( - onPressed: () async { + final isDesktopScreen = + TUIKitScreenUtils.getFormFactor(context) == DeviceType.Desktop; + + if (isDesktopScreen) { + TUIKitWidePopup.showSecondaryConfirmDialog( + operationKey: TUIKitWideModalOperationKey.confirmClearChatHistory, + context: context, + text: TIM_t("清空聊天记录"), + theme: theme, + onCancel: () {}, + onConfirm: () async { + if (PlatformUtils().isWeb) { + final res = await sdkInstance + .getConversationManager() + .deleteConversation(conversationID: "group_$groupID"); + if (res.code == 0) { + _timuiKitChatController.clearHistory(groupID); + } + } else { + final res = await sdkInstance + .getMessageManager() + .clearGroupHistoryMessage(groupID: groupID); + if (res.code == 0) { + _timuiKitChatController.clearHistory(groupID); + } + } + }); + } else { + showCupertinoModalPopup( + context: context, + builder: (BuildContext context) { + return CupertinoActionSheet( + cancelButton: CupertinoActionSheetAction( + onPressed: () { Navigator.pop( context, ); - if (PlatformUtils().isWeb) { - final res = await sdkInstance - .getConversationManager() - .deleteConversation(conversationID: "group_$groupID"); - if (res.code == 0) { - _timuiKitChatController.clearHistory(groupID); - } - } else { - final res = await sdkInstance - .getMessageManager() - .clearGroupHistoryMessage(groupID: groupID); - if (res.code == 0) { - _timuiKitChatController.clearHistory(groupID); - } - } }, - child: Text( - TIM_t("清空聊天记录"), - style: TextStyle(color: theme.cautionColor), - ), + child: Text(TIM_t("取消")), isDefaultAction: false, - ) - ], - ); - }, - ); + ), + actions: [ + CupertinoActionSheetAction( + onPressed: () async { + Navigator.pop( + context, + ); + if (PlatformUtils().isWeb) { + final res = await sdkInstance + .getConversationManager() + .deleteConversation(conversationID: "group_$groupID"); + if (res.code == 0) { + _timuiKitChatController.clearHistory(groupID); + } + } else { + final res = await sdkInstance + .getMessageManager() + .clearGroupHistoryMessage(groupID: groupID); + if (res.code == 0) { + _timuiKitChatController.clearHistory(groupID); + } + } + }, + child: Text( + TIM_t("清空聊天记录"), + style: TextStyle(color: theme.cautionColor), + ), + isDefaultAction: false, + ) + ], + ); + }, + ); + } } - _quitGroup(BuildContext context, theme) async { - showCupertinoModalPopup( - context: context, - builder: (BuildContext context) { - return CupertinoActionSheet( - title: Text(TIM_t("退出后不会接收到此群聊消息")), - cancelButton: CupertinoActionSheetAction( - onPressed: () { - Navigator.pop( - context, - ); - }, - child: Text(TIM_t("取消")), - isDefaultAction: false, - ), - actions: [ - CupertinoActionSheetAction( - onPressed: () async { + _quitGroup(BuildContext context, TUITheme theme) async { + final isDesktopScreen = + TUIKitScreenUtils.getFormFactor(context) == DeviceType.Desktop; + + if (isDesktopScreen) { + TUIKitWidePopup.showSecondaryConfirmDialog( + operationKey: TUIKitWideModalOperationKey.confirmExitGroup, + context: context, + text: TIM_t("退出后不会接收到此群聊消息"), + theme: theme, + onCancel: () {}, + onConfirm: () async { + final res = await sdkInstance.quitGroup(groupID: groupID); + if (res.code == 0) { + final deleteConvRes = await sdkInstance + .getConversationManager() + .deleteConversation(conversationID: "group_$groupID"); + if (deleteConvRes.code == 0) { + model.lifeCycle?.didLeaveGroup(); + } + } + }); + } else { + showCupertinoModalPopup( + context: context, + builder: (BuildContext context) { + return CupertinoActionSheet( + title: Text(TIM_t("退出后不会接收到此群聊消息")), + cancelButton: CupertinoActionSheetAction( + onPressed: () { Navigator.pop( context, ); - final res = await sdkInstance.quitGroup(groupID: groupID); - if (res.code == 0) { - final deleteConvRes = await sdkInstance - .getConversationManager() - .deleteConversation(conversationID: "group_$groupID"); - if (deleteConvRes.code == 0) { - model.lifeCycle?.didLeaveGroup(); - } - } }, - child: Text( - TIM_t("确定"), - style: TextStyle(color: theme.cautionColor), - ), + child: Text(TIM_t("取消")), isDefaultAction: false, - ) - ], - ); - }, - ); + ), + actions: [ + CupertinoActionSheetAction( + onPressed: () async { + Navigator.pop( + context, + ); + final res = await sdkInstance.quitGroup(groupID: groupID); + if (res.code == 0) { + final deleteConvRes = await sdkInstance + .getConversationManager() + .deleteConversation(conversationID: "group_$groupID"); + if (deleteConvRes.code == 0) { + model.lifeCycle?.didLeaveGroup(); + } + } + }, + child: Text( + TIM_t("确定"), + style: TextStyle(color: theme.cautionColor), + ), + isDefaultAction: false, + ) + ], + ); + }, + ); + } } _dismissGroup(BuildContext context, theme) async { - showCupertinoModalPopup( - context: context, - builder: (BuildContext context) { - return CupertinoActionSheet( - title: Text(TIM_t("解散后不会接收到此群聊消息")), - cancelButton: CupertinoActionSheetAction( - onPressed: () { - Navigator.pop( - context, - ); - }, - child: Text(TIM_t("取消")), - isDefaultAction: false, - ), - actions: [ - CupertinoActionSheetAction( - onPressed: () async { + final isDesktopScreen = + TUIKitScreenUtils.getFormFactor(context) == DeviceType.Desktop; + + if (isDesktopScreen) { + TUIKitWidePopup.showSecondaryConfirmDialog( + operationKey: TUIKitWideModalOperationKey.confirmDisbandGroup, + context: context, + text: TIM_t("解散后不会接收到此群聊消息"), + theme: theme, + onCancel: () {}, + onConfirm: () async { + final res = await sdkInstance.dismissGroup(groupID: groupID); + if (res.code == 0) { + await sdkInstance + .getConversationManager() + .deleteConversation(conversationID: "group_$groupID"); + model.lifeCycle?.didLeaveGroup(); + } + }); + } else { + showCupertinoModalPopup( + context: context, + builder: (BuildContext context) { + return CupertinoActionSheet( + title: Text(TIM_t("解散后不会接收到此群聊消息")), + cancelButton: CupertinoActionSheetAction( + onPressed: () { Navigator.pop( context, ); - final res = await sdkInstance.dismissGroup(groupID: groupID); - if (res.code == 0) { - await sdkInstance - .getConversationManager() - .deleteConversation(conversationID: "group_$groupID"); - model.lifeCycle?.didLeaveGroup(); - } }, - child: Text( - TIM_t("确定"), - style: TextStyle(color: theme.cautionColor), - ), + child: Text(TIM_t("取消")), isDefaultAction: false, - ) - ], - ); - }, - ); + ), + actions: [ + CupertinoActionSheetAction( + onPressed: () async { + Navigator.pop( + context, + ); + final res = await sdkInstance.dismissGroup(groupID: groupID); + if (res.code == 0) { + await sdkInstance + .getConversationManager() + .deleteConversation(conversationID: "group_$groupID"); + model.lifeCycle?.didLeaveGroup(); + } + }, + child: Text( + TIM_t("确定"), + style: TextStyle(color: theme.cautionColor), + ), + isDefaultAction: false, + ) + ], + ); + }, + ); + } } - _transimitOwner(BuildContext context, String groupID) async { - List? selectedMember = await Navigator.push( - context, - MaterialPageRoute( - builder: (context) => SelectTransimitOwner( + _transmitOwner(BuildContext context, String groupID) async { + final isDesktopScreen = + TUIKitScreenUtils.getFormFactor(context) == DeviceType.Desktop; + + if (isDesktopScreen) { + TUIKitWidePopup.showPopupWindow( + operationKey: TUIKitWideModalOperationKey.setAdmins, + context: context, + title: TIM_t("转让群主"), + width: MediaQuery.of(context).size.width * 0.5, + height: MediaQuery.of(context).size.height * 0.8, + onSubmit: () { + selectNewGroupOwnerKey.currentState?.onSubmit(); + }, + child: (onClose) => SelectNewGroupOwner( model: model, + key: selectNewGroupOwnerKey, groupID: groupID, + onSelectedMember: (selectedMember) async { + if (selectedMember.isNotEmpty) { + final userID = selectedMember.first.userID; + await sdkInstance + .getGroupManager() + .transferGroupOwner(groupID: groupID, userID: userID); + } + }, ), - ), - ); - if (selectedMember != null) { - final userID = selectedMember.first.userID; - await sdkInstance - .getGroupManager() - .transferGroupOwner(groupID: groupID, userID: userID); + ); + } else { + List? selectedMember = await Navigator.push( + context, + MaterialPageRoute( + builder: (context) => SelectNewGroupOwner( + model: model, + groupID: groupID, + ), + ), + ); + if (selectedMember != null) { + final userID = selectedMember.first.userID; + await sdkInstance + .getGroupManager() + .transferGroupOwner(groupID: groupID, userID: userID); + } } } List _renderGroupOperation( BuildContext context, TUITheme theme, bool isOwner, String groupType) { + final isDesktopScreen = + TUIKitScreenUtils.getFormFactor(context) == DeviceType.Desktop; return _operationList .where((element) { if (!isOwner) { @@ -194,33 +301,50 @@ class GroupProfileButtonArea extends TIMUIKitStatelessWidget { return true; } }) - .map((e) => InkWell( - onTap: () { - if (e["id"]! == "clearHistory") { - _clearHistory(context, theme); - } else if (e["id"] == "quitGroup") { - _quitGroup(context, theme); - } else if (e["id"] == "dismissGroup") { - _dismissGroup(context, theme); - } else if (e["id"] == "transimitOwner") { - _transimitOwner(context, groupID); - } - }, - child: Container( - alignment: Alignment.center, - padding: const EdgeInsets.symmetric(vertical: 15), - decoration: BoxDecoration( - color: Colors.white, - border: Border( - bottom: BorderSide( - color: theme.weakDividerColor ?? - CommonColor.weakDividerColor))), + .map((e) => isDesktopScreen + ? OutlinedButton( + onPressed: () { + if (e["id"]! == "clearHistory") { + _clearHistory(context, theme); + } else if (e["id"] == "quitGroup") { + _quitGroup(context, theme); + } else if (e["id"] == "dismissGroup") { + _dismissGroup(context, theme); + } else if (e["id"] == "transimitOwner") { + _transmitOwner(context, groupID); + } + }, child: Text( e["label"]!, - style: TextStyle(color: theme.cautionColor, fontSize: 17), + style: TextStyle(color: theme.cautionColor), + )) + : InkWell( + onTap: () { + if (e["id"]! == "clearHistory") { + _clearHistory(context, theme); + } else if (e["id"] == "quitGroup") { + _quitGroup(context, theme); + } else if (e["id"] == "dismissGroup") { + _dismissGroup(context, theme); + } else if (e["id"] == "transimitOwner") { + _transmitOwner(context, groupID); + } + }, + child: Container( + alignment: Alignment.center, + padding: const EdgeInsets.symmetric(vertical: 15), + decoration: BoxDecoration( + color: Colors.white, + border: Border( + bottom: BorderSide( + color: theme.weakDividerColor ?? + CommonColor.weakDividerColor))), + child: Text( + e["label"]!, + style: TextStyle(color: theme.cautionColor, fontSize: 17), + ), ), - ), - )) + )) .toList(); } @@ -228,6 +352,25 @@ class GroupProfileButtonArea extends TIMUIKitStatelessWidget { Widget tuiBuild(BuildContext context, TUIKitBuildValue value) { final theme = value.theme; final groupInfo = model.groupInfo; + + final isDesktopScreen = + TUIKitScreenUtils.getFormFactor(context) == DeviceType.Desktop; + if (isDesktopScreen) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Wrap( + spacing: 28, + children: [ + ..._renderGroupOperation( + context, + theme, + groupInfo?.owner == coreInstance.loginUserInfo?.userID, + groupInfo?.groupType ?? "") + ], + ), + ); + } + return Column( children: [ ..._renderGroupOperation( diff --git a/lib/ui/views/TIMUIKitGroupProfile/widgets/tim_uikit_group_detail_card.dart b/lib/ui/views/TIMUIKitGroupProfile/widgets/tim_uikit_group_detail_card.dart index 9b4c0db..8bf8458 100644 --- a/lib/ui/views/TIMUIKitGroupProfile/widgets/tim_uikit_group_detail_card.dart +++ b/lib/ui/views/TIMUIKitGroupProfile/widgets/tim_uikit_group_detail_card.dart @@ -1,8 +1,12 @@ +import 'dart:math'; + import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_statelesswidget.dart'; import 'package:tencent_cloud_chat_uikit/business_logic/separate_models/tui_group_profile_model.dart'; +import 'package:tencent_cloud_chat_uikit/ui/utils/screen_utils.dart'; +import 'package:tencent_cloud_chat_uikit/ui/widgets/text_input_bottom_sheet.dart'; import 'package:tencent_im_base/tencent_im_base.dart'; import 'package:tencent_cloud_chat_uikit/ui/widgets/avatar.dart'; @@ -13,9 +17,13 @@ class GroupProfileDetailCard extends TIMUIKitStatelessWidget { final V2TimGroupInfo groupInfo; final void Function(String groupName)? updateGroupName; final TextEditingController controller = TextEditingController(); + final bool isHavePermission; GroupProfileDetailCard( - {Key? key, required this.groupInfo, this.updateGroupName}) + {Key? key, + required this.groupInfo, + this.isHavePermission = false, + this.updateGroupName}) : super(key: key); @override @@ -25,131 +33,168 @@ class GroupProfileDetailCard extends TIMUIKitStatelessWidget { final faceUrl = groupInfo.faceUrl ?? ""; final groupID = groupInfo.groupID; final showName = groupInfo.groupName ?? groupID; + final isDesktopScreen = + TUIKitScreenUtils.getFormFactor(context) == DeviceType.Desktop; + return InkWell( - onTap: (() { - showCupertinoModalPopup( - context: context, - builder: (BuildContext context) { - return CupertinoActionSheet( - cancelButton: CupertinoActionSheetAction( - onPressed: () { - Navigator.pop( - context, - ); - }, - child: Text(TIM_t("取消")), - isDefaultAction: false, - ), - actions: [ - CupertinoActionSheetAction( - onPressed: () { - controller.text = groupInfo.groupName ?? ""; - showModalBottomSheet( - isScrollControlled: true, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(10.0), - ), - context: context, - builder: (context) { - return Container( - decoration: const BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.only( - topLeft: Radius.circular(10.0), - topRight: Radius.circular(10.0))), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Container( - padding: const EdgeInsets.symmetric( - vertical: 20), - child: Text(TIM_t("修改群名称")), - ), - Divider( - height: 2, color: theme.weakDividerColor), - Padding( - padding: const EdgeInsets.all(20), - child: Column( - crossAxisAlignment: - CrossAxisAlignment.start, - children: [ - TextField( - controller: controller, - decoration: InputDecoration( - border: InputBorder.none, - fillColor: - theme.weakBackgroundColor, - filled: true, - isDense: true, - hintText: ''), - ), - const SizedBox( - height: 10, - ), - Text( - TIM_t("修改群名称"), - style: TextStyle( - fontSize: 13, - color: theme.weakTextColor), - textAlign: TextAlign.left, - ), - const SizedBox( - height: 30, - ), - SizedBox( - width: double.infinity, - child: ElevatedButton( - onPressed: () { - final text = - controller.text.trim(); - if (updateGroupName != null) { - updateGroupName!(text); - } else { - model.setGroupName(text); - } - Navigator.pop(context); - Navigator.pop(context); - }, - child: Text(TIM_t("确定")), - )), - const SizedBox( - height: 20, - ), - Padding( - padding: EdgeInsets.only( - bottom: MediaQuery.of(context) - .viewInsets - .bottom), - ) - ], - ), - ) - ], - ), - ); - }); + onTapDown: !isHavePermission + ? null + : ((details) { + if (isDesktopScreen) { + TextInputBottomSheet.showTextInputBottomSheet( + context: context, + title: TIM_t("修改群名称"), + initText: showName, + initOffset: Offset( + min(details.globalPosition.dx, + MediaQuery.of(context).size.width - 350), + min(details.globalPosition.dy + 20, + MediaQuery.of(context).size.height - 470)), + onSubmitted: (String newText) async { + final text = newText.trim(); + if (updateGroupName != null) { + updateGroupName!(text); + } else { + model.setGroupName(text); + } }, - child: Text( - TIM_t("修改群名称"), - style: TextStyle(color: theme.primaryColor), - ), - isDefaultAction: false, - ) - ]); - }, - ); - }), + theme: theme); + } else { + showCupertinoModalPopup( + context: context, + builder: (BuildContext context) { + return CupertinoActionSheet( + cancelButton: CupertinoActionSheetAction( + onPressed: () { + Navigator.pop( + context, + ); + }, + child: Text(TIM_t("取消")), + isDefaultAction: false, + ), + actions: [ + CupertinoActionSheetAction( + onPressed: () { + controller.text = groupInfo.groupName ?? ""; + showModalBottomSheet( + isScrollControlled: true, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10.0), + ), + context: context, + builder: (context) { + return Container( + decoration: const BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.only( + topLeft: Radius.circular(10.0), + topRight: Radius.circular(10.0))), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + padding: const EdgeInsets.symmetric( + vertical: 20), + child: Text(TIM_t("修改群名称")), + ), + Divider( + height: 2, + color: theme.weakDividerColor), + Padding( + padding: const EdgeInsets.all(20), + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + TextField( + controller: controller, + decoration: InputDecoration( + border: InputBorder.none, + fillColor: theme + .weakBackgroundColor, + filled: true, + isDense: true, + hintText: ''), + ), + const SizedBox( + height: 10, + ), + Text( + TIM_t("修改群名称"), + style: TextStyle( + fontSize: 13, + color: + theme.weakTextColor), + textAlign: TextAlign.left, + ), + const SizedBox( + height: 30, + ), + SizedBox( + width: double.infinity, + child: ElevatedButton( + onPressed: () { + final text = controller + .text + .trim(); + if (updateGroupName != + null) { + updateGroupName!( + text); + } else { + model.setGroupName( + text); + } + Navigator.pop(context); + Navigator.pop(context); + }, + child: Text(TIM_t("确定")), + )), + const SizedBox( + height: 20, + ), + Padding( + padding: EdgeInsets.only( + bottom: + MediaQuery.of(context) + .viewInsets + .bottom), + ) + ], + ), + ) + ], + ), + ); + }); + }, + child: Text( + TIM_t("修改群名称"), + style: TextStyle(color: theme.primaryColor), + ), + isDefaultAction: false, + ) + ]); + }, + ); + } + }), child: Container( color: Colors.white, - padding: const EdgeInsets.only(top: 12, bottom: 12, left: 16), + padding: EdgeInsets.only( + top: isDesktopScreen ? 20 : 12, + bottom: isDesktopScreen ? 20 : 12, + right: isDesktopScreen ? 16 : 0, + left: 16), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.center, children: [ SizedBox( - width: 48, - height: 48, + width: isDesktopScreen ? 40 : 48, + height: isDesktopScreen ? 40 : 48, child: Avatar( faceUrl: faceUrl, showName: showName, @@ -162,25 +207,28 @@ class GroupProfileDetailCard extends TIMUIKitStatelessWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( + SelectableText( showName, - style: const TextStyle( - fontSize: 18, fontWeight: FontWeight.w600), + style: TextStyle( + fontSize: isDesktopScreen ? 15 : 18, + fontWeight: FontWeight.w600), ), - const SizedBox( - height: 8, + SizedBox( + height: isDesktopScreen ? 4 : 8, ), - Text("ID: $groupID", + SelectableText("ID: $groupID", style: TextStyle( - fontSize: 13, color: theme.weakTextColor)) + fontSize: isDesktopScreen ? 13 : 13, + color: theme.weakTextColor)) ], ), ), ), - Icon( - Icons.keyboard_arrow_right, - color: theme.weakTextColor, - ) + if (isHavePermission) + Icon( + Icons.keyboard_arrow_right, + color: theme.weakTextColor, + ) ], ), ), diff --git a/lib/ui/views/TIMUIKitGroupProfile/widgets/tim_uikit_group_manage.dart b/lib/ui/views/TIMUIKitGroupProfile/widgets/tim_uikit_group_manage.dart index dc9919a..625a781 100644 --- a/lib/ui/views/TIMUIKitGroupProfile/widgets/tim_uikit_group_manage.dart +++ b/lib/ui/views/TIMUIKitGroupProfile/widgets/tim_uikit_group_manage.dart @@ -1,56 +1,95 @@ +import 'dart:math'; + import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_slidable_for_tencent_im/flutter_slidable.dart'; +import 'package:tencent_cloud_chat_uikit/data_services/core/tim_uikit_wide_modal_operation_key.dart'; +import 'package:tencent_cloud_chat_uikit/ui/utils/screen_utils.dart'; +import 'package:tencent_cloud_chat_uikit/ui/views/TIMUIKitProfile/widget/tim_uikit_operation_item.dart'; +import 'package:tencent_cloud_chat_uikit/ui/widgets/column_menu.dart'; +import 'package:tencent_cloud_chat_uikit/ui/widgets/wide_popup.dart'; import 'package:tencent_im_base/tencent_im_base.dart'; import 'package:provider/provider.dart'; import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_state.dart'; -import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_statelesswidget.dart'; import 'package:tencent_cloud_chat_uikit/business_logic/separate_models/tui_group_profile_model.dart'; - import 'package:tencent_cloud_chat_uikit/data_services/services_locatar.dart'; - - import 'package:tencent_cloud_chat_uikit/ui/widgets/avatar.dart'; import 'package:tencent_cloud_chat_uikit/ui/widgets/radio_button.dart'; - import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_base.dart'; -class GroupProfileGroupManage extends TIMUIKitStatelessWidget { - GroupProfileGroupManage({Key? key}) : super(key: key); +GlobalKey<_GroupProfileAddAdminState> groupProfileAddAdminKey = GlobalKey(); + +class GroupProfileGroupManage extends StatefulWidget { + const GroupProfileGroupManage({Key? key}) : super(key: key); + + @override + State createState() => GroupProfileGroupManageState(); +} + +class GroupProfileGroupManageState + extends TIMUIKitState { + bool isShowManageBox = false; @override Widget tuiBuild(BuildContext context, TUIKitBuildValue value) { final TUITheme theme = value.theme; - + final isDesktopScreen = + TUIKitScreenUtils.getFormFactor(context) == DeviceType.Desktop; final model = Provider.of(context); - return InkWell( - onTap: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => GroupProfileGroupManagePage( - model: model, - ))); - }, - child: Container( - padding: const EdgeInsets.only(top: 12, left: 16, bottom: 12), - decoration: BoxDecoration( - color: Colors.white, - border: Border( - bottom: BorderSide( - color: theme.weakDividerColor ?? - CommonColor.weakDividerColor))), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - TIM_t("群管理"), - style: TextStyle(fontSize: 16, color: theme.darkTextColor), + return Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + decoration: BoxDecoration( + color: Colors.white, + border: isDesktopScreen + ? null + : Border( + bottom: BorderSide( + color: theme.weakDividerColor ?? + CommonColor.weakDividerColor))), + child: Column( + children: [ + InkWell( + onTap: () { + final isDesktopScreen = + TUIKitScreenUtils.getFormFactor(context) == + DeviceType.Desktop; + if (!isDesktopScreen) { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => GroupProfileGroupManagePage( + model: model, + ))); + } else { + setState(() { + isShowManageBox = !isShowManageBox; + }); + } + }, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + TIM_t("群管理"), + style: TextStyle( + fontSize: isDesktopScreen ? 14 : 16, + color: theme.darkTextColor), + ), + AnimatedRotation( + turns: isShowManageBox ? 0.25 : 0, + duration: const Duration(milliseconds: 200), + child: Icon(Icons.keyboard_arrow_right, + color: theme.weakTextColor), + ) + ], ), - Icon(Icons.keyboard_arrow_right, color: theme.weakTextColor) - ], - ), + ), + if (isShowManageBox) + GroupProfileGroupManagePage( + model: model, + ) + ], ), ); } @@ -59,6 +98,7 @@ class GroupProfileGroupManage extends TIMUIKitStatelessWidget { /// 管理员设置页面 class GroupProfileGroupManagePage extends StatefulWidget { final TUIGroupProfileModel model; + const GroupProfileGroupManagePage({ Key? key, required this.model, @@ -79,7 +119,7 @@ class _GroupProfileGroupManagePageState } void getServerTime() async { - final res = await TencentImSDKPlugin.v2TIMManager.getServerTime(); + final res = await TencentImSDKPlugin.v2TIMManager.getServerTime(); setState(() { serverTime = res.data; }); @@ -98,125 +138,152 @@ class _GroupProfileGroupManagePageState Provider.of(context).groupMemberList; final theme = Provider.of(context).theme; final isAllMuted = widget.model.groupInfo?.isAllMuted ?? false; - return Scaffold( - appBar: AppBar( - title: Text( - TIM_t("群管理"), - style: const TextStyle(color: Colors.white, fontSize: 17), - ), - shadowColor: theme.weakDividerColor, - flexibleSpace: Container( - decoration: BoxDecoration( - gradient: LinearGradient(colors: [ - theme.lightPrimaryColor ?? CommonColor.lightPrimaryColor, - theme.primaryColor ?? CommonColor.primaryColor - ]), - ), - ), - iconTheme: const IconThemeData( - color: Colors.white, - ), - leading: IconButton( - padding: const EdgeInsets.only(left: 16), - constraints: const BoxConstraints(), - icon: Image.asset( - 'images/arrow_back.png', - package: 'tencent_cloud_chat_uikit', - height: 34, - width: 34, - ), - onPressed: () async { - if (isAllMuted != widget.model.groupInfo?.isAllMuted) { - widget.model.setMuteAll(isAllMuted); - } - Navigator.pop(context); - }, - ), - ), - body: Column( + final bool isAllowMuteMember = + (widget.model.groupInfo?.groupType ?? "") != GroupType.Work; + final isDesktopScreen = + TUIKitScreenUtils.getFormFactor(context) == DeviceType.Desktop; + + Widget managePage() { + return Column( children: [ Container( - padding: const EdgeInsets.only( - top: 12, left: 16, bottom: 12, right: 12), + padding: EdgeInsets.only( + top: 12, + left: isDesktopScreen ? 0 : 16, + bottom: isDesktopScreen ? 0 : 12, + right: isDesktopScreen ? 0 : 12), decoration: BoxDecoration( color: Colors.white, - border: Border( - bottom: BorderSide( - color: theme.weakDividerColor ?? - CommonColor.weakDividerColor))), + border: isDesktopScreen + ? null + : Border( + bottom: BorderSide( + color: theme.weakDividerColor ?? + CommonColor.weakDividerColor))), child: InkWell( - onTap: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => GroupProfileSetManagerPage( - model: widget.model, - ), - )); - }, + onTap: isDesktopScreen + ? null + : () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => + GroupProfileSetManagerPage( + model: widget.model, + ), + )); + }, + child: isDesktopScreen + ? Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text(TIM_t("群管理员"), + style: TextStyle( + fontSize: 14, + color: theme.darkTextColor)), + ], + ) + : Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text(TIM_t("设置管理员"), + style: TextStyle( + fontSize: isDesktopScreen ? 14 : 16, + color: theme.darkTextColor)), + Icon(Icons.keyboard_arrow_right, + color: theme.weakTextColor) + ], + ), + ), + ), + if (isDesktopScreen) + GroupProfileSetManagerPage( + model: widget.model, + ), + if (!isDesktopScreen) + Container( + padding: const EdgeInsets.only( + top: 12, left: 16, bottom: 12, right: 12), + decoration: BoxDecoration( + color: Colors.white, + border: Border( + bottom: BorderSide( + color: theme.weakDividerColor ?? + CommonColor.weakDividerColor))), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Text(TIM_t("设置管理员"), - style: TextStyle( - fontSize: 16, color: theme.darkTextColor)), - Icon(Icons.keyboard_arrow_right, - color: theme.weakTextColor) + Text( + TIM_t("全员禁言"), + style: TextStyle( + fontSize: 16, color: theme.darkTextColor), + ), + CupertinoSwitch( + value: isAllMuted, + onChanged: (value) async { + widget.model.setMuteAll(value); + }, + activeColor: theme.primaryColor) ], ), ), - ), - Container( - padding: const EdgeInsets.only( - top: 12, left: 16, bottom: 12, right: 12), - decoration: BoxDecoration( - color: Colors.white, - border: Border( - bottom: BorderSide( - color: theme.weakDividerColor ?? - CommonColor.weakDividerColor))), - child: Row( + if (isDesktopScreen && isAllowMuteMember) + Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Text( - TIM_t("全员禁言"), - style: - TextStyle(fontSize: 16, color: theme.darkTextColor), - ), - CupertinoSwitch( - value: isAllMuted, - onChanged: (value) async { - widget.model.setMuteAll(value); - }, - activeColor: theme.primaryColor) + Text(TIM_t("禁言"), + style: TextStyle( + fontSize: 14, color: theme.darkTextColor)), ], ), - ), - Container( - padding: - const EdgeInsets.symmetric(vertical: 10, horizontal: 16), - color: theme.weakBackgroundColor, - alignment: Alignment.topLeft, - child: Text( - TIM_t("全员禁言开启后,只允许群主和管理员发言。"), - style: TextStyle(fontSize: 12, color: theme.weakTextColor), + if (isDesktopScreen) + Container( + padding: const EdgeInsets.symmetric(vertical: 8), + child: TIMUIKitOperationItem( + isEmpty: false, + operationName: TIM_t("全员禁言"), + type: "switch", + isUseCheckedBoxOnWide: true, + operationDescription: TIM_t("全员禁言开启后,只允许群主和管理员发言。"), + operationValue: isAllMuted, + onSwitchChange: (value) { + widget.model.setMuteAll(value); + }, + ), ), - ), - if (!isAllMuted) + if (!isDesktopScreen) + Container( + padding: const EdgeInsets.symmetric( + vertical: 10, horizontal: 16), + color: theme.weakBackgroundColor, + alignment: Alignment.topLeft, + child: Text( + TIM_t("全员禁言开启后,只允许群主和管理员发言。"), + style: + TextStyle(fontSize: 12, color: theme.weakTextColor), + ), + ), + if (!isAllMuted && isAllowMuteMember) InkWell( child: Container( color: Colors.white, padding: const EdgeInsets.only(left: 16), child: Container( - padding: const EdgeInsets.symmetric( - vertical: 12, - ), - decoration: BoxDecoration( - color: Colors.white, - border: Border( - bottom: BorderSide( - color: theme.weakDividerColor ?? - CommonColor.weakDividerColor))), + padding: !isDesktopScreen + ? const EdgeInsets.symmetric( + vertical: 12, + ) + : const EdgeInsets.only( + bottom: 4, + ), + decoration: isDesktopScreen + ? null + : BoxDecoration( + color: Colors.white, + border: Border( + bottom: BorderSide( + color: theme.weakDividerColor ?? + CommonColor.weakDividerColor))), child: Row( children: [ Icon( @@ -232,58 +299,149 @@ class _GroupProfileGroupManagePageState ), )), onTap: () async { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => GroupProfileAddAdmin( - appbarTitle: TIM_t("设置禁言"), - memberList: memberList.where((element) { - final isMute = (serverTime != null - ? (element?.muteUntil ?? 0) > - serverTime! - : false); - final isMember = element!.role == - GroupMemberRoleType - .V2TIM_GROUP_MEMBER_ROLE_MEMBER; - return !isMute && isMember; - }).toList(), - selectCompletedHandler: - (context, selectedMember) async { - if (selectedMember.isNotEmpty) { - for (var member in selectedMember) { - final userID = member!.userID; - widget.model - .muteGroupMember(userID, true, serverTime); - } - } - }, - ))); + Widget muteMember() { + return GroupProfileAddAdmin( + key: groupProfileAddAdminKey, + appbarTitle: TIM_t("设置禁言"), + memberList: memberList.where((element) { + final isMute = (serverTime != null + ? (element?.muteUntil ?? 0) > serverTime! + : false); + final isMember = element!.role == + GroupMemberRoleType + .V2TIM_GROUP_MEMBER_ROLE_MEMBER; + return !isMute && isMember; + }).toList(), + selectCompletedHandler: + (context, selectedMember) async { + if (selectedMember.isNotEmpty) { + for (var member in selectedMember) { + final userID = member!.userID; + widget.model + .muteGroupMember(userID, true, serverTime); + } + } + }, + ); + } + + if (isDesktopScreen) { + TUIKitWidePopup.showPopupWindow( + operationKey: TUIKitWideModalOperationKey.setMute, + context: context, + title: TIM_t("设置禁言"), + width: MediaQuery.of(context).size.width * 0.5, + height: MediaQuery.of(context).size.height * 0.8, + onSubmit: () { + groupProfileAddAdminKey.currentState?.onSubmit(); + }, + child: (onClose) => muteMember()); + } else { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => muteMember())); + } }, ), - if (!isAllMuted) + if (!isAllMuted && isAllowMuteMember) ...memberList .where((element) => (serverTime != null ? (element?.muteUntil ?? 0) > serverTime! : false)) - .map((e) => _buildListItem( - context, - e!, - ActionPane(motion: const DrawerMotion(), children: [ - SlidableAction( - onPressed: (_) { - widget.model.muteGroupMember(e.userID, false, serverTime); + .map((e) => Container( + padding: isDesktopScreen + ? const EdgeInsets.only(left: 16) + : null, + child: GestureDetector( + onSecondaryTapDown: (details) { + TUIKitWidePopup.showPopupWindow( + operationKey: + TUIKitWideModalOperationKey.setUnmute, + isDarkBackground: false, + borderRadius: const BorderRadius.all( + Radius.circular(4)), + context: context, + offset: Offset( + min( + details.globalPosition.dx, + MediaQuery.of(context).size.width - + 80), + details.globalPosition.dy), + child: (onClose) => TUIKitColumnMenu(data: [ + ColumnMenuItem( + label: TIM_t("删除"), + icon: const Icon( + Icons.remove_circle_outline, + size: 16), + onClick: () { + widget.model.muteGroupMember( + e?.userID ?? "", + false, + serverTime); + onClose(); + }), + ])); }, - flex: 1, - backgroundColor: theme.cautionColor ?? - CommonColor.cautionColor, - autoClose: true, - label: TIM_t("删除"), - ) - ]))) + child: _buildListItem( + context, + e!, + ActionPane( + motion: const DrawerMotion(), + children: [ + SlidableAction( + onPressed: (_) { + widget.model.muteGroupMember( + e.userID, false, serverTime); + }, + flex: 1, + backgroundColor: theme.cautionColor ?? + CommonColor.cautionColor, + autoClose: true, + label: TIM_t("删除"), + ) + ])), + ), + )) .toList() ], - ), - ); + ); + } + + return TUIKitScreenUtils.getDeviceWidget( + desktopWidget: managePage(), + defaultWidget: Scaffold( + appBar: AppBar( + title: Text( + TIM_t("群管理"), + style: + TextStyle(color: theme.appbarTextColor, fontSize: 17), + ), + backgroundColor: theme.appbarBgColor ?? theme.primaryColor, + shadowColor: theme.weakDividerColor, + iconTheme: IconThemeData( + color: theme.appbarTextColor, + ), + leading: IconButton( + padding: const EdgeInsets.only(left: 16), + constraints: const BoxConstraints(), + icon: Image.asset( + 'images/arrow_back.png', + package: 'tencent_cloud_chat_uikit', + height: 34, + width: 34, + color: theme.appbarTextColor, + ), + onPressed: () async { + if (isAllMuted != widget.model.groupInfo?.isAllMuted) { + widget.model.setMuteAll(isAllMuted); + } + Navigator.pop(context); + }, + ), + ), + body: managePage(), + )); }); } } @@ -305,43 +463,47 @@ _getShowName(V2TimGroupMemberFullInfo? item) { Widget _buildListItem(BuildContext context, V2TimGroupMemberFullInfo memberInfo, ActionPane? endActionPane) { final theme = Provider.of(context).theme; - return Container( + final isDesktopScreen = + TUIKitScreenUtils.getFormFactor(context) == DeviceType.Desktop; + + Widget nameItem() { + return Container( color: Colors.white, - child: ListView.builder( - itemCount: 1, - shrinkWrap: true, - itemBuilder: (context, index){ - return Slidable( - endActionPane: endActionPane, - child: Column(children: [ - ListTile( - tileColor: Colors.black, - leading: SizedBox( - width: 36, - height: 36, - child: Avatar( - faceUrl: memberInfo.faceUrl ?? "", - showName: _getShowName(memberInfo), - type: 2, - ), - ), - title: Row( - children: [ - Text(_getShowName(memberInfo), - style: const TextStyle(fontSize: 16)), - ], - ), - onTap: () {}, - ), - Divider( - thickness: 1, - indent: 74, - endIndent: 0, - color: theme.weakDividerColor, - height: 0) - ])); - }) - ); + child: Column(children: [ + ListTile( + tileColor: Colors.black, + leading: SizedBox( + width: isDesktopScreen ? 30 : 36, + height: isDesktopScreen ? 30 : 36, + child: Avatar( + faceUrl: memberInfo.faceUrl ?? "", + showName: _getShowName(memberInfo), + type: 2, + ), + ), + title: Row( + children: [ + Text(_getShowName(memberInfo), + style: TextStyle(fontSize: isDesktopScreen ? 14 : 16)), + ], + ), + onTap: () {}, + ), + if (!isDesktopScreen) + Divider( + thickness: 1, + indent: 74, + endIndent: 0, + color: theme.weakDividerColor, + height: 0) + ]), + ); + } + + return TUIKitScreenUtils.getDeviceWidget( + desktopWidget: nameItem(), + defaultWidget: SingleChildScrollView( + child: Slidable(endActionPane: endActionPane, child: nameItem()))); } /// 选择管理员 @@ -350,6 +512,7 @@ class GroupProfileSetManagerPage extends StatefulWidget { const GroupProfileSetManagerPage({Key? key, required this.model}) : super(key: key); + @override State createState() => _GroupProfileSetManagerPageState(); } @@ -395,54 +558,65 @@ class _GroupProfileSetManagerPageState final adminList = _getAdminMemberList(memberList); final ownerList = _getOwnerList(memberList); final String option2 = adminList.length.toString(); - return Scaffold( - appBar: AppBar( - title: Text( - TIM_t("设置管理员"), - style: const TextStyle(color: Colors.white, fontSize: 17), - ), - shadowColor: theme.weakDividerColor, - flexibleSpace: Container( - decoration: BoxDecoration( - gradient: LinearGradient(colors: [ - theme.lightPrimaryColor ?? CommonColor.lightPrimaryColor, - theme.primaryColor ?? CommonColor.primaryColor - ]), - ), - ), - iconTheme: const IconThemeData( - color: Colors.white, - ), - ), - body: SingleChildScrollView( + final isDesktopScreen = + TUIKitScreenUtils.getFormFactor(context) == DeviceType.Desktop; + + Widget adminPage() { + return SingleChildScrollView( child: Column( children: [ - Container( - alignment: Alignment.topLeft, - color: theme.weakDividerColor, - padding: - const EdgeInsets.symmetric(vertical: 6, horizontal: 16), - child: Text( - TIM_t("群主"), - style: TextStyle(fontSize: 14, color: theme.weakTextColor), + if (!isDesktopScreen) + Container( + alignment: Alignment.topLeft, + color: theme.weakDividerColor, + padding: + const EdgeInsets.symmetric(vertical: 6, horizontal: 16), + child: Text( + TIM_t("群主"), + style: TextStyle(fontSize: 14, color: theme.weakTextColor), + ), + ), + if (isDesktopScreen) + Container( + alignment: Alignment.topLeft, + padding: const EdgeInsets.only(top: 10, bottom: 4, left: 16), + child: Text( + TIM_t("群主"), + style: TextStyle(fontSize: 14, color: theme.primaryColor), + ), ), - ), ...ownerList .map( - (e) => _buildListItem(context, e!, null), + (e) => Container( + padding: isDesktopScreen + ? const EdgeInsets.only(left: 16) + : null, + child: _buildListItem(context, e!, null), + ), ) .toList(), - Container( - alignment: Alignment.topLeft, - color: theme.weakDividerColor, - padding: - const EdgeInsets.symmetric(vertical: 6, horizontal: 16), - child: Text( - TIM_t_para("管理员 ({{option2}}/10)", "管理员 ($option2/10)")( - option2: option2), - style: TextStyle(fontSize: 14, color: theme.weakTextColor), + if (!isDesktopScreen) + Container( + alignment: Alignment.topLeft, + color: theme.weakDividerColor, + padding: + const EdgeInsets.symmetric(vertical: 6, horizontal: 16), + child: Text( + TIM_t_para("管理员 ({{option2}}/10)", "管理员 ($option2/10)")( + option2: option2), + style: TextStyle(fontSize: 14, color: theme.weakTextColor), + ), + ), + if (isDesktopScreen) + Container( + alignment: Alignment.topLeft, + padding: const EdgeInsets.only(top: 10, bottom: 4, left: 16), + child: Text( + TIM_t_para("管理员 ({{option2}}/10)", "管理员 ($option2/10)")( + option2: option2), + style: TextStyle(fontSize: 14, color: theme.primaryColor), + ), ), - ), InkWell( child: Container( color: Colors.white, @@ -451,12 +625,14 @@ class _GroupProfileSetManagerPageState padding: const EdgeInsets.symmetric( vertical: 12, ), - decoration: BoxDecoration( - color: Colors.white, - border: Border( - bottom: BorderSide( - color: theme.weakDividerColor ?? - CommonColor.weakDividerColor))), + decoration: isDesktopScreen + ? null + : BoxDecoration( + color: Colors.white, + border: Border( + bottom: BorderSide( + color: theme.weakDividerColor ?? + CommonColor.weakDividerColor))), child: Row( children: [ Icon( @@ -472,49 +648,131 @@ class _GroupProfileSetManagerPageState ), )), onTap: () async { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => GroupProfileAddAdmin( - memberList: memberList - .where((element) => - element?.role == - GroupMemberRoleType - .V2TIM_GROUP_MEMBER_ROLE_MEMBER) - .toList(), - appbarTitle: TIM_t("设置管理员"), - selectCompletedHandler: - (context, selectedMember) async { - if (selectedMember.isNotEmpty) { - for (var member in selectedMember) { - final userID = member!.userID; - widget.model.setMemberToAdmin(userID); - } + if (isDesktopScreen) { + TUIKitWidePopup.showPopupWindow( + operationKey: TUIKitWideModalOperationKey.setAdmins, + context: context, + title: TIM_t("设置管理员"), + width: MediaQuery.of(context).size.width * 0.5, + height: MediaQuery.of(context).size.height * 0.8, + onSubmit: () { + groupProfileAddAdminKey.currentState?.onSubmit(); + }, + child: (onClose) => GroupProfileAddAdmin( + key: groupProfileAddAdminKey, + memberList: memberList + .where((element) => + element?.role == + GroupMemberRoleType + .V2TIM_GROUP_MEMBER_ROLE_MEMBER) + .toList(), + appbarTitle: TIM_t("设置管理员"), + selectCompletedHandler: + (context, selectedMember) async { + if (selectedMember.isNotEmpty) { + for (var member in selectedMember) { + final userID = member!.userID; + widget.model.setMemberToAdmin(userID); } - }, - ))); + } + }, + )); + } else { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => GroupProfileAddAdmin( + key: groupProfileAddAdminKey, + memberList: memberList + .where((element) => + element?.role == + GroupMemberRoleType + .V2TIM_GROUP_MEMBER_ROLE_MEMBER) + .toList(), + appbarTitle: TIM_t("设置管理员"), + selectCompletedHandler: + (context, selectedMember) async { + if (selectedMember.isNotEmpty) { + for (var member in selectedMember) { + final userID = member!.userID; + widget.model.setMemberToAdmin(userID); + } + } + }, + ))); + } }, ), ...adminList - .map((e) => _buildListItem( - context, - e!, - ActionPane(motion: const DrawerMotion(), children: [ - SlidableAction( - onPressed: (_) { - _removeAdmin(context, e); - }, - flex: 1, - backgroundColor: - theme.cautionColor ?? CommonColor.cautionColor, - autoClose: true, - label: TIM_t("删除"), - ) - ]))) + .map((e) => GestureDetector( + onSecondaryTapDown: (details) { + TUIKitWidePopup.showPopupWindow( + operationKey: + TUIKitWideModalOperationKey.deleteAdmin, + isDarkBackground: false, + borderRadius: + const BorderRadius.all(Radius.circular(4)), + context: context, + offset: Offset( + min(details.globalPosition.dx, + MediaQuery.of(context).size.width - 80), + details.globalPosition.dy), + child: (onClose) => TUIKitColumnMenu(data: [ + ColumnMenuItem( + label: TIM_t("删除"), + icon: const Icon( + Icons.remove_circle_outline, + size: 16), + onClick: () { + _removeAdmin(context, e!); + onClose(); + }), + ])); + }, + child: Container( + padding: isDesktopScreen + ? const EdgeInsets.only(left: 16) + : null, + child: _buildListItem( + context, + e!, + ActionPane( + motion: const DrawerMotion(), + children: [ + SlidableAction( + onPressed: (_) { + _removeAdmin(context, e); + }, + flex: 1, + backgroundColor: theme.cautionColor ?? + CommonColor.cautionColor, + autoClose: true, + label: TIM_t("删除"), + ) + ])), + ), + )) .toList(), ], - )), - ); + )); + } + + return TUIKitScreenUtils.getDeviceWidget( + desktopWidget: adminPage(), + defaultWidget: Scaffold( + appBar: AppBar( + title: Text( + TIM_t("设置管理员"), + style: TextStyle(color: theme.appbarTextColor, fontSize: 17), + ), + shadowColor: theme.weakDividerColor, + backgroundColor: theme.appbarBgColor ?? theme.primaryColor, + iconTheme: IconThemeData( + color: theme.appbarTextColor, + ), + ), + body: adminPage(), + )); }, ); } @@ -542,59 +800,18 @@ class GroupProfileAddAdmin extends StatefulWidget { class _GroupProfileAddAdminState extends TIMUIKitState { List selectedMemberList = []; + void onSubmit() { + if (widget.selectCompletedHandler != null) { + widget.selectCompletedHandler!(context, selectedMemberList); + } + } + @override Widget tuiBuild(BuildContext context, TUIKitBuildValue value) { final TUITheme theme = value.theme; - return Scaffold( - appBar: AppBar( - title: Text( - widget.appbarTitle, - style: const TextStyle(color: Colors.white, fontSize: 17), - ), - shadowColor: theme.weakDividerColor, - flexibleSpace: Container( - decoration: BoxDecoration( - gradient: LinearGradient(colors: [ - theme.lightPrimaryColor ?? CommonColor.lightPrimaryColor, - theme.primaryColor ?? CommonColor.primaryColor - ]), - ), - ), - iconTheme: const IconThemeData( - color: Colors.white, - ), - leading: TextButton( - onPressed: () { - Navigator.of(context).pop(); - }, - child: Text( - TIM_t("取消"), - style: const TextStyle( - color: Colors.white, - fontSize: 16, - ), - ), - ), - actions: [ - TextButton( - onPressed: () { - if (widget.selectCompletedHandler != null) { - widget.selectCompletedHandler!(context, selectedMemberList); - } - Navigator.of(context).pop(); - }, - child: Text( - TIM_t("完成"), - style: const TextStyle( - color: Colors.white, - fontSize: 16, - ), - ), - ) - ], - ), - body: SingleChildScrollView( + Widget addAdminPage() { + return SingleChildScrollView( child: Column( children: [ Container( @@ -655,7 +872,54 @@ class _GroupProfileAddAdminState extends TIMUIKitState { )) .toList(), ], - )), - ); + )); + } + + return TUIKitScreenUtils.getDeviceWidget( + desktopWidget: Container( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: addAdminPage(), + ), + defaultWidget: Scaffold( + appBar: AppBar( + title: Text( + widget.appbarTitle, + style: TextStyle(color: theme.appbarTextColor, fontSize: 17), + ), + shadowColor: theme.weakDividerColor, + backgroundColor: theme.appbarBgColor ?? theme.primaryColor, + iconTheme: IconThemeData( + color: theme.appbarTextColor, + ), + leadingWidth: 80, + leading: TextButton( + onPressed: () { + Navigator.of(context).pop(); + }, + child: Text( + TIM_t("取消"), + style: TextStyle( + color: theme.appbarTextColor, + fontSize: 14, + ), + ), + ), + actions: [ + TextButton( + onPressed: () { + onSubmit(); + Navigator.of(context).pop(); + }, + child: Text( + TIM_t("完成"), + style: TextStyle( + color: theme.appbarTextColor, + fontSize: 14, + ), + ), + ) + ], + ), + body: addAdminPage())); } } diff --git a/lib/ui/views/TIMUIKitGroupProfile/widgets/tim_uikit_group_member_tile.dart b/lib/ui/views/TIMUIKitGroupProfile/widgets/tim_uikit_group_member_tile.dart index 1b6661f..6f84e95 100644 --- a/lib/ui/views/TIMUIKitGroupProfile/widgets/tim_uikit_group_member_tile.dart +++ b/lib/ui/views/TIMUIKitGroupProfile/widgets/tim_uikit_group_member_tile.dart @@ -5,7 +5,8 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_statelesswidget.dart'; import 'package:tencent_cloud_chat_uikit/business_logic/separate_models/tui_group_profile_model.dart'; - +import 'package:tencent_cloud_chat_uikit/data_services/core/tim_uikit_wide_modal_operation_key.dart'; +import 'package:tencent_cloud_chat_uikit/ui/utils/screen_utils.dart'; import 'package:tencent_cloud_chat_uikit/ui/views/TIMUIKitGroupProfile/group_member/tui_add_group_member.dart'; import 'package:tencent_cloud_chat_uikit/ui/views/TIMUIKitGroupProfile/group_member/tui_delete_group_member.dart'; @@ -13,6 +14,7 @@ import 'package:tencent_cloud_chat_uikit/ui/views/TIMUIKitGroupProfile/group_mem import 'package:tencent_cloud_chat_uikit/ui/widgets/avatar.dart'; import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_base.dart'; +import 'package:tencent_cloud_chat_uikit/ui/widgets/wide_popup.dart'; import 'package:tencent_im_base/tencent_im_base.dart'; class GroupMemberTile extends TIMUIKitStatelessWidget { @@ -20,9 +22,9 @@ class GroupMemberTile extends TIMUIKitStatelessWidget { Key? key, }) : super(key: key); - List _getMemberList(memberList) { - if (memberList.length > 8) { - return memberList.getRange(0, 8).toList(); + List _getMemberList(memberList, int showRange) { + if (memberList.length > showRange) { + return memberList.getRange(0, showRange).toList(); } else { return memberList; } @@ -36,43 +38,47 @@ class GroupMemberTile extends TIMUIKitStatelessWidget { return friendRemark != "" ? friendRemark : showName; } - List _groupMemberListBuilder( - List memberList, TUITheme theme, TUIGroupProfileModel model) { - return _getMemberList(memberList).map((element) { + List _groupMemberListBuilder(List memberList, TUITheme theme, + TUIGroupProfileModel model, int showRange) { + final isDesktopScreen = TUIKitScreenUtils.getFormFactor() == DeviceType.Desktop; + return _getMemberList(memberList, showRange).map((element) { final faceUrl = element?.faceUrl ?? ""; final showName = _getShowName(element); - return GestureDetector( - onTap: () { + return InkWell( + onTapDown: (details) { if (model.onClickUser != null && element?.userID != null) { - model.onClickUser!(element!.userID); + model.onClickUser!(element!.userID, details); } }, child: SizedBox( - width: 60, - height: 76, + width: isDesktopScreen ? 36 : 60, + height: isDesktopScreen ? 36 : 76, child: Column( crossAxisAlignment: CrossAxisAlignment.center, children: [ SizedBox( - width: 50, - height: 50, + width: isDesktopScreen ? 36 : 50, + height: isDesktopScreen ? 36 : 50, child: Avatar( + borderRadius: isDesktopScreen ? BorderRadius.circular(18) : null, faceUrl: faceUrl, showName: showName, type: 1, ), ), - const SizedBox( - height: 8, - ), - Text( - showName, - textAlign: TextAlign.center, - style: TextStyle( - overflow: TextOverflow.ellipsis, - color: theme.weakTextColor, - fontSize: 10), - ) + if (!isDesktopScreen) + const SizedBox( + height: 8, + ), + if (!isDesktopScreen) + Text( + showName, + textAlign: TextAlign.center, + style: TextStyle( + overflow: TextOverflow.ellipsis, + color: theme.weakTextColor, + fontSize: 10), + ) ], ), ), @@ -85,51 +91,85 @@ class GroupMemberTile extends TIMUIKitStatelessWidget { return []; } + void navigateToMemberList(BuildContext context, TUIGroupProfileModel model, + List memberList) { + final isDesktopScreen = + TUIKitScreenUtils.getFormFactor(context) == DeviceType.Desktop; + if (!isDesktopScreen) { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => GroupProfileMemberListPage( + model: model, memberList: memberList), + )); + } else { + final option1 = memberList.length.toString(); + TUIKitWidePopup.showPopupWindow( + operationKey: TUIKitWideModalOperationKey.groupMembersList, + context: context, + width: MediaQuery.of(context).size.width * 0.5, + height: MediaQuery.of(context).size.height * 0.8, + title: TIM_t_para("群成员({{option1}}人)", "群成员($option1人)")( + option1: option1), + child: (onClose) => + GroupProfileMemberListPage(model: model, memberList: memberList)); + } + } + @override Widget tuiBuild(BuildContext context, TUIKitBuildValue value) { final TUITheme theme = value.theme; - + final isDesktopScreen = + TUIKitScreenUtils.getFormFactor(context) == DeviceType.Desktop; final model = Provider.of(context); final memberAmount = model.groupInfo?.memberCount ?? 0; final option1 = memberAmount.toString(); final memberList = model.groupMemberList; final isCanInviteMember = model.canInviteMember(); final isCanKickOffMember = model.canKickOffMember(); + + int showRange = isDesktopScreen ? 7 : 8; + if (isDesktopScreen && isCanInviteMember) { + showRange--; + } + if (isDesktopScreen && isCanKickOffMember) { + showRange--; + } + return Container( - padding: const EdgeInsets.only(top: 12, left: 16, bottom: 12), + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), color: Colors.white, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Container( padding: const EdgeInsets.only(bottom: 12), - decoration: BoxDecoration( - border: Border( - bottom: BorderSide( - color: theme.weakDividerColor ?? - CommonColor.weakDividerColor))), + decoration: isDesktopScreen + ? null + : BoxDecoration( + border: Border( + bottom: BorderSide( + color: theme.weakDividerColor ?? + CommonColor.weakDividerColor))), child: InkWell( onTap: () async { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => GroupProfileMemberListPage( - model: model, memberList: memberList), - )); + navigateToMemberList(context, model, memberList); }, child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text(TIM_t("群成员"), - style: - TextStyle(color: theme.darkTextColor, fontSize: 16)), + style: TextStyle( + color: theme.darkTextColor, + fontSize: isDesktopScreen ? 14 : 16)), Row( children: [ Text( TIM_t_para("{{option1}}人", "$option1人")( option1: option1), - style: - TextStyle(color: theme.darkTextColor, fontSize: 16), + style: TextStyle( + color: theme.darkTextColor, + fontSize: isDesktopScreen ? 14 : 16), ), Icon( Icons.keyboard_arrow_right, @@ -141,83 +181,151 @@ class GroupMemberTile extends TIMUIKitStatelessWidget { ), ), ), + if (isDesktopScreen) + InkWell( + onTap: () async { + navigateToMemberList(context, model, memberList); + }, + child: Container( + decoration: BoxDecoration( + border: Border.all( + width: 1, + color: theme.weakDividerColor ?? + CommonColor.weakDividerColor), + borderRadius: const BorderRadius.all(Radius.circular(4)), + ), + // height: 30, + padding: + const EdgeInsets.symmetric(horizontal: 16, vertical: 6), + child: Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Icon( + Icons.search, + color: hexToColor("979797"), + size: 16, + ), + const SizedBox(width: 6), + Text(TIM_t("搜索"), + style: TextStyle( + color: theme.weakTextColor, + fontSize: 12, + )), + ], + ), + ), + ), Container( // height: 90, padding: const EdgeInsets.only(top: 12), child: Wrap( - spacing: 20, + spacing: isDesktopScreen ? 10 : 20, runSpacing: 10, alignment: WrapAlignment.start, children: [ - ..._groupMemberListBuilder(memberList, theme, model), + ..._groupMemberListBuilder(memberList, theme, model, showRange), if (isCanInviteMember) DottedBorder( borderType: BorderType.RRect, - radius: const Radius.circular(4.5), + radius: Radius.circular(isDesktopScreen ? 18 : 4.5), color: theme.weakTextColor!, dashPattern: const [6, 3], child: SizedBox( - width: 48, - height: 48, + width: isDesktopScreen ? 32 : 48, + height: isDesktopScreen ? 32 : 48, child: IconButton( onPressed: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => - AddGroupMemberPage(model: model), - )); + if (isDesktopScreen) { + TUIKitWidePopup.showPopupWindow( + context: context, + operationKey: TUIKitWideModalOperationKey.addGroupMembers, + width: 350, + title: TIM_t("添加群成员"), + height: 460, + onSubmit: () { + addGroupMemberKey.currentState?.submitAdd(); + }, + child: (onClose) => AddGroupMemberPage( + model: model, + key: addGroupMemberKey, + )); + } else { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => AddGroupMemberPage( + model: model, + ), + )); + } }, - icon: const Icon(Icons.add), + icon: Icon( + Icons.add, + size: isDesktopScreen ? 16 : 18, + ), color: theme.weakTextColor, ), )), - // if (isCanInviteMember) - // const SizedBox( - // width: 21, - // ), if (isCanKickOffMember) DottedBorder( borderType: BorderType.RRect, - radius: const Radius.circular(4.5), + radius: Radius.circular(isDesktopScreen ? 18 : 4.5), color: theme.weakTextColor!, dashPattern: const [6, 3], child: SizedBox( - width: 48, - height: 48, + width: isDesktopScreen ? 32 : 48, + height: isDesktopScreen ? 32 : 48, child: IconButton( onPressed: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => - DeleteGroupMemberPage(model: model), - )); + if (isDesktopScreen) { + TUIKitWidePopup.showPopupWindow( + operationKey: TUIKitWideModalOperationKey.kickOffGroupMembers, + context: context, + width: 350, + title: TIM_t("删除群成员"), + height: 460, + onSubmit: () { + deleteGroupMemberKey.currentState + ?.submitDelete(); + }, + child: (onClose) => DeleteGroupMemberPage( + model: model, + key: deleteGroupMemberKey, + ), + ); + } else { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => + DeleteGroupMemberPage(model: model), + )); + } }, - icon: const Icon(Icons.remove), + icon: Icon( + Icons.remove, + size: isDesktopScreen ? 16 : 18, + ), color: theme.weakTextColor, ), )), ], ), ), - if (memberList.length > 8) - GestureDetector( + if (memberList.length > showRange) + InkWell( child: Container( alignment: Alignment.center, - margin: const EdgeInsets.only(top: 16), + margin: EdgeInsets.only(top: isDesktopScreen ? 12 : 16), child: Text( TIM_t("查看更多群成员"), - style: TextStyle(color: theme.weakTextColor, fontSize: 14), + style: TextStyle( + color: theme.weakTextColor, + fontSize: isDesktopScreen ? 12 : 14), ), ), onTap: () async { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => GroupProfileMemberListPage( - model: model, memberList: memberList), - )); + navigateToMemberList(context, model, memberList); }, ), ], diff --git a/lib/ui/views/TIMUIKitGroupProfile/widgets/tim_uikit_group_message_disturb.dart b/lib/ui/views/TIMUIKitGroupProfile/widgets/tim_uikit_group_message_disturb.dart index 41361f1..91cf421 100644 --- a/lib/ui/views/TIMUIKitGroupProfile/widgets/tim_uikit_group_message_disturb.dart +++ b/lib/ui/views/TIMUIKitGroupProfile/widgets/tim_uikit_group_message_disturb.dart @@ -15,8 +15,10 @@ class GroupMessageDisturb extends TIMUIKitStatelessWidget { final model = Provider.of(context); final isDisturb = model.conversation?.recvOpt != 0; return TIMUIKitOperationItem( + isEmpty: false, operationName: TIM_t("消息免打扰"), type: "switch", + isUseCheckedBoxOnWide: true, operationValue: isDisturb, onSwitchChange: (value) { model.setMessageDisturb(value); diff --git a/lib/ui/views/TIMUIKitGroupProfile/widgets/tim_uikit_group_name_card.dart b/lib/ui/views/TIMUIKitGroupProfile/widgets/tim_uikit_group_name_card.dart index de39696..cf6c39d 100644 --- a/lib/ui/views/TIMUIKitGroupProfile/widgets/tim_uikit_group_name_card.dart +++ b/lib/ui/views/TIMUIKitGroupProfile/widgets/tim_uikit_group_name_card.dart @@ -2,63 +2,141 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; -import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_statelesswidget.dart'; +import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_state.dart'; import 'package:tencent_cloud_chat_uikit/business_logic/separate_models/tui_group_profile_model.dart'; +import 'package:tencent_cloud_chat_uikit/ui/utils/screen_utils.dart'; import 'package:tencent_cloud_chat_uikit/ui/widgets/text_input_bottom_sheet.dart'; - import 'package:tencent_im_base/tencent_im_base.dart'; import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_base.dart'; -class GroupProfileNameCard extends TIMUIKitStatelessWidget { - GroupProfileNameCard({Key? key}) : super(key: key); +class GroupProfileNameCard extends StatefulWidget { + const GroupProfileNameCard({Key? key}) : super(key: key); + + @override + State createState() => GroupProfileNameCardState(); + +} + +class GroupProfileNameCardState extends TIMUIKitState{ final TextEditingController controller = TextEditingController(); + String? nameCard; @override Widget tuiBuild(BuildContext context, TUIKitBuildValue value) { final TUITheme theme = value.theme; - + final isDesktopScreen = + TUIKitScreenUtils.getFormFactor(context) == DeviceType.Desktop; final model = Provider.of(context); if (model == null) { return Container(); } - final nameCard = model.getSelfNameCard(); + nameCard = model.getSelfNameCard(); + controller.text = nameCard ?? ""; - controller.text = nameCard; return Container( - padding: const EdgeInsets.only(top: 12, left: 16, bottom: 12), + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), decoration: BoxDecoration( color: Colors.white, - border: Border( + border: isDesktopScreen ? null : Border( bottom: BorderSide( color: - theme.weakDividerColor ?? CommonColor.weakDividerColor))), - child: InkWell( + theme.weakDividerColor ?? CommonColor.weakDividerColor))), + child: GestureDetector( onTap: () async { - TextInputBottomSheet.showTextInputBottomSheet( - context, TIM_t("修改我的群昵称"), TIM_t("仅限中文、字母、数字和下划线,2-20个字"), - (String nameCard) async { - final text = nameCard.trim(); - model.setNameCard(text); - }, theme); + if (!isDesktopScreen) { + TextInputBottomSheet.showTextInputBottomSheet( + context: context, + title: TIM_t("修改我的群昵称"), + tips: TIM_t("仅限中文、字母、数字和下划线,2-20个字"), + onSubmitted: (String nameCard) async { + final text = nameCard.trim(); + model.setNameCard(text); + }, + theme: theme); + } }, - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - TIM_t("我的群昵称"), - style: TextStyle(fontSize: 16, color: theme.darkTextColor), - ), Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Text( - nameCard, - style: const TextStyle(fontSize: 16, color: Colors.black), + Container( + margin: const EdgeInsets.only(right: 10), + child: Text( + TIM_t("我的群昵称"), + style: TextStyle( + fontSize: isDesktopScreen ? 14 : 16, + color: theme.darkTextColor), + ), ), - Icon(Icons.keyboard_arrow_right, color: theme.weakTextColor) + if (!isDesktopScreen) + Expanded( + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Expanded( + child: Text( + nameCard ?? "", + style: TextStyle( + fontSize: isDesktopScreen ? 14 : 16, + color: theme.darkTextColor), + )), + Icon(Icons.keyboard_arrow_right, + color: theme.weakTextColor) + ], + )), ], - ) + ), + if (isDesktopScreen) + Text( + TIM_t("仅限中文、字母、数字和下划线,2-20个字"), + style: TextStyle(color: theme.weakTextColor, fontSize: 12), + ), + if (isDesktopScreen) + Container( + margin: const EdgeInsets.symmetric(vertical: 10), + height: 30, + child: TextField( + minLines: 1, + controller: controller, + maxLines: 1, + onSubmitted: (text) { + model.setNameCard(text.trim()); + }, + keyboardType: TextInputType.multiline, + autofocus: true, + textAlignVertical: TextAlignVertical.center, + textAlign: TextAlign.start, + style: const TextStyle(fontSize: 12), + decoration: InputDecoration( + contentPadding: const EdgeInsets.symmetric( + horizontal: 10, vertical: 10), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(5.0), + borderSide: BorderSide( + color: theme.weakDividerColor ?? Colors.grey, + )), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(5.0), + borderSide: BorderSide( + color: theme.weakDividerColor ?? Colors.grey, + ), + ), + focusedBorder: OutlineInputBorder( + //选中时外边框颜色 + borderRadius: BorderRadius.circular(5.0), + borderSide: BorderSide( + color: theme.weakTextColor ?? Colors.grey, + ), + ), + hintStyle: const TextStyle( + color: Color(0xFFAEA4A3), + ), + hintText: TIM_t("修改我的群昵称"))), + ), ], ), ), diff --git a/lib/ui/views/TIMUIKitGroupProfile/widgets/tim_uikit_group_notification.dart b/lib/ui/views/TIMUIKitGroupProfile/widgets/tim_uikit_group_notification.dart index a45c2e8..19cda17 100644 --- a/lib/ui/views/TIMUIKitGroupProfile/widgets/tim_uikit_group_notification.dart +++ b/lib/ui/views/TIMUIKitGroupProfile/widgets/tim_uikit_group_notification.dart @@ -1,63 +1,161 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_state.dart'; +import 'package:tencent_cloud_chat_uikit/ui/utils/screen_utils.dart'; import 'package:tencent_im_base/tencent_im_base.dart'; -import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_statelesswidget.dart'; import 'package:tencent_cloud_chat_uikit/business_logic/separate_models/tui_group_profile_model.dart'; - - import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_base.dart'; -class GroupProfileNotification extends TIMUIKitStatelessWidget { - GroupProfileNotification({Key? key}) : super(key: key); +class GroupProfileNotification extends StatefulWidget { + final bool isHavePermission; + + const GroupProfileNotification({Key? key, this.isHavePermission = false}) + : super(key: key); + + @override + State createState() => GroupProfileNotificationState(); +} + +class GroupProfileNotificationState + extends TIMUIKitState { + bool isShowEditBox = false; + final TextEditingController _controller = TextEditingController(); + @override Widget tuiBuild(BuildContext context, TUIKitBuildValue value) { final TUITheme theme = value.theme; + final isDesktopScreen = + TUIKitScreenUtils.getFormFactor(context) == DeviceType.Desktop; final model = Provider.of(context); final String notification = (model.groupInfo?.notification != null && model.groupInfo!.notification!.isNotEmpty) ? model.groupInfo!.notification! : TIM_t("暂无群公告"); + + _setGroupNotification() async { + setState(() { + isShowEditBox = false; + }); + final notification = _controller.text; + await model.setGroupNotification(notification); + } + return Container( - padding: const EdgeInsets.only(top: 12, left: 16, bottom: 12), + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), decoration: BoxDecoration( color: Colors.white, - border: Border( - bottom: BorderSide( - color: - theme.weakDividerColor ?? CommonColor.weakDividerColor))), + border: isDesktopScreen + ? null + : Border( + bottom: BorderSide( + color: theme.weakDividerColor ?? + CommonColor.weakDividerColor))), child: InkWell( - onTap: (() { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => GroupProfileNotificationPage( - model: model, notification: notification))); - }), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - crossAxisAlignment: CrossAxisAlignment.center, + onTap: !widget.isHavePermission + ? null + : (() { + final isDesktopScreen = + TUIKitScreenUtils.getFormFactor(context) == DeviceType.Desktop; + if (!isDesktopScreen) { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => GroupProfileNotificationPage( + model: model, notification: notification))); + } else { + setState(() { + isShowEditBox = !isShowEditBox; + if (isShowEditBox) { + _controller.text = notification; + } + }); + } + }), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Expanded( + child: Text( TIM_t("群公告"), - style: TextStyle(color: theme.darkTextColor, fontSize: 16), + style: TextStyle( + color: theme.darkTextColor, + fontSize: isDesktopScreen ? 14 : 16), ), - Text(notification, - overflow: TextOverflow.ellipsis, - softWrap: true, - style: - TextStyle(color: theme.weakTextColor, fontSize: 12)), - ], - ), + ), + if (widget.isHavePermission) + AnimatedRotation( + turns: isShowEditBox ? 0.25 : 0, + duration: const Duration(milliseconds: 200), + child: Icon(Icons.keyboard_arrow_right, + color: theme.weakTextColor), + ) + ], ), - Icon(Icons.keyboard_arrow_right, color: theme.weakTextColor) + if (!isShowEditBox) + Padding( + padding: EdgeInsets.only(top: isDesktopScreen ? 4 : 0), + child: SelectableText(notification, + // overflow: isDesktopScreen ? null : TextOverflow.ellipsis, + // softWrap: true, + style: TextStyle(color: theme.weakTextColor, fontSize: 12)), + ), + if (isShowEditBox) + Container( + margin: const EdgeInsets.only(top: 10, bottom: 10), + // height: 150, + child: TextField( + minLines: 1, + maxLines: 6, + controller: _controller, + keyboardType: TextInputType.multiline, + autofocus: true, + style: const TextStyle(fontSize: 13), + decoration: InputDecoration( + contentPadding: const EdgeInsets.symmetric( + horizontal: 10, vertical: 10), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(5.0), + borderSide: BorderSide( + color: theme.weakDividerColor ?? Colors.grey, + )), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(5.0), + borderSide: BorderSide( + color: theme.weakDividerColor ?? Colors.grey, + ), + ), + focusedBorder: OutlineInputBorder( + //选中时外边框颜色 + borderRadius: BorderRadius.circular(5.0), + borderSide: BorderSide( + color: theme.weakTextColor ?? Colors.grey, + ), + ), + hintStyle: const TextStyle( + color: Color(0xFFAEA4A3), + ), + hintText: '')), + ), + if (isShowEditBox) + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + OutlinedButton( + onPressed: _setGroupNotification, + child: Text( + TIM_t("保存"), + style: + TextStyle(fontSize: 13, color: theme.primaryColor), + )) + ], + ) ], ), ), @@ -104,19 +202,13 @@ class _GroupProfileNotificationPageState appBar: AppBar( title: Text( TIM_t("群公告"), - style: const TextStyle(color: Colors.white, fontSize: 17), + style: TextStyle(color: theme.appbarTextColor, fontSize: 17), ), + backgroundColor: theme.appbarBgColor ?? + theme.primaryColor, shadowColor: theme.weakDividerColor, - flexibleSpace: Container( - decoration: BoxDecoration( - gradient: LinearGradient(colors: [ - theme.lightPrimaryColor ?? CommonColor.lightPrimaryColor, - theme.primaryColor ?? CommonColor.primaryColor - ]), - ), - ), - iconTheme: const IconThemeData( - color: Colors.white, + iconTheme: IconThemeData( + color: theme.appbarTextColor, ), actions: [ TextButton( @@ -131,9 +223,9 @@ class _GroupProfileNotificationPageState }, child: Text( isUpdated ? TIM_t("编辑") : TIM_t("完成"), - style: const TextStyle( - color: Colors.white, - fontSize: 16, + style: TextStyle( + color: theme.appbarTextColor, + fontSize: 14, ), ), ) diff --git a/lib/ui/views/TIMUIKitGroupProfile/widgets/tim_uikit_group_pin_conversation.dart b/lib/ui/views/TIMUIKitGroupProfile/widgets/tim_uikit_group_pin_conversation.dart index e3605d9..eaf29a2 100644 --- a/lib/ui/views/TIMUIKitGroupProfile/widgets/tim_uikit_group_pin_conversation.dart +++ b/lib/ui/views/TIMUIKitGroupProfile/widgets/tim_uikit_group_pin_conversation.dart @@ -15,8 +15,10 @@ class GroupPinConversation extends TIMUIKitStatelessWidget { final model = Provider.of(context); final isPined = model.conversation?.isPinned ?? false; return TIMUIKitOperationItem( + isEmpty: false, operationName: TIM_t("置顶聊天"), type: "switch", + isUseCheckedBoxOnWide: true, operationValue: isPined, onSwitchChange: (value) { model.pinedConversation(value); diff --git a/lib/ui/views/TIMUIKitGroupProfile/widgets/tim_uikit_group_type.dart b/lib/ui/views/TIMUIKitGroupProfile/widgets/tim_uikit_group_type.dart index 0b24447..1f1284d 100644 --- a/lib/ui/views/TIMUIKitGroupProfile/widgets/tim_uikit_group_type.dart +++ b/lib/ui/views/TIMUIKitGroupProfile/widgets/tim_uikit_group_type.dart @@ -3,9 +3,8 @@ import 'package:provider/provider.dart'; import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_statelesswidget.dart'; import 'package:tencent_cloud_chat_uikit/business_logic/separate_models/tui_group_profile_model.dart'; - - import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_base.dart'; +import 'package:tencent_cloud_chat_uikit/ui/utils/screen_utils.dart'; import 'package:tencent_im_base/tencent_im_base.dart'; class GroupProfileType extends TIMUIKitStatelessWidget { @@ -17,6 +16,9 @@ class GroupProfileType extends TIMUIKitStatelessWidget { String groupType; final model = Provider.of(context); + final isDesktopScreen = + TUIKitScreenUtils.getFormFactor(context) == DeviceType.Desktop; + final type = model.groupInfo?.groupType; switch (type) { case GroupType.AVChatRoom: @@ -40,7 +42,7 @@ class GroupProfileType extends TIMUIKitStatelessWidget { padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 16), decoration: BoxDecoration( color: Colors.white, - border: Border( + border: isDesktopScreen ? null : Border( bottom: BorderSide( color: theme.weakDividerColor ?? CommonColor.weakDividerColor))), @@ -49,11 +51,13 @@ class GroupProfileType extends TIMUIKitStatelessWidget { children: [ Text( TIM_t("群类型"), - style: TextStyle(fontSize: 16, color: theme.darkTextColor), + style: TextStyle( + fontSize: isDesktopScreen ? 14 : 16, color: theme.darkTextColor), ), Text( groupType, - style: const TextStyle(fontSize: 16, color: Colors.black), + style: TextStyle( + fontSize: isDesktopScreen ? 14 : 16, color: theme.weakTextColor), ) ], ), diff --git a/lib/ui/views/TIMUIKitNewContact/tim_uikit_new_contact.dart b/lib/ui/views/TIMUIKitNewContact/tim_uikit_new_contact.dart index cfc071c..b59f2c0 100644 --- a/lib/ui/views/TIMUIKitNewContact/tim_uikit_new_contact.dart +++ b/lib/ui/views/TIMUIKitNewContact/tim_uikit_new_contact.dart @@ -4,7 +4,8 @@ import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_state.dart'; import 'package:tencent_cloud_chat_uikit/business_logic/life_cycle/new_contact_life_cycle.dart'; import 'package:tencent_cloud_chat_uikit/business_logic/view_models/tui_friendship_view_model.dart'; import 'package:tencent_cloud_chat_uikit/data_services/services_locatar.dart'; -import 'package:tencent_im_base/tencent_im_base.dart'; +import 'package:tencent_cloud_chat_uikit/tencent_cloud_chat_uikit.dart'; +import 'package:tencent_cloud_chat_uikit/ui/utils/screen_utils.dart'; import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_base.dart'; import 'package:tencent_cloud_chat_uikit/ui/widgets/avatar.dart'; @@ -44,9 +45,8 @@ class _TIMUIKitNewContactState extends TIMUIKitState { late TUIFriendShipViewModel model = serviceLocator(); _getShowName(V2TimFriendApplication item) { - final nickName = item.nickname ?? ""; - final userID = item.userID; - return nickName != "" ? nickName : userID; + return TencentUtils.checkString(item.nickname) ?? + TencentUtils.checkString(item.userID); } Widget _itemBuilder( @@ -54,104 +54,143 @@ class _TIMUIKitNewContactState extends TIMUIKitState { final theme = Provider.of(context).theme; final showName = _getShowName(applicationInfo); final faceUrl = applicationInfo.faceUrl ?? ""; - return Container( - padding: const EdgeInsets.only(top: 10, left: 16), - child: Row( - children: [ - Container( - padding: const EdgeInsets.only(bottom: 12), - margin: const EdgeInsets.only(right: 12), - child: SizedBox( - height: 40, - width: 40, - child: Avatar(faceUrl: faceUrl, showName: showName), - ), - ), - Expanded( - child: Container( - padding: const EdgeInsets.only(top: 10, bottom: 20), - decoration: BoxDecoration( - border: Border( - bottom: BorderSide( - color: theme.weakDividerColor ?? - CommonColor.weakDividerColor))), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - showName, - style: const TextStyle(color: Colors.black, fontSize: 18), + final applicationText = applicationInfo.addWording ?? ""; + final isDesktopScreen = + TUIKitScreenUtils.getFormFactor(context) == DeviceType.Desktop; + + return Material( + color: theme.wideBackgroundColor, + child: InkWell( + onTap: () {}, + child: Container( + padding: EdgeInsets.only( + top: isDesktopScreen ? 6 : 10, + left: 16, + right: isDesktopScreen ? 16 : 0), + child: Row( + children: [ + Container( + padding: EdgeInsets.only(bottom: isDesktopScreen ? 10 : 12), + margin: const EdgeInsets.only(right: 12), + child: SizedBox( + height: isDesktopScreen ? 30 : 40, + width: isDesktopScreen ? 30 : 40, + child: Avatar(faceUrl: faceUrl, showName: showName), ), - Expanded(child: Container()), - Container( - margin: const EdgeInsets.only(right: 8), - child: InkWell( - child: Container( - padding: const EdgeInsets.symmetric( - horizontal: 14, vertical: 6), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(8), - color: theme.primaryColor, - border: Border.all( - width: 1, - color: theme.weakTextColor ?? - CommonColor.weakTextColor)), - child: Text( - TIM_t("同意"), - style: const TextStyle( - color: Colors.white, - ), + ), + Expanded( + child: Container( + padding: const EdgeInsets.only(top: 10, bottom: 20), + decoration: BoxDecoration( + border: Border( + bottom: BorderSide( + color: theme.weakDividerColor ?? + CommonColor.weakDividerColor))), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Padding( + padding: EdgeInsets.only( + top: (applicationText.isNotEmpty && isDesktopScreen) + ? 10 + : 0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + showName, + style: TextStyle( + color: theme.darkTextColor, + fontSize: isDesktopScreen ? 14 : 18), + ), + if (applicationText.isNotEmpty && isDesktopScreen) + const SizedBox( + height: 4, + ), + if (applicationText.isNotEmpty && isDesktopScreen) + Text( + applicationText, + style: TextStyle( + color: theme.weakTextColor, fontSize: 12), + ), + ], ), ), - onTap: () async { - await model.acceptFriendApplication( - applicationInfo.userID, - applicationInfo.type, - ); - model.loadData(); - if (widget.onAccept != null) { - widget.onAccept!(applicationInfo); - } - // widget?.onAccept(); - }, - ), - ), - Container( - margin: const EdgeInsets.only(right: 8), - child: InkWell( - child: Container( - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(8), - color: Colors.white, - border: Border.all( - width: 1, - color: theme.weakTextColor ?? - CommonColor.weakTextColor)), - padding: const EdgeInsets.symmetric( - horizontal: 14, vertical: 6), - child: Text( - TIM_t("拒绝"), - style: TextStyle( - color: theme.primaryColor, + Expanded(child: Container()), + Container( + margin: const EdgeInsets.only(right: 8), + child: InkWell( + child: Container( + padding: const EdgeInsets.symmetric( + horizontal: 14, vertical: 6), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8), + color: theme.primaryColor, + border: Border.all( + width: 1, + color: theme.weakTextColor ?? + CommonColor.weakTextColor)), + child: Text( + TIM_t("同意"), + style: TextStyle( + color: Colors.white, + fontSize: isDesktopScreen ? 12 : null, + ), ), ), + onTap: () async { + await model.acceptFriendApplication( + applicationInfo.userID, + applicationInfo.type, + ); + model.loadData(); + if (widget.onAccept != null) { + widget.onAccept!(applicationInfo); + } + // widget?.onAccept(); + }, ), - onTap: () async { - await model.refuseFriendApplication( - applicationInfo.userID, - applicationInfo.type, - ); - model.loadData(); - if (widget.onRefuse != null) { - widget.onRefuse!(applicationInfo); - } - // refuse(context); - }, - )) - ], - ), - )) - ], + ), + Container( + margin: const EdgeInsets.only(right: 8), + child: InkWell( + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8), + color: Colors.white, + border: Border.all( + width: 1, + color: theme.weakTextColor ?? + CommonColor.weakTextColor)), + padding: const EdgeInsets.symmetric( + horizontal: 14, vertical: 6), + child: Text( + TIM_t("拒绝"), + style: TextStyle( + color: theme.primaryColor, + fontSize: isDesktopScreen ? 12 : null, + ), + ), + ), + onTap: () async { + await model.refuseFriendApplication( + applicationInfo.userID, + applicationInfo.type, + ); + model.loadData(); + if (widget.onRefuse != null) { + widget.onRefuse!(applicationInfo); + } + // refuse(context); + }, + )) + ], + ), + )) + ], + ), + ), ), ); } @@ -160,11 +199,6 @@ class _TIMUIKitNewContactState extends TIMUIKitState { return widget.itemBuilder ?? _itemBuilder; } - @override - void initState() { - super.initState(); - } - @override Widget tuiBuild(BuildContext context, TUIKitBuildValue value) { return MultiProvider( diff --git a/lib/ui/views/TIMUIKitNewContact/tim_uikit_unread_count.dart b/lib/ui/views/TIMUIKitNewContact/tim_uikit_unread_count.dart index 177bbea..230fbd5 100644 --- a/lib/ui/views/TIMUIKitNewContact/tim_uikit_unread_count.dart +++ b/lib/ui/views/TIMUIKitNewContact/tim_uikit_unread_count.dart @@ -20,10 +20,6 @@ class TIMUIKitUnreadCount extends StatefulWidget { class _TIMUIKitUnreadCountState extends TIMUIKitState { final TUIFriendShipViewModel model = serviceLocator(); - @override - void initState() { - super.initState(); - } @override void dispose() { diff --git a/lib/ui/views/TIMUIKitProfile/tim_uikit_profile.dart b/lib/ui/views/TIMUIKitProfile/tim_uikit_profile.dart index e8d788e..37a65cf 100644 --- a/lib/ui/views/TIMUIKitProfile/tim_uikit_profile.dart +++ b/lib/ui/views/TIMUIKitProfile/tim_uikit_profile.dart @@ -8,6 +8,7 @@ import 'package:tencent_cloud_chat_uikit/business_logic/view_models/tui_self_inf import 'package:tencent_cloud_chat_uikit/data_services/services_locatar.dart'; import 'package:tencent_cloud_chat_uikit/tencent_cloud_chat_uikit.dart'; import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_base.dart'; +import 'package:tencent_cloud_chat_uikit/ui/utils/screen_utils.dart'; import 'package:tencent_cloud_chat_uikit/ui/views/TIMUIKitProfile/profile_widget.dart'; import 'package:tencent_cloud_chat_uikit/ui/views/TIMUIKitProfile/widget/tim_uikit_profile_widget.dart'; @@ -79,6 +80,9 @@ class TIMUIKitProfile extends StatefulWidget { /// Default: [false]. final bool isSelf; + /// Is use the small card mode on Desktop. Usually shows on the Chat page. + final bool smallCardMode; + const TIMUIKitProfile( {Key? key, required this.userID, @@ -97,7 +101,8 @@ class TIMUIKitProfile extends StatefulWidget { this.profileWidgetsOrder, this.builder, this.isSelf = false, - this.lifeCycle}) + this.lifeCycle, + this.smallCardMode = false}) : super(key: key); @override @@ -125,8 +130,10 @@ class _TIMUIKitProfileState extends TIMUIKitState { @override void didUpdateWidget(covariant TIMUIKitProfile oldWidget) { super.didUpdateWidget(oldWidget); - if(oldWidget.userID != widget.userID){ - _model.loadData(userID: widget.userID, isNeedConversation: !widget.isSelf); + if (oldWidget.userID != widget.userID) { + _model.userProfile = null; + _model.loadData( + userID: widget.userID, isNeedConversation: !widget.isSelf); } } @@ -145,6 +152,8 @@ class _TIMUIKitProfileState extends TIMUIKitState { @override Widget tuiBuild(BuildContext context, TUIKitBuildValue value) { final theme = value.theme; + final isDesktopScreen = + TUIKitScreenUtils.getFormFactor(context) == DeviceType.Desktop; return ChangeNotifierProvider.value( value: _model, child: Consumer( @@ -164,7 +173,16 @@ class _TIMUIKitProfileState extends TIMUIKitState { } final conversation = model.userProfile?.conversation ?? - V2TimConversation(conversationID: "c2c_${widget.userID}"); + V2TimConversation( + conversationID: "c2c_${widget.userID}", + type: 1, + userID: widget.userID, + faceUrl: model.userProfile?.friendInfo?.userProfile?.faceUrl, + showName: TencentUtils.checkString( + model.userProfile?.friendInfo?.friendRemark) ?? + TencentUtils.checkString(model + .userProfile?.friendInfo?.userProfile?.nickName) ?? + widget.userID); final TUISelfInfoViewModel _selfInfoViewModel = serviceLocator(); @@ -172,12 +190,16 @@ class _TIMUIKitProfileState extends TIMUIKitState { final isSelf = (model.userProfile?.friendInfo?.userID == _selfInfoViewModel.loginInfo?.userID); final isMute = model.isDisturb ?? false; + Widget profilePage({required Widget child}) { - return SingleChildScrollView( - physics: const BouncingScrollPhysics( - parent: AlwaysScrollableScrollPhysics()), - child: Container( - child: child, + return Container( + color: isDesktopScreen ? theme.wideBackgroundColor : null, + child: SingleChildScrollView( + physics: const BouncingScrollPhysics( + parent: AlwaysScrollableScrollPhysics()), + child: Container( + child: child, + ), ), ); } @@ -194,16 +216,21 @@ class _TIMUIKitProfileState extends TIMUIKitState { model.setMessageDisturb(userInfo.userID, value); } - void handleTapRemarkBar() { + void handleTapRemarkBar({Offset? offset, String? initText}) { _controller.showTextInputBottomSheet( - context, TIM_t("修改备注名"), TIM_t("仅限汉字、英文、数字和下划线"), - (String remark) async { - final res = - await _controller.updateRemarks(widget.userID, remark); - if (res.code == 0) { - widget.lifeCycle?.didRemarkUpdated(remark); - } - }, theme); + initOffset: offset, + initText: initText, + context: context, + title: TIM_t("修改备注名"), + tips: TIM_t("仅限汉字、英文、数字和下划线"), + onSubmitted: (String remark) async { + final res = + await _controller.updateRemarks(widget.userID, remark); + if (res.code == 0) { + widget.lifeCycle?.didRemarkUpdated(remark); + } + }, + theme: theme); } void handleAddFriend() async { @@ -275,7 +302,7 @@ class _TIMUIKitProfileState extends TIMUIKitState { model.isAddToBlackList ?? false, context, handleAddToBlockList, - ))!; + widget.smallCardMode))!; case ProfileWidgetEnum.pinConversationBar: // if (!isFriend) { // return Container(); @@ -286,15 +313,16 @@ class _TIMUIKitProfileState extends TIMUIKitState { : TIMUIKitProfileWidget.pinConversationBar( conversation.isPinned ?? false, context, - handlePinConversation))!; + handlePinConversation, + widget.smallCardMode))!; case ProfileWidgetEnum.messageMute: // if (!isFriend) { // return Container(); // } return (customBuilder?.messageMute != null ? customBuilder?.messageMute!(isMute, handleMuteMessage) - : TIMUIKitProfileWidget.messageDisturb( - context, isMute, handleMuteMessage))!; + : TIMUIKitProfileWidget.messageDisturb(context, isMute, + handleMuteMessage, widget.smallCardMode))!; case ProfileWidgetEnum.searchBar: return (customBuilder?.searchBar != null ? customBuilder?.searchBar!(conversation) @@ -305,37 +333,43 @@ class _TIMUIKitProfileState extends TIMUIKitState { ? customBuilder?.portraitBar!(userInfo.userProfile) : TIMUIKitProfileWidget.portraitBar( TIMUIKitProfileWidget.defaultPortraitWidget( - userInfo.userProfile)))!; + userInfo.userProfile, widget.smallCardMode), + widget.smallCardMode))!; case ProfileWidgetEnum.nicknameBar: return (customBuilder?.nicknameBar != null ? customBuilder ?.nicknameBar!(userInfo.userProfile?.nickName ?? "") : TIMUIKitProfileWidget.nicknameBar( - userInfo.userProfile?.nickName ?? ""))!; + userInfo.userProfile?.nickName ?? "", + widget.smallCardMode))!; case ProfileWidgetEnum.userAccountBar: return (customBuilder?.userAccountBar != null ? customBuilder ?.userAccountBar!(userInfo.userProfile?.userID ?? "") : TIMUIKitProfileWidget.userAccountBar( - userInfo.userProfile?.userID ?? ""))!; + userInfo.userProfile?.userID ?? "", + widget.smallCardMode))!; case ProfileWidgetEnum.signatureBar: return (customBuilder?.signatureBar != null ? customBuilder?.signatureBar!( userInfo.userProfile?.selfSignature ?? "") : TIMUIKitProfileWidget.signatureBar( - userInfo.userProfile?.selfSignature ?? ""))!; + userInfo.userProfile?.selfSignature ?? "", + widget.smallCardMode))!; case ProfileWidgetEnum.genderBar: return (customBuilder?.genderBar != null ? customBuilder ?.genderBar!(userInfo.userProfile?.gender ?? 0) : TIMUIKitProfileWidget.genderBar( - userInfo.userProfile?.gender ?? 0))!; + userInfo.userProfile?.gender ?? 0, + widget.smallCardMode))!; case ProfileWidgetEnum.birthdayBar: return (customBuilder?.birthdayBar != null ? customBuilder ?.birthdayBar!(userInfo.userProfile?.birthday) : TIMUIKitProfileWidget.birthdayBar( - userInfo.userProfile?.birthday))!; + userInfo.userProfile?.birthday, + widget.smallCardMode))!; case ProfileWidgetEnum.addAndDeleteArea: if (isSelf) { return Container(); @@ -347,19 +381,37 @@ class _TIMUIKitProfileState extends TIMUIKitState { value.friendType, isMute, ) - : TIMUIKitProfileWidget.addAndDeleteArea( - userInfo, - conversation, - value.friendType, - isMute, - model.isAddToBlackList ?? false, - theme, - handleAddFriend, - handleDeleteFriend))!; + : isDesktopScreen + ? TIMUIKitProfileWidget.addAndDeleteAreaWide( + userInfo, + conversation, + value.friendType, + isMute, + model.isAddToBlackList ?? false, + theme, + handleAddFriend, + handleDeleteFriend, + widget.smallCardMode) + : TIMUIKitProfileWidget.addAndDeleteArea( + userInfo, + conversation, + value.friendType, + isMute, + model.isAddToBlackList ?? false, + theme, + handleAddFriend, + handleDeleteFriend, + widget.smallCardMode))!; case ProfileWidgetEnum.operationDivider: return (customBuilder?.operationDivider != null ? customBuilder?.operationDivider!() - : TIMUIKitProfileWidget.operationDivider())!; + : TIMUIKitProfileWidget.operationDivider( + color: theme.weakDividerColor, + height: isDesktopScreen ? 1 : 10, + margin: isDesktopScreen + ? EdgeInsets.symmetric( + vertical: widget.smallCardMode ? 4 : 20) + : null))!; case ProfileWidgetEnum.remarkBar: if (!isFriend) { return Container(); @@ -368,7 +420,10 @@ class _TIMUIKitProfileState extends TIMUIKitState { ? customBuilder?.remarkBar!( userInfo.friendRemark ?? "", handleTapRemarkBar) : TIMUIKitProfileWidget.remarkBar( - userInfo.friendRemark ?? "", handleTapRemarkBar))!; + context, + userInfo.friendRemark ?? "", + handleTapRemarkBar, + widget.smallCardMode))!; case ProfileWidgetEnum.customBuilderOne: return (customBuilder?.customBuilderOne != null ? customBuilder?.customBuilderOne!( @@ -407,10 +462,8 @@ class _TIMUIKitProfileState extends TIMUIKitState { } if (widget.builder != null) { - return profilePage( - child: widget.builder!( - context, userInfo, conversation, value.friendType, isMute), - ); + return widget.builder!( + context, userInfo, conversation, value.friendType, isMute); } else if (widget.profileWidgetsOrder != null) { return profilePage( child: Column( diff --git a/lib/ui/views/TIMUIKitProfile/widget/tim_uikit_operation_item.dart b/lib/ui/views/TIMUIKitProfile/widget/tim_uikit_operation_item.dart index c3c932d..2f55c91 100644 --- a/lib/ui/views/TIMUIKitProfile/widget/tim_uikit_operation_item.dart +++ b/lib/ui/views/TIMUIKitProfile/widget/tim_uikit_operation_item.dart @@ -3,6 +3,7 @@ import 'package:flutter/material.dart'; import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_statelesswidget.dart'; import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_base.dart'; import 'package:tencent_cloud_chat_uikit/tencent_cloud_chat_uikit.dart'; +import 'package:tencent_cloud_chat_uikit/ui/utils/screen_utils.dart'; class TIMUIKitOperationItem extends TIMUIKitStatelessWidget { final String operationName; @@ -10,89 +11,195 @@ class TIMUIKitOperationItem extends TIMUIKitStatelessWidget { /// shows on the second line final String? operationDescription; final bool? operationValue; - final bool isRightIcon; - /// if allow to show arrow to right - final bool showArrowRightIcon; + /// Is show allow edit status, while a right icon shows on mobile, and `showAllowEditStatus` shows on desktop. + final bool showAllowEditStatus; + + /// Used on wide screen. + final String? wideEditText; + + /// Used on wide screen. + final bool isEmpty; /// the operationText widget for replacement, for developers to define what to do final Widget? operationRightWidget; final String type; final void Function(bool newValue)? onSwitchChange; + final Key? itemBoxKey; + final bool isUseCheckedBoxOnWide; + + /// Is use the small card mode on Desktop. Usually shows on the Chat page. + final bool smallCardMode; TIMUIKitOperationItem( {Key? key, + this.wideEditText, + this.itemBoxKey, this.operationDescription, + required this.isEmpty, required this.operationName, + this.smallCardMode = false, this.operationValue, this.type = "arrow", + this.isUseCheckedBoxOnWide = false, this.onSwitchChange, this.operationRightWidget, - this.showArrowRightIcon = true, - this.isRightIcon = true}) + this.showAllowEditStatus = true}) : super(key: key); @override Widget tuiBuild(BuildContext context, TUIKitBuildValue value) { final TUITheme theme = value.theme; + final isDesktopScreen = + TUIKitScreenUtils.getFormFactor(context) == DeviceType.Desktop; return Container( - padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 16), - margin: const EdgeInsets.only(bottom: 1), + padding: isDesktopScreen + ? EdgeInsets.symmetric( + horizontal: isUseCheckedBoxOnWide ? 6 : 16, + vertical: smallCardMode ? 0 : 4) + : const EdgeInsets.symmetric(vertical: 8, horizontal: 16), + margin: isDesktopScreen ? null : const EdgeInsets.only(bottom: 1), color: Colors.white, - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, + child: (isDesktopScreen && isUseCheckedBoxOnWide) + ? Row( children: [ - Text(operationName), - if (operationDescription != null) - Text( - operationDescription!, - style: - TextStyle(color: theme.weakTextColor, fontSize: 12), - ) - ], - ), - ), - type == "switch" - ? Transform.scale( + Transform.scale( scale: 0.8, - child: CupertinoSwitch( - value: operationValue ?? false, - onChanged: onSwitchChange, - activeColor: theme.primaryColor, - ), - ) - : Row( - mainAxisAlignment: MainAxisAlignment.end, + child: Checkbox( + fillColor: MaterialStateProperty.all(theme.primaryColor), + value: operationValue ?? false, + onChanged: (val) { + if (onSwitchChange != null) { + onSwitchChange!(val ?? false); + } + }), + ), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.center, children: [ - Transform.scale( + Text( + operationName, + style: const TextStyle(fontSize: 14), + ), + if (operationDescription != null) + Text( + operationDescription!, + style: + TextStyle(color: theme.weakTextColor, fontSize: 12), + ) + ], + )), + ], + ) + : Row( + mainAxisAlignment: isDesktopScreen + ? MainAxisAlignment.start + : MainAxisAlignment.spaceBetween, + children: [ + if (isDesktopScreen) + SizedBox( + width: 130, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + operationName, + style: TextStyle( + color: + isDesktopScreen ? hexToColor("7f7f7f") : null), + ), + if (operationDescription != null) + Text( + operationDescription!, + style: TextStyle( + color: theme.weakTextColor, fontSize: 12), + ) + ], + ), + ), + if (!isDesktopScreen) + Expanded( + child: SizedBox( + width: 130, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + operationName, + style: TextStyle( + color: + isDesktopScreen ? hexToColor("7f7f7f") : null), + ), + if (operationDescription != null) + Text( + operationDescription!, + style: TextStyle( + color: theme.weakTextColor, fontSize: 12), + ) + ], + ), + )), + if (type == "switch") + Transform.scale( + key: itemBoxKey, + scale: 0.8, + child: CupertinoSwitch( + value: operationValue ?? false, + onChanged: onSwitchChange, + activeColor: theme.primaryColor, + ), + ), + if (type != "switch" && !isDesktopScreen) + Transform.scale( + scale: 0, + child: CupertinoSwitch( + value: false, + onChanged: onSwitchChange, + ), + ), + (type != "switch" && + isDesktopScreen && + showAllowEditStatus && + isEmpty) + ? MouseRegion( + key: itemBoxKey, + child: Text( + wideEditText ?? TIM_t("编辑"), + style: TextStyle(color: theme.weakTextColor), + ), + ) + : Container( + width: 0, + ), + if (type != "switch") + Expanded( + child: Row( + mainAxisAlignment: isDesktopScreen + ? MainAxisAlignment.start + : MainAxisAlignment.end, + children: [Expanded(child: operationRightWidget ?? const Text(""))], + )), + (type != "switch" && !isDesktopScreen && showAllowEditStatus) + ? const Icon(Icons.keyboard_arrow_right) + : Container( + width: 0, + ), + if (type != "switch" && isDesktopScreen) + SizedBox( + width: 0, + child: Transform.scale( scale: 0, child: CupertinoSwitch( value: false, onChanged: onSwitchChange, ), ), - ConstrainedBox( - constraints: BoxConstraints( - maxWidth: MediaQuery.of(context).size.width / 1.6, - ), - child: operationRightWidget ?? const Text(""), - ), - isRightIcon - ? showArrowRightIcon - ? const Icon(Icons.keyboard_arrow_right) - : Container( - width: 10, - ) - : Container(), - ], - ), - ], - ), + ), + ], + ), ); } } diff --git a/lib/ui/views/TIMUIKitProfile/widget/tim_uikit_profile_userinfo_card/tim_uikit_profile_userinfo_card.dart b/lib/ui/views/TIMUIKitProfile/widget/tim_uikit_profile_userinfo_card/tim_uikit_profile_userinfo_card.dart new file mode 100644 index 0000000..6e40ca1 --- /dev/null +++ b/lib/ui/views/TIMUIKitProfile/widget/tim_uikit_profile_userinfo_card/tim_uikit_profile_userinfo_card.dart @@ -0,0 +1,50 @@ +// ignore_for_file: deprecated_member_use_from_same_package + +import 'package:flutter/cupertino.dart'; +import 'package:tencent_cloud_chat_uikit/tencent_cloud_chat_uikit.dart'; +import 'package:tencent_cloud_chat_uikit/ui/utils/screen_utils.dart'; +import 'package:tencent_cloud_chat_uikit/ui/views/TIMUIKitProfile/widget/tim_uikit_profile_userinfo_card/tim_uikit_profile_userinfo_card_narrow.dart'; +import 'package:tencent_cloud_chat_uikit/ui/views/TIMUIKitProfile/widget/tim_uikit_profile_userinfo_card/tim_uikit_profile_userinfo_card_wide.dart'; + +class TIMUIKitProfileUserInfoCard extends StatelessWidget { + /// User info + final V2TimUserFullInfo? userInfo; + final bool isJumpToPersonalProfile; + final VoidCallback? onClickAvatar; + + /// If shows the arrow icon on the right + final bool showArrowRightIcon; + + const TIMUIKitProfileUserInfoCard( + {Key? key, + this.userInfo, + @Deprecated("This info card can no longer navigate to default personal profile page automatically, please deal with it manually.") + this.isJumpToPersonalProfile = false, + this.showArrowRightIcon = false, + this.onClickAvatar}) + : super(key: key); + + @override + Widget build(BuildContext context) { + return TUIKitScreenUtils.getDeviceWidget( + defaultWidget: TIMUIKitProfileUserInfoCardNarrow( + userInfo: userInfo, + isJumpToPersonalProfile: isJumpToPersonalProfile, + showArrowRightIcon: showArrowRightIcon, + onClickAvatar: onClickAvatar, + ), + desktopWidget: TIMUIKitProfileUserInfoCardWide( + userInfo: userInfo, + onClickAvatar: onClickAvatar, + isJumpToPersonalProfile: isJumpToPersonalProfile, + showArrowRightIcon: showArrowRightIcon, + ), + mobileWidget: TIMUIKitProfileUserInfoCardNarrow( + userInfo: userInfo, + onClickAvatar: onClickAvatar, + isJumpToPersonalProfile: isJumpToPersonalProfile, + showArrowRightIcon: showArrowRightIcon, + ), + ); + } +} diff --git a/lib/ui/views/TIMUIKitProfile/widget/tim_uikit_profile_userinfo_card.dart b/lib/ui/views/TIMUIKitProfile/widget/tim_uikit_profile_userinfo_card/tim_uikit_profile_userinfo_card_narrow.dart similarity index 78% rename from lib/ui/views/TIMUIKitProfile/widget/tim_uikit_profile_userinfo_card.dart rename to lib/ui/views/TIMUIKitProfile/widget/tim_uikit_profile_userinfo_card/tim_uikit_profile_userinfo_card_narrow.dart index 3feccbf..7f65f4c 100644 --- a/lib/ui/views/TIMUIKitProfile/widget/tim_uikit_profile_userinfo_card.dart +++ b/lib/ui/views/TIMUIKitProfile/widget/tim_uikit_profile_userinfo_card/tim_uikit_profile_userinfo_card_narrow.dart @@ -5,16 +5,18 @@ import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_statelesswidget import 'package:tencent_cloud_chat_uikit/ui/widgets/avatar.dart'; -class TIMUIKitProfileUserInfoCard extends TIMUIKitStatelessWidget { +class TIMUIKitProfileUserInfoCardNarrow extends TIMUIKitStatelessWidget { /// User info final V2TimUserFullInfo? userInfo; final bool isJumpToPersonalProfile; + final VoidCallback? onClickAvatar; /// If shows the arrow icon on the right final bool showArrowRightIcon; - TIMUIKitProfileUserInfoCard( + TIMUIKitProfileUserInfoCardNarrow( {Key? key, + this.onClickAvatar, this.userInfo, @Deprecated("This info card can no longer navigate to default personal profile page automatically, please deal with it manually.") this.isJumpToPersonalProfile = false, @@ -43,11 +45,14 @@ class TIMUIKitProfileUserInfoCard extends TIMUIKitStatelessWidget { SizedBox( width: 48, height: 48, - child: Avatar( - faceUrl: faceUrl, - isShowBigWhenClick: true, - showName: showName ?? "", - type: 1, + child: GestureDetector( + onTap: onClickAvatar, + child: Avatar( + faceUrl: faceUrl, + isShowBigWhenClick: onClickAvatar == null, + showName: showName ?? "", + type: 1, + ), ), ), const SizedBox( @@ -59,10 +64,9 @@ class TIMUIKitProfileUserInfoCard extends TIMUIKitStatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ SizedBox( - child: Text( + child: SelectableText( showName ?? "", style: const TextStyle(fontSize: 18, color: Colors.black), - softWrap: true, ), ), Container( @@ -71,16 +75,18 @@ class TIMUIKitProfileUserInfoCard extends TIMUIKitStatelessWidget { children: [ Text( "ID: ", - style: TextStyle(fontSize: 13, color: theme.weakTextColor), + style: + TextStyle(fontSize: 13, color: theme.weakTextColor), ), SelectableText( userInfo?.userID ?? "", - style: TextStyle(fontSize: 13, color: theme.weakTextColor), + style: + TextStyle(fontSize: 13, color: theme.weakTextColor), ), ], ), ), - Text(signatureText, + SelectableText(signatureText, style: TextStyle(fontSize: 13, color: theme.weakTextColor)) ], ), diff --git a/lib/ui/views/TIMUIKitProfile/widget/tim_uikit_profile_userinfo_card/tim_uikit_profile_userinfo_card_wide.dart b/lib/ui/views/TIMUIKitProfile/widget/tim_uikit_profile_userinfo_card/tim_uikit_profile_userinfo_card_wide.dart new file mode 100644 index 0000000..2e74373 --- /dev/null +++ b/lib/ui/views/TIMUIKitProfile/widget/tim_uikit_profile_userinfo_card/tim_uikit_profile_userinfo_card_wide.dart @@ -0,0 +1,103 @@ +import 'package:flutter/material.dart'; +import 'package:tencent_im_base/tencent_im_base.dart'; +import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_base.dart'; +import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_statelesswidget.dart'; +import 'package:tencent_cloud_chat_uikit/ui/widgets/avatar.dart'; + +class TIMUIKitProfileUserInfoCardWide extends TIMUIKitStatelessWidget { + /// User info + final V2TimUserFullInfo? userInfo; + final bool isJumpToPersonalProfile; + final VoidCallback? onClickAvatar; + + /// If shows the arrow icon on the right + final bool showArrowRightIcon; + + TIMUIKitProfileUserInfoCardWide( + {Key? key, + this.userInfo, + this.onClickAvatar, + @Deprecated("This info card can no longer navigate to default personal profile page automatically, please deal with it manually.") + this.isJumpToPersonalProfile = false, + this.showArrowRightIcon = false}) + : super(key: key); + + @override + Widget tuiBuild(BuildContext context, TUIKitBuildValue value) { + final TUITheme theme = value.theme; + final faceUrl = userInfo?.faceUrl ?? ""; + final nickName = userInfo?.nickName ?? ""; + final signature = userInfo?.selfSignature; + final showName = nickName != "" ? nickName : userInfo?.userID; + + return Container( + padding: const EdgeInsets.only(bottom: 10, left: 16, right: 16), + color: Colors.white, + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + child: SelectableText( + showName ?? "", + style: const TextStyle(fontSize: 20, color: Colors.black), + ), + margin: const EdgeInsets.only(right: 10), + ), + Row( + children: [ + Text( + "ID: ", + style: + TextStyle(fontSize: 12, color: theme.weakTextColor), + ), + Expanded(child: SelectableText( + userInfo?.userID ?? "", + style: + TextStyle(fontSize: 12, color: theme.weakTextColor), + )), + ], + ), + if (signature != null) + Container( + margin: const EdgeInsets.only(top: 18), + child: SelectableText(signature, + style: TextStyle( + fontSize: 14, color: hexToColor("7f7f7f"))), + ) + ], + ), + ), + Row( + children: [ + const SizedBox( + width: 40, + ), + SizedBox( + width: 80, + height: 80, + child: InkWell( + onTap: onClickAvatar, + child: Avatar( + faceUrl: faceUrl, + isShowBigWhenClick: onClickAvatar == null, + showName: showName ?? "", + type: 1, + ), + ), + ), + showArrowRightIcon + ? const Icon(Icons.keyboard_arrow_right) + : Container() + ], + ) + ], + ), + ); + } +} diff --git a/lib/ui/views/TIMUIKitProfile/widget/tim_uikit_profile_widget.dart b/lib/ui/views/TIMUIKitProfile/widget/tim_uikit_profile_widget.dart index 9904853..7576bb3 100644 --- a/lib/ui/views/TIMUIKitProfile/widget/tim_uikit_profile_widget.dart +++ b/lib/ui/views/TIMUIKitProfile/widget/tim_uikit_profile_widget.dart @@ -1,36 +1,63 @@ +import 'dart:math'; + import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_class.dart'; import 'package:tencent_cloud_chat_uikit/tencent_cloud_chat_uikit.dart'; +import 'package:tencent_cloud_chat_uikit/ui/utils/screen_utils.dart'; import 'package:tencent_cloud_chat_uikit/ui/widgets/avatar.dart'; class TIMUIKitProfileWidget extends TIMUIKitClass { - static Widget operationDivider() { - return const SizedBox( - height: 10, + static final bool isDesktopScreen = + TUIKitScreenUtils.getFormFactor() == DeviceType.Desktop; + + static Widget operationDivider( + {Color? color, double? height, EdgeInsetsGeometry? margin}) { + return Container( + color: color, + margin: margin, + height: height ?? 10, ); } /// Remarks - static Widget remarkBar(String remark, Function()? handleTap) { + static Widget remarkBar( + BuildContext context, + String remark, + Function({Offset? offset, String? initText})? handleTap, + bool smallCardMode) { + final GlobalKey key = GlobalKey(); return InkWell( - onTap: () { + onTapDown: (details) { if (handleTap != null) { - handleTap(); + handleTap( + offset: Offset( + min(details.globalPosition.dx, + MediaQuery.of(context).size.width - 400), + min(details.globalPosition.dy, + MediaQuery.of(context).size.height - 100)), + initText: remark); } }, child: TIMUIKitOperationItem( + smallCardMode: smallCardMode, + itemBoxKey: key, + isEmpty: remark.isEmpty, + wideEditText: TIM_t("设置备注名"), operationName: TIM_t("备注名"), - operationRightWidget: Text(remark), + operationRightWidget: + Text(remark, textAlign: isDesktopScreen ? null : TextAlign.end), ), ); } /// add to block list - static Widget addToBlackListBar( - bool value, BuildContext context, Function(bool value)? onChanged) { + static Widget addToBlackListBar(bool value, BuildContext context, + Function(bool value)? onChanged, bool smallCardMode) { return TIMUIKitOperationItem( + smallCardMode: smallCardMode, + isEmpty: false, operationName: TIM_t("加入黑名单"), type: "switch", operationValue: value, @@ -43,9 +70,11 @@ class TIMUIKitProfileWidget extends TIMUIKitClass { } /// pin the conversation to the top - static Widget pinConversationBar( - bool value, BuildContext context, Function(bool value)? onChanged) { + static Widget pinConversationBar(bool value, BuildContext context, + Function(bool value)? onChanged, bool smallCardMode) { return TIMUIKitOperationItem( + smallCardMode: smallCardMode, + isEmpty: false, operationName: TIM_t("置顶聊天"), type: "switch", operationValue: value, @@ -58,9 +87,11 @@ class TIMUIKitProfileWidget extends TIMUIKitClass { } /// message disturb - static Widget messageDisturb( - BuildContext context, bool isDisturb, Function(bool value)? onChanged) { + static Widget messageDisturb(BuildContext context, bool isDisturb, + Function(bool value)? onChanged, bool smallCardMode) { return TIMUIKitOperationItem( + smallCardMode: smallCardMode, + isEmpty: false, operationName: TIM_t("消息免打扰"), type: "switch", operationValue: isDisturb, @@ -72,25 +103,33 @@ class TIMUIKitProfileWidget extends TIMUIKitClass { ); } - static Widget operationItem({ - required String operationName, - required String type, - bool? operationValue, - String? operationText, - void Function(bool newValue)? onSwitchChange, - }) { + static Widget operationItem( + {required String operationName, + required String type, + bool? operationValue, + String? operationText, + required bool isEmpty, + void Function(bool newValue)? onSwitchChange, + required bool smallCardMode}) { return TIMUIKitOperationItem( + smallCardMode: smallCardMode, + isEmpty: isEmpty, operationName: operationName, type: type, - operationRightWidget: Text(operationText ?? ""), + operationRightWidget: Text(operationText ?? "", + textAlign: isDesktopScreen ? null : TextAlign.end), operationValue: operationValue, onSwitchChange: onSwitchChange, ); } /// find history message - static Widget searchBar(BuildContext context, V2TimConversation conversation, - {Function()? handleTap}) { + static Widget searchBar( + BuildContext context, + V2TimConversation conversation, + bool smallCardMode, { + Function()? handleTap, + }) { return InkWell( onTap: () { if (handleTap != null) { @@ -98,24 +137,29 @@ class TIMUIKitProfileWidget extends TIMUIKitClass { } }, child: TIMUIKitOperationItem( + isEmpty: true, + wideEditText: TIM_t("立即搜索"), operationName: TIM_t("查找聊天内容"), ), ); } /// portrait - static Widget portraitBar(Widget portraitWidget) { + static Widget portraitBar(Widget portraitWidget, bool smallCardMode) { return SizedBox( child: TIMUIKitOperationItem( + smallCardMode: smallCardMode, + isEmpty: false, operationName: TIM_t("头像"), operationRightWidget: portraitWidget, - showArrowRightIcon: false, + showAllowEditStatus: false, ), ); } /// defaultPortraitWidget - static Widget defaultPortraitWidget(V2TimUserFullInfo? userInfo) { + static Widget defaultPortraitWidget( + V2TimUserFullInfo? userInfo, bool smallCardMode) { return SizedBox( width: 48, height: 48, @@ -130,40 +174,52 @@ class TIMUIKitProfileWidget extends TIMUIKitClass { } /// nickname - static Widget nicknameBar(String nickName) { + static Widget nicknameBar( + String nickName, + bool smallCardMode, + ) { return SizedBox( child: TIMUIKitOperationItem( - showArrowRightIcon: false, + smallCardMode: smallCardMode, + isEmpty: nickName.isEmpty, + showAllowEditStatus: false, operationName: TIM_t("昵称"), - operationRightWidget: Text(nickName), + operationRightWidget: + Text(nickName, textAlign: isDesktopScreen ? null : TextAlign.end), ), ); } /// user account - static Widget userAccountBar(String userNum) { + static Widget userAccountBar(String userNum, bool smallCardMode) { return SizedBox( child: TIMUIKitOperationItem( - showArrowRightIcon: false, + smallCardMode: smallCardMode, + isEmpty: false, + showAllowEditStatus: false, operationName: TIM_t("账号"), - operationRightWidget: SelectableText(userNum), + operationRightWidget: SelectableText(userNum, + textAlign: isDesktopScreen ? null : TextAlign.end), ), ); } /// signature - static Widget signatureBar(String signature) { + static Widget signatureBar(String signature, bool smallCardMode) { return SizedBox( child: TIMUIKitOperationItem( - showArrowRightIcon: false, + smallCardMode: smallCardMode, + isEmpty: false, + showAllowEditStatus: false, operationName: TIM_t("个性签名"), - operationRightWidget: Text(signature), + operationRightWidget: + Text(signature, textAlign: isDesktopScreen ? null : TextAlign.end), ), ); } /// gender - static Widget genderBar(int gender) { + static Widget genderBar(int gender, bool smallCardMode) { Map genderMap = { 0: TIM_t("未填写"), 1: TIM_t("男"), @@ -171,15 +227,18 @@ class TIMUIKitProfileWidget extends TIMUIKitClass { }; return SizedBox( child: TIMUIKitOperationItem( - showArrowRightIcon: false, + smallCardMode: smallCardMode, + isEmpty: false, + showAllowEditStatus: false, operationName: TIM_t("性别"), - operationRightWidget: Text(genderMap[gender]), + operationRightWidget: Text(genderMap[gender], + textAlign: isDesktopScreen ? null : TextAlign.end), ), ); } /// gender - static Widget genderBarWithArrow(int gender) { + static Widget genderBarWithArrow(int gender, bool smallCardMode) { Map genderMap = { 0: TIM_t("未填写"), 1: TIM_t("男"), @@ -187,48 +246,51 @@ class TIMUIKitProfileWidget extends TIMUIKitClass { }; return SizedBox( child: TIMUIKitOperationItem( + smallCardMode: smallCardMode, + isEmpty: false, operationName: TIM_t("性别"), - operationRightWidget: Text(genderMap[gender]), + operationRightWidget: Text(genderMap[gender], + textAlign: isDesktopScreen ? null : TextAlign.end), ), ); } /// birthday - static Widget birthdayBar(int? birthday) { + static Widget birthdayBar(int? birthday, bool smallCardMode) { try { final date = DateTime.parse(birthday.toString()); DateFormat formatter = DateFormat('yyyy-MM-dd'); - return InkWell( - onTap: () {}, - child: TIMUIKitOperationItem( - showArrowRightIcon: false, - operationName: TIM_t("生日"), - operationRightWidget: Text(formatter.format(date)), - ), + return TIMUIKitOperationItem( + smallCardMode: smallCardMode, + isEmpty: false, + showAllowEditStatus: false, + operationName: TIM_t("生日"), + operationRightWidget: Text(formatter.format(date), + textAlign: isDesktopScreen ? null : TextAlign.end), ); } catch (e) { - return InkWell( - onTap: () {}, - child: TIMUIKitOperationItem( - showArrowRightIcon: false, - operationName: TIM_t("生日"), - operationRightWidget: Text(TIM_t("未填写")), - ), + return TIMUIKitOperationItem( + smallCardMode: smallCardMode, + isEmpty: false, + showAllowEditStatus: false, + operationName: TIM_t("生日"), + operationRightWidget: + Text(TIM_t("未填写"), textAlign: isDesktopScreen ? null : TextAlign.end), ); } } /// default button area static Widget addAndDeleteArea( - V2TimFriendInfo friendInfo, - V2TimConversation conversation, - int friendType, - bool isDisturb, - bool isBlocked, - TUITheme theme, - VoidCallback handleAddFriend, - VoidCallback handleDeleteFriend, - ) { + V2TimFriendInfo friendInfo, + V2TimConversation conversation, + int friendType, + bool isDisturb, + bool isBlocked, + TUITheme theme, + VoidCallback handleAddFriend, + VoidCallback handleDeleteFriend, + bool smallCardMode) { _buildDeleteFriend(V2TimConversation conversation, theme) { return InkWell( onTap: () { @@ -279,4 +341,75 @@ class TIMUIKitProfileWidget extends TIMUIKitClass { ], ); } + + static Widget wideButton({ + required VoidCallback onPressed, + required String text, + required Color color, + required bool smallCardMode, + EdgeInsets? margin, + }) { + return Container( + margin: margin ?? const EdgeInsets.symmetric(vertical: 10), + child: smallCardMode + ? OutlinedButton( + onPressed: onPressed, + child: Text( + text, + style: TextStyle(color: color), + ), + style: ButtonStyle( + minimumSize: + MaterialStateProperty.all(const Size(160, 40)), + )) + : ElevatedButton( + onPressed: onPressed, + child: Text(text), + style: ButtonStyle( + minimumSize: + MaterialStateProperty.all(const Size(180, 46)), + backgroundColor: MaterialStateProperty.all(color)), + ), + ); + } + + /// default button area + static Widget addAndDeleteAreaWide( + V2TimFriendInfo friendInfo, + V2TimConversation conversation, + int friendType, + bool isDisturb, + bool isBlocked, + TUITheme theme, + VoidCallback handleAddFriend, + VoidCallback handleDeleteFriend, + bool smallCardMode, + ) { + _buildDeleteFriend(V2TimConversation conversation, theme) { + return wideButton( + smallCardMode: smallCardMode, + onPressed: () { + handleDeleteFriend(); + }, + color: theme.cautionColor ?? Colors.red, + text: TIM_t("清除好友"), + ); + } + + _buildAddOperation() { + return wideButton( + smallCardMode: smallCardMode, + onPressed: handleAddFriend, + color: theme.primaryColor ?? hexToColor("3e4b67"), + text: TIM_t("加为好友"), + ); + } + + return Column( + children: [ + if (friendType != 0) _buildDeleteFriend(conversation, theme), + if (friendType == 0 && !isBlocked) _buildAddOperation() + ], + ); + } } diff --git a/lib/ui/views/TIMUIKitSearch/pureUI/tim_uikit_search_input.dart b/lib/ui/views/TIMUIKitSearch/pureUI/tim_uikit_search_input.dart index 5a4591d..e63f5e3 100644 --- a/lib/ui/views/TIMUIKitSearch/pureUI/tim_uikit_search_input.dart +++ b/lib/ui/views/TIMUIKitSearch/pureUI/tim_uikit_search_input.dart @@ -1,12 +1,9 @@ import 'package:flutter/material.dart'; import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_state.dart'; - - +import 'package:tencent_cloud_chat_uikit/ui/utils/screen_utils.dart'; import 'package:tencent_im_base/tencent_im_base.dart'; - import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_base.dart'; - class TIMUIKitSearchInput extends StatefulWidget { final ValueChanged onChange; final String? initValue; @@ -50,30 +47,38 @@ class TIMUIKitSearchInputState extends TIMUIKitState { @override Widget tuiBuild(BuildContext context, TUIKitBuildValue value) { final TUITheme theme = value.theme; - + final isDesktopScreen = TUIKitScreenUtils.getFormFactor(context) == DeviceType.Desktop; return Container( - height: 64, - padding: const EdgeInsets.fromLTRB(16, 8, 16, 16), - decoration: BoxDecoration(color: theme.primaryColor, boxShadow: [ - BoxShadow( - color: theme.weakBackgroundColor ?? hexToColor("E6E9EB"), - offset: const Offset(0.0, 2.0), - ) - ]), + // height: 64, + padding: EdgeInsets.fromLTRB(16, isDesktopScreen ? 16 : 8, 16, 16), + margin: isDesktopScreen ? const EdgeInsets.only(bottom: 2) : null, + decoration: BoxDecoration( + color: isDesktopScreen + ? theme.wideBackgroundColor + : theme.primaryColor, + boxShadow: [ + BoxShadow( + color: theme.weakBackgroundColor ?? hexToColor("E6E9EB"), + offset: const Offset(0.0, 2.0), + ) + ] + ), child: Row( crossAxisAlignment: CrossAxisAlignment.center, children: [ Expanded( - child: SizedBox( - height: 36, + child: ConstrainedBox( + constraints: BoxConstraints(maxHeight: isDesktopScreen ? 30 : 36), child: TextField( autofocus: widget.isAutoFocus ?? true, onChanged: (value) async { final trimValue = value.trim(); final isEmpty = trimValue.isEmpty; - setState(() { - isEmptyInput = isEmpty ? true : false; - }); + if(isEmpty != isEmptyInput){ + setState(() { + isEmptyInput = isEmpty ? true : false; + }); + } widget.onChange(trimValue); }, keyboardType: TextInputType.text, @@ -83,13 +88,18 @@ class TIMUIKitSearchInputState extends TIMUIKitState { focusNode: widget.focusNode, controller: textEditingController, textAlignVertical: TextAlignVertical.center, + textAlign: TextAlign.start, + style: isDesktopScreen ? const TextStyle( + fontSize: 12 + ) : null, decoration: InputDecoration( - border: InputBorder.none, + contentPadding: const EdgeInsets.all(0), + border: const OutlineInputBorder(borderSide: BorderSide.none), hintStyle: TextStyle( - fontSize: 14, + fontSize: isDesktopScreen ? 12 : 14, color: hexToColor("CCCCCC"), ), - fillColor: Colors.white, + fillColor: isDesktopScreen ? hexToColor("f3f3f4") : Colors.white, filled: true, isDense: true, hintText: TIM_t("搜索"), @@ -120,7 +130,7 @@ class TIMUIKitSearchInputState extends TIMUIKitState { ), ), )), - Container( + if(!isDesktopScreen) Container( margin: const EdgeInsets.fromLTRB(10, 0, 0, 0), child: GestureDetector( onTap: () { diff --git a/lib/ui/views/TIMUIKitSearch/pureUI/tim_uikit_search_item.dart b/lib/ui/views/TIMUIKitSearch/pureUI/tim_uikit_search_item.dart index 5d49743..4a5ed9f 100644 --- a/lib/ui/views/TIMUIKitSearch/pureUI/tim_uikit_search_item.dart +++ b/lib/ui/views/TIMUIKitSearch/pureUI/tim_uikit_search_item.dart @@ -1,6 +1,8 @@ import 'package:flutter/material.dart'; import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_statelesswidget.dart'; import 'package:tencent_cloud_chat_uikit/tencent_cloud_chat_uikit.dart'; +import 'package:tencent_cloud_chat_uikit/ui/utils/screen_utils.dart'; +import 'package:tencent_cloud_chat_uikit/ui/views/TIMUIKitSearch/tim_uikit_search_item_wide.dart'; import 'package:tencent_cloud_chat_uikit/ui/widgets/avatar.dart'; import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_base.dart'; @@ -51,57 +53,67 @@ class TIMUIKitSearchItem extends TIMUIKitStatelessWidget { Widget tuiBuild(BuildContext context, TUIKitBuildValue value) { final TUITheme theme = value.theme; - return GestureDetector( - onTap: onClick, - child: Container( - decoration: BoxDecoration( - border: Border( - bottom: BorderSide(color: hexToColor("DBDBDB"), width: 0.5))), - padding: const EdgeInsets.fromLTRB(0, 12, 0, 12), - child: Row( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - SizedBox( - width: 40, - height: 40, - child: Stack( - fit: StackFit.expand, - clipBehavior: Clip.none, - children: [Avatar(faceUrl: faceUrl, showName: showName)], + return TUIKitScreenUtils.getDeviceWidget( + defaultWidget: GestureDetector( + onTap: onClick, + child: Container( + decoration: BoxDecoration( + border: Border( + bottom: BorderSide(color: hexToColor("DBDBDB"), width: 0.5))), + padding: const EdgeInsets.fromLTRB(0, 12, 0, 12), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + SizedBox( + width: 40, + height: 40, + child: Stack( + fit: StackFit.expand, + clipBehavior: Clip.none, + children: [Avatar(faceUrl: faceUrl, showName: showName)], + ), ), - ), - Expanded( - child: Container( - margin: const EdgeInsets.only(left: 12), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Container( - // height: 24, - padding: const EdgeInsets.symmetric(vertical: 2), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Text( - lineOne, - style: const TextStyle( - color: Colors.black, - fontSize: 18.0, - fontWeight: FontWeight.w400), - ), - _renderLineOneRight(lineOneRight, theme), - ], + Expanded( + child: Container( + margin: const EdgeInsets.only(left: 12), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Container( + // height: 24, + padding: const EdgeInsets.symmetric(vertical: 2), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text( + lineOne, + style: TextStyle( + color: theme.darkTextColor, + fontSize: 18.0, + fontWeight: FontWeight.w400), + ), + _renderLineOneRight(lineOneRight, theme), + ], + ), ), - ), - _renderLineTwo(lineTwo, theme), - ], - ), - )) - ], + _renderLineTwo(lineTwo, theme), + ], + ), + )) + ], + ), ), ), + desktopWidget: TIMUIKitSearchWideItem( + lineOneRight: lineOneRight, + key: key, + lineTwo: lineTwo, + onClick: onClick, + faceUrl: faceUrl, + showName: showName, + lineOne: lineOne), ); } } diff --git a/lib/ui/views/TIMUIKitSearch/pureUI/tim_uikit_search_showAll.dart b/lib/ui/views/TIMUIKitSearch/pureUI/tim_uikit_search_showAll.dart index 590bb2c..e8837af 100644 --- a/lib/ui/views/TIMUIKitSearch/pureUI/tim_uikit_search_showAll.dart +++ b/lib/ui/views/TIMUIKitSearch/pureUI/tim_uikit_search_showAll.dart @@ -5,6 +5,7 @@ import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_statelesswidget import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_base.dart'; import 'package:tencent_cloud_chat_uikit/tencent_cloud_chat_uikit.dart'; +import 'package:tencent_cloud_chat_uikit/ui/utils/screen_utils.dart'; class TIMUIKitSearchShowALl extends TIMUIKitStatelessWidget { final String textShow; @@ -20,7 +21,8 @@ class TIMUIKitSearchShowALl extends TIMUIKitStatelessWidget { @override Widget tuiBuild(BuildContext context, TUIKitBuildValue value) { - return GestureDetector( + final isDesktopScreen = TUIKitScreenUtils.getFormFactor(context) == DeviceType.Desktop; + return InkWell( onTap: onClick, child: Container( decoration: BoxDecoration( @@ -47,9 +49,9 @@ class TIMUIKitSearchShowALl extends TIMUIKitStatelessWidget { padding: const EdgeInsets.symmetric(vertical: 2), child: Text( textShow, - style: const TextStyle( + style: TextStyle( color: Colors.black, - fontSize: 16.0, + fontSize: isDesktopScreen ? 14 : 16.0, fontWeight: FontWeight.w400), ), ), diff --git a/lib/ui/views/TIMUIKitSearch/tim_uikit_search.dart b/lib/ui/views/TIMUIKitSearch/tim_uikit_search.dart index 6d3f3b3..24c355c 100644 --- a/lib/ui/views/TIMUIKitSearch/tim_uikit_search.dart +++ b/lib/ui/views/TIMUIKitSearch/tim_uikit_search.dart @@ -3,7 +3,6 @@ import 'package:provider/provider.dart'; import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_state.dart'; import 'package:tencent_cloud_chat_uikit/data_services/services_locatar.dart'; import 'package:tencent_cloud_chat_uikit/business_logic/view_models/tui_search_view_model.dart'; - import 'package:tencent_cloud_chat_uikit/ui/utils/platform.dart'; import 'package:tencent_cloud_chat_uikit/ui/views/TIMUIKitSearch/pureUI/tim_uikit_search_indicator.dart'; import 'package:tencent_cloud_chat_uikit/ui/views/TIMUIKitSearch/tim_uikit_search_friend.dart'; @@ -31,6 +30,8 @@ class TIMUIKitSearch extends StatefulWidget { final Function(V2TimConversation conversation, String initKeyword)? onEnterSearchInConversation; + final VoidCallback? onBack; + final bool? isAutoFocus; const TIMUIKitSearch( @@ -41,7 +42,8 @@ class TIMUIKitSearch extends StatefulWidget { @Deprecated("You are supposed to use [onEnterSearchInConversation], though the effects are the same.") this.onEnterConversation, this.isAutoFocus = true, - this.onEnterSearchInConversation}) + this.onEnterSearchInConversation, + this.onBack}) : super(key: key); @override @@ -106,29 +108,45 @@ class TIMUIKitSearchState extends TIMUIKitState { controller: textEditingController, prefixIcon: Icon( Icons.search, + size: 16, color: hexToColor("979797"), ), ), Expanded( - child: SingleChildScrollView( - child: Column( - children: [ - if (friendResultList.isEmpty || - !(searchTypes.contains(SearchType.contact)) && - (groupList.isEmpty || - !(searchTypes.contains(SearchType.group))) && - (totalMsgCount == 0 || - !(searchTypes.contains(SearchType.history)))) - TIMUIKitSearchIndicator( - typeList: searchTypes, - onChange: (list) { - setState(() { - searchTypes = list; - }); - }, - ), - if (searchTypes.contains(SearchType.contact)) - TIMUIKitSearchFriend( + child: GestureDetector( + child: SingleChildScrollView( + child: Column( + children: [ + if ((friendResultList.isEmpty || + !(searchTypes.contains(SearchType.contact))) && + (groupList.isEmpty || + !(searchTypes + .contains(SearchType.group))) && + (totalMsgCount == 0 || + !(searchTypes + .contains(SearchType.history)))) + TIMUIKitSearchIndicator( + typeList: searchTypes, + onChange: (list) { + setState(() { + searchTypes = list; + }); + }, + ), + if (searchTypes.contains(SearchType.contact)) + TIMUIKitSearchFriend( + onTapConversation: (conversation, message) { + focusNode.unfocus(); + Future.delayed( + const Duration(milliseconds: 100), () { + widget.onTapConversation( + conversation, message); + }); + }, + friendResultList: friendResultList), + if (searchTypes.contains(SearchType.group)) + TIMUIKitSearchGroup( + groupList: groupList, onTapConversation: (conversation, message) { focusNode.unfocus(); Future.delayed(const Duration(milliseconds: 100), @@ -136,37 +154,33 @@ class TIMUIKitSearchState extends TIMUIKitState { widget.onTapConversation(conversation, message); }); }, - friendResultList: friendResultList), - if (searchTypes.contains(SearchType.group)) - TIMUIKitSearchGroup( - groupList: groupList, - onTapConversation: (conversation, message) { - focusNode.unfocus(); - Future.delayed(const Duration(milliseconds: 100), - () { - widget.onTapConversation(conversation, message); - }); - }, - ), - if (searchTypes.contains(SearchType.history)) - TIMUIKitSearchMsg( - onTapConversation: widget.onTapConversation, - keyword: textEditingController.text, - totalMsgCount: totalMsgCount, - msgList: msgList, - onEnterConversation: - (V2TimConversation conversation, String keyword) { - if (widget.onEnterSearchInConversation != null) { - widget.onEnterSearchInConversation!( - conversation, keyword); - } else if (widget.onEnterConversation != null) { - widget.onEnterConversation!( - conversation, keyword); - } - }, - ) - ], + ), + if (searchTypes.contains(SearchType.history)) + TIMUIKitSearchMsg( + onTapConversation: widget.onTapConversation, + keyword: textEditingController.text, + totalMsgCount: totalMsgCount, + msgList: msgList, + onEnterConversation: + (V2TimConversation conversation, + String keyword) { + if (widget.onEnterSearchInConversation != null) { + widget.onEnterSearchInConversation!( + conversation, keyword); + } else if (widget.onEnterConversation != null) { + widget.onEnterConversation!( + conversation, keyword); + } + }, + ), + ], + ), ), + onTap: () { + if (widget.onBack != null) { + widget.onBack!(); + } + }, )) ], ), diff --git a/lib/ui/views/TIMUIKitSearch/tim_uikit_search_item_wide.dart b/lib/ui/views/TIMUIKitSearch/tim_uikit_search_item_wide.dart new file mode 100644 index 0000000..4e27c91 --- /dev/null +++ b/lib/ui/views/TIMUIKitSearch/tim_uikit_search_item_wide.dart @@ -0,0 +1,109 @@ +import 'package:flutter/material.dart'; +import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_statelesswidget.dart'; +import 'package:tencent_cloud_chat_uikit/tencent_cloud_chat_uikit.dart'; + +import 'package:tencent_cloud_chat_uikit/ui/widgets/avatar.dart'; +import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_base.dart'; + +class TIMUIKitSearchWideItem extends TIMUIKitStatelessWidget { + final String faceUrl; + final String showName; + final String lineOne; + final String? lineOneRight; + final String? lineTwo; + final VoidCallback? onClick; + + TIMUIKitSearchWideItem( + {Key? key, + required this.faceUrl, + required this.showName, + required this.lineOne, + this.lineTwo, + this.lineOneRight, + this.onClick}) + : super(key: key); + + _renderLineOneRight(String? text, TUITheme theme) { + if (text != null) { + return Text(text, + style: TextStyle( + fontSize: 10, + color: theme.weakTextColor, + )); + } else { + return Container(); + } + } + + _renderLineTwo(String? text, TUITheme theme) { + return (text != null) + ? Container( + margin: const EdgeInsets.only(top: 0), + child: SelectableText( + text, + style: TextStyle( + color: theme.weakTextColor, height: 1.5, fontSize: 12), + ), + ) + : Container( + height: 0, + ); + } + + @override + Widget tuiBuild(BuildContext context, TUIKitBuildValue value) { + final TUITheme theme = value.theme; + + return Material( + color: Colors.white, + child: InkWell( + onTap: onClick, + child: Container( + decoration: BoxDecoration( + border: Border( + bottom: BorderSide(color: hexToColor("DBDBDB"), width: 0.5))), + padding: const EdgeInsets.fromLTRB(0, 12, 0, 12), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + SizedBox( + width: 30, + height: 30, + child: Avatar(faceUrl: faceUrl, showName: showName, isShowBigWhenClick: false,), + ), + Expanded( + child: Container( + margin: const EdgeInsets.only(left: 12), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Container( + // height: 24, + padding: const EdgeInsets.symmetric(vertical: 2), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Expanded(child: Text( + lineOne, + style: TextStyle( + color: theme.darkTextColor, + fontSize: 14.0, + fontWeight: FontWeight.w400), + )), + _renderLineOneRight(lineOneRight, theme), + ], + ), + ), + _renderLineTwo(lineTwo, theme), + ], + ), + )) + ], + ), + ), + ), + ); + } +} diff --git a/lib/ui/views/TIMUIKitSearch/tim_uikit_search_msg.dart b/lib/ui/views/TIMUIKitSearch/tim_uikit_search_msg.dart index 2d4ae79..3b840d1 100644 --- a/lib/ui/views/TIMUIKitSearch/tim_uikit_search_msg.dart +++ b/lib/ui/views/TIMUIKitSearch/tim_uikit_search_msg.dart @@ -3,6 +3,7 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_statelesswidget.dart'; +import 'package:tencent_cloud_chat_uikit/data_services/conversation/conversation_services.dart'; import 'package:tencent_cloud_chat_uikit/tencent_cloud_chat_uikit.dart'; import 'package:tencent_cloud_chat_uikit/business_logic/view_models/tui_search_view_model.dart'; import 'package:tencent_cloud_chat_uikit/ui/views/TIMUIKitSearch/pureUI/tim_uikit_search_item.dart'; @@ -47,13 +48,19 @@ class TIMUIKitSearchMsg extends TIMUIKitStatelessWidget { if (msgList.isNotEmpty) { return TIMUIKitSearchFolder(folderName: TIM_t("聊天记录"), children: [ ...msgList.map((conv) { - V2TimConversation conversation = _conversationList[ - _conversationList.indexWhere( - (item) => item!.conversationID == conv?.conversationID)]!; + V2TimConversation? conversation; + final index = _conversationList.indexWhere( + (item) => item!.conversationID == conv?.conversationID); + if(index > -1){ + conversation = _conversationList[index]!; + } + if(conversation == null){ + return Container(); + } final option1 = conv?.messageCount; return TIMUIKitSearchItem( onClick: () async { - onEnterConversation(conversation, keyword); + onEnterConversation(conversation!, keyword); }, faceUrl: conversation.faceUrl ?? "", showName: conversation.showName ?? "", diff --git a/lib/ui/views/TIMUIKitSearch/tim_uikit_search_msg_detail.dart b/lib/ui/views/TIMUIKitSearch/tim_uikit_search_msg_detail.dart index 3a9b0a8..622cf41 100644 --- a/lib/ui/views/TIMUIKitSearch/tim_uikit_search_msg_detail.dart +++ b/lib/ui/views/TIMUIKitSearch/tim_uikit_search_msg_detail.dart @@ -3,14 +3,16 @@ import 'package:provider/provider.dart'; import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_state.dart'; import 'package:tencent_cloud_chat_uikit/tencent_cloud_chat_uikit.dart'; import 'package:tencent_cloud_chat_uikit/ui/utils/platform.dart'; +import 'package:tencent_cloud_chat_uikit/ui/utils/screen_utils.dart'; +import 'package:tencent_cloud_chat_uikit/ui/utils/time_ago.dart'; import 'package:tencent_cloud_chat_uikit/ui/views/TIMUIKitSearch/pureUI/tim_uikit_search_input.dart'; import 'package:tencent_cloud_chat_uikit/ui/views/TIMUIKitSearch/pureUI/tim_uikit_search_item.dart'; import 'package:tencent_cloud_chat_uikit/business_logic/view_models/tui_search_view_model.dart'; import 'package:tencent_cloud_chat_uikit/data_services/services_locatar.dart'; import 'package:tencent_cloud_chat_uikit/ui/views/TIMUIKitSearch/pureUI/tim_uikit_search_showAll.dart'; - import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_base.dart'; import 'package:tencent_cloud_chat_uikit/ui/views/TIMUIKitSearch/tim_uikit_search_not_support.dart'; +import 'package:tencent_cloud_chat_uikit/ui/widgets/avatar.dart'; class TIMUIKitSearchMsgDetail extends StatefulWidget { /// Conversation need search @@ -19,6 +21,8 @@ class TIMUIKitSearchMsgDetail extends StatefulWidget { /// initial keyword final String keyword; + final List? initMessageList; + /// the callback after clicking each conversation message item final Function(V2TimConversation, V2TimMessage?) onTapConversation; @@ -29,7 +33,8 @@ class TIMUIKitSearchMsgDetail extends StatefulWidget { this.isAutoFocus = true, required this.currentConversation, required this.keyword, - required this.onTapConversation}) + required this.onTapConversation, + this.initMessageList}) : super(key: key); @override @@ -42,6 +47,8 @@ class TIMUIKitSearchMsgDetailState String keywordState = ""; int currentPage = 0; final FocusNode focusNode = FocusNode(); + final TextEditingController _controller = TextEditingController(); + final ScrollController _scrollController = ScrollController(); @override void initState() { @@ -54,7 +61,7 @@ class TIMUIKitSearchMsgDetailState final msgType = message.elemType; final isRevokedMessage = message.status == 6; if (isRevokedMessage) { - final isSelf = message.isSelf ?? false; + final isSelf = message.isSelf ?? true; final option2 = isSelf ? TIM_t("您") : message.nickName ?? message.sender; return TIM_t_para("{{option2}}撤回了一条消息", "$option2撤回了一条消息")( option2: option2); @@ -86,15 +93,12 @@ class TIMUIKitSearchMsgDetailState } List _renderListMessage( - List msgList, BuildContext context) { + List msgList, BuildContext context, bool isDesktopScreen) { List listWidget = []; listWidget = msgList.map((message) { return Container( padding: const EdgeInsets.fromLTRB(16, 0, 16, 0), - decoration: const BoxDecoration( - color: Colors.white, - ), child: TIMUIKitSearchItem( faceUrl: message.faceUrl ?? "", showName: TencentUtils.checkString(message.nickName) ?? @@ -105,6 +109,9 @@ class TIMUIKitSearchMsgDetailState TencentUtils.checkString(message.userID) ?? message.sender ?? "", + lineOneRight: (isDesktopScreen && message.timestamp != null) + ? TimeAgo().getTimeForMessage(message.timestamp!) + : null, lineTwo: _getMsgElem(message), onClick: () { focusNode.unfocus(); @@ -147,6 +154,7 @@ class TIMUIKitSearchMsgDetailState @override Widget tuiBuild(BuildContext context, TUIKitBuildValue value) { + final theme = value.theme; if (PlatformUtils().isWeb) { return TIMUIKitSearchNotSupport(); } @@ -156,9 +164,19 @@ class TIMUIKitSearchMsgDetailState value: serviceLocator()) ], builder: (context, w) { - final List currentMsgListForConversation = + final isDesktopScreen = + TUIKitScreenUtils.getFormFactor(context) == DeviceType.Desktop; + + List currentMsgListForConversation = Provider.of(context) .currentMsgListForConversation; + final currentText = _controller.text; + if (currentMsgListForConversation.isEmpty && + widget.initMessageList != null && + widget.initMessageList!.isNotEmpty && currentText.isEmpty) { + currentMsgListForConversation = widget.initMessageList!; + } + final int totalMsgInConversationCount = Provider.of(context) .totalMsgInConversationCount; @@ -169,35 +187,66 @@ class TIMUIKitSearchMsgDetailState currentFocus.unfocus(); } }, - child: Scaffold( - body: Column( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ + if (isDesktopScreen) + Container( + padding: const EdgeInsets.fromLTRB(16, 16, 16, 0), + child: Row( + children: [ + SizedBox( + child: Avatar( + faceUrl: widget.currentConversation.faceUrl ?? "", + showName: ""), + width: 30, + height: 30, + ), + const SizedBox(width: 16), + Text( + widget.currentConversation.showName ?? + widget.currentConversation.userID ?? + "", + style: TextStyle( + fontSize: 16, + color: theme.darkTextColor, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ], + ), + ), TIMUIKitSearchInput( focusNode: focusNode, + controller: _controller, isAutoFocus: widget.isAutoFocus, onChange: (String value) { updateMsgResult(value, true); }, initValue: widget.keyword, - prefixText: Text( - widget.currentConversation.showName ?? - widget.currentConversation.userID ?? - "", - maxLines: 1, - overflow: TextOverflow.ellipsis, + prefixIcon: Icon( + Icons.search, + size: 16, + color: hexToColor("979797"), ), ), Expanded( - child: ListView( - children: [ - ..._renderListMessage(currentMsgListForConversation, context), - _renderShowALl(keywordState.isNotEmpty && - totalMsgInConversationCount > - currentMsgListForConversation.length) - ], - )), + child: Scrollbar( + controller: _scrollController, + child: ListView( + controller: _scrollController, + children: [ + ..._renderListMessage( + currentMsgListForConversation, context, isDesktopScreen), + _renderShowALl(keywordState.isNotEmpty && + totalMsgInConversationCount > + currentMsgListForConversation.length) + ], + ), + )), ], - )), + ), ); }, ); diff --git a/lib/ui/widgets/avatar.dart b/lib/ui/widgets/avatar.dart index 888ffa2..25e5ac6 100644 --- a/lib/ui/widgets/avatar.dart +++ b/lib/ui/widgets/avatar.dart @@ -1,7 +1,7 @@ import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; import 'package:tencent_cloud_chat_uikit/business_logic/view_models/tui_self_info_view_model.dart'; -import 'package:tencent_cloud_chat_uikit/tencent_cloud_chat_uikit.dart'; +import 'package:tencent_cloud_chat_uikit/ui/utils/common_utils.dart'; import 'package:tencent_cloud_chat_uikit/ui/widgets/image_screen.dart'; import 'package:tencent_im_base/tencent_im_base.dart'; import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_statelesswidget.dart'; @@ -13,8 +13,8 @@ class Avatar extends TIMUIKitStatelessWidget { final String faceUrl; final String showName; final bool isFromLocalAsset; - final BorderRadius? borderRadius; final CoreServicesImpl coreService = serviceLocator(); + final BorderRadius? borderRadius; final V2TimUserStatus? onlineStatus; final int? type; // 1 c2c 2 group final bool isShowBigWhenClick; @@ -25,10 +25,10 @@ class Avatar extends TIMUIKitStatelessWidget { {Key? key, required this.faceUrl, this.onlineStatus, - this.borderRadius, - required this.showName, + required this.showName, this.isShowBigWhenClick = false, this.isFromLocalAsset = false, + this.borderRadius, this.type = 1}) : super(key: key); @@ -39,6 +39,7 @@ class Avatar extends TIMUIKitStatelessWidget { TencentUtils.checkString( selfInfoViewModel.globalConfig?.defaultAvatarAssetPath) ?? 'images/default_c2c_head.png', + fit: BoxFit.cover, package: selfInfoViewModel.globalConfig?.defaultAvatarAssetPath != null ? null @@ -48,6 +49,7 @@ class Avatar extends TIMUIKitStatelessWidget { TencentUtils.checkString( selfInfoViewModel.globalConfig?.defaultAvatarAssetPath) ?? 'images/default_group_head.png', + fit: BoxFit.cover, package: selfInfoViewModel.globalConfig?.defaultAvatarAssetPath != null ? null @@ -58,7 +60,10 @@ class Avatar extends TIMUIKitStatelessWidget { // final emptyAvatarBuilder = coreService.emptyAvatarBuilder; if (faceUrl != "") { if (isFromLocalAsset) { - return Image.asset(faceUrl); + return Image.asset( + faceUrl, + fit: BoxFit.cover, + ); } return CachedNetworkImage( imageUrl: faceUrl, @@ -79,6 +84,7 @@ class Avatar extends TIMUIKitStatelessWidget { TencentUtils.checkString(selfInfoViewModel .globalConfig?.defaultAvatarAssetPath) ?? 'images/default_c2c_head.png', + fit: BoxFit.cover, package: selfInfoViewModel.globalConfig?.defaultAvatarAssetPath != null @@ -90,6 +96,7 @@ class Avatar extends TIMUIKitStatelessWidget { TencentUtils.checkString(selfInfoViewModel .globalConfig?.defaultAvatarAssetPath) ?? 'images/default_group_head.png', + fit: BoxFit.cover, package: selfInfoViewModel.globalConfig?.defaultAvatarAssetPath != null @@ -133,14 +140,18 @@ class Avatar extends TIMUIKitStatelessWidget { child: Hero( tag: faceUrl, child: ClipRRect( - borderRadius: borderRadius ?? selfInfoViewModel.globalConfig?.defaultAvatarBorderRadius ?? BorderRadius.circular(4.8), + borderRadius: borderRadius ?? + selfInfoViewModel.globalConfig?.defaultAvatarBorderRadius ?? + BorderRadius.circular(4.8), child: getImageWidget(context, theme), ), ), ), if (!isShowBigWhenClick) ClipRRect( - borderRadius: borderRadius ?? selfInfoViewModel.globalConfig?.defaultAvatarBorderRadius ?? BorderRadius.circular(4.8), + borderRadius: borderRadius ?? + selfInfoViewModel.globalConfig?.defaultAvatarBorderRadius ?? + BorderRadius.circular(4.8), child: getImageWidget(context, theme), ), if (onlineStatus?.statusType != null && onlineStatus?.statusType != 0) diff --git a/lib/ui/widgets/az_list_view.dart b/lib/ui/widgets/az_list_view.dart index 95462eb..868a45a 100644 --- a/lib/ui/widgets/az_list_view.dart +++ b/lib/ui/widgets/az_list_view.dart @@ -1,10 +1,11 @@ -import 'package:azlistview/azlistview.dart'; +import 'package:azlistview_all_platforms/azlistview_all_platforms.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_state.dart'; import 'package:tencent_cloud_chat_uikit/data_services/services_locatar.dart'; import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_base.dart'; import 'package:tencent_cloud_chat_uikit/tencent_cloud_chat_uikit.dart'; +import 'package:tencent_cloud_chat_uikit/ui/utils/screen_utils.dart'; class AZListViewContainer extends StatefulWidget { final List? memberList; @@ -74,36 +75,40 @@ class _AZListViewContainerState extends TIMUIKitState { @override Widget tuiBuild(BuildContext context, TUIKitBuildValue value) { + final isDesktopScreen = + TUIKitScreenUtils.getFormFactor(context) == DeviceType.Desktop; return ChangeNotifierProvider.value( value: serviceLocator(), child: Consumer( - builder: (context, tuiTheme, child) => AzListView( - physics: const BouncingScrollPhysics( - parent: AlwaysScrollableScrollPhysics()), - data: _list!, - itemCount: _list!.length, - itemBuilder: widget.itemBuilder, - indexBarData: widget.isShowIndexBar - ? SuspensionUtil.getTagIndexList(_list!) - .where((element) => element != "@") - .toList() - : [], - susItemBuilder: (BuildContext context, int index) { - if (widget.susItemBuilder != null) { - return widget.susItemBuilder!(context, index); - } - ISuspensionBeanImpl model = _list![index]; - if (model.getSuspensionTag() == "@") { - return Container(); - } - return getSusItem(context, model.getSuspensionTag()); - }))); + builder: (context, tuiTheme, child) => Scrollbar( + child: AzListView( + physics: const BouncingScrollPhysics( + parent: AlwaysScrollableScrollPhysics()), + data: _list!, + itemCount: _list!.length, + itemBuilder: widget.itemBuilder, + indexBarData: (!isDesktopScreen && widget.isShowIndexBar) + ? SuspensionUtil.getTagIndexList(_list!) + .where((element) => element != "@") + .toList() + : [], + susItemBuilder: (BuildContext context, int index) { + if (widget.susItemBuilder != null) { + return widget.susItemBuilder!(context, index); + } + ISuspensionBeanImpl model = _list![index]; + if (model.getSuspensionTag() == "@") { + return Container(); + } + return getSusItem(context, model.getSuspensionTag()); + })))); } } class ISuspensionBeanImpl extends ISuspensionBean { String tagIndex; T memberInfo; + ISuspensionBeanImpl({required this.tagIndex, required this.memberInfo}); @override diff --git a/lib/ui/widgets/column_menu.dart b/lib/ui/widgets/column_menu.dart new file mode 100644 index 0000000..fdd8869 --- /dev/null +++ b/lib/ui/widgets/column_menu.dart @@ -0,0 +1,91 @@ +import 'dart:math'; +import 'package:flutter/material.dart'; +import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_base.dart'; +import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_state.dart'; +import 'package:tencent_cloud_chat_uikit/tencent_cloud_chat_uikit.dart'; + +class ColumnMenuItem { + String label; + VoidCallback onClick; + Widget? icon; + + ColumnMenuItem({required this.label, required this.onClick, this.icon}); +} + +class TUIKitColumnMenu extends StatefulWidget { + const TUIKitColumnMenu({Key? key, required this.data, this.padding}) : super(key: key); + + final List data; + final EdgeInsetsGeometry? padding; + + @override + State createState() => TUIKitColumnMenuState(); +} + +class TUIKitColumnMenuState extends TIMUIKitState { + + List renderMenuItems(TUITheme theme) { + return widget.data + .map( + (item) => Material( + color: Colors.white, + child: InkWell( + onTap: () { + item.onClick(); + }, + child: Container( + padding: widget.padding ?? const EdgeInsets.all(10), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + if (item.icon != null) item.icon!, + if (item.icon != null) const SizedBox( + height: 4, + width: 6, + ), + Text( + item.label, + style: TextStyle( + decoration: TextDecoration.none, + color: theme.darkTextColor, + fontSize: 14, + ), + ) + ], + ), + ), + ), + ), + ) + .toList(); + } + + @override + Widget tuiBuild(BuildContext context, TUIKitBuildValue value) { + final theme = value.theme; + return Container( + padding: const EdgeInsets.all(4), + decoration: const BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.all(Radius.circular(10)), + ), + child: ConstrainedBox( + constraints: BoxConstraints( + maxWidth: min(MediaQuery.of(context).size.width * 0.7, 350), + ), + child: Table( + columnWidths: const { + 0: IntrinsicColumnWidth(), + }, + children: [ + ...renderMenuItems(theme).map((e) => TableRow( + children: [ + e + ] + )) + ] + ), + ), + ); + } +} diff --git a/lib/ui/widgets/contact_list.dart b/lib/ui/widgets/contact_list.dart index eb3d18c..a5a9285 100644 --- a/lib/ui/widgets/contact_list.dart +++ b/lib/ui/widgets/contact_list.dart @@ -1,12 +1,13 @@ -import 'package:azlistview/azlistview.dart'; + +import 'package:azlistview_all_platforms/azlistview_all_platforms.dart'; import 'package:flutter/material.dart'; import 'package:lpinyin/lpinyin.dart'; +import 'package:tencent_cloud_chat_uikit/ui/utils/screen_utils.dart'; import 'package:tencent_im_base/tencent_im_base.dart'; import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_state.dart'; import 'package:tencent_cloud_chat_uikit/business_logic/view_models/tui_friendship_view_model.dart'; import 'package:tencent_cloud_chat_uikit/data_services/services_locatar.dart'; - import 'package:tencent_cloud_chat_uikit/ui/widgets/avatar.dart'; import 'package:tencent_cloud_chat_uikit/ui/widgets/az_list_view.dart'; import 'package:tencent_cloud_chat_uikit/ui/widgets/radio_button.dart'; @@ -19,6 +20,7 @@ class ContactList extends StatefulWidget { final Function(List selectedMember)? onSelectedMemberItemChange; final Function()? handleSlidableDelte; + final Color? bgColor; /// tap联系人列表项回调 final void Function(V2TimFriendInfo item)? onTapItem; @@ -39,21 +41,25 @@ class ContactList extends StatefulWidget { /// the builder for the empty item, especially when there is no contact final Widget Function(BuildContext context)? emptyBuilder; - const ContactList( - {Key? key, - required this.contactList, - this.isCanSelectMemberItem = false, - this.onSelectedMemberItemChange, - this.isCanSlidableDelete = false, - this.handleSlidableDelte, - this.onTapItem, - this.topList, - this.topListItemBuilder, - this.isShowOnlineStatus = false, - this.maxSelectNum, - this.groupMemberList, - this.emptyBuilder}) - : super(key: key); + final String? currentItem; + + const ContactList({ + Key? key, + required this.contactList, + this.isCanSelectMemberItem = false, + this.onSelectedMemberItemChange, + this.isCanSlidableDelete = false, + this.handleSlidableDelte, + this.onTapItem, + this.bgColor, + this.topList, + this.topListItemBuilder, + this.isShowOnlineStatus = false, + this.maxSelectNum, + this.groupMemberList, + this.emptyBuilder, + this.currentItem, + }) : super(key: key); @override State createState() => _ContactListState(); @@ -116,6 +122,9 @@ class _ContactListState extends TIMUIKitState { -1) > -1; } + + final isDesktopScreen = TUIKitScreenUtils.getFormFactor(context) == DeviceType.Desktop; + return Container( padding: const EdgeInsets.only(top: 8, left: 16, right: 12), decoration: BoxDecoration( @@ -151,8 +160,8 @@ class _ContactListState extends TIMUIKitState { padding: const EdgeInsets.only(bottom: 12), margin: const EdgeInsets.only(right: 12), child: SizedBox( - height: 40, - width: 40, + height: isDesktopScreen ? 30 : 40, + width: isDesktopScreen ? 30 : 40, child: Avatar( onlineStatus: onlineStatus, faceUrl: faceUrl, @@ -165,7 +174,8 @@ class _ContactListState extends TIMUIKitState { padding: const EdgeInsets.only(top: 10, bottom: 20, right: 28), child: Text( showName, - style: const TextStyle(color: Colors.black, fontSize: 18), + style: TextStyle( + color: Colors.black, fontSize: isDesktopScreen ? 14 : 18), ), )), ], @@ -174,6 +184,7 @@ class _ContactListState extends TIMUIKitState { } Widget generateTopItem(memberInfo) { + final isDesktopScreen = TUIKitScreenUtils.getFormFactor(context) == DeviceType.Desktop; if (widget.topListItemBuilder != null) { final customWidget = widget.topListItemBuilder!(memberInfo); if (customWidget != null) { @@ -193,8 +204,8 @@ class _ContactListState extends TIMUIKitState { child: Row( children: [ Container( - height: 40, - width: 40, + height: isDesktopScreen ? 30 : 40, + width: isDesktopScreen ? 30 : 40, margin: const EdgeInsets.only(right: 12, bottom: 12), child: memberInfo.icon, ), @@ -206,8 +217,9 @@ class _ContactListState extends TIMUIKitState { children: [ Text( memberInfo.name, - style: - TextStyle(color: hexToColor("111111"), fontSize: 18), + style: TextStyle( + color: hexToColor("111111"), + fontSize: isDesktopScreen ? 14 : 18), ), Expanded(child: Container()), // if (item.id == "newContact") @@ -232,6 +244,7 @@ class _ContactListState extends TIMUIKitState { final TUITheme theme = value.theme; final showList = _getShowList(widget.contactList); + final isDesktopScreen = TUIKitScreenUtils.getFormFactor(context) == DeviceType.Desktop; if (widget.topList != null && widget.topList!.isNotEmpty) { final topList = widget.topList! @@ -259,28 +272,35 @@ class _ContactListState extends TIMUIKitState { if (memberInfo is TopListItem) { return generateTopItem(memberInfo); } else { - return InkWell( - onTap: () { - if (widget.isCanSelectMemberItem) { - if (selectedMember.contains(memberInfo)) { - selectedMember.remove(memberInfo); - } else { - if (selectedMemberIsOverFlow()) { - return; + return Material( + color: (isDesktopScreen) + ? (widget.currentItem == memberInfo.userProfile.userID + ? theme.conversationItemChooseBgColor + : widget.bgColor) + : null, + child: InkWell( + onTap: () { + if (widget.isCanSelectMemberItem) { + if (selectedMember.contains(memberInfo)) { + selectedMember.remove(memberInfo); + } else { + if (selectedMemberIsOverFlow()) { + return; + } + selectedMember.add(memberInfo); } - selectedMember.add(memberInfo); + if (widget.onSelectedMemberItemChange != null) { + widget.onSelectedMemberItemChange!(selectedMember); + } + setState(() {}); + return; } - if (widget.onSelectedMemberItemChange != null) { - widget.onSelectedMemberItemChange!(selectedMember); + if (widget.onTapItem != null) { + widget.onTapItem!(memberInfo); } - setState(() {}); - return; - } - if (widget.onTapItem != null) { - widget.onTapItem!(memberInfo); - } - }, - child: _buildItem(theme, memberInfo), + }, + child: _buildItem(theme, memberInfo), + ), ); } }, @@ -293,5 +313,6 @@ class TopListItem { final String id; final Widget? icon; final Function()? onTap; + TopListItem({required this.name, required this.id, this.icon, this.onTap}); } diff --git a/lib/ui/widgets/customize_ball_pulse_header.dart b/lib/ui/widgets/customize_ball_pulse_header.dart index 20f04c9..6d984ec 100644 --- a/lib/ui/widgets/customize_ball_pulse_header.dart +++ b/lib/ui/widgets/customize_ball_pulse_header.dart @@ -115,10 +115,6 @@ class BallPulseHeaderWidgetState extends TIMUIKitState { // 是否运行动画 bool _isAnimated = false; - @override - void initState() { - super.initState(); - } // 循环动画 void _loopAnimated() { diff --git a/lib/ui/widgets/drag_widget.dart b/lib/ui/widgets/drag_widget.dart new file mode 100644 index 0000000..9cf12d7 --- /dev/null +++ b/lib/ui/widgets/drag_widget.dart @@ -0,0 +1,97 @@ +import 'dart:math'; + +import 'package:flutter/material.dart'; + +class TUIKitDragArea extends StatefulWidget { + final Widget child; + final Offset? initOffset; + final VoidCallback? closeFun; + final bool isAllowDrag; + final Color? backgroundColor; + + const TUIKitDragArea( + {Key? key, + required this.child, + this.initOffset, + this.closeFun, + this.isAllowDrag = false, this.backgroundColor}) + : super(key: key); + + @override + _DragAreaStateStateful createState() => _DragAreaStateStateful(); +} + +class _DragAreaStateStateful extends State { + late Offset position; + double prevScale = 1; + double scale = 1; + + @override + void initState() { + super.initState(); + position = widget.initOffset ?? const Offset(0, 200); + } + + void updateScale(double zoom) => setState(() => scale = prevScale * zoom); + + void commitScale() => setState(() => prevScale = scale); + + void updatePosition(Offset newPosition) { + final maxY = MediaQuery.of(context).size.height - 100; + final maxX = MediaQuery.of(context).size.width - 100; + + final rebuildPosition = Offset( + max(0, min(newPosition.dx, maxX)), max(0, min(newPosition.dy, maxY))); + position = rebuildPosition; + } + + @override + Widget build(BuildContext context) { + return Container( + color: widget.backgroundColor, + child: Stack( + children: [ + Row( + children: [ + Expanded( + child: Column( + children: [ + Expanded( + child: GestureDetector( + onTap: () { + if (widget.closeFun != null) { + widget.closeFun!(); + } + }, + child: Container( + color: Colors.transparent, + ), + ) + ) + ], + )) + ], + ), + if (widget.isAllowDrag) + Positioned( + left: position.dx, + top: position.dy, + child: Draggable( + maxSimultaneousDrags: 1, + feedback: widget.child, + childWhenDragging: Container(), + onDragEnd: (details) => updatePosition(details.offset), + child: widget.child, + ), + ), + if (!widget.isAllowDrag) + Positioned( + left: position.dx, + top: position.dy, + child: widget.child, + ) + ], + ), + ); + } +} diff --git a/lib/ui/widgets/emoji.dart b/lib/ui/widgets/emoji.dart index 4333474..f7ec647 100644 --- a/lib/ui/widgets/emoji.dart +++ b/lib/ui/widgets/emoji.dart @@ -1,22 +1,21 @@ -import 'package:json_annotation/json_annotation.dart'; -part 'emoji.g.dart'; - -@JsonSerializable() -class Emoji extends Object { - @JsonKey(name: 'name') +class Emoji { String name; - - @JsonKey(name: 'unicode') int unicode; - Emoji( - this.name, - this.unicode, - ); + Emoji({required this.name, required this.unicode}); - factory Emoji.fromJson(Map srcJson) => - _$EmojiFromJson(srcJson); + factory Emoji.fromJson(Map json) { + return Emoji( + name: json['name'], + unicode: json['unicode'], + ); + } - Map toJson() => _$EmojiToJson(this); -} + Map toJson() { + final Map data = {}; + data['name'] = name; + data['unicode'] = unicode; + return data; + } +} \ No newline at end of file diff --git a/lib/ui/widgets/emoji.g.dart b/lib/ui/widgets/emoji.g.dart deleted file mode 100644 index 02519f5..0000000 --- a/lib/ui/widgets/emoji.g.dart +++ /dev/null @@ -1,19 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'emoji.dart'; - -// ************************************************************************** -// JsonSerializableGenerator -// ************************************************************************** - -Emoji _$EmojiFromJson(Map json) { - return Emoji( - json['name'] as String, - json['unicode'] as int, - ); -} - -Map _$EmojiToJson(Emoji instance) => { - 'name': instance.name, - 'unicode': instance.unicode, - }; diff --git a/lib/ui/widgets/forward_message_screen.dart b/lib/ui/widgets/forward_message_screen.dart index d8b791d..781fb15 100644 --- a/lib/ui/widgets/forward_message_screen.dart +++ b/lib/ui/widgets/forward_message_screen.dart @@ -7,20 +7,24 @@ import 'package:tencent_cloud_chat_uikit/data_services/services_locatar.dart'; import 'package:tencent_cloud_chat_uikit/tencent_cloud_chat_uikit.dart'; import 'package:tencent_cloud_chat_uikit/ui/utils/message.dart'; +import 'package:tencent_cloud_chat_uikit/ui/utils/screen_utils.dart'; import 'package:tencent_cloud_chat_uikit/ui/widgets/recent_conversation_list.dart'; import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_base.dart'; +GlobalKey<_ForwardMessageScreenState> forwardMessageScreenKey = GlobalKey(); + class ForwardMessageScreen extends StatefulWidget { final bool isMergerForward; final ConvType conversationType; final TUIChatSeparateViewModel model; + final VoidCallback? onClose; const ForwardMessageScreen( {Key? key, this.isMergerForward = false, required this.conversationType, - required this.model}) + required this.model, this.onClose}) : super(key: key); @override @@ -56,7 +60,7 @@ class _ForwardMessageScreenState extends TIMUIKitState { }).toList(); } - _handleForwardMessage() async { + handleForwardMessage() async { if (widget.isMergerForward) { await widget.model.sendMergerMessage( conversationList: _conversationList, @@ -69,30 +73,44 @@ class _ForwardMessageScreenState extends TIMUIKitState { .sendForwardMessage(conversationList: _conversationList); } widget.model.updateMultiSelectStatus(false); - Navigator.pop(context); + + if(widget.onClose != null){ + widget.onClose!(); + }else{ + Navigator.pop(context); + } } @override Widget tuiBuild(BuildContext context, TUIKitBuildValue value) { + final isDesktopScreen = + TUIKitScreenUtils.getFormFactor(context) == DeviceType.Desktop; final TUITheme theme = value.theme; + if(isDesktopScreen){ + isMultiSelect = true; + return RecentForwardList( + isMultiSelect: isMultiSelect, + onChanged: (conversationList) { + _conversationList = conversationList; + + if (!isMultiSelect) { + handleForwardMessage(); + } + }, + ); + } return Scaffold( appBar: AppBar( title: Text( TIM_t("选择"), - style: const TextStyle( - color: Colors.white, + style: TextStyle( + color: theme.appbarTextColor, fontSize: 17, ), ), shadowColor: theme.weakBackgroundColor, - flexibleSpace: Container( - decoration: BoxDecoration( - gradient: LinearGradient(colors: [ - theme.lightPrimaryColor ?? CommonColor.lightPrimaryColor, - theme.primaryColor ?? CommonColor.primaryColor - ]), - ), - ), + backgroundColor: theme.appbarBgColor ?? + theme.primaryColor, leading: TextButton( onPressed: () { if (isMultiSelect) { @@ -102,17 +120,22 @@ class _ForwardMessageScreenState extends TIMUIKitState { }); } else { widget.model.updateMultiSelectStatus(false); - Navigator.pop(context); + if(widget.onClose != null){ + widget.onClose!(); + }else{ + Navigator.pop(context); + } } }, child: Text( TIM_t("取消"), - style: const TextStyle( - color: Colors.white, - fontSize: 16, + style: TextStyle( + color: theme.appbarTextColor, + fontSize: 14, ), ), ), + leadingWidth: 80, actions: [ TextButton( onPressed: () { @@ -121,7 +144,7 @@ class _ForwardMessageScreenState extends TIMUIKitState { isMultiSelect = true; }); } else { - _handleForwardMessage(); + handleForwardMessage(); } }, child: Text( @@ -140,7 +163,7 @@ class _ForwardMessageScreenState extends TIMUIKitState { _conversationList = conversationList; if (!isMultiSelect) { - _handleForwardMessage(); + handleForwardMessage(); } }, ), diff --git a/lib/ui/widgets/group_member_list.dart b/lib/ui/widgets/group_member_list.dart index 9972d16..464763c 100644 --- a/lib/ui/widgets/group_member_list.dart +++ b/lib/ui/widgets/group_member_list.dart @@ -1,10 +1,11 @@ // ignore_for_file: must_be_immutable -import 'package:azlistview/azlistview.dart'; +import 'package:azlistview_all_platforms/azlistview_all_platforms.dart'; import 'package:flutter/material.dart'; import 'package:flutter_slidable_for_tencent_im/flutter_slidable.dart'; import 'package:lpinyin/lpinyin.dart'; import 'package:provider/provider.dart'; +import 'package:tencent_cloud_chat_uikit/ui/utils/screen_utils.dart'; import 'package:tencent_im_base/tencent_im_base.dart'; import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_state.dart'; import 'package:tencent_cloud_chat_uikit/ui/utils/optimize_utils.dart'; @@ -25,7 +26,7 @@ class GroupProfileMemberList extends StatefulWidget { final Function(List selectedMember)? onSelectedMemberChange; // notice: onTapMemberItem and onSelectedMemberChange use together will triger together - final Function(V2TimGroupMemberFullInfo memberInfo)? onTapMemberItem; + final Function(V2TimGroupMemberFullInfo memberInfo, TapDownDetails? tapDetails)? onTapMemberItem; // When sliding to the bottom bar callBack final Function()? touchBottomCallBack; @@ -111,6 +112,8 @@ class _GroupProfileMemberListState Widget _buildListItem( BuildContext context, V2TimGroupMemberFullInfo memberInfo) { final theme = Provider.of(context).theme; + final isDesktopScreen = + TUIKitScreenUtils.getFormFactor() == DeviceType.Desktop; final isGroupMember = memberInfo.role == GroupMemberRoleType.V2TIM_GROUP_MEMBER_ROLE_MEMBER; return Container( @@ -142,12 +145,12 @@ class _GroupProfileMemberListState margin: const EdgeInsets.only(right: 10), child: CheckBoxButton( onChanged: (isChecked) { - if (widget.maxSelectNum != null && - selectedMember.length >= - widget.maxSelectNum!) { - return; - } if (isChecked) { + if (widget.maxSelectNum != null && + selectedMember.length >= + widget.maxSelectNum!) { + return; + } selectedMember.add(memberInfo); } else { selectedMember.remove(memberInfo); @@ -160,8 +163,8 @@ class _GroupProfileMemberListState isChecked: selectedMember.contains(memberInfo)), ), Container( - width: 36, - height: 36, + width: isDesktopScreen ? 30 : 36, + height: isDesktopScreen ? 30 : 36, margin: const EdgeInsets.only(right: 10), child: Avatar( faceUrl: memberInfo.faceUrl ?? "", @@ -170,7 +173,7 @@ class _GroupProfileMemberListState ), ), Text(_getShowName(memberInfo), - style: const TextStyle(fontSize: 16)), + style: TextStyle(fontSize: isDesktopScreen ? 14 : 16)), memberInfo.role == GroupMemberRoleType.V2TIM_GROUP_MEMBER_ROLE_OWNER ? Container( @@ -178,7 +181,7 @@ class _GroupProfileMemberListState child: Text(TIM_t("群主"), style: TextStyle( color: theme.ownerColor, - fontSize: 12, + fontSize: isDesktopScreen ? 10 :12, )), padding: const EdgeInsets.fromLTRB(5, 0, 5, 0), decoration: BoxDecoration( @@ -215,17 +218,17 @@ class _GroupProfileMemberListState ), onTap: () { if (widget.onTapMemberItem != null) { - widget.onTapMemberItem!(memberInfo); + widget.onTapMemberItem!(memberInfo, null); } if (widget.canSelectMember) { - if (widget.maxSelectNum != null && - selectedMember.length >= widget.maxSelectNum!) { - return; - } final isChecked = selectedMember.contains(memberInfo); if (isChecked) { selectedMember.remove(memberInfo); } else { + if (widget.maxSelectNum != null && + selectedMember.length >= widget.maxSelectNum!) { + return; + } selectedMember.add(memberInfo); } if (widget.onSelectedMemberChange != null) { @@ -270,6 +273,9 @@ class _GroupProfileMemberListState Widget tuiBuild(BuildContext context, TUIKitBuildValue value) { final TUITheme theme = value.theme; + final isDesktopScreen = + TUIKitScreenUtils.getFormFactor() == DeviceType.Desktop; + final throteFunction = OptimizeUtils.throttle((ScrollNotification notification) { final pixels = notification.metrics.pixels; @@ -283,7 +289,7 @@ class _GroupProfileMemberListState }, 300); final showList = _getShowList(widget.memberList); return Container( - color: theme.weakBackgroundColor, + color: isDesktopScreen ? null : theme.weakBackgroundColor, child: SafeArea( child: Column( children: [ @@ -298,19 +304,22 @@ class _GroupProfileMemberListState ? Center( child: Text(TIM_t("暂无群成员")), ) - : AZListViewContainer( - memberList: showList, - susItemBuilder: (context, index) { - final model = showList[index]; - return getSusItem( - context, theme, model.getSuspensionTag()); - }, - itemBuilder: (context, index) { - final memberInfo = showList[index].memberInfo - as V2TimGroupMemberFullInfo; + : Container( + padding: isDesktopScreen ? const EdgeInsets.symmetric( horizontal: 16) : null, + child: AZListViewContainer( + memberList: showList, + susItemBuilder: (context, index) { + final model = showList[index]; + return getSusItem( + context, theme, model.getSuspensionTag()); + }, + itemBuilder: (context, index) { + final memberInfo = showList[index].memberInfo + as V2TimGroupMemberFullInfo; - return _buildListItem(context, memberInfo); - }), + return _buildListItem(context, memberInfo); + }), + ), )) ], )), diff --git a/lib/ui/widgets/link_preview/common/utils.dart b/lib/ui/widgets/link_preview/common/utils.dart index f14ea36..f720e18 100644 --- a/lib/ui/widgets/link_preview/common/utils.dart +++ b/lib/ui/widgets/link_preview/common/utils.dart @@ -1,15 +1,14 @@ import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:link_preview_generator/link_preview_generator.dart'; -import 'package:tencent_im_base/tencent_im_base.dart'; +import 'package:tencent_cloud_chat_uikit/tencent_cloud_chat_uikit.dart'; import 'package:tencent_cloud_chat_uikit/ui/widgets/link_preview/common/extensions.dart'; import 'package:tencent_cloud_chat_uikit/ui/widgets/link_preview/link_preview_entry.dart'; -import 'package:tencent_cloud_chat_uikit/ui/widgets/link_preview/models/link_preview_content.dart'; import 'package:url_launcher/url_launcher.dart'; class LinkUtils { static RegExp urlReg = RegExp( - r"(https?|ftp|file)://[-A-Za-z0-9+&@#/%?=~_|!:,.;]+[-A-Za-z0-9+&@#/%=~_|]"); + r"([hH][tT]{2}[pP]:\/\/|[hH][tT]{2}[pP][sS]:\/\/|[wW]{3}.|[wW][aA][pP].|[fF][tT][pP].|[fF][iI][lL][eE].)[-A-Za-z0-9+&@#/%?=~_|!:,.;]+[-A-Za-z0-9+&@#/%=~_|]"); /// Get all the URL from a text message static List getURLMatches(String textMessage) { @@ -49,7 +48,7 @@ class LinkUtils { List urlMatches) async { // Request for preview information for all URL links synchronously final List urlPreview = - await Future.wait(urlMatches.map((e) async { + await Future.wait(urlMatches.map((e) async { String url = e; if (!e.contains("http")) { url = 'http://$e'; @@ -68,7 +67,7 @@ class LinkUtils { /// save the link info to local and call updating the message on UI, only works with [onUpdateMessage] static Future saveToLocalAndUpdate(V2TimMessage message, - LocalCustomDataModel previewItem, VoidCallback onUpdateMessage) async { + LocalCustomDataModel previewItem, ValueChanged onUpdateMessage) async { if (message.msgID != null) { String saveInfo = LinkPreviewEntry.linkInfoToString(previewItem); final currentInfo = message.localCustomData; @@ -80,10 +79,13 @@ class LinkUtils { data['description'] = previewItem.description; saveInfo = json.encode(data); } - final result = await TencentImSDKPlugin.v2TIMManager.v2TIMMessageManager - .setLocalCustomData(msgID: message.msgID!, localCustomData: saveInfo); - if (result.code == 0) { - onUpdateMessage(); + message.localCustomData = saveInfo; + if(saveInfo != currentInfo){ + final result = await TencentImSDKPlugin.v2TIMManager.v2TIMMessageManager + .setLocalCustomData(msgID: message.msgID!, localCustomData: saveInfo); + if (result.code == 0) { + onUpdateMessage(message); + } } } } diff --git a/lib/ui/widgets/link_preview/link_preview_entry.dart b/lib/ui/widgets/link_preview/link_preview_entry.dart index 9dc355f..1d42689 100644 --- a/lib/ui/widgets/link_preview/link_preview_entry.dart +++ b/lib/ui/widgets/link_preview/link_preview_entry.dart @@ -8,21 +8,17 @@ import 'models/link_preview_content.dart'; class LinkPreviewEntry { /// get the text message with hyperlinks - static LinkPreviewText? getHyperlinksText( - String messageText, bool isMarkdown, - [Function(String)? onLinkTap, + static LinkPreviewText? getHyperlinksText(String messageText, bool isMarkdown, + {Function(String)? onLinkTap, + bool? isEnableTextSelection, bool isUseDefaultEmoji = false, - List customEmojiStickerList = const []]) { - - if (messageText == null) { - return null; - } - + List customEmojiStickerList = const []}) { return ({TextStyle? style}) { return isMarkdown ? LinkTextMarkdown( messageText: messageText, style: style, onLinkTap: onLinkTap) : LinkText( + isEnableTextSelection: isEnableTextSelection, messageText: messageText, style: style, onLinkTap: onLinkTap, @@ -34,7 +30,8 @@ class LinkPreviewEntry { /// get the [LinkPreviewContent] with preview widget and website information for the first link. /// If you provide `onUpdateMessage(String linkInfoJson)`, it can save the link info to local custom data than call updating the message on UI automatically. static Future getFirstLinkPreviewContent( - {required V2TimMessage message, VoidCallback? onUpdateMessage}) async { + {required V2TimMessage message, + ValueChanged? onUpdateMessage}) async { final String? messageText = message.textElem?.text; if (messageText == null) { return null; @@ -93,22 +90,4 @@ class LinkPreviewEntry { static String linkInfoToString(LocalCustomDataModel linkInfo) { return linkInfo.toString(); } - - // static LinkPreviewModel? linkInfoFromString(String linkInfoString){ - // final Map data = json.decode(linkInfoString); - // LinkPreviewModel linkPreview = LinkPreviewModel( - // url: data['url'], - // image: data['image'], - // title: data['title'], - // description: data['description'] - // ); - // return isLinkInfoEmpty(linkPreview) ? null : linkPreview; - // } - // - // static bool isLinkInfoEmpty(LinkPreviewModel linkInfo){ - // if(linkInfo.image == null && linkInfo.title == null && linkInfo.description == null){ - // return true; - // } - // return false; - // } } diff --git a/lib/ui/widgets/link_preview/widgets/link_text.dart b/lib/ui/widgets/link_preview/widgets/link_text.dart index 0d304fe..a039b46 100644 --- a/lib/ui/widgets/link_preview/widgets/link_text.dart +++ b/lib/ui/widgets/link_preview/widgets/link_text.dart @@ -1,4 +1,7 @@ +// ignore_for_file: deprecated_member_use + import 'package:tencent_cloud_chat_uikit/ui/utils/platform.dart'; +import 'package:tencent_cloud_chat_uikit/ui/utils/screen_utils.dart'; import 'package:tencent_extended_text/extended_text.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; @@ -60,10 +63,13 @@ class LinkText extends TIMStatelessWidget { final List customEmojiStickerList; + final bool? isEnableTextSelection; + const LinkText( {Key? key, required this.messageText, this.onLinkTap, + this.isEnableTextSelection, this.style, this.isUseDefaultEmoji = false, this.customEmojiStickerList = const []}) @@ -124,6 +130,8 @@ class LinkText extends TIMStatelessWidget { @override Widget timBuild(BuildContext context) { + final isDesktopScreen = + TUIKitScreenUtils.getFormFactor(context) == DeviceType.Desktop; return // Text.rich( // TextSpan(children: [..._getContentSpan(messageText, context)]), @@ -140,6 +148,9 @@ class LinkText extends TIMStatelessWidget { } } }, + selectionEnabled: isEnableTextSelection != null + ? isEnableTextSelection! + : isDesktopScreen, style: style ?? const TextStyle(fontSize: 16.0), specialTextSpanBuilder: DefaultSpecialTextSpanBuilder( isUseDefaultEmoji: isUseDefaultEmoji, diff --git a/lib/ui/widgets/merger_message_screen.dart b/lib/ui/widgets/merger_message_screen.dart index 54cccba..d086f10 100644 --- a/lib/ui/widgets/merger_message_screen.dart +++ b/lib/ui/widgets/merger_message_screen.dart @@ -4,6 +4,8 @@ import 'package:flutter/material.dart'; import 'package:loading_animation_widget/loading_animation_widget.dart'; import 'package:provider/provider.dart'; import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_state.dart'; +import 'package:tencent_cloud_chat_uikit/ui/utils/common_utils.dart'; +import 'package:tencent_cloud_chat_uikit/ui/utils/screen_utils.dart'; import 'package:tencent_cloud_chat_uikit/ui/views/TIMUIKitChat/TIMUIKItMessageList/tim_uikit_chat_history_message_list_item.dart'; import 'package:tencent_im_base/tencent_im_base.dart'; import 'package:tencent_cloud_chat_uikit/business_logic/separate_models/tui_chat_separate_view_model.dart'; @@ -56,8 +58,10 @@ class MergerMessageScreenState extends TIMUIKitState { message.cloudCustomData != null && message.cloudCustomData != ""; if (hasCustomData) { try { - final CloudCustomData messageCloudCustomData = - CloudCustomData.fromJson(json.decode(message.cloudCustomData!)); + final CloudCustomData messageCloudCustomData = CloudCustomData.fromJson(json.decode( + TencentUtils.checkString(message.cloudCustomData) != null + ? message.cloudCustomData! + : "{}")); if (messageCloudCustomData.messageReply != null) { MessageRepliedData.fromJson(messageCloudCustomData.messageReply!); return true; @@ -72,7 +76,7 @@ class MergerMessageScreenState extends TIMUIKitState { Widget _getMsgItem(V2TimMessage message) { final type = message.elemType; - final isFromSelf = message.isSelf ?? false; + final isFromSelf = message.isSelf ?? true; switch (type) { case MessageElemType.V2TIM_ELEM_TYPE_CUSTOM: @@ -126,8 +130,8 @@ class MergerMessageScreenState extends TIMUIKitState { return TIMUIKitTextElem( chatModel: widget.model, message: message, - isFromSelf: message.isSelf ?? false, - clearJump: (){}, + isFromSelf: message.isSelf ?? true, + clearJump: () {}, isShowJump: false, isShowMessageReaction: false, ); @@ -229,7 +233,7 @@ class MergerMessageScreenState extends TIMUIKitState { final showName = message.nickName ?? message.userID ?? ""; final theme = Provider.of(context).theme; return Container( - margin: const EdgeInsets.only(bottom: 20), + margin: const EdgeInsets.only(top: 20), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -264,25 +268,11 @@ class MergerMessageScreenState extends TIMUIKitState { Widget tuiBuild(BuildContext context, TUIKitBuildValue value) { final TUITheme theme = value.theme; - return Scaffold( - appBar: AppBar( - title: Text( - TIM_t("聊天记录"), - style: const TextStyle(color: Colors.white, fontSize: 17), - ), - shadowColor: theme.weakDividerColor, - flexibleSpace: Container( - decoration: BoxDecoration( - gradient: LinearGradient(colors: [ - theme.lightPrimaryColor ?? CommonColor.lightPrimaryColor, - theme.primaryColor ?? CommonColor.primaryColor - ]), - ), - ), - iconTheme: const IconThemeData( - color: Colors.white, - )), - body: messageList.isEmpty + final isDesktopScreen = + TUIKitScreenUtils.getFormFactor(context) == DeviceType.Desktop; + + Widget messageListPage() { + return messageList.isEmpty ? Row( children: [ Expanded( @@ -301,8 +291,8 @@ class MergerMessageScreenState extends TIMUIKitState { )) ], ) - : Padding( - padding: const EdgeInsets.all(16), + : Container( + padding: isDesktopScreen ? null : const EdgeInsets.all(16), child: ListView.builder( shrinkWrap: true, itemCount: messageList.length, @@ -311,7 +301,27 @@ class MergerMessageScreenState extends TIMUIKitState { return _itemBuilder(message, context); }, ), - ), - ); + ); + } + + return TUIKitScreenUtils.getDeviceWidget( + desktopWidget: Container( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: messageListPage(), + ), + defaultWidget: Scaffold( + appBar: AppBar( + title: Text( + TIM_t("聊天记录"), + style: TextStyle(color: theme.appbarTextColor, fontSize: 17), + ), + shadowColor: theme.weakDividerColor, + backgroundColor: theme.appbarBgColor ?? + theme.primaryColor, + iconTheme: IconThemeData( + color: theme.appbarTextColor, + )), + body: messageListPage(), + )); } } diff --git a/lib/ui/widgets/message_read_receipt.dart b/lib/ui/widgets/message_read_receipt.dart index f2f9bbf..473ebff 100644 --- a/lib/ui/widgets/message_read_receipt.dart +++ b/lib/ui/widgets/message_read_receipt.dart @@ -3,10 +3,10 @@ import 'package:flutter/material.dart'; // ignore: unused_import import 'package:provider/provider.dart'; import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_state.dart'; +import 'package:tencent_cloud_chat_uikit/ui/utils/screen_utils.dart'; import 'package:tencent_im_base/tencent_im_base.dart'; import 'package:tencent_cloud_chat_uikit/business_logic/separate_models/tui_chat_separate_view_model.dart'; - import 'package:tencent_cloud_chat_uikit/ui/utils/message.dart'; import 'package:tencent_cloud_chat_uikit/ui/utils/time_ago.dart'; @@ -23,7 +23,7 @@ class MessageReadReceipt extends StatefulWidget { final V2TimMessage messageItem; final int unreadCount; final int readCount; - final void Function(String userID)? onTapAvatar; + final void Function(String userID, TapDownDetails tapDetails)? onTapAvatar; final TUIChatSeparateViewModel model; const MessageReadReceipt( @@ -92,7 +92,7 @@ class _MessageReadReceiptState extends TIMUIKitState { Widget _getMsgItem(V2TimMessage message) { final type = message.elemType; - final isFromSelf = message.isSelf ?? false; + final isFromSelf = message.isSelf ?? true; switch (type) { case MessageElemType.V2TIM_ELEM_TYPE_CUSTOM: @@ -170,10 +170,13 @@ class _MessageReadReceiptState extends TIMUIKitState { Widget _memberItemBuilder(V2TimGroupMemberInfo item, TUITheme theme) { final faceUrl = item.faceUrl ?? ''; final showName = _getShowName(item); + final isDesktopScreen = + TUIKitScreenUtils.getFormFactor(context) == DeviceType.Desktop; + return InkWell( - onTap: () { + onTapDown: (details) { if (widget.onTapAvatar != null) { - widget.onTapAvatar!(item.userID!); + widget.onTapAvatar!(item.userID!, details); } }, child: Container( @@ -181,15 +184,15 @@ class _MessageReadReceiptState extends TIMUIKitState { child: Row( children: [ Container( - height: 40, - width: 40, - margin: const EdgeInsets.only(right: 12), + height: isDesktopScreen ? 30 : 40, + width: isDesktopScreen ? 30 : 40, + margin: EdgeInsets.only(right: 12, bottom: isDesktopScreen ? 6 : 0), child: Avatar(faceUrl: faceUrl, showName: showName), ), Expanded( child: Container( alignment: Alignment.centerLeft, - padding: const EdgeInsets.only(top: 10, bottom: 19, right: 28), + padding: EdgeInsets.only(top: 10, bottom: isDesktopScreen ? 14 : 19, right: 28), decoration: BoxDecoration( border: Border( bottom: BorderSide( @@ -197,7 +200,7 @@ class _MessageReadReceiptState extends TIMUIKitState { CommonColor.weakDividerColor))), child: Text( showName, - style: const TextStyle(color: Colors.black, fontSize: 18), + style: TextStyle(color: Colors.black, fontSize: isDesktopScreen ? 14 : 18), ), )), ], @@ -211,170 +214,170 @@ class _MessageReadReceiptState extends TIMUIKitState { final TUITheme theme = value.theme; final option1 = widget.readCount; final option2 = widget.unreadCount; - return DefaultTabController( - length: 2, - child: Scaffold( - appBar: AppBar( - title: Text( - TIM_t("消息详情"), - style: const TextStyle(color: Colors.white, fontSize: 17), - ), - shadowColor: theme.weakDividerColor, - flexibleSpace: Container( - decoration: BoxDecoration( - gradient: LinearGradient(colors: [ - theme.lightPrimaryColor ?? CommonColor.lightPrimaryColor, - theme.primaryColor ?? CommonColor.primaryColor - ]), - ), - ), - iconTheme: const IconThemeData( - color: Colors.white, - )), - body: Container( - color: Colors.white, - child: Column( - children: [ - Padding( - padding: - const EdgeInsets.symmetric(vertical: 16, horizontal: 16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - Text(MessageUtils.getDisplayName(widget.messageItem)), - const SizedBox( - width: 8, - ), - Text( - TimeAgo().getTimeForMessage( - widget.messageItem.timestamp ?? 0), - softWrap: true, - style: TextStyle( - fontSize: 12, color: theme.weakTextColor), - ) - ], - ), - const SizedBox( - height: 6, - ), - _getMsgItem(widget.messageItem) - ], - ), - ), - Container( - height: 8, - color: theme.weakBackgroundColor, - ), - Row( - // direction: Axis.horizontal, - children: [ - Expanded( - flex: 1, - child: InkWell( - onTap: () { - currentIndex = 0; - setState(() {}); - }, - child: Container( - height: 50.0, - alignment: Alignment.bottomCenter, - padding: const EdgeInsets.only(bottom: 12), - decoration: BoxDecoration( - color: Colors.white, - border: Border( - bottom: BorderSide( - width: 2, - color: currentIndex == 0 - ? theme.primaryColor! - : Colors.white))), - child: Text( - TIM_t_para("{{option1}}人已读", "$option1人已读")( - option1: option1), - style: TextStyle( - color: currentIndex != 0 - ? theme.weakTextColor - : Colors.black, - fontSize: 18, - ), - ), - ), - ), - ), - Expanded( - flex: 1, - child: InkWell( - onTap: () { - currentIndex = 1; - setState(() {}); - }, - child: Container( - alignment: Alignment.bottomCenter, - height: 50.0, - padding: const EdgeInsets.only(bottom: 12), - decoration: BoxDecoration( - color: Colors.white, - border: Border( - bottom: BorderSide( - width: 2, - color: currentIndex == 1 - ? theme.primaryColor! - : Colors.white))), - child: Text( - TIM_t_para("{{option2}}人未读", "$option2人未读")( - option2: option2), - style: TextStyle( - color: currentIndex != 1 - ? theme.weakTextColor - : Colors.black, - fontSize: 18, - ), - ), - ), - ), - ), - ], - ), - Container( - height: 1, - decoration: BoxDecoration( - border: Border( - bottom: BorderSide( - color: theme.weakDividerColor ?? - CommonColor.weakDividerColor))), - ), - Expanded( - child: IndexedStack( - index: currentIndex, + final isDesktopScreen = + TUIKitScreenUtils.getFormFactor(context) == DeviceType.Desktop; + + Widget pageBody() { + return Container( + color: isDesktopScreen ? null : Colors.white, + child: Column( + children: [ + Padding( + padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - ListView.builder( - shrinkWrap: true, - itemCount: readMemberList.length, - itemBuilder: (context, index) { - if (!readMemberIsFinished && - index == readMemberList.length - 5) { - _getReadMemberList(); - } - return _memberItemBuilder(readMemberList[index], theme); - }), - ListView.builder( - shrinkWrap: true, - itemCount: unreadMemberList.length, - itemBuilder: (context, index) { - if (!unreadMemberIsFinished && - index == unreadMemberList.length - 5) { - _getUnreadMemberList(); - } - return _memberItemBuilder( - unreadMemberList[index], theme); - }), + Row( + children: [ + Text(MessageUtils.getDisplayName(widget.messageItem)), + const SizedBox( + width: 8, + ), + Text( + TimeAgo().getTimeForMessage( + widget.messageItem.timestamp ?? 0), + softWrap: true, + style: + TextStyle(fontSize: 12, color: theme.weakTextColor), + ) + ], + ), + const SizedBox( + height: 6, + ), + _getMsgItem(widget.messageItem) ], - )), - ], - ), + ), + ), + Container( + height: 8, + color: theme.weakBackgroundColor, + ), + Row( + // direction: Axis.horizontal, + children: [ + Expanded( + flex: 1, + child: InkWell( + onTap: () { + currentIndex = 0; + setState(() {}); + }, + child: Container( + height: isDesktopScreen ? 40 : 50.0, + alignment: Alignment.bottomCenter, + padding: EdgeInsets.only(bottom: isDesktopScreen ? 8 : 12), + decoration: BoxDecoration( + color: Colors.white, + border: Border( + bottom: BorderSide( + width: 2, + color: currentIndex == 0 + ? theme.primaryColor! + : Colors.white))), + child: Text( + TIM_t_para("{{option1}}人已读", "$option1人已读")( + option1: option1), + style: TextStyle( + color: currentIndex != 0 + ? theme.weakTextColor + : Colors.black, + fontSize: isDesktopScreen ? 14 : 18, + ), + ), + ), + ), + ), + Expanded( + flex: 1, + child: InkWell( + onTap: () { + currentIndex = 1; + setState(() {}); + }, + child: Container( + alignment: Alignment.bottomCenter, + height: isDesktopScreen ? 40 : 50.0, + padding: EdgeInsets.only(bottom: isDesktopScreen ? 8 : 12), + decoration: BoxDecoration( + color: Colors.white, + border: Border( + bottom: BorderSide( + width: 2, + color: currentIndex == 1 + ? theme.primaryColor! + : Colors.white))), + child: Text( + TIM_t_para("{{option2}}人未读", "$option2人未读")( + option2: option2), + style: TextStyle( + color: currentIndex != 1 + ? theme.weakTextColor + : Colors.black, + fontSize: isDesktopScreen ? 14 : 18, + ), + ), + ), + ), + ), + ], + ), + Container( + height: 1, + decoration: BoxDecoration( + border: Border( + bottom: BorderSide( + color: theme.weakDividerColor ?? + CommonColor.weakDividerColor))), + ), + Expanded( + child: IndexedStack( + index: currentIndex, + children: [ + ListView.builder( + shrinkWrap: true, + itemCount: readMemberList.length, + itemBuilder: (context, index) { + if (!readMemberIsFinished && + index == readMemberList.length - 5) { + _getReadMemberList(); + } + return _memberItemBuilder(readMemberList[index], theme); + }), + ListView.builder( + shrinkWrap: true, + itemCount: unreadMemberList.length, + itemBuilder: (context, index) { + if (!unreadMemberIsFinished && + index == unreadMemberList.length - 5) { + _getUnreadMemberList(); + } + return _memberItemBuilder(unreadMemberList[index], theme); + }), + ], + )), + ], ), - ), - ); + ); + } + + return TUIKitScreenUtils.getDeviceWidget( + desktopWidget: pageBody(), + defaultWidget: DefaultTabController( + length: 2, + child: Scaffold( + appBar: AppBar( + title: Text( + TIM_t("消息详情"), + style: TextStyle(color: theme.appbarTextColor, fontSize: 17), + ), + shadowColor: theme.weakDividerColor, + backgroundColor: theme.appbarBgColor ?? + theme.primaryColor, + iconTheme: IconThemeData( + color: theme.appbarTextColor, + )), + body: pageBody()), + )); } } diff --git a/lib/ui/widgets/recent_conversation_list.dart b/lib/ui/widgets/recent_conversation_list.dart index 15c5d71..ac1c707 100644 --- a/lib/ui/widgets/recent_conversation_list.dart +++ b/lib/ui/widgets/recent_conversation_list.dart @@ -3,6 +3,7 @@ import 'package:provider/provider.dart'; import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_state.dart'; import 'package:tencent_cloud_chat_uikit/business_logic/view_models/tui_conversation_view_model.dart'; import 'package:tencent_cloud_chat_uikit/data_services/services_locatar.dart'; +import 'package:tencent_cloud_chat_uikit/ui/utils/screen_utils.dart'; import 'package:tencent_cloud_chat_uikit/ui/widgets/avatar.dart'; import 'package:tencent_cloud_chat_uikit/ui/widgets/az_list_view.dart'; @@ -42,6 +43,9 @@ class _RecentForwardListState extends TIMUIKitState { } Widget _buildItem(V2TimConversation conversation) { + final isDesktopScreen = + TUIKitScreenUtils.getFormFactor(context) == DeviceType.Desktop; + final faceUrl = conversation.faceUrl ?? ""; final showName = conversation.showName ?? ""; @@ -49,8 +53,8 @@ class _RecentForwardListState extends TIMUIKitState { mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ if (widget.isMultiSelect) - Padding( - padding: const EdgeInsets.only(left: 16.0), + Container( + padding: EdgeInsets.only(left: isDesktopScreen ? 8 : 16.0, top: isDesktopScreen ? 10 : 0), child: CheckBoxButton( isChecked: _selectedConversation.contains(conversation), onChanged: (value) { @@ -91,8 +95,8 @@ class _RecentForwardListState extends TIMUIKitState { child: Row( children: [ Container( - height: 40, - width: 40, + height: isDesktopScreen ? 30 : 40, + width: isDesktopScreen ? 30 : 40, margin: const EdgeInsets.only(right: 12), child: Avatar( faceUrl: faceUrl, @@ -103,15 +107,15 @@ class _RecentForwardListState extends TIMUIKitState { Expanded( child: Container( alignment: Alignment.centerLeft, - padding: const EdgeInsets.only(top: 10, bottom: 19), - decoration: const BoxDecoration( + padding: EdgeInsets.only(top: 10, bottom: isDesktopScreen ? 12 : 19), + decoration: isDesktopScreen ? null : const BoxDecoration( border: Border(bottom: BorderSide(color: Color(0xFFDBDBDB)))), child: Text( showName, // textAlign: TextAlign.center, style: - const TextStyle(color: Color(0xFF111111), fontSize: 18), + TextStyle(color: const Color(0xFF111111), fontSize: isDesktopScreen ? 16 : 18), ), )) ], @@ -122,10 +126,6 @@ class _RecentForwardListState extends TIMUIKitState { ); } - @override - void initState() { - super.initState(); - } @override void dispose() { @@ -147,12 +147,14 @@ class _RecentForwardListState extends TIMUIKitState { final recentConvList = serviceLocator().conversationList; final showList = _buildMemberList(recentConvList); + final isDesktopScreen = + TUIKitScreenUtils.getFormFactor(context) == DeviceType.Desktop; return AZListViewContainer( memberList: showList, isShowIndexBar: false, susItemBuilder: (context, index) { - return Container( + return isDesktopScreen ? Container() : Container( height: 40, width: MediaQuery.of(context).size.width, padding: const EdgeInsets.only(left: 16.0), diff --git a/lib/ui/widgets/textSize.dart b/lib/ui/widgets/textSize.dart index 8a32090..6f49560 100644 --- a/lib/ui/widgets/textSize.dart +++ b/lib/ui/widgets/textSize.dart @@ -1,3 +1,4 @@ + // ignore_for_file: file_names import 'package:flutter/material.dart'; @@ -60,16 +61,12 @@ class CustomText extends StatefulWidget { class _ExtendTextState extends State { String? text; - countTextSize() { TextStyle style = widget.style ?? const TextStyle(fontSize: 14); double textwidth = TextSize.boundingTextSize(widget.text, style).width; int offset = widget.offset ?? 3; if (textwidth > widget.width) { int position = widget.text.lastIndexOf('.'); - if(position < 0){ - position = widget.text.length; - } String overflowtext = widget.overflowtext ?? '...'; int overflowtextLength = overflowtext.length; double singTextSize = textwidth / widget.text.length; diff --git a/lib/ui/widgets/text_input_bottom_sheet.dart b/lib/ui/widgets/text_input_bottom_sheet.dart index 6389d76..76f9b0e 100644 --- a/lib/ui/widgets/text_input_bottom_sheet.dart +++ b/lib/ui/widgets/text_input_bottom_sheet.dart @@ -1,74 +1,247 @@ import 'package:flutter/material.dart'; +import 'package:tencent_cloud_chat_uikit/ui/utils/screen_utils.dart'; +import 'package:tencent_cloud_chat_uikit/ui/widgets/drag_widget.dart'; import 'package:tencent_im_base/tencent_im_base.dart'; class TextInputBottomSheet { - static showTextInputBottomSheet(BuildContext context, String title, - String tips, Function(String text) onSubmitted, TUITheme theme) { - TextEditingController _selectionController = TextEditingController(); + static OverlayEntry? entry; - showModalBottomSheet( - isScrollControlled: true, // !important - context: context, - builder: (BuildContext context) { - return SingleChildScrollView( - child: Container( - padding: EdgeInsets.only( - top: 16, - left: 16, - right: 16, - bottom: MediaQuery.of(context).viewInsets.bottom + 30, - ), - child: Column( - mainAxisAlignment: MainAxisAlignment.start, + static Widget inputBoxContent( + {required BuildContext context, + required String title, + String? tips, + required Function(String text) onSubmitted, + required TUITheme theme, + bool isShowCancel = false, + Offset? initOffset, + String? initText, + required TextEditingController selectionController}) { + final isDesktopScreen = TUIKitScreenUtils.getFormFactor(context) == DeviceType.Desktop; + selectionController.text = initText ?? ""; + return SingleChildScrollView( + child: Container( + padding: EdgeInsets.only( + top: 16, + left: 16, + right: 16, + bottom: + isDesktopScreen ? 16 : MediaQuery.of(context).viewInsets.bottom + 30, + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.only(bottom: 16), + child: Text(title, + style: + const TextStyle(fontWeight: FontWeight.w500, fontSize: 16)), + ), + Divider(height: 2, color: theme.weakDividerColor), + TextField( + + onSubmitted: (text) { + onSubmitted(text); + if (entry != null) { + entry?.remove(); + entry = null; + } else { + Navigator.pop(context); + } + }, + autofocus: true, + controller: selectionController, + ), + if(tips != null) Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + padding: const EdgeInsets.symmetric(vertical: 5), + height: 40, + child: Text( + tips, + style: const TextStyle(color: Colors.grey, fontSize: 12), + ), + ) + ], + ), + if (isDesktopScreen) + Row( + mainAxisAlignment: MainAxisAlignment.end, children: [ - Padding( - padding: const EdgeInsets.only(bottom: 16), - child: Text(title, - style: const TextStyle( - fontWeight: FontWeight.w500, fontSize: 16)), - ), - Divider(height: 2, color: theme.weakDividerColor), - TextField( - controller: _selectionController, - ), - Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Container( - padding: const EdgeInsets.symmetric(vertical: 5), - height: 40, - child: Text( - tips, - style: - const TextStyle(color: Colors.grey, fontSize: 12), - ), - ) - ], - ), + if (isShowCancel) + Container( + margin: const EdgeInsets.only(right: 20), + child: SizedBox( + width: 84, + child: ElevatedButton( + style: ButtonStyle( + backgroundColor: MaterialStateProperty.all( + theme.wideBackgroundColor), + shape: MaterialStateProperty.all( + RoundedRectangleBorder( + borderRadius: BorderRadius.circular(5))), + ), + onPressed: () { + if (entry != null) { + entry?.remove(); + entry = null; + } else { + Navigator.pop(context); + } + }, + child: Text( + TIM_t("取消"), + style: TextStyle(color: theme.darkTextColor), + )), + )), SizedBox( - width: double.infinity, + width: 84, child: ElevatedButton( style: ButtonStyle( shape: MaterialStateProperty.all(RoundedRectangleBorder( borderRadius: BorderRadius.circular(5))), ), onPressed: () { - String text = _selectionController.text; - // if (text == "") { - // _coreService.callOnCallback(TIMCallback( - // type: TIMCallbackType.INFO, - // infoRecommendText: TIM_t("输入不能为空"), - // infoCode: 6661401)); - // return; - // } + String text = selectionController.text; onSubmitted(text); - Navigator.pop(context); + if (entry != null) { + entry?.remove(); + entry = null; + } else { + Navigator.pop(context); + } }, - child: Text(TIM_t("确定"))), + child: Text(TIM_t("保存"))), ), ], ), - )); - }); + if (!isDesktopScreen) + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + if (isShowCancel) + Expanded( + child: Container( + margin: const EdgeInsets.only(right: 20), + child: ElevatedButton( + style: ButtonStyle( + backgroundColor: MaterialStateProperty.all( + theme.wideBackgroundColor), + shape: MaterialStateProperty.all( + RoundedRectangleBorder( + borderRadius: BorderRadius.circular(5))), + ), + onPressed: () { + if (entry != null) { + entry?.remove(); + entry = null; + } else { + Navigator.pop(context); + } + }, + child: Text( + TIM_t("取消"), + style: TextStyle(color: theme.darkTextColor), + )), + )), + Expanded( + child: SizedBox( + child: ElevatedButton( + style: ButtonStyle( + shape: MaterialStateProperty.all(RoundedRectangleBorder( + borderRadius: BorderRadius.circular(5))), + ), + onPressed: () { + String text = selectionController.text; + onSubmitted(text); + if (entry != null) { + entry?.remove(); + entry = null; + } else { + Navigator.pop(context); + } + }, + child: Text(TIM_t("确定"))), + )), + ], + ), + ], + ), + )); + } + + static showTextInputBottomSheet({ + required BuildContext context, + required String title, + String? tips, + required Function(String text) onSubmitted, + required TUITheme theme, + Offset? initOffset, + String? initText, + }) { + TextEditingController _selectionController = TextEditingController(); + final isDesktopScreen = TUIKitScreenUtils.getFormFactor(context) == DeviceType.Desktop; + if (isDesktopScreen) { + if (entry != null) { + return; + } + entry = OverlayEntry(builder: (BuildContext context) { + return TUIKitDragArea( + closeFun: (){ + if(entry != null){ + entry?.remove(); + entry = null; + } + }, + initOffset: initOffset ?? + Offset(MediaQuery.of(context).size.height * 0.5 + 20, + MediaQuery.of(context).size.height * 0.5 - 100), + child: Container( + decoration: BoxDecoration( + borderRadius: const BorderRadius.all(Radius.circular(8)), + color: theme.wideBackgroundColor, + border: Border.all( + width: 2, + color: theme.weakBackgroundColor ?? const Color(0xFFbebebe), + ), + boxShadow: const [ + BoxShadow( + color: Color(0xFFbebebe), + offset: Offset(5, 5), + blurRadius: 10, + spreadRadius: 1, + ), + ], + ), + child: SizedBox( + width: 350, + child: inputBoxContent( + context: context, + isShowCancel: true, + title: title, + tips: tips, + onSubmitted: onSubmitted, + theme: theme, + initText: initText, + selectionController: _selectionController), + ), + )); + }); + Overlay.of(context)?.insert(entry!); + } else { + showModalBottomSheet( + isScrollControlled: true, // !important + context: context, + builder: (BuildContext context) { + return inputBoxContent( + context: context, + title: title, + tips: tips, + initText: initText, + onSubmitted: onSubmitted, + theme: theme, + selectionController: _selectionController); + }); + } } } diff --git a/lib/ui/widgets/transimit_group_owner_select.dart b/lib/ui/widgets/transimit_group_owner_select.dart index 52f1ce3..8dbe873 100644 --- a/lib/ui/widgets/transimit_group_owner_select.dart +++ b/lib/ui/widgets/transimit_group_owner_select.dart @@ -5,34 +5,36 @@ import 'package:tencent_cloud_chat_uikit/data_services/services_locatar.dart'; import 'package:tencent_cloud_chat_uikit/tencent_cloud_chat_uikit.dart'; import 'package:tencent_cloud_chat_uikit/ui/utils/platform.dart'; +import 'package:tencent_cloud_chat_uikit/ui/utils/screen_utils.dart'; import 'package:tencent_cloud_chat_uikit/ui/views/TIMUIKitGroupProfile/widgets/tim_ui_group_member_search.dart'; import 'package:tencent_cloud_chat_uikit/ui/widgets/group_member_list.dart'; import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_base.dart'; -class SelectTransimitOwner extends StatefulWidget { +GlobalKey<_SelectNewGroupOwner> selectNewGroupOwnerKey = GlobalKey(); + +class SelectNewGroupOwner extends StatefulWidget { final String? groupID; final TUIGroupProfileModel model; - const SelectTransimitOwner({ + final ValueChanged>? onSelectedMember; + + const SelectNewGroupOwner({ this.groupID, Key? key, required this.model, + this.onSelectedMember, }) : super(key: key); @override - State createState() => _SelectCallInviterState(); + State createState() => _SelectNewGroupOwner(); } -class _SelectCallInviterState extends TIMUIKitState { +class _SelectNewGroupOwner extends TIMUIKitState { final CoreServicesImpl _coreServicesImpl = serviceLocator(); List selectedMember = []; List? searchMemberList; String? searchText; - @override - void initState() { - super.initState(); - } @override void dispose() { @@ -81,81 +83,86 @@ class _SelectCallInviterState extends TIMUIKitState { }); } + onSubmit() { + if (widget.onSelectedMember != null) { + widget.onSelectedMember!(selectedMember); + } + } + @override Widget tuiBuild(BuildContext context, TUIKitBuildValue value) { final TUITheme theme = value.theme; - return Scaffold( - appBar: AppBar( - shadowColor: theme.weakBackgroundColor, - iconTheme: const IconThemeData( - color: Colors.white, - ), - flexibleSpace: Container( - decoration: BoxDecoration( - gradient: LinearGradient(colors: [ - theme.lightPrimaryColor ?? CommonColor.lightPrimaryColor, - theme.primaryColor ?? CommonColor.primaryColor - ]), - ), - ), - leading: TextButton( - onPressed: () { - Navigator.pop(context); - }, - child: Text( - TIM_t("取消"), - style: const TextStyle( - color: Colors.white, - fontSize: 16, + Widget memberBody() { + return GroupProfileMemberList( + customTopArea: PlatformUtils().isWeb + ? null + : GroupMemberSearchTextField( + onTextChange: (text) => handleSearchGroupMembers(text, context), ), - ), - ), - actions: [ - TextButton( - onPressed: () { - if (selectedMember.isNotEmpty) { - Navigator.pop(context, selectedMember); - } - }, - child: Text( - TIM_t("完成"), - style: const TextStyle( - color: Colors.white, + memberList: (searchMemberList ?? widget.model.groupMemberList) + .where((element) => + element?.userID != _coreServicesImpl.loginInfo.userID) + .toList(), + canSlideDelete: false, + canSelectMember: true, + maxSelectNum: 1, + onSelectedMemberChange: (member) { + selectedMember = member; + setState(() {}); + }, + touchBottomCallBack: () {}, + ); + } + + return TUIKitScreenUtils.getDeviceWidget( + defaultWidget: Scaffold( + appBar: AppBar( + shadowColor: theme.weakBackgroundColor, + iconTheme: IconThemeData( + color: theme.appbarTextColor, + ), + backgroundColor: theme.appbarBgColor ?? + theme.primaryColor, + leading: TextButton( + onPressed: () { + Navigator.pop(context); + }, + child: Text( + TIM_t("取消"), + style: TextStyle( + color: theme.appbarTextColor, + fontSize: 16, + ), + ), + ), + actions: [ + TextButton( + onPressed: () { + if (selectedMember.isNotEmpty) { + Navigator.pop(context, selectedMember); + } + }, + child: Text( + TIM_t("完成"), + style: TextStyle( + color: theme.appbarTextColor, + fontSize: 16, + ), + ), + ) + ], + centerTitle: true, + leadingWidth: 100, + title: Text( + "转让群主", + style: TextStyle( + color: theme.appbarTextColor, fontSize: 16, ), ), - ) - ], - centerTitle: true, - leadingWidth: 100, - title: const Text( - "转让群主", - style: TextStyle( - color: Colors.white, - fontSize: 17, ), - ), - ), - body: GroupProfileMemberList( - customTopArea: PlatformUtils().isWeb - ? null - : GroupMemberSearchTextField( - onTextChange: (text) => - handleSearchGroupMembers(text, context), - ), - memberList: (searchMemberList ?? widget.model.groupMemberList) - .where((element) => - element?.userID != _coreServicesImpl.loginInfo.userID) - .toList(), - canSlideDelete: false, - canSelectMember: true, - maxSelectNum: 1, - onSelectedMemberChange: (member) { - selectedMember = member; - setState(() {}); - }, - touchBottomCallBack: () {}, - )); + body: memberBody()), + desktopWidget: memberBody()); } } diff --git a/lib/ui/widgets/video_screen.dart b/lib/ui/widgets/video_screen.dart index 58dcb0f..933cfd3 100644 --- a/lib/ui/widgets/video_screen.dart +++ b/lib/ui/widgets/video_screen.dart @@ -1,13 +1,13 @@ import 'dart:convert'; import 'dart:io'; import 'dart:math'; - import 'package:crypto/crypto.dart'; +import 'package:device_info_plus/device_info_plus.dart'; +import 'package:image_gallery_saver/image_gallery_saver.dart'; import 'package:tencent_im_base/tencent_im_base.dart'; import 'package:extended_image/extended_image.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import 'package:image_gallery_saver/image_gallery_saver.dart'; import 'package:permission_handler/permission_handler.dart'; import 'package:tencent_cloud_chat_uikit/business_logic/view_models/tui_chat_global_model.dart'; import 'package:tencent_cloud_chat_uikit/data_services/services_locatar.dart'; @@ -18,7 +18,6 @@ import 'package:tencent_cloud_chat_uikit/ui/utils/permission.dart'; import 'package:tencent_cloud_chat_uikit/ui/utils/platform.dart'; import 'package:tencent_cloud_chat_uikit/ui/widgets/video_custom_control.dart'; import 'package:video_player/video_player.dart'; - import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_base.dart'; class VideoScreen extends StatefulWidget { @@ -79,27 +78,37 @@ class _VideoScreenState extends TIMUIKitState { xhr.send(); return; } - if (PlatformUtils().isIOS) { - if (!await Permissions.checkPermission( - context, - Permission.photosAddOnly.value, - )) { - return; - } - } else { - if (!await Permissions.checkPermission( - context, - Permission.storage.value, - )) { - return; + if(PlatformUtils().isMobile){ + if (PlatformUtils().isIOS) { + if (!await Permissions.checkPermission( + context, + Permission.photosAddOnly.value, + )) { + return; + } + } else { + final DeviceInfoPlugin deviceInfo = DeviceInfoPlugin(); + AndroidDeviceInfo androidInfo = await deviceInfo.androidInfo; + if ((androidInfo.version.sdkInt ?? 0) >= 33) { + final videos = await Permissions.checkPermission( + context,Permission.videos.value, + ); + + if(!videos){ + return; + } + } else { + final storage = await Permissions.checkPermission( + context, Permission.storage.value, + ); + if(!storage){ + return; + } + } } } String savePath = videoUrl; if (!isAsset) { - // var appDocDir = await getTemporaryDirectory(); - // savePath = appDocDir.path + "/temp.mp4"; - // await Dio().download(videoUrl, savePath); - if (widget.message.msgID == null || widget.message.msgID!.isEmpty) { return; } @@ -296,7 +305,7 @@ class _VideoScreenState extends TIMUIKitState { widget.videoElement.localVideoUrl!, )); await player.initialize(); - WidgetsBinding.instance?.addPostFrameCallback((_) { + WidgetsBinding.instance.addPostFrameCallback((_) { double w = getVideoWidth(); double h = getVideoHeight(); ChewieController controller = ChewieController( diff --git a/lib/ui/widgets/wide_popup.dart b/lib/ui/widgets/wide_popup.dart new file mode 100644 index 0000000..e866ac0 --- /dev/null +++ b/lib/ui/widgets/wide_popup.dart @@ -0,0 +1,229 @@ +import 'package:flutter/material.dart'; +import 'package:tencent_cloud_chat_uikit/business_logic/view_models/tui_self_info_view_model.dart'; +import 'package:tencent_cloud_chat_uikit/data_services/core/tim_uikit_wide_modal_operation_key.dart'; +import 'package:tencent_cloud_chat_uikit/data_services/services_locatar.dart'; +import 'package:tencent_cloud_chat_uikit/tencent_cloud_chat_uikit.dart'; +import 'package:tencent_cloud_chat_uikit/ui/widgets/drag_widget.dart'; + +class TUIKitWidePopup { + static OverlayEntry? entry; + + static showSecondaryConfirmDialog({ + required TUIKitWideModalOperationKey operationKey, + required BuildContext context, + required String text, + required TUITheme theme, + VoidCallback? onConfirm, + VoidCallback? onCancel, + }) { + return TUIKitWidePopup.showPopupWindow( + operationKey: operationKey, + context: context, + isDarkBackground: false, + onCancel: onCancel, + onConfirm: onConfirm, + width: 350, + height: 120, + child: (onClose) => Container( + padding: const EdgeInsets.only(left: 16, right: 16), + child: Row( + children: [ + Icon(Icons.info, color: theme.primaryColor), + const SizedBox( + width: 8, + ), + Expanded(child: Text(text)) + ], + ), + )); + } + + static showPopupWindow({ + /// You could determine this field as `TUIKitWideModalOperationKey.custom` for your own business needs. + required TUIKitWideModalOperationKey operationKey, + required BuildContext context, + required Widget Function(VoidCallback closeFunc) child, + TUITheme? theme, + double? width, + double? height, + Offset? offset, + String? initText, + BorderRadius? borderRadius, + bool isDarkBackground = true, + String? title, + VoidCallback? onSubmit, + Widget? submitWidget, + VoidCallback? onConfirm, + VoidCallback? onCancel, + }) async { + + final TUISelfInfoViewModel selfInfoViewModel = + serviceLocator(); + + if(selfInfoViewModel.globalConfig?.showDesktopModalFunc != null){ + final res = await selfInfoViewModel.globalConfig!.showDesktopModalFunc!( + operationKey, + context, + child, + theme, + width, + height, + offset, + initText, + borderRadius, + isDarkBackground, + title, + onSubmit, + submitWidget, + onConfirm, + onCancel + ); + + if(res == true){ + return; + } + } + + if (entry != null) { + return; + } + entry = OverlayEntry(builder: (BuildContext context) { + return TUIKitDragArea( + backgroundColor: isDarkBackground ? const Color(0x7F000000) : null, + closeFun: () { + if (entry != null) { + entry?.remove(); + entry = null; + } + }, + initOffset: offset ?? + (width != null && height != null + ? Offset(MediaQuery.of(context).size.width * 0.5 - width / 2, + MediaQuery.of(context).size.height * 0.5 - height / 2) + : null), + child: Container( + width: width, + height: height, + decoration: BoxDecoration( + borderRadius: + borderRadius ?? const BorderRadius.all(Radius.circular(16)), + color: theme?.wideBackgroundColor ?? const Color(0xFFffffff), + border: isDarkBackground + ? Border.all( + width: 2, + color: + theme?.weakBackgroundColor ?? const Color(0xFFbebebe), + ) + : null, + boxShadow: isDarkBackground + ? null + : const [ + BoxShadow( + color: Color(0xFFbebebe), + offset: Offset(3, 3), + blurRadius: 10, + spreadRadius: 1, + ), + ], + ), + child: Column( + children: [ + if (title != null) + Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: hexToColor("f5f6f7"), + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(16), + topRight: Radius.circular(16)), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + mainAxisSize: MainAxisSize.max, + children: [ + Text( + title, + style: TextStyle( + fontSize: 18, + color: theme?.darkTextColor ?? + const Color(0xFF444444)), + ), + InkWell( + onTap: () { + if (onSubmit != null) { + onSubmit(); + } + entry?.remove(); + entry = null; + }, + child: onSubmit != null + ? (submitWidget ?? const Icon(Icons.check)) + : const Icon(Icons.close), + ) + ], + ), + ), + if (title != null) + SizedBox( + height: 1, + child: Container( + color: theme?.weakDividerColor ?? const Color(0xFFE5E6E9), + ), + ), + if (height != null && width != null) + Expanded(child: child(() { + entry?.remove(); + entry = null; + })), + if (height == null || width == null) + child(() { + entry?.remove(); + entry = null; + }), + if (onCancel != null || onConfirm != null) + Container( + padding: const EdgeInsets.only(bottom: 16), + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + if (onCancel != null) + Container( + margin: const EdgeInsets.only(right: 16), + child: OutlinedButton( + onPressed: () { + entry?.remove(); + entry = null; + onCancel(); + }, + child: Text( + TIM_t("取消"), + style: TextStyle( + color: + theme?.weakTextColor ?? Colors.black), + )), + ), + if (onConfirm != null) + Container( + margin: const EdgeInsets.only(right: 16), + child: ElevatedButton( + onPressed: () { + entry?.remove(); + entry = null; + onConfirm(); + }, + child: Text( + TIM_t("确定"), + style: TextStyle(color: theme?.primaryColor), + )), + ), + ], + ), + ) + ], + ), + )); + }); + Overlay.of(context)?.insert(entry!); + + } +} diff --git a/pubspec.lock b/pubspec.lock index 754acb8..48af976 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -5,10 +5,10 @@ packages: dependency: transitive description: name: _fe_analyzer_shared - sha256: "569ddca58d535e601dd1584afa117710abc999d036c0cd2c51777fb257df78e8" + sha256: "3444216bfd127af50bbe4862d8843ed44db946dd933554f0d7285e89f10e28ac" url: "https://pub.dev" source: hosted - version: "53.0.0" + version: "50.0.0" adaptive_action_sheet: dependency: "direct main" description: @@ -21,34 +21,90 @@ packages: dependency: transitive description: name: analyzer - sha256: "10927c4b7c7c88b1adbca278c3d5531db92e2f4b4abf04e2919a800af965f3f5" + sha256: "68796c31f510c8455a06fed75fc97d8e5ad04d324a830322ab3efc9feb6201c1" url: "https://pub.dev" source: hosted - version: "5.5.0" + version: "5.2.0" args: dependency: transitive description: name: args - sha256: "4cab82a83ffef80b262ddedf47a0a8e56ee6fbf7fe21e6e768b02792034dd440" + sha256: b003c3098049a51720352d219b0bb5f219b60fbfb68e7a4748139a06a5676515 url: "https://pub.dev" source: hosted - version: "2.4.0" + version: "2.3.1" async: dependency: transitive description: name: async - sha256: bfe67ef28df125b7dddcea62755991f807aa39a2492a23e1550161692950bbe0 + sha256: "271b8899fc99f9df4f4ed419fa14e2fff392c7b2c162fbb87b222e2e963ddc73" url: "https://pub.dev" source: hosted - version: "2.10.0" - azlistview: + version: "2.9.0" + audioplayers: dependency: "direct main" description: - name: azlistview - sha256: "93e865f11777a271b439f0d6b00799c0797e9daeec2e082a2e01373809c4b90d" + name: audioplayers + sha256: "16451eab798b23ad9307aef6f9ca62bb8fb06542af8810eead0d236d3fd40a42" + url: "https://pub.dev" + source: hosted + version: "3.0.1" + audioplayers_android: + dependency: transitive + description: + name: audioplayers_android + sha256: b2c833e6f718b6b030454e329931229afafe9327fdb002874dd544dc8bf2484d url: "https://pub.dev" source: hosted version: "2.0.0" + audioplayers_darwin: + dependency: transitive + description: + name: audioplayers_darwin + sha256: e7a3c8759bf11ecfe4b20df338bf9f3d37c7719a5761c46a3833aba0ceeaacff + url: "https://pub.dev" + source: hosted + version: "3.0.1" + audioplayers_linux: + dependency: transitive + description: + name: audioplayers_linux + sha256: e95b65e1f4d4764601dac5e65f8d8186fc29401043ab020f1dacec483d708707 + url: "https://pub.dev" + source: hosted + version: "1.0.4" + audioplayers_platform_interface: + dependency: transitive + description: + name: audioplayers_platform_interface + sha256: "178581a44cb685fd798d2108111d2e98cca3400e30b9c3a05546f124fb37f600" + url: "https://pub.dev" + source: hosted + version: "4.0.0" + audioplayers_web: + dependency: transitive + description: + name: audioplayers_web + sha256: "859ba09be2a57e57a787273f18c8cf0d9b61383870c5ee4b5632fe9adbc37edf" + url: "https://pub.dev" + source: hosted + version: "2.2.0" + audioplayers_windows: + dependency: transitive + description: + name: audioplayers_windows + sha256: "622e01c4c357c2aaf1b956c3a0f89d97c3cb40315c03f16e3b6c2a31ff9c38bc" + url: "https://pub.dev" + source: hosted + version: "1.1.3" + azlistview_all_platforms: + dependency: "direct main" + description: + name: azlistview_all_platforms + sha256: "47ce2204863e0c3e481ca2a3813096d9818b153f1f677e839503e33d36e97993" + url: "https://pub.dev" + source: hosted + version: "2.1.2" build: dependency: transitive description: @@ -69,18 +125,18 @@ packages: dependency: transitive description: name: build_daemon - sha256: "6bc5544ea6ce4428266e7ea680e945c68806c4aae2da0eb5e9ccf38df8d6acbf" + sha256: "757153e5d9cd88253cb13f28c2fb55a537dc31fefd98137549895b5beb7c6169" url: "https://pub.dev" source: hosted - version: "3.1.0" + version: "3.1.1" build_resolvers: dependency: transitive description: name: build_resolvers - sha256: db49b8609ef8c81cca2b310618c3017c00f03a92af44c04d310b907b2d692d95 + sha256: "687cf90a3951affac1bd5f9ecb5e3e90b60487f3d9cdc359bb310f8876bb02a6" url: "https://pub.dev" source: hosted - version: "2.2.0" + version: "2.0.10" build_runner: dependency: "direct dev" description: @@ -109,76 +165,76 @@ packages: dependency: transitive description: name: built_value - sha256: "169565c8ad06adb760c3645bf71f00bff161b00002cace266cad42c5d22a7725" + sha256: "31b7c748fd4b9adf8d25d72a4c4a59ef119f12876cf414f94f8af5131d5fa2b0" url: "https://pub.dev" source: hosted - version: "8.4.3" + version: "8.4.4" cached_network_image: dependency: "direct main" description: name: cached_network_image - sha256: fd3d0dc1d451f9a252b32d95d3f0c3c487bc41a75eba2e6097cb0b9c71491b15 + sha256: e764e48ef036cabdf84319ba7b8b5871b6b43266e14de787cb43f77639089ae5 url: "https://pub.dev" source: hosted - version: "3.2.3" + version: "3.2.1" cached_network_image_platform_interface: dependency: transitive description: name: cached_network_image_platform_interface - sha256: bb2b8403b4ccdc60ef5f25c70dead1f3d32d24b9d6117cfc087f496b178594a7 + sha256: "8e2b5befefec5063bee8f209fda21751f6328d405d4237c70f21104568b2fee7" url: "https://pub.dev" source: hosted - version: "2.0.0" + version: "1.0.0" cached_network_image_web: dependency: transitive description: name: cached_network_image_web - sha256: b8eb814ebfcb4dea049680f8c1ffb2df399e4d03bf7a352c775e26fa06e02fa0 + sha256: d4351c7eb16767df129b0474a5ebc4e028870379c063e8ba265a56aa00831e70 url: "https://pub.dev" source: hosted - version: "1.0.2" + version: "1.0.1" camera: dependency: transitive description: name: camera - sha256: e7ac55af24a890d20276821eb3c95857074d03b7de6f9892b99a205ee30bd179 + sha256: ad1c53c554a2f3e5708f3b01eb738d60b902bb61f7f4ad420c65c715e65a7379 url: "https://pub.dev" source: hosted - version: "0.10.3" + version: "0.10.3+2" camera_android: dependency: transitive description: name: camera_android - sha256: e491c836147f60dd8a54cf3895fd2960e13b21b78a9d15b563a1b6c70894f142 + sha256: "16e46b32915fcbc53afc1f96ca868cd91495608935a20bd16f47b854bfed9b17" url: "https://pub.dev" source: hosted - version: "0.10.4" + version: "0.10.4+3" camera_avfoundation: dependency: transitive description: name: camera_avfoundation - sha256: "6a68c20593d4cd58974d555f74a48b244f9db28cc9156de57781122d11b8754b" + sha256: c328d477e5af88b3edcf277183c3b68f6e73b0f4e4e1053a22026e1c175e5bdb url: "https://pub.dev" source: hosted - version: "0.9.11" + version: "0.9.11+1" camera_platform_interface: dependency: transitive description: name: camera_platform_interface - sha256: b632be28e61d00a233f67d98ea90fd7041956f27a1c65500188ee459be60e15f + sha256: "00d972adee2e8a282b4d7445e8e694aa1dc0c36b70455b99afa96fbf5e814119" url: "https://pub.dev" source: hosted - version: "2.4.0" + version: "2.4.1" camera_web: dependency: transitive description: name: camera_web - sha256: "496de93c5d462738ce991dbfe91fb19026f115ed36406700a20a380fb0018299" + sha256: d77965f32479ee6d8f48205dcf10f845d7210595c6c00faa51eab265d1cae993 url: "https://pub.dev" source: hosted - version: "0.3.1+1" + version: "0.3.1+3" characters: - dependency: "direct main" + dependency: transitive description: name: characters sha256: e6a326c8af69605aec75ed6c187d06b349707a27fbff8222ca9cc2cff167975c @@ -197,10 +253,10 @@ packages: dependency: transitive description: name: checked_yaml - sha256: "3d1505d91afa809d177efd4eed5bb0eb65805097a1463abdd2add076effae311" + sha256: dd007e4fb8270916820a0d66e24f619266b60773cddd082c6439341645af2659 url: "https://pub.dev" source: hosted - version: "2.0.2" + version: "2.0.1" chewie: dependency: "direct main" description: @@ -237,12 +293,12 @@ packages: dependency: transitive description: name: convert - sha256: "0f08b14755d163f6e2134cb58222dd25ea2a2ee8a195e53983d57c075324d592" + sha256: "1be13198012c1d5bc042dc40ad1d7f16cbd522350984c0c1abf471d6d7e305c6" url: "https://pub.dev" source: hosted - version: "3.1.1" + version: "3.1.0" cross_file: - dependency: transitive + dependency: "direct main" description: name: cross_file sha256: "0b0036e8cccbfbe0555fd83c1d31a6f30b77a96b598b35a5d36dd41f718695e9" @@ -285,10 +341,74 @@ packages: dependency: transitive description: name: dart_style - sha256: "7a03456c3490394c8e7665890333e91ae8a49be43542b616e414449ac358acd4" + sha256: "5be16bf1707658e4c03078d4a9b90208ded217fb02c163e207d334082412f2fb" url: "https://pub.dev" source: hosted - version: "2.2.4" + version: "2.2.5" + desktop_drop: + dependency: "direct main" + description: + name: desktop_drop + sha256: "4ca4d960f4b11c032e9adfd2a0a8ac615bc3fddb4cbe73dcf840dd8077582186" + url: "https://pub.dev" + source: hosted + version: "0.4.1" + device_info_plus: + dependency: "direct main" + description: + name: device_info_plus + sha256: b809c4ed5f7fcdb325ccc70b80ad934677dc4e2aa414bf46859a42bfdfafcbb6 + url: "https://pub.dev" + source: hosted + version: "4.1.3" + device_info_plus_linux: + dependency: transitive + description: + name: device_info_plus_linux + sha256: "77a8b3c4af06bc46507f89304d9f49dfc64b4ae004b994532ed23b34adeae4b3" + url: "https://pub.dev" + source: hosted + version: "3.0.0" + device_info_plus_macos: + dependency: transitive + description: + name: device_info_plus_macos + sha256: "37961762fbd46d3620c7b69ca606671014db55fc1b7a11e696fd90ed2e8fe03d" + url: "https://pub.dev" + source: hosted + version: "3.0.0" + device_info_plus_platform_interface: + dependency: transitive + description: + name: device_info_plus_platform_interface + sha256: "83fdba24fcf6846d3b10f10dfdc8b6c6d7ada5f8ed21d62ea2909c2dfa043773" + url: "https://pub.dev" + source: hosted + version: "3.0.0" + device_info_plus_web: + dependency: transitive + description: + name: device_info_plus_web + sha256: "5890f6094df108181c7a29720bc23d0fd6159f17d82787fac093d1fefcaf6325" + url: "https://pub.dev" + source: hosted + version: "3.0.0" + device_info_plus_windows: + dependency: transitive + description: + name: device_info_plus_windows + sha256: "23a2874af0e23ee6e3a2a0ebcecec3a9da13241f2cb93a93a44c8764df123dd7" + url: "https://pub.dev" + source: hosted + version: "4.1.0" + diff_match_patch: + dependency: "direct main" + description: + name: diff_match_patch + sha256: "2efc9e6e8f449d0abe15be240e2c2a3bcd977c8d126cfd70598aee60af35c0a4" + url: "https://pub.dev" + source: hosted + version: "0.4.1" disk_space: dependency: "direct main" description: @@ -309,18 +429,18 @@ packages: dependency: "direct main" description: name: extended_image - sha256: a6b738d9b8d5513be72c545cc3e9c5c451fbee77c8db3cbec7c32ae85b82fb93 + sha256: a417f57e0434fc739c435e456ceace53753304157e85f77acaa680b266d4a7d8 url: "https://pub.dev" source: hosted - version: "6.4.1" + version: "6.2.2" extended_image_library: dependency: transitive description: name: extended_image_library - sha256: b1de389378589e4dffb3564d782373238f19064037458092c49b3043b2791b2b + sha256: "83285a9286db7d96154dc427d9c56cf00fb5931a2ce369efb707804d7461a896" url: "https://pub.dev" source: hosted - version: "3.4.1" + version: "3.3.0" fast_i18n: dependency: "direct dev" description: @@ -329,14 +449,22 @@ packages: url: "https://pub.dev" source: hosted version: "5.12.6" + fc_native_video_thumbnail_for_us: + dependency: "direct main" + description: + name: fc_native_video_thumbnail_for_us + sha256: db6fa2998195ef5eadac690ae58d6a909ddb5b0283ebbbf9ae4e0e8f99a54902 + url: "https://pub.dev" + source: hosted + version: "0.4.8+1" ffi: dependency: transitive description: name: ffi - sha256: "13a6ccf6a459a125b3fcdb6ec73bd5ff90822e071207c663bfd1f70062d51d18" + sha256: a38574032c5f1dd06c4aee541789906c12ccaab8ba01446e800d9c5b79c4a978 url: "https://pub.dev" source: hosted - version: "1.2.1" + version: "2.0.1" file: dependency: transitive description: @@ -349,10 +477,10 @@ packages: dependency: "direct main" description: name: file_picker - sha256: "704259669b5e9cb24e15c11cfcf02caf5f20d30901b3916d60b6d1c2d647035f" + sha256: dcde5ad1a0cebcf3715ea3f24d0db1888bf77027a26c77d7779e8ef63b8ade62 url: "https://pub.dev" source: hosted - version: "4.6.1" + version: "5.2.9" file_utils: dependency: transitive description: @@ -365,10 +493,10 @@ packages: dependency: transitive description: name: fixnum - sha256: "25517a4deb0c03aa0f32fd12db525856438902d9c16536311e76cdc57b31d7d1" + sha256: "04be3e934c52e082558cc9ee21f42f5c1cd7a1262f4c63cd0357c08d5bba81ec" url: "https://pub.dev" source: hosted - version: "1.1.0" + version: "1.0.1" flutter: dependency: "direct main" description: flutter @@ -414,11 +542,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.4" - flutter_localizations: - dependency: "direct main" - description: flutter - source: sdk - version: "0.0.0" flutter_markdown: dependency: "direct main" description: @@ -431,10 +554,10 @@ packages: dependency: transitive description: name: flutter_plugin_android_lifecycle - sha256: "60fc7b78455b94e6de2333d2f95196d32cf5c22f4b0b0520a628804cb463503b" + sha256: c224ac897bed083dabf11f238dd11a239809b446740be0c2044608c50029ffdf url: "https://pub.dev" source: hosted - version: "2.0.7" + version: "2.0.9" flutter_plugin_record_plus: dependency: "direct main" description: @@ -451,14 +574,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.4.0" - flutter_spinkit: - dependency: "direct main" - description: - name: flutter_spinkit - sha256: "77a2117c0517ff909221f3160b8eb20052ab5216107581168af574ac1f05dff8" - url: "https://pub.dev" - source: hosted - version: "5.1.0" flutter_svg: dependency: "direct main" description: @@ -516,10 +631,10 @@ packages: dependency: transitive description: name: html - sha256: d9793e10dbe0e6c364f4c59bf3e01fb33a9b2a674bc7a1081693dba0614b6269 + sha256: "79d498e6d6761925a34ee5ea8fa6dfef38607781d2fa91e37523474282af55cb" url: "https://pub.dev" source: hosted - version: "0.15.1" + version: "0.15.2" http: dependency: "direct main" description: @@ -564,42 +679,42 @@ packages: dependency: "direct main" description: name: image_picker - sha256: d39cc12402dab8365fe5b5370e64694ae0223d675c36b15ff0490b7cc3d32551 + sha256: "64b21d9f0e065f9ab0e4dde458076226c97382cc0c6949144cb874c62bf8e9f8" url: "https://pub.dev" source: hosted - version: "0.8.6+2" + version: "0.8.7" image_picker_android: dependency: transitive description: name: image_picker_android - sha256: "385f12ee9c7288575572c7873a332016ec45ebd092e1c2f6bd421b4a9ad21f1d" + sha256: "420ed22d2c9ce767ed96df723aaebfeb20ce92dfda8665cd2ba72d72a51ae669" url: "https://pub.dev" source: hosted - version: "0.8.5+6" + version: "0.8.6+1" image_picker_for_web: dependency: transitive description: name: image_picker_for_web - sha256: "7d319fb74955ca46d9bf7011497860e3923bb67feebcf068f489311065863899" + sha256: "98f50d6b9f294c8ba35e25cc0d13b04bfddd25dbc8d32fa9d566a6572f2c081c" url: "https://pub.dev" source: hosted - version: "2.1.10" + version: "2.1.12" image_picker_ios: dependency: transitive description: name: image_picker_ios - sha256: "884ed71165bc01ffe1b0b7813e6fa17e1e9442da974656f99b79a292371303d6" + sha256: "39aa70b5f1e5e7c94585b9738632d5fdb764a5655e40cd9e7b95fbd2fc50c519" url: "https://pub.dev" source: hosted - version: "0.8.6+8" + version: "0.8.6+9" image_picker_platform_interface: dependency: transitive description: name: image_picker_platform_interface - sha256: "7cef2f28f4f2fef99180f636c3d446b4ccbafd6ba0fad2adc9a80c4040f656b8" + sha256: "1991219d9dbc42a99aff77e663af8ca51ced592cd6685c9485e3458302d3d4f8" url: "https://pub.dev" source: hosted - version: "2.6.2" + version: "2.6.3" intl: dependency: "direct main" description: @@ -617,7 +732,7 @@ packages: source: hosted version: "1.0.4" js: - dependency: "direct main" + dependency: transitive description: name: js sha256: "5528c2f391ededb7775ec1daa69e65a2d61276f7552de2b5f7b8d34ee9fd4ab7" @@ -625,13 +740,13 @@ packages: source: hosted version: "0.6.5" json_annotation: - dependency: "direct main" + dependency: transitive description: name: json_annotation - sha256: c33da08e136c3df0190bd5bbe51ae1df4a7d96e7954d1d7249fea2968a72d317 + sha256: "3520fa844009431b5d4491a5a778603520cdc399ab3406332dcc50f93547258c" url: "https://pub.dev" source: hosted - version: "4.8.0" + version: "4.7.0" link_preview_generator: dependency: "direct main" description: @@ -660,10 +775,10 @@ packages: dependency: transitive description: name: logging - sha256: "04094f2eb032cbb06c6f6e8d3607edcfcb0455e2bb6cbc010cb01171dcb64e6d" + sha256: c0bbfe94d46aedf9b8b3e695cf3bd48c8e14b35e3b2c639e0aa7755d589ba946 url: "https://pub.dev" source: hosted - version: "1.1.1" + version: "1.1.0" lpinyin: dependency: "direct main" description: @@ -684,10 +799,10 @@ packages: dependency: transitive description: name: matcher - sha256: c94db23593b89766cda57aab9ac311e3616cf87c6fa4e9749df032f66f30dcb8 + sha256: "80c2989398773fa06e2457e9ff08580f24e9858b28462a722241cb53e5613478" url: "https://pub.dev" source: hosted - version: "0.12.14" + version: "0.12.12" material_color_utilities: dependency: transitive description: @@ -708,10 +823,10 @@ packages: dependency: transitive description: name: mime - sha256: e4ff8e8564c03f255408decd16e7899da1733852a9110a58fe6d1b817684a63e + sha256: "52e38f7e1143ef39daf532117d6b8f8f617bf4bcd6044ed8c29040d20d269630" url: "https://pub.dev" source: hosted - version: "1.0.4" + version: "1.0.3" nested: dependency: transitive description: @@ -740,10 +855,10 @@ packages: dependency: "direct main" description: name: package_info_plus - sha256: "7a6114becdf042be2b0777d77ace954d4a205644171a1cbd8712976b9bbb837c" + sha256: f62d7253edc197fe3c88d7c2ddab82d68f555e778d55390ccc3537eca8e8d637 url: "https://pub.dev" source: hosted - version: "1.4.2" + version: "1.4.3+1" package_info_plus_linux: dependency: transitive description: @@ -780,12 +895,20 @@ packages: dependency: transitive description: name: package_info_plus_windows - sha256: a6040b8695c82f8dd3c3d4dfab7ef96fbe9c67aac21b9bcf5db272589ef84441 + sha256: "79524f11c42dd9078b96d797b3cf79c0a2883a50c4920dc43da8562c115089bc" url: "https://pub.dev" source: hosted - version: "1.0.5" + version: "2.1.0" + pasteboard: + dependency: "direct main" + description: + name: pasteboard + sha256: "1c8b6a8b3f1d12e55d4e9404433cda1b4abe66db6b17bc2d2fb5965772c04674" + url: "https://pub.dev" + source: hosted + version: "0.2.0" path: - dependency: transitive + dependency: "direct main" description: name: path sha256: db9d4f58c908a4ba5953fcee2ae317c94889433e5024c27ce74a37f94267945b @@ -812,50 +935,50 @@ packages: dependency: "direct main" description: name: path_provider - sha256: dcea5feb97d8abf90cab9e9030b497fb7c3cbf26b7a1fe9e3ef7dcb0a1ddec95 + sha256: "04890b994ee89bfa80bf3080bfec40d5a92c5c7a785ebb02c13084a099d2b6f9" url: "https://pub.dev" source: hosted - version: "2.0.12" + version: "2.0.13" path_provider_android: dependency: transitive description: name: path_provider_android - sha256: a776c088d671b27f6e3aa8881d64b87b3e80201c64e8869b811325de7a76c15e + sha256: "019f18c9c10ae370b08dce1f3e3b73bc9f58e7f087bb5e921f06529438ac0ae7" url: "https://pub.dev" source: hosted - version: "2.0.22" + version: "2.0.24" path_provider_foundation: dependency: transitive description: name: path_provider_foundation - sha256: "62a68e7e1c6c459f9289859e2fae58290c981ce21d1697faf54910fe1faa4c74" + sha256: "026b97a6c29da75181a37aae2eba9227f5fe13cb2838c6b975ce209328b8ab4e" url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.1.3" path_provider_linux: dependency: transitive description: name: path_provider_linux - sha256: "2e32f1640f07caef0d3cb993680f181c79e54a3827b997d5ee221490d131fbd9" + sha256: "2ae08f2216225427e64ad224a24354221c2c7907e448e6e0e8b57b1eb9f10ad1" url: "https://pub.dev" source: hosted - version: "2.1.8" + version: "2.1.10" path_provider_platform_interface: dependency: transitive description: name: path_provider_platform_interface - sha256: f0abc8ebd7253741f05488b4813d936b4d07c6bae3e86148a09e342ee4b08e76 + sha256: "57585299a729335f1298b43245842678cb9f43a6310351b18fb577d6e33165ec" url: "https://pub.dev" source: hosted - version: "2.0.5" + version: "2.0.6" path_provider_windows: dependency: transitive description: name: path_provider_windows - sha256: a34ecd7fb548f8e57321fd8e50d865d266941b54e6c3b7758cf8f37c24116905 + sha256: f53720498d5a543f9607db4b0e997c4b5438884de25b0f73098cc2671a51b130 url: "https://pub.dev" source: hosted - version: "2.0.7" + version: "2.1.5" pedantic: dependency: transitive description: @@ -908,10 +1031,10 @@ packages: dependency: transitive description: name: petitparser - sha256: "49392a45ced973e8d94a85fdb21293fbb40ba805fc49f2965101ae748a3683b4" + sha256: "2ebb289dc4764ec397f5cd3ca9881c6d17196130a7d646ed022a0dd9c2e25a71" url: "https://pub.dev" source: hosted - version: "5.1.0" + version: "5.0.0" photo_manager: dependency: transitive description: @@ -920,14 +1043,6 @@ packages: url: "https://pub.dev" source: hosted version: "2.5.2" - photo_view: - dependency: "direct main" - description: - name: photo_view - sha256: "8036802a00bae2a78fc197af8a158e3e2f7b500561ed23b4c458107685e645bb" - url: "https://pub.dev" - source: hosted - version: "0.14.0" platform: dependency: transitive description: @@ -940,10 +1055,10 @@ packages: dependency: transitive description: name: plugin_platform_interface - sha256: dbf0f707c78beedc9200146ad3cb0ab4d5da13c246336987be6940f026500d3a + sha256: "6a2128648c854906c53fa8e33986fc0247a1116122f9534dd20e3ab9e16a32bc" url: "https://pub.dev" source: hosted - version: "2.1.3" + version: "2.1.4" pool: dependency: transitive description: @@ -1008,70 +1123,70 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.1" - scrollable_positioned_list: + scrollable_positioned_list_for_us: dependency: transitive description: - name: scrollable_positioned_list - sha256: "9566352ab9ba05794ee6c8864f154afba5d36c5637d0e3e32c615ba4ceb92772" + name: scrollable_positioned_list_for_us + sha256: b5bcbb35114902c004a4f98f2dbd5b0a5a7f80a0144a8b46297601e38fa5383d url: "https://pub.dev" source: hosted - version: "0.2.3" + version: "0.4.2" shared_preferences: dependency: "direct main" description: name: shared_preferences - sha256: "5949029e70abe87f75cfe59d17bf5c397619c4b74a099b10116baeb34786fad9" + sha256: ee6257848f822b8481691f20c3e6d2bfee2e9eccb2a3d249907fcfb198c55b41 url: "https://pub.dev" source: hosted - version: "2.0.17" + version: "2.0.18" shared_preferences_android: dependency: transitive description: name: shared_preferences_android - sha256: "955e9736a12ba776bdd261cf030232b30eadfcd9c79b32a3250dd4a494e8c8f7" + sha256: ad423a80fe7b4e48b50d6111b3ea1027af0e959e49d485712e134863d9c1c521 url: "https://pub.dev" source: hosted - version: "2.0.15" + version: "2.0.17" shared_preferences_foundation: dependency: transitive description: name: shared_preferences_foundation - sha256: "2b55c18636a4edc529fa5cd44c03d3f3100c00513f518c5127c951978efcccd0" + sha256: "1e755f8583229f185cfca61b1d80fb2344c9d660e1c69ede5450d8f478fa5310" url: "https://pub.dev" source: hosted - version: "2.1.3" + version: "2.1.5" shared_preferences_linux: dependency: transitive description: name: shared_preferences_linux - sha256: f8ea038aa6da37090093974ebdcf4397010605fd2ff65c37a66f9d28394cb874 + sha256: "3a59ed10890a8409ad0faad7bb2957dab4b92b8fbe553257b05d30ed8af2c707" url: "https://pub.dev" source: hosted - version: "2.1.3" + version: "2.1.5" shared_preferences_platform_interface: dependency: transitive description: name: shared_preferences_platform_interface - sha256: da9431745ede5ece47bc26d5d73a9d3c6936ef6945c101a5aca46f62e52c1cf3 + sha256: "824bfd02713e37603b2bdade0842e47d56e7db32b1dcdd1cae533fb88e2913fc" url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.1.1" shared_preferences_web: dependency: transitive description: name: shared_preferences_web - sha256: a4b5bc37fe1b368bbc81f953197d55e12f49d0296e7e412dfe2d2d77d6929958 + sha256: "0dc2633f215a3d4aa3184c9b2c5766f4711e4e5a6b256e62aafee41f89f1bfb8" url: "https://pub.dev" source: hosted - version: "2.0.4" + version: "2.0.6" shared_preferences_windows: dependency: transitive description: name: shared_preferences_windows - sha256: "5eaf05ae77658d3521d0e993ede1af962d4b326cd2153d312df716dc250f00c9" + sha256: "71bcd669bb9cdb6b39f22c4a7728b6d49e934f6cba73157ffa5a54f1eed67436" url: "https://pub.dev" source: hosted - version: "2.1.3" + version: "2.1.5" shelf: dependency: transitive description: @@ -1113,26 +1228,26 @@ packages: dependency: transitive description: name: sqflite - sha256: "78324387dc81df14f78df06019175a86a2ee0437624166c382e145d0a7fd9a4f" + sha256: b3a8307b9519af28518e271e548594bdc435225fc77e8fb22e71a296c69281cf url: "https://pub.dev" source: hosted - version: "2.2.4+1" + version: "2.0.3+1" sqflite_common: dependency: transitive description: name: sqflite_common - sha256: bfd6973aaeeb93475bc0d875ac9aefddf7965ef22ce09790eb963992ffc5183f + sha256: "4aa3b917d48f9a3161419efa4a901e33a38721bf9a7465e7e07d2d6ef8895276" url: "https://pub.dev" source: hosted - version: "2.4.2+2" + version: "2.4.0" stack_trace: dependency: transitive description: name: stack_trace - sha256: c3c7d8edb15bee7f0f74debd4b9c5f3c2ea86766fe4178eb2a18eb30a0bdaed5 + sha256: f8d9f247e2f9f90e32d1495ff32dac7e4ae34ffa7194c5ff8fcc0fd0e52df774 url: "https://pub.dev" source: hosted - version: "1.11.0" + version: "1.10.0" stream_channel: dependency: transitive description: @@ -1153,18 +1268,18 @@ packages: dependency: transitive description: name: string_scanner - sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" + sha256: "862015c5db1f3f3c4ea3b94dc2490363a84262994b88902315ed74be1155612f" url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "1.1.1" synchronized: dependency: transitive description: name: synchronized - sha256: "33b31b6beb98100bf9add464a36a8dd03eb10c7a8cf15aeec535e9b054aaf04b" + sha256: "7b530acd9cb7c71b0019a1e7fa22c4105e675557a4400b6a401c71c5e0ade1ac" url: "https://pub.dev" source: hosted - version: "3.0.1" + version: "3.0.0+3" system_info2: dependency: transitive description: @@ -1177,26 +1292,34 @@ packages: dependency: transitive description: name: tencent_cloud_chat_sdk - sha256: "32b7e40c5a1682b3ee85daa73504af3d0d4e60a93472b279591e769b15b4861c" + sha256: "765a93262a41080e155ce5b8a6ca20147a81c7d306f7f87444077c5eaae87e08" url: "https://pub.dev" source: hosted - version: "5.0.9" + version: "5.1.5" + tencent_cloud_uikit_core: + dependency: "direct main" + description: + name: tencent_cloud_uikit_core + sha256: "829dfde0c4fbdae019ba233f7f2c299e7cbd18c3ae20ecfe3ab4a43084a33064" + url: "https://pub.dev" + source: hosted + version: "1.0.2" tencent_extended_text: dependency: "direct main" description: name: tencent_extended_text - sha256: cf0d283c01a9e63f75666d8b5b1cabd463e18e51802bf1d093d7a65bd369b3d4 + sha256: "2904d064eeb9d3395f7d31bdc9f168ee75e7783f20161b1a6eb4647677a56721" url: "https://pub.dev" source: hosted - version: "1.0.2+1" + version: "1.0.0" tencent_extended_text_field: dependency: "direct main" description: name: tencent_extended_text_field - sha256: daa10f3775bfac1cc841b34275c2746ced7764f3b77222a93edb4c13bad1209b + sha256: "4bb5bb3863792b7cee48d76cd100b0084906baa2bf4e1a917283f5de62076b0b" url: "https://pub.dev" source: hosted - version: "1.0.1+2" + version: "1.0.0" tencent_extended_text_library: dependency: transitive description: @@ -1209,18 +1332,18 @@ packages: dependency: "direct main" description: name: tencent_im_base - sha256: b5917ff0bae7c31d52f54932349fc400d3752719a1c5a2f0209258f85c7a6c07 + sha256: "516356a80f43b94a6c0719b54e4c641cb1f164830b2b3e887d175ae862ebab3f" url: "https://pub.dev" source: hosted - version: "1.0.26" + version: "1.0.51" tencent_im_sdk_plugin_platform_interface: dependency: transitive description: name: tencent_im_sdk_plugin_platform_interface - sha256: "7aff6a8495eae3efc3aed1ed944bab7a0ce7eb1035d09b3f3d7974bcb3d8b137" + sha256: "04043582f1af698b4abe12d53cd0f043466228fae712677688988d8ff7bfc1f1" url: "https://pub.dev" source: hosted - version: "0.3.12" + version: "0.3.19" tencent_keyboard_visibility: dependency: "direct main" description: @@ -1233,10 +1356,10 @@ packages: dependency: "direct main" description: name: tencent_open_file - sha256: "98cbffe55e3245a308fbf76e18c9a0f808e534c624bc1a1cc0a00bd63a418290" + sha256: "01f94f618da42e5593bbad0657fcd13cfc1c2360cca805d8cdfefe898cbe5429" url: "https://pub.dev" source: hosted - version: "4.0.9+1" + version: "4.0.10" tencent_super_tooltip: dependency: "direct main" description: @@ -1265,10 +1388,10 @@ packages: dependency: "direct main" description: name: tim_ui_kit_sticker_plugin - sha256: cd5d6e2380deaf73e420602bbfe3da36c0f3253adc531f76e139efec80671175 + sha256: "2a825d33076f319f6c1c87d58e2b0d650c9284ae4afd8efdc206f3e6f3582e64" url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "2.0.1" timing: dependency: transitive description: @@ -1305,10 +1428,10 @@ packages: dependency: "direct main" description: name: universal_html - sha256: b5061c64c7c863c12e46279e032976f1c274f927fb3589b52b5928dcd2d52f7c + sha256: ed4f24120c9b1b4721d44e439f7a47d09d9f1b7b868bc84c9d6d373a4a8732af url: "https://pub.dev" source: hosted - version: "2.0.9" + version: "2.2.1" universal_io: dependency: transitive description: @@ -1321,66 +1444,66 @@ packages: dependency: "direct main" description: name: url_launcher - sha256: e8f2efc804810c0f2f5b485f49e7942179f56eabcfe81dce3387fec4bb55876b + sha256: "75f2846facd11168d007529d6cd8fcb2b750186bea046af9711f10b907e1587e" url: "https://pub.dev" source: hosted - version: "6.1.9" + version: "6.1.10" url_launcher_android: dependency: transitive description: name: url_launcher_android - sha256: "3e2f6dfd2c7d9cd123296cab8ef66cfc2c1a13f5845f42c7a0f365690a8a7dd1" + sha256: "845530e5e05db5500c1a4c1446785d60cbd8f9bd45e21e7dd643a3273bb4bbd1" url: "https://pub.dev" source: hosted - version: "6.0.23" + version: "6.0.25" url_launcher_ios: dependency: transitive description: name: url_launcher_ios - sha256: "0a5af0aefdd8cf820dd739886efb1637f1f24489900204f50984634c07a54815" + sha256: bb328b24d3bccc20bdf1024a0990ac4f869d57663660de9c936fb8c043edefe3 url: "https://pub.dev" source: hosted - version: "6.1.0" + version: "6.0.18" url_launcher_linux: dependency: transitive description: name: url_launcher_linux - sha256: "318c42cba924e18180c029be69caf0a1a710191b9ec49bb42b5998fdcccee3cc" + sha256: "206fb8334a700ef7754d6a9ed119e7349bc830448098f21a69bf1b4ed038cabc" url: "https://pub.dev" source: hosted - version: "3.0.2" + version: "3.0.4" url_launcher_macos: dependency: transitive description: name: url_launcher_macos - sha256: "41988b55570df53b3dd2a7fc90c76756a963de6a8c5f8e113330cb35992e2094" + sha256: "0ef2b4f97942a16523e51256b799e9aa1843da6c60c55eefbfa9dbc2dcb8331a" url: "https://pub.dev" source: hosted - version: "3.0.2" + version: "3.0.4" url_launcher_platform_interface: dependency: transitive description: name: url_launcher_platform_interface - sha256: "4eae912628763eb48fc214522e58e942fd16ce195407dbf45638239523c759a6" + sha256: "6c9ca697a5ae218ce56cece69d46128169a58aa8653c1b01d26fcd4aad8c4370" url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.1.2" url_launcher_web: dependency: transitive description: name: url_launcher_web - sha256: "44d79408ce9f07052095ef1f9a693c258d6373dc3944249374e30eff7219ccb0" + sha256: "81fe91b6c4f84f222d186a9d23c73157dc4c8e1c71489c4d08be1ad3b228f1aa" url: "https://pub.dev" source: hosted - version: "2.0.14" + version: "2.0.16" url_launcher_windows: dependency: transitive description: name: url_launcher_windows - sha256: b6217370f8eb1fd85c8890c539f5a639a01ab209a36db82c921ebeacefc7a615 + sha256: a83ba3607a507758669cfafb03f9de09bf6e6280c14d9b9cb18f013e406dcacd url: "https://pub.dev" source: hosted - version: "3.0.3" + version: "3.0.5" uuid: dependency: "direct main" description: @@ -1401,50 +1524,42 @@ packages: dependency: "direct main" description: name: video_player - sha256: "59f7f31c919c59cbedd37c617317045f5f650dc0eeb568b0b0de9a36472bdb28" + sha256: "868a139229acb5018d22aded3eb9cb4767ff43a8216573c086b6c535a4957481" url: "https://pub.dev" source: hosted - version: "2.5.1" + version: "2.6.0" video_player_android: dependency: transitive description: name: video_player_android - sha256: "984388511230bac63feb53b2911a70e829fe0976b6b2213f5c579c4e0a882db3" + sha256: dc31c60ae591aa3ccb2d460c3690ceceb86cbb826e73428d817a37650dc4e229 url: "https://pub.dev" source: hosted - version: "2.3.10" + version: "2.4.2" video_player_avfoundation: dependency: transitive description: name: video_player_avfoundation - sha256: d9f7a46d6a77680adb03ec05a381025d6e890ebe636637c6c3014cc3926b97e9 + sha256: "5df5411ff9d316f1dcbfee284e9838aa686e314f2a722b15c02cb7ce40ef9446" url: "https://pub.dev" source: hosted - version: "2.3.8" + version: "2.3.9" video_player_platform_interface: dependency: transitive description: name: video_player_platform_interface - sha256: "42bb75de5e9b79e1f20f1d95f688fac0f95beac4d89c6eb2cd421724d4432dae" + sha256: "72ba04ad0eff76123c6d782ac46621cb8be476a89c33c89173fce982b6ec049b" url: "https://pub.dev" source: hosted - version: "6.0.1" + version: "6.0.2" video_player_web: dependency: transitive description: name: video_player_web - sha256: b649b07b8f8f553bee4a97a0a53d0fe78a70b115eafaf0105b612b32b05ddb99 + sha256: fb3bbeaf0302cb0c31340ebd6075487939aa1fe3b379d1a8784ef852b679940e url: "https://pub.dev" source: hosted - version: "2.0.13" - video_thumbnail: - dependency: "direct main" - description: - name: video_thumbnail - sha256: "3455c189d3f0bb4e3fc2236475aa84fe598b9b2d0e08f43b9761f5bc44210016" - url: "https://pub.dev" - source: hosted - version: "0.5.3" + version: "2.0.15" wakelock: dependency: transitive description: @@ -1481,10 +1596,10 @@ packages: dependency: transitive description: name: wakelock_windows - sha256: "108b1b73711f1664ee462e73af34a9286ff496e27d4d8371e2fb4da8fde4cdac" + sha256: "857f77b3fe6ae82dd045455baa626bc4b93cb9bb6c86bf3f27c182167c3a5567" url: "https://pub.dev" source: hosted - version: "0.2.0" + version: "0.2.1" watcher: dependency: transitive description: @@ -1513,10 +1628,10 @@ packages: dependency: transitive description: name: win32 - sha256: c0e3a4f7be7dae51d8f152230b86627e3397c1ba8c3fa58e63d44a9f3edc9cef + sha256: a6f0236dbda0f63aa9a25ad1ff9a9d8a4eaaa5012da0dc59d21afdb1dc361ca4 url: "https://pub.dev" source: hosted - version: "2.6.1" + version: "3.1.4" xdg_directories: dependency: transitive description: @@ -1529,10 +1644,10 @@ packages: dependency: transitive description: name: xml - sha256: "979ee37d622dec6365e2efa4d906c37470995871fe9ae080d967e192d88286b5" + sha256: ac0e3f4bf00ba2708c33fbabbbe766300e509f8c82dbd4ab6525039813f7e2fb url: "https://pub.dev" source: hosted - version: "6.2.2" + version: "6.1.0" yaml: dependency: transitive description: @@ -1542,5 +1657,5 @@ packages: source: hosted version: "3.1.1" sdks: - dart: ">=2.19.0 <4.0.0" - flutter: ">=3.7.0" + dart: ">=2.17.0 <3.0.0" + flutter: ">=3.0.0" diff --git a/pubspec.yaml b/pubspec.yaml index 0d7aa5b..47a3703 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,24 +1,25 @@ name: tencent_cloud_chat_uikit -description: Chat UI components library and basic chat business logic for Tencent Cloud Chat, helping you build In-APP Chat module easily. -version: 1.7.0+1 +description: A powerful chat UI component library and business logic for Tencent Cloud Chat, creating seamless in-app chat modules for delightful user experiences. +version: 2.0.0 homepage: https://www.tencentcloud.com/products/im?from=pub -repository: https://github.com/TencentCloud/chat-uikit-flutter +repository: https://github.com/TencentCloud/tc-chat-uikit-flutter documentation: https://comm.qq.com/im/doc/flutter/en/TUIKit/readme.html + # publish_to: none platforms: android: ios: + macos: web: + windows: environment: - sdk: ">=2.15.0 <3.0.0" - flutter: ">=2.10.0" + sdk: ">=2.17.0 <3.0.0" + flutter: ">=3.0.0" dependencies: flutter: sdk: flutter - flutter_localizations: - sdk: flutter adaptive_action_sheet: ^2.0.1 provider: ^6.0.1 intl: ^0.17.0 @@ -26,50 +27,53 @@ dependencies: dotted_border: ^2.0.0+2 flutter_svg: ^1.0.0 image_picker: ^0.8.5+3 - file_picker: ^4.6.1 + file_picker: ^5.2.9 tencent_super_tooltip: ^0.0.1 video_player: ^2.4.2 chewie: ^1.3.2 flutter_slidable_for_tencent_im: ^1.4.0 flutter_plugin_record_plus: ^0.0.15 - azlistview: ^2.0.0 + azlistview_all_platforms: ^2.1.2 lpinyin: ^2.0.3 transparent_image: ^2.0.0 - video_thumbnail: ^0.5.2 image_gallery_saver: ^1.7.1 - photo_view: ^0.14.0 path_provider: ^2.0.8 - characters: ^1.1.0 cached_network_image: ^3.2.0 shared_preferences: ^2.0.13 - json_annotation: ^4.4.0 - js: ^0.6.3 scroll_to_index: ^2.1.1 wechat_assets_picker: ^7.2.0 tencent_wechat_camera_picker: ^3.6.5 flutter_easyrefresh: ^2.2.1 - flutter_spinkit: ^5.1.0 extended_image: ^6.0.2+1 tencent_extended_text_field: ^1.0.0 tencent_extended_text: ^1.0.0 package_info_plus: ^1.4.0 loading_animation_widget: ^1.1.0+3 - permission_handler: ^10.0.0 + permission_handler: ^10.2.0 tuple: ^2.0.0 flutter_markdown: ^0.6.9 url_launcher: ^6.1.4 universal_html: ^2.0.8 link_preview_generator: ^1.2.0 - tencent_im_base: ^1.0.26 disk_space: ^0.2.1 http: ^0.13.5 crypto: ^3.0.2 collection: ^1.15.0 flutter_image_compress: ^1.1.3 uuid: ^3.0.6 - tencent_open_file: ^4.0.9 + tencent_open_file: ^4.0.10 tencent_keyboard_visibility: ^1.0.1 - tim_ui_kit_sticker_plugin: ^1.2.0 + tim_ui_kit_sticker_plugin: ^2.0.1 + tencent_im_base: ^1.0.51 + fc_native_video_thumbnail_for_us: ^0.4.8+1 + audioplayers: ^3.0.1 + path: ^1.8.1 + tencent_cloud_uikit_core: ^1.0.2 + pasteboard: ^0.2.0 + desktop_drop: ^0.4.1 + device_info_plus: ^4.1.3 + cross_file: ^0.3.3+4 + diff_match_patch: ^0.4.1 dev_dependencies: flutter_lints: ^1.0.0 diff --git a/screenshots/chat.jpg b/screenshots/chat.jpg deleted file mode 100644 index 12bfa30..0000000 Binary files a/screenshots/chat.jpg and /dev/null differ diff --git a/screenshots/chat.png b/screenshots/chat.png deleted file mode 100644 index b9704ca..0000000 Binary files a/screenshots/chat.png and /dev/null differ diff --git a/screenshots/contact.jpg b/screenshots/contact.jpg deleted file mode 100644 index 5b3694c..0000000 Binary files a/screenshots/contact.jpg and /dev/null differ diff --git a/screenshots/conversation.jpg b/screenshots/conversation.jpg deleted file mode 100644 index 082ccc0..0000000 Binary files a/screenshots/conversation.jpg and /dev/null differ diff --git a/screenshots/group.jpg b/screenshots/group.jpg deleted file mode 100644 index dc24319..0000000 Binary files a/screenshots/group.jpg and /dev/null differ diff --git a/screenshots/profile.jpg b/screenshots/profile.jpg deleted file mode 100644 index d4bed18..0000000 Binary files a/screenshots/profile.jpg and /dev/null differ