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/GLOSSARY.md
|
||||||
.opencode/skills/writing-great-skills/SKILL.md
|
.opencode/skills/writing-great-skills/SKILL.md
|
||||||
.opencode/skills/writing-great-skills/agents/openai.yaml
|
.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 'HXPHPicker'
|
||||||
pod 'SwiftKeychainWrapper'
|
pod 'SwiftKeychainWrapper'
|
||||||
pod 'SGQRCode', '~> 4.1.0' #扫描
|
pod 'SGQRCode', '~> 4.1.0' #扫描
|
||||||
|
|
|
||||||
10
Podfile.lock
10
Podfile.lock
|
|
@ -26,7 +26,6 @@ PODS:
|
||||||
- CocoaMQTT/Core (2.2.5):
|
- CocoaMQTT/Core (2.2.5):
|
||||||
- MqttCocoaAsyncSocket (~> 1.0.8)
|
- MqttCocoaAsyncSocket (~> 1.0.8)
|
||||||
- Differentiator (5.0.0)
|
- Differentiator (5.0.0)
|
||||||
- FLEX (5.22.10)
|
|
||||||
- GTCommonSDK (3.2.3.0):
|
- GTCommonSDK (3.2.3.0):
|
||||||
- ZXSDK
|
- ZXSDK
|
||||||
- GYSDK (3.1.3.0):
|
- GYSDK (3.1.3.0):
|
||||||
|
|
@ -200,7 +199,6 @@ DEPENDENCIES:
|
||||||
- BRPickerView
|
- BRPickerView
|
||||||
- CocoaLumberjack/Swift
|
- CocoaLumberjack/Swift
|
||||||
- CocoaMQTT
|
- CocoaMQTT
|
||||||
- FLEX (from `https://github.com/FLEXTool/FLEX.git`)
|
|
||||||
- GYSDK
|
- GYSDK
|
||||||
- HXPHPicker
|
- HXPHPicker
|
||||||
- IQKeyboardManagerSwift
|
- IQKeyboardManagerSwift
|
||||||
|
|
@ -299,15 +297,10 @@ SPEC REPOS:
|
||||||
- ZXSDK
|
- ZXSDK
|
||||||
|
|
||||||
EXTERNAL SOURCES:
|
EXTERNAL SOURCES:
|
||||||
FLEX:
|
|
||||||
:git: https://github.com/FLEXTool/FLEX.git
|
|
||||||
MBProgressHUD:
|
MBProgressHUD:
|
||||||
:git: https://github.com/jdg/MBProgressHUD.git
|
:git: https://github.com/jdg/MBProgressHUD.git
|
||||||
|
|
||||||
CHECKOUT OPTIONS:
|
CHECKOUT OPTIONS:
|
||||||
FLEX:
|
|
||||||
:commit: 63a6f588841e94e4c3adaa045ff16eb8163f0bb4
|
|
||||||
:git: https://github.com/FLEXTool/FLEX.git
|
|
||||||
MBProgressHUD:
|
MBProgressHUD:
|
||||||
:commit: 4a7c5f3e53cdea77c5dcb8578c2ee5acacdf6781
|
:commit: 4a7c5f3e53cdea77c5dcb8578c2ee5acacdf6781
|
||||||
:git: https://github.com/jdg/MBProgressHUD.git
|
:git: https://github.com/jdg/MBProgressHUD.git
|
||||||
|
|
@ -323,7 +316,6 @@ SPEC CHECKSUMS:
|
||||||
CocoaLumberjack: e4ba3b414dfca8c1916c6303d37f63b3a95134c6
|
CocoaLumberjack: e4ba3b414dfca8c1916c6303d37f63b3a95134c6
|
||||||
CocoaMQTT: 1e2fa493d57045d66fb1eba2bb5ffb8d77039c60
|
CocoaMQTT: 1e2fa493d57045d66fb1eba2bb5ffb8d77039c60
|
||||||
Differentiator: e8497ceab83c1b10ca233716d547b9af21b9344d
|
Differentiator: e8497ceab83c1b10ca233716d547b9af21b9344d
|
||||||
FLEX: 6ec7816b9f392fb4c635cf50381e6bd4fb15541d
|
|
||||||
GTCommonSDK: 238c6735add91e654f8564638854686ee3dcb4ac
|
GTCommonSDK: 238c6735add91e654f8564638854686ee3dcb4ac
|
||||||
GYSDK: db5f4b3aae8df06201be5e786076211ec11791be
|
GYSDK: db5f4b3aae8df06201be5e786076211ec11791be
|
||||||
HXPHPicker: 451f6af62845d60d5dd2235f4665d81f2688a51b
|
HXPHPicker: 451f6af62845d60d5dd2235f4665d81f2688a51b
|
||||||
|
|
@ -374,6 +366,6 @@ SPEC CHECKSUMS:
|
||||||
YYImage: 1e1b62a9997399593e4b9c4ecfbbabbf1d3f3b54
|
YYImage: 1e1b62a9997399593e4b9c4ecfbbabbf1d3f3b54
|
||||||
ZXSDK: 786338c0a18e98e03eda00699c3bfd2700b97117
|
ZXSDK: 786338c0a18e98e03eda00699c3bfd2700b97117
|
||||||
|
|
||||||
PODFILE CHECKSUM: f676321d820ce55c18e69318732ac59e39a57e43
|
PODFILE CHECKSUM: 439991fe5523a65e07f0df7135995adfc31f6cb3
|
||||||
|
|
||||||
COCOAPODS: 1.16.2
|
COCOAPODS: 1.16.2
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,6 @@ PODS:
|
||||||
- CocoaMQTT/Core (2.2.5):
|
- CocoaMQTT/Core (2.2.5):
|
||||||
- MqttCocoaAsyncSocket (~> 1.0.8)
|
- MqttCocoaAsyncSocket (~> 1.0.8)
|
||||||
- Differentiator (5.0.0)
|
- Differentiator (5.0.0)
|
||||||
- FLEX (5.22.10)
|
|
||||||
- GTCommonSDK (3.2.3.0):
|
- GTCommonSDK (3.2.3.0):
|
||||||
- ZXSDK
|
- ZXSDK
|
||||||
- GYSDK (3.1.3.0):
|
- GYSDK (3.1.3.0):
|
||||||
|
|
@ -200,7 +199,6 @@ DEPENDENCIES:
|
||||||
- BRPickerView
|
- BRPickerView
|
||||||
- CocoaLumberjack/Swift
|
- CocoaLumberjack/Swift
|
||||||
- CocoaMQTT
|
- CocoaMQTT
|
||||||
- FLEX (from `https://github.com/FLEXTool/FLEX.git`)
|
|
||||||
- GYSDK
|
- GYSDK
|
||||||
- HXPHPicker
|
- HXPHPicker
|
||||||
- IQKeyboardManagerSwift
|
- IQKeyboardManagerSwift
|
||||||
|
|
@ -299,15 +297,10 @@ SPEC REPOS:
|
||||||
- ZXSDK
|
- ZXSDK
|
||||||
|
|
||||||
EXTERNAL SOURCES:
|
EXTERNAL SOURCES:
|
||||||
FLEX:
|
|
||||||
:git: https://github.com/FLEXTool/FLEX.git
|
|
||||||
MBProgressHUD:
|
MBProgressHUD:
|
||||||
:git: https://github.com/jdg/MBProgressHUD.git
|
:git: https://github.com/jdg/MBProgressHUD.git
|
||||||
|
|
||||||
CHECKOUT OPTIONS:
|
CHECKOUT OPTIONS:
|
||||||
FLEX:
|
|
||||||
:commit: 63a6f588841e94e4c3adaa045ff16eb8163f0bb4
|
|
||||||
:git: https://github.com/FLEXTool/FLEX.git
|
|
||||||
MBProgressHUD:
|
MBProgressHUD:
|
||||||
:commit: 4a7c5f3e53cdea77c5dcb8578c2ee5acacdf6781
|
:commit: 4a7c5f3e53cdea77c5dcb8578c2ee5acacdf6781
|
||||||
:git: https://github.com/jdg/MBProgressHUD.git
|
:git: https://github.com/jdg/MBProgressHUD.git
|
||||||
|
|
@ -323,7 +316,6 @@ SPEC CHECKSUMS:
|
||||||
CocoaLumberjack: e4ba3b414dfca8c1916c6303d37f63b3a95134c6
|
CocoaLumberjack: e4ba3b414dfca8c1916c6303d37f63b3a95134c6
|
||||||
CocoaMQTT: 1e2fa493d57045d66fb1eba2bb5ffb8d77039c60
|
CocoaMQTT: 1e2fa493d57045d66fb1eba2bb5ffb8d77039c60
|
||||||
Differentiator: e8497ceab83c1b10ca233716d547b9af21b9344d
|
Differentiator: e8497ceab83c1b10ca233716d547b9af21b9344d
|
||||||
FLEX: 6ec7816b9f392fb4c635cf50381e6bd4fb15541d
|
|
||||||
GTCommonSDK: 238c6735add91e654f8564638854686ee3dcb4ac
|
GTCommonSDK: 238c6735add91e654f8564638854686ee3dcb4ac
|
||||||
GYSDK: db5f4b3aae8df06201be5e786076211ec11791be
|
GYSDK: db5f4b3aae8df06201be5e786076211ec11791be
|
||||||
HXPHPicker: 451f6af62845d60d5dd2235f4665d81f2688a51b
|
HXPHPicker: 451f6af62845d60d5dd2235f4665d81f2688a51b
|
||||||
|
|
@ -374,6 +366,6 @@ SPEC CHECKSUMS:
|
||||||
YYImage: 1e1b62a9997399593e4b9c4ecfbbabbf1d3f3b54
|
YYImage: 1e1b62a9997399593e4b9c4ecfbbabbf1d3f3b54
|
||||||
ZXSDK: 786338c0a18e98e03eda00699c3bfd2700b97117
|
ZXSDK: 786338c0a18e98e03eda00699c3bfd2700b97117
|
||||||
|
|
||||||
PODFILE CHECKSUM: f676321d820ce55c18e69318732ac59e39a57e43
|
PODFILE CHECKSUM: 439991fe5523a65e07f0df7135995adfc31f6cb3
|
||||||
|
|
||||||
COCOAPODS: 1.16.2
|
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.
|
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
|
## GTCommonSDK
|
||||||
|
|
||||||
MIT License
|
MIT License
|
||||||
|
|
|
||||||
|
|
@ -214,46 +214,6 @@ SOFTWARE.
|
||||||
<key>Type</key>
|
<key>Type</key>
|
||||||
<string>PSGroupSpecifier</string>
|
<string>PSGroupSpecifier</string>
|
||||||
</dict>
|
</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>
|
<dict>
|
||||||
<key>FooterText</key>
|
<key>FooterText</key>
|
||||||
<string>MIT License
|
<string>MIT License
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,6 @@ ${BUILT_PRODUCTS_DIR}/BRPickerView/BRPickerView.framework
|
||||||
${BUILT_PRODUCTS_DIR}/CocoaLumberjack/CocoaLumberjack.framework
|
${BUILT_PRODUCTS_DIR}/CocoaLumberjack/CocoaLumberjack.framework
|
||||||
${BUILT_PRODUCTS_DIR}/CocoaMQTT/CocoaMQTT.framework
|
${BUILT_PRODUCTS_DIR}/CocoaMQTT/CocoaMQTT.framework
|
||||||
${BUILT_PRODUCTS_DIR}/Differentiator/Differentiator.framework
|
${BUILT_PRODUCTS_DIR}/Differentiator/Differentiator.framework
|
||||||
${BUILT_PRODUCTS_DIR}/FLEX/FLEX.framework
|
|
||||||
${BUILT_PRODUCTS_DIR}/HXPHPicker/HXPHPicker.framework
|
${BUILT_PRODUCTS_DIR}/HXPHPicker/HXPHPicker.framework
|
||||||
${BUILT_PRODUCTS_DIR}/IQKeyboardCore/IQKeyboardCore.framework
|
${BUILT_PRODUCTS_DIR}/IQKeyboardCore/IQKeyboardCore.framework
|
||||||
${BUILT_PRODUCTS_DIR}/IQKeyboardManagerSwift/IQKeyboardManagerSwift.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}/CocoaLumberjack.framework
|
||||||
${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/CocoaMQTT.framework
|
${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/CocoaMQTT.framework
|
||||||
${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Differentiator.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}/HXPHPicker.framework
|
||||||
${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/IQKeyboardCore.framework
|
${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/IQKeyboardCore.framework
|
||||||
${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/IQKeyboardManagerSwift.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}/CocoaLumberjack/CocoaLumberjack.framework"
|
||||||
install_framework "${BUILT_PRODUCTS_DIR}/CocoaMQTT/CocoaMQTT.framework"
|
install_framework "${BUILT_PRODUCTS_DIR}/CocoaMQTT/CocoaMQTT.framework"
|
||||||
install_framework "${BUILT_PRODUCTS_DIR}/Differentiator/Differentiator.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}/HXPHPicker/HXPHPicker.framework"
|
||||||
install_framework "${BUILT_PRODUCTS_DIR}/IQKeyboardCore/IQKeyboardCore.framework"
|
install_framework "${BUILT_PRODUCTS_DIR}/IQKeyboardCore/IQKeyboardCore.framework"
|
||||||
install_framework "${BUILT_PRODUCTS_DIR}/IQKeyboardManagerSwift/IQKeyboardManagerSwift.framework"
|
install_framework "${BUILT_PRODUCTS_DIR}/IQKeyboardManagerSwift/IQKeyboardManagerSwift.framework"
|
||||||
|
|
|
||||||
|
|
@ -2,13 +2,13 @@ ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES
|
||||||
ARCHS = $(ARCHS_STANDARD)
|
ARCHS = $(ARCHS_STANDARD)
|
||||||
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO
|
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO
|
||||||
ENABLE_USER_SCRIPT_SANDBOXING = 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
|
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'
|
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
|
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_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}/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_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
|
OTHER_SWIFT_FLAGS = $(inherited) -D COCOAPODS
|
||||||
PODS_BUILD_DIR = ${BUILD_DIR}
|
PODS_BUILD_DIR = ${BUILD_DIR}
|
||||||
PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)
|
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 */; };
|
30EFF3E82FCA8C7000227D26 /* AuthenticationServices.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 30EFF3E92FCA8C7000227D26 /* AuthenticationServices.framework */; };
|
||||||
55BF752D2FFE53F70055DA57 /* LocationPermissionPopView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55BF752C2FFE53F70055DA57 /* LocationPermissionPopView.swift */; };
|
55BF752D2FFE53F70055DA57 /* LocationPermissionPopView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55BF752C2FFE53F70055DA57 /* LocationPermissionPopView.swift */; };
|
||||||
55BF75352FFF91690055DA57 /* InteractionEmojiCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55BF75342FFF91690055DA57 /* InteractionEmojiCell.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 */; };
|
D698E9C56D6F2C152772131D /* Pods_QuickLocation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 475D33CAFA1E1911EB1F8D9F /* Pods_QuickLocation.framework */; };
|
||||||
/* End PBXBuildFile section */
|
/* 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; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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 */
|
/* End PBXFileReference section */
|
||||||
|
|
@ -1255,7 +1253,6 @@
|
||||||
305A767E2FCA8C7000227D26 /* AppDelegate.swift */,
|
305A767E2FCA8C7000227D26 /* AppDelegate.swift */,
|
||||||
305A767F2FCA8C7000227D26 /* Assets.xcassets */,
|
305A767F2FCA8C7000227D26 /* Assets.xcassets */,
|
||||||
305A76802FCA8C7000227D26 /* Info.plist */,
|
305A76802FCA8C7000227D26 /* Info.plist */,
|
||||||
A1B2C3D52FDABC8C009215C1 /* PrivacyInfo.xcprivacy */,
|
|
||||||
305A76822FCA8C7000227D26 /* LaunchScreen.storyboard */,
|
305A76822FCA8C7000227D26 /* LaunchScreen.storyboard */,
|
||||||
305A76842FCA8C7000227D26 /* Main.storyboard */,
|
305A76842FCA8C7000227D26 /* Main.storyboard */,
|
||||||
305A76852FCA8C7000227D26 /* QuickLocation-Bridging-Header.h */,
|
305A76852FCA8C7000227D26 /* QuickLocation-Bridging-Header.h */,
|
||||||
|
|
@ -1758,7 +1755,6 @@
|
||||||
305A77222FCA8C7000227D26 /* YouSheBiaoTiHei.ttf in Resources */,
|
305A77222FCA8C7000227D26 /* YouSheBiaoTiHei.ttf in Resources */,
|
||||||
305A77232FCA8C7000227D26 /* zihunbiantaoti-r.ttf in Resources */,
|
305A77232FCA8C7000227D26 /* zihunbiantaoti-r.ttf in Resources */,
|
||||||
305A77F92FCA8C7000227D26 /* Assets.xcassets in Resources */,
|
305A77F92FCA8C7000227D26 /* Assets.xcassets in Resources */,
|
||||||
A1B2C3D42FDABC8C009215C1 /* PrivacyInfo.xcprivacy in Resources */,
|
|
||||||
305A77FB2FCA8C7000227D26 /* LaunchScreen.storyboard in Resources */,
|
305A77FB2FCA8C7000227D26 /* LaunchScreen.storyboard in Resources */,
|
||||||
305A77FC2FCA8C7000227D26 /* Main.storyboard in Resources */,
|
305A77FC2FCA8C7000227D26 /* Main.storyboard in Resources */,
|
||||||
);
|
);
|
||||||
|
|
@ -2187,7 +2183,7 @@
|
||||||
SWIFT_OBJC_BRIDGING_HEADER = "QuickLocation/QuickLocation-Bridging-Header.h";
|
SWIFT_OBJC_BRIDGING_HEADER = "QuickLocation/QuickLocation-Bridging-Header.h";
|
||||||
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
|
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
|
||||||
SWIFT_VERSION = 5.0;
|
SWIFT_VERSION = 5.0;
|
||||||
TARGETED_DEVICE_FAMILY = 1;
|
TARGETED_DEVICE_FAMILY = "1,2";
|
||||||
};
|
};
|
||||||
name = Debug;
|
name = Debug;
|
||||||
};
|
};
|
||||||
|
|
@ -2240,7 +2236,7 @@
|
||||||
SWIFT_OBJC_BRIDGING_HEADER = "QuickLocation/QuickLocation-Bridging-Header.h";
|
SWIFT_OBJC_BRIDGING_HEADER = "QuickLocation/QuickLocation-Bridging-Header.h";
|
||||||
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
|
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
|
||||||
SWIFT_VERSION = 5.0;
|
SWIFT_VERSION = 5.0;
|
||||||
TARGETED_DEVICE_FAMILY = 1;
|
TARGETED_DEVICE_FAMILY = "1,2";
|
||||||
};
|
};
|
||||||
name = Release;
|
name = Release;
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -60,7 +60,9 @@ public let kScreenHeight = UIScreen.main.bounds.size.height
|
||||||
/// 状态栏高度
|
/// 状态栏高度
|
||||||
public var kStatusBarHeight: CGFloat {
|
public var kStatusBarHeight: CGFloat {
|
||||||
if #available(iOS 13.0, *) {
|
if #available(iOS 13.0, *) {
|
||||||
let statusBarHeight = kKeyWindow?
|
let statusBarHeight = UIApplication.shared.windows
|
||||||
|
.filter { $0.isKeyWindow }
|
||||||
|
.first?
|
||||||
.windowScene?
|
.windowScene?
|
||||||
.statusBarManager?
|
.statusBarManager?
|
||||||
.statusBarFrame
|
.statusBarFrame
|
||||||
|
|
|
||||||
|
|
@ -10,15 +10,7 @@ import UIKit
|
||||||
public extension UIApplication {
|
public extension UIApplication {
|
||||||
|
|
||||||
static var keyWindow: UIWindow? {
|
static var keyWindow: UIWindow? {
|
||||||
if #available(iOS 15.0, *) {
|
shared.windows.filter { $0.isKeyWindow }.first
|
||||||
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 }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static func delegate<T: UIApplicationDelegate>() -> T? {
|
static func delegate<T: UIApplicationDelegate>() -> T? {
|
||||||
|
|
|
||||||
|
|
@ -7,11 +7,9 @@
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
nonisolated public class DL {}
|
public class DL {}
|
||||||
|
|
||||||
// nonisolated: 退出工程默认的 MainActor 隔离。
|
public class DLWrapper<Base> {
|
||||||
// 该通用包装类会在 Release(-O) 归档时触发 EarlyPerfInliner 对合成 deinit 的编译器崩溃。
|
|
||||||
nonisolated public class DLWrapper<Base> {
|
|
||||||
|
|
||||||
/// 原始对象
|
/// 原始对象
|
||||||
public private(set) var base: Base
|
public private(set) var base: Base
|
||||||
|
|
|
||||||
|
|
@ -29,8 +29,7 @@ enum PagingRequestType {
|
||||||
typealias RefreshResult = (status: RefreshStatus, isEmpty: Bool)
|
typealias RefreshResult = (status: RefreshStatus, isEmpty: Bool)
|
||||||
|
|
||||||
/// ListType: 列表数据泛型类,例: ListService<ListModel<PaginationModel>>
|
/// ListType: 列表数据泛型类,例: ListService<ListModel<PaginationModel>>
|
||||||
// nonisolated: 泛型类在默认 MainActor 隔离下,Release(-O) 归档会触发 EarlyPerfInliner 编译器崩溃
|
class ListService<ListType>: ListServiceType where ListType: ListModelType, ListType.Item: Mappable {
|
||||||
nonisolated class ListService<ListType>: ListServiceType where ListType: ListModelType, ListType.Item: Mappable {
|
|
||||||
typealias BaseListModel = ListType
|
typealias BaseListModel = ListType
|
||||||
typealias BaseItem = ListType.Item
|
typealias BaseItem = ListType.Item
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -10,8 +10,8 @@ import RxCocoa
|
||||||
import RxSwift
|
import RxSwift
|
||||||
import YBImageBrowser
|
import YBImageBrowser
|
||||||
import UMCommon
|
import UMCommon
|
||||||
#if DEBUG
|
#if DEBUG || AdHoc
|
||||||
import FLEX
|
//import FLEX
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
class BaseViewController: UIViewController {
|
class BaseViewController: UIViewController {
|
||||||
|
|
@ -187,8 +187,8 @@ extension BaseViewController {
|
||||||
// MARK: - FLEX
|
// MARK: - FLEX
|
||||||
extension BaseViewController {
|
extension BaseViewController {
|
||||||
private func setupFlex() {
|
private func setupFlex() {
|
||||||
#if DEBUG
|
#if DEBUG || AdHoc
|
||||||
FLEXManager.shared.showExplorer()
|
// FLEXManager.shared.showExplorer()
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -49,16 +49,16 @@ extension DefaultsKeys {
|
||||||
func setupNetworkMode() {
|
func setupNetworkMode() {
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
setupApiEnv(1)
|
setupApiEnv(1)
|
||||||
openIM_API = "https://imapi.zuom8.cn" // http://38.207.176.65:10002
|
openIM_API = "https://imapi.zuom8.cn"
|
||||||
openIM_WS = "ws://imws.zuom8.cn" // ws://38.207.176.65:10010
|
openIM_WS = "ws://imws.zuom8.cn"
|
||||||
#elseif AdHoc
|
#elseif AdHoc
|
||||||
setupApiEnv(-1)
|
setupApiEnv(-1)
|
||||||
openIM_API = "https://imapi.zuom8.cn"
|
openIM_API = "http://38.207.176.65:10002"
|
||||||
openIM_WS = "ws://imws.zuom8.cn"
|
openIM_WS = "ws://38.207.176.65:10010"
|
||||||
#else
|
#else
|
||||||
setupApiEnv(1)
|
setupApiEnv(1)
|
||||||
openIM_API = "https://imapi.zuom8.cn"
|
openIM_API = "http://38.207.176.65:10002"
|
||||||
openIM_WS = "ws://imws.zuom8.cn"
|
openIM_WS = "ws://38.207.176.65:10010"
|
||||||
#endif
|
#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.
|
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 编译器崩溃
|
public final class Action<Input, Element> {
|
||||||
nonisolated public final class Action<Input, Element> {
|
|
||||||
public typealias WorkFactory = (Input) -> Observable<Element>
|
public typealias WorkFactory = (Input) -> Observable<Element>
|
||||||
|
|
||||||
public let _enabledIf: Observable<Bool>
|
public let _enabledIf: Observable<Bool>
|
||||||
|
|
|
||||||
|
|
@ -2,8 +2,7 @@ import Foundation
|
||||||
import RxSwift
|
import RxSwift
|
||||||
|
|
||||||
/// A special subject for Action.inputs. It only emits `.next` event.
|
/// A special subject for Action.inputs. It only emits `.next` event.
|
||||||
// nonisolated: 泛型类在默认 MainActor 隔离下,Release(-O) 归档会触发 EarlyPerfInliner 编译器崩溃
|
public class InputSubject<Element>: ObservableType, Cancelable, SubjectType, ObserverType {
|
||||||
nonisolated public class InputSubject<Element>: ObservableType, Cancelable, SubjectType, ObserverType {
|
|
||||||
|
|
||||||
public typealias E = Element
|
public typealias E = Element
|
||||||
typealias Key = UInt
|
typealias Key = UInt
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,6 @@ import AVFoundation
|
||||||
import AudioToolbox
|
import AudioToolbox
|
||||||
import HXPHPicker
|
import HXPHPicker
|
||||||
import IQKeyboardManagerSwift
|
import IQKeyboardManagerSwift
|
||||||
import MJRefresh
|
|
||||||
|
|
||||||
final class GroupChatVC: BaseViewController {
|
final class GroupChatVC: BaseViewController {
|
||||||
|
|
||||||
|
|
@ -23,11 +22,6 @@ final class GroupChatVC: BaseViewController {
|
||||||
fileprivate var rootView: GroupChatView!
|
fileprivate var rootView: GroupChatView!
|
||||||
private let viewModel = GroupChatViewModel()
|
private let viewModel = GroupChatViewModel()
|
||||||
private var msgListener: MessageListenerProxy?
|
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
|
// MARK: - Init
|
||||||
init(groupId: String) {
|
init(groupId: String) {
|
||||||
|
|
@ -52,17 +46,14 @@ final class GroupChatVC: BaseViewController {
|
||||||
setupMessageListener()
|
setupMessageListener()
|
||||||
setupVoiceRecording()
|
setupVoiceRecording()
|
||||||
setupPanelDismiss()
|
setupPanelDismiss()
|
||||||
setupChatLongPress()
|
|
||||||
setupMentionInput()
|
|
||||||
setupHistoryRefreshHeader()
|
|
||||||
|
|
||||||
// 并行加载:业务接口 + IM SDK 互不依赖,同时发起
|
// 并行加载:业务接口 + IM SDK 互不依赖,同时发起
|
||||||
requestGroupInfoByKey()
|
requestGroupInfoByKey()
|
||||||
GroupIMService.shared.ensureLogin { [weak self] success in
|
GroupIMService.shared.ensureLogin { [weak self] success in
|
||||||
guard let self = self, success else { return }
|
guard let self = self, success else { return }
|
||||||
self.viewModel.loadMessages()
|
self.viewModel.loadMessages()
|
||||||
}
|
}
|
||||||
|
|
||||||
// 处理系统配置
|
// 处理系统配置
|
||||||
guard let config = AppContextManager.shared.systemConfig else { return }
|
guard let config = AppContextManager.shared.systemConfig else { return }
|
||||||
rootView.chatWarningView.isHidden = config.chatWarning.isEmpty
|
rootView.chatWarningView.isHidden = config.chatWarning.isEmpty
|
||||||
|
|
@ -148,33 +139,7 @@ final class GroupChatVC: BaseViewController {
|
||||||
.skip(1)
|
.skip(1)
|
||||||
.observe(on: MainScheduler.asyncInstance)
|
.observe(on: MainScheduler.asyncInstance)
|
||||||
.subscribe(onNext: { [weak self] _ in
|
.subscribe(onNext: { [weak self] _ in
|
||||||
guard let self = self else { return }
|
self?.scrollToBottom()
|
||||||
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()
|
|
||||||
})
|
})
|
||||||
.disposed(by: disposeBag)
|
.disposed(by: disposeBag)
|
||||||
|
|
||||||
|
|
@ -184,58 +149,7 @@ final class GroupChatVC: BaseViewController {
|
||||||
.disposed(by: disposeBag)
|
.disposed(by: disposeBag)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func bringInputOverlaysToFront() {
|
private var hasScrolledToBottom = false
|
||||||
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 func scrollToBottom() {
|
private func scrollToBottom() {
|
||||||
let count = dataSource.sectionModels.first?.items.count ?? 0
|
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>> = {
|
private lazy var emojiDataSource: RxCollectionViewSectionedReloadDataSource<SectionModel<String, String>> = {
|
||||||
RxCollectionViewSectionedReloadDataSource<SectionModel<String, String>> { _, collectionView, indexPath, name in
|
RxCollectionViewSectionedReloadDataSource<SectionModel<String, String>> { _, collectionView, indexPath, name in
|
||||||
let cell: EmojiPanelCell = collectionView.dequeueReusableCell(for: indexPath)
|
let cell: EmojiPanelCell = collectionView.dequeueReusableCell(for: indexPath)
|
||||||
|
|
@ -282,157 +171,13 @@ final class GroupChatVC: BaseViewController {
|
||||||
|
|
||||||
// MARK: - Message Listener
|
// MARK: - Message Listener
|
||||||
private func setupMessageListener() {
|
private func setupMessageListener() {
|
||||||
msgListener = MessageListenerProxy(
|
msgListener = MessageListenerProxy { [weak self] msg in
|
||||||
onMessage: { [weak self] msg in
|
guard let self = self, msg.groupID == self.viewModel.groupId else { return }
|
||||||
guard let self = self, msg.groupID == self.viewModel.groupId else { return }
|
self.viewModel.onReceiveMessage(msg)
|
||||||
self.viewModel.onReceiveMessage(msg)
|
}
|
||||||
},
|
|
||||||
onRevoked: { [weak self] info in
|
|
||||||
self?.viewModel.onMessageRevoked(info)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
OIMManager.callbacker.addAdvancedMsgListener(listener: msgListener!)
|
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
|
// MARK: - Actions
|
||||||
private func reactiveAction() {
|
private func reactiveAction() {
|
||||||
rootView.backBtn.rx.tap
|
rootView.backBtn.rx.tap
|
||||||
|
|
@ -522,15 +267,16 @@ final class GroupChatVC: BaseViewController {
|
||||||
})
|
})
|
||||||
.disposed(by: disposeBag)
|
.disposed(by: disposeBag)
|
||||||
|
|
||||||
rootView.sendBtn.rx.tap
|
let sendText = Observable.merge(
|
||||||
.subscribe(onNext: { [weak self] in
|
rootView.sendBtn.rx.tap.map { [weak self] _ in self?.rootView.textField.text ?? "" },
|
||||||
guard let self = self else { return }
|
rootView.textField.rx.controlEvent(.editingDidEndOnExit)
|
||||||
let text = self.rootView.textField.text ?? ""
|
.map { [weak self] _ in self?.rootView.textField.text ?? "" }
|
||||||
guard !text.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else { return }
|
)
|
||||||
self.viewModel.input.sendMessage.onNext(text)
|
.filter { !$0.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty }
|
||||||
self.rootView.textField.text = ""
|
.do(onNext: { [weak self] _ in self?.rootView.textField.text = "" })
|
||||||
self.hideMentionPicker()
|
|
||||||
})
|
sendText
|
||||||
|
.bind(to: viewModel.input.sendMessage)
|
||||||
.disposed(by: disposeBag)
|
.disposed(by: disposeBag)
|
||||||
|
|
||||||
// 设置
|
// 设置
|
||||||
|
|
@ -683,7 +429,6 @@ final class GroupChatVC: BaseViewController {
|
||||||
|
|
||||||
// Send voice message via OpenIM
|
// Send voice message via OpenIM
|
||||||
let msg = OIMMessageInfo.createSoundMessage(fromFullPath: url.path, duration: recordDuration * 1000)
|
let msg = OIMMessageInfo.createSoundMessage(fromFullPath: url.path, duration: recordDuration * 1000)
|
||||||
viewModel.cacheMessage(msg)
|
|
||||||
OIMManager.manager.sendMessage(msg,
|
OIMManager.manager.sendMessage(msg,
|
||||||
recvID: "",
|
recvID: "",
|
||||||
groupID: viewModel.groupId,
|
groupID: viewModel.groupId,
|
||||||
|
|
@ -711,23 +456,15 @@ final class GroupChatVC: BaseViewController {
|
||||||
|
|
||||||
// MARK: - dataSource
|
// MARK: - dataSource
|
||||||
private lazy var dataSource: RxTableViewSectionedReloadDataSource<ChatSectionModel> = {
|
private lazy var dataSource: RxTableViewSectionedReloadDataSource<ChatSectionModel> = {
|
||||||
RxTableViewSectionedReloadDataSource<ChatSectionModel> { [weak self] _, tableView, indexPath, item in
|
RxTableViewSectionedReloadDataSource<ChatSectionModel> { _, tableView, indexPath, item in
|
||||||
switch item {
|
switch item {
|
||||||
case let .send(msg):
|
case let .send(msg):
|
||||||
let cell: TextSendMsgCell = tableView.dequeueReusableCell(for: indexPath)
|
let cell: TextSendMsgCell = tableView.dequeueReusableCell(for: indexPath)
|
||||||
cell.configure(msg)
|
cell.configure(msg)
|
||||||
cell.onQuoteTap = { [weak self] in
|
|
||||||
guard let id = msg.quotePreview?.quotedClientMsgID else { return }
|
|
||||||
self?.scrollToMessage(clientMsgID: id)
|
|
||||||
}
|
|
||||||
return cell
|
return cell
|
||||||
case let .received(msg):
|
case let .received(msg):
|
||||||
let cell: TextReceivedMsgCell = tableView.dequeueReusableCell(for: indexPath)
|
let cell: TextReceivedMsgCell = tableView.dequeueReusableCell(for: indexPath)
|
||||||
cell.configure(msg)
|
cell.configure(msg)
|
||||||
cell.onQuoteTap = { [weak self] in
|
|
||||||
guard let id = msg.quotePreview?.quotedClientMsgID else { return }
|
|
||||||
self?.scrollToMessage(clientMsgID: id)
|
|
||||||
}
|
|
||||||
return cell
|
return cell
|
||||||
case let .emojiSend(msg):
|
case let .emojiSend(msg):
|
||||||
let cell: EmojiSendMsgCell = tableView.dequeueReusableCell(for: indexPath)
|
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)
|
self?.showBigImage(imgUrlList: [msg.imageUrl], currentPage: 0, projectiveView: cell.photoView)
|
||||||
}
|
}
|
||||||
return cell
|
return cell
|
||||||
case let .notification(text, showTime, timestamp),
|
case let .notification(text, showTime, timestamp):
|
||||||
let .revoked(text, showTime, timestamp):
|
|
||||||
let cell: NotificationMsgCell = tableView.dequeueReusableCell(for: indexPath)
|
let cell: NotificationMsgCell = tableView.dequeueReusableCell(for: indexPath)
|
||||||
cell.configure(text, showTime: showTime, timestamp: timestamp)
|
cell.configure(text, showTime: showTime, timestamp: timestamp)
|
||||||
return cell
|
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
|
// MARK: - UITableViewDelegate
|
||||||
extension GroupChatVC: UITableViewDelegate {
|
extension GroupChatVC: UITableViewDelegate {
|
||||||
func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
|
func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
|
||||||
|
|
@ -807,32 +516,18 @@ extension GroupChatVC: UITableViewDelegate {
|
||||||
(cell as? EmojiSendMsgCell)?.stopAnimation()
|
(cell as? EmojiSendMsgCell)?.stopAnimation()
|
||||||
(cell as? EmojiReceivedMsgCell)?.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
|
// MARK: - MessageListenerProxy
|
||||||
private class MessageListenerProxy: NSObject, OIMAdvancedMsgListener {
|
private class MessageListenerProxy: NSObject, OIMAdvancedMsgListener {
|
||||||
private let onMessage: (OIMMessageInfo) -> Void
|
private let handler: (OIMMessageInfo) -> Void
|
||||||
private let onRevoked: (OIMMessageRevokedInfo) -> Void
|
|
||||||
|
|
||||||
init(onMessage: @escaping (OIMMessageInfo) -> Void,
|
init(handler: @escaping (OIMMessageInfo) -> Void) {
|
||||||
onRevoked: @escaping (OIMMessageRevokedInfo) -> Void) {
|
self.handler = handler
|
||||||
self.onMessage = onMessage
|
|
||||||
self.onRevoked = onRevoked
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func onRecvNewMessage(_ msg: OIMMessageInfo) {
|
func onRecvNewMessage(_ msg: OIMMessageInfo) {
|
||||||
onMessage(msg)
|
handler(msg)
|
||||||
}
|
|
||||||
|
|
||||||
func onRecvMessageRevoked(_ messageRevoked: OIMMessageRevokedInfo) {
|
|
||||||
onRevoked(messageRevoked)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -867,7 +562,6 @@ extension GroupChatVC: PhotoPickerControllerDelegate {
|
||||||
let msg = OIMMessageInfo.createImageMessage(fromFullPath: fileURL.path)
|
let msg = OIMMessageInfo.createImageMessage(fromFullPath: fileURL.path)
|
||||||
// 使用 SDK 的 clientMsgID 作为本地消息 ID,方便后续与监听器去重
|
// 使用 SDK 的 clientMsgID 作为本地消息 ID,方便后续与监听器去重
|
||||||
let localId = msg.clientMsgID ?? UUID().uuidString
|
let localId = msg.clientMsgID ?? UUID().uuidString
|
||||||
viewModel.cacheMessage(msg)
|
|
||||||
|
|
||||||
// 立即显示本地图片(带 loading)
|
// 立即显示本地图片(带 loading)
|
||||||
let localMsg = ChatMessage(
|
let localMsg = ChatMessage(
|
||||||
|
|
|
||||||
|
|
@ -32,63 +32,6 @@ struct ChatMessage {
|
||||||
let timestamp: TimeInterval
|
let timestamp: TimeInterval
|
||||||
var showTime: Bool = false
|
var showTime: Bool = false
|
||||||
var isUploading: 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 {
|
class GroupChatView: UIView {
|
||||||
|
|
@ -102,14 +45,6 @@ class GroupChatView: UIView {
|
||||||
/// 语音录制回调
|
/// 语音录制回调
|
||||||
var onVoiceRecordState: ((VoiceRecordState) -> Void)?
|
var onVoiceRecordState: ((VoiceRecordState) -> Void)?
|
||||||
private var emojiPlaybackGeneration = 0
|
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() {
|
private func setupRx() {
|
||||||
|
|
||||||
|
|
@ -120,13 +55,11 @@ class GroupChatView: UIView {
|
||||||
let needsReset = !emojiPanelView.isHidden
|
let needsReset = !emojiPanelView.isHidden
|
||||||
|| !voiceRecordView.isHidden
|
|| !voiceRecordView.isHidden
|
||||||
|| textField.isFirstResponder
|
|| textField.isFirstResponder
|
||||||
|| !mentionPickerView.isHidden
|
|
||||||
|
|
||||||
guard needsReset else { return }
|
guard needsReset else { return }
|
||||||
stopVisibleEmojiAnimations()
|
stopVisibleEmojiAnimations()
|
||||||
emojiPanelView.isHidden = true
|
emojiPanelView.isHidden = true
|
||||||
voiceRecordView.isHidden = true
|
voiceRecordView.isHidden = true
|
||||||
mentionPickerView.isHidden = true
|
|
||||||
if !excludeTextField { textField.resignFirstResponder() }
|
if !excludeTextField { textField.resignFirstResponder() }
|
||||||
UIView.animate(withDuration: 0.25) {
|
UIView.animate(withDuration: 0.25) {
|
||||||
self.bottomBar.layoutChain.bottom(kSafeBottomMargin + 20)
|
self.bottomBar.layoutChain.bottom(kSafeBottomMargin + 20)
|
||||||
|
|
@ -190,8 +123,6 @@ class GroupChatView: UIView {
|
||||||
|
|
||||||
addSubview(tableView)
|
addSubview(tableView)
|
||||||
addSubview(chatWarningView)
|
addSubview(chatWarningView)
|
||||||
addSubview(quoteBar)
|
|
||||||
addSubview(mentionPickerView)
|
|
||||||
addSubview(bottomBar)
|
addSubview(bottomBar)
|
||||||
addSubview(disableIMView)
|
addSubview(disableIMView)
|
||||||
bottomBar.addSubview(bottomBarCornerView)
|
bottomBar.addSubview(bottomBarCornerView)
|
||||||
|
|
@ -205,11 +136,6 @@ class GroupChatView: UIView {
|
||||||
bottomBar.addSubview(addBtn)
|
bottomBar.addSubview(addBtn)
|
||||||
bottomBar.addSubview(sendBtn)
|
bottomBar.addSubview(sendBtn)
|
||||||
|
|
||||||
quoteBar.addSubview(quoteBarLabel)
|
|
||||||
quoteBar.addSubview(quoteBarCloseBtn)
|
|
||||||
mentionPickerView.addSubview(mentionPickerContentView)
|
|
||||||
mentionPickerContentView.addSubview(mentionTableView)
|
|
||||||
|
|
||||||
navBgView.layoutChain
|
navBgView.layoutChain
|
||||||
.edges(excludingEdge: .bottom)
|
.edges(excludingEdge: .bottom)
|
||||||
.height(kNaviHeight)
|
.height(kNaviHeight)
|
||||||
|
|
@ -270,28 +196,6 @@ class GroupChatView: UIView {
|
||||||
chatWarningView.layoutChain
|
chatWarningView.layoutChain
|
||||||
.topToBottomOfView(navBarView)
|
.topToBottomOfView(navBarView)
|
||||||
.edgesHorzontal()
|
.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
|
bottomBar.layoutChain
|
||||||
.edgesHorzontal(15)
|
.edgesHorzontal(15)
|
||||||
|
|
@ -491,63 +395,6 @@ class GroupChatView: UIView {
|
||||||
return tv
|
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
|
// MARK: - Bottom Bar
|
||||||
lazy var bottomBar: UIView = {
|
lazy var bottomBar: UIView = {
|
||||||
let view = UIView()
|
let view = UIView()
|
||||||
|
|
@ -690,236 +537,35 @@ class GroupChatView: UIView {
|
||||||
required init?(coder: NSCoder) {
|
required init?(coder: NSCoder) {
|
||||||
fatalError("init(coder:) has not been implemented")
|
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
|
// MARK: - 发送的消息cell
|
||||||
class TextSendMsgCell: UITableViewCell {
|
class TextSendMsgCell: UITableViewCell {
|
||||||
|
|
||||||
var onQuoteTap: (() -> Void)?
|
|
||||||
private(set) var messageId: String = ""
|
|
||||||
var menuAnchorView: UIView { bubbleView }
|
|
||||||
|
|
||||||
func configure(_ msg: ChatMessage) {
|
func configure(_ msg: ChatMessage) {
|
||||||
messageId = msg.id
|
|
||||||
timeLabel.isHidden = !msg.showTime
|
timeLabel.isHidden = !msg.showTime
|
||||||
timeLabel.text = msg.showTime ? chatFormatTime(msg.timestamp) : nil
|
timeLabel.text = msg.showTime ? formatTime(msg.timestamp) : nil
|
||||||
avatarView.image = msg.avatar
|
avatarView.image = msg.avatar
|
||||||
contentLabel.attributedText = msg.attributedContent(isOutgoing: true)
|
contentLabel.text = msg.content
|
||||||
let hasQuote = msg.quotePreview != nil
|
|
||||||
quoteBlock.configure(msg.quotePreview)
|
|
||||||
quoteHeightConstraint?.constant = hasQuote ? 28 : 0
|
|
||||||
quoteTopConstraint?.constant = hasQuote ? 4 : 0
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func flashHighlight() {
|
private func formatTime(_ t: TimeInterval) -> String {
|
||||||
let old = contentView.backgroundColor
|
let date = Date(timeIntervalSince1970: t)
|
||||||
contentView.backgroundColor = UIColor(hexStr: "#16B3FF").withAlphaComponent(0.12)
|
let now = Date()
|
||||||
UIView.animate(withDuration: 0.8, delay: 0.3, options: []) {
|
let calendar = Calendar.current
|
||||||
self.contentView.backgroundColor = old
|
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 = {
|
private let timeLabel: UILabel = {
|
||||||
let label = UILabel()
|
let label = UILabel()
|
||||||
label.font = .systemFont(ofSize: 12)
|
label.font = .systemFont(ofSize: 12)
|
||||||
|
|
@ -928,7 +574,7 @@ class TextSendMsgCell: UITableViewCell {
|
||||||
return label
|
return label
|
||||||
}()
|
}()
|
||||||
|
|
||||||
let avatarView: UIImageView = {
|
private let avatarView: UIImageView = {
|
||||||
let iv = UIImageView()
|
let iv = UIImageView()
|
||||||
iv.contentMode = .scaleAspectFill
|
iv.contentMode = .scaleAspectFill
|
||||||
iv.cornerRadius = 15
|
iv.cornerRadius = 15
|
||||||
|
|
@ -951,102 +597,80 @@ class TextSendMsgCell: UITableViewCell {
|
||||||
label.numberOfLines = 0
|
label.numberOfLines = 0
|
||||||
return label
|
return label
|
||||||
}()
|
}()
|
||||||
|
|
||||||
private let quoteBlock = ChatQuoteBlockView()
|
|
||||||
private var quoteHeightConstraint: NSLayoutConstraint?
|
|
||||||
private var quoteTopConstraint: NSLayoutConstraint?
|
|
||||||
|
|
||||||
override init(style: CellStyle, reuseIdentifier: String?) {
|
override init(style: CellStyle, reuseIdentifier: String?) {
|
||||||
super.init(style: style, reuseIdentifier: reuseIdentifier)
|
super.init(style: style, reuseIdentifier: reuseIdentifier)
|
||||||
selectionStyle = .none
|
selectionStyle = .none
|
||||||
backgroundColor = .clear
|
backgroundColor = .clear
|
||||||
|
|
||||||
contentView.addSubview(timeLabel)
|
|
||||||
contentView.addSubview(bubbleView)
|
contentView.addSubview(bubbleView)
|
||||||
bubbleView.addSubview(contentLabel)
|
bubbleView.addSubview(contentLabel)
|
||||||
contentView.addSubview(quoteBlock)
|
contentView.addSubview(timeLabel)
|
||||||
contentView.addSubview(avatarView)
|
contentView.addSubview(avatarView)
|
||||||
|
|
||||||
timeLabel.layoutChain.top().centerX()
|
timeLabel.layoutChain
|
||||||
|
.top()
|
||||||
|
.centerX()
|
||||||
|
|
||||||
avatarView.layoutChain
|
avatarView.layoutChain
|
||||||
.topToBottomOfView(timeLabel, offset: 14)
|
.topToBottomOfView(timeLabel, offset: 14)
|
||||||
.right(12).width(30).height(30)
|
.right(12)
|
||||||
|
.width(30).height(30)
|
||||||
|
|
||||||
bubbleView.layoutChain
|
bubbleView.layoutChain
|
||||||
.topToBottomOfView(avatarView, offset: -15)
|
.topToBottomOfView(avatarView, offset: -15)
|
||||||
.rightToView(avatarView, offset: -13)
|
.rightToView(avatarView, offset: -13)
|
||||||
.left(60, relation: .greaterThanOrEqual)
|
.left(60, relation: .greaterThanOrEqual)
|
||||||
.width(100, relation: .greaterThanOrEqual)
|
.width(100, relation: .greaterThanOrEqual)
|
||||||
.height(30, relation: .greaterThanOrEqual)
|
.height(30, relation: .greaterThanOrEqual)
|
||||||
|
.bottom(10)
|
||||||
contentLabel.translatesAutoresizingMaskIntoConstraints = false
|
|
||||||
quoteBlock.translatesAutoresizingMaskIntoConstraints = false
|
contentLabel.layoutChain
|
||||||
NSLayoutConstraint.activate([
|
.edgesVertical(10)
|
||||||
contentLabel.topAnchor.constraint(equalTo: bubbleView.topAnchor, constant: 10),
|
.edgesHorzontal(20)
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@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() {
|
override func layoutSubviews() {
|
||||||
super.layoutSubviews()
|
super.layoutSubviews()
|
||||||
bubbleView.setNeedsLayout()
|
bubbleView.setNeedsLayout()
|
||||||
bubbleView.layoutIfNeeded()
|
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))
|
withCornerRadii: CGSize(width: bubbleView.dl.height / 2, height: bubbleView.dl.height / 2))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - 收到的消息cell
|
//MARK: - 收到的消息cell
|
||||||
class TextReceivedMsgCell: UITableViewCell {
|
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) {
|
func configure(_ msg: ChatMessage) {
|
||||||
messageId = msg.id
|
|
||||||
senderId = msg.senderId
|
|
||||||
senderName = msg.senderName
|
|
||||||
timeLabel.isHidden = !msg.showTime
|
timeLabel.isHidden = !msg.showTime
|
||||||
timeLabel.text = msg.showTime ? chatFormatTime(msg.timestamp) : nil
|
timeLabel.text = msg.showTime ? formatTime(msg.timestamp) : nil
|
||||||
avatarView.image = msg.avatar
|
avatarView.image = msg.avatar
|
||||||
nameLabel.text = msg.senderName
|
nameLabel.text = msg.senderName
|
||||||
contentLabel.attributedText = msg.attributedContent(isOutgoing: false)
|
contentLabel.text = msg.content
|
||||||
let hasQuote = msg.quotePreview != nil
|
|
||||||
quoteBlock.configure(msg.quotePreview)
|
|
||||||
quoteHeightConstraint?.constant = hasQuote ? 28 : 0
|
|
||||||
quoteTopConstraint?.constant = hasQuote ? 4 : 0
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func flashHighlight() {
|
private func formatTime(_ t: TimeInterval) -> String {
|
||||||
let old = contentView.backgroundColor
|
let date = Date(timeIntervalSince1970: t)
|
||||||
contentView.backgroundColor = UIColor(hexStr: "#16B3FF").withAlphaComponent(0.12)
|
let now = Date()
|
||||||
UIView.animate(withDuration: 0.8, delay: 0.3, options: []) {
|
let calendar = Calendar.current
|
||||||
self.contentView.backgroundColor = old
|
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 = {
|
private let timeLabel: UILabel = {
|
||||||
let label = UILabel()
|
let label = UILabel()
|
||||||
label.font = .systemFont(ofSize: 12)
|
label.font = .systemFont(ofSize: 12)
|
||||||
|
|
@ -1062,7 +686,7 @@ class TextReceivedMsgCell: UITableViewCell {
|
||||||
return label
|
return label
|
||||||
}()
|
}()
|
||||||
|
|
||||||
let avatarView: UIImageView = {
|
private let avatarView: UIImageView = {
|
||||||
let iv = UIImageView()
|
let iv = UIImageView()
|
||||||
iv.contentMode = .scaleAspectFill
|
iv.contentMode = .scaleAspectFill
|
||||||
iv.cornerRadius = 15
|
iv.cornerRadius = 15
|
||||||
|
|
@ -1070,7 +694,6 @@ class TextReceivedMsgCell: UITableViewCell {
|
||||||
iv.backgroundColor = UIColor(hexStr: "#E0E0E0")
|
iv.backgroundColor = UIColor(hexStr: "#E0E0E0")
|
||||||
iv.borderWidth = 2
|
iv.borderWidth = 2
|
||||||
iv.borderColor = .white
|
iv.borderColor = .white
|
||||||
iv.isUserInteractionEnabled = true
|
|
||||||
return iv
|
return iv
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
|
@ -1086,11 +709,7 @@ class TextReceivedMsgCell: UITableViewCell {
|
||||||
label.numberOfLines = 0
|
label.numberOfLines = 0
|
||||||
return label
|
return label
|
||||||
}()
|
}()
|
||||||
|
|
||||||
private let quoteBlock = ChatQuoteBlockView()
|
|
||||||
private var quoteHeightConstraint: NSLayoutConstraint?
|
|
||||||
private var quoteTopConstraint: NSLayoutConstraint?
|
|
||||||
|
|
||||||
override init(style: CellStyle, reuseIdentifier: String?) {
|
override init(style: CellStyle, reuseIdentifier: String?) {
|
||||||
super.init(style: style, reuseIdentifier: reuseIdentifier)
|
super.init(style: style, reuseIdentifier: reuseIdentifier)
|
||||||
selectionStyle = .none
|
selectionStyle = .none
|
||||||
|
|
@ -1099,61 +718,44 @@ class TextReceivedMsgCell: UITableViewCell {
|
||||||
contentView.addSubview(timeLabel)
|
contentView.addSubview(timeLabel)
|
||||||
contentView.addSubview(bubbleView)
|
contentView.addSubview(bubbleView)
|
||||||
bubbleView.addSubview(contentLabel)
|
bubbleView.addSubview(contentLabel)
|
||||||
contentView.addSubview(quoteBlock)
|
|
||||||
contentView.addSubview(avatarView)
|
contentView.addSubview(avatarView)
|
||||||
contentView.addSubview(nameLabel)
|
contentView.addSubview(nameLabel)
|
||||||
|
|
||||||
timeLabel.layoutChain.top().centerX()
|
timeLabel.layoutChain
|
||||||
|
.top()
|
||||||
|
.centerX()
|
||||||
|
|
||||||
bubbleView.layoutChain
|
bubbleView.layoutChain
|
||||||
.topToBottomOfView(timeLabel, offset: 14)
|
.topToBottomOfView(timeLabel, offset: 14)
|
||||||
.left(30)
|
.left(30)
|
||||||
.right(60, relation: .greaterThanOrEqual)
|
.right(60, relation: .greaterThanOrEqual)
|
||||||
.width(100, relation: .greaterThanOrEqual)
|
.width(100, relation: .greaterThanOrEqual)
|
||||||
.height(30, relation: .greaterThanOrEqual)
|
.height(30, relation: .greaterThanOrEqual)
|
||||||
|
|
||||||
contentLabel.translatesAutoresizingMaskIntoConstraints = false
|
avatarView.layoutChain
|
||||||
quoteBlock.translatesAutoresizingMaskIntoConstraints = false
|
.topToBottomOfView(bubbleView, offset: -15)
|
||||||
avatarView.translatesAutoresizingMaskIntoConstraints = false
|
.left(12)
|
||||||
nameLabel.translatesAutoresizingMaskIntoConstraints = false
|
.width(30).height(30)
|
||||||
NSLayoutConstraint.activate([
|
.bottom(10)
|
||||||
contentLabel.topAnchor.constraint(equalTo: bubbleView.topAnchor, constant: 10),
|
|
||||||
contentLabel.leadingAnchor.constraint(equalTo: bubbleView.leadingAnchor, constant: 20),
|
nameLabel.layoutChain
|
||||||
contentLabel.trailingAnchor.constraint(equalTo: bubbleView.trailingAnchor, constant: -20),
|
.leftToRightOfView(avatarView, offset: 5)
|
||||||
contentLabel.bottomAnchor.constraint(equalTo: bubbleView.bottomAnchor, constant: -10),
|
.bottomToView(avatarView)
|
||||||
// 头像 + 昵称贴在气泡下方
|
|
||||||
avatarView.topAnchor.constraint(equalTo: bubbleView.bottomAnchor, constant: -15),
|
contentLabel.layoutChain
|
||||||
avatarView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 12),
|
.edgesVertical(10)
|
||||||
avatarView.widthAnchor.constraint(equalToConstant: 30),
|
.edgesHorzontal(20)
|
||||||
avatarView.heightAnchor.constraint(equalToConstant: 30),
|
|
||||||
nameLabel.leadingAnchor.constraint(equalTo: avatarView.trailingAnchor, constant: 5),
|
|
||||||
nameLabel.bottomAnchor.constraint(equalTo: avatarView.bottomAnchor)
|
|
||||||
])
|
|
||||||
|
|
||||||
// 引用在 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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@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() {
|
override func layoutSubviews() {
|
||||||
super.layoutSubviews()
|
super.layoutSubviews()
|
||||||
bubbleView.setNeedsLayout()
|
bubbleView.setNeedsLayout()
|
||||||
bubbleView.layoutIfNeeded()
|
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))
|
withCornerRadii: CGSize(width: bubbleView.dl.height / 2, height: bubbleView.dl.height / 2))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1221,7 +823,6 @@ final class NotificationMsgCell: UITableViewCell {
|
||||||
|
|
||||||
// MARK: - 发送的表情消息
|
// MARK: - 发送的表情消息
|
||||||
final class EmojiSendMsgCell: UITableViewCell {
|
final class EmojiSendMsgCell: UITableViewCell {
|
||||||
var menuAnchorView: UIView { lottieView }
|
|
||||||
|
|
||||||
private let timeLabel: UILabel = {
|
private let timeLabel: UILabel = {
|
||||||
let label = UILabel()
|
let label = UILabel()
|
||||||
|
|
@ -1337,7 +938,6 @@ final class EmojiSendMsgCell: UITableViewCell {
|
||||||
|
|
||||||
// MARK: - 收到的表情消息
|
// MARK: - 收到的表情消息
|
||||||
final class EmojiReceivedMsgCell: UITableViewCell {
|
final class EmojiReceivedMsgCell: UITableViewCell {
|
||||||
var menuAnchorView: UIView { lottieView }
|
|
||||||
|
|
||||||
private let timeLabel: UILabel = {
|
private let timeLabel: UILabel = {
|
||||||
let label = UILabel()
|
let label = UILabel()
|
||||||
|
|
@ -1347,7 +947,7 @@ final class EmojiReceivedMsgCell: UITableViewCell {
|
||||||
return label
|
return label
|
||||||
}()
|
}()
|
||||||
|
|
||||||
let avatarView: UIImageView = {
|
private let avatarView: UIImageView = {
|
||||||
let iv = UIImageView()
|
let iv = UIImageView()
|
||||||
iv.contentMode = .scaleAspectFill
|
iv.contentMode = .scaleAspectFill
|
||||||
iv.cornerRadius = 15
|
iv.cornerRadius = 15
|
||||||
|
|
@ -1355,7 +955,6 @@ final class EmojiReceivedMsgCell: UITableViewCell {
|
||||||
iv.backgroundColor = UIColor(hexStr: "#E0E0E0")
|
iv.backgroundColor = UIColor(hexStr: "#E0E0E0")
|
||||||
iv.borderWidth = 2
|
iv.borderWidth = 2
|
||||||
iv.borderColor = .white
|
iv.borderColor = .white
|
||||||
iv.isUserInteractionEnabled = true
|
|
||||||
return iv
|
return iv
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
|
@ -1502,7 +1101,6 @@ extension GroupChatView: UICollectionViewDelegate {
|
||||||
|
|
||||||
// MARK: - 发送的语音消息
|
// MARK: - 发送的语音消息
|
||||||
final class VoiceSendMsgCell: UITableViewCell, VoicePlaybackView {
|
final class VoiceSendMsgCell: UITableViewCell, VoicePlaybackView {
|
||||||
var menuAnchorView: UIView { bubbleView }
|
|
||||||
|
|
||||||
private let timeLabel: UILabel = {
|
private let timeLabel: UILabel = {
|
||||||
let label = UILabel()
|
let label = UILabel()
|
||||||
|
|
@ -1640,7 +1238,6 @@ final class VoiceSendMsgCell: UITableViewCell, VoicePlaybackView {
|
||||||
|
|
||||||
// MARK: - 收到的语音消息
|
// MARK: - 收到的语音消息
|
||||||
final class VoiceReceivedMsgCell: UITableViewCell, VoicePlaybackView {
|
final class VoiceReceivedMsgCell: UITableViewCell, VoicePlaybackView {
|
||||||
var menuAnchorView: UIView { bubbleView }
|
|
||||||
|
|
||||||
func configure(_ msg: ChatMessage) {
|
func configure(_ msg: ChatMessage) {
|
||||||
timeLabel.isHidden = !msg.showTime
|
timeLabel.isHidden = !msg.showTime
|
||||||
|
|
@ -1692,7 +1289,7 @@ final class VoiceReceivedMsgCell: UITableViewCell, VoicePlaybackView {
|
||||||
return label
|
return label
|
||||||
}()
|
}()
|
||||||
|
|
||||||
let avatarView: UIImageView = {
|
private let avatarView: UIImageView = {
|
||||||
let iv = UIImageView()
|
let iv = UIImageView()
|
||||||
iv.contentMode = .scaleAspectFill
|
iv.contentMode = .scaleAspectFill
|
||||||
iv.cornerRadius = 15
|
iv.cornerRadius = 15
|
||||||
|
|
@ -1700,7 +1297,6 @@ final class VoiceReceivedMsgCell: UITableViewCell, VoicePlaybackView {
|
||||||
iv.backgroundColor = UIColor(hexStr: "#E0E0E0")
|
iv.backgroundColor = UIColor(hexStr: "#E0E0E0")
|
||||||
iv.borderWidth = 2
|
iv.borderWidth = 2
|
||||||
iv.borderColor = .white
|
iv.borderColor = .white
|
||||||
iv.isUserInteractionEnabled = true
|
|
||||||
return iv
|
return iv
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
|
@ -1798,7 +1394,7 @@ final class VoiceReceivedMsgCell: UITableViewCell, VoicePlaybackView {
|
||||||
|
|
||||||
// MARK: - 发送的图片消息
|
// MARK: - 发送的图片消息
|
||||||
final class ImageSendMsgCell: UITableViewCell {
|
final class ImageSendMsgCell: UITableViewCell {
|
||||||
var menuAnchorView: UIView { photoView }
|
|
||||||
var onImageTap: (() -> Void)?
|
var onImageTap: (() -> Void)?
|
||||||
|
|
||||||
func configure(_ msg: ChatMessage) {
|
func configure(_ msg: ChatMessage) {
|
||||||
|
|
@ -1908,7 +1504,7 @@ final class ImageSendMsgCell: UITableViewCell {
|
||||||
|
|
||||||
// MARK: - 收到的图片消息
|
// MARK: - 收到的图片消息
|
||||||
final class ImageReceivedMsgCell: UITableViewCell {
|
final class ImageReceivedMsgCell: UITableViewCell {
|
||||||
var menuAnchorView: UIView { photoView }
|
|
||||||
var onImageTap: (() -> Void)?
|
var onImageTap: (() -> Void)?
|
||||||
|
|
||||||
func configure(_ msg: ChatMessage) {
|
func configure(_ msg: ChatMessage) {
|
||||||
|
|
@ -1953,7 +1549,7 @@ final class ImageReceivedMsgCell: UITableViewCell {
|
||||||
return label
|
return label
|
||||||
}()
|
}()
|
||||||
|
|
||||||
let avatarView: UIImageView = {
|
private let avatarView: UIImageView = {
|
||||||
let iv = UIImageView()
|
let iv = UIImageView()
|
||||||
iv.contentMode = .scaleAspectFill
|
iv.contentMode = .scaleAspectFill
|
||||||
iv.cornerRadius = 15
|
iv.cornerRadius = 15
|
||||||
|
|
@ -1961,7 +1557,6 @@ final class ImageReceivedMsgCell: UITableViewCell {
|
||||||
iv.backgroundColor = UIColor(hexStr: "#E0E0E0")
|
iv.backgroundColor = UIColor(hexStr: "#E0E0E0")
|
||||||
iv.borderWidth = 2
|
iv.borderWidth = 2
|
||||||
iv.borderColor = .white
|
iv.borderColor = .white
|
||||||
iv.isUserInteractionEnabled = true
|
|
||||||
return iv
|
return iv
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -11,17 +11,6 @@ import RxCocoa
|
||||||
import OpenIMSDK
|
import OpenIMSDK
|
||||||
import Differentiator
|
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 {
|
enum ChatSectionItem {
|
||||||
case send(ChatMessage)
|
case send(ChatMessage)
|
||||||
case received(ChatMessage)
|
case received(ChatMessage)
|
||||||
|
|
@ -32,7 +21,6 @@ enum ChatSectionItem {
|
||||||
case imageSend(ChatMessage)
|
case imageSend(ChatMessage)
|
||||||
case imageReceived(ChatMessage)
|
case imageReceived(ChatMessage)
|
||||||
case notification(NSAttributedString, showTime: Bool = false, timestamp: TimeInterval = 0)
|
case notification(NSAttributedString, showTime: Bool = false, timestamp: TimeInterval = 0)
|
||||||
case revoked(NSAttributedString, showTime: Bool = false, timestamp: TimeInterval = 0)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
typealias ChatSectionModel = SectionModel<String, ChatSectionItem>
|
typealias ChatSectionModel = SectionModel<String, ChatSectionItem>
|
||||||
|
|
@ -45,7 +33,6 @@ final class GroupChatViewModel {
|
||||||
|
|
||||||
struct Output {
|
struct Output {
|
||||||
let messages: Observable<[ChatSectionItem]>
|
let messages: Observable<[ChatSectionItem]>
|
||||||
let pendingQuoteSummary: Observable<String?>
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let input: Input
|
let input: Input
|
||||||
|
|
@ -53,42 +40,22 @@ final class GroupChatViewModel {
|
||||||
|
|
||||||
private let messagesSubject = BehaviorRelay<[ChatSectionItem]>(value: [])
|
private let messagesSubject = BehaviorRelay<[ChatSectionItem]>(value: [])
|
||||||
private let sendMessageSubject = PublishSubject<String>()
|
private let sendMessageSubject = PublishSubject<String>()
|
||||||
private let pendingQuoteSummarySubject = BehaviorRelay<String?>(value: nil)
|
|
||||||
private var lastTimeGap: TimeInterval = 0
|
private var lastTimeGap: TimeInterval = 0
|
||||||
private let timeGapThreshold: TimeInterval = 300
|
/// 两条消息间隔超过此值(秒)显示时间戳
|
||||||
|
private let timeGapThreshold: TimeInterval = 300 // 5 minutes
|
||||||
|
|
||||||
var groupModel: GroupInfoModel?
|
var groupModel: GroupInfoModel?
|
||||||
var memberList: [GroupMemberModel] = [] {
|
var memberList: [GroupMemberModel] = [] {
|
||||||
didSet { buildAvatarCache() }
|
didSet {
|
||||||
|
buildAvatarCache()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
var groupId: String = ""
|
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
|
// MARK: - Init
|
||||||
init() {
|
init() {
|
||||||
input = Input(sendMessage: sendMessageSubject.asObserver())
|
input = Input(sendMessage: sendMessageSubject.asObserver())
|
||||||
output = Output(
|
output = Output(messages: messagesSubject.asObservable())
|
||||||
messages: messagesSubject.asObservable(),
|
|
||||||
pendingQuoteSummary: pendingQuoteSummarySubject.asObservable()
|
|
||||||
)
|
|
||||||
|
|
||||||
sendMessageSubject
|
sendMessageSubject
|
||||||
.subscribe(onNext: { [weak self] text in
|
.subscribe(onNext: { [weak self] text in
|
||||||
|
|
@ -97,36 +64,54 @@ final class GroupChatViewModel {
|
||||||
.disposed(by: disposeBag)
|
.disposed(by: disposeBag)
|
||||||
}
|
}
|
||||||
|
|
||||||
var currentMessages: [ChatSectionItem] { messagesSubject.value }
|
private let disposeBag = DisposeBag()
|
||||||
|
|
||||||
var conversationID: String { "sg_\(groupId)" }
|
|
||||||
|
|
||||||
// MARK: - Avatar
|
// MARK: - Avatar
|
||||||
private var avatarCache: [String: UIImage] = [:]
|
private var avatarCache: [String: UIImage] = [:]
|
||||||
|
|
||||||
|
/// memberList 更新后调用,预构建 userID → UIImage 映射,避免 30 条消息逐一遍历
|
||||||
func buildAvatarCache() {
|
func buildAvatarCache() {
|
||||||
var cache: [String: UIImage] = [:]
|
var cache: [String: UIImage] = [:]
|
||||||
for member in memberList {
|
for member in memberList {
|
||||||
cache[member.user_id] = member.userIcon
|
cache[member.user_id] = member.userIcon
|
||||||
}
|
}
|
||||||
avatarCache = cache
|
avatarCache = cache
|
||||||
|
// 刷新已有消息的头像(并行加载时 loadMessages 可能先于 memberList 完成)
|
||||||
refreshMessageAvatars()
|
refreshMessageAvatars()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 用 avatarCache 刷新已有消息的头像(并行加载时 loadMessages 可能先于 memberList 完成)
|
||||||
private func refreshMessageAvatars() {
|
private func refreshMessageAvatars() {
|
||||||
var items = messagesSubject.value
|
var items = messagesSubject.value
|
||||||
var didChange = false
|
var didChange = false
|
||||||
items = items.map { item in
|
items = items.map { item in
|
||||||
guard var m = item.chatMessage, updateAvatar(&m) else { return item }
|
switch item {
|
||||||
didChange = true
|
case var .send(m): if updateAvatar(&m) { didChange = true }; return .send(m)
|
||||||
return ChatSectionItem.with(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 {
|
private func updateAvatar(_ msg: inout ChatMessage) -> Bool {
|
||||||
guard let cached = avatarCache[msg.senderId], cached != msg.avatar else { return false }
|
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
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -144,359 +129,29 @@ final class GroupChatViewModel {
|
||||||
memberList.first { id == $0.user_id }?.nick_name ?? ""
|
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
|
// MARK: - Send
|
||||||
private func onSendMessage(_ text: String) {
|
private func onSendMessage(_ text: String) {
|
||||||
let trimmed = text.trimmingCharacters(in: .whitespacesAndNewlines)
|
let msg = OIMMessageInfo.createTextMessage(text)
|
||||||
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()
|
|
||||||
|
|
||||||
OIMManager.manager.sendMessage(msg,
|
OIMManager.manager.sendMessage(msg,
|
||||||
recvID: "",
|
recvID: "",
|
||||||
groupID: groupId,
|
groupID: groupId,
|
||||||
offlinePushInfo: nil,
|
offlinePushInfo: nil,
|
||||||
onSuccess: { [weak self] _ in
|
onSuccess: { [weak self] _ in
|
||||||
self?.cacheMessage(msg)
|
|
||||||
self?.appendMessage(msg)
|
self?.appendMessage(msg)
|
||||||
},
|
},
|
||||||
onProgress: nil as OIMNumberCallback?,
|
onProgress: nil as OIMNumberCallback?,
|
||||||
onFailure: { code, errMsg in
|
onFailure: { code, errMsg in
|
||||||
print("send failed: \(code) \(errMsg ?? "")")
|
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
|
// MARK: - Load
|
||||||
func loadMessages() {
|
func loadMessages() {
|
||||||
guard !groupId.isEmpty else { return }
|
guard !groupId.isEmpty else { return }
|
||||||
let param = OIMGetAdvancedHistoryMessageListParam()
|
let param = OIMGetAdvancedHistoryMessageListParam()
|
||||||
param.conversationID = conversationID
|
param.conversationID = "sg_\(groupId)"
|
||||||
param.count = historyPageSize
|
param.count = 30
|
||||||
|
|
||||||
OIMManager.manager.getAdvancedHistoryMessageList(
|
OIMManager.manager.getAdvancedHistoryMessageList(
|
||||||
param,
|
param,
|
||||||
|
|
@ -505,27 +160,29 @@ final class GroupChatViewModel {
|
||||||
let list = result?.messageList else { return }
|
let list = result?.messageList else { return }
|
||||||
var items: [ChatSectionItem] = []
|
var items: [ChatSectionItem] = []
|
||||||
for msg in list {
|
for msg in list {
|
||||||
self.cacheMessage(msg)
|
|
||||||
if let item = self.toSectionItem(msg) {
|
if let item = self.toSectionItem(msg) {
|
||||||
items.append(item)
|
items.append(item)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// messages come newest-first from API, sort by time ascending
|
||||||
items.sort {
|
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 {
|
for i in items.indices {
|
||||||
let ts = self.timestampFrom(item: items[i])
|
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)
|
items[i] = self.setShowTime(items[i], true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if let last = items.last {
|
if let last = items.last {
|
||||||
self.lastTimeGap = self.timestampFrom(item: last)
|
self.lastTimeGap = self.timestampFrom(item: last)
|
||||||
}
|
}
|
||||||
let isEnd = result?.isEnd ?? (list.count < self.historyPageSize)
|
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
self.hasMoreHistory = !isEnd && !list.isEmpty
|
|
||||||
self.messagesSubject.accept(items)
|
self.messagesSubject.accept(items)
|
||||||
|
// 进入会话后清除未读
|
||||||
self.markAsRead()
|
self.markAsRead()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
@ -534,22 +191,35 @@ final class GroupChatViewModel {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 标记当前会话为已读
|
||||||
private func markAsRead() {
|
private func markAsRead() {
|
||||||
|
let conversationID = "sg_\(groupId)"
|
||||||
OIMManager.manager.markConversationMessage(asRead: conversationID,
|
OIMManager.manager.markConversationMessage(asRead: conversationID,
|
||||||
onSuccess: nil,
|
onSuccess: nil,
|
||||||
onFailure: nil)
|
onFailure: nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 本地消息(发送中)
|
||||||
func appendLocalMessage(_ item: ChatSectionItem) {
|
func appendLocalMessage(_ item: ChatSectionItem) {
|
||||||
var items = messagesSubject.value
|
var items = messagesSubject.value
|
||||||
items.append(item)
|
items.append(item)
|
||||||
messagesSubject.accept(items)
|
messagesSubject.accept(items)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 根据 id 更新本地消息(图片上传成功/失败后替换本地占位消息)
|
||||||
func updateLocalMessage(id: String, update: (inout ChatMessage) -> Void) {
|
func updateLocalMessage(id: String, update: (inout ChatMessage) -> Void) {
|
||||||
var items = messagesSubject.value
|
var items = messagesSubject.value
|
||||||
guard let idx = items.firstIndex(where: { $0.chatMessage?.id == id }),
|
guard let idx = items.firstIndex(where: { item in
|
||||||
var chatMsg = items[idx].chatMessage else { return }
|
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)
|
update(&chatMsg)
|
||||||
items[idx] = ChatSectionItem.with(chatMsg)
|
items[idx] = ChatSectionItem.with(chatMsg)
|
||||||
messagesSubject.accept(items)
|
messagesSubject.accept(items)
|
||||||
|
|
@ -557,11 +227,11 @@ final class GroupChatViewModel {
|
||||||
|
|
||||||
// MARK: - Receive
|
// MARK: - Receive
|
||||||
func onReceiveMessage(_ msg: OIMMessageInfo) {
|
func onReceiveMessage(_ msg: OIMMessageInfo) {
|
||||||
cacheMessage(msg)
|
|
||||||
guard let item = toSectionItem(msg) else { return }
|
guard let item = toSectionItem(msg) else { return }
|
||||||
let ts = timestampFrom(item: item)
|
let ts = timestampFrom(item: item)
|
||||||
var items = messagesSubject.value
|
var items = messagesSubject.value
|
||||||
|
|
||||||
|
// 去重:如果 clientMsgID 已存在(本地占位消息),跳过监听器追加
|
||||||
if let clientMsgID = msg.clientMsgID, !clientMsgID.isEmpty,
|
if let clientMsgID = msg.clientMsgID, !clientMsgID.isEmpty,
|
||||||
items.contains(where: { $0.chatMessage?.id == clientMsgID }) {
|
items.contains(where: { $0.chatMessage?.id == clientMsgID }) {
|
||||||
return
|
return
|
||||||
|
|
@ -575,15 +245,11 @@ final class GroupChatViewModel {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - Append sent message
|
||||||
private func appendMessage(_ msg: OIMMessageInfo) {
|
private func appendMessage(_ msg: OIMMessageInfo) {
|
||||||
cacheMessage(msg)
|
|
||||||
guard let item = toSectionItem(msg) else { return }
|
guard let item = toSectionItem(msg) else { return }
|
||||||
let ts = timestampFrom(item: item)
|
let ts = timestampFrom(item: item)
|
||||||
var items = messagesSubject.value
|
var items = messagesSubject.value
|
||||||
// 去重
|
|
||||||
if let id = msg.clientMsgID, items.contains(where: { $0.chatMessage?.id == id }) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
let showTime = items.isEmpty || ts - lastTimeGap >= timeGapThreshold
|
let showTime = items.isEmpty || ts - lastTimeGap >= timeGapThreshold
|
||||||
lastTimeGap = ts
|
lastTimeGap = ts
|
||||||
items.append(showTime ? setShowTime(item, true) : item)
|
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 {
|
private func timestampFrom(item: ChatSectionItem) -> TimeInterval {
|
||||||
switch item {
|
switch item {
|
||||||
case let .send(m), let .received(m), let .emojiSend(m), let .emojiReceived(m),
|
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):
|
let .voiceSend(m), let .voiceReceived(m), let .imageSend(m), let .imageReceived(m):
|
||||||
return m.timestamp
|
return m.timestamp
|
||||||
case let .notification(_, _, ts), let .revoked(_, _, ts):
|
case let .notification(_, _, ts): return ts
|
||||||
return ts
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func setShowTime(_ item: ChatSectionItem, _ show: Bool) -> ChatSectionItem {
|
private func setShowTime(_ item: ChatSectionItem, _ show: Bool) -> ChatSectionItem {
|
||||||
switch item {
|
switch item {
|
||||||
case var .send(m): m.showTime = show; return .send(m)
|
case var .send(m): m.showTime = show; return .send(m)
|
||||||
case var .received(m): m.showTime = show; return .received(m)
|
case var .received(m): m.showTime = show; return .received(m)
|
||||||
case var .emojiSend(m): m.showTime = show; return .emojiSend(m)
|
case var .emojiSend(m): m.showTime = show; return .emojiSend(m)
|
||||||
case var .emojiReceived(m): m.showTime = show; return .emojiReceived(m)
|
case var .emojiReceived(m):m.showTime = show; return .emojiReceived(m)
|
||||||
case var .voiceSend(m): m.showTime = show; return .voiceSend(m)
|
case var .voiceSend(m): m.showTime = show; return .voiceSend(m)
|
||||||
case var .voiceReceived(m): m.showTime = show; return .voiceReceived(m)
|
case var .voiceReceived(m):m.showTime = show; return .voiceReceived(m)
|
||||||
case var .imageSend(m): m.showTime = show; return .imageSend(m)
|
case var .imageSend(m): m.showTime = show; return .imageSend(m)
|
||||||
case var .imageReceived(m): m.showTime = show; return .imageReceived(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 .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 {
|
private func convert(_ msg: OIMMessageInfo) -> ChatMessage {
|
||||||
let isSelf = msg.isSelf()
|
let isSelf = msg.isSelf()
|
||||||
let ts = TimeInterval(msg.sendTime) / 1000.0
|
let ts = TimeInterval(msg.sendTime) / 1000.0
|
||||||
var content = ""
|
let content: String
|
||||||
var voiceUrl = ""
|
let voiceUrl: String
|
||||||
var imageUrl = ""
|
let imageUrl: String
|
||||||
var imageW: CGFloat = 0
|
let imageW: CGFloat
|
||||||
var imageH: CGFloat = 0
|
let imageH: CGFloat
|
||||||
var quotePreview: QuotePreview?
|
|
||||||
var atUserIDs: [String] = []
|
// 图片消息: 计算图片的显示宽高
|
||||||
var atNicknames: [String] = []
|
|
||||||
var isAtAll = false
|
|
||||||
|
|
||||||
let maxWH: CGFloat = 200
|
let maxWH: CGFloat = 200
|
||||||
var msgImageW: CGFloat = 0
|
var msgImageW: CGFloat = 0
|
||||||
var msgImageH: CGFloat = 0
|
var msgImageH: CGFloat = 0
|
||||||
|
|
@ -650,37 +304,23 @@ final class GroupChatViewModel {
|
||||||
msgImageW = maxWH * CGFloat(width) / CGFloat(height)
|
msgImageW = maxWH * CGFloat(width) / CGFloat(height)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let type = msg.contentType.rawValue
|
if let sound = msg.soundElem {
|
||||||
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 {
|
|
||||||
content = "\(sound.duration)"
|
content = "\(sound.duration)"
|
||||||
voiceUrl = sound.sourceUrl ?? ""
|
voiceUrl = sound.sourceUrl ?? ""
|
||||||
|
imageUrl = ""
|
||||||
|
imageW = 0; imageH = 0
|
||||||
} else if let pic = msg.pictureElem {
|
} else if let pic = msg.pictureElem {
|
||||||
|
content = ""
|
||||||
|
voiceUrl = ""
|
||||||
imageUrl = pic.sourcePicture?.url ?? pic.bigPicture?.url ?? pic.sourcePath ?? ""
|
imageUrl = pic.sourcePicture?.url ?? pic.bigPicture?.url ?? pic.sourcePath ?? ""
|
||||||
imageW = msgImageW
|
imageW = msgImageW
|
||||||
imageH = msgImageH
|
imageH = msgImageH
|
||||||
} else {
|
} else {
|
||||||
content = msg.textElem?.content ?? ""
|
content = msg.textElem?.content ?? ""
|
||||||
|
voiceUrl = ""
|
||||||
|
imageUrl = ""
|
||||||
|
imageW = 0; imageH = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
let sendID = msg.sendID ?? ""
|
let sendID = msg.sendID ?? ""
|
||||||
|
|
@ -696,81 +336,37 @@ final class GroupChatViewModel {
|
||||||
imageWidth: imageW,
|
imageWidth: imageW,
|
||||||
imageHeight: imageH,
|
imageHeight: imageH,
|
||||||
timestamp: ts,
|
timestamp: ts,
|
||||||
showTime: false,
|
showTime: false
|
||||||
quotePreview: quotePreview,
|
|
||||||
atUserIDs: atUserIDs,
|
|
||||||
atNicknames: atNicknames,
|
|
||||||
isAtAll: isAtAll
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func makeQuotePreview(from msg: OIMMessageInfo) -> QuotePreview {
|
// emoji pattern: js_emoji:数字
|
||||||
let id = msg.clientMsgID
|
private let emojiPattern = try? NSRegularExpression(pattern: "^js_emoji:(\\d+)$", options: [])
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
private func toSectionItem(_ msg: OIMMessageInfo) -> ChatSectionItem? {
|
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),
|
if (msg.contentType.rawValue == 1501 || msg.contentType.rawValue == 1510 || msg.contentType.rawValue == 1520),
|
||||||
let noti = msg.notificationElem,
|
let noti = msg.notificationElem,
|
||||||
let text = parseNotification(noti, contentType: msg.contentType.rawValue) {
|
let text = parseNotification(noti, contentType: msg.contentType.rawValue) {
|
||||||
let ts = TimeInterval(msg.sendTime) / 1000.0
|
let ts = TimeInterval(msg.sendTime) / 1000.0
|
||||||
return .notification(text, showTime: false, timestamp: ts)
|
return .notification(text, showTime: false, timestamp: ts)
|
||||||
}
|
}
|
||||||
|
// 语音消息
|
||||||
// 语音(104 video 沿用历史兼容)
|
|
||||||
if msg.contentType.rawValue == 103 || msg.contentType.rawValue == 104 {
|
if msg.contentType.rawValue == 103 || msg.contentType.rawValue == 104 {
|
||||||
let chatMsg = convert(msg)
|
let chatMsg = convert(msg)
|
||||||
return chatMsg.isSelf ? .voiceSend(chatMsg) : .voiceReceived(chatMsg)
|
return chatMsg.isSelf ? .voiceSend(chatMsg) : .voiceReceived(chatMsg)
|
||||||
}
|
}
|
||||||
|
// 图片消息
|
||||||
if msg.contentType.rawValue == 102 {
|
if msg.contentType.rawValue == 102 {
|
||||||
let chatMsg = convert(msg)
|
let chatMsg = convert(msg)
|
||||||
return chatMsg.isSelf ? .imageSend(chatMsg) : .imageReceived(chatMsg)
|
return chatMsg.isSelf ? .imageSend(chatMsg) : .imageReceived(chatMsg)
|
||||||
}
|
}
|
||||||
|
// 普通文本消息
|
||||||
// 文本 / @ / 引用
|
|
||||||
let chatMsg = convert(msg)
|
let chatMsg = convert(msg)
|
||||||
if chatMsg.content.isEmpty && chatMsg.quotePreview == nil { return nil }
|
if chatMsg.content.isEmpty { return nil }
|
||||||
|
// 检测是否为纯 emoji 消息
|
||||||
if let pattern = emojiPattern,
|
if let pattern = emojiPattern,
|
||||||
pattern.firstMatch(in: chatMsg.content, options: [], range: NSRange(location: 0, length: chatMsg.content.utf16.count)) != nil,
|
pattern.firstMatch(in: chatMsg.content, options: [], range: NSRange(location: 0, length: chatMsg.content.utf16.count)) != nil {
|
||||||
chatMsg.quotePreview == nil {
|
|
||||||
return chatMsg.isSelf ? .emojiSend(chatMsg) : .emojiReceived(chatMsg)
|
return chatMsg.isSelf ? .emojiSend(chatMsg) : .emojiReceived(chatMsg)
|
||||||
}
|
}
|
||||||
return chatMsg.isSelf ? .send(chatMsg) : .received(chatMsg)
|
return chatMsg.isSelf ? .send(chatMsg) : .received(chatMsg)
|
||||||
|
|
@ -783,27 +379,35 @@ final class GroupChatViewModel {
|
||||||
|
|
||||||
switch contentType {
|
switch contentType {
|
||||||
case 1501:
|
case 1501:
|
||||||
|
// 群创建通知(admin 创建)
|
||||||
guard let opUser = json["opUser"] as? [String: Any] else { return nil }
|
guard let opUser = json["opUser"] as? [String: Any] else { return nil }
|
||||||
let groupID = opUser["groupID"] as? String ?? ""
|
let groupID = opUser["groupID"] as? String ?? ""
|
||||||
let isOwner = groupID.contains(AppContextManager.shared.userId)
|
let isOwner = groupID.contains(AppContextManager.shared.userId)
|
||||||
let text = isOwner ? "\(AppContextManager.shared.name) 创建了圈子" : "圈子已经创建"
|
let text = isOwner ? "\(AppContextManager.shared.name) 创建了圈子" : "圈子已经创建"
|
||||||
return NSAttributedString(string: text)
|
return NSAttributedString(string: text)
|
||||||
|
|
||||||
case 1510:
|
case 1510:
|
||||||
|
// 新成员加入通知
|
||||||
guard let entrantUser = json["entrantUser"] as? [String: Any] else { return nil }
|
guard let entrantUser = json["entrantUser"] as? [String: Any] else { return nil }
|
||||||
let nickName = entrantUser["nickname"] as? String ?? entrantUser["userID"] as? String ?? ""
|
let nickName = entrantUser["nickname"] as? String ?? entrantUser["userID"] as? String ?? ""
|
||||||
return NSAttributedString(string: "\(nickName) 加入了圈子")
|
let text = "\(nickName) 加入了圈子"
|
||||||
|
return NSAttributedString(string: text)
|
||||||
|
|
||||||
case 1520:
|
case 1520:
|
||||||
|
// 群名称改变通知
|
||||||
guard let opUser = json["opUser"] as? [String: Any] else { return nil }
|
guard let opUser = json["opUser"] as? [String: Any] else { return nil }
|
||||||
let opUserID = opUser["userID"] as? String ?? ""
|
let opUserID = opUser["userID"] as? String ?? ""
|
||||||
let opNickName = getUserNickName(id: opUserID)
|
let opNickName = getUserNickName(id: opUserID)
|
||||||
let newName = group["groupName"] as? String ?? ""
|
let newName = group["groupName"] as? String ?? ""
|
||||||
guard !newName.isEmpty else { return nil }
|
guard !newName.isEmpty else { return nil }
|
||||||
|
|
||||||
let tip = "\(opNickName) 将群名称修改为 "
|
let tip = "\(opNickName) 将群名称修改为 "
|
||||||
let result = NSMutableAttributedString(string: tip + newName)
|
let result = NSMutableAttributedString(string: tip + newName)
|
||||||
result.addAttribute(.font, value: UIFont.systemFont(ofSize: 12), range: NSRange(location: 0, length: result.length))
|
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)
|
let nameRange = NSRange(location: tip.count, length: newName.utf16.count)
|
||||||
result.addAttribute(.foregroundColor, value: UIColor(hexStr: "#16B3FF"), range: nameRange)
|
result.addAttribute(.foregroundColor, value: UIColor(hexStr: "#16B3FF"), range: nameRange)
|
||||||
return result
|
return result
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
@ -812,24 +416,22 @@ final class GroupChatViewModel {
|
||||||
|
|
||||||
// MARK: - ChatSectionItem Helpers
|
// MARK: - ChatSectionItem Helpers
|
||||||
extension ChatSectionItem {
|
extension ChatSectionItem {
|
||||||
|
/// 提取 ChatMessage(仅用于有消息的 case)
|
||||||
var chatMessage: ChatMessage? {
|
var chatMessage: ChatMessage? {
|
||||||
switch self {
|
switch self {
|
||||||
case let .send(m), let .received(m), let .emojiSend(m), let .emojiReceived(m),
|
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):
|
let .voiceSend(m), let .voiceReceived(m), let .imageSend(m), let .imageReceived(m):
|
||||||
return m
|
return m
|
||||||
case .notification, .revoked:
|
case .notification: return nil
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 撤回项可挂原 id 时用(当前 revoked 不存 id,定位走 chatMessage)
|
/// 用给定 ChatMessage 重建 case
|
||||||
var revokedClientMsgID: String? { nil }
|
|
||||||
|
|
||||||
static func with(_ msg: ChatMessage) -> ChatSectionItem {
|
static func with(_ msg: ChatMessage) -> ChatSectionItem {
|
||||||
if !msg.imageUrl.isEmpty {
|
if !msg.imageUrl.isEmpty {
|
||||||
return msg.isSelf ? .imageSend(msg) : .imageReceived(msg)
|
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)
|
return msg.isSelf ? .emojiSend(msg) : .emojiReceived(msg)
|
||||||
}
|
}
|
||||||
if !msg.voiceUrl.isEmpty {
|
if !msg.voiceUrl.isEmpty {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue