From 4547b9411ecd7d7ec3cfa6d2fa26cf70981438b6 Mon Sep 17 00:00:00 2001 From: Zayar <zayariosdev@icloud.com> Date: Fri, 22 Nov 2024 19:22:01 +0700 Subject: [PATCH] Update AmityUIKit v4.0.0-beta29 --- .../AmityUIKit.xcodeproj/project.pbxproj | 8 +- .../AmityUIKit4.xcodeproj/project.pbxproj | 22 +- .../Core/Components/TargetSelectionView.swift | 30 +- .../Extenral/Pager/SwiftUIPager/Pager.swift | 4 +- .../Pager/SwiftUIPager/PagerContent.swift | 7 +- .../Core/Extensions/String+Extension.swift | 4 +- .../SocialHome/AmityPostMenuComponent.swift | 2 + .../CommunityProfileViewModel.swift | 11 +- .../PostDetail/AmityPostDetailPage.swift | 2 +- .../StoryTab/AmityStoryTabComponent.swift | 17 +- .../Story/Models/AmityStoryTargetModel.swift | 14 +- .../Pages/ViewStory/AmityViewStoryPage.swift | 436 +++--------------- .../AmityViewStoryPageViewModel.swift | 109 +++++ .../ChildViews/ProgressBarView.swift | 14 +- .../ViewStory/ChildViews/StoryAdView.swift | 10 +- .../ViewStory/ChildViews/StoryCoreView.swift | 415 +++++++++-------- .../ChildViews/StoryCoreViewModel.swift | 190 ++++++++ .../ViewStory/ChildViews/StoryImageView.swift | 51 ++ .../ViewStory/ChildViews/StoryVideoView.swift | 47 ++ .../project.pbxproj | 8 +- .../SampleApp.xcodeproj/project.pbxproj | 18 +- .../contents.xcworkspacedata | 4 - UpstraUIKit/SharedFrameworks/Package.swift | 20 +- .../Components/AmityExpandableLabel.swift | 8 +- 24 files changed, 821 insertions(+), 630 deletions(-) create mode 100644 UpstraUIKit/AmityUIKit4/AmityUIKit4/Story/Pages/ViewStory/AmityViewStoryPageViewModel.swift create mode 100644 UpstraUIKit/AmityUIKit4/AmityUIKit4/Story/Pages/ViewStory/ChildViews/StoryCoreViewModel.swift create mode 100644 UpstraUIKit/AmityUIKit4/AmityUIKit4/Story/Pages/ViewStory/ChildViews/StoryImageView.swift create mode 100644 UpstraUIKit/AmityUIKit4/AmityUIKit4/Story/Pages/ViewStory/ChildViews/StoryVideoView.swift delete mode 100644 UpstraUIKit/SharedFrameworks/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata diff --git a/UpstraUIKit/AmityUIKit.xcodeproj/project.pbxproj b/UpstraUIKit/AmityUIKit.xcodeproj/project.pbxproj index b690a85..855c71e 100644 --- a/UpstraUIKit/AmityUIKit.xcodeproj/project.pbxproj +++ b/UpstraUIKit/AmityUIKit.xcodeproj/project.pbxproj @@ -79,6 +79,7 @@ 1AF0D2CA251371780083D12C /* Log.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1AF0D2C9251371780083D12C /* Log.swift */; }; 68251A632ADEA16200395696 /* AmityPreviewLinkCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 68251A612ADEA16200395696 /* AmityPreviewLinkCell.swift */; }; 68251A642ADEA16200395696 /* AmityPreviewLinkCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 68251A622ADEA16200395696 /* AmityPreviewLinkCell.xib */; }; + 682E02182CF0AE4D00FE1042 /* SharedFrameworks in Frameworks */ = {isa = PBXBuildFile; productRef = 682E02172CF0AE4D00FE1042 /* SharedFrameworks */; }; 6860B01B2ADE3D650042ED45 /* AmityPreviewLinkWizard.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6860B01A2ADE3D650042ED45 /* AmityPreviewLinkWizard.swift */; }; 686E488F2B19A44900591E2D /* AmityStoryTabViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 686E488D2B19A44900591E2D /* AmityStoryTabViewController.swift */; }; 686E48902B19A44900591E2D /* AmityStoryTabViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 686E488E2B19A44900591E2D /* AmityStoryTabViewController.xib */; }; @@ -659,7 +660,6 @@ D4D7683E26006D7000AD4367 /* AmitySettingContentIcon.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4D7683D26006D7000AD4367 /* AmitySettingContentIcon.swift */; }; D4EC11E525B93DCE0095F507 /* AmityPostSharingSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4EC11E425B93DCE0095F507 /* AmityPostSharingSettings.swift */; }; D4EC11EA25B93E000095F507 /* AmityPostSharingTarget.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4EC11E925B93E000095F507 /* AmityPostSharingTarget.swift */; }; - ED5232192CDE38FB00ABA50D /* SharedFrameworks in Frameworks */ = {isa = PBXBuildFile; productRef = ED5232182CDE38FB00ABA50D /* SharedFrameworks */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -1340,7 +1340,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - ED5232192CDE38FB00ABA50D /* SharedFrameworks in Frameworks */, + 682E02182CF0AE4D00FE1042 /* SharedFrameworks in Frameworks */, 68F5D9FA2B481E4000A9FA0D /* AmityUIKit4.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -4507,7 +4507,7 @@ ); name = AmityUIKit; packageProductDependencies = ( - ED5232182CDE38FB00ABA50D /* SharedFrameworks */, + 682E02172CF0AE4D00FE1042 /* SharedFrameworks */, ); productName = UpstraUIKit; productReference = 72A3503024EA811500DA9D46 /* AmityUIKit.framework */; @@ -5523,7 +5523,7 @@ /* End XCConfigurationList section */ /* Begin XCSwiftPackageProductDependency section */ - ED5232182CDE38FB00ABA50D /* SharedFrameworks */ = { + 682E02172CF0AE4D00FE1042 /* SharedFrameworks */ = { isa = XCSwiftPackageProductDependency; productName = SharedFrameworks; }; diff --git a/UpstraUIKit/AmityUIKit4/AmityUIKit4.xcodeproj/project.pbxproj b/UpstraUIKit/AmityUIKit4/AmityUIKit4.xcodeproj/project.pbxproj index 4e13c11..88d4359 100644 --- a/UpstraUIKit/AmityUIKit4/AmityUIKit4.xcodeproj/project.pbxproj +++ b/UpstraUIKit/AmityUIKit4/AmityUIKit4.xcodeproj/project.pbxproj @@ -11,6 +11,11 @@ 68124D9F2B1748DE009B5B4C /* AmityProgressBarElement.swift in Sources */ = {isa = PBXBuildFile; fileRef = 68124D9E2B1748DE009B5B4C /* AmityProgressBarElement.swift */; }; 682C76172B3208CC00018F80 /* StoryPermissionChecker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 682C76162B3208CC00018F80 /* StoryPermissionChecker.swift */; }; 682C761B2B3302AB00018F80 /* AmityUIKitConfig.json in Resources */ = {isa = PBXBuildFile; fileRef = 682C761A2B3302AB00018F80 /* AmityUIKitConfig.json */; }; + 682E020E2CEE6A7900FE1042 /* AmityViewStoryPageViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 682E020D2CEE6A7900FE1042 /* AmityViewStoryPageViewModel.swift */; }; + 682E02122CEE6A8200FE1042 /* StoryCoreViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 682E020F2CEE6A8200FE1042 /* StoryCoreViewModel.swift */; }; + 682E02132CEE6A8200FE1042 /* StoryImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 682E02102CEE6A8200FE1042 /* StoryImageView.swift */; }; + 682E02142CEE6A8200FE1042 /* StoryVideoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 682E02112CEE6A8200FE1042 /* StoryVideoView.swift */; }; + 682E02162CF0AE3D00FE1042 /* SharedFrameworks in Frameworks */ = {isa = PBXBuildFile; productRef = 682E02152CF0AE3D00FE1042 /* SharedFrameworks */; }; 683B9D642C995AFF005619FE /* AmityUserProfileHeaderComponent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 683B9D632C995AFF005619FE /* AmityUserProfileHeaderComponent.swift */; }; 683DF10A2C6B1CAD005BF06C /* AmityCommunityAddUserPage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 683DF1092C6B1CAD005BF06C /* AmityCommunityAddUserPage.swift */; }; 683DF10C2C6B27A4005BF06C /* AmityUserModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 683DF10B2C6B27A4005BF06C /* AmityUserModel.swift */; }; @@ -494,7 +499,6 @@ ED2D02712CB6354C00BF94FA /* PostContentPollView.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED2D02702CB6354C00BF94FA /* PostContentPollView.swift */; }; ED4FA84A2BA01123005D6871 /* AmityLiveChatHeaderViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED4FA8492BA01123005D6871 /* AmityLiveChatHeaderViewModel.swift */; }; ED4FA84C2BA01169005D6871 /* ChannelManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED4FA84B2BA01169005D6871 /* ChannelManager.swift */; }; - ED5232172CDE38EF00ABA50D /* SharedFrameworks in Frameworks */ = {isa = PBXBuildFile; productRef = ED5232162CDE38EF00ABA50D /* SharedFrameworks */; }; ED6EFA112CB39DB6003A176F /* AmityPollPostComposerPage.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED6EFA102CB39DB6003A176F /* AmityPollPostComposerPage.swift */; }; ED6EFA142CB3D3D2003A176F /* PollAnswerResultView.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED6EFA132CB3D3D2003A176F /* PollAnswerResultView.swift */; }; ED6EFA612CB4FE0F003A176F /* PollOptionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED6EFA602CB4FE0F003A176F /* PollOptionView.swift */; }; @@ -519,6 +523,10 @@ 68124D9E2B1748DE009B5B4C /* AmityProgressBarElement.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AmityProgressBarElement.swift; sourceTree = "<group>"; }; 682C76162B3208CC00018F80 /* StoryPermissionChecker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoryPermissionChecker.swift; sourceTree = "<group>"; }; 682C761A2B3302AB00018F80 /* AmityUIKitConfig.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = AmityUIKitConfig.json; sourceTree = "<group>"; }; + 682E020D2CEE6A7900FE1042 /* AmityViewStoryPageViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AmityViewStoryPageViewModel.swift; sourceTree = "<group>"; }; + 682E020F2CEE6A8200FE1042 /* StoryCoreViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoryCoreViewModel.swift; sourceTree = "<group>"; }; + 682E02102CEE6A8200FE1042 /* StoryImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoryImageView.swift; sourceTree = "<group>"; }; + 682E02112CEE6A8200FE1042 /* StoryVideoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoryVideoView.swift; sourceTree = "<group>"; }; 683B9D632C995AFF005619FE /* AmityUserProfileHeaderComponent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AmityUserProfileHeaderComponent.swift; sourceTree = "<group>"; }; 683DF1092C6B1CAD005BF06C /* AmityCommunityAddUserPage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AmityCommunityAddUserPage.swift; sourceTree = "<group>"; }; 683DF10B2C6B27A4005BF06C /* AmityUserModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AmityUserModel.swift; sourceTree = "<group>"; }; @@ -1034,7 +1042,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - ED5232172CDE38EF00ABA50D /* SharedFrameworks in Frameworks */, + 682E02162CF0AE3D00FE1042 /* SharedFrameworks in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1949,6 +1957,7 @@ 68D11A312B2239440046D0FB /* ViewStory */ = { isa = PBXGroup; children = ( + 682E020D2CEE6A7900FE1042 /* AmityViewStoryPageViewModel.swift */, 68CC6A642B1733D10052B77E /* AmityViewStoryPage.swift */, 68D11A322B22399A0046D0FB /* ChildViews */, ); @@ -1958,6 +1967,9 @@ 68D11A322B22399A0046D0FB /* ChildViews */ = { isa = PBXGroup; children = ( + 682E020F2CEE6A8200FE1042 /* StoryCoreViewModel.swift */, + 682E02102CEE6A8200FE1042 /* StoryImageView.swift */, + 682E02112CEE6A8200FE1042 /* StoryVideoView.swift */, 68D11A332B2239C50046D0FB /* StoryCoreView.swift */, 68D11A352B223A000046D0FB /* ProgressBarView.swift */, 68D9BCC82C32FC2B0082685B /* StoryAdView.swift */, @@ -2889,9 +2901,13 @@ 6840981F2B313A6F00697E1B /* VideoCacheManager.swift in Sources */, 685F16CA2BFCAD060016685F /* MessageReactionConfiguration.swift in Sources */, 684AE10C2B0C5CFE00FD7270 /* AmityUIKit4.swift in Sources */, + 682E020E2CEE6A7900FE1042 /* AmityViewStoryPageViewModel.swift in Sources */, 6877D37B2C75DA2F008B3598 /* AmityCommunityMembershipPage.swift in Sources */, 6861984C2C116AF900BA81BE /* AmityPostComposerPage.swift in Sources */, 686830332C993D52009B1694 /* AmityUserProfilePage.swift in Sources */, + 682E02122CEE6A8200FE1042 /* StoryCoreViewModel.swift in Sources */, + 682E02132CEE6A8200FE1042 /* StoryImageView.swift in Sources */, + 682E02142CEE6A8200FE1042 /* StoryVideoView.swift in Sources */, 689EE6A22BEDCB4E00927D51 /* PostManager.swift in Sources */, 68EE70C42C1AD40B005A7002 /* AmityMediaAttachmentViewModel.swift in Sources */, A9CEB8212C7F19A10062823A /* AmityTrendingCommunitiesComponent.swift in Sources */, @@ -3646,7 +3662,7 @@ /* End XCConfigurationList section */ /* Begin XCSwiftPackageProductDependency section */ - ED5232162CDE38EF00ABA50D /* SharedFrameworks */ = { + 682E02152CF0AE3D00FE1042 /* SharedFrameworks */ = { isa = XCSwiftPackageProductDependency; productName = SharedFrameworks; }; diff --git a/UpstraUIKit/AmityUIKit4/AmityUIKit4/Core/Components/TargetSelectionView.swift b/UpstraUIKit/AmityUIKit4/AmityUIKit4/Core/Components/TargetSelectionView.swift index 85b6b38..4665e1a 100644 --- a/UpstraUIKit/AmityUIKit4/AmityUIKit4/Core/Components/TargetSelectionView.swift +++ b/UpstraUIKit/AmityUIKit4/AmityUIKit4/Core/Components/TargetSelectionView.swift @@ -101,19 +101,29 @@ class TargetSelectionViewModel: ObservableObject { communities.map { community -> AnyPublisher<AmityCommunityModel?, Never> in let communityModel = AmityCommunityModel(object: community) - if community.onlyAdminCanPost { + // Story permission specifically need to check without considering onlyAdminCanPost + if contentType == .story { return Future<AmityCommunityModel?, Never> { promise in - let permission: AmityPermission - switch contentType { - case .post, .poll: - permission = .createPrivilegedPost - - case .story: - permission = .manageStoryCommunity + AmityUIKit4Manager.client.hasPermission(.manageStoryCommunity, forCommunity: community.communityId) { success in + let hasPermission = success + let allowAllUserCreation = AmityUIKitManagerInternal.shared.client.getSocialSettings()?.story?.allowAllUserToCreateStory ?? false + let hasStoryManagePermission = (allowAllUserCreation || hasPermission) && communityModel.isJoined + + if hasStoryManagePermission { + promise(.success(communityModel)) + } else { + promise(.success(nil)) + } } - - AmityUIKit4Manager.client.hasPermission(permission, forCommunity: community.communityId) { success in + } + .eraseToAnyPublisher() + } + + // Check for post permission + if community.onlyAdminCanPost { + return Future<AmityCommunityModel?, Never> { promise in + AmityUIKit4Manager.client.hasPermission(.createPrivilegedPost, forCommunity: community.communityId) { success in if success { promise(.success(communityModel)) } else { diff --git a/UpstraUIKit/AmityUIKit4/AmityUIKit4/Core/Extenral/Pager/SwiftUIPager/Pager.swift b/UpstraUIKit/AmityUIKit4/AmityUIKit4/Core/Extenral/Pager/SwiftUIPager/Pager.swift index ccb097e..5038a7d 100644 --- a/UpstraUIKit/AmityUIKit4/AmityUIKit4/Core/Extenral/Pager/SwiftUIPager/Pager.swift +++ b/UpstraUIKit/AmityUIKit4/AmityUIKit4/Core/Extenral/Pager/SwiftUIPager/Pager.swift @@ -45,10 +45,10 @@ public struct Pager<Element, ID, PageView>: View where PageView: View, Element: /*** Constants ***/ /// Angle of rotation when should rotate - let rotationDegrees: Double = 20 + let rotationDegrees: Double = -90 /// Axis of rotation when should rotate - let rotationAxis: (x: CGFloat, y: CGFloat, z: CGFloat) = (0, 1, 0) + let rotationAxis: (x: CGFloat, y: CGFloat, z: CGFloat) = (0, 1, 0.001) /*** Dependencies ***/ diff --git a/UpstraUIKit/AmityUIKit4/AmityUIKit4/Core/Extenral/Pager/SwiftUIPager/PagerContent.swift b/UpstraUIKit/AmityUIKit4/AmityUIKit4/Core/Extenral/Pager/SwiftUIPager/PagerContent.swift index f083cf4..19ac638 100644 --- a/UpstraUIKit/AmityUIKit4/AmityUIKit4/Core/Extenral/Pager/SwiftUIPager/PagerContent.swift +++ b/UpstraUIKit/AmityUIKit4/AmityUIKit4/Core/Extenral/Pager/SwiftUIPager/PagerContent.swift @@ -26,10 +26,10 @@ extension Pager { /*** Constants ***/ /// Angle of rotation when should rotate - let rotationDegrees: Double = 20 + let rotationDegrees: Double = -90 /// Axis of rotation when should rotate - let rotationAxis: (x: CGFloat, y: CGFloat, z: CGFloat) = (0, 1, 0) + let rotationAxis: (x: CGFloat, y: CGFloat, z: CGFloat) = (0, 1, 0.001) /*** Dependencies ***/ @@ -197,6 +197,9 @@ extension Pager { } } .offset(x: self.xOffset, y : self.yOffset) + .onChange(of: pagerModel.index) { index in + onPageChanged?(index) + } } .frame(size: size) diff --git a/UpstraUIKit/AmityUIKit4/AmityUIKit4/Core/Extensions/String+Extension.swift b/UpstraUIKit/AmityUIKit4/AmityUIKit4/Core/Extensions/String+Extension.swift index dce077e..7f026c3 100644 --- a/UpstraUIKit/AmityUIKit4/AmityUIKit4/Core/Extensions/String+Extension.swift +++ b/UpstraUIKit/AmityUIKit4/AmityUIKit4/Core/Extensions/String+Extension.swift @@ -19,8 +19,8 @@ extension String { } var isValidURL: Bool { - let detector = try! NSDataDetector(types: NSTextCheckingResult.CheckingType.link.rawValue) - if let match = detector.firstMatch(in: self, options: [], range: NSRange(location: 0, length: self.utf16.count)) { + let detector = try? NSDataDetector(types: NSTextCheckingResult.CheckingType.link.rawValue) + if let match = detector?.firstMatch(in: self, options: [], range: NSRange(location: 0, length: self.utf16.count)) { // it is a link, if the match covers the whole string return match.range.length == self.utf16.count } else { diff --git a/UpstraUIKit/AmityUIKit4/AmityUIKit4/Social/Components/SocialHome/AmityPostMenuComponent.swift b/UpstraUIKit/AmityUIKit4/AmityUIKit4/Social/Components/SocialHome/AmityPostMenuComponent.swift index 8beedf4..9ea1a13 100644 --- a/UpstraUIKit/AmityUIKit4/AmityUIKit4/Social/Components/SocialHome/AmityPostMenuComponent.swift +++ b/UpstraUIKit/AmityUIKit4/AmityUIKit4/Social/Components/SocialHome/AmityPostMenuComponent.swift @@ -28,6 +28,7 @@ public struct AmityCreatePostMenuComponent: AmityComponentView { @Binding private var isPresented: Bool @State private var showPostCreationMenuScaleEffect: Bool = false + private let allowAllUserToCreateStory = AmityUIKitManagerInternal.shared.client.getSocialSettings()?.story?.allowAllUserToCreateStory ?? false public var id: ComponentId { .createPostMenu @@ -80,6 +81,7 @@ public struct AmityCreatePostMenuComponent: AmityComponentView { .onTapGesture { goToStoryCreation() } + .isHidden(!allowAllUserToCreateStory) .accessibilityIdentifier(AccessibilityID.Social.CreatePostMenu.createStoryButton) case .poll: let icon = AmityIcon.createPollMenuIcon diff --git a/UpstraUIKit/AmityUIKit4/AmityUIKit4/Social/Pages/CommunityProfile/CommunityProfileViewModel.swift b/UpstraUIKit/AmityUIKit4/AmityUIKit4/Social/Pages/CommunityProfile/CommunityProfileViewModel.swift index 3b5e2a8..d8d42b3 100644 --- a/UpstraUIKit/AmityUIKit4/AmityUIKit4/Social/Pages/CommunityProfile/CommunityProfileViewModel.swift +++ b/UpstraUIKit/AmityUIKit4/AmityUIKit4/Social/Pages/CommunityProfile/CommunityProfileViewModel.swift @@ -51,9 +51,6 @@ public class CommunityProfileViewModel: ObservableObject { loadStories() loadPinnedFeed() - Task { @MainActor in - hasStoryManagePermission = await StoryPermissionChecker.checkUserHasManagePermission(communityId: communityId) - } } deinit { @@ -69,6 +66,14 @@ public class CommunityProfileViewModel: ObservableObject { self?.pendingPostCount = community.pendingPostCount + // Check StoryManage Permission + Task { @MainActor [weak self] in + let hasPermission = await StoryPermissionChecker.checkUserHasManagePermission(communityId: community.communityId) + let allowAllUserCreation = AmityUIKitManagerInternal.shared.client.getSocialSettings()?.story?.allowAllUserToCreateStory ?? false + + self?.hasStoryManagePermission = (allowAllUserCreation || hasPermission) && community.isJoined + } + if communityObject.onlyAdminCanPost { AmityUIKit4Manager.client.hasPermission(.createPrivilegedPost, forCommunity: community.communityId) { success in self?.hasCreatePostPermission = success && community.isJoined diff --git a/UpstraUIKit/AmityUIKit4/AmityUIKit4/Social/Pages/PostDetail/AmityPostDetailPage.swift b/UpstraUIKit/AmityUIKit4/AmityUIKit4/Social/Pages/PostDetail/AmityPostDetailPage.swift index 31c9fc5..b1f898b 100644 --- a/UpstraUIKit/AmityUIKit4/AmityUIKit4/Social/Pages/PostDetail/AmityPostDetailPage.swift +++ b/UpstraUIKit/AmityUIKit4/AmityUIKit4/Social/Pages/PostDetail/AmityPostDetailPage.swift @@ -242,7 +242,7 @@ class AmityPostDetailPageViewModel: ObservableObject { postObject = postManager.getPost(withId: postId) token = postObject?.observe({ livePost, error in - if let snapshot = livePost.snapshot, let _ = snapshot.getPollInfo() { + if let snapshot = livePost.snapshot { self.post = AmityPostModel(post: snapshot) } }) diff --git a/UpstraUIKit/AmityUIKit4/AmityUIKit4/Story/Components/StoryTab/AmityStoryTabComponent.swift b/UpstraUIKit/AmityUIKit4/AmityUIKit4/Story/Components/StoryTab/AmityStoryTabComponent.swift index 4117c4d..e0e5397 100644 --- a/UpstraUIKit/AmityUIKit4/AmityUIKit4/Story/Components/StoryTab/AmityStoryTabComponent.swift +++ b/UpstraUIKit/AmityUIKit4/AmityUIKit4/Story/Components/StoryTab/AmityStoryTabComponent.swift @@ -163,16 +163,25 @@ class AmityStoryTabComponentViewModel: ObservableObject { } private func loadCommunityStoryTarget(_ communityId: String) { - Task { @MainActor in - self.hasManagePermission = await StoryPermissionChecker.checkUserHasManagePermission(communityId: communityId) - } - storyTargetObject = storyManager.getStoryTarget(targetType: .community, targetId: communityId) cancellable = nil cancellable = storyTargetObject?.$snapshot .sink(receiveValue: { [weak self] target in guard let target else { return } + // Check StoryManage Permission + Task { @MainActor [weak self] in + let hasPermission = await StoryPermissionChecker.checkUserHasManagePermission(communityId: communityId) + let allowAllUserCreation = AmityUIKitManagerInternal.shared.client.getSocialSettings()?.story?.allowAllUserToCreateStory ?? false + + guard let community = target.community else { + self?.hasManagePermission = false + return + } + + self?.hasManagePermission = (allowAllUserCreation || hasPermission) && community.isJoined + } + if let existingModel = self?.communityFeedStoryTarget { existingModel.updateModel(target) } else { diff --git a/UpstraUIKit/AmityUIKit4/AmityUIKit4/Story/Models/AmityStoryTargetModel.swift b/UpstraUIKit/AmityUIKit4/AmityUIKit4/Story/Models/AmityStoryTargetModel.swift index 1f45003..6338fb6 100644 --- a/UpstraUIKit/AmityUIKit4/AmityUIKit4/Story/Models/AmityStoryTargetModel.swift +++ b/UpstraUIKit/AmityUIKit4/AmityUIKit4/Story/Models/AmityStoryTargetModel.swift @@ -28,6 +28,7 @@ public class AmityStoryTargetModel: ObservableObject, Identifiable, Equatable { @Published var items: [PaginatedItem<AmityStoryModel>] = [] @Published var itemCount: Int = 0 + @Published var storyLoadingStatus: AmityLoadingStatus = .notLoading @Published var hasUnseenStory: Bool = false @Published var hasFailedStory: Bool = false @Published var hasSyncingStory: Bool = false @@ -72,6 +73,7 @@ public class AmityStoryTargetModel: ObservableObject, Identifiable, Equatable { var surplus = 0 if let adFrequency = AdEngine.shared.getAdFrequency(at: .story), adFrequency.value > 0 { surplus = totalSeenStoryCount % adFrequency.value + Log.add(event: .info, "Surplus: \(surplus)") } paginatorCancellable = nil @@ -81,7 +83,7 @@ public class AmityStoryTargetModel: ObservableObject, Identifiable, Equatable { paginator?.load() paginatorCancellable = paginator?.$snapshots - .debounce(for: .milliseconds(100), scheduler: DispatchQueue.main) + .debounce(for: .milliseconds(300), scheduler: DispatchQueue.main) .sink(receiveValue: { [weak self] items in guard let self, let stories = self.storyCollection?.snapshots else { return } @@ -109,10 +111,8 @@ public class AmityStoryTargetModel: ObservableObject, Identifiable, Equatable { } self.items = newSnapshot.items - - if self.itemCount != newSnapshot.items.count { - self.itemCount = newSnapshot.items.count - } + self.itemCount = newSnapshot.items.count + VideoPlayer.preload(urls: newSnapshot.videoURLs) @@ -121,8 +121,10 @@ public class AmityStoryTargetModel: ObservableObject, Identifiable, Equatable { self.hasFailedStory = false self.hasSyncingStory = false } + + self.storyLoadingStatus = storyCollection.loadingStatus }) - + } private func mapToModel(_ items: [PaginatedItem<AmityStory>]) -> (items: [PaginatedItem<AmityStoryModel>], videoURLs: [URL]) { diff --git a/UpstraUIKit/AmityUIKit4/AmityUIKit4/Story/Pages/ViewStory/AmityViewStoryPage.swift b/UpstraUIKit/AmityUIKit4/AmityUIKit4/Story/Pages/ViewStory/AmityViewStoryPage.swift index 18c687a..f8ef01b 100644 --- a/UpstraUIKit/AmityUIKit4/AmityUIKit4/Story/Pages/ViewStory/AmityViewStoryPage.swift +++ b/UpstraUIKit/AmityUIKit4/AmityUIKit4/Story/Pages/ViewStory/AmityViewStoryPage.swift @@ -10,8 +10,6 @@ import AVKit import AmitySDK import Combine -let STORY_DURATION: CGFloat = 4.0 - public enum AmityViewStoryPageType { case communityFeed(String) // User can start viewing any specific story target in Global Feed. @@ -27,404 +25,102 @@ public struct AmityViewStoryPage: AmityPageView { .storyPage } - @StateObject var viewModel: AmityStoryPageViewModel - - @State private var totalDuration: CGFloat = STORY_DURATION - - @State private var storyTargetIndex = 0 - @State private var storySegmentIndex: Int = 0 - @State private var progressSegmentWidth: CGFloat = 0.0 - @State private var progressValueToIncrease: CGFloat = 0.0 - @State private var showBottomSheet: Bool = false - @State private var isAlertShown: Bool = false - @State private var showToast: Bool = false - + @StateObject var viewModel: AmityViewStoryPageViewModel @State private var hasStoryManagePermission: Bool = false - @StateObject private var progressBarViewModel: ProgressBarViewModel = ProgressBarViewModel(progressArray: []) - @StateObject private var storyCoreViewModel: StoryCoreViewModel = StoryCoreViewModel() + @State private var page: Page = Page.first() private let storyPageType: AmityViewStoryPageType - @State private var page: Page = Page.first() @StateObject private var viewConfig: AmityViewConfigController @Environment(\.colorScheme) private var colorScheme public init(type: AmityViewStoryPageType) { self.storyPageType = type - _viewModel = StateObject(wrappedValue: AmityStoryPageViewModel(type: type)) + _viewModel = StateObject(wrappedValue: AmityViewStoryPageViewModel(type: type)) _viewConfig = StateObject(wrappedValue: AmityViewConfigController(pageId: .storyPage)) } public var body: some View { - - AmityView(configId: configId, - config: { configDict in - }) { config in - GeometryReader { geometry in - Pager(page: page, data: viewModel.storyTargets, id: \.targetId) { storyTarget in - ZStack() { - - StoryCoreView(self, storyTarget: storyTarget, - storySegmentIndex: $storySegmentIndex, - totalDuration: $totalDuration, - moveStoryTarget: { direction in moveStoryTarget(direction: direction) }, - moveStorySegment: { direction in moveStorySegment(direction: direction) }) - .environmentObject(viewModel) - .environmentObject(storyCoreViewModel) - .environmentObject(host) - - - VStack(alignment: .trailing) { - ProgressBarView(pageId: id, progressBarViewModel: progressBarViewModel) - .frame(height: 3) - .padding(EdgeInsets(top: 16, leading: 20, bottom: 10, trailing: 20)) - .animation(nil) - .onAppear { - Log.add(event: .info, "ProgressBar Appeared") - } - .onReceive(storyTarget.$itemCount) { count in - Log.add(event: .info, "Story Segment Changed: \(count) - \(storyTarget.targetName)") - updateProgressSegmentWidth(totalWidth: geometry.size.width, numberOfStories: count) - - if storyTarget.hasUnseenStory { - storySegmentIndex = storyTarget.unseenStoryIndex - } - - progressBarViewModel.progressArray = (0..<count).map({ index in - // If a story is deleted and story count is changed, this block will trigger again. - // All segments till storySegmentIndex need to be filled. - if index < storySegmentIndex && storySegmentIndex != 0 { - let model = AmityProgressBarElementViewModel() - model.progress = progressSegmentWidth - return model - } - return AmityProgressBarElementViewModel() - }) - } - .isHidden(viewConfig.isHidden(elementId: .progressBarElement), remove: false) - - HStack(spacing: 0) { - /// Show overflow menu if item is the story - /// Hide it if item is ads - if let item = viewModel.storyTargets[storyTargetIndex].items.element(at: storySegmentIndex), - case let .content(_) = item.type { - Button { - showBottomSheet.toggle() - } label: { - let icon = AmityIcon.getImageResource(named: viewConfig.getConfig(elementId: .overflowMenuElement, key: "overflow_menu_icon", of: String.self) ?? "") - Image(icon) - .frame(width: 24, height: 20) - .padding(.trailing, 20) - } - .onAppear { - Task { - let storyTargetId = viewModel.storyTargets[storyTargetIndex].targetId - hasStoryManagePermission = await StoryPermissionChecker.checkUserHasManagePermission(communityId: storyTargetId) - } - } - .isHidden(!hasStoryManagePermission, remove: false) - .accessibilityIdentifier(AccessibilityID.Story.AmityViewStoryPage.meatballsButton) - .isHidden(viewConfig.isHidden(elementId: .overflowMenuElement), remove: false) - } - - Button { - Log.add(event: .info, "Tapped Closed!!!") - host.controller?.dismiss(animated: true) - } label: { - let icon = AmityIcon.getImageResource(named: viewConfig.getConfig(elementId: .closeButtonElement, key: "close_icon", of: String.self) ?? "") - Image(icon) - .frame(width: 24, height: 18) - .padding(.trailing, 25) - } - .accessibilityIdentifier(AccessibilityID.Story.AmityViewStoryPage.closeButton) - .isHidden(viewConfig.isHidden(elementId: .closeButtonElement), remove: false) - } - - Spacer() - } - } - } - .contentLoadingPolicy(.lazy(recyclingRatio: 1)) - .bottomSheet(isPresented: $showBottomSheet, height: 148, topBarBackgroundColor: Color(viewConfig.theme.backgroundColor), animation: .easeIn(duration: 0.1)) { - getBottomSheetView() - } - .onAppear { - page.update(.new(index: storyTargetIndex)) - } - .onDisappear { - viewModel.shouldRunTimer = false - } - .onReceive(viewModel.timer, perform: { _ in - guard viewModel.shouldRunTimer else { return } - timerAction() - }) - .onChange(of: showBottomSheet) { value in - viewModel.shouldRunTimer = !value - storyCoreViewModel.playVideo = !value - } - .onChange(of: isAlertShown) { value in - viewModel.shouldRunTimer = !value - storyCoreViewModel.playVideo = !value - } - .onChange(of: showToast) { value in - Toast.showToast(style: viewModel.toastStyle, message: viewModel.toastMessage, bottomPadding: 70) - } - .onReceive(viewModel.$storyTargets) { value in + ZStack { + Color.clear + .overlay ( + ProgressView().progressViewStyle(CircularProgressViewStyle(tint: .white)) + .isHidden(!viewModel.showActivityIndicator) + ) + .overlay( + closeButton + .padding(.top, 40) + .isHidden(viewModel.storyTargets.element(at: viewModel.storyTargetIndex)?.itemCount != 0) + , alignment: .topTrailing) + .onReceive(viewModel.$storyTargetLoadingStatus) { status in if case .globalFeed(let communityId) = storyPageType { - storyTargetIndex = value.firstIndex { $0.targetId == communityId } ?? 0 + let index = viewModel.storyTargets.firstIndex { $0.targetId == communityId } ?? 0 + viewModel.storyTargetIndex = index + page.update(.new(index: index)) } - } - .onChange(of: storyTargetIndex) { value in - viewModel.preloadStoryTargets(value, viewModel.storyTargets) - storySegmentIndex = 0 - page.update(.new(index: value)) - updateProgressSegmentWidth(totalWidth: geometry.size.width, numberOfStories: viewModel.storyTargets[value].items.count) + if case .communityFeed(_) = storyPageType { + viewModel.storyTargetIndex = 0 + page.update(.new(index: 0)) + } } - .onChange(of: totalDuration) { value in - progressValueToIncrease = progressSegmentWidth / CGFloat(value / 0.001) + .zIndex(1) + + if viewModel.storyTargetLoadingStatus == .loaded || !viewModel.storyTargets.isEmpty { + Pager(page: page, data: viewModel.storyTargets, id: \.targetId) { storyTarget in + StoryCoreView(self, + storyPageViewModel: viewModel, + storyTarget: storyTarget) + .environmentObject(host) + } + .contentLoadingPolicy(.lazy(recyclingRatio: 4)) + .interactive(rotation: true) + .interactive(scale: 0.7) + .itemSpacing(-60) + .horizontal() + .allowsDragging(viewModel.storyTargets.count != 1) + .pagingPriority(.simultaneous) + .sensitivity(.custom(0.35)) + .delaysTouches(false) + .onDraggingBegan({ + viewModel.debounceUpdateShouldRunTimer(false) + }) + .onDraggingEnded({ + viewModel.debounceUpdateShouldRunTimer(true) + }) + .draggingAnimation(onChange: .custom(animation: .easeInOut(duration: 0.3)), onEnded: .custom(animation: .easeInOut(duration: 0.3))) + .onPageChanged({ index in + viewModel.debounceUpdateStoryTargetIndex(index) + }) + .onChange(of: viewModel.storyTargetIndex) { index in + guard page.index != index else { return } + page.update(.new(index: index)) } - .onChange(of: storySegmentIndex) { _ in - + .onDisappear { + viewModel.shouldRunTimer = false } - .tabViewStyle(.page(indexDisplayMode: .never)) } } .environmentObject(viewConfig) - .background(Color.black.ignoresSafeArea()) - .ignoresSafeArea(.keyboard) + .background(Color.black.ignoresSafeArea(edges: .top)) + .ignoresSafeArea(edges: .bottom) + .statusBarHidden() .onChange(of: colorScheme) { value in viewConfig.updateTheme() } } - @ViewBuilder - private func getBottomSheetView() -> some View { - VStack { - HStack(spacing: 12) { - Image(AmityIcon.trashBinIcon.getImageResource()) - .renderingMode(.template) - .resizable() - .aspectRatio(contentMode: .fill) - .frame(width: 20, height: 24) - .foregroundColor(Color(viewConfig.theme.baseColor)) - - Button { - isAlertShown.toggle() - showBottomSheet.toggle() - } label: { - Text("Delete story") - .applyTextStyle(.bodyBold(Color(viewConfig.theme.baseColor))) - } - .buttonStyle(.plain) - .alert(isPresented: $isAlertShown, content: { - Alert(title: Text(AmityLocalizedStringSet.Story.deleteStoryTitle.localizedString), message: Text(AmityLocalizedStringSet.Story.deleteStoryMessage.localizedString), primaryButton: .cancel(), secondaryButton: .destructive(Text(AmityLocalizedStringSet.General.delete.localizedString), action: { - if let item = viewModel.storyTargets[storyTargetIndex].items.element(at: storySegmentIndex), - case let .content(story) = item.type { - Task { @MainActor in - try await viewModel.deleteStory(storyId: story.storyId) - storyCoreViewModel.playVideo = false - moveStorySegment(direction: .backward) - showToast.toggle() - } - } - })) - }) - .accessibilityIdentifier(AccessibilityID.Story.AmityViewStoryPage.BottomSheet.deleteButton) - Spacer() - } - .padding(EdgeInsets(top: 16, leading: 20, bottom: 0, trailing: 20)) - Spacer() - } - .background(Color(viewConfig.theme.backgroundColor).ignoresSafeArea()) - } - - - private func updateProgressSegmentWidth(totalWidth: CGFloat, numberOfStories: Int) { - progressSegmentWidth = (totalWidth - 37 - (3.0 * CGFloat(numberOfStories))) / CGFloat(numberOfStories) - progressValueToIncrease = progressSegmentWidth / CGFloat(totalDuration / 0.001) - } - - - private func timerAction() { - - guard storySegmentIndex < progressBarViewModel.progressArray.count else { + private var closeButton: some View { + Button { + Log.add(event: .info, "Tapped Closed!!!") host.controller?.dismiss(animated: true) - return - } - - if progressBarViewModel.progressArray[storySegmentIndex].progress == progressSegmentWidth { - - let lastStoryTargetIndex = viewModel.storyTargets.count - 1 - let lastStorySegmentIndex = viewModel.storyTargets[storyTargetIndex].items.count - 1 - - - if storySegmentIndex != lastStorySegmentIndex { - storySegmentIndex += 1 - } else { - - if storyTargetIndex != lastStoryTargetIndex { - storyTargetIndex += 1 - } else { - host.controller?.dismiss(animated: true) - } - - } - - } else { - let oldProgress = progressBarViewModel.progressArray[storySegmentIndex].progress - let newProgress = oldProgress + progressValueToIncrease - progressBarViewModel.progressArray[storySegmentIndex].progress = min(max(oldProgress, newProgress), progressSegmentWidth) - } - } - - - func moveStorySegment(direction: MoveDirection) { - guard storySegmentIndex < progressBarViewModel.progressArray.count else { - return - } - - switch direction { - case .forward: - progressBarViewModel.progressArray[storySegmentIndex].progress = progressSegmentWidth - - let lastStoryTargetIndex = viewModel.storyTargets.count - 1 - let lastStorySegmentIndex = viewModel.storyTargets[storyTargetIndex].items.count - 1 - - if storySegmentIndex != lastStorySegmentIndex { - storySegmentIndex += 1 - } else if storyTargetIndex == lastStoryTargetIndex && storySegmentIndex == lastStorySegmentIndex { - host.controller?.dismiss(animated: true) - } - - case .backward: - progressBarViewModel.progressArray[storySegmentIndex].progress = 0 - - if storySegmentIndex >= 1 { - storySegmentIndex -= 1 - progressBarViewModel.progressArray[storySegmentIndex].progress = 0 - } else if storyTargetIndex >= 1 { - storyTargetIndex -= 1 - } - } - } - - - func moveStoryTarget(direction: MoveDirection) { - switch direction { - case .forward: - guard storyTargetIndex < viewModel.storyTargets.count - 1 else { return } - storyTargetIndex += 1 - case .backward: - guard storyTargetIndex > 0 else { return } - storyTargetIndex -= 1 - } - } -} - -enum MoveDirection { - case forward, backward -} - -class AmityStoryPageViewModel: ObservableObject { - - @Published var storyTargets: [AmityStoryTargetModel] = [] - @Published var toastMessage: String = "" - @Published var toastStyle: ToastStyle = .success - private var seenStoryCount: Int = 0 - - private var cancellable: AnyCancellable? - private var storyTargetObject: AmityObject<AmityStoryTarget>? - private var storyTargetCollection: AmityCollection<AmityStoryTarget>? - private var seenStoryModels: Set<AmityStoryModel> = Set() - - let storyManager = StoryManager() - let timer = Timer.publish(every: 0.001, on: .main, in: .common).autoconnect() - var shouldRunTimer: Bool = false - - init(type: AmityViewStoryPageType) { - - switch type { - case .communityFeed(let communityId): - storyTargetObject = storyManager.getStoryTarget(targetType: .community, targetId: communityId) - cancellable = nil - cancellable = storyTargetObject?.$snapshot - .first { [weak self] _ in - self?.storyTargetObject?.dataStatus == .fresh - } - .sink { [weak self] target in - guard let target else { return } - - let storyTarget = AmityStoryTargetModel(target) - storyTarget.fetchStory() - - self?.storyTargets = [storyTarget] - } - - case .globalFeed(let communityId): - storyTargetCollection = storyManager.getGlobaFeedStoryTargets(options: .smart) - cancellable = nil - cancellable = storyTargetCollection?.$snapshots - .first { !$0.isEmpty } - .sink { [weak self] targets in - let storyTargets = targets.compactMap { target -> AmityStoryTargetModel? in - return AmityStoryTargetModel(target) - }.removeDuplicates() - self?.preloadStoryTargets(storyTargets.firstIndex { $0.targetId == communityId } ?? 0, storyTargets) - - self?.storyTargets = storyTargets - } - } - } - - deinit { - seenStoryModels.forEach { $0.analytics.markAsSeen() } - timer.upstream.connect().cancel() - URLImageService.defaultImageService.inMemoryStore?.removeAllImages() - } - - func preloadStoryTargets(_ index: Int, _ storyTargets: [AmityStoryTargetModel]) { - let nextStoryTargetIndex = index + 1 - let previousStoryTargetIndex = index - 1 - - if index <= storyTargets.count - 1 { - storyTargets[index].fetchStory(totalSeenStoryCount: seenStoryCount) - } - - if nextStoryTargetIndex <= storyTargets.count - 1 { - Log.add(event: .info, "Preloaded next index: \(nextStoryTargetIndex)") - storyTargets[nextStoryTargetIndex].fetchStory(totalSeenStoryCount: seenStoryCount) - } - - if previousStoryTargetIndex >= 0 { - Log.add(event: .info, "Preloaded previous index: \(previousStoryTargetIndex)") - storyTargets[previousStoryTargetIndex].fetchStory(totalSeenStoryCount: seenStoryCount) + } label: { + let icon = AmityIcon.getImageResource(named: viewConfig.getConfig(elementId: .closeButtonElement, key: "close_icon", of: String.self) ?? "") + Image(icon) + .frame(width: 24, height: 18) + .padding(.trailing, 25) } } - - func markAsSeen(_ storyModel: AmityStoryModel) { - seenStoryModels.insert(storyModel) - seenStoryCount += 1 - } - - @MainActor - func deleteStory(storyId: String) async throws { - try await storyManager.deleteStory(storyId: storyId) - toastMessage = AmityLocalizedStringSet.Story.storyDeletedToastMessage.localizedString - } - - @MainActor - @discardableResult - func addReaction(storyId: String) async throws -> Bool { - try await storyManager.addReaction(storyId: storyId) - } - - @MainActor - @discardableResult - func removeReaction(storyId: String) async throws -> Bool { - try await storyManager.removeReaction(storyId: storyId) - } - } diff --git a/UpstraUIKit/AmityUIKit4/AmityUIKit4/Story/Pages/ViewStory/AmityViewStoryPageViewModel.swift b/UpstraUIKit/AmityUIKit4/AmityUIKit4/Story/Pages/ViewStory/AmityViewStoryPageViewModel.swift new file mode 100644 index 0000000..6ef41d3 --- /dev/null +++ b/UpstraUIKit/AmityUIKit4/AmityUIKit4/Story/Pages/ViewStory/AmityViewStoryPageViewModel.swift @@ -0,0 +1,109 @@ +// +// AmityStoryPageViewModel.swift +// AmityUIKit4 +// +// Created by Zay Yar Htun on 9/3/24. +// + +import SwiftUI +import AmitySDK +import Combine + +let STORY_DURATION: CGFloat = 4.0 + +class AmityViewStoryPageViewModel: ObservableObject { + @Published var storyTargetIndex: Int = 0 + + @Published var storyTargets: [AmityStoryTargetModel] = [] + @Published var totalDuration: CGFloat = STORY_DURATION + @Published var storyTargetLoadingStatus: AmityLoadingStatus = .notLoading + @Published var shouldRunTimer: Bool = false + @Published var showActivityIndicator: Bool = true + var seenStoryCount: Int = 0 + + private var cancellable = Set<AnyCancellable>() + private var storyTargetObject: AmityObject<AmityStoryTarget>? + private var storyTargetCollection: AmityCollection<AmityStoryTarget>? + private var seenStoryModels: Set<AmityStoryModel> = Set() + private let updateShouldRunTimedebouncer = Debouncer(delay: 0.1) + private let storyTargetMovingDebouncer = Debouncer(delay: 0.3) + + let storyManager = StoryManager() + let timer = Timer.publish(every: 0.001, on: .main, in: .common).autoconnect() + + var activeStoryTarget: AmityStoryTargetModel { + storyTargets[storyTargetIndex] + } + + init(type: AmityViewStoryPageType) { + + switch type { + case .communityFeed(let communityId): + storyTargetObject = storyManager.getStoryTarget(targetType: .community, targetId: communityId) + storyTargetObject?.$snapshot + .first() + .sink { [weak self] target in + guard let self, let target else { return } + + let storyTarget = AmityStoryTargetModel(target) + self.storyTargets = [storyTarget] + } + .store(in: &cancellable) + + storyTargetObject?.$loadingStatus + .sink(receiveValue: { [weak self] status in + guard let self else { return } + self.storyTargetLoadingStatus = status + }) + .store(in: &cancellable) + + case .globalFeed(_): + storyTargetCollection = storyManager.getGlobaFeedStoryTargets(options: .smart) + storyTargetCollection?.$snapshots + .first { !$0.isEmpty } + .sink { [weak self] targets in + guard let self else { return } + + let storyTargets = targets.compactMap { target -> AmityStoryTargetModel? in + return AmityStoryTargetModel(target) + }.removeDuplicates() + + self.storyTargets = storyTargets + } + .store(in: &cancellable) + + storyTargetCollection?.$loadingStatus + .sink(receiveValue: { [weak self] status in + guard let self else { return } + self.storyTargetLoadingStatus = status + }) + .store(in: &cancellable) + } + } + + deinit { + timer.upstream.connect().cancel() + URLImageService.defaultImageService.inMemoryStore?.removeAllImages() + } + + func markAsSeen(_ storyModel: AmityStoryModel) { + if !storyModel.isSeen { + storyModel.analytics.markAsSeen() + } + seenStoryCount += 1 + } + + func debounceUpdateShouldRunTimer(_ value: Bool) { + updateShouldRunTimedebouncer.run { [weak self] in + guard self?.shouldRunTimer != value else { return } + self?.shouldRunTimer = value + } + } + + func debounceUpdateStoryTargetIndex(_ value: Int) { + storyTargetMovingDebouncer.run { [weak self] in + guard self?.storyTargetIndex != value else { return } + self?.storyTargetIndex = value + } + } +} diff --git a/UpstraUIKit/AmityUIKit4/AmityUIKit4/Story/Pages/ViewStory/ChildViews/ProgressBarView.swift b/UpstraUIKit/AmityUIKit4/AmityUIKit4/Story/Pages/ViewStory/ChildViews/ProgressBarView.swift index 57512e1..ba31965 100644 --- a/UpstraUIKit/AmityUIKit4/AmityUIKit4/Story/Pages/ViewStory/ChildViews/ProgressBarView.swift +++ b/UpstraUIKit/AmityUIKit4/AmityUIKit4/Story/Pages/ViewStory/ChildViews/ProgressBarView.swift @@ -19,18 +19,26 @@ struct ProgressBarView: View { HStack(spacing: spacing) { ForEach(0..<progressBarViewModel.progressArray.count, id: \.self) { index in AmityProgressBarElement(pageId: pageId, progressBarViewModel: progressBarViewModel.progressArray[index]) - } - .onAppear { - Log.add(event: .info, "ProgressBar Stack Appeared") + .onAppear { + updateSegmentFullProgress(geometry) + } + .onChange(of: progressBarViewModel.progressArray.count) { _ in + updateSegmentFullProgress(geometry) + } } } } } + + private func updateSegmentFullProgress(_ geometry: GeometryProxy) { + progressBarViewModel.segmentFullProgress = (geometry.size.width - (3.0 * CGFloat(progressBarViewModel.progressArray.count))) / CGFloat(progressBarViewModel.progressArray.count) + } } class ProgressBarViewModel: ObservableObject { @Published var progressArray: [AmityProgressBarElementViewModel] + fileprivate(set) var segmentFullProgress: CGFloat = 0.0 init(progressArray: [AmityProgressBarElementViewModel]) { self.progressArray = progressArray diff --git a/UpstraUIKit/AmityUIKit4/AmityUIKit4/Story/Pages/ViewStory/ChildViews/StoryAdView.swift b/UpstraUIKit/AmityUIKit4/AmityUIKit4/Story/Pages/ViewStory/ChildViews/StoryAdView.swift index 089b4d2..d91baba 100644 --- a/UpstraUIKit/AmityUIKit4/AmityUIKit4/Story/Pages/ViewStory/ChildViews/StoryAdView.swift +++ b/UpstraUIKit/AmityUIKit4/AmityUIKit4/Story/Pages/ViewStory/ChildViews/StoryAdView.swift @@ -10,7 +10,7 @@ import SwiftUI struct StoryAdView<Content: View>: View { @EnvironmentObject private var viewConfig: AmityViewConfigController - @EnvironmentObject var storyPageViewModel: AmityStoryPageViewModel + @EnvironmentObject var storyPageViewModel: AmityViewStoryPageViewModel @EnvironmentObject var storyCoreViewModel: StoryCoreViewModel @State private var showAdInfo: Bool = false let ad: AmityAd @@ -131,10 +131,10 @@ struct StoryAdView<Content: View>: View { .sheet(isPresented: $showAdInfo, content: { AmityAdInfoView(advertiserName: ad.advertiser?.companyName ?? "-") }) - .onChange(of: showAdInfo) { isShown in - storyPageViewModel.shouldRunTimer = !isShown - storyCoreViewModel.playVideo = !isShown - } +// .onChange(of: showAdInfo) { isShown in +// storyPageViewModel.shouldRunTimer = !isShown +// storyCoreViewModel.playVideo = !isShown +// } .onAppear { ad.analytics.markAsSeen(placement: .story) } diff --git a/UpstraUIKit/AmityUIKit4/AmityUIKit4/Story/Pages/ViewStory/ChildViews/StoryCoreView.swift b/UpstraUIKit/AmityUIKit4/AmityUIKit4/Story/Pages/ViewStory/ChildViews/StoryCoreView.swift index 5db6546..0e6dceb 100644 --- a/UpstraUIKit/AmityUIKit4/AmityUIKit4/Story/Pages/ViewStory/ChildViews/StoryCoreView.swift +++ b/UpstraUIKit/AmityUIKit4/AmityUIKit4/Story/Pages/ViewStory/ChildViews/StoryCoreView.swift @@ -6,9 +6,9 @@ // import SwiftUI -import AVKit import AmitySDK import Combine +import AVKit struct StoryCoreView: View, AmityViewIdentifiable { var viewStoryPage: AmityViewStoryPage @@ -18,84 +18,184 @@ struct StoryCoreView: View, AmityViewIdentifiable { @EnvironmentObject var host: AmitySwiftUIHostWrapper @ObservedObject var storyTarget: AmityStoryTargetModel - @EnvironmentObject var storyPageViewModel: AmityStoryPageViewModel - @EnvironmentObject var storyCoreViewModel: StoryCoreViewModel - - @Binding var storySegmentIndex: Int - @Binding var totalDuration: CGFloat + @ObservedObject var storyPageViewModel: AmityViewStoryPageViewModel + @StateObject private var viewModel: StoryCoreViewModel + @State private var muteVideo: Bool = false @State private var showRetryAlert: Bool = false @State private var showCommentTray: Bool = false - + @State private var showBottomSheet: Bool = false + @State private var isAlertShown: Bool = false + @StateObject private var page = Page.first() @State private var hasStoryManagePermission: Bool = false - - var moveStoryTarget: ((MoveDirection) -> Void)? - var moveStorySegment: ((MoveDirection) -> Void)? - - @State private var page: Page = Page.first() @EnvironmentObject private var viewConfig: AmityViewConfigController - init(_ viewStoryPage: AmityViewStoryPage, storyTarget: AmityStoryTargetModel, - storySegmentIndex: Binding<Int>, - totalDuration: Binding<CGFloat>, - moveStoryTarget: ((MoveDirection) -> Void)? = nil, - moveStorySegment: ((MoveDirection) -> Void)? = nil) { + private var isActiveTarget: Bool { + storyPageViewModel.activeStoryTarget == storyTarget + } + + init(_ viewStoryPage: AmityViewStoryPage, + storyPageViewModel: AmityViewStoryPageViewModel, + storyTarget: AmityStoryTargetModel) { self.viewStoryPage = viewStoryPage self.storyTarget = storyTarget - self._storySegmentIndex = storySegmentIndex - self._totalDuration = totalDuration self.targetName = storyTarget.targetName self.avatar = storyTarget.avatar self.isVerified = storyTarget.isVerifiedTarget - self.moveStoryTarget = moveStoryTarget - self.moveStorySegment = moveStorySegment + + self.storyPageViewModel = storyPageViewModel + self._viewModel = StateObject(wrappedValue: StoryCoreViewModel(storyTarget, storyPageViewModel: storyPageViewModel)) } var body: some View { - Pager(page: page, data: storyTarget.items, id: \.id) { item in - switch item.type { - case .ad(let ad): - StoryAdView(ad: ad, gestureView: getGestureView) - case .content(let storyModel): - getStoryView(storyModel) + ZStack(alignment: .top) { + Pager(page: page, data: storyTarget.items, id: \.id) { item in + getContentView(item) + .environmentObject(storyPageViewModel) + .environmentObject(viewModel) + } + .contentLoadingPolicy(.lazy(recyclingRatio: 1)) + .disableDragging() + .onPageChanged { segmentIndex in + viewModel.storySegmentIndex = segmentIndex + guard isActiveTarget else { return } + + viewModel.playIfVideoStory() + + // Mark Seen + if let item = storyTarget.items.element(at: viewModel.storySegmentIndex), case .content(let storyModel) = item.type { + storyPageViewModel.markAsSeen(storyModel) + } } + .onChange(of: viewModel.storySegmentIndex) { segmentIndex in + guard page.index != segmentIndex else { return } + page.update(.new(index: segmentIndex)) + } + + ProgressBarView(pageId: .storyPage, progressBarViewModel: viewModel.progressBarViewModel) + .frame(height: 3) + .padding(EdgeInsets(top: 16, leading: 20, bottom: 10, trailing: 20)) + .onReceive(storyTarget.$itemCount) { count in + guard count != viewModel.progressBarViewModel.progressArray.count else { return } + viewModel.createProgressBarViewModelProgressArray(count) + + DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) { + viewModel.updateProgressBarViewModelProgressArray(viewModel.storySegmentIndex) + } + } + .isHidden(viewConfig.isHidden(elementId: .progressBarElement), remove: false) } - .contentLoadingPolicy(.lazy(recyclingRatio: 1)) - .overlay ( - ProgressView().progressViewStyle(CircularProgressViewStyle(tint: .white)) - .offset(y: -40) - .isHidden(storyTarget.items.count != 0, remove: false) - ) - .onAppear { - page.update(.new(index: storySegmentIndex)) + .onChange(of: storyPageViewModel.storyTargetIndex) { targetIndex in + viewModel.updateProgressBarViewModelProgressArray(viewModel.storySegmentIndex) + viewModel.stopVideo() + guard isActiveTarget else { return } + viewModel.storyTarget.fetchStory(totalSeenStoryCount: storyPageViewModel.seenStoryCount) + viewModel.playIfVideoStory() + + // Mark Seen + if let item = storyTarget.items.element(at: viewModel.storySegmentIndex), case .content(let storyModel) = item.type { + storyPageViewModel.markAsSeen(storyModel) + } + } + .onReceive(storyTarget.$unseenStoryIndex) { index in + viewModel.moveToUnseenStory(index) + } + .onReceive(storyPageViewModel.timer, perform: { _ in + guard storyPageViewModel.shouldRunTimer, isActiveTarget else { return } + guard storyTarget.storyLoadingStatus == .loaded || !storyTarget.items.isEmpty else { return } + guard viewModel.isLoadedIfVideoStory() else { return } + + viewModel.timerAction(host) + }) + .onChange(of: storyPageViewModel.shouldRunTimer) { shouldRunTimer in + guard isActiveTarget else { return } + viewModel.playPauseVideo(shouldRunTimer) + } + .onAppear { + storyTarget.fetchStory() + checkStoryPermission() + Log.add(event: .info, "StoryTarget: \(storyTarget.targetName) appeared!!!") // Handle the case going back from Community Page host.controller?.navigationController?.navigationBar.isHidden = true } - .onChange(of: storySegmentIndex) { value in - page.update(.new(index: value)) + .onDisappear { + Log.add(event: .info, "StoryTarget: index \(storyTarget.targetName) disappeared!!!") + } + .onChange(of: showBottomSheet) { value in + storyPageViewModel.debounceUpdateShouldRunTimer(!value) + } + .onChange(of: isAlertShown) { value in + storyPageViewModel.debounceUpdateShouldRunTimer(!value) } .onChange(of: showRetryAlert) { value in - storyPageViewModel.shouldRunTimer = !value - storyCoreViewModel.playVideo = !value + storyPageViewModel.debounceUpdateShouldRunTimer(!value) } .onChange(of: showCommentTray) { value in - storyPageViewModel.shouldRunTimer = !value - storyCoreViewModel.playVideo = !value + storyPageViewModel.debounceUpdateShouldRunTimer(!value) if value == false { hideKeyboard() } } + .padding(.bottom, 25) .sheet(isPresented: $showCommentTray) { getCommentSheetView() } - .gesture(DragGesture().onChanged{ _ in}) + .bottomSheet(isShowing: $showBottomSheet, height: .contentSize, sheetContent: { + getBottomSheetView() + }) .environmentObject(viewConfig) .environmentObject(host) .animation(nil) } + @ViewBuilder + func getContentView(_ item: PaginatedItem<AmityStoryModel>) -> some View { + GeometryReader { geometry in + ZStack(alignment: .topTrailing) { + + switch item.type { + case .ad(let ad): + StoryAdView(ad: ad, gestureView: getGestureView) + case .content(let storyModel): + getStoryView(storyModel) + } + + HStack(spacing: 0) { + /// Show overflow menu if item is the story + /// Hide it if item is ads + if case .content(_) = item.type { + Button { + showBottomSheet.toggle() + } label: { + let icon = AmityIcon.getImageResource(named: viewConfig.getConfig(elementId: .overflowMenuElement, key: "overflow_menu_icon", of: String.self) ?? "") + Image(icon) + .frame(width: 24, height: 20) + .padding(.trailing, 20) + } + .isHidden(!hasStoryManagePermission, remove: false) + .accessibilityIdentifier(AccessibilityID.Story.AmityViewStoryPage.meatballsButton) + .isHidden(viewConfig.isHidden(elementId: .overflowMenuElement), remove: false) + } + + Button { + Log.add(event: .info, "Tapped Closed!!!") + host.controller?.dismiss(animated: true) + } label: { + let icon = AmityIcon.getImageResource(named: viewConfig.getConfig(elementId: .closeButtonElement, key: "close_icon", of: String.self) ?? "") + Image(icon) + .frame(width: 24, height: 18) + .padding(.trailing, 25) + } + .accessibilityIdentifier(AccessibilityID.Story.AmityViewStoryPage.closeButton) + .isHidden(viewConfig.isHidden(elementId: .closeButtonElement), remove: false) + } + .padding(.top, 40) + } + } + } + func getStoryView(_ storyModel: AmityStoryModel) -> some View { VStack(spacing: 0) { @@ -103,18 +203,36 @@ struct StoryCoreView: View, AmityViewIdentifiable { GeometryReader { geometry in if let imageURL = storyModel.imageURL { StoryImageView(imageURL: imageURL, - totalDuration: $totalDuration, - displayMode: storyModel.imageDisplayMode, - size: geometry.size) + displayMode: storyModel.imageDisplayMode, + size: geometry.size, + onLoading: { + guard isActiveTarget else { return } + storyPageViewModel.shouldRunTimer = false + storyPageViewModel.showActivityIndicator = true + }, onLoaded: { + guard isActiveTarget else { return } + storyPageViewModel.shouldRunTimer = true + storyPageViewModel.showActivityIndicator = false + }) .frame(width: geometry.size.width, height: geometry.size.height) .overlay( storyModel.syncState == .error || storyModel.syncState == .syncing ? Color.black.opacity(0.5) : nil ) } else if let videoURL = storyModel.videoURL { StoryVideoView(videoURL: videoURL, - totalDuration: $totalDuration, - muteVideo: $muteVideo, - playVideo: $storyCoreViewModel.playVideo) + muteVideo: $muteVideo, + time: $viewModel.playTime, + playVideo: $viewModel.playVideo, onLoading: { + guard isActiveTarget else { return } + viewModel.isVideoLoading = true + storyPageViewModel.showActivityIndicator = true + }, onPlaying: { videoDuration in + guard isActiveTarget else { return } + viewModel.isVideoLoading = false + storyPageViewModel.shouldRunTimer = true + storyPageViewModel.showActivityIndicator = false + storyPageViewModel.totalDuration = videoDuration + }) .frame(width: geometry.size.width, height: geometry.size.height) .overlay( storyModel.syncState == .error || storyModel.syncState == .syncing ? Color.black.opacity(0.5) : nil @@ -142,9 +260,7 @@ struct StoryCoreView: View, AmityViewIdentifiable { getHyperLinkView(data: model, story: storyModel) } } - .offset(y: 30) // height + padding top, bottom of progressBarView - - + .padding(.top, 30) } if storyModel.syncState == .error { @@ -156,16 +272,7 @@ struct StoryCoreView: View, AmityViewIdentifiable { getAnalyticView(storyModel) } } - .onAppear { - storyPageViewModel.markAsSeen(storyModel) - - switch storyModel.storyType { - case .image: - storyCoreViewModel.playVideo = false - case .video: - storyCoreViewModel.playVideo = true - } - } + .environmentObject(viewModel) } @@ -190,11 +297,6 @@ struct StoryCoreView: View, AmityViewIdentifiable { AmityUIKit4Manager.behaviour.viewStoryPageBehaviour?.goToCreateStoryPage(context: context) } } - .onAppear { - Task { - hasStoryManagePermission = await StoryPermissionChecker.checkUserHasManagePermission(communityId: story.targetId) - } - } VStack(alignment: .leading) { HStack(alignment: .center) { @@ -283,22 +385,20 @@ struct StoryCoreView: View, AmityViewIdentifiable { func getGestureView() -> some View { GestureView(onLeftTap: { - moveStorySegment?(.backward) + viewModel.moveStorySegment(direction: .backward, host) }, onRightTap: { - moveStorySegment?(.forward) + viewModel.moveStorySegment(direction: .forward, host) }, onTouchAndHoldStart: { storyPageViewModel.shouldRunTimer = false - storyCoreViewModel.playVideo = false }, onTouchAndHoldEnd: { storyPageViewModel.shouldRunTimer = true - storyCoreViewModel.playVideo = true - },onDragChanged: { direction, translation in + }, onDragChanged: { direction, translation in guard let view = host.controller?.view else { return } + guard direction == .downward || direction == .upward else { return } if translation.y < 0 || (translation.y > 0 && translation.y <= 50) { return } - storyPageViewModel.shouldRunTimer = false - storyCoreViewModel.playVideo = false + storyPageViewModel.debounceUpdateShouldRunTimer(false) UIView.animate(withDuration: 0.5, delay: 0, usingSpringWithDamping: 0.6, initialSpringVelocity: 1, options: .curveEaseInOut, animations: { view.transform = CGAffineTransform(translationX: 0, y: translation.y) @@ -306,13 +406,13 @@ struct StoryCoreView: View, AmityViewIdentifiable { }, onDragEnded: { direction, translation in guard let view = host.controller?.view else { return } + guard direction == .downward || direction == .upward else { return } - storyPageViewModel.shouldRunTimer = true - storyCoreViewModel.playVideo = true + storyPageViewModel.debounceUpdateShouldRunTimer(true) if translation.y <= 200 { UIView.animate(withDuration: 0.5, delay: 0, usingSpringWithDamping: 0.6, initialSpringVelocity: 1, options: .curveEaseInOut, animations: { - if view.transform == .identity && direction == .upward { + if view.transform == .identity && direction == .upward && translation.y < -100 { showCommentTray = true } @@ -321,7 +421,7 @@ struct StoryCoreView: View, AmityViewIdentifiable { } else { host.controller?.dismiss(animated: true) } - }) + }) } @@ -401,9 +501,9 @@ struct StoryCoreView: View, AmityViewIdentifiable { Task { if story.isLiked { - try await storyPageViewModel.removeReaction(storyId: story.storyId) + try await viewModel.removeReaction(storyId: story.storyId) } else { - try await storyPageViewModel.addReaction(storyId: story.storyId) + try await viewModel.addReaction(storyId: story.storyId) } } } @@ -432,7 +532,7 @@ struct StoryCoreView: View, AmityViewIdentifiable { let retryAction = UIAlertAction(title: AmityLocalizedStringSet.General.retry.localizedString, style: .default, handler: {_ in Task { do { - try await storyCoreViewModel.storyManager.deleteStory(storyId: storyModel.storyId) + try await viewModel.storyManager.deleteStory(storyId: storyModel.storyId) switch storyModel.storyType { case .image: @@ -440,7 +540,7 @@ struct StoryCoreView: View, AmityViewIdentifiable { let targetType: AmityStoryTargetType = AmityStoryTargetType(rawValue: storyTarget.targetType) ?? .community let createOptions = AmityImageStoryCreateOptions(targetType: targetType, tartgetId: storyTarget.targetId, imageFileURL: url, items: storyModel.storyItems) - try await storyCoreViewModel.storyManager.createImageStory(in: storyTarget.targetId, createOption: createOptions) + try await viewModel.storyManager.createImageStory(in: storyTarget.targetId, createOption: createOptions) } case .video: @@ -448,7 +548,7 @@ struct StoryCoreView: View, AmityViewIdentifiable { let targetType: AmityStoryTargetType = AmityStoryTargetType(rawValue: storyTarget.targetType) ?? .community let createOptions = AmityVideoStoryCreateOptions(targetType: targetType, tartgetId: storyTarget.targetId, videoFileURL: url, items: storyModel.storyItems) - try await storyCoreViewModel.storyManager.createVideoStory(in: storyTarget.targetId, createOption: createOptions) + try await viewModel.storyManager.createVideoStory(in: storyTarget.targetId, createOption: createOptions) } } @@ -468,7 +568,7 @@ struct StoryCoreView: View, AmityViewIdentifiable { let deleteAction = UIAlertAction(title: AmityLocalizedStringSet.General.delete.localizedString, style: .destructive) { _ in Task { - try await storyCoreViewModel.storyManager.deleteStory(storyId: storyModel.storyId) + try await viewModel.storyManager.deleteStory(storyId: storyModel.storyId) } showRetryAlert.toggle() } @@ -527,7 +627,7 @@ struct StoryCoreView: View, AmityViewIdentifiable { @ViewBuilder func getCommentSheetContentView() -> some View { - if let item = storyTarget.items.element(at: storySegmentIndex), + if let item = storyTarget.items.element(at: viewModel.storySegmentIndex), case let .content(story) = item.type { let isCommunityMember = story.storyTarget?.community?.isJoined ?? true let allowCreateComment = story.storyTarget?.community?.storySettings.allowComment ?? false @@ -539,114 +639,57 @@ struct StoryCoreView: View, AmityViewIdentifiable { shouldAllowCreation: allowCreateComment) } } -} - - -class StoryCoreViewModel: ObservableObject { - - @Published var playVideo: Bool = true - let storyManager = StoryManager() - - init() {} -} - -struct StoryImageView: View { - - @EnvironmentObject var storyPageViewModel: AmityStoryPageViewModel - - private let imageURL: URL - private let displayMode: ContentMode - private let size: CGSize - @Binding private var totalDuration: CGFloat - init(imageURL: URL, totalDuration: Binding<CGFloat>, displayMode: ContentMode, size: CGSize) { - self.imageURL = imageURL - self._totalDuration = totalDuration - self.displayMode = displayMode - self.size = size - } - var body: some View { - URLImage(imageURL) { progress in - ProgressView().progressViewStyle(CircularProgressViewStyle(tint: .white)) - .onAppear { - storyPageViewModel.shouldRunTimer = false - } - - } content: { image, imageInfo in - image - .resizable() - .aspectRatio(contentMode: displayMode) - .frame(width: size.width, height: size.height) - .background( - LinearGradient( - gradient: Gradient(colors: UIImage(cgImage: imageInfo.cgImage).averageGradientColor ?? [.black]), - startPoint: .topLeading, - endPoint: .bottomTrailing - ) - ) - .onAppear { - storyPageViewModel.shouldRunTimer = true + @ViewBuilder + private func getBottomSheetView() -> some View { + VStack(spacing: 0) { + BottomSheetItemView(icon: AmityIcon.trashBinIcon.getImageResource(), text: "Delete story", isDestructive: true) + .onTapGesture { + let alertController = UIAlertController(title: AmityLocalizedStringSet.Story.deleteStoryTitle.localizedString, message: AmityLocalizedStringSet.Story.deleteStoryMessage.localizedString, preferredStyle: .alert) + + let cancelAction = UIAlertAction(title: AmityLocalizedStringSet.General.cancel.localizedString, style: .cancel) { _ in + isAlertShown.toggle() + } + + let deleteAction = UIAlertAction(title: AmityLocalizedStringSet.General.delete.localizedString, style: .destructive) { _ in + isAlertShown.toggle() + if let item = storyTarget.items.element(at: viewModel.storySegmentIndex), + case let .content(story) = item.type { + Task { @MainActor in + try await viewModel.deleteStory(storyId: story.storyId, host) + Toast.showToast(style: .success, message: "Successfully deleted the story.") + } + } + } + + alertController.addAction(cancelAction) + alertController.addAction(deleteAction) + + showBottomSheet.toggle() + isAlertShown.toggle() + + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { + host.controller?.present(alertController, animated: true) + } } - .accessibilityIdentifier(AccessibilityID.Story.AmityViewStoryPage.storyImageView) - } - .environment(\.urlImageOptions, URLImageOptions.amityOptions) - .onAppear { - totalDuration = STORY_DURATION - Log.add(event: .info, "Story TotalDuration: \(totalDuration)") } + .padding(.bottom, 32) } -} - -struct StoryVideoView: View { - - @EnvironmentObject var storyPageViewModel: AmityStoryPageViewModel - @EnvironmentObject var storyCoreViewModel: StoryCoreViewModel - - private let videoURL: URL - @Binding var totalDuration: CGFloat - @Binding var muteVideo: Bool - @Binding var playVideo: Bool - - @State private var showActivityIndicator: Bool = false - @State private var time: CMTime = .zero - init(videoURL: URL, totalDuration: Binding<CGFloat>, muteVideo: Binding<Bool>, playVideo: Binding<Bool>) { - self.videoURL = videoURL - self._totalDuration = totalDuration - self._muteVideo = muteVideo - self._playVideo = playVideo - } - - var body: some View { - VideoPlayer(url: videoURL, play: $playVideo, time: $time) - .autoReplay(false) - .mute(muteVideo) - .contentMode(.scaleAspectFit) - .onStateChanged({ state in - switch state { - case .loading: - storyPageViewModel.shouldRunTimer = false - showActivityIndicator = true - case .playing(totalDuration: let totalDuration): - storyPageViewModel.shouldRunTimer = true - self.totalDuration = totalDuration - showActivityIndicator = false - Log.add(event: .info, "Story TotalDuration: \(totalDuration)") - case .paused(playProgress: _, bufferProgress: _): break - case .error(_): break - - } - }) - .overlay( - ActivityIndicatorView(isAnimating: $showActivityIndicator, style: .medium) - ) - .onAppear { - time = .zero - storyPageViewModel.shouldRunTimer = true - } - .onDisappear { + private func checkStoryPermission() { + // Check StoryManage Permission + Task { + let storyTargetId = storyTarget.targetId + let hasPermission = await StoryPermissionChecker.checkUserHasManagePermission(communityId: storyTargetId) + let allowAllUserCreation = AmityUIKitManagerInternal.shared.client.getSocialSettings()?.story?.allowAllUserToCreateStory ?? false + + guard let community = storyTarget.storyTarget?.community else { + hasStoryManagePermission = false + return } - .accessibilityIdentifier(AccessibilityID.Story.AmityViewStoryPage.storyVideoView) + + hasStoryManagePermission = (allowAllUserCreation || hasPermission) && community.isJoined + } } } diff --git a/UpstraUIKit/AmityUIKit4/AmityUIKit4/Story/Pages/ViewStory/ChildViews/StoryCoreViewModel.swift b/UpstraUIKit/AmityUIKit4/AmityUIKit4/Story/Pages/ViewStory/ChildViews/StoryCoreViewModel.swift new file mode 100644 index 0000000..dc369c0 --- /dev/null +++ b/UpstraUIKit/AmityUIKit4/AmityUIKit4/Story/Pages/ViewStory/ChildViews/StoryCoreViewModel.swift @@ -0,0 +1,190 @@ +// +// StoryCoreViewModel.swift +// AmityUIKit4 +// +// Created by Zay Yar Htun on 9/3/24. +// + +import SwiftUI +import AVKit +import Combine +import AmitySDK + +enum MoveDirection { + case forward, backward +} + +class StoryCoreViewModel: ObservableObject, Equatable { + + static func == (lhs: StoryCoreViewModel, rhs: StoryCoreViewModel) -> Bool { + lhs.storyTarget.targetId == rhs.storyTarget.targetId + } + + @Published var storySegmentIndex: Int = 0 + @Published var playVideo: Bool = false + @Published var playTime: CMTime = .zero + @Published var isVideoLoading: Bool = false + @Published var progressBarViewModel = ProgressBarViewModel(progressArray: []) + + weak var storyPageViewModel: AmityViewStoryPageViewModel! + weak var storyTarget: AmityStoryTargetModel! + + let storyManager = StoryManager() + private var cancellable: AnyCancellable? + private let debouncer = Debouncer(delay: 0.01) + + init(_ storyTarget: AmityStoryTargetModel, storyPageViewModel: AmityViewStoryPageViewModel) { + self.storyTarget = storyTarget + self.storyPageViewModel = storyPageViewModel + } + + func playPauseVideo(_ play: Bool) { + if let item = storyTarget.items.element(at: storySegmentIndex), + case let .content(storyModel) = item.type, + case .video = storyModel.storyType { + playVideo = play + } + } + + func stopVideo() { + playVideo = false + playTime = .zero + } + + func playIfVideoStory() { + if let item = storyTarget.items.element(at: storySegmentIndex), + case let .content(storyModel) = item.type, + case .video = storyModel.storyType { + playTime = .zero + playVideo = true + } else { + playVideo = false + playTime = .zero + storyPageViewModel.totalDuration = STORY_DURATION + } + } + + func isLoadedIfVideoStory() -> Bool { + if let item = storyTarget.items.element(at: storySegmentIndex), + case let .content(storyModel) = item.type, + case .video = storyModel.storyType { + return !isVideoLoading + } else { + return true + } + } + + func moveStorySegment(direction: MoveDirection, _ host: AmitySwiftUIHostWrapper) { + guard 0..<progressBarViewModel.progressArray.count ~= storySegmentIndex else { + return + } + + switch direction { + case .forward: + progressBarViewModel.progressArray[storySegmentIndex].progress = progressBarViewModel.segmentFullProgress + + let lastSegmentIndex = progressBarViewModel.progressArray.count - 1 + let lastTargetIndex = storyPageViewModel.storyTargets.count - 1 + + if storySegmentIndex != lastSegmentIndex { + storySegmentIndex += 1 + } else if storySegmentIndex == lastSegmentIndex && storyPageViewModel.storyTargetIndex != lastTargetIndex { + storySegmentIndex = 0 + storyPageViewModel.storyTargetIndex += 1 + } + else if storyPageViewModel.storyTargetIndex == lastTargetIndex { + host.controller?.dismiss(animated: true) + } + + case .backward: + progressBarViewModel.progressArray[storySegmentIndex].progress = 0 + + if storySegmentIndex >= 1 { + storySegmentIndex -= 1 + progressBarViewModel.progressArray[storySegmentIndex].progress = 0 + } else if storyPageViewModel.storyTargetIndex >= 1 { + updateProgressBarViewModelProgressArray(0) + storyPageViewModel.storyTargetIndex -= 1 + } + } + } + + func timerAction(_ host: AmitySwiftUIHostWrapper) { + guard 0..<progressBarViewModel.progressArray.count ~= storySegmentIndex else { + return + } + + let lastSegmentIndex = progressBarViewModel.progressArray.count - 1 + let lastTargetIndex = storyPageViewModel.storyTargets.count - 1 + + if progressBarViewModel.progressArray[storySegmentIndex].progress == progressBarViewModel.segmentFullProgress { + if storySegmentIndex != lastSegmentIndex { + storySegmentIndex += 1 + } else if storyPageViewModel.storyTargetIndex != lastTargetIndex { + storySegmentIndex = 0 + updateProgressBarViewModelProgressArray(0) + storyPageViewModel.storyTargetIndex += 1 + } else if storyPageViewModel.storyTargetIndex == lastTargetIndex { + host.controller?.dismiss(animated: true) + } + } else { + let progressValueToIncrease = progressBarViewModel.segmentFullProgress / CGFloat(storyPageViewModel.totalDuration / 0.001) + let oldProgress = progressBarViewModel.progressArray[storySegmentIndex].progress + let newProgress = oldProgress + progressValueToIncrease + progressBarViewModel.progressArray[storySegmentIndex].progress = min(max(oldProgress, newProgress), progressBarViewModel.segmentFullProgress) + } + } + + func createProgressBarViewModelProgressArray(_ segmentCount: Int) { + progressBarViewModel.progressArray = (0..<segmentCount).map({ index in + return AmityProgressBarElementViewModel() + }) + } + + func updateProgressBarViewModelProgressArray(_ currentIndex: Int) { + progressBarViewModel.progressArray.enumerated() + .forEach { index, model in + if index < currentIndex { + model.progress = progressBarViewModel.segmentFullProgress + } else { + model.progress = 0.0 + } + } + } + + func moveToUnseenStory(_ index: Int) { + debouncer.run { [weak self] in + guard self?.storySegmentIndex != index else { return } + self?.updateProgressBarViewModelProgressArray(index) + self?.storySegmentIndex = index + } + } + + @MainActor + func deleteStory(storyId: String, _ host: AmitySwiftUIHostWrapper) async throws { + let isLastStory = storyTarget.itemCount == 1 + let isLastTarget = storyPageViewModel.storyTargets.count == 1 + try await storyManager.deleteStory(storyId: storyId) + + if isLastStory && !isLastTarget { + storyPageViewModel.storyTargets.remove(at: storyPageViewModel.storyTargetIndex) + } else if isLastStory && isLastTarget { + host.controller?.dismiss(animated: true) + } + + moveStorySegment(direction: .backward, host) + } + + @MainActor + @discardableResult + func addReaction(storyId: String) async throws -> Bool { + try await storyManager.addReaction(storyId: storyId) + } + + @MainActor + @discardableResult + func removeReaction(storyId: String) async throws -> Bool { + try await storyManager.removeReaction(storyId: storyId) + } + +} diff --git a/UpstraUIKit/AmityUIKit4/AmityUIKit4/Story/Pages/ViewStory/ChildViews/StoryImageView.swift b/UpstraUIKit/AmityUIKit4/AmityUIKit4/Story/Pages/ViewStory/ChildViews/StoryImageView.swift new file mode 100644 index 0000000..cd206b3 --- /dev/null +++ b/UpstraUIKit/AmityUIKit4/AmityUIKit4/Story/Pages/ViewStory/ChildViews/StoryImageView.swift @@ -0,0 +1,51 @@ +// +// StoryImageView.swift +// AmityUIKit4 +// +// Created by Zay Yar Htun on 9/3/24. +// + +import SwiftUI + +struct StoryImageView: View { + private let imageURL: URL + private let displayMode: ContentMode + private let size: CGSize + private let onLoading: () -> Void + private let onLoaded: () -> Void + + init(imageURL: URL, displayMode: ContentMode, size: CGSize, onLoading: @escaping () -> Void, onLoaded: @escaping () -> Void) { + self.imageURL = imageURL + self.displayMode = displayMode + self.size = size + self.onLoading = onLoading + self.onLoaded = onLoaded + } + + var body: some View { + URLImage(imageURL) { progress in + Color.clear + .onAppear { + onLoading() + } + + } content: { image, imageInfo in + image + .resizable() + .aspectRatio(contentMode: displayMode) + .frame(width: size.width, height: size.height) + .background( + LinearGradient( + gradient: Gradient(colors: UIImage(cgImage: imageInfo.cgImage).averageGradientColor ?? [.black]), + startPoint: .topLeading, + endPoint: .bottomTrailing + ) + ) + .onAppear { + onLoaded() + } + .accessibilityIdentifier(AccessibilityID.Story.AmityViewStoryPage.storyImageView) + } + .environment(\.urlImageOptions, URLImageOptions.amityOptions) + } +} diff --git a/UpstraUIKit/AmityUIKit4/AmityUIKit4/Story/Pages/ViewStory/ChildViews/StoryVideoView.swift b/UpstraUIKit/AmityUIKit4/AmityUIKit4/Story/Pages/ViewStory/ChildViews/StoryVideoView.swift new file mode 100644 index 0000000..06ab786 --- /dev/null +++ b/UpstraUIKit/AmityUIKit4/AmityUIKit4/Story/Pages/ViewStory/ChildViews/StoryVideoView.swift @@ -0,0 +1,47 @@ +// +// StoryVideoView.swift +// AmityUIKit4 +// +// Created by Zay Yar Htun on 9/3/24. +// + +import SwiftUI +import AVKit + +struct StoryVideoView: View { + + private let videoURL: URL + @Binding private var muteVideo: Bool + @Binding private var playVideo: Bool + @Binding private var time: CMTime + private let onLoading: () -> Void + private let onPlaying: (Double) -> Void + + init(videoURL: URL, muteVideo: Binding<Bool>, time: Binding<CMTime>, playVideo: Binding<Bool>, onLoading: @escaping () -> Void, onPlaying: @escaping (Double) -> Void) { + self.videoURL = videoURL + self._muteVideo = muteVideo + self._time = time + self._playVideo = playVideo + self.onLoading = onLoading + self.onPlaying = onPlaying + } + + var body: some View { + VideoPlayer(url: videoURL, play: $playVideo, time: $time) + .autoReplay(false) + .mute(muteVideo) + .contentMode(.scaleAspectFit) + .onStateChanged({ state in + switch state { + case .loading: + onLoading() + case .playing(totalDuration: let totalDuration): + onPlaying(totalDuration) + case .paused(playProgress: _, bufferProgress: _): break + case .error(_): break + } + }) + .accessibilityIdentifier(AccessibilityID.Story.AmityViewStoryPage.storyVideoView) + } +} + diff --git a/UpstraUIKit/AmityUIKitLiveStream/AmityUIKitLiveStream.xcodeproj/project.pbxproj b/UpstraUIKit/AmityUIKitLiveStream/AmityUIKitLiveStream.xcodeproj/project.pbxproj index 9d30013..dd1be74 100644 --- a/UpstraUIKit/AmityUIKitLiveStream/AmityUIKitLiveStream.xcodeproj/project.pbxproj +++ b/UpstraUIKit/AmityUIKitLiveStream/AmityUIKitLiveStream.xcodeproj/project.pbxproj @@ -7,6 +7,7 @@ objects = { /* Begin PBXBuildFile section */ + 682E021A2CF0AE6900FE1042 /* SharedFrameworks in Frameworks */ = {isa = PBXBuildFile; productRef = 682E02192CF0AE6900FE1042 /* SharedFrameworks */; }; 92952FDF2CC0E78E0012B2B9 /* LivestreamPlayerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 92952FDE2CC0E78E0012B2B9 /* LivestreamPlayerView.swift */; }; 92952FE52CC5B1F90012B2B9 /* RecordedStreamPlayerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 92952FE42CC5B1F90012B2B9 /* RecordedStreamPlayerView.swift */; }; A0B68B3026E07278007D7B5B /* LiveStreamViewController+GoLive.swift in Sources */ = {isa = PBXBuildFile; fileRef = A0B68B2F26E07278007D7B5B /* LiveStreamViewController+GoLive.swift */; }; @@ -31,7 +32,6 @@ 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 */; }; - ED52321B2CDE391700ABA50D /* SharedFrameworks in Frameworks */ = {isa = PBXBuildFile; productRef = ED52321A2CDE391700ABA50D /* SharedFrameworks */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ @@ -73,7 +73,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - ED52321B2CDE391700ABA50D /* SharedFrameworks in Frameworks */, + 682E021A2CF0AE6900FE1042 /* SharedFrameworks in Frameworks */, A0BD0B3426DDD9820054088B /* AmityUIKit.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -251,7 +251,7 @@ ); name = AmityUIKitLiveStream; packageProductDependencies = ( - ED52321A2CDE391700ABA50D /* SharedFrameworks */, + 682E02192CF0AE6900FE1042 /* SharedFrameworks */, ); productName = AmityUIKitLiveStream; productReference = A0BD0B1526DCE4F50054088B /* AmityUIKitLiveStream.framework */; @@ -537,7 +537,7 @@ /* End XCConfigurationList section */ /* Begin XCSwiftPackageProductDependency section */ - ED52321A2CDE391700ABA50D /* SharedFrameworks */ = { + 682E02192CF0AE6900FE1042 /* SharedFrameworks */ = { isa = XCSwiftPackageProductDependency; productName = SharedFrameworks; }; diff --git a/UpstraUIKit/SampleApp/SampleApp.xcodeproj/project.pbxproj b/UpstraUIKit/SampleApp/SampleApp.xcodeproj/project.pbxproj index 73adbcd..0239349 100644 --- a/UpstraUIKit/SampleApp/SampleApp.xcodeproj/project.pbxproj +++ b/UpstraUIKit/SampleApp/SampleApp.xcodeproj/project.pbxproj @@ -22,6 +22,7 @@ 680DC0042C8AC2B2006D8764 /* UIDevice+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 680DC0022C8ABE09006D8764 /* UIDevice+Extension.swift */; }; 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, ); }; }; + 682E021C2CF0AE8100FE1042 /* SharedFrameworks in Frameworks */ = {isa = PBXBuildFile; productRef = 682E021B2CF0AE8100FE1042 /* SharedFrameworks */; }; 684097562B30607F00697E1B /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 684097552B30607F00697E1B /* GoogleService-Info.plist */; }; 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, ); }; }; @@ -98,6 +99,8 @@ 92DBE8E52ACA98CF007D873C /* AmityUIKit.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = D478D153262409E5006EA140 /* AmityUIKit.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 97F101F526A69FBF00AD84A1 /* CustomChannelEventHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 97F101F426A69FBF00AD84A1 /* CustomChannelEventHandler.swift */; }; 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, ); }; }; 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 */; }; @@ -125,7 +128,6 @@ D4AFF1CF25ECE8AD002C001C /* PostPreviewTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4AFF1CD25ECE8AD002C001C /* PostPreviewTableViewCell.swift */; }; D4AFF23225ED1E7C002C001C /* GlobalPostsFeedViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4AFF23125ED1E7C002C001C /* GlobalPostsFeedViewController.swift */; }; D4AFF23625ED1F88002C001C /* GlobalPostsDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4AFF23525ED1F88002C001C /* GlobalPostsDataSource.swift */; }; - ED52321D2CDE392A00ABA50D /* SharedFrameworks in Frameworks */ = {isa = PBXBuildFile; productRef = ED52321C2CDE392A00ABA50D /* SharedFrameworks */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -171,6 +173,7 @@ dstPath = ""; dstSubfolderSpec = 10; files = ( + A0BD0B4926DDE0E30054088B /* AmityUIKitLiveStream.framework in Embed Frameworks */, 68F5D9FF2B481E4700A9FA0D /* AmityUIKit4.framework in Embed Frameworks */, D478D16A26240A5E006EA140 /* AmityUIKit.framework in Embed Frameworks */, ); @@ -276,9 +279,10 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 682E021C2CF0AE8100FE1042 /* SharedFrameworks in Frameworks */, + A0BD0B4826DDE0E30054088B /* AmityUIKitLiveStream.framework in Frameworks */, 68F5D9FE2B481E4700A9FA0D /* AmityUIKit4.framework in Frameworks */, D478D16926240A5E006EA140 /* AmityUIKit.framework in Frameworks */, - ED52321D2CDE392A00ABA50D /* SharedFrameworks in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -601,7 +605,7 @@ ); name = SampleApp; packageProductDependencies = ( - ED52321C2CDE392A00ABA50D /* SharedFrameworks */, + 682E021B2CF0AE8100FE1042 /* SharedFrameworks */, ); productName = SampleApp; productReference = B78DA47524BED7D300EE902B /* SampleApp.app */; @@ -1284,15 +1288,15 @@ /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ + 682E021B2CF0AE8100FE1042 /* SharedFrameworks */ = { + isa = XCSwiftPackageProductDependency; + productName = SharedFrameworks; + }; 92DBE8A32ACA98CF007D873C /* FirebaseCrashlytics */ = { isa = XCSwiftPackageProductDependency; package = 92DBE8A42ACA98CF007D873C /* XCRemoteSwiftPackageReference "firebase-ios-sdk" */; productName = FirebaseCrashlytics; }; - ED52321C2CDE392A00ABA50D /* SharedFrameworks */ = { - isa = XCSwiftPackageProductDependency; - productName = SharedFrameworks; - }; /* End XCSwiftPackageProductDependency section */ }; rootObject = B78DA46D24BED7D300EE902B /* Project object */; diff --git a/UpstraUIKit/SharedFrameworks/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata b/UpstraUIKit/SharedFrameworks/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata deleted file mode 100644 index 94b2795..0000000 --- a/UpstraUIKit/SharedFrameworks/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata +++ /dev/null @@ -1,4 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> -<Workspace - version = "1.0"> -</Workspace> diff --git a/UpstraUIKit/SharedFrameworks/Package.swift b/UpstraUIKit/SharedFrameworks/Package.swift index ddb499f..765de48 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-beta28/AmitySDK.xcframework.zip", - checksum: "8efe8405ac20d5c9278cb51c46dae5f949806598de1a51e4f9d9f6358a9ac8f5" + url: "https://sdk.amity.co/sdk-release/ios-uikit-frameworks/4.0.0-beta29/AmitySDK.xcframework.zip", + checksum: "59e9996a707e3ba630e53265157795b4f486b382ccfbcd833fd3cb2574310051" ), .binaryTarget( name: "Realm", - url: "https://sdk.amity.co/sdk-release/ios-uikit-frameworks/4.0.0-beta28/Realm.xcframework.zip", - checksum: "3b49824e12e89f6a5ff13374d1f31e45b5c7748a32f7b8148a14ba84c372b586" + url: "https://sdk.amity.co/sdk-release/ios-uikit-frameworks/4.0.0-beta29/Realm.xcframework.zip", + checksum: "8c2b1e560b094f2d538f8a1ae59515a0137f34aade896bb81dca797633828c14" ), .binaryTarget( name: "RealmSwift", - url: "https://sdk.amity.co/sdk-release/ios-uikit-frameworks/4.0.0-beta28/RealmSwift.xcframework.zip", - checksum: "b14bb4032f9240dad52034f10f9796adaee4f258b53af7c9bd9e68036a392ed2" + url: "https://sdk.amity.co/sdk-release/ios-uikit-frameworks/4.0.0-beta29/RealmSwift.xcframework.zip", + checksum: "19eccbecdbed93e800539fd8f45cc1526fe9642440728b87990a8c0e4cbd2517" ), .binaryTarget( name: "AmityLiveVideoBroadcastKit", - url: "https://sdk.amity.co/sdk-release/ios-uikit-frameworks/4.0.0-beta28/AmityLiveVideoBroadcastKit.xcframework.zip", - checksum: "da25546df19250e3ad31092ca7049b7b16b59da087346b264c1a7fb2c6a25352" + url: "https://sdk.amity.co/sdk-release/ios-uikit-frameworks/4.0.0-beta29/AmityLiveVideoBroadcastKit.xcframework.zip", + checksum: "623717aa399da68759432c9e27d0cfc31374f20d90cd365252ce2f09e02fa784" ), .binaryTarget( name: "AmityVideoPlayerKit", - url: "https://sdk.amity.co/sdk-release/ios-uikit-frameworks/4.0.0-beta28/AmityVideoPlayerKit.xcframework.zip", - checksum: "f3e45aa5657504c0fc017b47edafa125f60ea738d735ddeaac50941d3b4417bb" + url: "https://sdk.amity.co/sdk-release/ios-uikit-frameworks/4.0.0-beta29/AmityVideoPlayerKit.xcframework.zip", + checksum: "94aadec8a3aeae3190d5ebab3644fa460bde87a863bcea1ab3dd62e12064695a" ), .binaryTarget( name: "MobileVLCKit", diff --git a/UpstraUIKit/UpstraUIKit/Components/AmityExpandableLabel.swift b/UpstraUIKit/UpstraUIKit/Components/AmityExpandableLabel.swift index 4103ecf..031a357 100644 --- a/UpstraUIKit/UpstraUIKit/Components/AmityExpandableLabel.swift +++ b/UpstraUIKit/UpstraUIKit/Components/AmityExpandableLabel.swift @@ -156,8 +156,8 @@ open class AmityExpandableLabel: UILabel { set(text) { if let text = text { let attributedString = NSMutableAttributedString(string: text) - let detector = try! NSDataDetector(types: NSTextCheckingResult.CheckingType.link.rawValue) - let matches = detector.matches(in: text, options: [], range: NSRange(location: 0, length: text.utf16.count)) + let detector = try? NSDataDetector(types: NSTextCheckingResult.CheckingType.link.rawValue) + let matches = detector?.matches(in: text, options: [], range: NSRange(location: 0, length: text.utf16.count)) ?? [] var _hyperLinkTextRange: [Hyperlink] = [] for match in matches { guard let textRange = Range(match.range, in: text) else { continue } @@ -606,8 +606,8 @@ extension AmityExpandableLabel { func setText(_ text: String, withAttributes attributes: [MentionAttribute]) { let attributedString = NSMutableAttributedString(string: text) - let detector = try! NSDataDetector(types: NSTextCheckingResult.CheckingType.link.rawValue) - let matches = detector.matches(in: text, options: [], range: NSRange(location: 0, length: text.utf16.count)) + let detector = try? NSDataDetector(types: NSTextCheckingResult.CheckingType.link.rawValue) + let matches = detector?.matches(in: text, options: [], range: NSRange(location: 0, length: text.utf16.count)) ?? [] var _hyperLinkTextRange: [Hyperlink] = [] for match in matches { guard let textRange = Range(match.range, in: text) else { continue }