diff --git a/UpstraUIKit/AmityUIKit.xcodeproj/project.pbxproj b/UpstraUIKit/AmityUIKit.xcodeproj/project.pbxproj index 4fa94c4..5be4e1b 100644 --- a/UpstraUIKit/AmityUIKit.xcodeproj/project.pbxproj +++ b/UpstraUIKit/AmityUIKit.xcodeproj/project.pbxproj @@ -89,7 +89,6 @@ 68B30A7C2B70E38B006A4102 /* AmityStoryCommentSettingsScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 68B30A752B70E38B006A4102 /* AmityStoryCommentSettingsScreenViewModelProtocol.swift */; }; 68B30A7D2B70E38B006A4102 /* AmityStoryCommentSettingsScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 68B30A762B70E38B006A4102 /* AmityStoryCommentSettingsScreenViewModel.swift */; }; 68B30A7E2B70E38B006A4102 /* AmityStoryCommentSettingsItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 68B30A782B70E38B006A4102 /* AmityStoryCommentSettingsItem.swift */; }; - 68D959D42C4A41F3005DC4FA /* SharedFrameworks in Frameworks */ = {isa = PBXBuildFile; productRef = 68D959D32C4A41F3005DC4FA /* SharedFrameworks */; }; 68F0EE062BC6BBDF004B3AA4 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 68F0EE052BC6BBDF004B3AA4 /* PrivacyInfo.xcprivacy */; }; 68F5D9FA2B481E4000A9FA0D /* AmityUIKit4.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 68F5D9F92B481E4000A9FA0D /* AmityUIKit4.framework */; }; 720D599A2525BDB1009734EF /* DispatchGroupWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 720D59992525BDB1009734EF /* DispatchGroupWrapper.swift */; }; @@ -491,7 +490,12 @@ A0B68B6126E7238E007D7B5B /* AmityPostLiveStreamTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = A0B68B5F26E7238E007D7B5B /* AmityPostLiveStreamTableViewCell.xib */; }; A0E2B23226B7B68200F1C4D5 /* AmityPostGalleryViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A0E2B23026B7B68200F1C4D5 /* AmityPostGalleryViewController.swift */; }; A0E2B23326B7B68200F1C4D5 /* AmityPostGalleryViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = A0E2B23126B7B68200F1C4D5 /* AmityPostGalleryViewController.xib */; }; + A959BBD02C5D012B00E23ABB /* SharedFrameworks in Frameworks */ = {isa = PBXBuildFile; productRef = A959BBCF2C5D012B00E23ABB /* SharedFrameworks */; }; A98B3BC12914BD7700C0A56D /* AmityCommentViewLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = A98B3BC02914BD7700C0A56D /* AmityCommentViewLayout.swift */; }; + A9E106532C3B9498002BB4A1 /* TextHighlighter.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9E106522C3B9498002BB4A1 /* TextHighlighter.swift */; }; + A9E106552C3B9511002BB4A1 /* AmityMentionListProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9E106542C3B9511002BB4A1 /* AmityMentionListProvider.swift */; }; + A9E106572C3B954D002BB4A1 /* AmityMentionTextEditor.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9E106562C3B954D002BB4A1 /* AmityMentionTextEditor.swift */; }; + A9E106592C3B9582002BB4A1 /* ASCMentionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9E106582C3B9582002BB4A1 /* ASCMentionManager.swift */; }; A9E890F529F8F43C00A22B35 /* AmityReactionUsersViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9E890F429F8F43C00A22B35 /* AmityReactionUsersViewController.swift */; }; A9E890F729F933D900A22B35 /* AmityReactionUsersViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = A9E890F629F933D900A22B35 /* AmityReactionUsersViewController.xib */; }; A9E890FB29F93DCE00A22B35 /* AmityReactionUserTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9E890F929F93DCE00A22B35 /* AmityReactionUserTableViewCell.swift */; }; @@ -1160,6 +1164,10 @@ A0E2B23026B7B68200F1C4D5 /* AmityPostGalleryViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AmityPostGalleryViewController.swift; sourceTree = ""; }; A0E2B23126B7B68200F1C4D5 /* AmityPostGalleryViewController.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = AmityPostGalleryViewController.xib; sourceTree = ""; }; A98B3BC02914BD7700C0A56D /* AmityCommentViewLayout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AmityCommentViewLayout.swift; sourceTree = ""; }; + A9E106522C3B9498002BB4A1 /* TextHighlighter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextHighlighter.swift; sourceTree = ""; }; + A9E106542C3B9511002BB4A1 /* AmityMentionListProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AmityMentionListProvider.swift; sourceTree = ""; }; + A9E106562C3B954D002BB4A1 /* AmityMentionTextEditor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AmityMentionTextEditor.swift; sourceTree = ""; }; + A9E106582C3B9582002BB4A1 /* ASCMentionManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ASCMentionManager.swift; sourceTree = ""; }; A9E890F429F8F43C00A22B35 /* AmityReactionUsersViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AmityReactionUsersViewController.swift; sourceTree = ""; }; A9E890F629F933D900A22B35 /* AmityReactionUsersViewController.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = AmityReactionUsersViewController.xib; sourceTree = ""; }; A9E890F929F93DCE00A22B35 /* AmityReactionUserTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AmityReactionUserTableViewCell.swift; sourceTree = ""; }; @@ -1332,7 +1340,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 68D959D42C4A41F3005DC4FA /* SharedFrameworks in Frameworks */, + A959BBD02C5D012B00E23ABB /* SharedFrameworks in Frameworks */, 68F5D9FA2B481E4000A9FA0D /* AmityUIKit4.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -4426,6 +4434,10 @@ isa = PBXGroup; children = ( D42E1B89274CED5C006C41ED /* AmityMentionManager.swift */, + A9E106522C3B9498002BB4A1 /* TextHighlighter.swift */, + A9E106542C3B9511002BB4A1 /* AmityMentionListProvider.swift */, + A9E106562C3B954D002BB4A1 /* AmityMentionTextEditor.swift */, + A9E106582C3B9582002BB4A1 /* ASCMentionManager.swift */, ); path = Manager; sourceTree = ""; @@ -4495,7 +4507,7 @@ ); name = AmityUIKit; packageProductDependencies = ( - 68D959D32C4A41F3005DC4FA /* SharedFrameworks */, + A959BBCF2C5D012B00E23ABB /* SharedFrameworks */, ); productName = UpstraUIKit; productReference = 72A3503024EA811500DA9D46 /* AmityUIKit.framework */; @@ -4810,6 +4822,7 @@ 72A3511C24EA820800DA9D46 /* AmityGalleryCollectionView.swift in Sources */, 72A3514524EA820800DA9D46 /* AmityColorSet.swift in Sources */, 72A3514424EA820800DA9D46 /* AmitySettings.swift in Sources */, + A9E106572C3B954D002BB4A1 /* AmityMentionTextEditor.swift in Sources */, B7A9242324F42873001F4B49 /* AmityNewsfeedViewController.swift in Sources */, B7B31C5C250F4ACF000AC6E4 /* IndicatorInfo.swift in Sources */, 0D43B17A25D8330800E6B604 /* AmityFeedScreenViewModel.swift in Sources */, @@ -4992,6 +5005,7 @@ 97D723DB2695B133000735F5 /* AmityUserFollowersScreenViewModelProtocol.swift in Sources */, 721C8C87262D1EA100BA576C /* AmityCommentEditorViewController.swift.swift in Sources */, 72A3514124EA820800DA9D46 /* AmityComunity.swift in Sources */, + A9E106552C3B9511002BB4A1 /* AmityMentionListProvider.swift in Sources */, B788B935252D8B71002F4F12 /* AmityTrendingCommunityScreenViewModelProtocol.swift in Sources */, A0A903B226BA749B002949B2 /* Protocol.swift in Sources */, A9E890FE29F93F9B00A22B35 /* AmityReactionUsersScreenViewModel.swift in Sources */, @@ -5066,6 +5080,7 @@ 97D7C4A9264BF91000EA24F5 /* AmityChannelMemberViewController.swift in Sources */, 97D0EBB026A14A8800E9FE6A /* AmityPendingPostsDetailScreenViewModel.swift in Sources */, 78C1C4AD25F3EE0500D6F092 /* AmitySettingsItemTextContentTableViewCell.swift in Sources */, + A9E106532C3B9498002BB4A1 /* TextHighlighter.swift in Sources */, 0D95687A25D58F1C00DE98FD /* AmityPostComposable.swift in Sources */, 0DD94A6325A783250066FA0B /* AmityCommunityProfileScreenViewModel.swift in Sources */, 72A3515624EA820800DA9D46 /* AmityMessageListConstant.swift in Sources */, @@ -5218,6 +5233,7 @@ 72A3512624EA820800DA9D46 /* AmityGalleryCollectionViewCell.swift in Sources */, B70D2BC82577200F00E2D56B /* AmityMessageAudioController.swift in Sources */, 72354424252F2D09006F9872 /* AmityHUD.swift in Sources */, + A9E106592C3B9582002BB4A1 /* ASCMentionManager.swift in Sources */, 0D12BD2B25D4307100D3E4BC /* AmityPostHeaderProtocol.swift in Sources */, 72A351A524EA821900DA9D46 /* AmityIconSet.swift in Sources */, 72DAC85B2614734E00422325 /* CommunityNotificationSettingItem.swift in Sources */, @@ -5296,7 +5312,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 12.3; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; ONLY_ACTIVE_ARCH = YES; @@ -5354,7 +5370,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 12.3; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; SDKROOT = iphoneos; @@ -5507,7 +5523,7 @@ /* End XCConfigurationList section */ /* Begin XCSwiftPackageProductDependency section */ - 68D959D32C4A41F3005DC4FA /* SharedFrameworks */ = { + A959BBCF2C5D012B00E23ABB /* SharedFrameworks */ = { isa = XCSwiftPackageProductDependency; productName = SharedFrameworks; }; diff --git a/UpstraUIKit/AmityUIKit4/AmityUIKit4.xcodeproj/project.pbxproj b/UpstraUIKit/AmityUIKit4/AmityUIKit4.xcodeproj/project.pbxproj index 32abe8a..be3b679 100644 --- a/UpstraUIKit/AmityUIKit4/AmityUIKit4.xcodeproj/project.pbxproj +++ b/UpstraUIKit/AmityUIKit4/AmityUIKit4.xcodeproj/project.pbxproj @@ -210,7 +210,6 @@ 68D4358D2B984698004482D7 /* AmityCommunityModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 68D4358C2B984698004482D7 /* AmityCommunityModel.swift */; }; 68D4358F2B984889004482D7 /* AmityTextEditorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 68D4358E2B984889004482D7 /* AmityTextEditorView.swift */; }; 68D435962BA0DCDE004482D7 /* AmityViewBuildable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 68D435952BA0DCDE004482D7 /* AmityViewBuildable.swift */; }; - 68D959D22C4A41E7005DC4FA /* SharedFrameworks in Frameworks */ = {isa = PBXBuildFile; productRef = 68D959D12C4A41E7005DC4FA /* SharedFrameworks */; }; 68D9BCC92C32FC2B0082685B /* StoryAdView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 68D9BCC82C32FC2B0082685B /* StoryAdView.swift */; }; 68D9BCCB2C348F420082685B /* UIKitStoryPaginator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 68D9BCCA2C348F420082685B /* UIKitStoryPaginator.swift */; }; 68D9BCCD2C348FB50082685B /* StoryFixedFrequencyAdInjector.swift in Sources */ = {isa = PBXBuildFile; fileRef = 68D9BCCC2C348FB50082685B /* StoryFixedFrequencyAdInjector.swift */; }; @@ -300,6 +299,7 @@ A955BA842C228B9300585C59 /* AdAsset.swift in Sources */ = {isa = PBXBuildFile; fileRef = A955BA832C228B9300585C59 /* AdAsset.swift */; }; A955BA862C228BAF00585C59 /* AdSeenEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = A955BA852C228BAF00585C59 /* AdSeenEvent.swift */; }; A955BA882C228BD500585C59 /* SDKModel+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = A955BA872C228BD500585C59 /* SDKModel+Extension.swift */; }; + A959BBD22C5D015300E23ABB /* SharedFrameworks in Frameworks */ = {isa = PBXBuildFile; productRef = A959BBD12C5D015300E23ABB /* SharedFrameworks */; }; A9634BD32BD5275200EF6E83 /* MessageCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9634BD22BD5275200EF6E83 /* MessageCache.swift */; }; A96B90EA2BF5B5D000DC7DEB /* ReactionListContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = A96B90E92BF5B5D000DC7DEB /* ReactionListContent.swift */; }; A96B90EC2BF5B60900DC7DEB /* ReactionListHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = A96B90EB2BF5B60900DC7DEB /* ReactionListHeader.swift */; }; @@ -709,6 +709,7 @@ A955BA832C228B9300585C59 /* AdAsset.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdAsset.swift; sourceTree = ""; }; A955BA852C228BAF00585C59 /* AdSeenEvent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdSeenEvent.swift; sourceTree = ""; }; A955BA872C228BD500585C59 /* SDKModel+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SDKModel+Extension.swift"; sourceTree = ""; }; + A959BBD32C5D016000E23ABB /* AmityUIKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = AmityUIKit.framework; sourceTree = BUILT_PRODUCTS_DIR; }; A9634BD22BD5275200EF6E83 /* MessageCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageCache.swift; sourceTree = ""; }; A96B90E92BF5B5D000DC7DEB /* ReactionListContent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReactionListContent.swift; sourceTree = ""; }; A96B90EB2BF5B60900DC7DEB /* ReactionListHeader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReactionListHeader.swift; sourceTree = ""; }; @@ -807,7 +808,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 68D959D22C4A41E7005DC4FA /* SharedFrameworks in Frameworks */, + A959BBD22C5D015300E23ABB /* SharedFrameworks in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1229,6 +1230,7 @@ 684AE10D2B0C5D2000FD7270 /* Frameworks */ = { isa = PBXGroup; children = ( + A959BBD32C5D016000E23ABB /* AmityUIKit.framework */, 689EE69A2BECC07800927D51 /* AmityUIKit.framework */, 684AE1332B0C968F00FD7270 /* AmitySDK.xcframework */, 684AE1342B0C968F00FD7270 /* Realm.xcframework */, @@ -2309,7 +2311,7 @@ ); name = AmityUIKit4; packageProductDependencies = ( - 68D959D12C4A41E7005DC4FA /* SharedFrameworks */, + A959BBD12C5D015300E23ABB /* SharedFrameworks */, ); productName = AmityUIKit4; productReference = 684AE0F12B0C5B0200FD7270 /* AmityUIKit4.framework */; @@ -3059,7 +3061,7 @@ /* End XCConfigurationList section */ /* Begin XCSwiftPackageProductDependency section */ - 68D959D12C4A41E7005DC4FA /* SharedFrameworks */ = { + A959BBD12C5D015300E23ABB /* SharedFrameworks */ = { isa = XCSwiftPackageProductDependency; productName = SharedFrameworks; }; diff --git a/UpstraUIKit/AmityUIKit4/AmityUIKit4/Ads/Engine/AdEngine.swift b/UpstraUIKit/AmityUIKit4/AmityUIKit4/Ads/Engine/AdEngine.swift index 9d4f4ad..500abe5 100644 --- a/UpstraUIKit/AmityUIKit4/AmityUIKit4/Ads/Engine/AdEngine.swift +++ b/UpstraUIKit/AmityUIKit4/AmityUIKit4/Ads/Engine/AdEngine.swift @@ -277,15 +277,15 @@ extension AdEngine { // AdEngine query ads filtering by activeness, readiness, and placement, then use AdSupplier to determine which ads to recommend. // This is the entry points for paginator. public func getRecommendedAds(count: Int, placement: AmityAdPlacement, communityId: String?) -> [AmityAd] { - let applicableAds = getApplicableAds(placement: placement) + let applicableAds = getApplicableAds(placement: placement, communityId: communityId) // Ask supplier to provide us with relevant ads return AdSupplier.shared.recommendAds(count: count, placement: placement, communityId: communityId, from: applicableAds) } // Determines ads which suits for given placement & whose assets are downloaded & ready to be used. - private func getApplicableAds(placement: AmityAdPlacement) -> [AmityAd] { - return ads.filter { + private func getApplicableAds(placement: AmityAdPlacement, communityId: String?) -> [AmityAd] { + let readyAds = ads.filter { // Criteria 1: Placement should be valid. let isPlacementValid = $0.placements.contains(placement) @@ -311,6 +311,32 @@ extension AdEngine { return isPlacementValid && isEndDateValid && isAssetReady } + + // Find if any ready ads targets matches community id + if let communityId { + let targetedAds = readyAds.filter { ad in + let commIds = ad.target?.communityIds ?? [] + return commIds.contains(communityId) + } + + // If there are no ads which target particular communities, we return non targeted ads + if targetedAds.isEmpty { + let nonTargetedAds = readyAds.filter { ad in + let commIds = ad.target?.communityIds ?? [] + return commIds.isEmpty + } + return nonTargetedAds + } + + return targetedAds + } else { + let nonTargetedAds = readyAds.filter { ad in + let commIds = ad.target?.communityIds ?? [] + return commIds.isEmpty + } + + return nonTargetedAds + } } } diff --git a/UpstraUIKit/AmityUIKit4/AmityUIKit4/Core/Managers/MentionListProvider.swift b/UpstraUIKit/AmityUIKit4/AmityUIKit4/Core/Managers/MentionListProvider.swift index 1239837..90b67ae 100644 --- a/UpstraUIKit/AmityUIKit4/AmityUIKit4/Core/Managers/MentionListProvider.swift +++ b/UpstraUIKit/AmityUIKit4/AmityUIKit4/Core/Managers/MentionListProvider.swift @@ -19,7 +19,8 @@ class MentionListProvider { // Repositories private var userRepository: AmityUserRepository = AmityUserRepository(client: AmityUIKitManagerInternal.shared.client) - private var channelMembersRepository: AmityChannelMembership? + private var channelMembersRepo: AmityChannelMembership? + private var communityMembersRepo: AmityCommunityMembership? private var communityRepository: AmityCommunityRepository = AmityCommunityRepository(client: AmityUIKitManagerInternal.shared.client) // Collection @@ -51,7 +52,7 @@ class MentionListProvider { } case .message(let subChannelId): if let channelId = subChannelId { - channelMembersRepository = AmityChannelMembership(client: client, andChannel: channelId) + channelMembersRepo = AmityChannelMembership(client: client, andChannel: channelId) } } @@ -116,6 +117,7 @@ class MentionListProvider { } private func setupCommunity(withId communityId: String) { + communityMembersRepo = AmityCommunityMembership(client: AmityUIKitManagerInternal.shared.client, andCommunityId: communityId) communityToken = communityRepository.getCommunity(withId: communityId).observe { [weak self] liveObject, error in if liveObject.dataStatus == .fresh { self?.communityToken?.invalidate() @@ -135,7 +137,7 @@ class MentionListProvider { mentionListToken = nil mentionListToken?.invalidate() - channelMembersCollection = channelMembersRepository?.searchMembers(displayName: displayName, filterBuilder: builder, roles: []) + channelMembersCollection = channelMembersRepo?.searchMembers(displayName: displayName, filterBuilder: builder, roles: [], includeDeleted: false) mentionListToken = channelMembersCollection?.observe({ [weak self] liveCollection, _, error in self?.handleSearchResponse(with: liveCollection) }) @@ -155,7 +157,7 @@ class MentionListProvider { mentionListToken = nil mentionListToken?.invalidate() - communityMembersCollection = communityRepository.searchMembers(communityId: communityId, displayName: displayName, membership: .member, roles: [], sortBy: .lastCreated) + communityMembersCollection = communityMembersRepo?.searchMembers(keyword: displayName, filter: [.member], roles: [], sortBy: .lastCreated, includeDeleted: false) mentionListToken = communityMembersCollection?.observe { [weak self] liveCollection, _, error in self?.handleSearchResponse(with: liveCollection) } diff --git a/UpstraUIKit/AmityUIKit4/AmityUIKit4/Core/Managers/MentionTextEditor.swift b/UpstraUIKit/AmityUIKit4/AmityUIKit4/Core/Managers/MentionTextEditor.swift index 8251f43..08c7a26 100644 --- a/UpstraUIKit/AmityUIKit4/AmityUIKit4/Core/Managers/MentionTextEditor.swift +++ b/UpstraUIKit/AmityUIKit4/AmityUIKit4/Core/Managers/MentionTextEditor.swift @@ -15,13 +15,6 @@ protocol MentionTextEditorDelegate: AnyObject { func didUpdateAttributedText(text: NSAttributedString) } -extension AmityMention: CustomStringConvertible { - - public var description: String { - return "Mention: \(self.userId ?? "") | Index: \(self.index) | Length: \(self.length)" - } -} - /// Highlights mentions in Text Editor class MentionTextEditor { diff --git a/UpstraUIKit/AmityUIKit4/AmityUIKit4/Social/Components/CommunityProfile/AmityCommunityFeedComponent.swift b/UpstraUIKit/AmityUIKit4/AmityUIKit4/Social/Components/CommunityProfile/AmityCommunityFeedComponent.swift index a9cbc05..270ab5c 100644 --- a/UpstraUIKit/AmityUIKit4/AmityUIKit4/Social/Components/CommunityProfile/AmityCommunityFeedComponent.swift +++ b/UpstraUIKit/AmityUIKit4/AmityUIKit4/Social/Components/CommunityProfile/AmityCommunityFeedComponent.swift @@ -95,6 +95,12 @@ public struct AmityCommunityFeedComponent: AmityComponentView { onTapPostDetailAction?(post, category) }, pageId: pageId) .contentShape(Rectangle()) + .background(GeometryReader { geometry in + Color.clear + .onChange(of: geometry.frame(in: .global)) { frame in + checkVisibilityAndMarkSeen(postContentFrame: frame, post: post) + } + }) Rectangle() .fill(Color(viewConfig.theme.baseColorShade4)) @@ -124,4 +130,18 @@ public struct AmityCommunityFeedComponent: AmityComponentView { } .updateTheme(with: viewConfig) } + + + private func checkVisibilityAndMarkSeen(postContentFrame: CGRect, post: AmityPostModel) { + let screenHeight = UIScreen.main.bounds.height + let visibleHeight = min(screenHeight, postContentFrame.maxY) - max(0, postContentFrame.minY) + let visiblePercentage = (visibleHeight / postContentFrame.height) * 100 + + if visiblePercentage > 60 && !postFeedViewModel.seenPostIds.contains(post.postId) { + postFeedViewModel.seenPostIds.insert(post.postId) + DispatchQueue.main.async { + post.analytic.markAsViewed() + } + } + } } diff --git a/UpstraUIKit/AmityUIKit4/AmityUIKit4/Social/Components/PostCreation/AmityMediaAttatchmentComponent.swift b/UpstraUIKit/AmityUIKit4/AmityUIKit4/Social/Components/PostCreation/AmityMediaAttatchmentComponent.swift index 7fc2bb5..5bed94c 100644 --- a/UpstraUIKit/AmityUIKit4/AmityUIKit4/Social/Components/PostCreation/AmityMediaAttatchmentComponent.swift +++ b/UpstraUIKit/AmityUIKit4/AmityUIKit4/Social/Components/PostCreation/AmityMediaAttatchmentComponent.swift @@ -34,11 +34,10 @@ public struct AmityMediaAttachmentComponent: AmityComponentView { public var body: some View { - HStack(spacing: 0) { - - if (currentType != nil) { - Spacer() - } + HStack(alignment: .center, spacing: 0) { + + Spacer() + .isHidden(currentType == nil, remove: true) let cameraButtonIcon = viewConfig.getConfig(elementId: .cameraButton, key: "image", of: String.self) ?? "" getItemView(image: AmityIcon.getImageResource(named: cameraButtonIcon), isHidden: false) { @@ -52,10 +51,9 @@ public struct AmityMediaAttachmentComponent: AmityComponentView { showCamera.isShown.toggle() hideKeyboard() } - - if currentType == .image || currentType == nil { - Spacer() - } + + Spacer() + .isHidden(currentType == .video, remove: true) let imageButtonIcon = viewConfig.getConfig(elementId: .imageButton, key: "image", of: String.self) ?? "" getItemView(image: AmityIcon.getImageResource(named: imageButtonIcon), isHidden: viewModel.medias.first?.type ?? .image != .image) { @@ -65,9 +63,8 @@ public struct AmityMediaAttachmentComponent: AmityComponentView { hideKeyboard() } - if currentType == .video || currentType == nil { - Spacer() - } + Spacer() + .isHidden(currentType == .image, remove: true) let videoButtonIcon = viewConfig.getConfig(elementId: .videoButton, key: "image", of: String.self) ?? "" getItemView(image: AmityIcon.getImageResource(named: videoButtonIcon), isHidden: viewModel.medias.first?.type ?? .video != .video) { @@ -77,14 +74,9 @@ public struct AmityMediaAttachmentComponent: AmityComponentView { hideKeyboard() } - if (currentType != nil) { - Spacer() - } - -// Spacer() + Spacer() + .isHidden(currentType == nil, remove: true) -// getItemView(image: AmityIcon.attatchmentIcon.getImageResource(), isDisable: false) {} - } .frame(maxWidth: .infinity) .padding(.bottom, 10) @@ -134,6 +126,9 @@ public struct AmityMediaAttachmentComponent: AmityComponentView { pickerViewModel.selectedImage = nil pickerViewModel.selectedMediaURL = nil } + .onAppear { + currentType = viewModel.medias.first?.type + } .alert(isPresented: $showMaximumMediaAlert) { let typeString = currentType == .image ? "images" : "videoes" return Alert(title: Text("Maximum upload limit reached"), message: Text("You’ve reached the upload limit of 10 \(typeString). Any additional \(typeString) will not be saved."), dismissButton: .cancel(Text("Close"))) diff --git a/UpstraUIKit/AmityUIKit4/AmityUIKit4/Social/Components/PostDetail/AmityPostContentComponent.swift b/UpstraUIKit/AmityUIKit4/AmityUIKit4/Social/Components/PostDetail/AmityPostContentComponent.swift index 1873553..18176fd 100644 --- a/UpstraUIKit/AmityUIKit4/AmityUIKit4/Social/Components/PostDetail/AmityPostContentComponent.swift +++ b/UpstraUIKit/AmityUIKit4/AmityUIKit4/Social/Components/PostDetail/AmityPostContentComponent.swift @@ -342,30 +342,27 @@ public struct AmityPostContentComponent: AmityComponentView { if style == .detail { VStack(spacing: 8) { HStack(spacing: 4) { - if post.reactionsCount != 0 { - Group { - Image(AmityIcon.likeReactionIcon.getImageResource()) - .resizable() - .frame(width: 20.0, height: 20.0) - Text("\(post.reactionsCount.formattedCountString) \(post.reactionsCount == 1 ? "like" : "likes")") - .font(.system(size: 13)) - .foregroundColor(Color(viewConfig.theme.baseColorShade2)) - } - .onTapGesture { - showReactionList.toggle() - } + Group { + Image(AmityIcon.likeReactionIcon.getImageResource()) + .resizable() + .frame(width: 20.0, height: 20.0) + .isHidden(post.reactionsCount == 0, remove: true) + + Text("\(post.reactionsCount.formattedCountString) \(post.reactionsCount == 1 ? "like" : "likes")") + .font(.system(size: 13)) + .foregroundColor(Color(viewConfig.theme.baseColorShade2)) + } + .onTapGesture { + showReactionList.toggle() } Spacer() - if post.allCommentCount != 0 { - Text("\(post.allCommentCount.formattedCountString) \(post.allCommentCount == 1 ? "comment" : "comments")") - .font(.system(size: 13)) - .foregroundColor(Color(viewConfig.theme.baseColorShade2)) - } + Text("\(post.allCommentCount.formattedCountString) \(post.allCommentCount == 1 ? "comment" : "comments")") + .font(.system(size: 13)) + .foregroundColor(Color(viewConfig.theme.baseColorShade2)) } .frame(height: 20) - .isHidden(post.reactionsCount == 0 && post.allCommentCount == 0, remove: true) } .padding([.leading, .trailing], 16) .sheet(isPresented: $showReactionList) { diff --git a/UpstraUIKit/AmityUIKit4/AmityUIKit4/Social/Components/SocialHome/NewsFeed/AmityNewsFeedComponent.swift b/UpstraUIKit/AmityUIKit4/AmityUIKit4/Social/Components/SocialHome/NewsFeed/AmityNewsFeedComponent.swift index 383a75b..02be809 100644 --- a/UpstraUIKit/AmityUIKit4/AmityUIKit4/Social/Components/SocialHome/NewsFeed/AmityNewsFeedComponent.swift +++ b/UpstraUIKit/AmityUIKit4/AmityUIKit4/Social/Components/SocialHome/NewsFeed/AmityNewsFeedComponent.swift @@ -141,7 +141,9 @@ public struct AmityNewsFeedComponent: AmityComponentView { if visiblePercentage > 60 && !postFeedViewModel.seenPostIds.contains(post.postId) { postFeedViewModel.seenPostIds.insert(post.postId) - post.analytic.markAsViewed() + DispatchQueue.main.async { + post.analytic.markAsViewed() + } } } } diff --git a/UpstraUIKit/AmityUIKit4/AmityUIKit4/Social/Components/SocialHome/NewsFeed/PostFeedViewModel.swift b/UpstraUIKit/AmityUIKit4/AmityUIKit4/Social/Components/SocialHome/NewsFeed/PostFeedViewModel.swift index 1b10e25..82428aa 100644 --- a/UpstraUIKit/AmityUIKit4/AmityUIKit4/Social/Components/SocialHome/NewsFeed/PostFeedViewModel.swift +++ b/UpstraUIKit/AmityUIKit4/AmityUIKit4/Social/Components/SocialHome/NewsFeed/PostFeedViewModel.swift @@ -50,15 +50,17 @@ class PostFeedViewModel: ObservableObject { let collection: AmityCollection + var paginatorCommunityId: String? = nil + switch feedType { case .community(let communityId): collection = feedManager.getCommunityFeedPosts(communityId: communityId) + paginatorCommunityId = communityId case .globalFeed: collection = feedManager.getGlobalFeedPosts() } - - paginator = UIKitPaginator(liveCollection: collection, adPlacement: .feed, modelIdentifier: { model in + paginator = UIKitPaginator(liveCollection: collection, adPlacement: .feed, communityId: paginatorCommunityId, modelIdentifier: { model in return model.postId }) paginator?.load() diff --git a/UpstraUIKit/AmityUIKit4/AmityUIKit4/Social/Pages/PostCreation/AmityPostComposerPage.swift b/UpstraUIKit/AmityUIKit4/AmityUIKit4/Social/Pages/PostCreation/AmityPostComposerPage.swift index 9d21d48..1b44825 100644 --- a/UpstraUIKit/AmityUIKit4/AmityUIKit4/Social/Pages/PostCreation/AmityPostComposerPage.swift +++ b/UpstraUIKit/AmityUIKit4/AmityUIKit4/Social/Pages/PostCreation/AmityPostComposerPage.swift @@ -258,6 +258,7 @@ class AmityPostComposerViewModel: ObservableObject { private let post: AmityPostModel? let mode: AmityPostComposerMode let targetType: AmityPostTargetType + private let networkMonitor = NetworkMonitor() @Published var displayName: String @@ -288,6 +289,7 @@ class AmityPostComposerViewModel: ObservableObject { @discardableResult func createPost(medias: [AmityMedia], files: [AmityFile]) async throws -> AmityPost { + guard networkMonitor.isConnected else { throw NSError(domain: "Internet is not connected.", code: 500) } let targetType: AmityPostTargetType = targetId == nil ? .user : .community var postBuilder: AmityPostBuilder diff --git a/UpstraUIKit/AmityUIKit4/AmityUIKit4/Social/Pages/PostDetail/AmityPostDetailPage.swift b/UpstraUIKit/AmityUIKit4/AmityUIKit4/Social/Pages/PostDetail/AmityPostDetailPage.swift index e2b5594..83561d5 100644 --- a/UpstraUIKit/AmityUIKit4/AmityUIKit4/Social/Pages/PostDetail/AmityPostDetailPage.swift +++ b/UpstraUIKit/AmityUIKit4/AmityUIKit4/Social/Pages/PostDetail/AmityPostDetailPage.swift @@ -27,9 +27,10 @@ public struct AmityPostDetailPage: AmityPageView { } public init(id: String) { - self._viewModel = StateObject(wrappedValue: AmityPostDetailPageViewModel(id: id)) - self._commentCoreViewModel = StateObject(wrappedValue: CommentCoreViewModel(referenceId: id, referenceType: .post, hideEmptyText: true, hideCommentButtons: false)) - self._commentComposerViewModel = StateObject(wrappedValue: CommentComposerViewModel(referenceId: id, referenceType: .post, community: nil, allowCreateComment: true)) + let postDetailViewModel = AmityPostDetailPageViewModel(id: id) + self._viewModel = StateObject(wrappedValue: postDetailViewModel) + self._commentCoreViewModel = StateObject(wrappedValue: CommentCoreViewModel(referenceId: id, referenceType: .post, hideEmptyText: true, hideCommentButtons: false, communityId: postDetailViewModel.post?.targetCommunity?.communityId)) + self._commentComposerViewModel = StateObject(wrappedValue: CommentComposerViewModel(referenceId: id, referenceType: .post, community: postDetailViewModel.post?.targetCommunity, allowCreateComment: true)) self._viewConfig = StateObject(wrappedValue: AmityViewConfigController(pageId: .postDetailPage)) } @@ -38,8 +39,8 @@ public struct AmityPostDetailPage: AmityPageView { self.postCategory = category self.hideTarget = hideTarget self._viewModel = StateObject(wrappedValue: AmityPostDetailPageViewModel(post: post)) - self._commentCoreViewModel = StateObject(wrappedValue: CommentCoreViewModel(referenceId: post.postId, referenceType: .post, hideEmptyText: true, hideCommentButtons: false)) - self._commentComposerViewModel = StateObject(wrappedValue: CommentComposerViewModel(referenceId: post.postId, referenceType: .post, community: nil, allowCreateComment: true)) + self._commentCoreViewModel = StateObject(wrappedValue: CommentCoreViewModel(referenceId: post.postId, referenceType: .post, hideEmptyText: true, hideCommentButtons: false, communityId: post.targetCommunity?.communityId)) + self._commentComposerViewModel = StateObject(wrappedValue: CommentComposerViewModel(referenceId: post.postId, referenceType: .post, community: post.targetCommunity, allowCreateComment: true)) self._viewConfig = StateObject(wrappedValue: AmityViewConfigController(pageId: .postDetailPage)) } @@ -143,8 +144,8 @@ public struct AmityPostDetailPage: AmityPageView { .onAppear { host.controller?.navigationController?.isNavigationBarHidden = true - if let isCommunityMember = viewModel.post?.isGroupMember { - commentCoreViewModel.hideCommentButtons = !isCommunityMember + if let targetCommunity = viewModel.post?.targetCommunity { + commentCoreViewModel.hideCommentButtons = !targetCommunity.isJoined } } diff --git a/UpstraUIKit/AmityUIKit4/AmityUIKit4/Social/Pages/SocialHome/AmitySocialHomePage.swift b/UpstraUIKit/AmityUIKit4/AmityUIKit4/Social/Pages/SocialHome/AmitySocialHomePage.swift index e180eea..6c3ac75 100644 --- a/UpstraUIKit/AmityUIKit4/AmityUIKit4/Social/Pages/SocialHome/AmitySocialHomePage.swift +++ b/UpstraUIKit/AmityUIKit4/AmityUIKit4/Social/Pages/SocialHome/AmitySocialHomePage.swift @@ -15,7 +15,7 @@ public struct AmitySocialHomePage: AmityPageView { .socialHomePage } - @State private var selectedTab: AmitySocialHomePageTab = .newsFeed + @StateObject private var viewModel: AmitySocialHomePageViewModel = AmitySocialHomePageViewModel() @StateObject private var viewConfig: AmityViewConfigController public init() { @@ -24,21 +24,21 @@ public struct AmitySocialHomePage: AmityPageView { public var body: some View { VStack(alignment: .leading, spacing: 0) { - AmitySocialHomeTopNavigationComponent(pageId: id, selectedTab: selectedTab, searchButtonAction: { - if selectedTab == .newsFeed { + AmitySocialHomeTopNavigationComponent(pageId: id, selectedTab: viewModel.selectedTab, searchButtonAction: { + if viewModel.selectedTab == .newsFeed { let context = AmitySocialHomePageBehavior.Context(page: self) AmityUIKitManagerInternal.shared.behavior.socialHomePageBehavior?.goToGlobalSearchPage(context: context) - } else if selectedTab == .myCommunities { + } else if viewModel.selectedTab == .myCommunities { let context = AmitySocialHomePageBehavior.Context(page: self) AmityUIKitManagerInternal.shared.behavior.socialHomePageBehavior?.goToMyCommunitiesSearchPage(context: context) } }) - SocialHomePageTabView($selectedTab) + SocialHomePageTabView($viewModel.selectedTab) .frame(height: 62) - SocialHomeContainerView($selectedTab, pageId: id) + SocialHomeContainerView($viewModel.selectedTab, pageId: id) } .background(Color(viewConfig.theme.backgroundColor)) .ignoresSafeArea(edges: .bottom) @@ -48,3 +48,22 @@ public struct AmitySocialHomePage: AmityPageView { } } } + + +class AmitySocialHomePageViewModel: ObservableObject { + @Published var selectedTab: AmitySocialHomePageTab = .newsFeed + + init() { + /// Observe didPostCreated event sent from AmityPostCreationPage + /// We need to explicitly change the tab to Newsfeed. + NotificationCenter.default.addObserver(self, selector: #selector(didPostCreated(_:)), name: .didPostCreated, object: nil) + } + + deinit { + NotificationCenter.default.removeObserver(self) + } + + @objc private func didPostCreated(_ notification: Notification) { + selectedTab = .newsFeed + } +} diff --git a/UpstraUIKit/AmityUIKit4/AmityUIKit4/Social/Pages/SocialHome/ChildViews/SocialHomePageTabView.swift b/UpstraUIKit/AmityUIKit4/AmityUIKit4/Social/Pages/SocialHome/ChildViews/SocialHomePageTabView.swift index b71414d..f854985 100644 --- a/UpstraUIKit/AmityUIKit4/AmityUIKit4/Social/Pages/SocialHome/ChildViews/SocialHomePageTabView.swift +++ b/UpstraUIKit/AmityUIKit4/AmityUIKit4/Social/Pages/SocialHome/ChildViews/SocialHomePageTabView.swift @@ -39,16 +39,17 @@ struct SocialHomePageTabView: View { TabButtonView(title: getTitle(tab: item.tab), selected: item.selected) .onTapGesture { selectedTab = item.tab - - for (index, item) in tabItems.enumerated() { - tabItems[index].selected = item.tab == selectedTab - } } } } .padding(.leading, 20) .padding(.trailing, 2) } + .onChange(of: selectedTab) { value in + for (index, item) in tabItems.enumerated() { + tabItems[index].selected = item.tab == value + } + } .onAppear { /// Filter out tabs if element is excluded in config tabItems = tabItems.compactMap({ item in diff --git a/UpstraUIKit/AmityUIKit4/AmityUIKit4/Story/Components/AdComponent/AmityCommentAdComponent.swift b/UpstraUIKit/AmityUIKit4/AmityUIKit4/Story/Components/AdComponent/AmityCommentAdComponent.swift index 22a42f3..62a640c 100644 --- a/UpstraUIKit/AmityUIKit4/AmityUIKit4/Story/Components/AdComponent/AmityCommentAdComponent.swift +++ b/UpstraUIKit/AmityUIKit4/AmityUIKit4/Story/Components/AdComponent/AmityCommentAdComponent.swift @@ -129,7 +129,7 @@ struct AmityCommentAdComponent: View { } .contentShape(Rectangle()) .background(Color(viewConfig.theme.backgroundColor)) - .padding(.bottom, 1) + .padding([.top, .bottom], 3) .onAppear { AdEngine.shared.markAsSeen(ad: ad, placement: .comment) } diff --git a/UpstraUIKit/AmityUIKit4/AmityUIKit4/Story/Components/StoryComment/AmityCommentTrayComponent.swift b/UpstraUIKit/AmityUIKit4/AmityUIKit4/Story/Components/StoryComment/AmityCommentTrayComponent.swift index 303ae8f..f2c086f 100644 --- a/UpstraUIKit/AmityUIKit4/AmityUIKit4/Story/Components/StoryComment/AmityCommentTrayComponent.swift +++ b/UpstraUIKit/AmityUIKit4/AmityUIKit4/Story/Components/StoryComment/AmityCommentTrayComponent.swift @@ -31,7 +31,7 @@ public struct AmityCommentTrayComponent: AmityComponentView { shouldAllowCreation: Bool = false, pageId: PageId? = nil) { - self._commentCoreViewModel = StateObject(wrappedValue: CommentCoreViewModel(referenceId: referenceId, referenceType: referenceType, hideEmptyText: false, hideCommentButtons: !shouldAllowInteraction)) + self._commentCoreViewModel = StateObject(wrappedValue: CommentCoreViewModel(referenceId: referenceId, referenceType: referenceType, hideEmptyText: false, hideCommentButtons: !shouldAllowInteraction, communityId: community?.communityId)) self._commentComposerViewModel = StateObject(wrappedValue: CommentComposerViewModel(referenceId: referenceId, referenceType: referenceType, community: community, allowCreateComment: shouldAllowCreation)) self._viewConfig = StateObject(wrappedValue: AmityViewConfigController(pageId: pageId, componentId: .commentTrayComponent)) self.pageId = pageId diff --git a/UpstraUIKit/AmityUIKit4/AmityUIKit4/Story/Components/StoryComment/ChildViews/CommentBottomSheetView.swift b/UpstraUIKit/AmityUIKit4/AmityUIKit4/Story/Components/StoryComment/ChildViews/CommentBottomSheetView.swift index 6570ae9..aff2430 100644 --- a/UpstraUIKit/AmityUIKit4/AmityUIKit4/Story/Components/StoryComment/ChildViews/CommentBottomSheetView.swift +++ b/UpstraUIKit/AmityUIKit4/AmityUIKit4/Story/Components/StoryComment/ChildViews/CommentBottomSheetView.swift @@ -23,8 +23,8 @@ struct CommentBottomSheetView: View { getOwnerBottomSheetView() } else { getNonOwnerBottomSheetView() - .onChange(of: viewModel.sheetState.isShown) { value in - if value, let comment = viewModel.sheetState.comment { + .onAppear { + if let comment = viewModel.sheetState.comment { viewModel.updateCommentFlaggedByMeState(id: comment.id) } } diff --git a/UpstraUIKit/AmityUIKit4/AmityUIKit4/Story/Components/StoryComment/ChildViews/CommentComposerView.swift b/UpstraUIKit/AmityUIKit4/AmityUIKit4/Story/Components/StoryComment/ChildViews/CommentComposerView.swift index 33b7aac..1c2ada3 100644 --- a/UpstraUIKit/AmityUIKit4/AmityUIKit4/Story/Components/StoryComment/ChildViews/CommentComposerView.swift +++ b/UpstraUIKit/AmityUIKit4/AmityUIKit4/Story/Components/StoryComment/ChildViews/CommentComposerView.swift @@ -43,7 +43,7 @@ struct CommentComposerView: View { } } .frame(height: 40) - .background(Color(viewConfig.theme.backgroundColor)) + .background(Color(viewConfig.theme.baseColorShade4)) .isHidden(!viewModel.replyState.showToReply) HStack(spacing: 8) { @@ -80,7 +80,11 @@ struct CommentComposerView: View { hideKeyboard() } label: { Text("Post") - .foregroundColor(.accentColor) + .overlay( + Text("Post") + .foregroundColor(Color(viewConfig.theme.primaryColor)) + .opacity(viewModel.text.isEmpty ? 0.4 : 1.0) + ) } .padding(.trailing, 12) .disabled(viewModel.text.isEmpty) diff --git a/UpstraUIKit/AmityUIKit4/AmityUIKit4/Story/Components/StoryComment/ChildViews/CommentCoreView.swift b/UpstraUIKit/AmityUIKit4/AmityUIKit4/Story/Components/StoryComment/ChildViews/CommentCoreView.swift index c71def8..e2715a6 100644 --- a/UpstraUIKit/AmityUIKit4/AmityUIKit4/Story/Components/StoryComment/ChildViews/CommentCoreView.swift +++ b/UpstraUIKit/AmityUIKit4/AmityUIKit4/Story/Components/StoryComment/ChildViews/CommentCoreView.swift @@ -57,7 +57,7 @@ class CommentCoreViewModel: ObservableObject { private var paginatorCancellable: AnyCancellable? - init(referenceId: String, referenceType: AmityCommentReferenceType, hideEmptyText: Bool, hideCommentButtons: Bool) { + init(referenceId: String, referenceType: AmityCommentReferenceType, hideEmptyText: Bool, hideCommentButtons: Bool, communityId: String? = nil) { self.referenceId = referenceId self.referenceType = referenceType self.hideEmptyText = hideEmptyText @@ -69,7 +69,7 @@ class CommentCoreViewModel: ObservableObject { includeDeleted: true) let collection = commentManager.getComments(queryOptions: queryOptions) commentCollection = collection - paginator = UIKitPaginator(liveCollection: collection, adPlacement: .comment, modelIdentifier: { model in + paginator = UIKitPaginator(liveCollection: collection, adPlacement: .comment, communityId: communityId, modelIdentifier: { model in return model.commentId }) paginator.load() diff --git a/UpstraUIKit/AmityUIKit4/AmityUIKit4/Story/Components/StoryComment/ChildViews/CommentListView.swift b/UpstraUIKit/AmityUIKit4/AmityUIKit4/Story/Components/StoryComment/ChildViews/CommentListView.swift index 0ebd6fb..d44fbad 100644 --- a/UpstraUIKit/AmityUIKit4/AmityUIKit4/Story/Components/StoryComment/ChildViews/CommentListView.swift +++ b/UpstraUIKit/AmityUIKit4/AmityUIKit4/Story/Components/StoryComment/ChildViews/CommentListView.swift @@ -32,7 +32,6 @@ struct CommentListView: View where Content: View { ScrollViewReader { value in ScrollView { LazyVStack { - Color.clear.frame(height: 6) headerView() @@ -98,8 +97,6 @@ struct CommentListView: View where Content: View { } else { getDeletedMessageView() - .padding([.top, .bottom], 3) - .padding(.leading, 52) } if comment.childrenNumber != 0 { @@ -127,24 +124,32 @@ struct CommentListView: View where Content: View { @ViewBuilder func getDeletedMessageView() -> some View { - HStack { - HStack(spacing: 8) { + VStack(spacing: 10) { + Rectangle() + .fill(Color(viewConfig.theme.baseColorShade4)) + .frame(height: 1) + + HStack(spacing: 16) { Image(AmityIcon.deletedMessageIcon.getImageResource()) .resizable() .renderingMode(.template) .frame(width: 16, height: 16) - .padding(.leading, 8) + .padding(.leading, 18) .foregroundColor(Color(viewConfig.theme.baseColorShade2)) + Text(AmityLocalizedStringSet.Comment.deletedCommentMessage.localizedString) - .font(.system(size: 13)) + .font(.system(size: 15)) .foregroundColor(Color(viewConfig.theme.baseColorShade2)) .padding(.trailing, 16) + + Spacer() } - .frame(height: 28) - .background(Color(viewConfig.theme.baseColorShade4)) - .clipShape(RoundedRectangle(cornerRadius: 4)) - Spacer() + + Rectangle() + .fill(Color(viewConfig.theme.baseColorShade4)) + .frame(height: 1) } + .accessibilityIdentifier(AccessibilityID.AmityCommentTrayComponent.CommentBubble.deletedComment) } } diff --git a/UpstraUIKit/AmityUIKit4/AmityUIKit4/Story/Components/StoryComment/ChildViews/ReplyCommentView.swift b/UpstraUIKit/AmityUIKit4/AmityUIKit4/Story/Components/StoryComment/ChildViews/ReplyCommentView.swift index 27ca827..9505f66 100644 --- a/UpstraUIKit/AmityUIKit4/AmityUIKit4/Story/Components/StoryComment/ChildViews/ReplyCommentView.swift +++ b/UpstraUIKit/AmityUIKit4/AmityUIKit4/Story/Components/StoryComment/ChildViews/ReplyCommentView.swift @@ -28,7 +28,7 @@ struct ReplyCommentView: View { // Reply Comments VStack { getViewReplyCommentButton(viewModel.parentComment) - .padding([.top, .bottom], 3) + .padding([.top, .bottom], 6) .padding(.leading, 52) .isHidden(hideViewReplyCommentButton) @@ -36,9 +36,10 @@ struct ReplyCommentView: View { ReplyCommentListView(collection: collection, hideCommentButtons: hideCommentButtons, commentButtonAction: commentButtonAction) + .padding(.top, 6) getViewMoreReplyCommentButton() - .padding([.top, .bottom], 3) + .padding([.top, .bottom], 6) .padding(.leading, 52) .isHidden(!collection.hasPrevious) diff --git a/UpstraUIKit/AmityUIKit4/AmityUIKit4/Story/Models/AmityStoryTargetModel.swift b/UpstraUIKit/AmityUIKit4/AmityUIKit4/Story/Models/AmityStoryTargetModel.swift index 58732e6..1f45003 100644 --- a/UpstraUIKit/AmityUIKit4/AmityUIKit4/Story/Models/AmityStoryTargetModel.swift +++ b/UpstraUIKit/AmityUIKit4/AmityUIKit4/Story/Models/AmityStoryTargetModel.swift @@ -42,6 +42,7 @@ public class AmityStoryTargetModel: ObservableObject, Identifiable, Equatable { public init(_ storyTarget: AmityStoryTarget) { + self.storyTarget = storyTarget self.targetId = storyTarget.targetId self.targetName = storyTarget.community?.displayName ?? AmityLocalizedStringSet.General.anonymous.localizedString self.isVerifiedTarget = storyTarget.community?.isOfficial ?? false @@ -53,6 +54,7 @@ public class AmityStoryTargetModel: ObservableObject, Identifiable, Equatable { } func updateModel(_ storyTarget: AmityStoryTarget) { + self.storyTarget = storyTarget self.hasUnseenStory = storyTarget.hasUnseen self.hasFailedStory = storyTarget.failedStoriesCount != 0 self.hasSyncingStory = storyTarget.syncingStoriesCount != 0 @@ -74,7 +76,8 @@ public class AmityStoryTargetModel: ObservableObject, Identifiable, Equatable { paginatorCancellable = nil - paginator = UIKitStoryPaginator(liveCollection: storyCollection, surplus: surplus, modelIdentifier: { $0.storyId }) + let communityId = storyTarget?.community?.communityId + paginator = UIKitStoryPaginator(liveCollection: storyCollection, surplus: surplus, communityId: communityId, modelIdentifier: { $0.storyId }) paginator?.load() paginatorCancellable = paginator?.$snapshots diff --git a/UpstraUIKit/AmityUIKitLiveStream/AmityUIKitLiveStream.xcodeproj/project.pbxproj b/UpstraUIKit/AmityUIKitLiveStream/AmityUIKitLiveStream.xcodeproj/project.pbxproj index f3fa371..a15ac42 100644 --- a/UpstraUIKit/AmityUIKitLiveStream/AmityUIKitLiveStream.xcodeproj/project.pbxproj +++ b/UpstraUIKit/AmityUIKitLiveStream/AmityUIKitLiveStream.xcodeproj/project.pbxproj @@ -7,7 +7,6 @@ objects = { /* Begin PBXBuildFile section */ - 68D959D62C4A41FE005DC4FA /* SharedFrameworks in Frameworks */ = {isa = PBXBuildFile; productRef = 68D959D52C4A41FE005DC4FA /* SharedFrameworks */; }; A0B68B3026E07278007D7B5B /* LiveStreamViewController+GoLive.swift in Sources */ = {isa = PBXBuildFile; fileRef = A0B68B2F26E07278007D7B5B /* LiveStreamViewController+GoLive.swift */; }; A0B68B3626E07824007D7B5B /* AsyncOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = A0B68B3526E07824007D7B5B /* AsyncOperation.swift */; }; A0B68B3F26E07912007D7B5B /* CreatePost.swift in Sources */ = {isa = PBXBuildFile; fileRef = A0B68B3E26E07912007D7B5B /* CreatePost.swift */; }; @@ -30,6 +29,7 @@ A0BD0B5E26DF37160054088B /* LiveStreamBroadcastVC+Keyboard.swift in Sources */ = {isa = PBXBuildFile; fileRef = A0BD0B5D26DF37160054088B /* LiveStreamBroadcastVC+Keyboard.swift */; }; A0BD0B6026DF377A0054088B /* LiveStreamBroadcastVC+CoverImagePicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = A0BD0B5F26DF377A0054088B /* LiveStreamBroadcastVC+CoverImagePicker.swift */; }; A0BD0B6226DF3A3F0054088B /* LiveStreamBroadcast+UIContainerState.swift in Sources */ = {isa = PBXBuildFile; fileRef = A0BD0B6126DF3A3F0054088B /* LiveStreamBroadcast+UIContainerState.swift */; }; + A959BBD82C5D017000E23ABB /* SharedFrameworks in Frameworks */ = {isa = PBXBuildFile; productRef = A959BBD72C5D017000E23ABB /* SharedFrameworks */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ @@ -69,8 +69,8 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 68D959D62C4A41FE005DC4FA /* SharedFrameworks in Frameworks */, A0BD0B3426DDD9820054088B /* AmityUIKit.framework in Frameworks */, + A959BBD82C5D017000E23ABB /* SharedFrameworks in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -245,7 +245,7 @@ ); name = AmityUIKitLiveStream; packageProductDependencies = ( - 68D959D52C4A41FE005DC4FA /* SharedFrameworks */, + A959BBD72C5D017000E23ABB /* SharedFrameworks */, ); productName = AmityUIKitLiveStream; productReference = A0BD0B1526DCE4F50054088B /* AmityUIKitLiveStream.framework */; @@ -529,7 +529,7 @@ /* End XCConfigurationList section */ /* Begin XCSwiftPackageProductDependency section */ - 68D959D52C4A41FE005DC4FA /* SharedFrameworks */ = { + A959BBD72C5D017000E23ABB /* SharedFrameworks */ = { isa = XCSwiftPackageProductDependency; productName = SharedFrameworks; }; diff --git a/UpstraUIKit/AmityUIKitLiveStream/AmityUIKitLiveStream/New Group/LiveStreamBroadcastViewController.swift b/UpstraUIKit/AmityUIKitLiveStream/AmityUIKitLiveStream/New Group/LiveStreamBroadcastViewController.swift index ec7fa3d..310174e 100644 --- a/UpstraUIKit/AmityUIKitLiveStream/AmityUIKitLiveStream/New Group/LiveStreamBroadcastViewController.swift +++ b/UpstraUIKit/AmityUIKitLiveStream/AmityUIKitLiveStream/New Group/LiveStreamBroadcastViewController.swift @@ -37,7 +37,7 @@ final public class LiveStreamBroadcastViewController: UIViewController { let liveDurationFormatter = DateComponentsFormatter() // MARK: - Private Const Properties - private let mentionManager: AmityMentionManager + private let mentionManager: ASCMentionManager // MARK: - States private var hasSetupBroadcaster = false @@ -129,7 +129,7 @@ final public class LiveStreamBroadcastViewController: UIViewController { streamRepository = AmityStreamRepository(client: client) postRepository = AmityPostRepository(client: client) broadcaster = AmityVideoBroadcaster(client: client) - mentionManager = AmityMentionManager(withType: .post(communityId: targetId)) + mentionManager = ASCMentionManager(withType: .post(communityId: targetId)) let bundle = Bundle(for: type(of: self)) super.init(nibName: "LiveStreamBroadcastViewController", bundle: bundle) @@ -173,8 +173,8 @@ final public class LiveStreamBroadcastViewController: UIViewController { updateCoverImageSelection() switchToUIState(.create) mentionManager.delegate = self - mentionManager.setFont(AmityFontSet.body, highlightFont: AmityFontSet.bodyBold) - mentionManager.setColor(.white, highlightColor: .white) + mentionManager.highlightAttributes = [.font: AmityFontSet.bodyBold, .foregroundColor: UIColor.white] + mentionManager.typingAttributes = [.font: AmityFontSet.body, .foregroundColor: UIColor.white] // Observe app life cycle notfications NotificationCenter.default.addObserver(self, selector: #selector(suspendLiveStream), name: UIApplication.didEnterBackgroundNotification, object: nil) @@ -418,15 +418,6 @@ final public class LiveStreamBroadcastViewController: UIViewController { mentionTableView.register(AmityMentionTableViewCell.nib, forCellReuseIdentifier: AmityMentionTableViewCell.identifier) } - private func showAlertForMaximumCharacters() { - let title = "Unable to post" - let message = "Unable message" - let alertController = UIAlertController(title: title, message: message, preferredStyle: .alert) - let cancelAction = UIAlertAction(title: "Done", style: .cancel, handler: nil) - alertController.addAction(cancelAction) - present(alertController, animated: true, completion: nil) - } - // MARK: - IBActions @IBAction private func switchCameraButtonDidTouch() { @@ -494,8 +485,8 @@ extension LiveStreamBroadcastViewController: AmityTextViewDelegate { } public func textView(_ textView: AmityTextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool { - if textView.text?.count ?? 0 > AmityMentionManager.maximumCharacterCountForPost { - showAlertForMaximumCharacters() + if textView.text?.count ?? 0 > ASCMentionManager.maximumCharacterCountForPost { + didReachMaxCharacterCountLimit() return false } return mentionManager.shouldChangeTextIn(textView, inRange: range, replacementText: text, currentText: textView.text ?? "") @@ -528,9 +519,10 @@ extension LiveStreamBroadcastViewController: AmityVideoBroadcasterDelegate { } -// MARK: - AmityMentionManagerDelegate -extension LiveStreamBroadcastViewController: AmityMentionManagerDelegate { - public func didGetUsers(users: [AmityMentionUserModel]) { +// MARK: - ASCMentionManagerDelegate +extension LiveStreamBroadcastViewController: ASCMentionManagerDelegate { + + public func didUpdateMentionUsers(users: [AmityUIKit.AmityMentionUserModel]) { if users.isEmpty { mentionTableViewHeightConstraint.constant = 0 mentionTableView.isHidden = true @@ -545,32 +537,41 @@ extension LiveStreamBroadcastViewController: AmityMentionManagerDelegate { } } - public func didCreateAttributedString(attributedString: NSAttributedString) { - descriptionTextView.attributedText = attributedString - descriptionTextView.typingAttributes = [.font: AmityFontSet.body, .foregroundColor: UIColor.white] - } - - public func didMentionsReachToMaximumLimit() { + public func didReachMaxMentionLimit() { let alertController = UIAlertController(title: AmityLocalizedStringSet.Mention.unableToMentionTitle.localizedString, message: AmityLocalizedStringSet.Mention.unableToMentionReplyDescription.localizedString, preferredStyle: .alert) let cancelAction = UIAlertAction(title: AmityLocalizedStringSet.General.done.localizedString, style: .cancel, handler: nil) alertController.addAction(cancelAction) present(alertController, animated: true, completion: nil) } - public func didCharactersReachToMaximumLimit() { - showAlertForMaximumCharacters() + public func didReachMaxCharacterCountLimit() { + let title = "Unable to post" + let message = "Unable message" + let alertController = UIAlertController(title: title, message: message, preferredStyle: .alert) + let cancelAction = UIAlertAction(title: "Done", style: .cancel, handler: nil) + alertController.addAction(cancelAction) + present(alertController, animated: true, completion: nil) + } + + public func didCreateAttributedString(attributedString: NSAttributedString) { + descriptionTextView.attributedText = attributedString + descriptionTextView.typingAttributes = [.font: AmityFontSet.body, .foregroundColor: UIColor.white] } } // MARK: - UITableViewDataSource extension LiveStreamBroadcastViewController: UITableViewDataSource { + public func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - return mentionManager.users.count + return mentionManager.mentionProvider.mentionList.count } public func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { guard let cell = tableView.dequeueReusableCell(withIdentifier: AmityMentionTableViewCell.identifier) as? AmityMentionTableViewCell else { return UITableViewCell() } - if let model = mentionManager.item(at: indexPath) { + + let provider = mentionManager.mentionProvider + if indexPath.row < provider.mentionList.count { + let model = provider.mentionList[indexPath.row] cell.display(with: model) } @@ -597,7 +598,7 @@ extension LiveStreamBroadcastViewController: UITableViewDelegate { public func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) { if tableView.isBottomReached { - mentionManager.loadMore() + mentionManager.mentionProvider.loadMore() } } } diff --git a/UpstraUIKit/SampleApp/SampleApp.xcodeproj/project.pbxproj b/UpstraUIKit/SampleApp/SampleApp.xcodeproj/project.pbxproj index 5b1f4c5..5db8fba 100644 --- a/UpstraUIKit/SampleApp/SampleApp.xcodeproj/project.pbxproj +++ b/UpstraUIKit/SampleApp/SampleApp.xcodeproj/project.pbxproj @@ -21,7 +21,6 @@ 682C761C2B331DAE00018F80 /* AmityUIKit4.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 684AE12D2B0C841400FD7270 /* AmityUIKit4.framework */; }; 682C761D2B331DAE00018F80 /* AmityUIKit4.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 684AE12D2B0C841400FD7270 /* AmityUIKit4.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 684097562B30607F00697E1B /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 684097552B30607F00697E1B /* GoogleService-Info.plist */; }; - 68D959D82C4A420C005DC4FA /* SharedFrameworks in Frameworks */ = {isa = PBXBuildFile; productRef = 68D959D72C4A420C005DC4FA /* SharedFrameworks */; }; 68F5D9FE2B481E4700A9FA0D /* AmityUIKit4.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 68F5D9FD2B481E4700A9FA0D /* AmityUIKit4.framework */; }; 68F5D9FF2B481E4700A9FA0D /* AmityUIKit4.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 68F5D9FD2B481E4700A9FA0D /* AmityUIKit4.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 7214C9CB2632BE5500192BB3 /* UserLevelPushNotificationsTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7214C9CA2632BE5500192BB3 /* UserLevelPushNotificationsTableViewController.swift */; }; @@ -99,6 +98,7 @@ A03190A7272169C1008A85DC /* PostCreatorSettingsPage.swift in Sources */ = {isa = PBXBuildFile; fileRef = A03190A6272169C1008A85DC /* PostCreatorSettingsPage.swift */; }; A0BD0B4826DDE0E30054088B /* AmityUIKitLiveStream.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A0BD0B4726DDE0E30054088B /* AmityUIKitLiveStream.framework */; }; A0BD0B4926DDE0E30054088B /* AmityUIKitLiveStream.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = A0BD0B4726DDE0E30054088B /* AmityUIKitLiveStream.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + A959BBDA2C5D026300E23ABB /* SharedFrameworks in Frameworks */ = {isa = PBXBuildFile; productRef = A959BBD92C5D026300E23ABB /* SharedFrameworks */; }; A9FF80E62BBD10010088A317 /* LiveChatListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9FF80E52BBD10010088A317 /* LiveChatListView.swift */; }; A9FF80ED2BBD55870088A317 /* LiveChatListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9FF80E52BBD10010088A317 /* LiveChatListView.swift */; }; B72861D924C573B100ECC563 /* TabbarViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B72861D824C573B100ECC563 /* TabbarViewController.swift */; }; @@ -276,9 +276,9 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 68D959D82C4A420C005DC4FA /* SharedFrameworks in Frameworks */, A0BD0B4826DDE0E30054088B /* AmityUIKitLiveStream.framework in Frameworks */, 68F5D9FE2B481E4700A9FA0D /* AmityUIKit4.framework in Frameworks */, + A959BBDA2C5D026300E23ABB /* SharedFrameworks in Frameworks */, D478D16926240A5E006EA140 /* AmityUIKit.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -601,7 +601,7 @@ ); name = SampleApp; packageProductDependencies = ( - 68D959D72C4A420C005DC4FA /* SharedFrameworks */, + A959BBD92C5D026300E23ABB /* SharedFrameworks */, ); productName = SampleApp; productReference = B78DA47524BED7D300EE902B /* SampleApp.app */; @@ -1282,15 +1282,15 @@ /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ - 68D959D72C4A420C005DC4FA /* SharedFrameworks */ = { - isa = XCSwiftPackageProductDependency; - productName = SharedFrameworks; - }; 92DBE8A32ACA98CF007D873C /* FirebaseCrashlytics */ = { isa = XCSwiftPackageProductDependency; package = 92DBE8A42ACA98CF007D873C /* XCRemoteSwiftPackageReference "firebase-ios-sdk" */; productName = FirebaseCrashlytics; }; + A959BBD92C5D026300E23ABB /* SharedFrameworks */ = { + isa = XCSwiftPackageProductDependency; + productName = SharedFrameworks; + }; /* End XCSwiftPackageProductDependency section */ }; rootObject = B78DA46D24BED7D300EE902B /* Project object */; diff --git a/UpstraUIKit/SharedFrameworks/Package.swift b/UpstraUIKit/SharedFrameworks/Package.swift index a394827..4f98433 100644 --- a/UpstraUIKit/SharedFrameworks/Package.swift +++ b/UpstraUIKit/SharedFrameworks/Package.swift @@ -23,28 +23,28 @@ let package = Package( dependencies: []), .binaryTarget( name: "AmitySDK", - url: "https://sdk.amity.co/sdk-release/ios-uikit-frameworks/4.0.0-beta15/AmitySDK.xcframework.zip", - checksum: "3632d5f9446b2ca24879fa18f3adbc6b48bcf494c830357b8d6c7bc026853f15" + url: "https://sdk.amity.co/sdk-release/ios-uikit-frameworks/4.0.0-beta16/AmitySDK.xcframework.zip", + checksum: "7b79b61f0a9d821edd9a5f9a2db08e5219a87aa119fd885c455bf6dd43fcb51c" ), .binaryTarget( name: "Realm", - url: "https://sdk.amity.co/sdk-release/ios-uikit-frameworks/4.0.0-beta15/Realm.xcframework.zip", - checksum: "ac6ef7d2ad4734216dff9c09eb8c69e81e93f41e6d3f00093d6775365abb7c1c" + url: "https://sdk.amity.co/sdk-release/ios-uikit-frameworks/4.0.0-beta16/Realm.xcframework.zip", + checksum: "1bfb24961f7f57fc434da6447d6c187a70925a04b2668d4dc6c3f49e18e4b72c" ), .binaryTarget( name: "RealmSwift", - url: "https://sdk.amity.co/sdk-release/ios-uikit-frameworks/4.0.0-beta15/RealmSwift.xcframework.zip", - checksum: "a8a04f816e317b1c3336b6cf2e0a2f9914d2ca226b266c3568db5b0b92de40e6" + url: "https://sdk.amity.co/sdk-release/ios-uikit-frameworks/4.0.0-beta16/RealmSwift.xcframework.zip", + checksum: "4737e8315f17ec40d95e17e39863d5116b1ae273124d2ac49f15dc7a8aa289a1" ), .binaryTarget( name: "AmityLiveVideoBroadcastKit", - url: "https://sdk.amity.co/sdk-release/ios-uikit-frameworks/4.0.0-beta15/AmityLiveVideoBroadcastKit.xcframework.zip", - checksum: "0b266e5c4da68fa66f5200ba12606912c8d4dedc5f1bb49540291b72ef5f10af" + url: "https://sdk.amity.co/sdk-release/ios-uikit-frameworks/4.0.0-beta16/AmityLiveVideoBroadcastKit.xcframework.zip", + checksum: "3c383c6c3fa82d56c3eaed71aab2dabfd6fd6279883f058b802480f4b0163f88" ), .binaryTarget( name: "AmityVideoPlayerKit", - url: "https://sdk.amity.co/sdk-release/ios-uikit-frameworks/4.0.0-beta15/AmityVideoPlayerKit.xcframework.zip", - checksum: "44672b3cae229a1ae2121a44b3a309186355b0770fbf612667820a8206313873" + url: "https://sdk.amity.co/sdk-release/ios-uikit-frameworks/4.0.0-beta16/AmityVideoPlayerKit.xcframework.zip", + checksum: "fc19c62c9ad42fe34f63e7d4c59e504c32be6e7e53b0647547b20e44f8790ac5" ), .binaryTarget( name: "MobileVLCKit", diff --git a/UpstraUIKit/UpstraUIKit/Assets.xcassets/icons/Community Settings/icon_item_edit_profile.imageset/Contents.json b/UpstraUIKit/UpstraUIKit/Assets.xcassets/icons/Community Settings/icon_item_edit_profile.imageset/Contents.json deleted file mode 100644 index 6eaa875..0000000 --- a/UpstraUIKit/UpstraUIKit/Assets.xcassets/icons/Community Settings/icon_item_edit_profile.imageset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "images" : [ - { - "filename" : "icon_item_edit_profile.pdf", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/UpstraUIKit/UpstraUIKit/Assets.xcassets/icons/Community Settings/icon_item_edit_profile.imageset/icon_item_edit_profile.pdf b/UpstraUIKit/UpstraUIKit/Assets.xcassets/icons/Community Settings/icon_item_edit_profile.imageset/icon_item_edit_profile.pdf deleted file mode 100644 index 2a12a1e..0000000 Binary files a/UpstraUIKit/UpstraUIKit/Assets.xcassets/icons/Community Settings/icon_item_edit_profile.imageset/icon_item_edit_profile.pdf and /dev/null differ diff --git a/UpstraUIKit/UpstraUIKit/Components/AmityExpandableLabel.swift b/UpstraUIKit/UpstraUIKit/Components/AmityExpandableLabel.swift index 8d157be..4103ecf 100644 --- a/UpstraUIKit/UpstraUIKit/Components/AmityExpandableLabel.swift +++ b/UpstraUIKit/UpstraUIKit/Components/AmityExpandableLabel.swift @@ -7,6 +7,7 @@ // import UIKit +import AmitySDK typealias LineIndexTuple = (line: CTLine, index: Int) @@ -595,6 +596,14 @@ extension UILabel { } extension AmityExpandableLabel { + + func setText(_ text: String, metadata: [String: Any], mentionees: [AmityMentionees]) { + let result = TextHighlighter.highlightLinksAndMentions(text: text, metadata: metadata, mentionees: mentionees) + + self.hyperLinks = result.hyperlinks + self.attributedText = result.text + } + func setText(_ text: String, withAttributes attributes: [MentionAttribute]) { let attributedString = NSMutableAttributedString(string: text) let detector = try! NSDataDetector(types: NSTextCheckingResult.CheckingType.link.rawValue) @@ -622,9 +631,3 @@ extension AmityExpandableLabel { self.attributedText = attributedString } } - -struct MentionAttribute { - let attributes: [NSAttributedString.Key: Any] - let range: NSRange - let userId: String -} diff --git a/UpstraUIKit/UpstraUIKit/Modules/Chat/Edit/AmityEditTextViewController.swift b/UpstraUIKit/UpstraUIKit/Modules/Chat/Edit/AmityEditTextViewController.swift index afac335..7e151d6 100644 --- a/UpstraUIKit/UpstraUIKit/Modules/Chat/Edit/AmityEditTextViewController.swift +++ b/UpstraUIKit/UpstraUIKit/Modules/Chat/Edit/AmityEditTextViewController.swift @@ -36,7 +36,7 @@ public class AmityEditTextViewController: AmityViewController { private let message: String var editHandler: ((String, [String: Any]?, AmityMentioneesBuilder?) -> Void)? var dismissHandler: (() -> Void)? - private let mentionManager: AmityMentionManager + private let mentionManager: ASCMentionManager private var metadata: [String: Any]? = nil // MARK: - View lifecycle @@ -47,11 +47,11 @@ public class AmityEditTextViewController: AmityViewController { self.editMode = editMode switch editMode { case .editMessage: - mentionManager = AmityMentionManager(withType: .message(channelId: nil)) + mentionManager = ASCMentionManager(withType: .message(channelId: nil)) case .create(let communityId, _): - mentionManager = AmityMentionManager(withType: .comment(communityId: communityId)) + mentionManager = ASCMentionManager(withType: .comment(communityId: communityId)) case .edit(let communityId, let metadata, _): - mentionManager = AmityMentionManager(withType: .comment(communityId: communityId)) + mentionManager = ASCMentionManager(withType: .comment(communityId: communityId)) self.metadata = metadata } super.init(nibName: AmityEditTextViewController.identifier, bundle: AmityUIKitManager.bundle) @@ -72,8 +72,7 @@ public class AmityEditTextViewController: AmityViewController { setupMentionTableView() mentionManager.delegate = self - mentionManager.setColor(AmityColorSet.base, highlightColor: AmityColorSet.primary) - mentionManager.setFont(AmityFontSet.body, highlightFont: AmityFontSet.bodyBold) + if let metadata = metadata { mentionManager.setMentions(metadata: metadata, inText: message) } @@ -138,22 +137,6 @@ public class AmityEditTextViewController: AmityViewController { strongSelf.editHandler?(strongSelf.textView.text ?? "", metadata, mentionees) } } - - private func showAlertForMaximumCharacters() { - var title = AmityLocalizedStringSet.postUnableToCommentTitle.localizedString - var message = AmityLocalizedStringSet.postUnableToCommentDescription.localizedString - switch editMode { - case .edit(_, _, let isReply), .create(_, let isReply): - title = isReply ? AmityLocalizedStringSet.postUnableToReplyTitle.localizedString : AmityLocalizedStringSet.postUnableToCommentTitle.localizedString - message = isReply ? AmityLocalizedStringSet.postUnableToReplyDescription.localizedString : AmityLocalizedStringSet.postUnableToCommentDescription.localizedString - default: - break - } - let alertController = UIAlertController(title: title, message: message, preferredStyle: .alert) - let cancelAction = UIAlertAction(title: AmityLocalizedStringSet.General.done.localizedString, style: .cancel, handler: nil) - alertController.addAction(cancelAction) - present(alertController, animated: true, completion: nil) - } } extension AmityEditTextViewController: AmityKeyboardServiceDelegate { @@ -183,30 +166,33 @@ extension AmityEditTextViewController: AmityTextViewDelegate { } public func textView(_ textView: AmityTextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool { - if textView.text.count > AmityMentionManager.maximumCharacterCountForPost { - showAlertForMaximumCharacters() + if textView.text.count > ASCMentionManager.maximumCharacterCountForPost { + didReachMaxCharacterCountLimit() return false } return mentionManager.shouldChangeTextIn(textView, inRange: range, replacementText: text, currentText: textView.text) } } -// MARK: - UITableViewDataSource -extension AmityEditTextViewController: UITableViewDataSource { +// MARK: - Mention TableView UITableViewDataSource & UITableViewDelegate +extension AmityEditTextViewController: UITableViewDataSource, UITableViewDelegate { + public func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - return mentionManager.users.count + return mentionManager.mentionProvider.mentionList.count } public func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + guard let cell = tableView.dequeueReusableCell(withIdentifier: AmityMentionTableViewCell.identifier) as? AmityMentionTableViewCell else { return UITableViewCell() } + + let provider = mentionManager.mentionProvider + if indexPath.row < provider.mentionList.count { + let model = provider.mentionList[indexPath.row] + cell.display(with: model) + } - guard let cell = tableView.dequeueReusableCell(withIdentifier: AmityMentionTableViewCell.identifier) as? AmityMentionTableViewCell, let model = mentionManager.item(at: indexPath) else { return UITableViewCell() } - cell.display(with: model) return cell } -} -// MARK: - UITableViewDelegate -extension AmityEditTextViewController: UITableViewDelegate { public func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { return AmityMentionTableViewCell.height } @@ -217,19 +203,15 @@ extension AmityEditTextViewController: UITableViewDelegate { public func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) { if tableView.isBottomReached { - mentionManager.loadMore() + mentionManager.mentionProvider.loadMore() } } } -// MARK: - AmityMentionManagerDelegate -extension AmityEditTextViewController: AmityMentionManagerDelegate { - public func didCreateAttributedString(attributedString: NSAttributedString) { - textView.attributedText = attributedString - textView.typingAttributes = [.font: AmityFontSet.body, .foregroundColor: AmityColorSet.base] - } +// MARK: - ASCMentionManagerDelegate +extension AmityEditTextViewController: ASCMentionManagerDelegate { - public func didGetUsers(users: [AmityMentionUserModel]) { + public func didUpdateMentionUsers(users: [AmityMentionUserModel]) { if users.isEmpty { mentionTableViewHeightConstraint.constant = 0 mentionTableView.isHidden = true @@ -244,14 +226,26 @@ extension AmityEditTextViewController: AmityMentionManagerDelegate { } } - public func didMentionsReachToMaximumLimit() { - let alertController = UIAlertController(title: AmityLocalizedStringSet.Mention.unableToMentionTitle.localizedString, message: AmityLocalizedStringSet.Mention.unableToMentionReplyDescription.localizedString, preferredStyle: .alert) - let cancelAction = UIAlertAction(title: AmityLocalizedStringSet.General.done.localizedString, style: .cancel, handler: nil) - alertController.addAction(cancelAction) - present(alertController, animated: true, completion: nil) + public func didReachMaxMentionLimit() { + AlertController.showAlert(in: self, title: AmityLocalizedStringSet.Mention.unableToMentionTitle.localizedString, message: AmityLocalizedStringSet.Mention.unableToMentionReplyDescription.localizedString) } - public func didCharactersReachToMaximumLimit() { - showAlertForMaximumCharacters() + public func didReachMaxCharacterCountLimit() { + var title = AmityLocalizedStringSet.postUnableToCommentTitle.localizedString + var message = AmityLocalizedStringSet.postUnableToCommentDescription.localizedString + switch editMode { + case .edit(_, _, let isReply), .create(_, let isReply): + title = isReply ? AmityLocalizedStringSet.postUnableToReplyTitle.localizedString : AmityLocalizedStringSet.postUnableToCommentTitle.localizedString + message = isReply ? AmityLocalizedStringSet.postUnableToReplyDescription.localizedString : AmityLocalizedStringSet.postUnableToCommentDescription.localizedString + default: + break + } + + AlertController.showAlert(in: self, title: title, message: message) + } + + public func didCreateAttributedString(attributedString: NSAttributedString) { + textView.attributedText = attributedString + textView.typingAttributes = [.font: AmityFontSet.body, .foregroundColor: AmityColorSet.base] } } diff --git a/UpstraUIKit/UpstraUIKit/Modules/Chat/Member/AmityChanneluserModeratorController.swift b/UpstraUIKit/UpstraUIKit/Modules/Chat/Member/AmityChanneluserModeratorController.swift index d2c4127..16c4c39 100644 --- a/UpstraUIKit/UpstraUIKit/Modules/Chat/Member/AmityChanneluserModeratorController.swift +++ b/UpstraUIKit/UpstraUIKit/Modules/Chat/Member/AmityChanneluserModeratorController.swift @@ -14,18 +14,19 @@ protocol AmityChannelUserRolesControllerProtocol { } final class AmityChannelUserRolesController: AmityChannelUserRolesControllerProtocol { - private var membershipParticipation: AmityChannelParticipation? + + private var membersRepo: AmityChannelMembership? private var membership: AmityChannelMember? private var token: AmityNotificationToken? init(channelId: String) { - membershipParticipation = AmityChannelParticipation(client: AmityUIKitManagerInternal.shared.client, andChannel: channelId) + membersRepo = AmityChannelMembership(client: AmityUIKitManagerInternal.shared.client, andChannel: channelId) } func getUserRoles(withUserId userId: String, role: AmityChannelRole, completionHandler: @escaping (Bool) -> ()) { token?.invalidate() completionHandler(false) - token = membershipParticipation?.getMembers(filter: .all, sortBy: .lastCreated, roles: []).observe({ [weak self] collection, change, error in + token = membersRepo?.getMembers(filter: .all, sortBy: .lastCreated, roles: [], includeDeleted: false).observe({ [weak self] collection, change, error in guard let weakSelf = self else { return } if error != nil { completionHandler(false) diff --git a/UpstraUIKit/UpstraUIKit/Modules/Chat/Member/General/AmityChannelFetchMemberController.swift b/UpstraUIKit/UpstraUIKit/Modules/Chat/Member/General/AmityChannelFetchMemberController.swift index 0ed5066..cc26a80 100644 --- a/UpstraUIKit/UpstraUIKit/Modules/Chat/Member/General/AmityChannelFetchMemberController.swift +++ b/UpstraUIKit/UpstraUIKit/Modules/Chat/Member/General/AmityChannelFetchMemberController.swift @@ -16,16 +16,16 @@ protocol AmityChannelFetchMemberControllerProtocol { final class AmityChannelFetchMemberController: AmityChannelFetchMemberControllerProtocol { - private var membershipParticipation: AmityChannelParticipation? + private var membership: AmityChannelMembership private var memberCollection: AmityCollection? private var memberToken: AmityNotificationToken? init(channelId: String) { - membershipParticipation = AmityChannelParticipation(client: AmityUIKitManagerInternal.shared.client, andChannel: channelId) + membership = AmityChannelMembership(client: AmityUIKitManagerInternal.shared.client, andChannel: channelId) } func fetch(roles: [String], _ completion: @escaping (Result<[AmityChannelMembershipModel], Error>) -> Void) { - memberCollection = membershipParticipation?.getMembers(filter: .all, sortBy: .lastCreated, roles: roles) + memberCollection = membership.getMembers(filter: .all, sortBy: .lastCreated, roles: roles, includeDeleted: false) memberToken?.invalidate() memberToken = memberCollection?.observe { (collection, change, error) in if let error = error { diff --git a/UpstraUIKit/UpstraUIKit/Modules/Comunity/Feed Posts/Contents/Cells/File/AmityPostFileTableViewCell.swift b/UpstraUIKit/UpstraUIKit/Modules/Comunity/Feed Posts/Contents/Cells/File/AmityPostFileTableViewCell.swift index 6d0d52c..80cbe73 100644 --- a/UpstraUIKit/UpstraUIKit/Modules/Comunity/Feed Posts/Contents/Cells/File/AmityPostFileTableViewCell.swift +++ b/UpstraUIKit/UpstraUIKit/Modules/Comunity/Feed Posts/Contents/Cells/File/AmityPostFileTableViewCell.swift @@ -43,8 +43,7 @@ public final class AmityPostFileTableViewCell: UITableViewCell, Nibbable, AmityP fileTableView.configure(files: post.files) if let metadata = post.metadata, let mentionees = post.mentionees { - let attributes = AmityMentionManager.getAttributes(fromText: post.text, withMetadata: metadata, mentionees: mentionees) - contentLabel.setText(post.text, withAttributes: attributes) + contentLabel.setText(post.text, metadata: metadata, mentionees: mentionees) } else { contentLabel.text = post.text } diff --git a/UpstraUIKit/UpstraUIKit/Modules/Comunity/Feed Posts/Contents/Cells/Gallery/AmityPostGalleryTableViewCell.swift b/UpstraUIKit/UpstraUIKit/Modules/Comunity/Feed Posts/Contents/Cells/Gallery/AmityPostGalleryTableViewCell.swift index b1e37eb..e2ade8f 100644 --- a/UpstraUIKit/UpstraUIKit/Modules/Comunity/Feed Posts/Contents/Cells/Gallery/AmityPostGalleryTableViewCell.swift +++ b/UpstraUIKit/UpstraUIKit/Modules/Comunity/Feed Posts/Contents/Cells/Gallery/AmityPostGalleryTableViewCell.swift @@ -46,8 +46,7 @@ public final class AmityPostGalleryTableViewCell: UITableViewCell, Nibbable, Ami galleryCollectionView.configure(medias: post.medias) if let metadata = post.metadata, let mentionees = post.mentionees { - let attributes = AmityMentionManager.getAttributes(fromText: post.text, withMetadata: metadata, mentionees: mentionees) - contentLabel.setText(post.text, withAttributes: attributes) + contentLabel.setText(post.text, metadata: metadata, mentionees: mentionees) } else { contentLabel.text = post.text } diff --git a/UpstraUIKit/UpstraUIKit/Modules/Comunity/Feed Posts/Contents/Cells/Poll/AmityPostPollTableViewCell.swift b/UpstraUIKit/UpstraUIKit/Modules/Comunity/Feed Posts/Contents/Cells/Poll/AmityPostPollTableViewCell.swift index 82ab569..4fb164c 100644 --- a/UpstraUIKit/UpstraUIKit/Modules/Comunity/Feed Posts/Contents/Cells/Poll/AmityPostPollTableViewCell.swift +++ b/UpstraUIKit/UpstraUIKit/Modules/Comunity/Feed Posts/Contents/Cells/Poll/AmityPostPollTableViewCell.swift @@ -56,8 +56,7 @@ final public class AmityPostPollTableViewCell: UITableViewCell, Nibbable, AmityP guard let poll = post.poll else { return } if let metadata = post.metadata, let mentionees = post.mentionees { - let attributes = AmityMentionManager.getAttributes(fromText: poll.question, withMetadata: metadata, mentionees: mentionees) - titlePollLabel.setText(poll.question, withAttributes: attributes) + titlePollLabel.setText(poll.question, metadata: metadata, mentionees: mentionees) } else { titlePollLabel.text = poll.question } diff --git a/UpstraUIKit/UpstraUIKit/Modules/Comunity/Feed Posts/Contents/Cells/Text/AmityPostTextTableViewCell.swift b/UpstraUIKit/UpstraUIKit/Modules/Comunity/Feed Posts/Contents/Cells/Text/AmityPostTextTableViewCell.swift index 0078519..cabdf98 100644 --- a/UpstraUIKit/UpstraUIKit/Modules/Comunity/Feed Posts/Contents/Cells/Text/AmityPostTextTableViewCell.swift +++ b/UpstraUIKit/UpstraUIKit/Modules/Comunity/Feed Posts/Contents/Cells/Text/AmityPostTextTableViewCell.swift @@ -46,9 +46,7 @@ public final class AmityPostTextTableViewCell: UITableViewCell, Nibbable, AmityP // We picky back to render title/description for live stream post here. // By getting post.liveStream if let metadata = post.metadata, let mentionees = post.mentionees { - let attributes = AmityMentionManager.getAttributes(fromText: post.text, withMetadata: metadata, mentionees: mentionees) - - contentLabel.setText(post.text, withAttributes: attributes) + contentLabel.setText(post.text, metadata: metadata, mentionees: mentionees) } else { contentLabel.text = post.text } @@ -56,8 +54,7 @@ public final class AmityPostTextTableViewCell: UITableViewCell, Nibbable, AmityP // The default render behaviour just to grab text from post.text if let metadata = post.metadata, let mentionees = post.mentionees { - let attributes = AmityMentionManager.getAttributes(fromText: post.text, withMetadata: metadata, mentionees: mentionees) - contentLabel.setText(post.text, withAttributes: attributes) + contentLabel.setText(post.text, metadata: metadata, mentionees: mentionees) } else { contentLabel.text = post.text } diff --git a/UpstraUIKit/UpstraUIKit/Modules/Comunity/Feed Posts/Scenes/Post Detail/ViewController/AmityPostDetailViewController.swift b/UpstraUIKit/UpstraUIKit/Modules/Comunity/Feed Posts/Scenes/Post Detail/ViewController/AmityPostDetailViewController.swift index f9988a7..9dd8d53 100644 --- a/UpstraUIKit/UpstraUIKit/Modules/Comunity/Feed Posts/Scenes/Post Detail/ViewController/AmityPostDetailViewController.swift +++ b/UpstraUIKit/UpstraUIKit/Modules/Comunity/Feed Posts/Scenes/Post Detail/ViewController/AmityPostDetailViewController.swift @@ -32,7 +32,7 @@ open class AmityPostDetailViewController: AmityViewController { private var referenceId: String? private var expandedIds: Set = [] private var showReplyIds: [String] = [] - private var mentionManager: AmityMentionManager? + private var mentionManager: ASCMentionManager? private var parentComment: AmityCommentModel? { didSet { @@ -79,8 +79,6 @@ open class AmityPostDetailViewController: AmityViewController { navigationController?.setBackgroundColor(with: .white) AmityKeyboardService.shared.delegate = self mentionManager?.delegate = self - mentionManager?.setColor(AmityColorSet.base, highlightColor: AmityColorSet.primary) - mentionManager?.setFont(AmityFontSet.body, highlightFont: AmityFontSet.bodyBold) } public override func viewWillDisappear(_ animated: Bool) { @@ -216,21 +214,12 @@ open class AmityPostDetailViewController: AmityViewController { } - private func showAlertForMaximumCharacters() { - let title = parentComment == nil ? AmityLocalizedStringSet.postUnableToCommentTitle.localizedString : AmityLocalizedStringSet.postUnableToReplyTitle.localizedString - let message = parentComment == nil ? AmityLocalizedStringSet.postUnableToCommentDescription.localizedString : AmityLocalizedStringSet.postUnableToReplyDescription.localizedString - let alertController = UIAlertController(title: title, message: message, preferredStyle: .alert) - let cancelAction = UIAlertAction(title: AmityLocalizedStringSet.General.done.localizedString, style: .cancel, handler: nil) - alertController.addAction(cancelAction) - present(alertController, animated: true, completion: nil) - } - private func setupMentionManager() { if mentionManager != nil { return } let community = screenViewModel.community let isPublic = community?.isPublic ?? false let communityId: String? = isPublic ? nil : community?.communityId - mentionManager = AmityMentionManager(withType: .comment(communityId: communityId)) + mentionManager = ASCMentionManager(withType: .comment(communityId: communityId)) mentionManager?.delegate = self } @@ -628,8 +617,8 @@ extension AmityPostDetailViewController: AmityPostDetailCompostViewDelegate { } func composeView(_ view: AmityPostDetailCompostView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool { - if view.textView.text.count > AmityMentionManager.maximumCharacterCountForPost { - showAlertForMaximumCharacters() + if view.textView.text.count > ASCMentionManager.maximumCharacterCountForPost { + didReachMaxCharacterCountLimit() return false } return mentionManager?.shouldChangeTextIn(view.textView, inRange: range, replacementText: text, currentText: view.textView.text) ?? true @@ -832,22 +821,24 @@ extension AmityPostDetailViewController: AmityCommentTableViewCellDelegate { } } -// MARK: - UITableViewDataSource -extension AmityPostDetailViewController: UITableViewDataSource { +// MARK: - Mention TableView DataSource & Delegate +extension AmityPostDetailViewController: UITableViewDataSource, UITableViewDelegate { + public func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - return mentionManager?.users.count ?? 0 + return mentionManager?.mentionProvider.mentionList.count ?? 0 } public func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + guard let cell = tableView.dequeueReusableCell(withIdentifier: AmityMentionTableViewCell.identifier) as? AmityMentionTableViewCell else { return UITableViewCell() } + + if let provider = mentionManager?.mentionProvider, indexPath.row < provider.mentionList.count { + let model = provider.mentionList[indexPath.row] + cell.display(with: model) + } - guard let cell = tableView.dequeueReusableCell(withIdentifier: AmityMentionTableViewCell.identifier) as? AmityMentionTableViewCell, let model = mentionManager?.item(at: indexPath) else { return UITableViewCell() } - cell.display(with: model) return cell } -} -// MARK: - UITableViewDelegate -extension AmityPostDetailViewController: UITableViewDelegate { public func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { return AmityMentionTableViewCell.height } @@ -857,25 +848,35 @@ extension AmityPostDetailViewController: UITableViewDelegate { } public func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) { - if indexPath.row == (mentionManager?.users.count ?? 0) - 4 { - mentionManager?.loadMore() + let usersCount = mentionManager?.mentionProvider.mentionList.count ?? 0 + if indexPath.row == usersCount - 4 { + mentionManager?.mentionProvider.loadMore() } } } -// MARK: - AmityMentionManagerDelegate -extension AmityPostDetailViewController: AmityMentionManagerDelegate { - public func didCreateAttributedString(attributedString: NSAttributedString) { - commentComposeBarView.textView.attributedText = attributedString - commentComposeBarView.textView.typingAttributes = [.font: AmityFontSet.body, .foregroundColor: AmityColorSet.base] +extension AmityPostDetailViewController: ASCMentionManagerDelegate { + + public func didReachMaxMentionLimit() { + let title = AmityLocalizedStringSet.Mention.unableToMentionTitle.localizedString + let message = parentComment == nil ? AmityLocalizedStringSet.Mention.unableToMentionCommentDescription.localizedString : AmityLocalizedStringSet.Mention.unableToMentionReplyDescription.localizedString + + AlertController.showAlert(in: self, title: title, message: message) + } + + public func didReachMaxCharacterCountLimit() { + let title = parentComment == nil ? AmityLocalizedStringSet.postUnableToCommentTitle.localizedString : AmityLocalizedStringSet.postUnableToReplyTitle.localizedString + let message = parentComment == nil ? AmityLocalizedStringSet.postUnableToCommentDescription.localizedString : AmityLocalizedStringSet.postUnableToReplyDescription.localizedString + + AlertController.showAlert(in: self, title: title, message: message) } - public func didGetUsers(users: [AmityMentionUserModel]) { + public func didUpdateMentionUsers(users: [AmityMentionUserModel]) { if users.isEmpty { mentionTableViewHeightConstraint.constant = 0 mentionTableView.isHidden = true } else { - var heightConstant:CGFloat = 240.0 + var heightConstant: CGFloat = 240.0 if users.count < 5 { heightConstant = CGFloat(users.count) * 52.0 } @@ -885,15 +886,8 @@ extension AmityPostDetailViewController: AmityMentionManagerDelegate { } } - public func didMentionsReachToMaximumLimit() { - let message = parentComment == nil ? AmityLocalizedStringSet.Mention.unableToMentionCommentDescription.localizedString : AmityLocalizedStringSet.Mention.unableToMentionReplyDescription.localizedString - let alertController = UIAlertController(title: AmityLocalizedStringSet.Mention.unableToMentionTitle.localizedString, message: message, preferredStyle: .alert) - let cancelAction = UIAlertAction(title: AmityLocalizedStringSet.General.done.localizedString, style: .cancel, handler: nil) - alertController.addAction(cancelAction) - present(alertController, animated: true, completion: nil) - } - - public func didCharactersReachToMaximumLimit() { - showAlertForMaximumCharacters() + public func didCreateAttributedString(attributedString: NSAttributedString) { + commentComposeBarView.textView.attributedText = attributedString + commentComposeBarView.textView.typingAttributes = [.font: AmityFontSet.body, .foregroundColor: AmityColorSet.base] } } diff --git a/UpstraUIKit/UpstraUIKit/Modules/Comunity/General/Base Classes/Post Text Editor/AmityPostTextEditorViewController.swift b/UpstraUIKit/UpstraUIKit/Modules/Comunity/General/Base Classes/Post Text Editor/AmityPostTextEditorViewController.swift index 015b3f9..d73d492 100644 --- a/UpstraUIKit/UpstraUIKit/Modules/Comunity/General/Base Classes/Post Text Editor/AmityPostTextEditorViewController.swift +++ b/UpstraUIKit/UpstraUIKit/Modules/Comunity/General/Base Classes/Post Text Editor/AmityPostTextEditorViewController.swift @@ -56,7 +56,7 @@ public class AmityPostTextEditorViewController: AmityViewController { private var filePicker: AmityFilePicker! private var mentionTableView: AmityMentionTableView private var mentionTableViewHeightConstraint: NSLayoutConstraint! - private var mentionManager: AmityMentionManager? + private var mentionManager: ASCMentionManager? private var isValueChanged: Bool { return !textView.text.isEmpty || !galleryView.medias.isEmpty || !fileView.files.isEmpty @@ -85,7 +85,7 @@ public class AmityPostTextEditorViewController: AmityViewController { communityId = community.isPublic ? nil : community.communityId default: break } - mentionManager = AmityMentionManager(withType: .post(communityId: communityId)) + mentionManager = ASCMentionManager(withType: .post(communityId: communityId)) } super.init(nibName: nil, bundle: nil) @@ -401,7 +401,6 @@ public class AmityPostTextEditorViewController: AmityViewController { self?.showUploadFailureAlert() } } - } private func uploadVideos() { @@ -668,8 +667,8 @@ extension AmityPostTextEditorViewController: AmityTextViewDelegate { } public func textView(_ textView: AmityTextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool { - if textView.text.count > AmityMentionManager.maximumCharacterCountForPost { - showAlertForMaximumCharacters() + if textView.text.count > ASCMentionManager.maximumCharacterCountForPost { + didReachMaxCharacterCountLimit() return false } return mentionManager?.shouldChangeTextIn(textView, inRange: range, replacementText: text, currentText: textView.text) ?? true @@ -903,13 +902,6 @@ extension AmityPostTextEditorViewController: AmityPostTextEditorMenuViewDelegate } updateConstraints() } - - private func showAlertForMaximumCharacters() { - let alertController = UIAlertController(title: AmityLocalizedStringSet.postUnableToPostTitle.localizedString, message: AmityLocalizedStringSet.postUnableToPostDescription.localizedString, preferredStyle: .alert) - let cancelAction = UIAlertAction(title: AmityLocalizedStringSet.General.done.localizedString, style: .cancel, handler: nil) - alertController.addAction(cancelAction) - present(alertController, animated: true, completion: nil) - } } extension AmityPostTextEditorViewController: UIImagePickerControllerDelegate, UINavigationControllerDelegate { @@ -968,7 +960,10 @@ extension AmityPostTextEditorViewController: UIImagePickerControllerDelegate, UI extension AmityPostTextEditorViewController { public override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool { - guard isValueChanged, !(mentionManager?.isSearchingStarted ?? false) else { + // Note: + // I'm not sure why we are checking if mention search is started or not before showing a popup. Removing this line for now + // isValueChanged, !(mentionManager?.isSearchingStarted ?? false) + guard isValueChanged else { return super.gestureRecognizerShouldBegin(gestureRecognizer) } @@ -994,7 +989,8 @@ extension AmityPostTextEditorViewController { } // MARK: - UITableViewDelegate -extension AmityPostTextEditorViewController: UITableViewDelegate { +extension AmityPostTextEditorViewController: UITableViewDelegate, UITableViewDataSource { + public func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { return AmityMentionTableViewCell.height } @@ -1004,33 +1000,32 @@ extension AmityPostTextEditorViewController: UITableViewDelegate { } public func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) { - if indexPath.row == (mentionManager?.users.count ?? 0) - 4 { - mentionManager?.loadMore() + let usersCount = mentionManager?.mentionProvider.mentionList.count ?? 0 + if indexPath.row == usersCount - 4 { + mentionManager?.mentionProvider.loadMore() } } -} - -// MARK: - UITableViewDataSource -extension AmityPostTextEditorViewController: UITableViewDataSource { + public func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - return mentionManager?.users.count ?? 0 + return mentionManager?.mentionProvider.mentionList.count ?? 0 } public func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - guard let cell = tableView.dequeueReusableCell(withIdentifier: AmityMentionTableViewCell.identifier) as? AmityMentionTableViewCell, let model = mentionManager?.item(at: indexPath) else { return UITableViewCell() } - cell.display(with: model) + guard let cell = tableView.dequeueReusableCell(withIdentifier: AmityMentionTableViewCell.identifier) as? AmityMentionTableViewCell else { return UITableViewCell() } + + if let provider = mentionManager?.mentionProvider, indexPath.row < provider.mentionList.count { + let model = provider.mentionList[indexPath.row] + cell.display(with: model) + } + return cell } } -// MARK: - AmityMentionManagerDelegate -extension AmityPostTextEditorViewController: AmityMentionManagerDelegate { - public func didCreateAttributedString(attributedString: NSAttributedString) { - textView.attributedText = attributedString - textView.typingAttributes = [.font: AmityFontSet.body, .foregroundColor: AmityColorSet.base] - } +// MARK: - ASCMentionManagerDelegate +extension AmityPostTextEditorViewController: ASCMentionManagerDelegate { - public func didGetUsers(users: [AmityMentionUserModel]) { + public func didUpdateMentionUsers(users: [AmityMentionUserModel]) { if users.isEmpty { mentionTableViewHeightConstraint.constant = 0 mentionTableView.isHidden = true @@ -1045,27 +1040,28 @@ extension AmityPostTextEditorViewController: AmityMentionManagerDelegate { } } - public func didMentionsReachToMaximumLimit() { - let alertController = UIAlertController(title: AmityLocalizedStringSet.Mention.unableToMentionTitle.localizedString, message: AmityLocalizedStringSet.Mention.unableToMentionPostDescription.localizedString, preferredStyle: .alert) - let cancelAction = UIAlertAction(title: AmityLocalizedStringSet.General.done.localizedString, style: .cancel, handler: nil) - alertController.addAction(cancelAction) - present(alertController, animated: true, completion: nil) + public func didReachMaxMentionLimit() { + AlertController.showAlert(in: self, title: AmityLocalizedStringSet.Mention.unableToMentionTitle.localizedString, message: AmityLocalizedStringSet.Mention.unableToMentionPostDescription.localizedString) } - public func didCharactersReachToMaximumLimit() { - showAlertForMaximumCharacters() + public func didReachMaxCharacterCountLimit() { + AlertController.showAlert(in: self, title: AmityLocalizedStringSet.postUnableToPostTitle.localizedString, message: AmityLocalizedStringSet.postUnableToPostDescription.localizedString) + } + + public func didCreateAttributedString(attributedString: NSAttributedString) { + textView.attributedText = attributedString + textView.typingAttributes = [.font: AmityFontSet.body, .foregroundColor: AmityColorSet.base] } } // MARK: - Private methods private extension AmityPostTextEditorViewController { + func setupMentionManager(withPost post: AmityPostModel) { guard mentionManager == nil else { return } let communityId: String? = (currentPost?.targetCommunity?.isPublic ?? true) ? nil : currentPost?.targetCommunity?.communityId - mentionManager = AmityMentionManager(withType: .post(communityId: communityId)) + mentionManager = ASCMentionManager(withType: .post(communityId: communityId)) mentionManager?.delegate = self - mentionManager?.setColor(AmityColorSet.base, highlightColor: AmityColorSet.primary) - mentionManager?.setFont(AmityFontSet.body, highlightFont: AmityFontSet.bodyBold) if let metadata = post.metadata { mentionManager?.setMentions(metadata: metadata, inText: post.text) diff --git a/UpstraUIKit/UpstraUIKit/Modules/Comunity/General/Cell/AmityCommentView/AmityCommentView.swift b/UpstraUIKit/UpstraUIKit/Modules/Comunity/General/Cell/AmityCommentView/AmityCommentView.swift index cd28e10..1bd1c11 100644 --- a/UpstraUIKit/UpstraUIKit/Modules/Comunity/General/Cell/AmityCommentView/AmityCommentView.swift +++ b/UpstraUIKit/UpstraUIKit/Modules/Comunity/General/Cell/AmityCommentView/AmityCommentView.swift @@ -144,8 +144,7 @@ class AmityCommentView: AmityView { } if let metadata = comment.metadata, let mentionees = comment.mentionees { - let attributes = AmityMentionManager.getAttributes(fromText: comment.text, withMetadata: metadata, mentionees: mentionees) - contentLabel.setText(comment.text, withAttributes: attributes) + contentLabel.setText(comment.text, metadata: metadata, mentionees: mentionees) } else { contentLabel.text = comment.text } diff --git a/UpstraUIKit/UpstraUIKit/Modules/Comunity/General/Controllers/AmityCommunityFetchMemberController.swift b/UpstraUIKit/UpstraUIKit/Modules/Comunity/General/Controllers/AmityCommunityFetchMemberController.swift index 85f8b09..e8468b5 100644 --- a/UpstraUIKit/UpstraUIKit/Modules/Comunity/General/Controllers/AmityCommunityFetchMemberController.swift +++ b/UpstraUIKit/UpstraUIKit/Modules/Comunity/General/Controllers/AmityCommunityFetchMemberController.swift @@ -16,16 +16,16 @@ protocol AmityCommunityFetchMemberControllerProtocol { final class AmityCommunityFetchMemberController: AmityCommunityFetchMemberControllerProtocol { - private var membershipParticipation: AmityCommunityParticipation? + private var membership: AmityCommunityMembership? private var memberCollection: AmityCollection? private var memberToken: AmityNotificationToken? init(communityId: String) { - membershipParticipation = AmityCommunityParticipation(client: AmityUIKitManagerInternal.shared.client, andCommunityId: communityId) + membership = AmityCommunityMembership(client: AmityUIKitManagerInternal.shared.client, andCommunityId: communityId) } func fetch(roles: [String], _ completion: @escaping (Result<[AmityCommunityMembershipModel], Error>) -> Void) { - memberCollection = membershipParticipation?.getMembers(membershipOptions: [.member], roles: roles, sortBy: .lastCreated) + memberCollection = membership?.getMembers(filter: .member, roles: roles, sortBy: .lastCreated, includeDeleted: false) memberToken = memberCollection?.observe { (collection, change, error) in if let error = error { completion(.failure(error)) diff --git a/UpstraUIKit/UpstraUIKit/Modules/Comunity/Mention/Manager/ASCMentionManager.swift b/UpstraUIKit/UpstraUIKit/Modules/Comunity/Mention/Manager/ASCMentionManager.swift new file mode 100644 index 0000000..b2af29c --- /dev/null +++ b/UpstraUIKit/UpstraUIKit/Modules/Comunity/Mention/Manager/ASCMentionManager.swift @@ -0,0 +1,214 @@ +// +// ASCMentionManager.swift +// AmityUIKit +// +// Created by Nishan on 8/7/2567 BE. +// Copyright © 2567 BE Amity. All rights reserved. +// + +import UIKit +import AmitySDK + +struct MentionAttribute { + let attributes: [NSAttributedString.Key: Any] + let range: NSRange + let userId: String +} + +public enum AmityMentionManagerType { + case post(communityId: String?) + case comment(communityId: String?) + case message(channelId: String?) +} + +public struct AmityMentionUserModel { + + let userId: String + let displayName: String + let avatarURL: String + let isGlobalBan: Bool + let isChannelMention: Bool + + // We don't have mention in chat in uikit v3. + var type: AmityMessageMentionType { + return isChannelMention ? .channel : .user + } + + init(user: AmityUser) { + self.userId = user.userId + self.displayName = user.displayName ?? AmityLocalizedStringSet.General.anonymous.localizedString + self.avatarURL = user.getAvatarInfo()?.fileURL ?? "" + self.isGlobalBan = user.isGlobalBanned + self.isChannelMention = false + } + + internal init(userId: String, displayName: String, avatarURL: String, isGlobalBan: Bool, isChannelMention: Bool) { + self.userId = userId + self.displayName = displayName + self.avatarURL = avatarURL + self.isGlobalBan = isGlobalBan + self.isChannelMention = isChannelMention + } + + static let channelMention = AmityMentionUserModel(userId: "", displayName: "All", avatarURL: "", isGlobalBan: false, isChannelMention: true) +} + +public protocol ASCMentionManagerDelegate: AnyObject { + func didUpdateMentionUsers(users: [AmityMentionUserModel]) + func didCreateAttributedString(attributedString: NSAttributedString) + func didReachMaxMentionLimit() + func didReachMaxCharacterCountLimit() +} + +/// ASCMentionManager works in combination with: +/// +/// - MentionEditor: Handle editing of mention text in textview +/// - MentionListProvider: Handle query of mention users & return it. +public final class ASCMentionManager: MentionTextEditorDelegate { + + public static let maximumCharacterCountForPost = 50000 + public static let maximumMentionsCount = 30 + + // Properties + private let type: AmityMentionManagerType + + // Default Attributes used to highlight mentions + public var highlightAttributes: [NSAttributedString.Key: Any] = [.font: AmityFontSet.bodyBold, .foregroundColor: AmityColorSet.primary] { + didSet { + mentionEditor.highlightAttributes = highlightAttributes + } + } + + // Default Attributes used for text while typing + public var typingAttributes: [NSAttributedString.Key: Any] = [.font: AmityFontSet.body, .foregroundColor: AmityColorSet.base] { + didSet { + mentionEditor.typingAttributes = typingAttributes + } + } + + public weak var delegate: ASCMentionManagerDelegate? + + // Provides list of users for mention + public let mentionProvider: MentionListProvider + let mentionEditor: MentionTextEditor + + public init(withType type: AmityMentionManagerType) { + self.mentionEditor = MentionTextEditor() + self.type = type + self.mentionProvider = MentionListProvider(type: type) + self.mentionProvider.didGetMentionList = { [weak self] users in + guard let self else { return } + + switch mentionEditor.mentionState { + case .search: + self.delegate?.didUpdateMentionUsers(users: users) + case .idle: + break + } + } + self.mentionEditor.delegate = self + } + + // MARK: Delegate MentionTextEditorDelegate + + func didChangeMentionState(state: MentionTextEditor.MentionState) { + switch state { + case .idle: + mentionProvider.mentionList = [] + delegate?.didUpdateMentionUsers(users: []) + case .search(let key): + mentionProvider.searchUser(text: key) + } + } + + func didUpdateAttributedText(text: NSAttributedString) { + delegate?.didCreateAttributedString(attributedString: text) + + if text.string.count > ASCMentionManager.maximumCharacterCountForPost { + delegate?.didReachMaxCharacterCountLimit() + } + } + + public func changeSelection(_ textInput: UITextInput) { + mentionEditor.changeSelection(textInput) + } + + public func shouldChangeTextIn(_ textInput: UITextInput, inRange range: NSRange, replacementText: String, currentText text: String) -> Bool { + return mentionEditor.processUserInput(in: textInput, range: range, replacementText: replacementText, currentText: text) + } + + public func addMention(from textInput: UITextInput, in text: String, at indexPath: IndexPath) { + if mentionEditor.mentions.count >= ASCMentionManager.maximumMentionsCount { + delegate?.didReachMaxMentionLimit() + return + } + + guard indexPath.row < mentionProvider.mentionList.count else { return } + let member = mentionProvider.mentionList[indexPath.row] + + mentionEditor.addMention(member: member, textInput: textInput, currentText: text) + } + + public func isMentionWithinLimit(limit: Int) -> Bool { + return mentionEditor.mentions.count < limit + } + + public func addMention(from textInput: UITextInput, in text: String, member: AmityMentionUserModel) { + if mentionEditor.mentions.count >= ASCMentionManager.maximumMentionsCount { + delegate?.didReachMaxMentionLimit() + return + } + + mentionEditor.addMention(member: member, textInput: textInput, currentText: text) + } + + public func setMentions(metadata: [String: Any], inText text: String) { + self.mentionEditor.setMentions(metadata: metadata, inText: text) + } + + public func getMetadata(shift: Int = 0) -> [String: Any]? { + if mentionEditor.mentions.isEmpty { + return nil + } + + let finalMentions = mentionEditor.mentions + + if shift != 0 { + for i in 0.. AmityMentioneesBuilder? { + if mentionEditor.mentions.isEmpty { + return nil + } + + let mentionees: AmityMentioneesBuilder = AmityMentioneesBuilder() + + let userIds = mentionEditor.mentions.filter{ $0.type == .user }.compactMap { $0.userId } + if !userIds.isEmpty { + mentionees.mentionUsers(userIds: userIds) + } + + switch type { + case .message: + if mentionEditor.mentions.contains(where: { $0.type == .channel }) { + mentionees.mentionChannel() + } + default: + break + } + + return mentionees + } + + // Reset everything to state before mention. + public func resetState() { + mentionEditor.reset() + mentionProvider.reset() + } +} diff --git a/UpstraUIKit/UpstraUIKit/Modules/Comunity/Mention/Manager/AmityMentionListProvider.swift b/UpstraUIKit/UpstraUIKit/Modules/Comunity/Mention/Manager/AmityMentionListProvider.swift new file mode 100644 index 0000000..09074c5 --- /dev/null +++ b/UpstraUIKit/UpstraUIKit/Modules/Comunity/Mention/Manager/AmityMentionListProvider.swift @@ -0,0 +1,208 @@ +// +// AmityMentionListProvider.swift +// AmityUIKit +// +// Created by Nishan on 8/7/2567 BE. +// Copyright © 2567 BE Amity. All rights reserved. +// + +import Foundation +import AmitySDK + +// Work In Progress: Decoupled class which provides list of users to mention from live collection. +public class MentionListProvider { + + // Mention Configuration + // We can use this to determine if mention @all is enabled or not. + private var mentionConfiguration: AmityMentionConfigurations? = AmityUIKitManagerInternal.shared.client.mentionConfigurations + private var mentionType: AmityMentionManagerType + private var canMentionAll = false // Mention all members in a channel + + // Repositories + private var userRepository: AmityUserRepository = AmityUserRepository(client: AmityUIKitManagerInternal.shared.client) + private var channelMembersRepo: AmityChannelMembership? + private var communityMembersRepo: AmityCommunityMembership? + private var communityRepository: AmityCommunityRepository = AmityCommunityRepository(client: AmityUIKitManagerInternal.shared.client) + + // Collection + private var channelMembersCollection: AmityCollection? // Channel Members + private var usersCollection: AmityCollection? // All Users + private var communityMembersCollection: AmityCollection? // Private community members + + // Token + private var mentionListToken: AmityNotificationToken? + private var communityToken: AmityNotificationToken? + + // Models + private var community: AmityCommunityModel? + + // If sdk is searching for provided display name + public var mentionList: [AmityMentionUserModel] = [] + + // Callback + public var didGetMentionList: (([AmityMentionUserModel]) -> Void)? + + public init(type: AmityMentionManagerType) { + self.mentionType = type + let client = AmityUIKitManagerInternal.shared.client + + switch type { + case .post(let communityId), .comment(let communityId): + if let communityId { + setupCommunity(withId: communityId) + } + case .message(let subChannelId): + if let channelId = subChannelId { + channelMembersRepo = AmityChannelMembership(client: client, andChannel: channelId) + } + } + +// self.checkMentionPermission() + } + +// func checkMentionPermission() { +// if case let .message(subChannelId) = mentionType { +// ChatPermissionChecker.hasModeratorPermission(for: subChannelId ?? "") { hasPermission in +// self.canMentionAll = hasPermission +// } +// } +// } + + public func searchUser(text: String) { + switch mentionType { + case .post(let communityId), .comment(let communityId): + if let communityId, !(community?.isPublic ?? true) { + // Search for members in that community + searchCommunityMembers(with: text, communityId: communityId) + return + } + + // Search for users + searchUsers(with: text) + case .message: + searchChannelMembers(with: text) + } + } + + public func loadMore() { + switch mentionType { + case .post(let communityId), .comment(let communityId): + if let communityId, !(community?.isPublic ?? true) { + // load more members in that community + if let communityMembersCollection, communityMembersCollection.hasNext { + communityMembersCollection.nextPage() + } + return + } + + // load more users + if let usersCollection, usersCollection.hasNext { + usersCollection.nextPage() + } + case .message: + if let channelMembersCollection, channelMembersCollection.hasNext { + channelMembersCollection.nextPage() + } + } + } + + // Equivalent to reset state() + public func reset() { + mentionList = [] + + mentionListToken?.invalidate() + mentionListToken = nil + + channelMembersCollection = nil + usersCollection = nil + communityMembersCollection = nil + } + + private func setupCommunity(withId communityId: String) { + communityMembersRepo = AmityCommunityMembership(client: AmityUIKitManager.client, andCommunityId: communityId) + communityToken = communityRepository.getCommunity(withId: communityId).observe { [weak self] liveObject, error in + if liveObject.dataStatus == .fresh { + self?.communityToken?.invalidate() + } + + guard let community = liveObject.snapshot else { return } + self?.community = AmityCommunityModel(object: community) + } + } + + private func searchChannelMembers(with displayName: String) { + let builder = AmityChannelMembershipFilterBuilder() + builder.add(filter: .member) + builder.add(filter: .mute) + + // Invalidate existing token + mentionListToken = nil + mentionListToken?.invalidate() + + channelMembersCollection = channelMembersRepo?.searchMembers(displayName: displayName, filterBuilder: builder, roles: [], includeDeleted: false) + mentionListToken = channelMembersCollection?.observe({ [weak self] liveCollection, _, error in + self?.handleSearchResponse(with: liveCollection) + }) + } + + private func searchUsers(with displayName: String) { + mentionListToken = nil + mentionListToken?.invalidate() + + usersCollection = userRepository.searchUsers(displayName, sortBy: .displayName) + mentionListToken = usersCollection?.observe { [weak self] liveCollection, _, error in + self?.handleSearchResponse(with: liveCollection) + } + } + + private func searchCommunityMembers(with displayName: String, communityId: String) { + mentionListToken = nil + mentionListToken?.invalidate() + + communityMembersCollection = communityMembersRepo?.searchMembers(keyword: displayName, filter: [.member], roles: [], sortBy: .lastCreated, includeDeleted: false) + mentionListToken = communityMembersCollection?.observe { [weak self] liveCollection, _, error in + self?.handleSearchResponse(with: liveCollection) + } + } + + private func handleSearchResponse(with collection: AmityCollection) { + switch collection.dataStatus { + case .fresh: + var updatedList = [AmityMentionUserModel]() + + for i in 0.. [MentionAttribute] { var attributes = [MentionAttribute]() diff --git a/UpstraUIKit/UpstraUIKit/Modules/Comunity/Mention/Manager/AmityMentionTextEditor.swift b/UpstraUIKit/UpstraUIKit/Modules/Comunity/Mention/Manager/AmityMentionTextEditor.swift new file mode 100644 index 0000000..ba5219c --- /dev/null +++ b/UpstraUIKit/UpstraUIKit/Modules/Comunity/Mention/Manager/AmityMentionTextEditor.swift @@ -0,0 +1,335 @@ +// +// AmityMentionTextEditor.swift +// AmityUIKit +// +// Created by Nishan on 8/7/2567 BE. +// Copyright © 2567 BE Amity. All rights reserved. +// +import Foundation +import UIKit +import AmitySDK + +protocol MentionTextEditorDelegate: AnyObject { + + func didChangeMentionState(state: MentionTextEditor.MentionState) + func didUpdateAttributedText(text: NSAttributedString) +} + +extension AmityMention: CustomStringConvertible { + + public var description: String { + return "Mention: \(self.userId ?? "") | Index: \(self.index) | Length: \(self.length)" + } +} + +/// Highlights mentions in Text Editor +class MentionTextEditor { + + enum MentionState { + case idle // Mention + case search(key: String) // Mention Started + } + + private let mentionTrigger = "@" + private var mentionSearchKey = "" // This is used for searching in sdk. It does not include initial "@" character. + private var mentionRange = NSRange.init() // Range where mention search gets triggered. This includes initial "@" character. + + private(set) var mentionState = MentionState.idle + private(set) var mentions: [AmityMention] = [] // Mentions which are added to the text + + weak var delegate: MentionTextEditorDelegate? + + // Attributes used to highlight mentions + var highlightAttributes: [NSAttributedString.Key: Any] = [ + .font: UIFont.systemFont(ofSize: 15, weight: .bold), + .foregroundColor: UIColor.systemBlue] + + // Attributes used for non-highlighted text + var typingAttributes: [NSAttributedString.Key: Any] = [ + .font: UIFont.systemFont(ofSize: 15), + .foregroundColor: UIColor(hex: "#000000")] + + func processUserInput(in textInput: UITextInput, range: NSRange, replacementText: String, currentText: String) -> Bool { + + // Mention Trigger + if replacementText == mentionTrigger { + + // If a new string is added before existing mentions, update index of those mentions. + for (index, mention) in self.mentions.enumerated() where range.location < mention.index { + mentions[index].index += replacementText.count + } + + // Check if "@" is entered as independent character. + if shouldHandleMentionTrigger(in: textInput, range: range, replacementText: replacementText, currentText: currentText) { + mentionSearchKey = "" + self.updateMentionState(state: .search(key: mentionSearchKey)) + + // If mention range is not initialized yet, initialize it + if mentionRange.length == 0 { + mentionRange = NSRange(location: range.location, length: 1) + } + } else { + // If input is something@ or @something or @@, mention is dismissed. + mentionSearchKey = "" + self.updateMentionState(state: .idle) + } + + } else { + + if replacementText == "" { + + switch mentionState { + case .idle: + // When removing text, we need to remove mention text as a whole. First we check if the text that we + // want to remove contains mention. + if let foundRange = findMentionWithinRange(range: range) { + if foundRange == range { + // Remove mentions from array too + removeMentionWithinRange(range: range) + + let deleteLength = range.length + 1 + for (index, mention) in self.mentions.enumerated() where range.location <= mention.index { + mentions[index].index -= deleteLength // There is a space after every mention, range.length + 1 to count total length included the space + } + } else { + // We don't allow deleting individual character of mention, so we highlight the whole mention text if user + // tries to delete individual character. + if let startPosition = textInput.position(from: textInput.beginningOfDocument, offset: foundRange.location), + let endPosition = textInput.position(from: startPosition, offset: foundRange.length) { + + textInput.selectedTextRange = textInput.textRange(from: startPosition, to: endPosition) + return false + } + } + } else { + // Didn't find any mention. Update mention ranges if necessary + var deleteLength = range.length + deleteLength = deleteLength > 1 ? deleteLength + 1 : deleteLength // Selecting word seems to delete space too. + + for (index, mention) in self.mentions.enumerated() where range.location <= mention.index { + mentions[index].index -= deleteLength + } + } + + case .search(_): + // If removal falls within mention text range, remove and search the location + if NSLocationInRange(range.location, mentionRange) { + // Mention range includes "@" character. If mentionSearchKey is empty but range still falls within mentionRange, + // it means that user is removing "@" character. + if !mentionSearchKey.isEmpty { + mentionSearchKey.removeLast() + mentionRange.length -= max(replacementText.count, 1) + + self.updateMentionState(state: .search(key: mentionSearchKey)) + } else { + // Stop searching + self.updateMentionState(state: .idle) + } + } else { + // Removing text outside mention range. We terminate the search. + self.updateMentionState(state: .idle) + } + return true + } + + } else { + + switch mentionState { + case .idle: + mentionSearchKey = "" + self.updateMentionState(state: .idle) + + case .search(_): + mentionSearchKey += replacementText + mentionRange.length += replacementText.count + self.updateMentionState(state: .search(key: mentionSearchKey)) + } + + // If user added text before existing mentions, update it. + for (index, mention) in self.mentions.enumerated() where range.location < mention.index { + mentions[index].index += replacementText.count + } + } + } + return true + } + + func changeSelection(_ textInput: UITextInput) { + switch mentionState { + case .idle: + guard let selectedRange = textInput.selectedTextRange, selectedRange != textInput.textRange(from: textInput.endOfDocument, to: textInput.endOfDocument), selectedRange != textInput.textRange(from: textInput.beginningOfDocument, to: textInput.beginningOfDocument) else { return } + + let cursorPosition = textInput.offset(from: textInput.beginningOfDocument, to: selectedRange.start) + + for mention in mentions { + if mention.index <= cursorPosition && mention.index + mention.length >= cursorPosition, let startPosition = textInput.position(from: textInput.beginningOfDocument, offset: mention.index), let endPosition = textInput.position(from: textInput.beginningOfDocument, offset: mention.index + mention.length + 1) { + if selectedRange == textInput.textRange(from:startPosition, to: endPosition) { return } + textInput.selectedTextRange = textInput.textRange(from:startPosition, to: endPosition) + } + } + case .search(_): + break + } + } + + func setMentions(metadata: [String: Any], inText text: String) { + mentions = AmityMentionMapper.mentions(fromMetadata: metadata) + let highlightedText = highlightMentions(in: text, mentions: mentions, highlightAttributes: highlightAttributes) + self.delegate?.didUpdateAttributedText(text: highlightedText) + } + + private func findMentionWithinRange(range: NSRange) -> NSRange? { + // Note: This at the moment doesn't find mention if user selects between two ranges. It works if user is deleting each individual character. + for mention in mentions { +// let mentionRange = NSRange(location: mention.index, length: mention.length + 1) +// if NSLocationInRange(mentionRange.location, range) { return mentionRange } + if mention.index <= range.location && mention.index + mention.length >= range.location { + let mentionRange = NSRange(location: mention.index, length: mention.length + 1) + return mentionRange + } + } + + return nil + } + + private func removeMentionWithinRange(range: NSRange) { + var remainingMentions = [AmityMention]() + + for mention in mentions { + if !(mention.index <= range.location && mention.index + mention.length >= range.location) { + remainingMentions.append(mention) + } + } + + self.mentions = remainingMentions + } + + private func updateMentionState(state: MentionState) { + self.mentionState = state + switch state { + case .idle: + self.mentionSearchKey = "" + self.mentionRange = .init() + default: + break + } + + delegate?.didChangeMentionState(state: state) + } + + // When user taps on mention list. + func addMention(member: AmityMentionUserModel, textInput: UITextInput, currentText: String) { + // Global banned user cannot be mentioned. + guard !member.isGlobalBan else { return } + + // Range of "@xyz" in current text. If range != 0, it means that mention search is in progress. + guard mentionRange.length != 0 else { + Log.warn("Trying to add mention when mention range is not set") + return + } + + //Log.add(event: .info, "Adding mention \(member.displayName) to index: \(mentionRange.location), length: \(mentionRange.length)") + + // Append mention display name to current text + let searchInput = mentionSearchKey.isEmpty ? "@" : "@\(mentionSearchKey)" // Actual search input in UITextView with "@" + var finalText = currentText.replacingOccurrences(of: searchInput, with: "@\(member.displayName)", options: .caseInsensitive, range: Range(mentionRange, in: currentText)) + + // Note: Remove this logic if we support sending whitespaces + newlines at the beginning of the message text. + // Remove whitespace occurrence in beginning of the text & adjusts mention range. + let whitespaces = finalText.prefix { char in + char.isWhitespace + } + mentionRange.location -= whitespaces.count + finalText = finalText.trimmingCharacters(in: .whitespacesAndNewlines) + + // Append mention array + let mention = AmityMention(type: member.type, index: mentionRange.location, length: member.displayName.count, userId: member.userId) + mentions.append(mention) + + // Determine index to be shifted for existing mentions + let mentionSearchKeyLength = mentionSearchKey.utf8.count // Length of search key already used + let mentionOffset = member.displayName.count - mentionSearchKeyLength // Amount of search text to be replaced. + //Log.add(event: .info, "Mention Offset: \(mentionOffset)") + + // Update the length of previous mention + for (index, mention) in self.mentions.enumerated() where mentionRange.location < mention.index { + mentions[index].index = mentionOffset + mention.index + } + + // Terminate mention search + self.mentionSearchKey = "" + self.updateMentionState(state: .idle) + + // Notify mention addition + //Log.add(event: .info, "Final Text: \(finalText)") + let highlightedText = self.highlightMentions(in: finalText, mentions: mentions, highlightAttributes: highlightAttributes) + self.delegate?.didUpdateAttributedText(text: highlightedText) + } + + // We want to handle mention trigger only if "@" character is entered independently. Not if any other text has "@" as a + // prefix or suffix. + private func shouldHandleMentionTrigger(in textInput: UITextInput, range: NSRange, replacementText: String, currentText: String) -> Bool { + let rangeIndex = range.location + let existingText = currentText.trimmingCharacters(in: .whitespacesAndNewlines) + + // Range of mention trigger in this string + //let triggerRange = Range(range, in: currentText) + + if existingText.isEmpty { + return true + } else { + // Determine text on left & right side of the @ character + let triggerLeftRange = NSRange(location: rangeIndex - 1, length: 1) + let triggerRightRange = NSRange(location: rangeIndex + 1, length: 1) + + var isMentionAllowed = true + + // If left character range is valid + if NSLocationInRange(triggerLeftRange.location, NSRange(location: 0, length: currentText.utf16.count)) { + let beforeCharacter = (currentText as NSString).substring(with: triggerLeftRange).trimmingCharacters(in: .whitespacesAndNewlines) + if !beforeCharacter.isEmpty { + isMentionAllowed = false + } + } + + // If Right character is valid. + if NSLocationInRange(triggerRightRange.location, NSRange(location: 0, length: currentText.utf16.count)) { + let afterCharacter = (currentText as NSString).substring(with: triggerLeftRange).trimmingCharacters(in: .whitespacesAndNewlines) + if !afterCharacter.isEmpty { + isMentionAllowed = false + } + } + + return isMentionAllowed + } + } + + func reset() { + mentions = [] + mentionSearchKey = "" + mentionState = .idle + } +} + +// Highlighter +extension MentionTextEditor { + // Highlight all mentions in a text & returns attributed string + func highlightMentions(in text: String, mentions: [AmityMention], highlightAttributes: [NSAttributedString.Key: Any]) -> NSMutableAttributedString { + + let attributedString = NSMutableAttributedString(string: text, attributes: typingAttributes) + + // We generate attributes for part of the text that needs to be highlighted. + for mention in mentions { + if mention.index < 0 || mention.length <= 0 { continue } + + // Create range for highlighting that text. Here length + 1 is for '@' character. + let range = NSRange(location: mention.index, length: mention.length + 1) + + if range.location != NSNotFound && (range.location + range.length) <= text.count { + attributedString.addAttributes(highlightAttributes, range: range) + } + } + + return attributedString + } +} diff --git a/UpstraUIKit/UpstraUIKit/Modules/Comunity/Mention/Manager/TextHighlighter.swift b/UpstraUIKit/UpstraUIKit/Modules/Comunity/Mention/Manager/TextHighlighter.swift new file mode 100644 index 0000000..5d20009 --- /dev/null +++ b/UpstraUIKit/UpstraUIKit/Modules/Comunity/Mention/Manager/TextHighlighter.swift @@ -0,0 +1,99 @@ +// +// AmityMentionTextHighlighter.swift +// AmityUIKit +// +// Created by Nishan on 8/7/2567 BE. +// Copyright © 2567 BE Amity. All rights reserved. +// + +import Foundation +import AmitySDK +import UIKit +import SwiftUI + +/// Highlights mentions & links and returns AttributedString +class TextHighlighter { + + // MARK: UIKit V3 Specific Implementation + private static func detectAndHighlightLinks(text: String) -> (text: NSMutableAttributedString, hyperlinks: [Hyperlink]) { + + let attributedString = NSMutableAttributedString(string: text) + + guard let detector = try? NSDataDetector(types: NSTextCheckingResult.CheckingType.link.rawValue) else { + return (attributedString, []) + } + + let matches = detector.matches(in: text, options: [], range: NSRange(location: 0, length: text.utf16.count)) + + var links = [Hyperlink]() + for match in matches { + guard let textRange = Range(match.range, in: text) else { continue } + + let urlString = String(text[textRange]) + let validUrlString = urlString.hasPrefixIgnoringCase("http") ? urlString : "http://\(urlString)" + + guard let formattedString = validUrlString.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed), + let url = URL(string: formattedString) else { continue } + + attributedString.addAttributes([ + .foregroundColor: AmityColorSet.highlight, + .attachment: url], range: match.range) + attributedString.addAttribute(.underlineStyle, value: NSUnderlineStyle.single.rawValue, range: match.range) + + links.append(Hyperlink(range: match.range, type: .url(url: url))) + } + + return (attributedString, links) + } + + public static func highlightLinksAndMentions(text: String, metadata: [String: Any], mentionees: [AmityMentionees]) -> (text: NSMutableAttributedString, hyperlinks: [Hyperlink]) { + + var attributedString = NSMutableAttributedString(string: text) + var tappableLinks = [Hyperlink]() + + // 1. Detect links & highlight it + let linkResult = TextHighlighter.detectAndHighlightLinks(text: text) + attributedString = linkResult.text + tappableLinks.append(contentsOf: linkResult.hyperlinks) + + // 2. + // Detect mentions and highlight it. AmityMention array should not be empty + let mentions = AmityMentionMapper.mentions(fromMetadata: metadata) + if !mentions.isEmpty { + // Note: From sdk code, AmityMentionees will never contain any information about channel mention. + var users: [AmityUser] = [] + mentionees.forEach { + if $0.type == .user, let mentionedUsers = $0.users { + users.append(contentsOf: mentionedUsers) + } + } + + // We generate attributes for part of the text that needs to be highlighted. + // This can be useful if we want to highlight specific mention with different attributes. + for mention in mentions { + if mention.index < 0 || mention.length <= 0 { continue } + + var shouldHighlight = true + if mention.type == .user { + // If user is mentioned and the id of the user matches with that present in mentionees array, we highlight that user. + shouldHighlight = users.contains(where: { user in + user.userId == mention.userId + }) + } + + // Create range for highlighting that text. Here length + 1 is for '@' character. + let range = NSRange(location: mention.index, length: mention.length + 1) + + if shouldHighlight, range.location != NSNotFound && (range.location + range.length) <= text.count { + let mentionAttr = MentionAttribute(attributes: [.foregroundColor: AmityColorSet.highlight, .font: AmityFontSet.bodyBold], range: range, userId: mention.userId ?? "") + + attributedString.addAttributes(mentionAttr.attributes, range: mentionAttr.range) + + tappableLinks.append(Hyperlink(range: mentionAttr.range, type: .mention(userId: mentionAttr.userId))) + } + } + } + + return (attributedString, tappableLinks) + } +} diff --git a/UpstraUIKit/UpstraUIKit/Modules/Comunity/Poll/Create/ViewController/AmityPollCreatorViewController.swift b/UpstraUIKit/UpstraUIKit/Modules/Comunity/Poll/Create/ViewController/AmityPollCreatorViewController.swift index 2c4953a..f4fa611 100644 --- a/UpstraUIKit/UpstraUIKit/Modules/Comunity/Poll/Create/ViewController/AmityPollCreatorViewController.swift +++ b/UpstraUIKit/UpstraUIKit/Modules/Comunity/Poll/Create/ViewController/AmityPollCreatorViewController.swift @@ -30,7 +30,7 @@ public final class AmityPollCreatorViewController: AmityViewController { // MARK: - Properties private var postButton: UIBarButtonItem? private var screenViewModel: AmityPollCreatorScreenViewModelType? - private var mentionManager: AmityMentionManager? + private var mentionManager: ASCMentionManager? // MARK: - View's lifecycle public override func viewDidLoad() { @@ -57,7 +57,7 @@ public final class AmityPollCreatorViewController: AmityViewController { } default: break } - vc.mentionManager = AmityMentionManager(withType: .post(communityId: communityId)) + vc.mentionManager = ASCMentionManager(withType: .post(communityId: communityId)) return vc } @@ -196,7 +196,8 @@ extension AmityPollCreatorViewController: AmityPollCreatorScreenViewModelDelegat } } -extension AmityPollCreatorViewController: UITableViewDelegate { +extension AmityPollCreatorViewController: UITableViewDelegate, UITableViewDataSource { + public func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { return UITableView.automaticDimension } @@ -210,13 +211,10 @@ extension AmityPollCreatorViewController: UITableViewDelegate { public func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) { if tableView == mentionTableView { if tableView.isBottomReached { - mentionManager?.loadMore() + mentionManager?.mentionProvider.loadMore() } } } -} - -extension AmityPollCreatorViewController: UITableViewDataSource { public func numberOfSections(in tableView: UITableView) -> Int { if tableView == mentionTableView { @@ -227,7 +225,7 @@ extension AmityPollCreatorViewController: UITableViewDataSource { public func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { if tableView == mentionTableView { - return mentionManager?.users.count ?? 0 + return mentionManager?.mentionProvider.mentionList.count ?? 0 } guard let section = Section(rawValue: section) else { return 0 } switch section { @@ -240,10 +238,15 @@ extension AmityPollCreatorViewController: UITableViewDataSource { public func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { if tableView == mentionTableView { - guard let cell = tableView.dequeueReusableCell(withIdentifier: AmityMentionTableViewCell.identifier) as? AmityMentionTableViewCell, let model = mentionManager?.item(at: indexPath) else { return UITableViewCell() } - cell.display(with: model) + guard let cell = tableView.dequeueReusableCell(withIdentifier: AmityMentionTableViewCell.identifier) as? AmityMentionTableViewCell else { return UITableViewCell() } + + if let provider = mentionManager?.mentionProvider, indexPath.row < provider.mentionList.count { + let model = provider.mentionList[indexPath.row] + cell.display(with: model) + } return cell } + guard let section = Section(rawValue: indexPath.section) else { return UITableViewCell() } switch section { case .question: @@ -347,17 +350,10 @@ extension AmityPollCreatorViewController: AmityPollCreatorCellProtocolDelegate { } } -// MARK: - AmityMentionManagerDelegate -extension AmityPollCreatorViewController: AmityMentionManagerDelegate { - public func didCreateAttributedString(attributedString: NSAttributedString) { - if let cell = tableView.cellForRow(at: IndexPath(row: 0, section: 0)) as? AmityPollCreatorQusetionTableViewCell, let textView = cell.getTextView() { - textView.attributedText = attributedString - textView.typingAttributes = [.font: AmityFontSet.body, .foregroundColor: AmityColorSet.base] - screenViewModel?.action.setPollQuestion(textView.text) - } - } +// MARK: - ASCMentionManagerDelegate +extension AmityPollCreatorViewController: ASCMentionManagerDelegate { - public func didGetUsers(users: [AmityMentionUserModel]) { + public func didUpdateMentionUsers(users: [AmityMentionUserModel]) { if users.isEmpty { mentionTableViewHeightConstraint.constant = 0 mentionTableView.isHidden = true @@ -372,13 +368,19 @@ extension AmityPollCreatorViewController: AmityMentionManagerDelegate { } } - public func didMentionsReachToMaximumLimit() { - let alertController = UIAlertController(title: AmityLocalizedStringSet.Mention.unableToMentionTitle.localizedString, message: AmityLocalizedStringSet.Mention.unableToMentionReplyDescription.localizedString, preferredStyle: .alert) - let cancelAction = UIAlertAction(title: AmityLocalizedStringSet.General.done.localizedString, style: .cancel, handler: nil) - alertController.addAction(cancelAction) - present(alertController, animated: true, completion: nil) + public func didReachMaxMentionLimit() { + AlertController.showAlert(in: self, title: AmityLocalizedStringSet.Mention.unableToMentionTitle.localizedString, message: AmityLocalizedStringSet.Mention.unableToMentionReplyDescription.localizedString) } - public func didCharactersReachToMaximumLimit() { + public func didReachMaxCharacterCountLimit() { + // Intentionally left empty + } + + public func didCreateAttributedString(attributedString: NSAttributedString) { + if let cell = tableView.cellForRow(at: IndexPath(row: 0, section: 0)) as? AmityPollCreatorQusetionTableViewCell, let textView = cell.getTextView() { + textView.attributedText = attributedString + textView.typingAttributes = [.font: AmityFontSet.body, .foregroundColor: AmityColorSet.base] + screenViewModel?.action.setPollQuestion(textView.text) + } } } diff --git a/UpstraUIKit/UpstraUIKit/Utilities/AmityUtilities.swift b/UpstraUIKit/UpstraUIKit/Utilities/AmityUtilities.swift index 000f30f..89843c1 100644 --- a/UpstraUIKit/UpstraUIKit/Utilities/AmityUtilities.swift +++ b/UpstraUIKit/UpstraUIKit/Utilities/AmityUtilities.swift @@ -19,3 +19,20 @@ struct AmityUtilities { return UINib(nibName: nibName, bundle: AmityUIKitManager.bundle) } } + +class AlertController { + + struct DismissConfig { + let title: String + let action: (() -> Void)? + } + + static func showAlert(in vc: UIViewController, title: String, message: String, dismiss: DismissConfig = .init(title: AmityLocalizedStringSet.General.ok.localizedString, action: nil)) { + let cancelAction = UIAlertAction(title: dismiss.title, style: .cancel, handler: { _ in dismiss.action?() }) + + let alertController = UIAlertController(title: title, message: message, preferredStyle: .alert) + alertController.addAction(cancelAction) + + vc.present(alertController, animated: true, completion: nil) + } +}