Compare commits
No commits in common. "9d370f4273d31e6319d66555196a7e8486da1cf7" and "bd03abefc233e5acbfe8657ac0f7980bcea9f4cb" have entirely different histories.
9d370f4273
...
bd03abefc2
|
|
@ -112,29 +112,3 @@ screenshot/
|
|||
.opencode/skills/writing-great-skills/GLOSSARY.md
|
||||
.opencode/skills/writing-great-skills/SKILL.md
|
||||
.opencode/skills/writing-great-skills/agents/openai.yaml
|
||||
.cursor/commands/opsx-apply.md
|
||||
.cursor/commands/opsx-archive.md
|
||||
.cursor/commands/opsx-explore.md
|
||||
.cursor/commands/opsx-propose.md
|
||||
.cursor/commands/opsx-sync.md
|
||||
.cursor/commands/opsx-update.md
|
||||
.cursor/skills/openspec-apply-change/SKILL.md
|
||||
.cursor/skills/openspec-archive-change/SKILL.md
|
||||
.cursor/skills/openspec-explore/SKILL.md
|
||||
.cursor/skills/openspec-propose/SKILL.md
|
||||
.cursor/skills/openspec-sync-specs/SKILL.md
|
||||
.cursor/skills/openspec-update-change/SKILL.md
|
||||
.opencode/commands/opsx-apply.md
|
||||
.opencode/commands/opsx-archive.md
|
||||
.opencode/commands/opsx-explore.md
|
||||
.opencode/commands/opsx-propose.md
|
||||
.opencode/commands/opsx-sync.md
|
||||
.opencode/commands/opsx-update.md
|
||||
.opencode/skills/openspec-apply-change/SKILL.md
|
||||
.opencode/skills/openspec-archive-change/SKILL.md
|
||||
.opencode/skills/openspec-explore/SKILL.md
|
||||
.opencode/skills/openspec-propose/SKILL.md
|
||||
.opencode/skills/openspec-sync-specs/SKILL.md
|
||||
.opencode/skills/openspec-update-change/SKILL.md
|
||||
openspec/config.yaml
|
||||
openspec/CONVENTIONS.md
|
||||
|
|
|
|||
2
Podfile
2
Podfile
|
|
@ -25,7 +25,7 @@ target 'QuickLocation' do
|
|||
|
||||
|
||||
#工具
|
||||
pod 'FLEX', :git => 'https://github.com/FLEXTool/FLEX.git', :configurations => ['Debug']
|
||||
#pod 'FLEX', :configurations => ['Debug', 'AdHoc']
|
||||
pod 'HXPHPicker'
|
||||
pod 'SwiftKeychainWrapper'
|
||||
pod 'SGQRCode', '~> 4.1.0' #扫描
|
||||
|
|
|
|||
10
Podfile.lock
10
Podfile.lock
|
|
@ -26,7 +26,6 @@ PODS:
|
|||
- CocoaMQTT/Core (2.2.5):
|
||||
- MqttCocoaAsyncSocket (~> 1.0.8)
|
||||
- Differentiator (5.0.0)
|
||||
- FLEX (5.22.10)
|
||||
- GTCommonSDK (3.2.3.0):
|
||||
- ZXSDK
|
||||
- GYSDK (3.1.3.0):
|
||||
|
|
@ -200,7 +199,6 @@ DEPENDENCIES:
|
|||
- BRPickerView
|
||||
- CocoaLumberjack/Swift
|
||||
- CocoaMQTT
|
||||
- FLEX (from `https://github.com/FLEXTool/FLEX.git`)
|
||||
- GYSDK
|
||||
- HXPHPicker
|
||||
- IQKeyboardManagerSwift
|
||||
|
|
@ -299,15 +297,10 @@ SPEC REPOS:
|
|||
- ZXSDK
|
||||
|
||||
EXTERNAL SOURCES:
|
||||
FLEX:
|
||||
:git: https://github.com/FLEXTool/FLEX.git
|
||||
MBProgressHUD:
|
||||
:git: https://github.com/jdg/MBProgressHUD.git
|
||||
|
||||
CHECKOUT OPTIONS:
|
||||
FLEX:
|
||||
:commit: 63a6f588841e94e4c3adaa045ff16eb8163f0bb4
|
||||
:git: https://github.com/FLEXTool/FLEX.git
|
||||
MBProgressHUD:
|
||||
:commit: 4a7c5f3e53cdea77c5dcb8578c2ee5acacdf6781
|
||||
:git: https://github.com/jdg/MBProgressHUD.git
|
||||
|
|
@ -323,7 +316,6 @@ SPEC CHECKSUMS:
|
|||
CocoaLumberjack: e4ba3b414dfca8c1916c6303d37f63b3a95134c6
|
||||
CocoaMQTT: 1e2fa493d57045d66fb1eba2bb5ffb8d77039c60
|
||||
Differentiator: e8497ceab83c1b10ca233716d547b9af21b9344d
|
||||
FLEX: 6ec7816b9f392fb4c635cf50381e6bd4fb15541d
|
||||
GTCommonSDK: 238c6735add91e654f8564638854686ee3dcb4ac
|
||||
GYSDK: db5f4b3aae8df06201be5e786076211ec11791be
|
||||
HXPHPicker: 451f6af62845d60d5dd2235f4665d81f2688a51b
|
||||
|
|
@ -374,6 +366,6 @@ SPEC CHECKSUMS:
|
|||
YYImage: 1e1b62a9997399593e4b9c4ecfbbabbf1d3f3b54
|
||||
ZXSDK: 786338c0a18e98e03eda00699c3bfd2700b97117
|
||||
|
||||
PODFILE CHECKSUM: f676321d820ce55c18e69318732ac59e39a57e43
|
||||
PODFILE CHECKSUM: 439991fe5523a65e07f0df7135995adfc31f6cb3
|
||||
|
||||
COCOAPODS: 1.16.2
|
||||
|
|
|
|||
|
|
@ -26,7 +26,6 @@ PODS:
|
|||
- CocoaMQTT/Core (2.2.5):
|
||||
- MqttCocoaAsyncSocket (~> 1.0.8)
|
||||
- Differentiator (5.0.0)
|
||||
- FLEX (5.22.10)
|
||||
- GTCommonSDK (3.2.3.0):
|
||||
- ZXSDK
|
||||
- GYSDK (3.1.3.0):
|
||||
|
|
@ -200,7 +199,6 @@ DEPENDENCIES:
|
|||
- BRPickerView
|
||||
- CocoaLumberjack/Swift
|
||||
- CocoaMQTT
|
||||
- FLEX (from `https://github.com/FLEXTool/FLEX.git`)
|
||||
- GYSDK
|
||||
- HXPHPicker
|
||||
- IQKeyboardManagerSwift
|
||||
|
|
@ -299,15 +297,10 @@ SPEC REPOS:
|
|||
- ZXSDK
|
||||
|
||||
EXTERNAL SOURCES:
|
||||
FLEX:
|
||||
:git: https://github.com/FLEXTool/FLEX.git
|
||||
MBProgressHUD:
|
||||
:git: https://github.com/jdg/MBProgressHUD.git
|
||||
|
||||
CHECKOUT OPTIONS:
|
||||
FLEX:
|
||||
:commit: 63a6f588841e94e4c3adaa045ff16eb8163f0bb4
|
||||
:git: https://github.com/FLEXTool/FLEX.git
|
||||
MBProgressHUD:
|
||||
:commit: 4a7c5f3e53cdea77c5dcb8578c2ee5acacdf6781
|
||||
:git: https://github.com/jdg/MBProgressHUD.git
|
||||
|
|
@ -323,7 +316,6 @@ SPEC CHECKSUMS:
|
|||
CocoaLumberjack: e4ba3b414dfca8c1916c6303d37f63b3a95134c6
|
||||
CocoaMQTT: 1e2fa493d57045d66fb1eba2bb5ffb8d77039c60
|
||||
Differentiator: e8497ceab83c1b10ca233716d547b9af21b9344d
|
||||
FLEX: 6ec7816b9f392fb4c635cf50381e6bd4fb15541d
|
||||
GTCommonSDK: 238c6735add91e654f8564638854686ee3dcb4ac
|
||||
GYSDK: db5f4b3aae8df06201be5e786076211ec11791be
|
||||
HXPHPicker: 451f6af62845d60d5dd2235f4665d81f2688a51b
|
||||
|
|
@ -374,6 +366,6 @@ SPEC CHECKSUMS:
|
|||
YYImage: 1e1b62a9997399593e4b9c4ecfbbabbf1d3f3b54
|
||||
ZXSDK: 786338c0a18e98e03eda00699c3bfd2700b97117
|
||||
|
||||
PODFILE CHECKSUM: f676321d820ce55c18e69318732ac59e39a57e43
|
||||
PODFILE CHECKSUM: 439991fe5523a65e07f0df7135995adfc31f6cb3
|
||||
|
||||
COCOAPODS: 1.16.2
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -143,40 +143,6 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|||
SOFTWARE.
|
||||
|
||||
|
||||
## FLEX
|
||||
|
||||
Copyright (c) 2014-2016, Flipboard
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification,
|
||||
are permitted provided that the following conditions are met:
|
||||
|
||||
* 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 Flipboard nor the names of its
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
|
||||
* You must NOT include this project in an application to be submitted
|
||||
to the App Store™, as this project uses too many private APIs.
|
||||
|
||||
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 HOLDER 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.
|
||||
|
||||
|
||||
## GTCommonSDK
|
||||
|
||||
MIT License
|
||||
|
|
|
|||
|
|
@ -214,46 +214,6 @@ SOFTWARE.
|
|||
<key>Type</key>
|
||||
<string>PSGroupSpecifier</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>FooterText</key>
|
||||
<string>Copyright (c) 2014-2016, Flipboard
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification,
|
||||
are permitted provided that the following conditions are met:
|
||||
|
||||
* 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 Flipboard nor the names of its
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
|
||||
* You must NOT include this project in an application to be submitted
|
||||
to the App Store™, as this project uses too many private APIs.
|
||||
|
||||
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 HOLDER 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.
|
||||
</string>
|
||||
<key>License</key>
|
||||
<string>BSD</string>
|
||||
<key>Title</key>
|
||||
<string>FLEX</string>
|
||||
<key>Type</key>
|
||||
<string>PSGroupSpecifier</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>FooterText</key>
|
||||
<string>MIT License
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ ${BUILT_PRODUCTS_DIR}/BRPickerView/BRPickerView.framework
|
|||
${BUILT_PRODUCTS_DIR}/CocoaLumberjack/CocoaLumberjack.framework
|
||||
${BUILT_PRODUCTS_DIR}/CocoaMQTT/CocoaMQTT.framework
|
||||
${BUILT_PRODUCTS_DIR}/Differentiator/Differentiator.framework
|
||||
${BUILT_PRODUCTS_DIR}/FLEX/FLEX.framework
|
||||
${BUILT_PRODUCTS_DIR}/HXPHPicker/HXPHPicker.framework
|
||||
${BUILT_PRODUCTS_DIR}/IQKeyboardCore/IQKeyboardCore.framework
|
||||
${BUILT_PRODUCTS_DIR}/IQKeyboardManagerSwift/IQKeyboardManagerSwift.framework
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ ${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/BRPickerView.framework
|
|||
${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/CocoaLumberjack.framework
|
||||
${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/CocoaMQTT.framework
|
||||
${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Differentiator.framework
|
||||
${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/FLEX.framework
|
||||
${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/HXPHPicker.framework
|
||||
${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/IQKeyboardCore.framework
|
||||
${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/IQKeyboardManagerSwift.framework
|
||||
|
|
|
|||
|
|
@ -181,7 +181,6 @@ if [[ "$CONFIGURATION" == "Debug" ]]; then
|
|||
install_framework "${BUILT_PRODUCTS_DIR}/CocoaLumberjack/CocoaLumberjack.framework"
|
||||
install_framework "${BUILT_PRODUCTS_DIR}/CocoaMQTT/CocoaMQTT.framework"
|
||||
install_framework "${BUILT_PRODUCTS_DIR}/Differentiator/Differentiator.framework"
|
||||
install_framework "${BUILT_PRODUCTS_DIR}/FLEX/FLEX.framework"
|
||||
install_framework "${BUILT_PRODUCTS_DIR}/HXPHPicker/HXPHPicker.framework"
|
||||
install_framework "${BUILT_PRODUCTS_DIR}/IQKeyboardCore/IQKeyboardCore.framework"
|
||||
install_framework "${BUILT_PRODUCTS_DIR}/IQKeyboardManagerSwift/IQKeyboardManagerSwift.framework"
|
||||
|
|
|
|||
|
|
@ -2,13 +2,13 @@ ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES
|
|||
ARCHS = $(ARCHS_STANDARD)
|
||||
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = NO
|
||||
FRAMEWORK_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/Alamofire" "${PODS_CONFIGURATION_BUILD_DIR}/BRPickerView" "${PODS_CONFIGURATION_BUILD_DIR}/CocoaLumberjack" "${PODS_CONFIGURATION_BUILD_DIR}/CocoaMQTT" "${PODS_CONFIGURATION_BUILD_DIR}/Differentiator" "${PODS_CONFIGURATION_BUILD_DIR}/FLEX" "${PODS_CONFIGURATION_BUILD_DIR}/HXPHPicker" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardCore" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardManagerSwift" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardNotification" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardReturnManager" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardToolbar" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardToolbarManager" "${PODS_CONFIGURATION_BUILD_DIR}/IQTextInputViewNotification" "${PODS_CONFIGURATION_BUILD_DIR}/IQTextView" "${PODS_CONFIGURATION_BUILD_DIR}/Kingfisher" "${PODS_CONFIGURATION_BUILD_DIR}/KingfisherWebP" "${PODS_CONFIGURATION_BUILD_DIR}/MBProgressHUD" "${PODS_CONFIGURATION_BUILD_DIR}/MJExtension" "${PODS_CONFIGURATION_BUILD_DIR}/MJRefresh" "${PODS_CONFIGURATION_BUILD_DIR}/MarqueeLabel" "${PODS_CONFIGURATION_BUILD_DIR}/Masonry" "${PODS_CONFIGURATION_BUILD_DIR}/Moya" "${PODS_CONFIGURATION_BUILD_DIR}/MqttCocoaAsyncSocket" "${PODS_CONFIGURATION_BUILD_DIR}/ObjectMapper" "${PODS_CONFIGURATION_BUILD_DIR}/OpenIMSDK" "${PODS_CONFIGURATION_BUILD_DIR}/Popover" "${PODS_CONFIGURATION_BUILD_DIR}/RxCocoa" "${PODS_CONFIGURATION_BUILD_DIR}/RxDataSources" "${PODS_CONFIGURATION_BUILD_DIR}/RxGesture" "${PODS_CONFIGURATION_BUILD_DIR}/RxRelay" "${PODS_CONFIGURATION_BUILD_DIR}/RxSwift" "${PODS_CONFIGURATION_BUILD_DIR}/RxSwiftExt" "${PODS_CONFIGURATION_BUILD_DIR}/SDCycleScrollView" "${PODS_CONFIGURATION_BUILD_DIR}/SDWebImage" "${PODS_CONFIGURATION_BUILD_DIR}/SGQRCode" "${PODS_CONFIGURATION_BUILD_DIR}/SnapKit" "${PODS_CONFIGURATION_BUILD_DIR}/SwiftDate" "${PODS_CONFIGURATION_BUILD_DIR}/SwiftKeychainWrapper" "${PODS_CONFIGURATION_BUILD_DIR}/SwiftyJSON" "${PODS_CONFIGURATION_BUILD_DIR}/SwiftyUserDefaults" "${PODS_CONFIGURATION_BUILD_DIR}/TagListView" "${PODS_CONFIGURATION_BUILD_DIR}/URLNavigator" "${PODS_CONFIGURATION_BUILD_DIR}/YBImageBrowser" "${PODS_CONFIGURATION_BUILD_DIR}/YYImage" "${PODS_CONFIGURATION_BUILD_DIR}/libwebp" "${PODS_CONFIGURATION_BUILD_DIR}/lottie-ios" "${PODS_ROOT}/AMapSearch-NO-IDFA" "${PODS_ROOT}/AlipaySDK-iOS" "${PODS_ROOT}/OpenIMSDKCore/Framework" "${PODS_ROOT}/UMAPM/UMAPM_2.0.7" "${PODS_ROOT}/UMCommon/UMCommon_7.5.11" "${PODS_ROOT}/UMDevice/UMDevice_3.6.0" "${PODS_ROOT}/WechatOpenSDK-XCFramework" "${PODS_ROOT}/YYImage/Vendor" "${PODS_XCFRAMEWORKS_BUILD_DIR}/AlipaySDK-iOS" "${PODS_XCFRAMEWORKS_BUILD_DIR}/OpenIMSDKCore" "${PODS_XCFRAMEWORKS_BUILD_DIR}/UMAPM" "${PODS_XCFRAMEWORKS_BUILD_DIR}/UMCommon" "${PODS_XCFRAMEWORKS_BUILD_DIR}/UMDevice" "${PODS_XCFRAMEWORKS_BUILD_DIR}/WechatOpenSDK-XCFramework"
|
||||
FRAMEWORK_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/Alamofire" "${PODS_CONFIGURATION_BUILD_DIR}/BRPickerView" "${PODS_CONFIGURATION_BUILD_DIR}/CocoaLumberjack" "${PODS_CONFIGURATION_BUILD_DIR}/CocoaMQTT" "${PODS_CONFIGURATION_BUILD_DIR}/Differentiator" "${PODS_CONFIGURATION_BUILD_DIR}/HXPHPicker" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardCore" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardManagerSwift" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardNotification" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardReturnManager" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardToolbar" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardToolbarManager" "${PODS_CONFIGURATION_BUILD_DIR}/IQTextInputViewNotification" "${PODS_CONFIGURATION_BUILD_DIR}/IQTextView" "${PODS_CONFIGURATION_BUILD_DIR}/Kingfisher" "${PODS_CONFIGURATION_BUILD_DIR}/KingfisherWebP" "${PODS_CONFIGURATION_BUILD_DIR}/MBProgressHUD" "${PODS_CONFIGURATION_BUILD_DIR}/MJExtension" "${PODS_CONFIGURATION_BUILD_DIR}/MJRefresh" "${PODS_CONFIGURATION_BUILD_DIR}/MarqueeLabel" "${PODS_CONFIGURATION_BUILD_DIR}/Masonry" "${PODS_CONFIGURATION_BUILD_DIR}/Moya" "${PODS_CONFIGURATION_BUILD_DIR}/MqttCocoaAsyncSocket" "${PODS_CONFIGURATION_BUILD_DIR}/ObjectMapper" "${PODS_CONFIGURATION_BUILD_DIR}/OpenIMSDK" "${PODS_CONFIGURATION_BUILD_DIR}/Popover" "${PODS_CONFIGURATION_BUILD_DIR}/RxCocoa" "${PODS_CONFIGURATION_BUILD_DIR}/RxDataSources" "${PODS_CONFIGURATION_BUILD_DIR}/RxGesture" "${PODS_CONFIGURATION_BUILD_DIR}/RxRelay" "${PODS_CONFIGURATION_BUILD_DIR}/RxSwift" "${PODS_CONFIGURATION_BUILD_DIR}/RxSwiftExt" "${PODS_CONFIGURATION_BUILD_DIR}/SDCycleScrollView" "${PODS_CONFIGURATION_BUILD_DIR}/SDWebImage" "${PODS_CONFIGURATION_BUILD_DIR}/SGQRCode" "${PODS_CONFIGURATION_BUILD_DIR}/SnapKit" "${PODS_CONFIGURATION_BUILD_DIR}/SwiftDate" "${PODS_CONFIGURATION_BUILD_DIR}/SwiftKeychainWrapper" "${PODS_CONFIGURATION_BUILD_DIR}/SwiftyJSON" "${PODS_CONFIGURATION_BUILD_DIR}/SwiftyUserDefaults" "${PODS_CONFIGURATION_BUILD_DIR}/TagListView" "${PODS_CONFIGURATION_BUILD_DIR}/URLNavigator" "${PODS_CONFIGURATION_BUILD_DIR}/YBImageBrowser" "${PODS_CONFIGURATION_BUILD_DIR}/YYImage" "${PODS_CONFIGURATION_BUILD_DIR}/libwebp" "${PODS_CONFIGURATION_BUILD_DIR}/lottie-ios" "${PODS_ROOT}/AMapSearch-NO-IDFA" "${PODS_ROOT}/AlipaySDK-iOS" "${PODS_ROOT}/OpenIMSDKCore/Framework" "${PODS_ROOT}/UMAPM/UMAPM_2.0.7" "${PODS_ROOT}/UMCommon/UMCommon_7.5.11" "${PODS_ROOT}/UMDevice/UMDevice_3.6.0" "${PODS_ROOT}/WechatOpenSDK-XCFramework" "${PODS_ROOT}/YYImage/Vendor" "${PODS_XCFRAMEWORKS_BUILD_DIR}/AlipaySDK-iOS" "${PODS_XCFRAMEWORKS_BUILD_DIR}/OpenIMSDKCore" "${PODS_XCFRAMEWORKS_BUILD_DIR}/UMAPM" "${PODS_XCFRAMEWORKS_BUILD_DIR}/UMCommon" "${PODS_XCFRAMEWORKS_BUILD_DIR}/UMDevice" "${PODS_XCFRAMEWORKS_BUILD_DIR}/WechatOpenSDK-XCFramework"
|
||||
GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1
|
||||
HEADER_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/Alamofire/Alamofire.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/BRPickerView/BRPickerView.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/CocoaLumberjack/CocoaLumberjack.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/CocoaMQTT/CocoaMQTT.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/Differentiator/Differentiator.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/FLEX/FLEX.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/HXPHPicker/HXPHPicker.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardCore/IQKeyboardCore.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardManagerSwift/IQKeyboardManagerSwift.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardNotification/IQKeyboardNotification.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardReturnManager/IQKeyboardReturnManager.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardToolbar/IQKeyboardToolbar.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardToolbarManager/IQKeyboardToolbarManager.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/IQTextInputViewNotification/IQTextInputViewNotification.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/IQTextView/IQTextView.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/Kingfisher/Kingfisher.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/KingfisherWebP/KingfisherWebP.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/MBProgressHUD/MBProgressHUD.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/MJExtension/MJExtension.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/MJRefresh/MJRefresh.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/MarqueeLabel/MarqueeLabel.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/Masonry/Masonry.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/Moya/Moya.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/MqttCocoaAsyncSocket/MqttCocoaAsyncSocket.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/ObjectMapper/ObjectMapper.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/OpenIMSDK/OpenIMSDK.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/Popover/Popover.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/RxCocoa/RxCocoa.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/RxDataSources/RxDataSources.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/RxGesture/RxGesture.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/RxRelay/RxRelay.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/RxSwift/RxSwift.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/RxSwiftExt/RxSwiftExt.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/SDCycleScrollView/SDCycleScrollView.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/SDWebImage/SDWebImage.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/SGQRCode/SGQRCode.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/SnapKit/SnapKit.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/SwiftDate/SwiftDate.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/SwiftKeychainWrapper/SwiftKeychainWrapper.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/SwiftyJSON/SwiftyJSON.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/SwiftyUserDefaults/SwiftyUserDefaults.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/TagListView/TagListView.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/URLNavigator/URLNavigator.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/YBImageBrowser/YBImageBrowser.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/YYImage/YYImage.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/libwebp/libwebp.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/lottie-ios/Lottie.framework/Headers"
|
||||
HEADER_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/Alamofire/Alamofire.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/BRPickerView/BRPickerView.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/CocoaLumberjack/CocoaLumberjack.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/CocoaMQTT/CocoaMQTT.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/Differentiator/Differentiator.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/HXPHPicker/HXPHPicker.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardCore/IQKeyboardCore.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardManagerSwift/IQKeyboardManagerSwift.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardNotification/IQKeyboardNotification.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardReturnManager/IQKeyboardReturnManager.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardToolbar/IQKeyboardToolbar.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardToolbarManager/IQKeyboardToolbarManager.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/IQTextInputViewNotification/IQTextInputViewNotification.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/IQTextView/IQTextView.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/Kingfisher/Kingfisher.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/KingfisherWebP/KingfisherWebP.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/MBProgressHUD/MBProgressHUD.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/MJExtension/MJExtension.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/MJRefresh/MJRefresh.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/MarqueeLabel/MarqueeLabel.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/Masonry/Masonry.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/Moya/Moya.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/MqttCocoaAsyncSocket/MqttCocoaAsyncSocket.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/ObjectMapper/ObjectMapper.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/OpenIMSDK/OpenIMSDK.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/Popover/Popover.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/RxCocoa/RxCocoa.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/RxDataSources/RxDataSources.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/RxGesture/RxGesture.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/RxRelay/RxRelay.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/RxSwift/RxSwift.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/RxSwiftExt/RxSwiftExt.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/SDCycleScrollView/SDCycleScrollView.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/SDWebImage/SDWebImage.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/SGQRCode/SGQRCode.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/SnapKit/SnapKit.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/SwiftDate/SwiftDate.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/SwiftKeychainWrapper/SwiftKeychainWrapper.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/SwiftyJSON/SwiftyJSON.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/SwiftyUserDefaults/SwiftyUserDefaults.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/TagListView/TagListView.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/URLNavigator/URLNavigator.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/YBImageBrowser/YBImageBrowser.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/YYImage/YYImage.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/libwebp/libwebp.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/lottie-ios/Lottie.framework/Headers"
|
||||
LD_RUNPATH_SEARCH_PATHS = $(inherited) /usr/lib/swift '@executable_path/Frameworks' '@loader_path/Frameworks'
|
||||
LIBRARY_SEARCH_PATHS = $(inherited) "${TOOLCHAIN_DIR}/usr/lib/swift/${PLATFORM_NAME}" /usr/lib/swift $(SDKROOT)/usr/lib/swift
|
||||
OTHER_LDFLAGS = $(inherited) -ObjC -l"c++" -l"iconv" -l"resolv" -l"sqlite3" -l"sqlite3.0" -l"stdc++" -l"swiftCoreGraphics" -l"z" -framework "AMapSearchKit" -framework "AVFoundation" -framework "Accelerate" -framework "AdSupport" -framework "Alamofire" -framework "AlipaySDK" -framework "AssetsLibrary" -framework "AudioToolbox" -framework "BRPickerView" -framework "CFNetwork" -framework "CallKit" -framework "CocoaLumberjack" -framework "CocoaMQTT" -framework "Combine" -framework "CoreFoundation" -framework "CoreGraphics" -framework "CoreLocation" -framework "CoreMotion" -framework "CoreServices" -framework "CoreTelephony" -framework "CoreText" -framework "Differentiator" -framework "ExternalAccessory" -framework "FLEX" -framework "Foundation" -framework "GLKit" -framework "HXPHPicker" -framework "IQKeyboardCore" -framework "IQKeyboardManagerSwift" -framework "IQKeyboardNotification" -framework "IQKeyboardReturnManager" -framework "IQKeyboardToolbar" -framework "IQKeyboardToolbarManager" -framework "IQTextInputViewNotification" -framework "IQTextView" -framework "ImageIO" -framework "Kingfisher" -framework "KingfisherWebP" -framework "Lottie" -framework "MBProgressHUD" -framework "MJExtension" -framework "MJRefresh" -framework "MarqueeLabel" -framework "Masonry" -framework "MobileCoreServices" -framework "Moya" -framework "MqttCocoaAsyncSocket" -framework "ObjectMapper" -framework "OpenGLES" -framework "OpenIMCore" -framework "OpenIMSDK" -framework "Photos" -framework "PhotosUI" -framework "Popover" -framework "QuartzCore" -framework "RxCocoa" -framework "RxDataSources" -framework "RxGesture" -framework "RxRelay" -framework "RxSwift" -framework "RxSwiftExt" -framework "SDCycleScrollView" -framework "SDWebImage" -framework "SGQRCode" -framework "SceneKit" -framework "Security" -framework "SnapKit" -framework "SwiftDate" -framework "SwiftKeychainWrapper" -framework "SwiftyJSON" -framework "SwiftyUserDefaults" -framework "SystemConfiguration" -framework "TagListView" -framework "UIKit" -framework "UMAPM" -framework "UMCommon" -framework "UMDevice" -framework "URLNavigator" -framework "WebKit" -framework "WechatOpenSDK" -framework "YBImageBrowser" -framework "YYImage" -framework "libwebp" -weak_framework "AppTrackingTransparency" -weak_framework "Combine" -weak_framework "Network" -weak_framework "SwiftUI"
|
||||
OTHER_MODULE_VERIFIER_FLAGS = $(inherited) "-F${PODS_CONFIGURATION_BUILD_DIR}/AMapFoundation-NO-IDFA" "-F${PODS_CONFIGURATION_BUILD_DIR}/AMapLocation-NO-IDFA" "-F${PODS_CONFIGURATION_BUILD_DIR}/AMapNavi-NO-IDFA" "-F${PODS_CONFIGURATION_BUILD_DIR}/AMapSearch-NO-IDFA" "-F${PODS_CONFIGURATION_BUILD_DIR}/Alamofire" "-F${PODS_CONFIGURATION_BUILD_DIR}/AlipaySDK-iOS" "-F${PODS_CONFIGURATION_BUILD_DIR}/BRPickerView" "-F${PODS_CONFIGURATION_BUILD_DIR}/CocoaLumberjack" "-F${PODS_CONFIGURATION_BUILD_DIR}/CocoaMQTT" "-F${PODS_CONFIGURATION_BUILD_DIR}/Differentiator" "-F${PODS_CONFIGURATION_BUILD_DIR}/FLEX" "-F${PODS_CONFIGURATION_BUILD_DIR}/GTCommonSDK" "-F${PODS_CONFIGURATION_BUILD_DIR}/GYSDK" "-F${PODS_CONFIGURATION_BUILD_DIR}/HXPHPicker" "-F${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardCore" "-F${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardManagerSwift" "-F${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardNotification" "-F${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardReturnManager" "-F${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardToolbar" "-F${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardToolbarManager" "-F${PODS_CONFIGURATION_BUILD_DIR}/IQTextInputViewNotification" "-F${PODS_CONFIGURATION_BUILD_DIR}/IQTextView" "-F${PODS_CONFIGURATION_BUILD_DIR}/Kingfisher" "-F${PODS_CONFIGURATION_BUILD_DIR}/KingfisherWebP" "-F${PODS_CONFIGURATION_BUILD_DIR}/MBProgressHUD" "-F${PODS_CONFIGURATION_BUILD_DIR}/MJExtension" "-F${PODS_CONFIGURATION_BUILD_DIR}/MJRefresh" "-F${PODS_CONFIGURATION_BUILD_DIR}/MarqueeLabel" "-F${PODS_CONFIGURATION_BUILD_DIR}/Masonry" "-F${PODS_CONFIGURATION_BUILD_DIR}/Moya" "-F${PODS_CONFIGURATION_BUILD_DIR}/MqttCocoaAsyncSocket" "-F${PODS_CONFIGURATION_BUILD_DIR}/ObjectMapper" "-F${PODS_CONFIGURATION_BUILD_DIR}/OpenIMSDK" "-F${PODS_CONFIGURATION_BUILD_DIR}/OpenIMSDKCore" "-F${PODS_CONFIGURATION_BUILD_DIR}/Popover" "-F${PODS_CONFIGURATION_BUILD_DIR}/RxCocoa" "-F${PODS_CONFIGURATION_BUILD_DIR}/RxDataSources" "-F${PODS_CONFIGURATION_BUILD_DIR}/RxGesture" "-F${PODS_CONFIGURATION_BUILD_DIR}/RxRelay" "-F${PODS_CONFIGURATION_BUILD_DIR}/RxSwift" "-F${PODS_CONFIGURATION_BUILD_DIR}/RxSwiftExt" "-F${PODS_CONFIGURATION_BUILD_DIR}/SDCycleScrollView" "-F${PODS_CONFIGURATION_BUILD_DIR}/SDWebImage" "-F${PODS_CONFIGURATION_BUILD_DIR}/SGQRCode" "-F${PODS_CONFIGURATION_BUILD_DIR}/SnapKit" "-F${PODS_CONFIGURATION_BUILD_DIR}/SwiftDate" "-F${PODS_CONFIGURATION_BUILD_DIR}/SwiftKeychainWrapper" "-F${PODS_CONFIGURATION_BUILD_DIR}/SwiftyJSON" "-F${PODS_CONFIGURATION_BUILD_DIR}/SwiftyUserDefaults" "-F${PODS_CONFIGURATION_BUILD_DIR}/TagListView" "-F${PODS_CONFIGURATION_BUILD_DIR}/UMAPM" "-F${PODS_CONFIGURATION_BUILD_DIR}/UMCommon" "-F${PODS_CONFIGURATION_BUILD_DIR}/UMDevice" "-F${PODS_CONFIGURATION_BUILD_DIR}/URLNavigator" "-F${PODS_CONFIGURATION_BUILD_DIR}/WechatOpenSDK-XCFramework" "-F${PODS_CONFIGURATION_BUILD_DIR}/YBImageBrowser" "-F${PODS_CONFIGURATION_BUILD_DIR}/YYImage" "-F${PODS_CONFIGURATION_BUILD_DIR}/ZXSDK" "-F${PODS_CONFIGURATION_BUILD_DIR}/libwebp" "-F${PODS_CONFIGURATION_BUILD_DIR}/lottie-ios"
|
||||
OTHER_LDFLAGS = $(inherited) -ObjC -l"c++" -l"iconv" -l"resolv" -l"sqlite3" -l"sqlite3.0" -l"swiftCoreGraphics" -l"z" -framework "AMapSearchKit" -framework "AVFoundation" -framework "Accelerate" -framework "AdSupport" -framework "Alamofire" -framework "AlipaySDK" -framework "AssetsLibrary" -framework "AudioToolbox" -framework "BRPickerView" -framework "CFNetwork" -framework "CallKit" -framework "CocoaLumberjack" -framework "CocoaMQTT" -framework "Combine" -framework "CoreFoundation" -framework "CoreGraphics" -framework "CoreLocation" -framework "CoreMotion" -framework "CoreServices" -framework "CoreTelephony" -framework "CoreText" -framework "Differentiator" -framework "ExternalAccessory" -framework "Foundation" -framework "GLKit" -framework "HXPHPicker" -framework "IQKeyboardCore" -framework "IQKeyboardManagerSwift" -framework "IQKeyboardNotification" -framework "IQKeyboardReturnManager" -framework "IQKeyboardToolbar" -framework "IQKeyboardToolbarManager" -framework "IQTextInputViewNotification" -framework "IQTextView" -framework "ImageIO" -framework "Kingfisher" -framework "KingfisherWebP" -framework "Lottie" -framework "MBProgressHUD" -framework "MJExtension" -framework "MJRefresh" -framework "MarqueeLabel" -framework "Masonry" -framework "MobileCoreServices" -framework "Moya" -framework "MqttCocoaAsyncSocket" -framework "ObjectMapper" -framework "OpenGLES" -framework "OpenIMCore" -framework "OpenIMSDK" -framework "Photos" -framework "PhotosUI" -framework "Popover" -framework "QuartzCore" -framework "RxCocoa" -framework "RxDataSources" -framework "RxGesture" -framework "RxRelay" -framework "RxSwift" -framework "RxSwiftExt" -framework "SDCycleScrollView" -framework "SDWebImage" -framework "SGQRCode" -framework "Security" -framework "SnapKit" -framework "SwiftDate" -framework "SwiftKeychainWrapper" -framework "SwiftyJSON" -framework "SwiftyUserDefaults" -framework "SystemConfiguration" -framework "TagListView" -framework "UIKit" -framework "UMAPM" -framework "UMCommon" -framework "UMDevice" -framework "URLNavigator" -framework "WebKit" -framework "WechatOpenSDK" -framework "YBImageBrowser" -framework "YYImage" -framework "libwebp" -weak_framework "AppTrackingTransparency" -weak_framework "Combine" -weak_framework "Network" -weak_framework "SwiftUI"
|
||||
OTHER_MODULE_VERIFIER_FLAGS = $(inherited) "-F${PODS_CONFIGURATION_BUILD_DIR}/AMapFoundation-NO-IDFA" "-F${PODS_CONFIGURATION_BUILD_DIR}/AMapLocation-NO-IDFA" "-F${PODS_CONFIGURATION_BUILD_DIR}/AMapNavi-NO-IDFA" "-F${PODS_CONFIGURATION_BUILD_DIR}/AMapSearch-NO-IDFA" "-F${PODS_CONFIGURATION_BUILD_DIR}/Alamofire" "-F${PODS_CONFIGURATION_BUILD_DIR}/AlipaySDK-iOS" "-F${PODS_CONFIGURATION_BUILD_DIR}/BRPickerView" "-F${PODS_CONFIGURATION_BUILD_DIR}/CocoaLumberjack" "-F${PODS_CONFIGURATION_BUILD_DIR}/CocoaMQTT" "-F${PODS_CONFIGURATION_BUILD_DIR}/Differentiator" "-F${PODS_CONFIGURATION_BUILD_DIR}/GTCommonSDK" "-F${PODS_CONFIGURATION_BUILD_DIR}/GYSDK" "-F${PODS_CONFIGURATION_BUILD_DIR}/HXPHPicker" "-F${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardCore" "-F${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardManagerSwift" "-F${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardNotification" "-F${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardReturnManager" "-F${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardToolbar" "-F${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardToolbarManager" "-F${PODS_CONFIGURATION_BUILD_DIR}/IQTextInputViewNotification" "-F${PODS_CONFIGURATION_BUILD_DIR}/IQTextView" "-F${PODS_CONFIGURATION_BUILD_DIR}/Kingfisher" "-F${PODS_CONFIGURATION_BUILD_DIR}/KingfisherWebP" "-F${PODS_CONFIGURATION_BUILD_DIR}/MBProgressHUD" "-F${PODS_CONFIGURATION_BUILD_DIR}/MJExtension" "-F${PODS_CONFIGURATION_BUILD_DIR}/MJRefresh" "-F${PODS_CONFIGURATION_BUILD_DIR}/MarqueeLabel" "-F${PODS_CONFIGURATION_BUILD_DIR}/Masonry" "-F${PODS_CONFIGURATION_BUILD_DIR}/Moya" "-F${PODS_CONFIGURATION_BUILD_DIR}/MqttCocoaAsyncSocket" "-F${PODS_CONFIGURATION_BUILD_DIR}/ObjectMapper" "-F${PODS_CONFIGURATION_BUILD_DIR}/OpenIMSDK" "-F${PODS_CONFIGURATION_BUILD_DIR}/OpenIMSDKCore" "-F${PODS_CONFIGURATION_BUILD_DIR}/Popover" "-F${PODS_CONFIGURATION_BUILD_DIR}/RxCocoa" "-F${PODS_CONFIGURATION_BUILD_DIR}/RxDataSources" "-F${PODS_CONFIGURATION_BUILD_DIR}/RxGesture" "-F${PODS_CONFIGURATION_BUILD_DIR}/RxRelay" "-F${PODS_CONFIGURATION_BUILD_DIR}/RxSwift" "-F${PODS_CONFIGURATION_BUILD_DIR}/RxSwiftExt" "-F${PODS_CONFIGURATION_BUILD_DIR}/SDCycleScrollView" "-F${PODS_CONFIGURATION_BUILD_DIR}/SDWebImage" "-F${PODS_CONFIGURATION_BUILD_DIR}/SGQRCode" "-F${PODS_CONFIGURATION_BUILD_DIR}/SnapKit" "-F${PODS_CONFIGURATION_BUILD_DIR}/SwiftDate" "-F${PODS_CONFIGURATION_BUILD_DIR}/SwiftKeychainWrapper" "-F${PODS_CONFIGURATION_BUILD_DIR}/SwiftyJSON" "-F${PODS_CONFIGURATION_BUILD_DIR}/SwiftyUserDefaults" "-F${PODS_CONFIGURATION_BUILD_DIR}/TagListView" "-F${PODS_CONFIGURATION_BUILD_DIR}/UMAPM" "-F${PODS_CONFIGURATION_BUILD_DIR}/UMCommon" "-F${PODS_CONFIGURATION_BUILD_DIR}/UMDevice" "-F${PODS_CONFIGURATION_BUILD_DIR}/URLNavigator" "-F${PODS_CONFIGURATION_BUILD_DIR}/WechatOpenSDK-XCFramework" "-F${PODS_CONFIGURATION_BUILD_DIR}/YBImageBrowser" "-F${PODS_CONFIGURATION_BUILD_DIR}/YYImage" "-F${PODS_CONFIGURATION_BUILD_DIR}/ZXSDK" "-F${PODS_CONFIGURATION_BUILD_DIR}/libwebp" "-F${PODS_CONFIGURATION_BUILD_DIR}/lottie-ios"
|
||||
OTHER_SWIFT_FLAGS = $(inherited) -D COCOAPODS
|
||||
PODS_BUILD_DIR = ${BUILD_DIR}
|
||||
PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)
|
||||
|
|
|
|||
|
|
@ -298,7 +298,6 @@
|
|||
30EFF3E82FCA8C7000227D26 /* AuthenticationServices.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 30EFF3E92FCA8C7000227D26 /* AuthenticationServices.framework */; };
|
||||
55BF752D2FFE53F70055DA57 /* LocationPermissionPopView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55BF752C2FFE53F70055DA57 /* LocationPermissionPopView.swift */; };
|
||||
55BF75352FFF91690055DA57 /* InteractionEmojiCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55BF75342FFF91690055DA57 /* InteractionEmojiCell.swift */; };
|
||||
A1B2C3D42FDABC8C009215C1 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = A1B2C3D52FDABC8C009215C1 /* PrivacyInfo.xcprivacy */; };
|
||||
D698E9C56D6F2C152772131D /* Pods_QuickLocation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 475D33CAFA1E1911EB1F8D9F /* Pods_QuickLocation.framework */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
|
|
@ -607,7 +606,6 @@
|
|||
475D33CAFA1E1911EB1F8D9F /* Pods_QuickLocation.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_QuickLocation.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
55BF752C2FFE53F70055DA57 /* LocationPermissionPopView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationPermissionPopView.swift; sourceTree = "<group>"; };
|
||||
55BF75342FFF91690055DA57 /* InteractionEmojiCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InteractionEmojiCell.swift; sourceTree = "<group>"; };
|
||||
A1B2C3D52FDABC8C009215C1 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = "<group>"; };
|
||||
DA16D49AA46D4F6838340B55 /* Pods-QuickLocation.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-QuickLocation.debug.xcconfig"; path = "Target Support Files/Pods-QuickLocation/Pods-QuickLocation.debug.xcconfig"; sourceTree = "<group>"; };
|
||||
E483B9929C03809ADEDE8341 /* Pods-QuickLocation.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-QuickLocation.release.xcconfig"; path = "Target Support Files/Pods-QuickLocation/Pods-QuickLocation.release.xcconfig"; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
|
@ -1255,7 +1253,6 @@
|
|||
305A767E2FCA8C7000227D26 /* AppDelegate.swift */,
|
||||
305A767F2FCA8C7000227D26 /* Assets.xcassets */,
|
||||
305A76802FCA8C7000227D26 /* Info.plist */,
|
||||
A1B2C3D52FDABC8C009215C1 /* PrivacyInfo.xcprivacy */,
|
||||
305A76822FCA8C7000227D26 /* LaunchScreen.storyboard */,
|
||||
305A76842FCA8C7000227D26 /* Main.storyboard */,
|
||||
305A76852FCA8C7000227D26 /* QuickLocation-Bridging-Header.h */,
|
||||
|
|
@ -1758,7 +1755,6 @@
|
|||
305A77222FCA8C7000227D26 /* YouSheBiaoTiHei.ttf in Resources */,
|
||||
305A77232FCA8C7000227D26 /* zihunbiantaoti-r.ttf in Resources */,
|
||||
305A77F92FCA8C7000227D26 /* Assets.xcassets in Resources */,
|
||||
A1B2C3D42FDABC8C009215C1 /* PrivacyInfo.xcprivacy in Resources */,
|
||||
305A77FB2FCA8C7000227D26 /* LaunchScreen.storyboard in Resources */,
|
||||
305A77FC2FCA8C7000227D26 /* Main.storyboard in Resources */,
|
||||
);
|
||||
|
|
@ -2187,7 +2183,7 @@
|
|||
SWIFT_OBJC_BRIDGING_HEADER = "QuickLocation/QuickLocation-Bridging-Header.h";
|
||||
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = 1;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
|
|
@ -2240,7 +2236,7 @@
|
|||
SWIFT_OBJC_BRIDGING_HEADER = "QuickLocation/QuickLocation-Bridging-Header.h";
|
||||
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = 1;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -60,7 +60,9 @@ public let kScreenHeight = UIScreen.main.bounds.size.height
|
|||
/// 状态栏高度
|
||||
public var kStatusBarHeight: CGFloat {
|
||||
if #available(iOS 13.0, *) {
|
||||
let statusBarHeight = kKeyWindow?
|
||||
let statusBarHeight = UIApplication.shared.windows
|
||||
.filter { $0.isKeyWindow }
|
||||
.first?
|
||||
.windowScene?
|
||||
.statusBarManager?
|
||||
.statusBarFrame
|
||||
|
|
|
|||
|
|
@ -10,15 +10,7 @@ import UIKit
|
|||
public extension UIApplication {
|
||||
|
||||
static var keyWindow: UIWindow? {
|
||||
if #available(iOS 15.0, *) {
|
||||
return UIApplication.shared.connectedScenes
|
||||
.filter { $0.activationState == .foregroundActive }
|
||||
.compactMap { $0 as? UIWindowScene }
|
||||
.first?.windows
|
||||
.first { $0.isKeyWindow }
|
||||
} else {
|
||||
return UIApplication.shared.windows.first { $0.isKeyWindow }
|
||||
}
|
||||
shared.windows.filter { $0.isKeyWindow }.first
|
||||
}
|
||||
|
||||
static func delegate<T: UIApplicationDelegate>() -> T? {
|
||||
|
|
|
|||
|
|
@ -7,11 +7,9 @@
|
|||
|
||||
import Foundation
|
||||
|
||||
nonisolated public class DL {}
|
||||
public class DL {}
|
||||
|
||||
// nonisolated: 退出工程默认的 MainActor 隔离。
|
||||
// 该通用包装类会在 Release(-O) 归档时触发 EarlyPerfInliner 对合成 deinit 的编译器崩溃。
|
||||
nonisolated public class DLWrapper<Base> {
|
||||
public class DLWrapper<Base> {
|
||||
|
||||
/// 原始对象
|
||||
public private(set) var base: Base
|
||||
|
|
|
|||
|
|
@ -29,8 +29,7 @@ enum PagingRequestType {
|
|||
typealias RefreshResult = (status: RefreshStatus, isEmpty: Bool)
|
||||
|
||||
/// ListType: 列表数据泛型类,例: ListService<ListModel<PaginationModel>>
|
||||
// nonisolated: 泛型类在默认 MainActor 隔离下,Release(-O) 归档会触发 EarlyPerfInliner 编译器崩溃
|
||||
nonisolated class ListService<ListType>: ListServiceType where ListType: ListModelType, ListType.Item: Mappable {
|
||||
class ListService<ListType>: ListServiceType where ListType: ListModelType, ListType.Item: Mappable {
|
||||
typealias BaseListModel = ListType
|
||||
typealias BaseItem = ListType.Item
|
||||
|
||||
|
|
|
|||
|
|
@ -10,8 +10,8 @@ import RxCocoa
|
|||
import RxSwift
|
||||
import YBImageBrowser
|
||||
import UMCommon
|
||||
#if DEBUG
|
||||
import FLEX
|
||||
#if DEBUG || AdHoc
|
||||
//import FLEX
|
||||
#endif
|
||||
|
||||
class BaseViewController: UIViewController {
|
||||
|
|
@ -187,8 +187,8 @@ extension BaseViewController {
|
|||
// MARK: - FLEX
|
||||
extension BaseViewController {
|
||||
private func setupFlex() {
|
||||
#if DEBUG
|
||||
FLEXManager.shared.showExplorer()
|
||||
#if DEBUG || AdHoc
|
||||
// FLEXManager.shared.showExplorer()
|
||||
#endif
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -49,16 +49,16 @@ extension DefaultsKeys {
|
|||
func setupNetworkMode() {
|
||||
#if DEBUG
|
||||
setupApiEnv(1)
|
||||
openIM_API = "https://imapi.zuom8.cn" // http://38.207.176.65:10002
|
||||
openIM_WS = "ws://imws.zuom8.cn" // ws://38.207.176.65:10010
|
||||
openIM_API = "https://imapi.zuom8.cn"
|
||||
openIM_WS = "ws://imws.zuom8.cn"
|
||||
#elseif AdHoc
|
||||
setupApiEnv(-1)
|
||||
openIM_API = "https://imapi.zuom8.cn"
|
||||
openIM_WS = "ws://imws.zuom8.cn"
|
||||
openIM_API = "http://38.207.176.65:10002"
|
||||
openIM_WS = "ws://38.207.176.65:10010"
|
||||
#else
|
||||
setupApiEnv(1)
|
||||
openIM_API = "https://imapi.zuom8.cn"
|
||||
openIM_WS = "ws://imws.zuom8.cn"
|
||||
openIM_API = "http://38.207.176.65:10002"
|
||||
openIM_WS = "ws://38.207.176.65:10010"
|
||||
#endif
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,31 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>NSPrivacyTracking</key>
|
||||
<false/>
|
||||
<key>NSPrivacyTrackingDomains</key>
|
||||
<array/>
|
||||
<key>NSPrivacyCollectedDataTypes</key>
|
||||
<array/>
|
||||
<key>NSPrivacyAccessedAPITypes</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>NSPrivacyAccessedAPIType</key>
|
||||
<string>NSPrivacyAccessedAPICategoryUserDefaults</string>
|
||||
<key>NSPrivacyAccessedAPITypeReasons</key>
|
||||
<array>
|
||||
<string>CA92.1</string>
|
||||
</array>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>NSPrivacyAccessedAPIType</key>
|
||||
<string>NSPrivacyAccessedAPICategoryFileTimestamp</string>
|
||||
<key>NSPrivacyAccessedAPITypeReasons</key>
|
||||
<array>
|
||||
<string>C617.1</string>
|
||||
</array>
|
||||
</dict>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
|
|
@ -19,8 +19,7 @@ and produces an Observable<Element> as its output.
|
|||
|
||||
When this excuted via execute() or inputs subject, it passes its parameter to this closure and subscribes to the work.
|
||||
*/
|
||||
// nonisolated: 泛型类在默认 MainActor 隔离下,Release(-O) 归档会触发 EarlyPerfInliner 编译器崩溃
|
||||
nonisolated public final class Action<Input, Element> {
|
||||
public final class Action<Input, Element> {
|
||||
public typealias WorkFactory = (Input) -> Observable<Element>
|
||||
|
||||
public let _enabledIf: Observable<Bool>
|
||||
|
|
|
|||
|
|
@ -2,8 +2,7 @@ import Foundation
|
|||
import RxSwift
|
||||
|
||||
/// A special subject for Action.inputs. It only emits `.next` event.
|
||||
// nonisolated: 泛型类在默认 MainActor 隔离下,Release(-O) 归档会触发 EarlyPerfInliner 编译器崩溃
|
||||
nonisolated public class InputSubject<Element>: ObservableType, Cancelable, SubjectType, ObserverType {
|
||||
public class InputSubject<Element>: ObservableType, Cancelable, SubjectType, ObserverType {
|
||||
|
||||
public typealias E = Element
|
||||
typealias Key = UInt
|
||||
|
|
|
|||
|
|
@ -14,7 +14,6 @@ import AVFoundation
|
|||
import AudioToolbox
|
||||
import HXPHPicker
|
||||
import IQKeyboardManagerSwift
|
||||
import MJRefresh
|
||||
|
||||
final class GroupChatVC: BaseViewController {
|
||||
|
||||
|
|
@ -23,11 +22,6 @@ final class GroupChatVC: BaseViewController {
|
|||
fileprivate var rootView: GroupChatView!
|
||||
private let viewModel = GroupChatViewModel()
|
||||
private var msgListener: MessageListenerProxy?
|
||||
/// mention 列表:首行 @所有人 + 成员
|
||||
private var mentionRows: [(userId: String?, nickname: String)] = []
|
||||
private var isMentionPickerVisible = false
|
||||
/// 加载更早历史前记录,用于保持阅读位置
|
||||
private var pendingOffsetRestore: (oldHeight: CGFloat, oldOffset: CGFloat)?
|
||||
|
||||
// MARK: - Init
|
||||
init(groupId: String) {
|
||||
|
|
@ -52,9 +46,6 @@ final class GroupChatVC: BaseViewController {
|
|||
setupMessageListener()
|
||||
setupVoiceRecording()
|
||||
setupPanelDismiss()
|
||||
setupChatLongPress()
|
||||
setupMentionInput()
|
||||
setupHistoryRefreshHeader()
|
||||
|
||||
// 并行加载:业务接口 + IM SDK 互不依赖,同时发起
|
||||
requestGroupInfoByKey()
|
||||
|
|
@ -148,33 +139,7 @@ final class GroupChatVC: BaseViewController {
|
|||
.skip(1)
|
||||
.observe(on: MainScheduler.asyncInstance)
|
||||
.subscribe(onNext: { [weak self] _ in
|
||||
guard let self = self else { return }
|
||||
if self.viewModel.suppressAutoScroll {
|
||||
self.viewModel.suppressAutoScroll = false
|
||||
self.restoreOffsetAfterPrependIfNeeded()
|
||||
return
|
||||
}
|
||||
self.rootView.tableView.mj_header?.isHidden = !self.viewModel.hasMoreHistory
|
||||
self.scrollToBottom()
|
||||
})
|
||||
.disposed(by: disposeBag)
|
||||
|
||||
viewModel.output.pendingQuoteSummary
|
||||
.observe(on: MainScheduler.instance)
|
||||
.subscribe(onNext: { [weak self] summary in
|
||||
guard let self = self else { return }
|
||||
if let summary {
|
||||
self.rootView.setQuoteBarVisible(true, summary: summary)
|
||||
self.bringInputOverlaysToFront()
|
||||
} else {
|
||||
self.rootView.setQuoteBarVisible(false)
|
||||
}
|
||||
})
|
||||
.disposed(by: disposeBag)
|
||||
|
||||
rootView.quoteBarCloseBtn.rx.tap
|
||||
.subscribe(onNext: { [weak self] in
|
||||
self?.viewModel.clearPendingQuote()
|
||||
self?.scrollToBottom()
|
||||
})
|
||||
.disposed(by: disposeBag)
|
||||
|
||||
|
|
@ -184,58 +149,7 @@ final class GroupChatVC: BaseViewController {
|
|||
.disposed(by: disposeBag)
|
||||
}
|
||||
|
||||
private func bringInputOverlaysToFront() {
|
||||
rootView.bringSubviewToFront(rootView.mentionPickerView)
|
||||
rootView.bringSubviewToFront(rootView.quoteBar)
|
||||
rootView.bringSubviewToFront(rootView.bottomBar)
|
||||
rootView.bringSubviewToFront(rootView.disableIMView)
|
||||
}
|
||||
|
||||
// MARK: - 顶部加载更早历史
|
||||
private func setupHistoryRefreshHeader() {
|
||||
let header = MJRefreshNormalHeader { [weak self] in
|
||||
self?.loadOlderHistoryFromHeader()
|
||||
}
|
||||
header.addFeedbackGenerator()
|
||||
header.lastUpdatedTimeLabel?.isHidden = true
|
||||
header.setTitle("下拉加载更早消息", for: .idle)
|
||||
header.setTitle("松开加载", for: .pulling)
|
||||
header.setTitle("加载中...", for: .refreshing)
|
||||
rootView.tableView.mj_header = header
|
||||
}
|
||||
|
||||
private func loadOlderHistoryFromHeader() {
|
||||
guard viewModel.hasMoreHistory else {
|
||||
rootView.tableView.mj_header?.endRefreshing()
|
||||
rootView.tableView.mj_header?.isHidden = true
|
||||
return
|
||||
}
|
||||
let tableView = rootView.tableView
|
||||
pendingOffsetRestore = (tableView.contentSize.height, tableView.contentOffset.y)
|
||||
viewModel.loadOlderHistory { [weak self] result in
|
||||
guard let self = self else { return }
|
||||
self.rootView.tableView.mj_header?.endRefreshing()
|
||||
if !result.hasMore {
|
||||
self.rootView.tableView.mj_header?.isHidden = true
|
||||
}
|
||||
// suppressAutoScroll 路径里会 restoreOffset;若未 prepend 也清掉 pending
|
||||
if result.prependedCount == 0 {
|
||||
self.pendingOffsetRestore = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func restoreOffsetAfterPrependIfNeeded() {
|
||||
guard let pending = pendingOffsetRestore else { return }
|
||||
pendingOffsetRestore = nil
|
||||
let tableView = rootView.tableView
|
||||
tableView.layoutIfNeeded()
|
||||
let delta = tableView.contentSize.height - pending.oldHeight
|
||||
guard delta > 0 else { return }
|
||||
var offset = tableView.contentOffset
|
||||
offset.y = max(0, pending.oldOffset + delta)
|
||||
tableView.setContentOffset(offset, animated: false)
|
||||
}
|
||||
private var hasScrolledToBottom = false
|
||||
|
||||
private func scrollToBottom() {
|
||||
let count = dataSource.sectionModels.first?.items.count ?? 0
|
||||
|
|
@ -247,31 +161,6 @@ final class GroupChatVC: BaseViewController {
|
|||
}
|
||||
}
|
||||
|
||||
private func scrollToMessage(clientMsgID: String) {
|
||||
viewModel.ensureMessageVisible(clientMsgID: clientMsgID) { [weak self] found in
|
||||
guard let self = self else { return }
|
||||
guard found, let indexPath = self.viewModel.indexPath(forClientMsgID: clientMsgID) else {
|
||||
DLToast.showError(text: "无法定位到原消息")
|
||||
return
|
||||
}
|
||||
self.rootView.tableView.layoutIfNeeded()
|
||||
self.rootView.tableView.scrollToRow(at: indexPath, at: .middle, animated: true)
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.25) {
|
||||
if let cell = self.rootView.tableView.cellForRow(at: indexPath) as? TextSendMsgCell {
|
||||
cell.flashHighlight()
|
||||
} else if let cell = self.rootView.tableView.cellForRow(at: indexPath) as? TextReceivedMsgCell {
|
||||
cell.flashHighlight()
|
||||
} else if let cell = self.rootView.tableView.cellForRow(at: indexPath) {
|
||||
let old = cell.contentView.backgroundColor
|
||||
cell.contentView.backgroundColor = UIColor(hexStr: "#16B3FF").withAlphaComponent(0.12)
|
||||
UIView.animate(withDuration: 0.8, delay: 0.3, options: []) {
|
||||
cell.contentView.backgroundColor = old
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private lazy var emojiDataSource: RxCollectionViewSectionedReloadDataSource<SectionModel<String, String>> = {
|
||||
RxCollectionViewSectionedReloadDataSource<SectionModel<String, String>> { _, collectionView, indexPath, name in
|
||||
let cell: EmojiPanelCell = collectionView.dequeueReusableCell(for: indexPath)
|
||||
|
|
@ -282,157 +171,13 @@ final class GroupChatVC: BaseViewController {
|
|||
|
||||
// MARK: - Message Listener
|
||||
private func setupMessageListener() {
|
||||
msgListener = MessageListenerProxy(
|
||||
onMessage: { [weak self] msg in
|
||||
guard let self = self, msg.groupID == self.viewModel.groupId else { return }
|
||||
self.viewModel.onReceiveMessage(msg)
|
||||
},
|
||||
onRevoked: { [weak self] info in
|
||||
self?.viewModel.onMessageRevoked(info)
|
||||
}
|
||||
)
|
||||
msgListener = MessageListenerProxy { [weak self] msg in
|
||||
guard let self = self, msg.groupID == self.viewModel.groupId else { return }
|
||||
self.viewModel.onReceiveMessage(msg)
|
||||
}
|
||||
OIMManager.callbacker.addAdvancedMsgListener(listener: msgListener!)
|
||||
}
|
||||
|
||||
// MARK: - Long press (quote / revoke / avatar @)
|
||||
private func setupChatLongPress() {
|
||||
let longPress = UILongPressGestureRecognizer(target: self, action: #selector(handleChatLongPress(_:)))
|
||||
longPress.minimumPressDuration = 0.45
|
||||
rootView.tableView.addGestureRecognizer(longPress)
|
||||
}
|
||||
|
||||
@objc private func handleChatLongPress(_ gesture: UILongPressGestureRecognizer) {
|
||||
guard gesture.state == .began else { return }
|
||||
let point = gesture.location(in: rootView.tableView)
|
||||
guard let indexPath = rootView.tableView.indexPathForRow(at: point),
|
||||
let cell = rootView.tableView.cellForRow(at: indexPath) else { return }
|
||||
let items = viewModel.currentMessages
|
||||
guard indexPath.row < items.count,
|
||||
let msg = items[indexPath.row].chatMessage else { return }
|
||||
|
||||
// 长按对方头像 → @
|
||||
if !msg.isSelf, let avatar = avatarView(in: cell) {
|
||||
let p = gesture.location(in: avatar)
|
||||
if avatar.bounds.insetBy(dx: -6, dy: -6).contains(p) {
|
||||
insertMention(userId: msg.senderId, nickname: msg.senderName, replaceTriggerAt: false)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
showBubbleActionMenu(for: msg, from: cell)
|
||||
}
|
||||
|
||||
private func avatarView(in cell: UITableViewCell) -> UIImageView? {
|
||||
if let c = cell as? TextReceivedMsgCell { return c.avatarView }
|
||||
if let c = cell as? EmojiReceivedMsgCell { return c.avatarView }
|
||||
if let c = cell as? VoiceReceivedMsgCell { return c.avatarView }
|
||||
if let c = cell as? ImageReceivedMsgCell { return c.avatarView }
|
||||
return nil
|
||||
}
|
||||
|
||||
private func menuAnchorView(in cell: UITableViewCell) -> UIView {
|
||||
if let c = cell as? TextSendMsgCell { return c.menuAnchorView }
|
||||
if let c = cell as? TextReceivedMsgCell { return c.menuAnchorView }
|
||||
if let c = cell as? EmojiSendMsgCell { return c.menuAnchorView }
|
||||
if let c = cell as? EmojiReceivedMsgCell { return c.menuAnchorView }
|
||||
if let c = cell as? VoiceSendMsgCell { return c.menuAnchorView }
|
||||
if let c = cell as? VoiceReceivedMsgCell { return c.menuAnchorView }
|
||||
if let c = cell as? ImageSendMsgCell { return c.menuAnchorView }
|
||||
if let c = cell as? ImageReceivedMsgCell { return c.menuAnchorView }
|
||||
return cell.contentView
|
||||
}
|
||||
|
||||
private weak var bubbleMenu: ChatBubbleActionMenu?
|
||||
|
||||
private func showBubbleActionMenu(for msg: ChatMessage, from cell: UITableViewCell) {
|
||||
bubbleMenu?.dismiss()
|
||||
rootView.dismissAllPanels()
|
||||
|
||||
let menu = ChatBubbleActionMenu()
|
||||
bubbleMenu = menu
|
||||
menu.onQuote = { [weak self] in
|
||||
guard let self = self,
|
||||
let oim = self.viewModel.oimMessage(forClientMsgID: msg.id) else {
|
||||
DLToast.showError(text: "无法引用该消息")
|
||||
return
|
||||
}
|
||||
self.viewModel.setPendingQuote(oim)
|
||||
self.rootView.textField.becomeFirstResponder()
|
||||
}
|
||||
menu.onRevoke = { [weak self] in
|
||||
self?.viewModel.revokeMessage(clientMsgID: msg.id)
|
||||
}
|
||||
let anchor = menuAnchorView(in: cell)
|
||||
// menu.show(in: rootView, anchor: anchor, showRevoke: msg.isSelf)
|
||||
}
|
||||
|
||||
// MARK: - Mention
|
||||
private func setupMentionInput() {
|
||||
rootView.textField.delegate = self
|
||||
rootView.mentionTableView.delegate = self
|
||||
rootView.mentionTableView.dataSource = self
|
||||
rootView.textField.rx.controlEvent(.editingChanged)
|
||||
.subscribe(onNext: { [weak self] in
|
||||
self?.handleMentionTrigger()
|
||||
})
|
||||
.disposed(by: disposeBag)
|
||||
}
|
||||
|
||||
private func handleMentionTrigger() {
|
||||
guard let text = rootView.textField.text,
|
||||
let selected = rootView.textField.selectedTextRange else {
|
||||
hideMentionPicker()
|
||||
return
|
||||
}
|
||||
let cursor = rootView.textField.offset(from: rootView.textField.beginningOfDocument, to: selected.start)
|
||||
let ns = text as NSString
|
||||
let prefix = ns.substring(to: min(cursor, ns.length))
|
||||
if prefix.hasSuffix("@") {
|
||||
showMentionPicker()
|
||||
} else if isMentionPickerVisible {
|
||||
// 继续输入非空格时保持;遇到空格关闭
|
||||
if prefix.last == " " || !prefix.contains("@") {
|
||||
hideMentionPicker()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func showMentionPicker() {
|
||||
mentionRows = [(nil, "所有人")] + viewModel.mentionCandidates().map { ($0.user_id, $0.nick_name) }
|
||||
rootView.mentionTableView.reloadData()
|
||||
rootView.mentionPickerView.isHidden = false
|
||||
isMentionPickerVisible = true
|
||||
bringInputOverlaysToFront()
|
||||
rootView.layoutIfNeeded()
|
||||
let bounds = rootView.mentionPickerView.bounds
|
||||
rootView.mentionPickerView.layer.shadowPath = UIBezierPath(roundedRect: bounds, cornerRadius: 12).cgPath
|
||||
}
|
||||
|
||||
private func hideMentionPicker() {
|
||||
rootView.mentionPickerView.isHidden = true
|
||||
isMentionPickerVisible = false
|
||||
}
|
||||
|
||||
private func insertMention(userId: String?, nickname: String, replaceTriggerAt: Bool = true) {
|
||||
hideMentionPicker()
|
||||
let insert: String
|
||||
if let userId {
|
||||
insert = viewModel.mentionUser(userId: userId, nickname: nickname)
|
||||
} else {
|
||||
viewModel.setPendingAtAll(true)
|
||||
insert = "@所有人 "
|
||||
}
|
||||
var current = rootView.textField.text ?? ""
|
||||
if replaceTriggerAt, let atIdx = current.lastIndex(of: "@") {
|
||||
let after = current[current.index(after: atIdx)...]
|
||||
if after.isEmpty || !after.contains(where: { $0.isWhitespace }) {
|
||||
current = String(current[..<atIdx])
|
||||
}
|
||||
}
|
||||
rootView.textField.text = current + insert
|
||||
rootView.textField.becomeFirstResponder()
|
||||
}
|
||||
|
||||
// MARK: - Actions
|
||||
private func reactiveAction() {
|
||||
rootView.backBtn.rx.tap
|
||||
|
|
@ -522,15 +267,16 @@ final class GroupChatVC: BaseViewController {
|
|||
})
|
||||
.disposed(by: disposeBag)
|
||||
|
||||
rootView.sendBtn.rx.tap
|
||||
.subscribe(onNext: { [weak self] in
|
||||
guard let self = self else { return }
|
||||
let text = self.rootView.textField.text ?? ""
|
||||
guard !text.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else { return }
|
||||
self.viewModel.input.sendMessage.onNext(text)
|
||||
self.rootView.textField.text = ""
|
||||
self.hideMentionPicker()
|
||||
})
|
||||
let sendText = Observable.merge(
|
||||
rootView.sendBtn.rx.tap.map { [weak self] _ in self?.rootView.textField.text ?? "" },
|
||||
rootView.textField.rx.controlEvent(.editingDidEndOnExit)
|
||||
.map { [weak self] _ in self?.rootView.textField.text ?? "" }
|
||||
)
|
||||
.filter { !$0.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty }
|
||||
.do(onNext: { [weak self] _ in self?.rootView.textField.text = "" })
|
||||
|
||||
sendText
|
||||
.bind(to: viewModel.input.sendMessage)
|
||||
.disposed(by: disposeBag)
|
||||
|
||||
// 设置
|
||||
|
|
@ -683,7 +429,6 @@ final class GroupChatVC: BaseViewController {
|
|||
|
||||
// Send voice message via OpenIM
|
||||
let msg = OIMMessageInfo.createSoundMessage(fromFullPath: url.path, duration: recordDuration * 1000)
|
||||
viewModel.cacheMessage(msg)
|
||||
OIMManager.manager.sendMessage(msg,
|
||||
recvID: "",
|
||||
groupID: viewModel.groupId,
|
||||
|
|
@ -711,23 +456,15 @@ final class GroupChatVC: BaseViewController {
|
|||
|
||||
// MARK: - dataSource
|
||||
private lazy var dataSource: RxTableViewSectionedReloadDataSource<ChatSectionModel> = {
|
||||
RxTableViewSectionedReloadDataSource<ChatSectionModel> { [weak self] _, tableView, indexPath, item in
|
||||
RxTableViewSectionedReloadDataSource<ChatSectionModel> { _, tableView, indexPath, item in
|
||||
switch item {
|
||||
case let .send(msg):
|
||||
let cell: TextSendMsgCell = tableView.dequeueReusableCell(for: indexPath)
|
||||
cell.configure(msg)
|
||||
cell.onQuoteTap = { [weak self] in
|
||||
guard let id = msg.quotePreview?.quotedClientMsgID else { return }
|
||||
self?.scrollToMessage(clientMsgID: id)
|
||||
}
|
||||
return cell
|
||||
case let .received(msg):
|
||||
let cell: TextReceivedMsgCell = tableView.dequeueReusableCell(for: indexPath)
|
||||
cell.configure(msg)
|
||||
cell.onQuoteTap = { [weak self] in
|
||||
guard let id = msg.quotePreview?.quotedClientMsgID else { return }
|
||||
self?.scrollToMessage(clientMsgID: id)
|
||||
}
|
||||
return cell
|
||||
case let .emojiSend(msg):
|
||||
let cell: EmojiSendMsgCell = tableView.dequeueReusableCell(for: indexPath)
|
||||
|
|
@ -759,8 +496,7 @@ final class GroupChatVC: BaseViewController {
|
|||
self?.showBigImage(imgUrlList: [msg.imageUrl], currentPage: 0, projectiveView: cell.photoView)
|
||||
}
|
||||
return cell
|
||||
case let .notification(text, showTime, timestamp),
|
||||
let .revoked(text, showTime, timestamp):
|
||||
case let .notification(text, showTime, timestamp):
|
||||
let cell: NotificationMsgCell = tableView.dequeueReusableCell(for: indexPath)
|
||||
cell.configure(text, showTime: showTime, timestamp: timestamp)
|
||||
return cell
|
||||
|
|
@ -769,33 +505,6 @@ final class GroupChatVC: BaseViewController {
|
|||
}()
|
||||
}
|
||||
|
||||
// MARK: - UITextFieldDelegate / Mention table
|
||||
extension GroupChatVC: UITextFieldDelegate, UITableViewDataSource {
|
||||
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
|
||||
let text = textField.text ?? ""
|
||||
guard !text.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else { return true }
|
||||
viewModel.input.sendMessage.onNext(text)
|
||||
textField.text = ""
|
||||
hideMentionPicker()
|
||||
return true
|
||||
}
|
||||
|
||||
func numberOfSections(in tableView: UITableView) -> Int { 1 }
|
||||
|
||||
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
||||
tableView === rootView.mentionTableView ? mentionRows.count : 0
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: "MentionCell", for: indexPath)
|
||||
let row = mentionRows[indexPath.row]
|
||||
cell.textLabel?.font = .systemFont(ofSize: 14)
|
||||
cell.textLabel?.text = row.userId == nil ? "@所有人" : "@\(row.nickname)"
|
||||
cell.selectionStyle = .default
|
||||
return cell
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - UITableViewDelegate
|
||||
extension GroupChatVC: UITableViewDelegate {
|
||||
func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
|
||||
|
|
@ -807,32 +516,18 @@ extension GroupChatVC: UITableViewDelegate {
|
|||
(cell as? EmojiSendMsgCell)?.stopAnimation()
|
||||
(cell as? EmojiReceivedMsgCell)?.stopAnimation()
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
||||
guard tableView === rootView.mentionTableView else { return }
|
||||
tableView.deselectRow(at: indexPath, animated: true)
|
||||
let row = mentionRows[indexPath.row]
|
||||
insertMention(userId: row.userId, nickname: row.nickname)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - MessageListenerProxy
|
||||
private class MessageListenerProxy: NSObject, OIMAdvancedMsgListener {
|
||||
private let onMessage: (OIMMessageInfo) -> Void
|
||||
private let onRevoked: (OIMMessageRevokedInfo) -> Void
|
||||
private let handler: (OIMMessageInfo) -> Void
|
||||
|
||||
init(onMessage: @escaping (OIMMessageInfo) -> Void,
|
||||
onRevoked: @escaping (OIMMessageRevokedInfo) -> Void) {
|
||||
self.onMessage = onMessage
|
||||
self.onRevoked = onRevoked
|
||||
init(handler: @escaping (OIMMessageInfo) -> Void) {
|
||||
self.handler = handler
|
||||
}
|
||||
|
||||
func onRecvNewMessage(_ msg: OIMMessageInfo) {
|
||||
onMessage(msg)
|
||||
}
|
||||
|
||||
func onRecvMessageRevoked(_ messageRevoked: OIMMessageRevokedInfo) {
|
||||
onRevoked(messageRevoked)
|
||||
handler(msg)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -867,7 +562,6 @@ extension GroupChatVC: PhotoPickerControllerDelegate {
|
|||
let msg = OIMMessageInfo.createImageMessage(fromFullPath: fileURL.path)
|
||||
// 使用 SDK 的 clientMsgID 作为本地消息 ID,方便后续与监听器去重
|
||||
let localId = msg.clientMsgID ?? UUID().uuidString
|
||||
viewModel.cacheMessage(msg)
|
||||
|
||||
// 立即显示本地图片(带 loading)
|
||||
let localMsg = ChatMessage(
|
||||
|
|
|
|||
|
|
@ -32,63 +32,6 @@ struct ChatMessage {
|
|||
let timestamp: TimeInterval
|
||||
var showTime: Bool = false
|
||||
var isUploading: Bool = false
|
||||
var quotePreview: QuotePreview? = nil
|
||||
var atUserIDs: [String] = []
|
||||
var atNicknames: [String] = []
|
||||
var isAtAll: Bool = false
|
||||
|
||||
func with(avatar: UIImage? = nil,
|
||||
showTime: Bool? = nil,
|
||||
isUploading: Bool? = nil,
|
||||
imageUrl: String? = nil,
|
||||
quotePreview: QuotePreview? = nil) -> ChatMessage {
|
||||
ChatMessage(
|
||||
id: id,
|
||||
isSelf: isSelf,
|
||||
senderId: senderId,
|
||||
avatar: avatar ?? self.avatar,
|
||||
senderName: senderName,
|
||||
content: content,
|
||||
voiceUrl: voiceUrl,
|
||||
imageUrl: imageUrl ?? self.imageUrl,
|
||||
imageWidth: imageWidth,
|
||||
imageHeight: imageHeight,
|
||||
timestamp: timestamp,
|
||||
showTime: showTime ?? self.showTime,
|
||||
isUploading: isUploading ?? self.isUploading,
|
||||
quotePreview: quotePreview ?? self.quotePreview,
|
||||
atUserIDs: atUserIDs,
|
||||
atNicknames: atNicknames,
|
||||
isAtAll: isAtAll
|
||||
)
|
||||
}
|
||||
|
||||
/// 正文 attributed(含 @ 高亮)
|
||||
func attributedContent(isOutgoing: Bool) -> NSAttributedString {
|
||||
let baseColor: UIColor = isOutgoing ? .white : UIColor(hexStr: "#333333")
|
||||
let atColor: UIColor = isOutgoing ? UIColor(hexStr: "#E8F8FF") : UIColor(hexStr: "#16B3FF")
|
||||
let result = NSMutableAttributedString(
|
||||
string: content,
|
||||
attributes: [
|
||||
.font: UIFont.systemFont(ofSize: 14),
|
||||
.foregroundColor: baseColor
|
||||
]
|
||||
)
|
||||
var tokens = atNicknames.map { "@\($0)" }
|
||||
if isAtAll { tokens.append("@所有人") }
|
||||
for token in Set(tokens) where !token.isEmpty && token != "@" {
|
||||
var searchRange = NSRange(location: 0, length: result.length)
|
||||
while searchRange.location < result.length {
|
||||
let found = (result.string as NSString).range(of: token, options: [], range: searchRange)
|
||||
if found.location == NSNotFound { break }
|
||||
result.addAttribute(.foregroundColor, value: atColor, range: found)
|
||||
result.addAttribute(.font, value: UIFont.systemFont(ofSize: 14, weight: .semibold), range: found)
|
||||
let next = found.location + found.length
|
||||
searchRange = NSRange(location: next, length: result.length - next)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
class GroupChatView: UIView {
|
||||
|
|
@ -102,14 +45,6 @@ class GroupChatView: UIView {
|
|||
/// 语音录制回调
|
||||
var onVoiceRecordState: ((VoiceRecordState) -> Void)?
|
||||
private var emojiPlaybackGeneration = 0
|
||||
private var quoteBarHeightConstraint: NSLayoutConstraint?
|
||||
|
||||
func setQuoteBarVisible(_ visible: Bool, summary: String? = nil) {
|
||||
quoteBar.isHidden = !visible
|
||||
quoteBarHeightConstraint?.constant = visible ? 36 : 0
|
||||
if let summary { quoteBarLabel.text = summary }
|
||||
UIView.animate(withDuration: 0.2) { self.layoutIfNeeded() }
|
||||
}
|
||||
|
||||
private func setupRx() {
|
||||
|
||||
|
|
@ -120,13 +55,11 @@ class GroupChatView: UIView {
|
|||
let needsReset = !emojiPanelView.isHidden
|
||||
|| !voiceRecordView.isHidden
|
||||
|| textField.isFirstResponder
|
||||
|| !mentionPickerView.isHidden
|
||||
|
||||
guard needsReset else { return }
|
||||
stopVisibleEmojiAnimations()
|
||||
emojiPanelView.isHidden = true
|
||||
voiceRecordView.isHidden = true
|
||||
mentionPickerView.isHidden = true
|
||||
if !excludeTextField { textField.resignFirstResponder() }
|
||||
UIView.animate(withDuration: 0.25) {
|
||||
self.bottomBar.layoutChain.bottom(kSafeBottomMargin + 20)
|
||||
|
|
@ -190,8 +123,6 @@ class GroupChatView: UIView {
|
|||
|
||||
addSubview(tableView)
|
||||
addSubview(chatWarningView)
|
||||
addSubview(quoteBar)
|
||||
addSubview(mentionPickerView)
|
||||
addSubview(bottomBar)
|
||||
addSubview(disableIMView)
|
||||
bottomBar.addSubview(bottomBarCornerView)
|
||||
|
|
@ -205,11 +136,6 @@ class GroupChatView: UIView {
|
|||
bottomBar.addSubview(addBtn)
|
||||
bottomBar.addSubview(sendBtn)
|
||||
|
||||
quoteBar.addSubview(quoteBarLabel)
|
||||
quoteBar.addSubview(quoteBarCloseBtn)
|
||||
mentionPickerView.addSubview(mentionPickerContentView)
|
||||
mentionPickerContentView.addSubview(mentionTableView)
|
||||
|
||||
navBgView.layoutChain
|
||||
.edges(excludingEdge: .bottom)
|
||||
.height(kNaviHeight)
|
||||
|
|
@ -271,28 +197,6 @@ class GroupChatView: UIView {
|
|||
.topToBottomOfView(navBarView)
|
||||
.edgesHorzontal()
|
||||
|
||||
quoteBar.layoutChain
|
||||
.edgesHorzontal(15)
|
||||
.bottomToTopOfView(bottomBar, offset: -6)
|
||||
quoteBarHeightConstraint = quoteBar.heightAnchor.constraint(equalToConstant: 0)
|
||||
quoteBarHeightConstraint?.isActive = true
|
||||
|
||||
quoteBarLabel.layoutChain
|
||||
.left(12).rightToLeftOfView(quoteBarCloseBtn, offset: -8)
|
||||
.centerY()
|
||||
|
||||
quoteBarCloseBtn.layoutChain
|
||||
.right(8).centerY()
|
||||
.width(24).height(24)
|
||||
|
||||
mentionPickerView.layoutChain
|
||||
.edgesHorzontal(15)
|
||||
.height(180)
|
||||
.bottomToTopOfView(quoteBar, offset: -6)
|
||||
|
||||
mentionPickerContentView.layoutChain.edges()
|
||||
mentionTableView.layoutChain.edges()
|
||||
|
||||
bottomBar.layoutChain
|
||||
.edgesHorzontal(15)
|
||||
.height(50)
|
||||
|
|
@ -491,63 +395,6 @@ class GroupChatView: UIView {
|
|||
return tv
|
||||
}()
|
||||
|
||||
// MARK: - Quote / Mention
|
||||
lazy var quoteBar: UIView = {
|
||||
let view = UIView()
|
||||
view.backgroundColor = UIColor(hexStr: "#EEF6FB")
|
||||
view.cornerRadius = 10
|
||||
view.isHidden = true
|
||||
return view
|
||||
}()
|
||||
|
||||
lazy var quoteBarLabel: UILabel = {
|
||||
let label = UILabel()
|
||||
label.font = .systemFont(ofSize: 12)
|
||||
label.textColor = UIColor(hexStr: "#666666")
|
||||
label.lineBreakMode = .byTruncatingTail
|
||||
return label
|
||||
}()
|
||||
|
||||
lazy var quoteBarCloseBtn: UIButton = {
|
||||
let btn = UIButton(type: .custom)
|
||||
btn.setTitle("✕", for: .normal)
|
||||
btn.setTitleColor(UIColor(hexStr: "#999999"), for: .normal)
|
||||
btn.titleLabel?.font = .systemFont(ofSize: 14)
|
||||
return btn
|
||||
}()
|
||||
|
||||
lazy var mentionPickerView: UIView = {
|
||||
let view = UIView()
|
||||
view.backgroundColor = .clear
|
||||
view.isHidden = true
|
||||
// 阴影在外层;圆角裁剪在内层,避免 clipsToBounds 吃掉阴影
|
||||
view.layer.shadowColor = UIColor(hexStr: "#0F2846").cgColor
|
||||
view.layer.shadowOpacity = 0.18
|
||||
view.layer.shadowRadius = 12
|
||||
view.layer.shadowOffset = CGSize(width: 0, height: 4)
|
||||
view.layer.masksToBounds = false
|
||||
return view
|
||||
}()
|
||||
|
||||
lazy var mentionPickerContentView: UIView = {
|
||||
let view = UIView()
|
||||
view.backgroundColor = .white
|
||||
view.layer.cornerRadius = 12
|
||||
view.layer.borderWidth = 0.5
|
||||
view.layer.borderColor = UIColor(hexStr: "#E5E5E5").cgColor
|
||||
view.clipsToBounds = true
|
||||
return view
|
||||
}()
|
||||
|
||||
lazy var mentionTableView: UITableView = {
|
||||
let tv = UITableView(frame: .zero, style: .plain)
|
||||
tv.separatorStyle = .singleLine
|
||||
tv.rowHeight = 44
|
||||
tv.backgroundColor = .white
|
||||
tv.register(UITableViewCell.self, forCellReuseIdentifier: "MentionCell")
|
||||
return tv
|
||||
}()
|
||||
|
||||
// MARK: - Bottom Bar
|
||||
lazy var bottomBar: UIView = {
|
||||
let view = UIView()
|
||||
|
|
@ -690,234 +537,33 @@ class GroupChatView: UIView {
|
|||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override func layoutSubviews() {
|
||||
super.layoutSubviews()
|
||||
if !mentionPickerView.isHidden {
|
||||
mentionPickerView.layer.shadowPath = UIBezierPath(
|
||||
roundedRect: mentionPickerView.bounds,
|
||||
cornerRadius: 12
|
||||
).cgPath
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Quote block(气泡外侧下方)
|
||||
final class ChatQuoteBlockView: UIControl {
|
||||
private let lineView: UIView = {
|
||||
let v = UIView()
|
||||
v.backgroundColor = UIColor(hexStr: "#16B3FF").withAlphaComponent(0.55)
|
||||
return v
|
||||
}()
|
||||
private let label: UILabel = {
|
||||
let lab = UILabel()
|
||||
lab.font = .systemFont(ofSize: 11)
|
||||
lab.textColor = UIColor(hexStr: "#666666")
|
||||
lab.numberOfLines = 2
|
||||
return lab
|
||||
}()
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
cornerRadius = 4
|
||||
backgroundColor = UIColor(hexStr: "#EEEEF0")
|
||||
isHidden = true
|
||||
// addSubview(lineView)
|
||||
addSubview(label)
|
||||
// lineView.layoutChain.left().top(2).bottom(2).width(2)
|
||||
label.layoutChain.edgesHorzontal(10).edgesVertical()
|
||||
// label.layoutChain.leftToRightOfView(lineView, offset: 6).right().top().bottom()
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
|
||||
|
||||
func configure(_ preview: QuotePreview?) {
|
||||
guard let preview else {
|
||||
isHidden = true
|
||||
return
|
||||
}
|
||||
isHidden = false
|
||||
let name = preview.senderName.isEmpty ? "" : "\(preview.senderName): "
|
||||
label.text = name + preview.summary
|
||||
label.textColor = UIColor(hexStr: "#666666")
|
||||
lineView.backgroundColor = UIColor(hexStr: "#16B3FF").withAlphaComponent(0.55)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - 气泡上方操作菜单
|
||||
final class ChatBubbleActionMenu: UIView {
|
||||
var onQuote: (() -> Void)?
|
||||
var onRevoke: (() -> Void)?
|
||||
var onDismiss: (() -> Void)?
|
||||
|
||||
private let panel = UIView()
|
||||
private let arrow = UIView()
|
||||
private let stack = UIStackView()
|
||||
private let dimView = UIControl()
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
backgroundColor = .clear
|
||||
|
||||
dimView.backgroundColor = .clear
|
||||
dimView.addTarget(self, action: #selector(handleDismiss), for: .touchUpInside)
|
||||
addSubview(dimView)
|
||||
dimView.translatesAutoresizingMaskIntoConstraints = false
|
||||
NSLayoutConstraint.activate([
|
||||
dimView.topAnchor.constraint(equalTo: topAnchor),
|
||||
dimView.leadingAnchor.constraint(equalTo: leadingAnchor),
|
||||
dimView.trailingAnchor.constraint(equalTo: trailingAnchor),
|
||||
dimView.bottomAnchor.constraint(equalTo: bottomAnchor)
|
||||
])
|
||||
|
||||
panel.backgroundColor = UIColor(hexStr: "#333333")
|
||||
panel.layer.cornerRadius = 10
|
||||
panel.clipsToBounds = true
|
||||
addSubview(panel)
|
||||
|
||||
stack.axis = .horizontal
|
||||
stack.alignment = .fill
|
||||
stack.distribution = .fill
|
||||
stack.spacing = 0
|
||||
panel.addSubview(stack)
|
||||
stack.translatesAutoresizingMaskIntoConstraints = false
|
||||
NSLayoutConstraint.activate([
|
||||
stack.topAnchor.constraint(equalTo: panel.topAnchor),
|
||||
stack.leadingAnchor.constraint(equalTo: panel.leadingAnchor),
|
||||
stack.trailingAnchor.constraint(equalTo: panel.trailingAnchor),
|
||||
stack.bottomAnchor.constraint(equalTo: panel.bottomAnchor),
|
||||
panel.heightAnchor.constraint(equalToConstant: 44)
|
||||
])
|
||||
|
||||
arrow.backgroundColor = UIColor(hexStr: "#333333")
|
||||
addSubview(arrow)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
|
||||
|
||||
func show(in host: UIView, anchor: UIView, showRevoke: Bool) {
|
||||
removeFromSuperview()
|
||||
host.addSubview(self)
|
||||
frame = host.bounds
|
||||
autoresizingMask = [.flexibleWidth, .flexibleHeight]
|
||||
|
||||
stack.arrangedSubviews.forEach {
|
||||
stack.removeArrangedSubview($0)
|
||||
$0.removeFromSuperview()
|
||||
}
|
||||
let quoteBtn = makeButton(title: "引用", action: #selector(handleQuote))
|
||||
quoteBtn.widthAnchor.constraint(equalToConstant: 72).isActive = true
|
||||
stack.addArrangedSubview(quoteBtn)
|
||||
if showRevoke {
|
||||
let sep = UIView()
|
||||
sep.backgroundColor = UIColor.white.withAlphaComponent(0.2)
|
||||
sep.translatesAutoresizingMaskIntoConstraints = false
|
||||
sep.widthAnchor.constraint(equalToConstant: 1).isActive = true
|
||||
stack.addArrangedSubview(sep)
|
||||
let revokeBtn = makeButton(title: "撤回", action: #selector(handleRevoke))
|
||||
revokeBtn.widthAnchor.constraint(equalToConstant: 72).isActive = true
|
||||
stack.addArrangedSubview(revokeBtn)
|
||||
}
|
||||
|
||||
let panelW: CGFloat = showRevoke ? 145 : 72
|
||||
let panelH: CGFloat = 44
|
||||
let arrowSize: CGFloat = 8
|
||||
let gap: CGFloat = 6
|
||||
|
||||
layoutIfNeeded()
|
||||
let anchorRect = anchor.convert(anchor.bounds, to: host)
|
||||
var panelX = anchorRect.midX - panelW / 2
|
||||
panelX = max(12, min(panelX, host.bounds.width - panelW - 12))
|
||||
|
||||
var panelY = anchorRect.minY - gap - panelH - arrowSize
|
||||
var arrowAbove = true
|
||||
if panelY < host.safeAreaInsets.top + 8 {
|
||||
panelY = anchorRect.maxY + gap + arrowSize
|
||||
arrowAbove = false
|
||||
}
|
||||
|
||||
panel.frame = CGRect(x: panelX, y: panelY, width: panelW, height: panelH)
|
||||
let arrowX = min(max(anchorRect.midX - arrowSize / 2, panelX + 10), panelX + panelW - arrowSize - 10)
|
||||
if arrowAbove {
|
||||
arrow.frame = CGRect(x: arrowX, y: panel.frame.maxY - 1, width: arrowSize, height: arrowSize)
|
||||
} else {
|
||||
arrow.frame = CGRect(x: arrowX, y: panel.frame.minY - arrowSize + 1, width: arrowSize, height: arrowSize)
|
||||
}
|
||||
arrow.transform = CGAffineTransform(rotationAngle: .pi / 4)
|
||||
|
||||
alpha = 0
|
||||
UIView.animate(withDuration: 0.15) { self.alpha = 1 }
|
||||
}
|
||||
|
||||
private func makeButton(title: String, action: Selector) -> UIButton {
|
||||
let btn = UIButton(type: .system)
|
||||
btn.setTitle(title, for: .normal)
|
||||
btn.setTitleColor(.white, for: .normal)
|
||||
btn.titleLabel?.font = .systemFont(ofSize: 14, weight: .medium)
|
||||
btn.addTarget(self, action: action, for: .touchUpInside)
|
||||
btn.translatesAutoresizingMaskIntoConstraints = false
|
||||
return btn
|
||||
}
|
||||
|
||||
@objc private func handleQuote() {
|
||||
onQuote?()
|
||||
dismiss()
|
||||
}
|
||||
|
||||
@objc private func handleRevoke() {
|
||||
onRevoke?()
|
||||
dismiss()
|
||||
}
|
||||
|
||||
@objc private func handleDismiss() { dismiss() }
|
||||
|
||||
func dismiss() {
|
||||
UIView.animate(withDuration: 0.12, animations: {
|
||||
self.alpha = 0
|
||||
}, completion: { _ in
|
||||
self.onDismiss?()
|
||||
self.removeFromSuperview()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
private func chatFormatTime(_ t: TimeInterval) -> String {
|
||||
let date = Date(timeIntervalSince1970: t)
|
||||
let now = Date()
|
||||
let calendar = Calendar.current
|
||||
let f = DateFormatter()
|
||||
if calendar.isDateInToday(date) { f.dateFormat = "HH:mm" }
|
||||
else if calendar.isDateInYesterday(date) { f.dateFormat = "'昨天' HH:mm" }
|
||||
else if calendar.isDate(date, equalTo: now, toGranularity: .year) { f.dateFormat = "M-d HH:mm" }
|
||||
else { f.dateFormat = "yyyy-M-d HH:mm" }
|
||||
return f.string(from: date)
|
||||
}
|
||||
|
||||
// MARK: - 发送的消息cell
|
||||
class TextSendMsgCell: UITableViewCell {
|
||||
|
||||
var onQuoteTap: (() -> Void)?
|
||||
private(set) var messageId: String = ""
|
||||
var menuAnchorView: UIView { bubbleView }
|
||||
|
||||
func configure(_ msg: ChatMessage) {
|
||||
messageId = msg.id
|
||||
timeLabel.isHidden = !msg.showTime
|
||||
timeLabel.text = msg.showTime ? chatFormatTime(msg.timestamp) : nil
|
||||
timeLabel.text = msg.showTime ? formatTime(msg.timestamp) : nil
|
||||
avatarView.image = msg.avatar
|
||||
contentLabel.attributedText = msg.attributedContent(isOutgoing: true)
|
||||
let hasQuote = msg.quotePreview != nil
|
||||
quoteBlock.configure(msg.quotePreview)
|
||||
quoteHeightConstraint?.constant = hasQuote ? 28 : 0
|
||||
quoteTopConstraint?.constant = hasQuote ? 4 : 0
|
||||
contentLabel.text = msg.content
|
||||
}
|
||||
|
||||
func flashHighlight() {
|
||||
let old = contentView.backgroundColor
|
||||
contentView.backgroundColor = UIColor(hexStr: "#16B3FF").withAlphaComponent(0.12)
|
||||
UIView.animate(withDuration: 0.8, delay: 0.3, options: []) {
|
||||
self.contentView.backgroundColor = old
|
||||
private func formatTime(_ t: TimeInterval) -> String {
|
||||
let date = Date(timeIntervalSince1970: t)
|
||||
let now = Date()
|
||||
let calendar = Calendar.current
|
||||
let f = DateFormatter()
|
||||
if calendar.isDateInToday(date) {
|
||||
f.dateFormat = "HH:mm"
|
||||
} else if calendar.isDateInYesterday(date) {
|
||||
f.dateFormat = "'昨天' HH:mm"
|
||||
} else if calendar.isDate(date, equalTo: now, toGranularity: .year) {
|
||||
f.dateFormat = "M-d HH:mm"
|
||||
} else {
|
||||
f.dateFormat = "yyyy-M-d HH:mm"
|
||||
}
|
||||
return f.string(from: date)
|
||||
}
|
||||
|
||||
private let timeLabel: UILabel = {
|
||||
|
|
@ -928,7 +574,7 @@ class TextSendMsgCell: UITableViewCell {
|
|||
return label
|
||||
}()
|
||||
|
||||
let avatarView: UIImageView = {
|
||||
private let avatarView: UIImageView = {
|
||||
let iv = UIImageView()
|
||||
iv.contentMode = .scaleAspectFill
|
||||
iv.cornerRadius = 15
|
||||
|
|
@ -952,99 +598,77 @@ class TextSendMsgCell: UITableViewCell {
|
|||
return label
|
||||
}()
|
||||
|
||||
private let quoteBlock = ChatQuoteBlockView()
|
||||
private var quoteHeightConstraint: NSLayoutConstraint?
|
||||
private var quoteTopConstraint: NSLayoutConstraint?
|
||||
|
||||
override init(style: CellStyle, reuseIdentifier: String?) {
|
||||
super.init(style: style, reuseIdentifier: reuseIdentifier)
|
||||
selectionStyle = .none
|
||||
backgroundColor = .clear
|
||||
|
||||
contentView.addSubview(timeLabel)
|
||||
contentView.addSubview(bubbleView)
|
||||
bubbleView.addSubview(contentLabel)
|
||||
contentView.addSubview(quoteBlock)
|
||||
contentView.addSubview(timeLabel)
|
||||
contentView.addSubview(avatarView)
|
||||
|
||||
timeLabel.layoutChain.top().centerX()
|
||||
timeLabel.layoutChain
|
||||
.top()
|
||||
.centerX()
|
||||
|
||||
avatarView.layoutChain
|
||||
.topToBottomOfView(timeLabel, offset: 14)
|
||||
.right(12).width(30).height(30)
|
||||
.right(12)
|
||||
.width(30).height(30)
|
||||
|
||||
bubbleView.layoutChain
|
||||
.topToBottomOfView(avatarView, offset: -15)
|
||||
.rightToView(avatarView, offset: -13)
|
||||
.left(60, relation: .greaterThanOrEqual)
|
||||
.width(100, relation: .greaterThanOrEqual)
|
||||
.height(30, relation: .greaterThanOrEqual)
|
||||
.bottom(10)
|
||||
|
||||
contentLabel.translatesAutoresizingMaskIntoConstraints = false
|
||||
quoteBlock.translatesAutoresizingMaskIntoConstraints = false
|
||||
NSLayoutConstraint.activate([
|
||||
contentLabel.topAnchor.constraint(equalTo: bubbleView.topAnchor, constant: 10),
|
||||
contentLabel.leadingAnchor.constraint(equalTo: bubbleView.leadingAnchor, constant: 20),
|
||||
contentLabel.trailingAnchor.constraint(equalTo: bubbleView.trailingAnchor, constant: -20),
|
||||
contentLabel.bottomAnchor.constraint(equalTo: bubbleView.bottomAnchor, constant: -10)
|
||||
])
|
||||
|
||||
let qTop = quoteBlock.topAnchor.constraint(equalTo: bubbleView.bottomAnchor, constant: 4)
|
||||
let qH = quoteBlock.heightAnchor.constraint(equalToConstant: 0)
|
||||
quoteTopConstraint = qTop
|
||||
quoteHeightConstraint = qH
|
||||
NSLayoutConstraint.activate([
|
||||
qTop,
|
||||
quoteBlock.trailingAnchor.constraint(equalTo: bubbleView.trailingAnchor),
|
||||
quoteBlock.leadingAnchor.constraint(greaterThanOrEqualTo: contentView.leadingAnchor, constant: 60),
|
||||
quoteBlock.widthAnchor.constraint(lessThanOrEqualTo: bubbleView.widthAnchor),
|
||||
quoteBlock.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -10),
|
||||
qH
|
||||
])
|
||||
quoteBlock.addTarget(self, action: #selector(handleQuoteTap), for: .touchUpInside)
|
||||
contentLabel.layoutChain
|
||||
.edgesVertical(10)
|
||||
.edgesHorzontal(20)
|
||||
}
|
||||
|
||||
@objc private func handleQuoteTap() { onQuoteTap?() }
|
||||
|
||||
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override func layoutSubviews() {
|
||||
super.layoutSubviews()
|
||||
bubbleView.setNeedsLayout()
|
||||
bubbleView.layoutIfNeeded()
|
||||
bubbleView.setCornerRadius(corners: [.topLeft, .bottomLeft, .bottomRight],
|
||||
bubbleView.setCornerRadius(corners: [.topLeft ,.bottomLeft, .bottomRight],
|
||||
withCornerRadii: CGSize(width: bubbleView.dl.height / 2, height: bubbleView.dl.height / 2))
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - 收到的消息cell
|
||||
//MARK: - 收到的消息cell
|
||||
class TextReceivedMsgCell: UITableViewCell {
|
||||
|
||||
var onQuoteTap: (() -> Void)?
|
||||
private(set) var messageId: String = ""
|
||||
private(set) var senderId: String = ""
|
||||
private(set) var senderName: String = ""
|
||||
var menuAnchorView: UIView { bubbleView }
|
||||
|
||||
func configure(_ msg: ChatMessage) {
|
||||
messageId = msg.id
|
||||
senderId = msg.senderId
|
||||
senderName = msg.senderName
|
||||
timeLabel.isHidden = !msg.showTime
|
||||
timeLabel.text = msg.showTime ? chatFormatTime(msg.timestamp) : nil
|
||||
timeLabel.text = msg.showTime ? formatTime(msg.timestamp) : nil
|
||||
avatarView.image = msg.avatar
|
||||
nameLabel.text = msg.senderName
|
||||
contentLabel.attributedText = msg.attributedContent(isOutgoing: false)
|
||||
let hasQuote = msg.quotePreview != nil
|
||||
quoteBlock.configure(msg.quotePreview)
|
||||
quoteHeightConstraint?.constant = hasQuote ? 28 : 0
|
||||
quoteTopConstraint?.constant = hasQuote ? 4 : 0
|
||||
contentLabel.text = msg.content
|
||||
}
|
||||
|
||||
func flashHighlight() {
|
||||
let old = contentView.backgroundColor
|
||||
contentView.backgroundColor = UIColor(hexStr: "#16B3FF").withAlphaComponent(0.12)
|
||||
UIView.animate(withDuration: 0.8, delay: 0.3, options: []) {
|
||||
self.contentView.backgroundColor = old
|
||||
private func formatTime(_ t: TimeInterval) -> String {
|
||||
let date = Date(timeIntervalSince1970: t)
|
||||
let now = Date()
|
||||
let calendar = Calendar.current
|
||||
let f = DateFormatter()
|
||||
if calendar.isDateInToday(date) {
|
||||
f.dateFormat = "HH:mm"
|
||||
} else if calendar.isDateInYesterday(date) {
|
||||
f.dateFormat = "'昨天' HH:mm"
|
||||
} else if calendar.isDate(date, equalTo: now, toGranularity: .year) {
|
||||
f.dateFormat = "M-d HH:mm"
|
||||
} else {
|
||||
f.dateFormat = "yyyy-M-d HH:mm"
|
||||
}
|
||||
return f.string(from: date)
|
||||
}
|
||||
|
||||
private let timeLabel: UILabel = {
|
||||
|
|
@ -1062,7 +686,7 @@ class TextReceivedMsgCell: UITableViewCell {
|
|||
return label
|
||||
}()
|
||||
|
||||
let avatarView: UIImageView = {
|
||||
private let avatarView: UIImageView = {
|
||||
let iv = UIImageView()
|
||||
iv.contentMode = .scaleAspectFill
|
||||
iv.cornerRadius = 15
|
||||
|
|
@ -1070,7 +694,6 @@ class TextReceivedMsgCell: UITableViewCell {
|
|||
iv.backgroundColor = UIColor(hexStr: "#E0E0E0")
|
||||
iv.borderWidth = 2
|
||||
iv.borderColor = .white
|
||||
iv.isUserInteractionEnabled = true
|
||||
return iv
|
||||
}()
|
||||
|
||||
|
|
@ -1087,10 +710,6 @@ class TextReceivedMsgCell: UITableViewCell {
|
|||
return label
|
||||
}()
|
||||
|
||||
private let quoteBlock = ChatQuoteBlockView()
|
||||
private var quoteHeightConstraint: NSLayoutConstraint?
|
||||
private var quoteTopConstraint: NSLayoutConstraint?
|
||||
|
||||
override init(style: CellStyle, reuseIdentifier: String?) {
|
||||
super.init(style: style, reuseIdentifier: reuseIdentifier)
|
||||
selectionStyle = .none
|
||||
|
|
@ -1099,11 +718,13 @@ class TextReceivedMsgCell: UITableViewCell {
|
|||
contentView.addSubview(timeLabel)
|
||||
contentView.addSubview(bubbleView)
|
||||
bubbleView.addSubview(contentLabel)
|
||||
contentView.addSubview(quoteBlock)
|
||||
contentView.addSubview(avatarView)
|
||||
contentView.addSubview(nameLabel)
|
||||
|
||||
timeLabel.layoutChain.top().centerX()
|
||||
timeLabel.layoutChain
|
||||
.top()
|
||||
.centerX()
|
||||
|
||||
bubbleView.layoutChain
|
||||
.topToBottomOfView(timeLabel, offset: 14)
|
||||
.left(30)
|
||||
|
|
@ -1111,49 +732,30 @@ class TextReceivedMsgCell: UITableViewCell {
|
|||
.width(100, relation: .greaterThanOrEqual)
|
||||
.height(30, relation: .greaterThanOrEqual)
|
||||
|
||||
contentLabel.translatesAutoresizingMaskIntoConstraints = false
|
||||
quoteBlock.translatesAutoresizingMaskIntoConstraints = false
|
||||
avatarView.translatesAutoresizingMaskIntoConstraints = false
|
||||
nameLabel.translatesAutoresizingMaskIntoConstraints = false
|
||||
NSLayoutConstraint.activate([
|
||||
contentLabel.topAnchor.constraint(equalTo: bubbleView.topAnchor, constant: 10),
|
||||
contentLabel.leadingAnchor.constraint(equalTo: bubbleView.leadingAnchor, constant: 20),
|
||||
contentLabel.trailingAnchor.constraint(equalTo: bubbleView.trailingAnchor, constant: -20),
|
||||
contentLabel.bottomAnchor.constraint(equalTo: bubbleView.bottomAnchor, constant: -10),
|
||||
// 头像 + 昵称贴在气泡下方
|
||||
avatarView.topAnchor.constraint(equalTo: bubbleView.bottomAnchor, constant: -15),
|
||||
avatarView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 12),
|
||||
avatarView.widthAnchor.constraint(equalToConstant: 30),
|
||||
avatarView.heightAnchor.constraint(equalToConstant: 30),
|
||||
nameLabel.leadingAnchor.constraint(equalTo: avatarView.trailingAnchor, constant: 5),
|
||||
nameLabel.bottomAnchor.constraint(equalTo: avatarView.bottomAnchor)
|
||||
])
|
||||
avatarView.layoutChain
|
||||
.topToBottomOfView(bubbleView, offset: -15)
|
||||
.left(12)
|
||||
.width(30).height(30)
|
||||
.bottom(10)
|
||||
|
||||
// 引用在 senderName 下方
|
||||
let qTop = quoteBlock.topAnchor.constraint(equalTo: nameLabel.bottomAnchor, constant: 4)
|
||||
let qH = quoteBlock.heightAnchor.constraint(equalToConstant: 0)
|
||||
quoteTopConstraint = qTop
|
||||
quoteHeightConstraint = qH
|
||||
NSLayoutConstraint.activate([
|
||||
qTop,
|
||||
quoteBlock.leadingAnchor.constraint(equalTo: nameLabel.leadingAnchor),
|
||||
quoteBlock.trailingAnchor.constraint(lessThanOrEqualTo: contentView.trailingAnchor, constant: -60),
|
||||
quoteBlock.widthAnchor.constraint(lessThanOrEqualTo: bubbleView.widthAnchor),
|
||||
quoteBlock.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -10),
|
||||
qH
|
||||
])
|
||||
quoteBlock.addTarget(self, action: #selector(handleQuoteTap), for: .touchUpInside)
|
||||
nameLabel.layoutChain
|
||||
.leftToRightOfView(avatarView, offset: 5)
|
||||
.bottomToView(avatarView)
|
||||
|
||||
contentLabel.layoutChain
|
||||
.edgesVertical(10)
|
||||
.edgesHorzontal(20)
|
||||
}
|
||||
|
||||
@objc private func handleQuoteTap() { onQuoteTap?() }
|
||||
|
||||
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override func layoutSubviews() {
|
||||
super.layoutSubviews()
|
||||
bubbleView.setNeedsLayout()
|
||||
bubbleView.layoutIfNeeded()
|
||||
bubbleView.setCornerRadius(corners: [.topLeft, .topRight, .bottomRight],
|
||||
bubbleView.setCornerRadius(corners: [.topLeft , .topRight, .bottomRight],
|
||||
withCornerRadii: CGSize(width: bubbleView.dl.height / 2, height: bubbleView.dl.height / 2))
|
||||
}
|
||||
}
|
||||
|
|
@ -1221,7 +823,6 @@ final class NotificationMsgCell: UITableViewCell {
|
|||
|
||||
// MARK: - 发送的表情消息
|
||||
final class EmojiSendMsgCell: UITableViewCell {
|
||||
var menuAnchorView: UIView { lottieView }
|
||||
|
||||
private let timeLabel: UILabel = {
|
||||
let label = UILabel()
|
||||
|
|
@ -1337,7 +938,6 @@ final class EmojiSendMsgCell: UITableViewCell {
|
|||
|
||||
// MARK: - 收到的表情消息
|
||||
final class EmojiReceivedMsgCell: UITableViewCell {
|
||||
var menuAnchorView: UIView { lottieView }
|
||||
|
||||
private let timeLabel: UILabel = {
|
||||
let label = UILabel()
|
||||
|
|
@ -1347,7 +947,7 @@ final class EmojiReceivedMsgCell: UITableViewCell {
|
|||
return label
|
||||
}()
|
||||
|
||||
let avatarView: UIImageView = {
|
||||
private let avatarView: UIImageView = {
|
||||
let iv = UIImageView()
|
||||
iv.contentMode = .scaleAspectFill
|
||||
iv.cornerRadius = 15
|
||||
|
|
@ -1355,7 +955,6 @@ final class EmojiReceivedMsgCell: UITableViewCell {
|
|||
iv.backgroundColor = UIColor(hexStr: "#E0E0E0")
|
||||
iv.borderWidth = 2
|
||||
iv.borderColor = .white
|
||||
iv.isUserInteractionEnabled = true
|
||||
return iv
|
||||
}()
|
||||
|
||||
|
|
@ -1502,7 +1101,6 @@ extension GroupChatView: UICollectionViewDelegate {
|
|||
|
||||
// MARK: - 发送的语音消息
|
||||
final class VoiceSendMsgCell: UITableViewCell, VoicePlaybackView {
|
||||
var menuAnchorView: UIView { bubbleView }
|
||||
|
||||
private let timeLabel: UILabel = {
|
||||
let label = UILabel()
|
||||
|
|
@ -1640,7 +1238,6 @@ final class VoiceSendMsgCell: UITableViewCell, VoicePlaybackView {
|
|||
|
||||
// MARK: - 收到的语音消息
|
||||
final class VoiceReceivedMsgCell: UITableViewCell, VoicePlaybackView {
|
||||
var menuAnchorView: UIView { bubbleView }
|
||||
|
||||
func configure(_ msg: ChatMessage) {
|
||||
timeLabel.isHidden = !msg.showTime
|
||||
|
|
@ -1692,7 +1289,7 @@ final class VoiceReceivedMsgCell: UITableViewCell, VoicePlaybackView {
|
|||
return label
|
||||
}()
|
||||
|
||||
let avatarView: UIImageView = {
|
||||
private let avatarView: UIImageView = {
|
||||
let iv = UIImageView()
|
||||
iv.contentMode = .scaleAspectFill
|
||||
iv.cornerRadius = 15
|
||||
|
|
@ -1700,7 +1297,6 @@ final class VoiceReceivedMsgCell: UITableViewCell, VoicePlaybackView {
|
|||
iv.backgroundColor = UIColor(hexStr: "#E0E0E0")
|
||||
iv.borderWidth = 2
|
||||
iv.borderColor = .white
|
||||
iv.isUserInteractionEnabled = true
|
||||
return iv
|
||||
}()
|
||||
|
||||
|
|
@ -1798,7 +1394,7 @@ final class VoiceReceivedMsgCell: UITableViewCell, VoicePlaybackView {
|
|||
|
||||
// MARK: - 发送的图片消息
|
||||
final class ImageSendMsgCell: UITableViewCell {
|
||||
var menuAnchorView: UIView { photoView }
|
||||
|
||||
var onImageTap: (() -> Void)?
|
||||
|
||||
func configure(_ msg: ChatMessage) {
|
||||
|
|
@ -1908,7 +1504,7 @@ final class ImageSendMsgCell: UITableViewCell {
|
|||
|
||||
// MARK: - 收到的图片消息
|
||||
final class ImageReceivedMsgCell: UITableViewCell {
|
||||
var menuAnchorView: UIView { photoView }
|
||||
|
||||
var onImageTap: (() -> Void)?
|
||||
|
||||
func configure(_ msg: ChatMessage) {
|
||||
|
|
@ -1953,7 +1549,7 @@ final class ImageReceivedMsgCell: UITableViewCell {
|
|||
return label
|
||||
}()
|
||||
|
||||
let avatarView: UIImageView = {
|
||||
private let avatarView: UIImageView = {
|
||||
let iv = UIImageView()
|
||||
iv.contentMode = .scaleAspectFill
|
||||
iv.cornerRadius = 15
|
||||
|
|
@ -1961,7 +1557,6 @@ final class ImageReceivedMsgCell: UITableViewCell {
|
|||
iv.backgroundColor = UIColor(hexStr: "#E0E0E0")
|
||||
iv.borderWidth = 2
|
||||
iv.borderColor = .white
|
||||
iv.isUserInteractionEnabled = true
|
||||
return iv
|
||||
}()
|
||||
|
||||
|
|
|
|||
|
|
@ -11,17 +11,6 @@ import RxCocoa
|
|||
import OpenIMSDK
|
||||
import Differentiator
|
||||
|
||||
enum QuoteKind {
|
||||
case text, image, voice, emoji, unknown
|
||||
}
|
||||
|
||||
struct QuotePreview {
|
||||
let senderName: String
|
||||
let kind: QuoteKind
|
||||
let summary: String
|
||||
let quotedClientMsgID: String?
|
||||
}
|
||||
|
||||
enum ChatSectionItem {
|
||||
case send(ChatMessage)
|
||||
case received(ChatMessage)
|
||||
|
|
@ -32,7 +21,6 @@ enum ChatSectionItem {
|
|||
case imageSend(ChatMessage)
|
||||
case imageReceived(ChatMessage)
|
||||
case notification(NSAttributedString, showTime: Bool = false, timestamp: TimeInterval = 0)
|
||||
case revoked(NSAttributedString, showTime: Bool = false, timestamp: TimeInterval = 0)
|
||||
}
|
||||
|
||||
typealias ChatSectionModel = SectionModel<String, ChatSectionItem>
|
||||
|
|
@ -45,7 +33,6 @@ final class GroupChatViewModel {
|
|||
|
||||
struct Output {
|
||||
let messages: Observable<[ChatSectionItem]>
|
||||
let pendingQuoteSummary: Observable<String?>
|
||||
}
|
||||
|
||||
let input: Input
|
||||
|
|
@ -53,42 +40,22 @@ final class GroupChatViewModel {
|
|||
|
||||
private let messagesSubject = BehaviorRelay<[ChatSectionItem]>(value: [])
|
||||
private let sendMessageSubject = PublishSubject<String>()
|
||||
private let pendingQuoteSummarySubject = BehaviorRelay<String?>(value: nil)
|
||||
private var lastTimeGap: TimeInterval = 0
|
||||
private let timeGapThreshold: TimeInterval = 300
|
||||
/// 两条消息间隔超过此值(秒)显示时间戳
|
||||
private let timeGapThreshold: TimeInterval = 300 // 5 minutes
|
||||
|
||||
var groupModel: GroupInfoModel?
|
||||
var memberList: [GroupMemberModel] = [] {
|
||||
didSet { buildAvatarCache() }
|
||||
didSet {
|
||||
buildAvatarCache()
|
||||
}
|
||||
}
|
||||
var groupId: String = ""
|
||||
|
||||
/// 引用中的原始消息
|
||||
private(set) var pendingQuote: OIMMessageInfo?
|
||||
/// 待发送 @ 成员
|
||||
private(set) var pendingMentions: [(userId: String, nickname: String)] = []
|
||||
private(set) var pendingAtAll: Bool = false
|
||||
|
||||
/// clientMsgID → 原始消息(引用 / 定位)
|
||||
private(set) var messageCache: [String: OIMMessageInfo] = [:]
|
||||
|
||||
/// 定位跳转 / 加载更早历史时抑制自动滚底
|
||||
var suppressAutoScroll = false
|
||||
/// 是否还有更早历史
|
||||
private(set) var hasMoreHistory = true
|
||||
private var isLoadingOlder = false
|
||||
|
||||
private let disposeBag = DisposeBag()
|
||||
private let emojiPattern = try? NSRegularExpression(pattern: "^js_emoji:(\\d+)$", options: [])
|
||||
private let historyPageSize = 30
|
||||
|
||||
// MARK: - Init
|
||||
init() {
|
||||
input = Input(sendMessage: sendMessageSubject.asObserver())
|
||||
output = Output(
|
||||
messages: messagesSubject.asObservable(),
|
||||
pendingQuoteSummary: pendingQuoteSummarySubject.asObservable()
|
||||
)
|
||||
output = Output(messages: messagesSubject.asObservable())
|
||||
|
||||
sendMessageSubject
|
||||
.subscribe(onNext: { [weak self] text in
|
||||
|
|
@ -97,36 +64,54 @@ final class GroupChatViewModel {
|
|||
.disposed(by: disposeBag)
|
||||
}
|
||||
|
||||
var currentMessages: [ChatSectionItem] { messagesSubject.value }
|
||||
|
||||
var conversationID: String { "sg_\(groupId)" }
|
||||
private let disposeBag = DisposeBag()
|
||||
|
||||
// MARK: - Avatar
|
||||
private var avatarCache: [String: UIImage] = [:]
|
||||
|
||||
/// memberList 更新后调用,预构建 userID → UIImage 映射,避免 30 条消息逐一遍历
|
||||
func buildAvatarCache() {
|
||||
var cache: [String: UIImage] = [:]
|
||||
for member in memberList {
|
||||
cache[member.user_id] = member.userIcon
|
||||
}
|
||||
avatarCache = cache
|
||||
// 刷新已有消息的头像(并行加载时 loadMessages 可能先于 memberList 完成)
|
||||
refreshMessageAvatars()
|
||||
}
|
||||
|
||||
/// 用 avatarCache 刷新已有消息的头像(并行加载时 loadMessages 可能先于 memberList 完成)
|
||||
private func refreshMessageAvatars() {
|
||||
var items = messagesSubject.value
|
||||
var didChange = false
|
||||
items = items.map { item in
|
||||
guard var m = item.chatMessage, updateAvatar(&m) else { return item }
|
||||
didChange = true
|
||||
return ChatSectionItem.with(m)
|
||||
switch item {
|
||||
case var .send(m): if updateAvatar(&m) { didChange = true }; return .send(m)
|
||||
case var .received(m): if updateAvatar(&m) { didChange = true }; return .received(m)
|
||||
case var .emojiSend(m): if updateAvatar(&m) { didChange = true }; return .emojiSend(m)
|
||||
case var .emojiReceived(m): if updateAvatar(&m) { didChange = true }; return .emojiReceived(m)
|
||||
case var .voiceSend(m): if updateAvatar(&m) { didChange = true }; return .voiceSend(m)
|
||||
case var .voiceReceived(m): if updateAvatar(&m) { didChange = true }; return .voiceReceived(m)
|
||||
case var .imageSend(m): if updateAvatar(&m) { didChange = true }; return .imageSend(m)
|
||||
case var .imageReceived(m): if updateAvatar(&m) { didChange = true }; return .imageReceived(m)
|
||||
default: return item
|
||||
}
|
||||
}
|
||||
if didChange {
|
||||
messagesSubject.accept(items)
|
||||
}
|
||||
if didChange { messagesSubject.accept(items) }
|
||||
}
|
||||
|
||||
/// 尝试用缓存更新单条消息的头像,返回是否变更
|
||||
private func updateAvatar(_ msg: inout ChatMessage) -> Bool {
|
||||
guard let cached = avatarCache[msg.senderId], cached != msg.avatar else { return false }
|
||||
msg = msg.with(avatar: cached)
|
||||
msg = ChatMessage(
|
||||
id: msg.id, isSelf: msg.isSelf, senderId: msg.senderId,
|
||||
avatar: cached, senderName: msg.senderName,
|
||||
content: msg.content, voiceUrl: msg.voiceUrl, imageUrl: msg.imageUrl,
|
||||
imageWidth: msg.imageWidth, imageHeight: msg.imageHeight,
|
||||
timestamp: msg.timestamp, showTime: msg.showTime, isUploading: msg.isUploading
|
||||
)
|
||||
return true
|
||||
}
|
||||
|
||||
|
|
@ -144,359 +129,29 @@ final class GroupChatViewModel {
|
|||
memberList.first { id == $0.user_id }?.nick_name ?? ""
|
||||
}
|
||||
|
||||
// MARK: - Quote / Mention state
|
||||
func setPendingQuote(_ msg: OIMMessageInfo?) {
|
||||
pendingQuote = msg
|
||||
if let msg {
|
||||
let preview = makeQuotePreview(from: msg)
|
||||
let name = preview.senderName.isEmpty ? "消息" : preview.senderName
|
||||
pendingQuoteSummarySubject.accept("\(name): \(preview.summary)")
|
||||
} else {
|
||||
pendingQuoteSummarySubject.accept(nil)
|
||||
}
|
||||
}
|
||||
|
||||
func clearPendingQuote() {
|
||||
setPendingQuote(nil)
|
||||
}
|
||||
|
||||
func mentionUser(userId: String, nickname: String) -> String {
|
||||
let name = nickname.isEmpty ? getUserNickName(id: userId) : nickname
|
||||
let display = name.isEmpty ? userId : name
|
||||
if !pendingMentions.contains(where: { $0.userId == userId }) {
|
||||
pendingMentions.append((userId, display))
|
||||
}
|
||||
return "@\(display) "
|
||||
}
|
||||
|
||||
func setPendingAtAll(_ value: Bool) {
|
||||
pendingAtAll = value
|
||||
}
|
||||
|
||||
func clearPendingMentions() {
|
||||
pendingMentions = []
|
||||
pendingAtAll = false
|
||||
}
|
||||
|
||||
func mentionCandidates(excludingSelf: Bool = true) -> [GroupMemberModel] {
|
||||
let selfId = AppContextManager.shared.userId
|
||||
return memberList.filter { !excludingSelf || $0.user_id != selfId }
|
||||
}
|
||||
|
||||
func oimMessage(forClientMsgID id: String) -> OIMMessageInfo? {
|
||||
messageCache[id]
|
||||
}
|
||||
|
||||
// MARK: - Send
|
||||
private func onSendMessage(_ text: String) {
|
||||
let trimmed = text.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
guard !trimmed.isEmpty else { return }
|
||||
|
||||
let quote = pendingQuote
|
||||
let atAll = pendingAtAll
|
||||
let mentions = pendingMentions
|
||||
|
||||
let msg: OIMMessageInfo
|
||||
if atAll {
|
||||
msg = OIMMessageInfo.createText(atAllMessage: trimmed,
|
||||
displayText: "@所有人",
|
||||
message: quote)
|
||||
} else if !mentions.isEmpty {
|
||||
let ids = mentions.map(\.userId)
|
||||
let infos: [OIMAtInfo] = mentions.map { m in
|
||||
let info = OIMAtInfo()
|
||||
info.atUserID = m.userId
|
||||
info.groupNickname = m.nickname
|
||||
return info
|
||||
}
|
||||
msg = OIMMessageInfo.createText(atMessage: trimmed,
|
||||
atUsersID: ids,
|
||||
atUsersInfo: infos,
|
||||
message: quote)
|
||||
} else if let quote {
|
||||
msg = OIMMessageInfo.createQuoteMessage(trimmed, message: quote)
|
||||
} else {
|
||||
msg = OIMMessageInfo.createTextMessage(trimmed)
|
||||
}
|
||||
|
||||
clearPendingQuote()
|
||||
clearPendingMentions()
|
||||
let msg = OIMMessageInfo.createTextMessage(text)
|
||||
|
||||
OIMManager.manager.sendMessage(msg,
|
||||
recvID: "",
|
||||
groupID: groupId,
|
||||
offlinePushInfo: nil,
|
||||
onSuccess: { [weak self] _ in
|
||||
self?.cacheMessage(msg)
|
||||
self?.appendMessage(msg)
|
||||
},
|
||||
onProgress: nil as OIMNumberCallback?,
|
||||
onFailure: { code, errMsg in
|
||||
print("send failed: \(code) \(errMsg ?? "")")
|
||||
DispatchQueue.main.async {
|
||||
DLToast.showError(text: errMsg ?? "发送失败")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// MARK: - Revoke
|
||||
func revokeMessage(clientMsgID: String) {
|
||||
OIMManager.manager.revokeMessage(conversationID,
|
||||
clientMsgID: clientMsgID,
|
||||
onSuccess: { [weak self] _ in
|
||||
DispatchQueue.main.async {
|
||||
self?.applyRevoke(clientMsgID: clientMsgID,
|
||||
revokerID: AppContextManager.shared.userId,
|
||||
revokerNickname: AppContextManager.shared.name)
|
||||
}
|
||||
}, onFailure: { code, errMsg in
|
||||
DispatchQueue.main.async {
|
||||
DLToast.showError(text: errMsg ?? "撤回失败(\(code))")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func onMessageRevoked(_ info: OIMMessageRevokedInfo) {
|
||||
applyRevoke(clientMsgID: info.clientMsgID,
|
||||
revokerID: info.revokerID,
|
||||
revokerNickname: info.revokerNickname)
|
||||
}
|
||||
|
||||
private func applyRevoke(clientMsgID: String, revokerID: String, revokerNickname: String) {
|
||||
if pendingQuote?.clientMsgID == clientMsgID {
|
||||
clearPendingQuote()
|
||||
}
|
||||
messageCache.removeValue(forKey: clientMsgID)
|
||||
|
||||
var items = messagesSubject.value
|
||||
guard let idx = items.firstIndex(where: { $0.chatMessage?.id == clientMsgID || $0.revokedClientMsgID == clientMsgID }) else {
|
||||
// 仍更新引用摘要文案
|
||||
refreshQuotePreviewsAfterRevoke(clientMsgID)
|
||||
return
|
||||
}
|
||||
let ts = timestampFrom(item: items[idx])
|
||||
let showTime: Bool = {
|
||||
switch items[idx] {
|
||||
case let .send(m), let .received(m), let .emojiSend(m), let .emojiReceived(m),
|
||||
let .voiceSend(m), let .voiceReceived(m), let .imageSend(m), let .imageReceived(m):
|
||||
return m.showTime
|
||||
case let .notification(_, s, _), let .revoked(_, s, _):
|
||||
return s
|
||||
}
|
||||
}()
|
||||
let tip = revokeTip(revokerID: revokerID, revokerNickname: revokerNickname)
|
||||
items[idx] = .revoked(tip, showTime: showTime, timestamp: ts)
|
||||
messagesSubject.accept(items)
|
||||
refreshQuotePreviewsAfterRevoke(clientMsgID)
|
||||
}
|
||||
|
||||
private func revokeTip(revokerID: String, revokerNickname: String) -> NSAttributedString {
|
||||
let isSelf = revokerID == AppContextManager.shared.userId
|
||||
let text = isSelf ? "你撤回了一条消息" : "\(revokerNickname.isEmpty ? "对方" : revokerNickname) 撤回了一条消息"
|
||||
return NSAttributedString(string: text, attributes: [
|
||||
.font: UIFont.systemFont(ofSize: 12),
|
||||
.foregroundColor: UIColor(hexStr: "#999999")
|
||||
])
|
||||
}
|
||||
|
||||
private func refreshQuotePreviewsAfterRevoke(_ revokedId: String) {
|
||||
var items = messagesSubject.value
|
||||
var changed = false
|
||||
for i in items.indices {
|
||||
guard var m = items[i].chatMessage,
|
||||
m.quotePreview?.quotedClientMsgID == revokedId else { continue }
|
||||
m.quotePreview = QuotePreview(
|
||||
senderName: m.quotePreview?.senderName ?? "",
|
||||
kind: .unknown,
|
||||
summary: "引用内容已撤回",
|
||||
quotedClientMsgID: revokedId
|
||||
)
|
||||
items[i] = ChatSectionItem.with(m)
|
||||
changed = true
|
||||
}
|
||||
if changed { messagesSubject.accept(items) }
|
||||
}
|
||||
|
||||
// MARK: - Locate quoted message
|
||||
func indexPath(forClientMsgID id: String) -> IndexPath? {
|
||||
guard let idx = messagesSubject.value.firstIndex(where: { $0.chatMessage?.id == id }) else { return nil }
|
||||
return IndexPath(row: idx, section: 0)
|
||||
}
|
||||
|
||||
/// 若目标不在列表,向上分页加载直至找到或达上限
|
||||
func ensureMessageVisible(clientMsgID: String, maxPages: Int = 5, completion: @escaping (Bool) -> Void) {
|
||||
if indexPath(forClientMsgID: clientMsgID) != nil {
|
||||
completion(true)
|
||||
return
|
||||
}
|
||||
|
||||
// 先确认消息是否仍存在
|
||||
let param = OIMFindMessageListParam()
|
||||
param.conversationID = conversationID
|
||||
param.clientMsgIDList = [clientMsgID]
|
||||
OIMManager.manager.findMessageList([param], onSuccess: { [weak self] result in
|
||||
guard let self = self else { return }
|
||||
let found = (result?.findResultItems ?? []).contains { item in
|
||||
(item.messageList).contains { $0.clientMsgID == clientMsgID }
|
||||
}
|
||||
// 找不到或已撤回
|
||||
if !found {
|
||||
if let cached = self.messageCache[clientMsgID],
|
||||
cached.status.rawValue == OIMMessageStatus.revoke.rawValue || cached.contentType.rawValue == 2101 {
|
||||
DispatchQueue.main.async { completion(false) }
|
||||
return
|
||||
}
|
||||
// find 可能因 SDK 差异失败,仍尝试拉历史
|
||||
}
|
||||
self.loadOlderPages(targeting: clientMsgID, remaining: maxPages, completion: completion)
|
||||
}, onFailure: { [weak self] _, _ in
|
||||
self?.loadOlderPages(targeting: clientMsgID, remaining: maxPages, completion: completion)
|
||||
})
|
||||
}
|
||||
|
||||
/// 结果:是否成功拉取到新消息、是否还有更多
|
||||
struct LoadOlderResult {
|
||||
let prependedCount: Int
|
||||
let hasMore: Bool
|
||||
}
|
||||
|
||||
/// 下拉加载更早历史(供 UI 触发)
|
||||
func loadOlderHistory(completion: ((LoadOlderResult) -> Void)? = nil) {
|
||||
guard !isLoadingOlder else {
|
||||
completion?(LoadOlderResult(prependedCount: 0, hasMore: hasMoreHistory))
|
||||
return
|
||||
}
|
||||
guard hasMoreHistory else {
|
||||
completion?(LoadOlderResult(prependedCount: 0, hasMore: false))
|
||||
return
|
||||
}
|
||||
guard let oldest = oldestClientMsgID() else {
|
||||
hasMoreHistory = false
|
||||
completion?(LoadOlderResult(prependedCount: 0, hasMore: false))
|
||||
return
|
||||
}
|
||||
|
||||
isLoadingOlder = true
|
||||
fetchOlderHistory(startClientMsgID: oldest) { [weak self] list, isEnd, success in
|
||||
guard let self = self else { return }
|
||||
self.isLoadingOlder = false
|
||||
guard success else {
|
||||
DispatchQueue.main.async {
|
||||
completion?(LoadOlderResult(prependedCount: 0, hasMore: self.hasMoreHistory))
|
||||
}
|
||||
return
|
||||
}
|
||||
if list.isEmpty {
|
||||
self.hasMoreHistory = false
|
||||
DispatchQueue.main.async {
|
||||
completion?(LoadOlderResult(prependedCount: 0, hasMore: false))
|
||||
}
|
||||
return
|
||||
}
|
||||
let count = self.prependHistory(list)
|
||||
if isEnd || count == 0 {
|
||||
self.hasMoreHistory = false
|
||||
}
|
||||
DispatchQueue.main.async {
|
||||
completion?(LoadOlderResult(prependedCount: count, hasMore: self.hasMoreHistory))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func loadOlderPages(targeting clientMsgID: String, remaining: Int, completion: @escaping (Bool) -> Void) {
|
||||
if indexPath(forClientMsgID: clientMsgID) != nil {
|
||||
DispatchQueue.main.async { completion(true) }
|
||||
return
|
||||
}
|
||||
guard remaining > 0, hasMoreHistory else {
|
||||
DispatchQueue.main.async { completion(false) }
|
||||
return
|
||||
}
|
||||
loadOlderHistory { [weak self] result in
|
||||
guard let self = self else { return }
|
||||
if self.indexPath(forClientMsgID: clientMsgID) != nil {
|
||||
completion(true)
|
||||
} else if result.prependedCount > 0, result.hasMore {
|
||||
self.loadOlderPages(targeting: clientMsgID, remaining: remaining - 1, completion: completion)
|
||||
} else {
|
||||
completion(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func oldestClientMsgID() -> String? {
|
||||
for item in messagesSubject.value {
|
||||
if let id = item.chatMessage?.id { return id }
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
private func fetchOlderHistory(startClientMsgID: String,
|
||||
completion: @escaping ([OIMMessageInfo], Bool, Bool) -> Void) {
|
||||
let param = OIMGetAdvancedHistoryMessageListParam()
|
||||
param.conversationID = conversationID
|
||||
param.count = historyPageSize
|
||||
param.startClientMsgID = startClientMsgID
|
||||
|
||||
OIMManager.manager.getAdvancedHistoryMessageList(
|
||||
param,
|
||||
onSuccess: { [weak self] result in
|
||||
let pageSize = self?.historyPageSize ?? 30
|
||||
let list = result?.messageList ?? []
|
||||
let isEnd = result?.isEnd ?? (list.isEmpty || list.count < pageSize)
|
||||
completion(list, isEnd, true)
|
||||
},
|
||||
onFailure: { code, msg in
|
||||
print("loadOlderHistory failed: \(code) \(msg ?? "")")
|
||||
completion([], false, false)
|
||||
})
|
||||
}
|
||||
|
||||
/// 将更早消息插入列表头部,返回实际新增条数
|
||||
@discardableResult
|
||||
private func prependHistory(_ list: [OIMMessageInfo]) -> Int {
|
||||
var newItems: [ChatSectionItem] = []
|
||||
for msg in list {
|
||||
cacheMessage(msg)
|
||||
if let item = toSectionItem(msg) {
|
||||
newItems.append(item)
|
||||
}
|
||||
}
|
||||
newItems.sort { timestampFrom(item: $0) < timestampFrom(item: $1) }
|
||||
|
||||
let items = messagesSubject.value
|
||||
let existingIDs = Set(items.compactMap { $0.chatMessage?.id })
|
||||
newItems = newItems.filter { item in
|
||||
guard let id = item.chatMessage?.id else { return true }
|
||||
return !existingIDs.contains(id)
|
||||
}
|
||||
guard !newItems.isEmpty else { return 0 }
|
||||
|
||||
var result = newItems + items
|
||||
for i in result.indices {
|
||||
let ts = timestampFrom(item: result[i])
|
||||
let show = i == 0 || ts - timestampFrom(item: result[i - 1]) >= timeGapThreshold
|
||||
result[i] = setShowTime(result[i], show)
|
||||
}
|
||||
let count = newItems.count
|
||||
// 同步更新,便于调用方立刻根据 contentSize 校正 offset
|
||||
suppressAutoScroll = true
|
||||
if Thread.isMainThread {
|
||||
messagesSubject.accept(result)
|
||||
} else {
|
||||
DispatchQueue.main.sync {
|
||||
self.messagesSubject.accept(result)
|
||||
}
|
||||
}
|
||||
return count
|
||||
}
|
||||
|
||||
// MARK: - Load
|
||||
func loadMessages() {
|
||||
guard !groupId.isEmpty else { return }
|
||||
let param = OIMGetAdvancedHistoryMessageListParam()
|
||||
param.conversationID = conversationID
|
||||
param.count = historyPageSize
|
||||
param.conversationID = "sg_\(groupId)"
|
||||
param.count = 30
|
||||
|
||||
OIMManager.manager.getAdvancedHistoryMessageList(
|
||||
param,
|
||||
|
|
@ -505,27 +160,29 @@ final class GroupChatViewModel {
|
|||
let list = result?.messageList else { return }
|
||||
var items: [ChatSectionItem] = []
|
||||
for msg in list {
|
||||
self.cacheMessage(msg)
|
||||
if let item = self.toSectionItem(msg) {
|
||||
items.append(item)
|
||||
}
|
||||
}
|
||||
// messages come newest-first from API, sort by time ascending
|
||||
items.sort {
|
||||
self.timestampFrom(item: $0) < self.timestampFrom(item: $1)
|
||||
let t1 = self.timestampFrom(item: $0)
|
||||
let t2 = self.timestampFrom(item: $1)
|
||||
return t1 < t2
|
||||
}
|
||||
// set showTime flag
|
||||
for i in items.indices {
|
||||
let ts = self.timestampFrom(item: items[i])
|
||||
if i == 0 || ts - self.timestampFrom(item: items[i - 1]) >= self.timeGapThreshold {
|
||||
if i == 0 || ts - self.timestampFrom(item: items[i-1]) >= self.timeGapThreshold {
|
||||
items[i] = self.setShowTime(items[i], true)
|
||||
}
|
||||
}
|
||||
if let last = items.last {
|
||||
self.lastTimeGap = self.timestampFrom(item: last)
|
||||
}
|
||||
let isEnd = result?.isEnd ?? (list.count < self.historyPageSize)
|
||||
DispatchQueue.main.async {
|
||||
self.hasMoreHistory = !isEnd && !list.isEmpty
|
||||
self.messagesSubject.accept(items)
|
||||
// 进入会话后清除未读
|
||||
self.markAsRead()
|
||||
}
|
||||
},
|
||||
|
|
@ -534,22 +191,35 @@ final class GroupChatViewModel {
|
|||
})
|
||||
}
|
||||
|
||||
/// 标记当前会话为已读
|
||||
private func markAsRead() {
|
||||
let conversationID = "sg_\(groupId)"
|
||||
OIMManager.manager.markConversationMessage(asRead: conversationID,
|
||||
onSuccess: nil,
|
||||
onFailure: nil)
|
||||
onSuccess: nil,
|
||||
onFailure: nil)
|
||||
}
|
||||
|
||||
/// 本地消息(发送中)
|
||||
func appendLocalMessage(_ item: ChatSectionItem) {
|
||||
var items = messagesSubject.value
|
||||
items.append(item)
|
||||
messagesSubject.accept(items)
|
||||
}
|
||||
|
||||
/// 根据 id 更新本地消息(图片上传成功/失败后替换本地占位消息)
|
||||
func updateLocalMessage(id: String, update: (inout ChatMessage) -> Void) {
|
||||
var items = messagesSubject.value
|
||||
guard let idx = items.firstIndex(where: { $0.chatMessage?.id == id }),
|
||||
var chatMsg = items[idx].chatMessage else { return }
|
||||
guard let idx = items.firstIndex(where: { item in
|
||||
switch item {
|
||||
case let .imageSend(m): return m.id == id
|
||||
case let .voiceSend(m): return m.id == id
|
||||
case let .send(m): return m.id == id
|
||||
case let .emojiSend(m): return m.id == id
|
||||
default: return false
|
||||
}
|
||||
}),
|
||||
var chatMsg = items[idx].chatMessage
|
||||
else { return }
|
||||
update(&chatMsg)
|
||||
items[idx] = ChatSectionItem.with(chatMsg)
|
||||
messagesSubject.accept(items)
|
||||
|
|
@ -557,11 +227,11 @@ final class GroupChatViewModel {
|
|||
|
||||
// MARK: - Receive
|
||||
func onReceiveMessage(_ msg: OIMMessageInfo) {
|
||||
cacheMessage(msg)
|
||||
guard let item = toSectionItem(msg) else { return }
|
||||
let ts = timestampFrom(item: item)
|
||||
var items = messagesSubject.value
|
||||
|
||||
// 去重:如果 clientMsgID 已存在(本地占位消息),跳过监听器追加
|
||||
if let clientMsgID = msg.clientMsgID, !clientMsgID.isEmpty,
|
||||
items.contains(where: { $0.chatMessage?.id == clientMsgID }) {
|
||||
return
|
||||
|
|
@ -575,15 +245,11 @@ final class GroupChatViewModel {
|
|||
}
|
||||
}
|
||||
|
||||
// MARK: - Append sent message
|
||||
private func appendMessage(_ msg: OIMMessageInfo) {
|
||||
cacheMessage(msg)
|
||||
guard let item = toSectionItem(msg) else { return }
|
||||
let ts = timestampFrom(item: item)
|
||||
var items = messagesSubject.value
|
||||
// 去重
|
||||
if let id = msg.clientMsgID, items.contains(where: { $0.chatMessage?.id == id }) {
|
||||
return
|
||||
}
|
||||
let showTime = items.isEmpty || ts - lastTimeGap >= timeGapThreshold
|
||||
lastTimeGap = ts
|
||||
items.append(showTime ? setShowTime(item, true) : item)
|
||||
|
|
@ -592,51 +258,39 @@ final class GroupChatViewModel {
|
|||
}
|
||||
}
|
||||
|
||||
func cacheMessage(_ msg: OIMMessageInfo) {
|
||||
if let id = msg.clientMsgID, !id.isEmpty {
|
||||
messageCache[id] = msg
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Helpers
|
||||
private func timestampFrom(item: ChatSectionItem) -> TimeInterval {
|
||||
switch item {
|
||||
case let .send(m), let .received(m), let .emojiSend(m), let .emojiReceived(m),
|
||||
let .voiceSend(m), let .voiceReceived(m), let .imageSend(m), let .imageReceived(m):
|
||||
return m.timestamp
|
||||
case let .notification(_, _, ts), let .revoked(_, _, ts):
|
||||
return ts
|
||||
case let .notification(_, _, ts): return ts
|
||||
}
|
||||
}
|
||||
|
||||
private func setShowTime(_ item: ChatSectionItem, _ show: Bool) -> ChatSectionItem {
|
||||
switch item {
|
||||
case var .send(m): m.showTime = show; return .send(m)
|
||||
case var .received(m): m.showTime = show; return .received(m)
|
||||
case var .emojiSend(m): m.showTime = show; return .emojiSend(m)
|
||||
case var .emojiReceived(m): m.showTime = show; return .emojiReceived(m)
|
||||
case var .voiceSend(m): m.showTime = show; return .voiceSend(m)
|
||||
case var .voiceReceived(m): m.showTime = show; return .voiceReceived(m)
|
||||
case var .imageSend(m): m.showTime = show; return .imageSend(m)
|
||||
case var .imageReceived(m): m.showTime = show; return .imageReceived(m)
|
||||
case var .send(m): m.showTime = show; return .send(m)
|
||||
case var .received(m): m.showTime = show; return .received(m)
|
||||
case var .emojiSend(m): m.showTime = show; return .emojiSend(m)
|
||||
case var .emojiReceived(m):m.showTime = show; return .emojiReceived(m)
|
||||
case var .voiceSend(m): m.showTime = show; return .voiceSend(m)
|
||||
case var .voiceReceived(m):m.showTime = show; return .voiceReceived(m)
|
||||
case var .imageSend(m): m.showTime = show; return .imageSend(m)
|
||||
case var .imageReceived(m):m.showTime = show; return .imageReceived(m)
|
||||
case let .notification(text, _, ts): return .notification(text, showTime: show, timestamp: ts)
|
||||
case let .revoked(text, _, ts): return .revoked(text, showTime: show, timestamp: ts)
|
||||
}
|
||||
}
|
||||
|
||||
private func convert(_ msg: OIMMessageInfo) -> ChatMessage {
|
||||
let isSelf = msg.isSelf()
|
||||
let ts = TimeInterval(msg.sendTime) / 1000.0
|
||||
var content = ""
|
||||
var voiceUrl = ""
|
||||
var imageUrl = ""
|
||||
var imageW: CGFloat = 0
|
||||
var imageH: CGFloat = 0
|
||||
var quotePreview: QuotePreview?
|
||||
var atUserIDs: [String] = []
|
||||
var atNicknames: [String] = []
|
||||
var isAtAll = false
|
||||
let content: String
|
||||
let voiceUrl: String
|
||||
let imageUrl: String
|
||||
let imageW: CGFloat
|
||||
let imageH: CGFloat
|
||||
|
||||
// 图片消息: 计算图片的显示宽高
|
||||
let maxWH: CGFloat = 200
|
||||
var msgImageW: CGFloat = 0
|
||||
var msgImageH: CGFloat = 0
|
||||
|
|
@ -651,36 +305,22 @@ final class GroupChatViewModel {
|
|||
}
|
||||
}
|
||||
|
||||
let type = msg.contentType.rawValue
|
||||
if type == 103, let sound = msg.soundElem {
|
||||
content = "\(sound.duration)"
|
||||
voiceUrl = sound.sourceUrl ?? ""
|
||||
} else if type == 102, let pic = msg.pictureElem {
|
||||
imageUrl = pic.sourcePicture?.url ?? pic.bigPicture?.url ?? pic.sourcePath ?? ""
|
||||
imageW = msgImageW
|
||||
imageH = msgImageH
|
||||
} else if type == 106, let at = msg.atTextElem {
|
||||
content = at.text ?? ""
|
||||
atUserIDs = at.atUserList ?? []
|
||||
atNicknames = (at.atUsersInfo ?? []).compactMap { $0.groupNickname }
|
||||
isAtAll = at.isAtAll
|
||||
if let q = at.quoteMessage {
|
||||
quotePreview = makeQuotePreview(from: q)
|
||||
}
|
||||
} else if type == 114, let q = msg.quoteElem {
|
||||
content = q.text ?? ""
|
||||
if let quoted = q.quoteMessage {
|
||||
quotePreview = makeQuotePreview(from: quoted)
|
||||
}
|
||||
} else if let sound = msg.soundElem {
|
||||
if let sound = msg.soundElem {
|
||||
content = "\(sound.duration)"
|
||||
voiceUrl = sound.sourceUrl ?? ""
|
||||
imageUrl = ""
|
||||
imageW = 0; imageH = 0
|
||||
} else if let pic = msg.pictureElem {
|
||||
content = ""
|
||||
voiceUrl = ""
|
||||
imageUrl = pic.sourcePicture?.url ?? pic.bigPicture?.url ?? pic.sourcePath ?? ""
|
||||
imageW = msgImageW
|
||||
imageH = msgImageH
|
||||
} else {
|
||||
content = msg.textElem?.content ?? ""
|
||||
voiceUrl = ""
|
||||
imageUrl = ""
|
||||
imageW = 0; imageH = 0
|
||||
}
|
||||
|
||||
let sendID = msg.sendID ?? ""
|
||||
|
|
@ -696,81 +336,37 @@ final class GroupChatViewModel {
|
|||
imageWidth: imageW,
|
||||
imageHeight: imageH,
|
||||
timestamp: ts,
|
||||
showTime: false,
|
||||
quotePreview: quotePreview,
|
||||
atUserIDs: atUserIDs,
|
||||
atNicknames: atNicknames,
|
||||
isAtAll: isAtAll
|
||||
showTime: false
|
||||
)
|
||||
}
|
||||
|
||||
private func makeQuotePreview(from msg: OIMMessageInfo) -> QuotePreview {
|
||||
let id = msg.clientMsgID
|
||||
let name = msg.senderNickname ?? getUserNickName(id: msg.sendID ?? "")
|
||||
if msg.status.rawValue == OIMMessageStatus.revoke.rawValue || msg.contentType.rawValue == 2101 {
|
||||
return QuotePreview(senderName: name, kind: .unknown, summary: "引用内容已撤回", quotedClientMsgID: id)
|
||||
}
|
||||
let type = msg.contentType.rawValue
|
||||
if type == 102 {
|
||||
return QuotePreview(senderName: name, kind: .image, summary: "[图片]", quotedClientMsgID: id)
|
||||
}
|
||||
if type == 103 {
|
||||
let dur = (msg.soundElem?.duration ?? 0) / 1000
|
||||
let summary = dur > 0 ? "[语音] \(dur)''" : "[语音]"
|
||||
return QuotePreview(senderName: name, kind: .voice, summary: summary, quotedClientMsgID: id)
|
||||
}
|
||||
var text = ""
|
||||
if type == 106 {
|
||||
text = msg.atTextElem?.text ?? ""
|
||||
} else if type == 114 {
|
||||
text = msg.quoteElem?.text ?? ""
|
||||
} else {
|
||||
text = msg.textElem?.content ?? ""
|
||||
}
|
||||
if let pattern = emojiPattern,
|
||||
pattern.firstMatch(in: text, options: [], range: NSRange(location: 0, length: text.utf16.count)) != nil {
|
||||
return QuotePreview(senderName: name, kind: .emoji, summary: "[表情]", quotedClientMsgID: id)
|
||||
}
|
||||
if text.isEmpty {
|
||||
return QuotePreview(senderName: name, kind: .unknown, summary: "[消息]", quotedClientMsgID: id)
|
||||
}
|
||||
let truncated = text.count > 40 ? String(text.prefix(40)) + "…" : text
|
||||
return QuotePreview(senderName: name, kind: .text, summary: truncated, quotedClientMsgID: id)
|
||||
}
|
||||
// emoji pattern: js_emoji:数字
|
||||
private let emojiPattern = try? NSRegularExpression(pattern: "^js_emoji:(\\d+)$", options: [])
|
||||
|
||||
private func toSectionItem(_ msg: OIMMessageInfo) -> ChatSectionItem? {
|
||||
if msg.contentType.rawValue == 2101 || msg.status.rawValue == OIMMessageStatus.revoke.rawValue {
|
||||
let ts = TimeInterval(msg.sendTime) / 1000.0
|
||||
let tip = revokeTip(revokerID: msg.sendID ?? "",
|
||||
revokerNickname: msg.senderNickname ?? "")
|
||||
return .revoked(tip, showTime: false, timestamp: ts)
|
||||
}
|
||||
|
||||
// 通知消息
|
||||
if (msg.contentType.rawValue == 1501 || msg.contentType.rawValue == 1510 || msg.contentType.rawValue == 1520),
|
||||
let noti = msg.notificationElem,
|
||||
let text = parseNotification(noti, contentType: msg.contentType.rawValue) {
|
||||
let ts = TimeInterval(msg.sendTime) / 1000.0
|
||||
return .notification(text, showTime: false, timestamp: ts)
|
||||
}
|
||||
|
||||
// 语音(104 video 沿用历史兼容)
|
||||
// 语音消息
|
||||
if msg.contentType.rawValue == 103 || msg.contentType.rawValue == 104 {
|
||||
let chatMsg = convert(msg)
|
||||
return chatMsg.isSelf ? .voiceSend(chatMsg) : .voiceReceived(chatMsg)
|
||||
}
|
||||
|
||||
// 图片消息
|
||||
if msg.contentType.rawValue == 102 {
|
||||
let chatMsg = convert(msg)
|
||||
return chatMsg.isSelf ? .imageSend(chatMsg) : .imageReceived(chatMsg)
|
||||
}
|
||||
|
||||
// 文本 / @ / 引用
|
||||
// 普通文本消息
|
||||
let chatMsg = convert(msg)
|
||||
if chatMsg.content.isEmpty && chatMsg.quotePreview == nil { return nil }
|
||||
|
||||
if chatMsg.content.isEmpty { return nil }
|
||||
// 检测是否为纯 emoji 消息
|
||||
if let pattern = emojiPattern,
|
||||
pattern.firstMatch(in: chatMsg.content, options: [], range: NSRange(location: 0, length: chatMsg.content.utf16.count)) != nil,
|
||||
chatMsg.quotePreview == nil {
|
||||
pattern.firstMatch(in: chatMsg.content, options: [], range: NSRange(location: 0, length: chatMsg.content.utf16.count)) != nil {
|
||||
return chatMsg.isSelf ? .emojiSend(chatMsg) : .emojiReceived(chatMsg)
|
||||
}
|
||||
return chatMsg.isSelf ? .send(chatMsg) : .received(chatMsg)
|
||||
|
|
@ -783,27 +379,35 @@ final class GroupChatViewModel {
|
|||
|
||||
switch contentType {
|
||||
case 1501:
|
||||
// 群创建通知(admin 创建)
|
||||
guard let opUser = json["opUser"] as? [String: Any] else { return nil }
|
||||
let groupID = opUser["groupID"] as? String ?? ""
|
||||
let isOwner = groupID.contains(AppContextManager.shared.userId)
|
||||
let text = isOwner ? "\(AppContextManager.shared.name) 创建了圈子" : "圈子已经创建"
|
||||
return NSAttributedString(string: text)
|
||||
|
||||
case 1510:
|
||||
// 新成员加入通知
|
||||
guard let entrantUser = json["entrantUser"] as? [String: Any] else { return nil }
|
||||
let nickName = entrantUser["nickname"] as? String ?? entrantUser["userID"] as? String ?? ""
|
||||
return NSAttributedString(string: "\(nickName) 加入了圈子")
|
||||
let text = "\(nickName) 加入了圈子"
|
||||
return NSAttributedString(string: text)
|
||||
|
||||
case 1520:
|
||||
// 群名称改变通知
|
||||
guard let opUser = json["opUser"] as? [String: Any] else { return nil }
|
||||
let opUserID = opUser["userID"] as? String ?? ""
|
||||
let opNickName = getUserNickName(id: opUserID)
|
||||
let newName = group["groupName"] as? String ?? ""
|
||||
guard !newName.isEmpty else { return nil }
|
||||
|
||||
let tip = "\(opNickName) 将群名称修改为 "
|
||||
let result = NSMutableAttributedString(string: tip + newName)
|
||||
result.addAttribute(.font, value: UIFont.systemFont(ofSize: 12), range: NSRange(location: 0, length: result.length))
|
||||
let nameRange = NSRange(location: tip.count, length: newName.utf16.count)
|
||||
result.addAttribute(.foregroundColor, value: UIColor(hexStr: "#16B3FF"), range: nameRange)
|
||||
return result
|
||||
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
|
|
@ -812,24 +416,22 @@ final class GroupChatViewModel {
|
|||
|
||||
// MARK: - ChatSectionItem Helpers
|
||||
extension ChatSectionItem {
|
||||
/// 提取 ChatMessage(仅用于有消息的 case)
|
||||
var chatMessage: ChatMessage? {
|
||||
switch self {
|
||||
case let .send(m), let .received(m), let .emojiSend(m), let .emojiReceived(m),
|
||||
let .voiceSend(m), let .voiceReceived(m), let .imageSend(m), let .imageReceived(m):
|
||||
return m
|
||||
case .notification, .revoked:
|
||||
return nil
|
||||
case .notification: return nil
|
||||
}
|
||||
}
|
||||
|
||||
/// 撤回项可挂原 id 时用(当前 revoked 不存 id,定位走 chatMessage)
|
||||
var revokedClientMsgID: String? { nil }
|
||||
|
||||
/// 用给定 ChatMessage 重建 case
|
||||
static func with(_ msg: ChatMessage) -> ChatSectionItem {
|
||||
if !msg.imageUrl.isEmpty {
|
||||
return msg.isSelf ? .imageSend(msg) : .imageReceived(msg)
|
||||
}
|
||||
if msg.content.hasPrefix("js_emoji:"), msg.quotePreview == nil {
|
||||
if msg.content.hasPrefix("js_emoji:") {
|
||||
return msg.isSelf ? .emojiSend(msg) : .emojiReceived(msg)
|
||||
}
|
||||
if !msg.voiceUrl.isEmpty {
|
||||
|
|
|
|||
Loading…
Reference in New Issue