diff --git a/CHANGELOG.md b/CHANGELOG.md index 957f0c1..503391e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,2 +1,2 @@ -### v1.0.0-beta.1 (Sep 2, 2024) -- Fixed the package's `CFBundleShortVersionString` format to comply to the required format +### v1.0.0-beta.2 (Sep 30, 2024) +- Fixed multiple navigationBars appearing issue diff --git a/Package.swift b/Package.swift index 8f81a9e..6d799d9 100644 --- a/Package.swift +++ b/Package.swift @@ -17,14 +17,14 @@ let package = Package( .package( name: "SendbirdChatSDK", url: "https://github.com/sendbird/sendbird-chat-sdk-ios", - from: "4.20.0" + from: "4.21.1" ), ], targets: [ .binaryTarget( name: "SendbirdSwiftUI", - url: "https://github.com/sendbird/sendbird-swiftui-ios/releases/download/1.0.0-beta.1/SendbirdSwiftUI.xcframework.zip", - checksum: "cd882856d77ba496eb313b16eeec35d8c981f49dcac3818194badac4a3960183" + url: "https://github.com/sendbird/sendbird-swiftui-ios/releases/download/1.0.0-beta.2/SendbirdSwiftUI.xcframework.zip", + checksum: "097139b0498cf40ad4840f62a6366cd6eea7ffc0683995c03152dd19131e9913" ), .target( diff --git a/Sample/QuickStartSwiftUI.xcodeproj/project.pbxproj b/Sample/QuickStartSwiftUI.xcodeproj/project.pbxproj index 96ef7ab..20c6fa6 100644 --- a/Sample/QuickStartSwiftUI.xcodeproj/project.pbxproj +++ b/Sample/QuickStartSwiftUI.xcodeproj/project.pbxproj @@ -52,6 +52,7 @@ 0B206B68339562497D2F44A3 /* CustomGroupChannelSettings.SubView.Builder.moderations.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3300D14C142B4740F0438811 /* CustomGroupChannelSettings.SubView.Builder.moderations.swift */; }; 0B3121A9A5C7036C3181007E /* SBUQuotedMessageViewProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = F680D037CDE9E80938B09CC6 /* SBUQuotedMessageViewProtocol.swift */; }; 0B34A5551E2DED6ADDE63CFF /* GroupChannelRegisterOperatorView+ViewConverter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3580081863865A49C61883FE /* GroupChannelRegisterOperatorView+ViewConverter.swift */; }; + 0B873482FEFADB21E16A6EEE /* SBUMessageTemplateCellLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 374258FE63226944670893DB /* SBUMessageTemplateCellLayout.swift */; }; 0B8D422DA84270DAC09CF0C7 /* CustomInviteUser.ViewConverter.List.userNameLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA1DD2E5AE504E97921165C8 /* CustomInviteUser.ViewConverter.List.userNameLabel.swift */; }; 0BA7595DAD97702571BA7916 /* SBUBaseChannelViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 304A09ECDE1B9834CD9A4E19 /* SBUBaseChannelViewModel.swift */; }; 0BB8F37564A4E3DCF2F276EE /* CustomOpenChannel.SwiftUI.View.CustomMain.swift in Sources */ = {isa = PBXBuildFile; fileRef = CF631CBCF396FA6414917B91 /* CustomOpenChannel.SwiftUI.View.CustomMain.swift */; }; @@ -130,6 +131,7 @@ 1CCC15F9D135ABB7F92046C0 /* CustomCreateGroupChannel.ViewConverter.List.entireView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5DEE6B3475E267184B7C0921 /* CustomCreateGroupChannel.ViewConverter.List.entireView.swift */; }; 1CD2122F20210ADFEBF694E5 /* CustomGroupChannelRegisterOperator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E59F203B7488D4BC92B8103 /* CustomGroupChannelRegisterOperator.swift */; }; 1CD7213BF7AE6B3C5FD68BCA /* SBUOpenMutedParticipantListUserCell+SwiftUI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4891397AAB5DCBD36AAE7D51 /* SBUOpenMutedParticipantListUserCell+SwiftUI.swift */; }; + 1CDB503F30FC2AB8FF73576F /* SBUMessageFormView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 384A0D8AB5B526308F89634E /* SBUMessageFormView.swift */; }; 1CF6CC40EECB4EC563E19158 /* SBUVoiceMessageConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33A19DC31A19B9871C014844 /* SBUVoiceMessageConfiguration.swift */; }; 1D2B28D0D92973CF4081216C /* GroupModerationsViewConverter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 472483201FA056ED9A255321 /* GroupModerationsViewConverter.swift */; }; 1D318FFF730CD58BF3E3D03C /* BaseMessage+SBUIKit.MessageTemplate.swift in Sources */ = {isa = PBXBuildFile; fileRef = F566B012103F8EEBE03AECA3 /* BaseMessage+SBUIKit.MessageTemplate.swift */; }; @@ -146,12 +148,14 @@ 1E435408BA84F9E42973B50A /* SBUInviteUserViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 16A073C3C063FECB43EE3886 /* SBUInviteUserViewController.swift */; }; 1E4E18D92843E192F76648F7 /* CustomGroupMutedMemberList.ViewConverter.List.entireView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C5266F61E9850A492752AF7 /* CustomGroupMutedMemberList.ViewConverter.List.entireView.swift */; }; 1E9CFDDE19604AF05970F40E /* SBURegisterOperatorModule.Header.swift in Sources */ = {isa = PBXBuildFile; fileRef = B461281DDE306B84C3893B7F /* SBURegisterOperatorModule.Header.swift */; }; + 1EB0F0FB0921F42151B381F8 /* SBUMessageFormChipView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 904618EB6B0A591FC15D1542 /* SBUMessageFormChipView.swift */; }; 1EBD15B2C5E7487E46E4E43A /* CustomOpenChannelList.SubView.Builder.createChannel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3689A9086F74AAAEAB2FC8F7 /* CustomOpenChannelList.SubView.Builder.createChannel.swift */; }; 1EF752D3E3CA604EB7052956 /* SBUMessageCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1C082271530831A0A97BA43 /* SBUMessageCache.swift */; }; 1F51C756D745C5DE1758F91A /* CustomTheme.ColorSet.Custom.Main.swift in Sources */ = {isa = PBXBuildFile; fileRef = 44AC2120ADDF0EB8ED8D341E /* CustomTheme.ColorSet.Custom.Main.swift */; }; 1F816F12112DFC4D2DF55764 /* CustomGroupMemberList.ViewConverter.Header.rightView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FD7B856C7B2765E3C158DCE /* CustomGroupMemberList.ViewConverter.Header.rightView.swift */; }; 1FFA61368D52D367BDBEFC5A /* InviteUserView+SubViewBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3BCF8D7F6481CF9321250DF9 /* InviteUserView+SubViewBuilder.swift */; }; 200064B087980A103CE23DDC /* CustomMessageThread.ViewConverter.Input.entireView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0F4F269E21BC1771EA562A1E /* CustomMessageThread.ViewConverter.Input.entireView.swift */; }; + 201120B35CFB5D19CCF88E89 /* MessageForm+SBUIKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 86D998504A47DE9BAAC543C1 /* MessageForm+SBUIKit.swift */; }; 2024C4E99883B72EF605CE2B /* CustomGroupChannelSettings.ViewConverter.List.moderation.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE3455B16C9555F970F4D601 /* CustomGroupChannelSettings.ViewConverter.List.moderation.swift */; }; 2057B0454F27A7A20B1373C5 /* SBUCommonViewControllerSet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 93C6EE37AC969FB9BBAF7B8E /* SBUCommonViewControllerSet.swift */; }; 209CA9D4547B30990757AAFD /* CustomGroupChannel.ViewConverter.Input.leftView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18DAC3A6A235AF92E38CC492 /* CustomGroupChannel.ViewConverter.Input.leftView.swift */; }; @@ -168,6 +172,7 @@ 2320A12AD4A24FD50A506784 /* GroupChannelListViewConverter.List.swift in Sources */ = {isa = PBXBuildFile; fileRef = F37A3C0CAC8D9964CABBB7D5 /* GroupChannelListViewConverter.List.swift */; }; 2365770F3E1395793D9121BD /* CustomOpenChannel.SubView.Builder.channelSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6A90A31A500C5E9AAE45A08C /* CustomOpenChannel.SubView.Builder.channelSettings.swift */; }; 237893FFC266BAB3578B4489 /* SBUUserMessageCellParams.swift in Sources */ = {isa = PBXBuildFile; fileRef = B05DC3A2E750EC0A12CC4393 /* SBUUserMessageCellParams.swift */; }; + 2396C41E13FDB99A2EC97888 /* SBUFormViewParams.Deprecated.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DB8F69105F07B296E8C63F7 /* SBUFormViewParams.Deprecated.swift */; }; 23C626C143D5E5872BFBAC6E /* GroupMutedMemberListViewConverter.swift in Sources */ = {isa = PBXBuildFile; fileRef = D9349402FEC3ACCC843C056E /* GroupMutedMemberListViewConverter.swift */; }; 242B18DA84070DC4F609033E /* SBUUserMessageTextViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2AB52183CDCA494948E5976E /* SBUUserMessageTextViewModel.swift */; }; 24457EA1BBCECDB69A3D7136 /* OpenBannedUserListView+ViewConverter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 051BED79FDD0D9A17F350838 /* OpenBannedUserListView+ViewConverter.swift */; }; @@ -180,6 +185,7 @@ 2598789B240EB34C5FCE28A2 /* SBUBaseMessageCellParams.swift in Sources */ = {isa = PBXBuildFile; fileRef = CB0B81E6414A56CEC58EB58E /* SBUBaseMessageCellParams.swift */; }; 25B7BDF88C9F8F4473A01931 /* SBUOpenChannelModule.Media.swift in Sources */ = {isa = PBXBuildFile; fileRef = 25858342E4D2B978B8BAD861 /* SBUOpenChannelModule.Media.swift */; }; 25C520E41FC4936B075752F8 /* SBUUnknownMessageCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1CD45E3398D54B7995E3867F /* SBUUnknownMessageCell.swift */; }; + 2637DC0A0C8F965A37AE303E /* SBUFormView.Deprecated.swift in Sources */ = {isa = PBXBuildFile; fileRef = 515A6FCA6E34F58EB62146AD /* SBUFormView.Deprecated.swift */; }; 265BA70DB6A018F1AE1D2ECE /* UIFont+Sendbird.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42FDCEFB59A8DBF122F0523E /* UIFont+Sendbird.swift */; }; 2687C154EC3EEBF0580FB234 /* CustomMessageSearch.ViewConverter.Header.leftView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F9FF61E04422CDF019FC8A14 /* CustomMessageSearch.ViewConverter.Header.leftView.swift */; }; 2730E7FEBE4EEE5FD355C129 /* BaseMesssage+SBUIKit.Deprecated.swift in Sources */ = {isa = PBXBuildFile; fileRef = 002B39B769D0499CC67311F8 /* BaseMesssage+SBUIKit.Deprecated.swift */; }; @@ -224,9 +230,11 @@ 3247A8C9C728645C9F0A65DC /* SBUModerationsViewModel.Deprecated.swift in Sources */ = {isa = PBXBuildFile; fileRef = 86392DB847368C4FDA9AEB72 /* SBUModerationsViewModel.Deprecated.swift */; }; 32BC25347884DF68E5195B63 /* CustomCreateGroupChannel.ViewConverter.List.profileImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96A2372978263A7131ECE94F /* CustomCreateGroupChannel.ViewConverter.List.profileImage.swift */; }; 3312D1908DF7F512FBB8A6C7 /* CustomGroupChannelList.ViewConverter.List.entireView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9587E1B40C8128A76DCAF7A8 /* CustomGroupChannelList.ViewConverter.List.entireView.swift */; }; + 3352E799FEA119A35F28E0ED /* SBUMessageTemplateCellParams.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E542BCE47135E2CF12B69A8 /* SBUMessageTemplateCellParams.swift */; }; 33ECE34C057447D26C83E6CA /* SBUVoiceMessageInputView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C73D7E5C13B65599FAE5D2E3 /* SBUVoiceMessageInputView.swift */; }; 341397411E718FA1877632CE /* SBUMessageThreadModule.Deprecated.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8573A563BC2E14A69A49B2C /* SBUMessageThreadModule.Deprecated.swift */; }; 34E3C948C64092A0AAABB60A /* InviteUserViewConverter.Header.swift in Sources */ = {isa = PBXBuildFile; fileRef = 72AADB71AB3EE728FC6C0993 /* InviteUserViewConverter.Header.swift */; }; + 3513E41C4A69F8983B75B7C1 /* SBUError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C15ED108FD8018112C6E8B7 /* SBUError.swift */; }; 35541402EA0341E6AC422FDC /* OpenChannelRegisterOperatorView+SubViewBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = AE5AE7AE8AFB0332BF6B91B9 /* OpenChannelRegisterOperatorView+SubViewBuilder.swift */; }; 356B573E33FFBF70AE484926 /* CustomOpenBannedUserList.ViewConverter.List.profileImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F40D34A8DAA0338D60BCC95 /* CustomOpenBannedUserList.ViewConverter.List.profileImage.swift */; }; 35A042C34CB866EFBF4ADA98 /* SBUCreateChannelViewController.Deprecated.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9AA2862AB56710CEAAEB6A77 /* SBUCreateChannelViewController.Deprecated.swift */; }; @@ -408,7 +416,6 @@ 64FAAC91380DD6A0E3D0EA92 /* SBUGroupChannelModule.List+SwiftUI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19E5681F3154A4F68F3F2C2E /* SBUGroupChannelModule.List+SwiftUI.swift */; }; 654ADA1832E52D1D4E90DBCA /* Formatter+SBUIKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3407D92E992D53C7D02B7CB3 /* Formatter+SBUIKit.swift */; }; 65958603D664C13D2AB935BB /* SBUOpenChannelBaseMessageCell+SwiftUI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 10245F542657CABD9F636A3D /* SBUOpenChannelBaseMessageCell+SwiftUI.swift */; }; - 65C75B61D994D49E029BAF6A /* SBUUserMessageCell.MessageTemplate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8FDAE9FA5128686B5ABA8011 /* SBUUserMessageCell.MessageTemplate.swift */; }; 6632A6391C5F963979F17FFD /* OpenChannelRegisterOperatorViewConverter.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB82FCDC07B9737FF5237466 /* OpenChannelRegisterOperatorViewConverter.swift */; }; 66ADA4F8CFC6EB6A77D60B1B /* SBUFontSet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74FEA7AFCAA577BB7A5935A5 /* SBUFontSet.swift */; }; 66E3D318F724C2DD14BB3995 /* SBUPaddingLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 722A8173CBC9E33AB320794E /* SBUPaddingLabel.swift */; }; @@ -440,8 +447,10 @@ 6CF60D9B84F4C8F2CDBDC94E /* SBUBaseMessageCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66E8A3A8B613A74979FB0FD2 /* SBUBaseMessageCell.swift */; }; 6D3AB85EAF69007A452A9355 /* CustomGroupBannedUserList.SwiftUI.View.Main.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29996001A7AE321A2AF1FDE8 /* CustomGroupBannedUserList.SwiftUI.View.Main.swift */; }; 6E32FBA7FEF4B1DF15CBA365 /* OpenChannelView+SubViewBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = FB5A989D9069803A11632F6A /* OpenChannelView+SubViewBuilder.swift */; }; + 6E56A4C38E8FB2A9ABFE5100 /* SBUMessageFormViewParams.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09E6606143A887F262F79FA9 /* SBUMessageFormViewParams.swift */; }; 6E87F95D609F2E99C85A2155 /* InviteUserView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 16D3BFEEF9AF232331AE1BDC /* InviteUserView.swift */; }; 6EB8AE7FE569D606693E7E34 /* CustomOpenOperatorList.ViewConverter.Header.leftView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8E194EB59871973C28A282B5 /* CustomOpenOperatorList.ViewConverter.Header.leftView.swift */; }; + 6F337F85F1EC26FB5747C0B0 /* SBUMessageFormMultiTextItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52F1328AE03EAAF1AD426D08 /* SBUMessageFormMultiTextItemView.swift */; }; 6F35423AB5238BD924BF8EB1 /* SwiftUIViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0B43C3575A86FB54C7F46C0C /* SwiftUIViewController.swift */; }; 6F5EDDDB302028D0FE8790D7 /* CustomGroupChannel.ViewConverter.List.rowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E90127DF72AC51AA2AE0001 /* CustomGroupChannel.ViewConverter.List.rowView.swift */; }; 6FEB53022DA164CA48ED8F6E /* LoginView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF17E587AFF1B661A648930B /* LoginView.swift */; }; @@ -464,6 +473,7 @@ 72E1942B6F0714E5C5E3FD0E /* SBUGroupChannelListModule.List.swift in Sources */ = {isa = PBXBuildFile; fileRef = DF0E4D61160B2C4D056ABF96 /* SBUGroupChannelListModule.List.swift */; }; 7305337E0F61C0623B07C64A /* OpenMutedParticipantListViewConverter.List.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1243D603EB01314B012C1D6E /* OpenMutedParticipantListViewConverter.List.swift */; }; 7397E736C9B53B71C241F874 /* GroupMutedMemberListViewConverter.Header.swift in Sources */ = {isa = PBXBuildFile; fileRef = BC85F62E529A705813ED5671 /* GroupMutedMemberListViewConverter.Header.swift */; }; + 73A07126956A34CDDF79C72B /* SBUMessageTemplateCell.MessageTemplateLayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = E7F4A5C2DE942F918F081586 /* SBUMessageTemplateCell.MessageTemplateLayer.swift */; }; 73B5E951B0FB998EDACC8BB4 /* OpenOperatorListView+Item.swift in Sources */ = {isa = PBXBuildFile; fileRef = 76799FA15AA5786517BEF343 /* OpenOperatorListView+Item.swift */; }; 73B6B5337FF2FC877DBDB6B5 /* CustomOpenBannedUserList.ViewConverter.List.userNameLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CC7C6EE71D9C0203FB5FB1A /* CustomOpenBannedUserList.ViewConverter.List.userNameLabel.swift */; }; 73CDF9A5384577E92EACC44C /* SBUCreateGroupChannelModule.List+SwiftUI.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2E6F8EEF508DA948CDEA60A /* SBUCreateGroupChannelModule.List+SwiftUI.swift */; }; @@ -484,7 +494,6 @@ 76A52219FACE19DDD99554CD /* SBUGroupChannelSettingsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 16B94B84000BF8542CAE6747 /* SBUGroupChannelSettingsViewModel.swift */; }; 76AFE05AB31FFE1CC4F96941 /* SBUMessageSearchViewController.Deprecated.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE4692268960D6FB3AD51F3A /* SBUMessageSearchViewController.Deprecated.swift */; }; 76CF2A78B301A445C3DD58B2 /* SBUMessageThreadModule.Input.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E35F2241D8BFF297E9C7880 /* SBUMessageThreadModule.Input.swift */; }; - 76DEE0DC4C4B264BEDC939AD /* SBUFormViewParams.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA500BA4D05A2F6897095A67 /* SBUFormViewParams.swift */; }; 76EDABA453D0957F41C42684 /* CustomOpenMutedParticipantList.ViewConverter.List.profileImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14EA076EB6C73BA4290914FE /* CustomOpenMutedParticipantList.ViewConverter.List.profileImage.swift */; }; 7705D4416461633D988BF2D4 /* SBUCreateOpenChannelViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 633ED5B1F7A4CAB56DE1F5BB /* SBUCreateOpenChannelViewController.swift */; }; 770D93EADA297635BA603621 /* SBUInviteUserModule.Header.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4EB1DE72843F9F2E4E067BC /* SBUInviteUserModule.Header.swift */; }; @@ -497,6 +506,7 @@ 785FE7080A6BB20F6FC9117F /* SendbirdUI.Deprecated.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4DBC0F7C21761BD70C04903 /* SendbirdUI.Deprecated.swift */; }; 786261EBAF78615F8AFEAF41 /* MainView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 940843643FFA366733C4118D /* MainView.swift */; }; 78C2FBC31DD3F2C8A6160AC8 /* CustomInviteUser.ViewConverter.List.rowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11EC6E379AF8CC994489450D /* CustomInviteUser.ViewConverter.List.rowView.swift */; }; + 790BDF268EE5BE9FF7D4C985 /* SBUMessageTemplate.Container.swift in Sources */ = {isa = PBXBuildFile; fileRef = 130BDF243558344563BDD04F /* SBUMessageTemplate.Container.swift */; }; 7912D438DD630BA7680173B5 /* SBUDateFormatSet.swift in Sources */ = {isa = PBXBuildFile; fileRef = A0A6460B6ABA537A9DF9E077 /* SBUDateFormatSet.swift */; }; 791745E57EDBDDC81EDDEFDB /* CustomMessageThread.ViewConverter.ParentInfo.fileContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34DF51D99B16E5DE6AF5DE56 /* CustomMessageThread.ViewConverter.ParentInfo.fileContentView.swift */; }; 791E5F692866B1DC5DB07A79 /* SBUPhotoCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D98EB11025EC91C28DF16EA2 /* SBUPhotoCollectionViewCell.swift */; }; @@ -623,6 +633,7 @@ 9705E72ED1723E182B038BA7 /* CustomOpenChannelSettings.ViewConverter.Header.leftView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9AD7AABB1BA8EA4D43105BD4 /* CustomOpenChannelSettings.ViewConverter.Header.leftView.swift */; }; 972F86B5517BE88F359765E3 /* CustomGroupMutedMemberList.ViewConverter.List.profileImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69AA5A484CD6F75C2FB63B42 /* CustomGroupMutedMemberList.ViewConverter.List.profileImage.swift */; }; 9753354376AA464C7686A373 /* OpenChannelRegisterOperatorView+Item.swift in Sources */ = {isa = PBXBuildFile; fileRef = 868865ED19B43E30C0DCE6BF /* OpenChannelRegisterOperatorView+Item.swift */; }; + 978C78CD1448E70538A47AB8 /* SBUMessageTemplateCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2F7CFF8FDC3CA84CA743B250 /* SBUMessageTemplateCell.swift */; }; 978FED43298D0E7AC78726A2 /* CustomGroupMutedMemberList.ViewConverter.Header.rightView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 315F7B885B880AEE8C60AC67 /* CustomGroupMutedMemberList.ViewConverter.Header.rightView.swift */; }; 97A63E76BDCB65E0EE19C5E4 /* CustomGroupMemberList.ViewConverter.List.operatorStateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DA1AD0747C09319189DDF68 /* CustomGroupMemberList.ViewConverter.List.operatorStateView.swift */; }; 97B0D361A9BC7A26A22021FD /* SBUUnknownMessageCellParams.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D5EA770115CE527CEF39044 /* SBUUnknownMessageCellParams.swift */; }; @@ -735,12 +746,12 @@ AEF46BD164C362CD5EBF9465 /* SendbirdSwiftUI-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 377CB683EA76DA82E16E556A /* SendbirdSwiftUI-Info.plist */; }; AEFFDC8D114E5D8840877C81 /* CustomGroupMemberList.ViewConverter.Header.titleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F34DA7D7D4B2985E294981E7 /* CustomGroupMemberList.ViewConverter.Header.titleView.swift */; }; AF20C220160DCC107926F516 /* SBUGlobals.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8B980032E44E8D2CD11B79A /* SBUGlobals.swift */; }; + AF83514E03BDB74C06340303 /* SBUFormFieldView.Deprecated.swift in Sources */ = {isa = PBXBuildFile; fileRef = FCD53856699176A2A465111B /* SBUFormFieldView.Deprecated.swift */; }; AFB7CB7B79CF3807A64F52E3 /* SBUDebouncer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A95670973E944B4C3A95416 /* SBUDebouncer.swift */; }; AFBC68E1B521FEA3FE3C5891 /* CustomGroupOperatorList.ViewConverter.List.userNameLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F60F28A03D242FFB2D615190 /* CustomGroupOperatorList.ViewConverter.List.userNameLabel.swift */; }; AFD355B4DADBB95C7411FC98 /* SBUMessageThreadModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 07B875B23CC937172B1A0557 /* SBUMessageThreadModule.swift */; }; AFD48BD36352900AF6B964D5 /* MessageThreadView+ViewConverter.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB042AC39230E43C6B58A32E /* MessageThreadView+ViewConverter.swift */; }; AFEF62DD6CD4464AA4687B82 /* SBUScrollOptions.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3AEA60115A8DACD156EFE14 /* SBUScrollOptions.swift */; }; - B0205DE7C34AD49E722B49E3 /* SBUFormFieldView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC1973CFB8CDEF2249804990 /* SBUFormFieldView.swift */; }; B0B3EADA0ACFE07ACAD2A89A /* SBUQuotedBaseMessageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6BFEDEA0282EC80B5682BC6B /* SBUQuotedBaseMessageView.swift */; }; B11DFF996C8B40C236E5167A /* CustomOpenModerations.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02676C1FF2C5DD89C779D257 /* CustomOpenModerations.swift */; }; B172261CD25AC117AEC18668 /* SBUGroupChannelCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE2A55A25B370F6C8ED56E24 /* SBUGroupChannelCell.swift */; }; @@ -790,6 +801,7 @@ BD2F982C9CA605F747407620 /* GroupChannelSettingsView+ViewConverter.swift in Sources */ = {isa = PBXBuildFile; fileRef = D47B80C8F70B7B35CB44832C /* GroupChannelSettingsView+ViewConverter.swift */; }; BD6080969BB3E78B1ED0C952 /* SBUMessageInputView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0AEAE4D3A35CB4AE694FB592 /* SBUMessageInputView.swift */; }; BDE73C3C5B5901C89B7749B2 /* SBUStackView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC66BFE52B21FA2B30AE8E30 /* SBUStackView.swift */; }; + BDF9A8DE063A5D26AD903A5A /* SBUMessageFormFallbackView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D23CE29CE2785C1F89E0FE88 /* SBUMessageFormFallbackView.swift */; }; BEA54478E13DAC0FB099126F /* SBUTypingIndicatorMessageCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = A0B27BB4D8B2F159844A77BF /* SBUTypingIndicatorMessageCell.swift */; }; BEA61A98C3197318CB54E1F3 /* CustomOpenOperatorList.ViewConverter.Header.rightView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0D6086A2472531DE282E4A98 /* CustomOpenOperatorList.ViewConverter.Header.rightView.swift */; }; BECC583E161FDC7F9FB35264 /* CustomGroupChannelPushSettings.SwiftUI.View.CustomMain.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1BE7FE947E6F68414C95978 /* CustomGroupChannelPushSettings.SwiftUI.View.CustomMain.swift */; }; @@ -863,6 +875,7 @@ D00EBD058B6290476424286D /* CustomGroupChannel.ViewConverter.Header.coverImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D38AA27BCCE944F35F787E7 /* CustomGroupChannel.ViewConverter.Header.coverImage.swift */; }; D0161EAD1D4B16A02139A84D /* MessageThreadViewConverter.Header.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3575D8324F8DCEC741A02E9C /* MessageThreadViewConverter.Header.swift */; }; D01F0F1DD3A5CD13D0460014 /* SBULazyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FDEE72ED520123188DB93FF /* SBULazyView.swift */; }; + D0B9BCABF4D8E0174A29C3C7 /* SBUMessageFormSingleTextItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BE7259CA31206A469BB51C25 /* SBUMessageFormSingleTextItemView.swift */; }; D105B90AD27E4A1D177141B2 /* CustomMessageThread.ViewConverter.List.multipleFilesMessageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D44ADE8F289364F71D86202A /* CustomMessageThread.ViewConverter.List.multipleFilesMessageView.swift */; }; D130BC83D31456C9DADF559F /* CustomGroupModerations.ViewConverter.Header.rightView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EDE250657479E5DFC57BEF3 /* CustomGroupModerations.ViewConverter.Header.rightView.swift */; }; D15448A2AFC32086A87FD72D /* UserDefaults+Ext.swift in Sources */ = {isa = PBXBuildFile; fileRef = BB75D9350081AF38A4FAA594 /* UserDefaults+Ext.swift */; }; @@ -879,7 +892,6 @@ D3B5643C1E822D974328D4BB /* GroupModerationsView+Item.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD8F9ED1B50DBD995EEF594B /* GroupModerationsView+Item.swift */; }; D4297F02CDEFB90DD1C01C0B /* OpenParticipantListView+Item.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1992BFA4DDDB1636ACFF4A97 /* OpenParticipantListView+Item.swift */; }; D4500C33046BB2D60EA64C66 /* SBUGroupChannelPushSettingsModule.Header+SwiftUI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D62381A39883A6AF9749655 /* SBUGroupChannelPushSettingsModule.Header+SwiftUI.swift */; }; - D4E6406BF304B2FCC0AF2ADC /* SBUFormView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E9A72E3F57CDEED75481704 /* SBUFormView.swift */; }; D50E407E80A53447FF9B3F83 /* SBUOpenChannelSettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0862BAC5CCDFC6641C3B97C3 /* SBUOpenChannelSettingsViewController.swift */; }; D51031FEFC9399DD2BB8301C /* CustomOpenChannel.ViewConverter.List.entireView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE41B8A5727684B5E30295B4 /* CustomOpenChannel.ViewConverter.List.entireView.swift */; }; D57118BC69FAEA9F2DA17FDA /* CustomGroupChannel.ViewConverter.List.multipleFilesMessageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0F5806465F7C6A9458CFA95 /* CustomGroupChannel.ViewConverter.List.multipleFilesMessageView.swift */; }; @@ -891,6 +903,7 @@ D700D51CF1431F165C099FA1 /* CustomMessageThread.ViewConverter.Header.rightView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FBC4A6592CF9264B9334EC0C /* CustomMessageThread.ViewConverter.Header.rightView.swift */; }; D7531BC55DDC2FD53CFCF70A /* UIColor+SBUIKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = A3CF699C92A450F181836CD9 /* UIColor+SBUIKit.swift */; }; D76D94A74969D557C5D8E414 /* CustomGroupBannedUserList.SwiftUI.View.CustomMain.swift in Sources */ = {isa = PBXBuildFile; fileRef = 344A9753F8B782EDD9E411B6 /* CustomGroupBannedUserList.SwiftUI.View.CustomMain.swift */; }; + D78B56FF68EC5CAF2F39F522 /* SBUMessageFormChipsItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7E39A804B558F66F7EB947 /* SBUMessageFormChipsItemView.swift */; }; D7A6DA898753003B05ECC100 /* GroupChannelSettingsViewConverter.List.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8A4F7A4428AA6840190A27E2 /* GroupChannelSettingsViewConverter.List.swift */; }; D7C3BCFB400E6BA692406943 /* SBUMentionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9854C9F75B1B5DF2C5C09594 /* SBUMentionManager.swift */; }; D7DE86DB66F59C1D32492FE0 /* SBUOpenChannelViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 41CE55F82D74BE766CC40BC8 /* SBUOpenChannelViewModel.swift */; }; @@ -1001,6 +1014,7 @@ F09AD512141C60DD538627BE /* SBUFileMessageCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27F66C34721B8052037668EF /* SBUFileMessageCell.swift */; }; F09CF2430BE102203A4AEBB9 /* SBUSuggestedReplyViewParams.swift in Sources */ = {isa = PBXBuildFile; fileRef = D86D4D360EBE5B44F0D0606D /* SBUSuggestedReplyViewParams.swift */; }; F0A38A8E7B664D62CDE9CDDB /* CustomGroupBannedUserList.ViewConverter.List.userNameLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0AE3AA13449F68E87D7DD3D /* CustomGroupBannedUserList.ViewConverter.List.userNameLabel.swift */; }; + F0D502B07BE7FF2D8FDE7967 /* SBUMessageFormItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A4FE9D61A27CDE2A743D3CFE /* SBUMessageFormItemView.swift */; }; F16A0166E2F1F0B4B596C82A /* MessageSearchView+Item.swift in Sources */ = {isa = PBXBuildFile; fileRef = 735C35C7BF5726DE097A6252 /* MessageSearchView+Item.swift */; }; F1A6CCACDA3EDC4EB8401ACD /* GroupBannedUserListViewConverter.List.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAB28CD649CAB03111FF852B /* GroupBannedUserListViewConverter.List.swift */; }; F1C659C2EC12CAB1DF3BE706 /* CustomOpenMutedParticipantList.SwiftUI.View.Main.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C4C185DFE1CA6B5583DB42A /* CustomOpenMutedParticipantList.SwiftUI.View.Main.swift */; }; @@ -1029,6 +1043,7 @@ F65DF3951996885296957169 /* SBUNewNotificationInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 681FCE6FE5FEC50EDECCB1BB /* SBUNewNotificationInfo.swift */; }; F6A55FD4F939E34B974EC276 /* SBUNotificationTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C080BD93D8577C641E87E1D5 /* SBUNotificationTimelineView.swift */; }; F76A1290E87A7042FE910883 /* CustomCreateOpenChannel.SwiftUI.View.CustomMain.swift in Sources */ = {isa = PBXBuildFile; fileRef = 434BE19EF6D070299A1BBB96 /* CustomCreateOpenChannel.SwiftUI.View.CustomMain.swift */; }; + F78D508B82FF6AF8575ECC04 /* UICollectionView+SBUIKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3064CE17D9A94272F21E223E /* UICollectionView+SBUIKit.swift */; }; F79FCC71D80ECA4481F1B234 /* MessageTemplateParserTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7FD3CDCCBBB8FB5E0C3164B8 /* MessageTemplateParserTest.swift */; }; F7AA58AB82F4C495E5284CA0 /* CustomGroupChannel.ViewConverter.List.userMessageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27D1D6BA515FD2C31FF5DF98 /* CustomGroupChannel.ViewConverter.List.userMessageView.swift */; }; F7CBD1BF0C0DA146850E183B /* GroupMemberListViewConverter.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAD9C78B13DA1261EFA4A175 /* GroupMemberListViewConverter.swift */; }; @@ -1036,6 +1051,7 @@ F876788358C112410ECAB4E8 /* MessageThreadView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 482F356F3F2D59068AF4D959 /* MessageThreadView.swift */; }; F8BA54072955D783FCD276AB /* SBULabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 089EFCFAB4CC0905D70E98F7 /* SBULabel.swift */; }; F8C970D71462EFD8D116E260 /* CustomOpenChannel.ViewConverter.Header.titleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6D5AFF1C0FFAF7FB44385303 /* CustomOpenChannel.ViewConverter.Header.titleView.swift */; }; + F8F0B40DE98424420598D32A /* SBUTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2752645C33797142AEE30A20 /* SBUTextView.swift */; }; F9CF4B42BDC33490C9BB589C /* CustomTheme.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5251C5F71C7B463C54D6474C /* CustomTheme.swift */; }; F9F2D651B0D67D56FB3387DA /* SBULoading.swift in Sources */ = {isa = PBXBuildFile; fileRef = 24FA1C9F270095F0BBE68991 /* SBULoading.swift */; }; FA7025F697F842992DE3DB75 /* SBUScrollBottomView+SwiftUI.swift in Sources */ = {isa = PBXBuildFile; fileRef = C74FF03A1BD9590647429D9E /* SBUScrollBottomView+SwiftUI.swift */; }; @@ -1128,6 +1144,7 @@ 08AE1C3D2DAF93702E8C3E6B /* OpenOperatorListView+SubViewBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OpenOperatorListView+SubViewBuilder.swift"; sourceTree = ""; }; 0906752602F6996C99B03220 /* CustomGroupChannelList.ViewConverter.Header.rightView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomGroupChannelList.ViewConverter.Header.rightView.swift; sourceTree = ""; }; 092C81FE6CB8C115FFB896E6 /* SBUVoicePlayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SBUVoicePlayer.swift; sourceTree = ""; }; + 09E6606143A887F262F79FA9 /* SBUMessageFormViewParams.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SBUMessageFormViewParams.swift; sourceTree = ""; }; 09FB473379288E2643AFD21B /* CustomOpenChannelRegisterOperator.ViewConverter.List.entireView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomOpenChannelRegisterOperator.ViewConverter.List.entireView.swift; sourceTree = ""; }; 0A26F750C0BD5F985CE0488F /* OpenModerationsView+SubViewBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OpenModerationsView+SubViewBuilder.swift"; sourceTree = ""; }; 0A4B1C1EC52CFF027D65EFA5 /* SBUGlobalCustomParams.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SBUGlobalCustomParams.swift; sourceTree = ""; }; @@ -1171,6 +1188,7 @@ 1243D603EB01314B012C1D6E /* OpenMutedParticipantListViewConverter.List.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenMutedParticipantListViewConverter.List.swift; sourceTree = ""; }; 1286E9C8A5FDC69006356104 /* UIImage+SBUIKit.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIImage+SBUIKit.swift"; sourceTree = ""; }; 12DC37AE9F9A2EAAB346B524 /* OpenParticipantListView+ViewConverter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OpenParticipantListView+ViewConverter.swift"; sourceTree = ""; }; + 130BDF243558344563BDD04F /* SBUMessageTemplate.Container.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SBUMessageTemplate.Container.swift; sourceTree = ""; }; 1332D36686F1F249E74B711C /* SBUUserListViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SBUUserListViewModel.swift; sourceTree = ""; }; 138034D59666A1D306B840DA /* CustomGroupChannelSettings.ViewConverter.List.member.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomGroupChannelSettings.ViewConverter.List.member.swift; sourceTree = ""; }; 13A39F0CA1ACC3DC69F56F19 /* SBUGroupChannelPushSettingsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SBUGroupChannelPushSettingsViewController.swift; sourceTree = ""; }; @@ -1220,6 +1238,7 @@ 1D2EAD5AE3B031549D4E8E9D /* CustomMessageThread.ViewConverter.List.senderProfileImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomMessageThread.ViewConverter.List.senderProfileImage.swift; sourceTree = ""; }; 1D38AA27BCCE944F35F787E7 /* CustomGroupChannel.ViewConverter.Header.coverImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomGroupChannel.ViewConverter.Header.coverImage.swift; sourceTree = ""; }; 1DA1AD0747C09319189DDF68 /* CustomGroupMemberList.ViewConverter.List.operatorStateView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomGroupMemberList.ViewConverter.List.operatorStateView.swift; sourceTree = ""; }; + 1DB8F69105F07B296E8C63F7 /* SBUFormViewParams.Deprecated.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SBUFormViewParams.Deprecated.swift; sourceTree = ""; }; 1DDB1C8B0B6F4041F5B74E5F /* Collection+SBUIKit.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Collection+SBUIKit.swift"; sourceTree = ""; }; 1DEBA9D64BDD959D8117D5C8 /* Date+SBUIKit.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Date+SBUIKit.swift"; sourceTree = ""; }; 1E15C3CDDBC6D533E0C64E57 /* SBUMessageSearchResultCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SBUMessageSearchResultCell.swift; sourceTree = ""; }; @@ -1260,6 +1279,7 @@ 27093994AF106EAB97B7F24D /* SBUMessageSearchModule.Deprecated.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SBUMessageSearchModule.Deprecated.swift; sourceTree = ""; }; 274069D20D2F3B5E40ADC2D2 /* CustomGroupChannelPushSettings.ViewConverter.Header.rightView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomGroupChannelPushSettings.ViewConverter.Header.rightView.swift; sourceTree = ""; }; 274DE7206A600067D64B8AF8 /* OpenChannelSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenChannelSettingsView.swift; sourceTree = ""; }; + 2752645C33797142AEE30A20 /* SBUTextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SBUTextView.swift; sourceTree = ""; }; 279B0E88887FD0F09FD5290E /* CustomGroupMemberList.ViewConverter.List.rowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomGroupMemberList.ViewConverter.List.rowView.swift; sourceTree = ""; }; 279B677541AB48613E5A2EA8 /* CustomOpenChannelSettings.SubView.Builder.moderations.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomOpenChannelSettings.SubView.Builder.moderations.swift; sourceTree = ""; }; 27C26C3082D49E50ADAB0561 /* SBUBaseChannelListViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SBUBaseChannelListViewModel.swift; sourceTree = ""; }; @@ -1281,6 +1301,7 @@ 2B41B61973E52E8F8B861399 /* CustomCreateOpenChannel.ViewConverter.List.profileImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomCreateOpenChannel.ViewConverter.List.profileImage.swift; sourceTree = ""; }; 2BCED6324E56293CB6D4F266 /* SBUTypingIndicatorMessageManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SBUTypingIndicatorMessageManager.swift; sourceTree = ""; }; 2BD3F92049FFA549EE2A62ED /* CustomOpenParticipantList.ViewConverter.List.rowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomOpenParticipantList.ViewConverter.List.rowView.swift; sourceTree = ""; }; + 2C15ED108FD8018112C6E8B7 /* SBUError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SBUError.swift; sourceTree = ""; }; 2C29B6EED1FE32956FF834A8 /* SBURegisterOperatorModule.Deprecated.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SBURegisterOperatorModule.Deprecated.swift; sourceTree = ""; }; 2C593374BB12107EC560C715 /* StringProtocol+SBUIKit.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "StringProtocol+SBUIKit.swift"; sourceTree = ""; }; 2C6C9F8F3C88F07B39F5C385 /* SBUMessageTemplate.Syntax.Styles.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SBUMessageTemplate.Syntax.Styles.swift; sourceTree = ""; }; @@ -1297,11 +1318,13 @@ 2EE881D701BE62C2C5A1297C /* CustomOpenChannelSettings.SubView.Builder.userList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomOpenChannelSettings.SubView.Builder.userList.swift; sourceTree = ""; }; 2EF0CBE3236AA5C29F96124D /* CustomOpenChannel.SubView.Builder.userList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomOpenChannel.SubView.Builder.userList.swift; sourceTree = ""; }; 2F5596CBB3FB434BE59F8BBA /* GroupMemberListView+Item.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "GroupMemberListView+Item.swift"; sourceTree = ""; }; + 2F7CFF8FDC3CA84CA743B250 /* SBUMessageTemplateCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SBUMessageTemplateCell.swift; sourceTree = ""; }; 2F88656EDE7A31FD01B7ED7D /* SBUChatNotificationChannelModule.Deprecated.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SBUChatNotificationChannelModule.Deprecated.swift; sourceTree = ""; }; 2FF459520D3ED7BBC083BB7E /* SBUEnums.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SBUEnums.swift; sourceTree = ""; }; 30394872D50BA64B985DA1FF /* OpenChannelView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenChannelView.swift; sourceTree = ""; }; 304A09ECDE1B9834CD9A4E19 /* SBUBaseChannelViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SBUBaseChannelViewModel.swift; sourceTree = ""; }; 3063FC91B4A7751B29DC80B1 /* CustomGroupMutedMemberList.ViewConverter.Header.leftView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomGroupMutedMemberList.ViewConverter.Header.leftView.swift; sourceTree = ""; }; + 3064CE17D9A94272F21E223E /* UICollectionView+SBUIKit.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UICollectionView+SBUIKit.swift"; sourceTree = ""; }; 30D114A3FEC7A9B2EC46E9EE /* SBUUnderLineTextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SBUUnderLineTextField.swift; sourceTree = ""; }; 310897972F208CD1051FDB43 /* CustomGroupChannelList.ViewConverter.Header.titleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomGroupChannelList.ViewConverter.Header.titleView.swift; sourceTree = ""; }; 315F7B885B880AEE8C60AC67 /* CustomGroupMutedMemberList.ViewConverter.Header.rightView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomGroupMutedMemberList.ViewConverter.Header.rightView.swift; sourceTree = ""; }; @@ -1331,12 +1354,14 @@ 3705CC5FA738AA9FAB298E7D /* SBUGroupChannelSettingCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SBUGroupChannelSettingCell.swift; sourceTree = ""; }; 3716519FE7F6D63C6FE4A48A /* SBUOpenChannelUnknownMessageCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SBUOpenChannelUnknownMessageCell.swift; sourceTree = ""; }; 371A733CC45A9696DEBF8F79 /* CustomCreateGroupChannel.ViewConverter.Header.rightView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomCreateGroupChannel.ViewConverter.Header.rightView.swift; sourceTree = ""; }; + 374258FE63226944670893DB /* SBUMessageTemplateCellLayout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SBUMessageTemplateCellLayout.swift; sourceTree = ""; }; 37663F0868F7387F231EF23F /* VoiceMessageStatus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VoiceMessageStatus.swift; sourceTree = ""; }; 377CB683EA76DA82E16E556A /* SendbirdSwiftUI-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = "SendbirdSwiftUI-Info.plist"; sourceTree = ""; }; 377FEABC82180302EA83C74A /* CustomGroupModerations.SwiftUI.View.CustomMain.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomGroupModerations.SwiftUI.View.CustomMain.swift; sourceTree = ""; }; 37AAC2A470701025FC17A63C /* CustomCreateGroupChannel.SwiftUI.View.CustomMain.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomCreateGroupChannel.SwiftUI.View.CustomMain.swift; sourceTree = ""; }; 37C11167BE05D634FB689838 /* SBUMultipleFilesMessageCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SBUMultipleFilesMessageCollectionViewCell.swift; sourceTree = ""; }; 3846C23C9BCC45CA38A719FE /* SBUReactionCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SBUReactionCollectionViewCell.swift; sourceTree = ""; }; + 384A0D8AB5B526308F89634E /* SBUMessageFormView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SBUMessageFormView.swift; sourceTree = ""; }; 388F6BF164AE95A69B80EB39 /* CustomInviteUser.ViewConverter.Header.rightView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomInviteUser.ViewConverter.Header.rightView.swift; sourceTree = ""; }; 39177B76E752035056578234 /* SBUQuotedFileMessageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SBUQuotedFileMessageView.swift; sourceTree = ""; }; 393297B39B4586C83A5FE3D0 /* GroupMutedMemberListView+SubViewBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "GroupMutedMemberListView+SubViewBuilder.swift"; sourceTree = ""; }; @@ -1420,9 +1445,9 @@ 4CF215E5C5EAE38BCDB732CD /* SBUGroupChannelSettingCell+SwiftUI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SBUGroupChannelSettingCell+SwiftUI.swift"; sourceTree = ""; }; 4DEF0B14DE38EB619FD6405B /* SBUModuleSet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SBUModuleSet.swift; sourceTree = ""; }; 4E03A57B44CC6B14D4C4031B /* GroupOperatorListView+ViewConverter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "GroupOperatorListView+ViewConverter.swift"; sourceTree = ""; }; + 4E542BCE47135E2CF12B69A8 /* SBUMessageTemplateCellParams.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SBUMessageTemplateCellParams.swift; sourceTree = ""; }; 4E59F203B7488D4BC92B8103 /* CustomGroupChannelRegisterOperator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomGroupChannelRegisterOperator.swift; sourceTree = ""; }; 4E7A992C1BA6BBD9F68F735A /* CustomGroupChannelList.ViewConverter.List.rowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomGroupChannelList.ViewConverter.List.rowView.swift; sourceTree = ""; }; - 4E9A72E3F57CDEED75481704 /* SBUFormView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SBUFormView.swift; sourceTree = ""; }; 4F19E0129A6247FB94372E4C /* CustomOpenMutedParticipantList.ViewConverter.List.operatorStateView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomOpenMutedParticipantList.ViewConverter.List.operatorStateView.swift; sourceTree = ""; }; 4F5A3BE9830FFE3F5AD5F422 /* CustomMessageSearch.SubView.Builder.groupChannel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomMessageSearch.SubView.Builder.groupChannel.swift; sourceTree = ""; }; 4FBED55AA88DBE899ACF3025 /* CustomGroupMemberList.SubView.Builder.inviteUser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomGroupMemberList.SubView.Builder.inviteUser.swift; sourceTree = ""; }; @@ -1433,6 +1458,7 @@ 505C2553E67C0C1CDDE626B2 /* SBUGlobals.Deprecated.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SBUGlobals.Deprecated.swift; sourceTree = ""; }; 506B49D35067C017A092EB8C /* CustomInviteUser.ViewConverter.List.profileImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomInviteUser.ViewConverter.List.profileImage.swift; sourceTree = ""; }; 51454BECF0ACB34F18848A01 /* CustomGroupChannel.ViewConverter.Input.addButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomGroupChannel.ViewConverter.Input.addButton.swift; sourceTree = ""; }; + 515A6FCA6E34F58EB62146AD /* SBUFormView.Deprecated.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SBUFormView.Deprecated.swift; sourceTree = ""; }; 519230E62500F1040802B744 /* SBUMessageWebView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SBUMessageWebView.swift; sourceTree = ""; }; 519AF0B13E4AD698AEFB0303 /* CustomGroupOperatorList.ViewConverter.List.profileImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomGroupOperatorList.ViewConverter.List.profileImage.swift; sourceTree = ""; }; 51C56CFE8E82E6E482183B7D /* SBUBaseChannelViewController.Deprecated.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SBUBaseChannelViewController.Deprecated.swift; sourceTree = ""; }; @@ -1440,6 +1466,7 @@ 5251C5F71C7B463C54D6474C /* CustomTheme.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomTheme.swift; sourceTree = ""; }; 526ED0C01EE8EF2AC7D76E68 /* OpenChannelSettingsView+ViewConverter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OpenChannelSettingsView+ViewConverter.swift"; sourceTree = ""; }; 52E4D495AFCFF546A54C3B24 /* SwiftUIMessageInputInterface.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftUIMessageInputInterface.swift; sourceTree = ""; }; + 52F1328AE03EAAF1AD426D08 /* SBUMessageFormMultiTextItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SBUMessageFormMultiTextItemView.swift; sourceTree = ""; }; 53346E81604409C68486DBB2 /* CustomGroupChannel.ViewConverter.List.fileMessageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomGroupChannel.ViewConverter.List.fileMessageView.swift; sourceTree = ""; }; 533FBEB0CF02D20487819307 /* CustomOpenParticipantList.ViewConverter.Header.titleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomOpenParticipantList.ViewConverter.Header.titleView.swift; sourceTree = ""; }; 534BB97DAD333885A76085C0 /* GroupModerationsViewConverter.List.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupModerationsViewConverter.List.swift; sourceTree = ""; }; @@ -1499,6 +1526,7 @@ 5DBA3FA486B355A6F23DFD00 /* CustomGroupOperatorList.ViewConverter.List.rowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomGroupOperatorList.ViewConverter.List.rowView.swift; sourceTree = ""; }; 5DEE6B3475E267184B7C0921 /* CustomCreateGroupChannel.ViewConverter.List.entireView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomCreateGroupChannel.ViewConverter.List.entireView.swift; sourceTree = ""; }; 5E7AF0A36D04D33BA1B28E46 /* CustomMessageThread.ViewConverter.Input.addButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomMessageThread.ViewConverter.Input.addButton.swift; sourceTree = ""; }; + 5E7E39A804B558F66F7EB947 /* SBUMessageFormChipsItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SBUMessageFormChipsItemView.swift; sourceTree = ""; }; 5ECC1263CB743FBD32569570 /* SBUOpenChannelSettingsModule.Header+SwiftUI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SBUOpenChannelSettingsModule.Header+SwiftUI.swift"; sourceTree = ""; }; 5EDD7A1F27B543A5A2CC0C50 /* SBUMessageTemplate.Syntax.Item.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SBUMessageTemplate.Syntax.Item.swift; sourceTree = ""; }; 5F1E6CC210604D9FFC1F3234 /* SBUBaseChannelModule.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SBUBaseChannelModule.swift; sourceTree = ""; }; @@ -1652,6 +1680,7 @@ 869EAD18504324BF7D9CF724 /* SBUBaseChannelSettingsModule.List.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SBUBaseChannelSettingsModule.List.swift; sourceTree = ""; }; 86CD79484A29B4EA66EC4B0C /* SBUQuotedUserMessageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SBUQuotedUserMessageView.swift; sourceTree = ""; }; 86D37B65E1EF649577EA0D37 /* SBUImageContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SBUImageContentView.swift; sourceTree = ""; }; + 86D998504A47DE9BAAC543C1 /* MessageForm+SBUIKit.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MessageForm+SBUIKit.swift"; sourceTree = ""; }; 8731B4E84E8F9AFDB3DCCF5D /* OpenChannelListView+Item.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OpenChannelListView+Item.swift"; sourceTree = ""; }; 878A8DA40EB2448FB0724D26 /* CustomMessageSearch.SwiftUI.View.CustomMain.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomMessageSearch.SwiftUI.View.CustomMain.swift; sourceTree = ""; }; 88134BA06CADFAE21FFFC2D0 /* SBUEmojiManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SBUEmojiManager.swift; sourceTree = ""; }; @@ -1686,7 +1715,7 @@ 8E8801C8B570BA053A2B2FEA /* CreateOpenChannelView+ViewConverter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CreateOpenChannelView+ViewConverter.swift"; sourceTree = ""; }; 8F27A78EE60A93797E010244 /* SBUConfigManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SBUConfigManager.swift; sourceTree = ""; }; 8F30867017D52BC55BF8A8F2 /* OpenOperatorListViewConverter.List.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenOperatorListViewConverter.List.swift; sourceTree = ""; }; - 8FDAE9FA5128686B5ABA8011 /* SBUUserMessageCell.MessageTemplate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SBUUserMessageCell.MessageTemplate.swift; sourceTree = ""; }; + 904618EB6B0A591FC15D1542 /* SBUMessageFormChipView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SBUMessageFormChipView.swift; sourceTree = ""; }; 905A88DC20838A94B21B3544 /* CustomGroupChannelRegisterOperator.ViewConverter.Header.titleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomGroupChannelRegisterOperator.ViewConverter.Header.titleView.swift; sourceTree = ""; }; 9087883EC823CC5667850BE6 /* CustomGroupChannelSettings.ViewConverter.Header.leftView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomGroupChannelSettings.ViewConverter.Header.leftView.swift; sourceTree = ""; }; 90D8DE1D14DD3A1D25ED47F3 /* CustomOpenOperatorList.SubView.Builder.registerOperator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomOpenOperatorList.SubView.Builder.registerOperator.swift; sourceTree = ""; }; @@ -1775,6 +1804,7 @@ A4D6A060025AC392A913D587 /* SBUQuotedBaseMessageViewParams.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SBUQuotedBaseMessageViewParams.swift; sourceTree = ""; }; A4E6B3FBFE02752B58883E4B /* GroupOperatorListView+Item.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "GroupOperatorListView+Item.swift"; sourceTree = ""; }; A4E9A824E45E051EC112A416 /* SBUOpenChannelListViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SBUOpenChannelListViewController.swift; sourceTree = ""; }; + A4FE9D61A27CDE2A743D3CFE /* SBUMessageFormItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SBUMessageFormItemView.swift; sourceTree = ""; }; A506ABBB4475B2B23B51459C /* CustomOpenChannel.ViewConverter.List.scrollBottomView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomOpenChannel.ViewConverter.List.scrollBottomView.swift; sourceTree = ""; }; A570D54C533C1A8E4328595F /* CustomOpenChannel.ViewConverter.Header.titleLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomOpenChannel.ViewConverter.Header.titleLabel.swift; sourceTree = ""; }; A64F81096C2DBE9610059AA9 /* ColorSet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ColorSet.swift; sourceTree = ""; }; @@ -1882,6 +1912,7 @@ BD1D2FF674AE425302A71903 /* SBURegisterOperatorModule.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SBURegisterOperatorModule.swift; sourceTree = ""; }; BD7AC31657ED213E32ECBD42 /* UIImageView+SBUIKit.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIImageView+SBUIKit.swift"; sourceTree = ""; }; BDE26BC1BAF5CEC2C2C5135F /* CustomGroupChannelSettings.SubView.Builder.userList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomGroupChannelSettings.SubView.Builder.userList.swift; sourceTree = ""; }; + BE7259CA31206A469BB51C25 /* SBUMessageFormSingleTextItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SBUMessageFormSingleTextItemView.swift; sourceTree = ""; }; BEFE3796A35F972AC95F9705 /* CustomGroupChannel.ViewConverter.Input.rightView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomGroupChannel.ViewConverter.Input.rightView.swift; sourceTree = ""; }; BF2C918C951707F9AC84EA4F /* OpenBannedUserListView+Item.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OpenBannedUserListView+Item.swift"; sourceTree = ""; }; BF61B959A003633F9C9EB07D /* SBUOpenChannelModule.Header+SwiftUI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SBUOpenChannelModule.Header+SwiftUI.swift"; sourceTree = ""; }; @@ -1931,7 +1962,6 @@ C9C266B8A2DF6EB7F1504782 /* CustomGroupChannelRegisterOperator.ViewConverter.Header.rightView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomGroupChannelRegisterOperator.ViewConverter.Header.rightView.swift; sourceTree = ""; }; CA1DD2E5AE504E97921165C8 /* CustomInviteUser.ViewConverter.List.userNameLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomInviteUser.ViewConverter.List.userNameLabel.swift; sourceTree = ""; }; CA33454C78840807B7841838 /* OpenBannedUserListViewConverter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenBannedUserListViewConverter.swift; sourceTree = ""; }; - CA500BA4D05A2F6897095A67 /* SBUFormViewParams.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SBUFormViewParams.swift; sourceTree = ""; }; CAC0DD1043216CB7D20D5D0C /* CustomOpenChannel.ViewConverter.List.userMessageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomOpenChannel.ViewConverter.List.userMessageView.swift; sourceTree = ""; }; CADB3058FF6F4B079FB37BB8 /* SBUBaseMessageCellParams.Deprecated.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SBUBaseMessageCellParams.Deprecated.swift; sourceTree = ""; }; CAF2F3B78EE368AFF2C5AAFC /* CustomOpenChannelList.SwiftUI.View.Main.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomOpenChannelList.SwiftUI.View.Main.swift; sourceTree = ""; }; @@ -1940,7 +1970,6 @@ CB4BEBA0FDEDCCE9A5804BB9 /* SBUOpenChannelCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SBUOpenChannelCell.swift; sourceTree = ""; }; CBD4AF9A5A39CD6EB954C272 /* SBUMessageTemplate.Syntax.Aligns.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SBUMessageTemplate.Syntax.Aligns.swift; sourceTree = ""; }; CBDE8C489CF491949336BA54 /* CreateGroupChannelView+ViewConverter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CreateGroupChannelView+ViewConverter.swift"; sourceTree = ""; }; - CC1973CFB8CDEF2249804990 /* SBUFormFieldView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SBUFormFieldView.swift; sourceTree = ""; }; CC3F606B79E0EAB2E982DC39 /* GroupOperatorListView+SubViewBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "GroupOperatorListView+SubViewBuilder.swift"; sourceTree = ""; }; CC66BFE52B21FA2B30AE8E30 /* SBUStackView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SBUStackView.swift; sourceTree = ""; }; CC6D68CE2E93880EB54D12ED /* UIScrollView+SBUIKit.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIScrollView+SBUIKit.swift"; sourceTree = ""; }; @@ -1961,6 +1990,7 @@ D123A133B83CA3E4C6A55CB3 /* CustomOpenMutedParticipantList.ViewConverter.Header.rightView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomOpenMutedParticipantList.ViewConverter.Header.rightView.swift; sourceTree = ""; }; D1ADCD8B486C3D101AA4CBF8 /* CustomOpenParticipantList.ViewConverter.List.entireView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomOpenParticipantList.ViewConverter.List.entireView.swift; sourceTree = ""; }; D1BE7FE947E6F68414C95978 /* CustomGroupChannelPushSettings.SwiftUI.View.CustomMain.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomGroupChannelPushSettings.SwiftUI.View.CustomMain.swift; sourceTree = ""; }; + D23CE29CE2785C1F89E0FE88 /* SBUMessageFormFallbackView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SBUMessageFormFallbackView.swift; sourceTree = ""; }; D26E0EABCE381C370B68E9A8 /* SBUOpenChannelListModule.Header+SwiftUI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SBUOpenChannelListModule.Header+SwiftUI.swift"; sourceTree = ""; }; D2CCFB76C36D83C085F65E5C /* SBUCategoryFilterCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SBUCategoryFilterCell.swift; sourceTree = ""; }; D2F9885440D0492C8904699E /* SBUMarkdownTransfer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SBUMarkdownTransfer.swift; sourceTree = ""; }; @@ -2048,6 +2078,7 @@ E672677E26CA7ADF545827E4 /* OpenModerationsViewConverter.Header.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenModerationsViewConverter.Header.swift; sourceTree = ""; }; E6AB5AF4E86499D0DFEE881B /* SBULayoutableButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SBULayoutableButton.swift; sourceTree = ""; }; E700A49675E9D9B07C018C41 /* SBUModuleSet.Deprecated.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SBUModuleSet.Deprecated.swift; sourceTree = ""; }; + E7F4A5C2DE942F918F081586 /* SBUMessageTemplateCell.MessageTemplateLayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SBUMessageTemplateCell.MessageTemplateLayer.swift; sourceTree = ""; }; E7FA5ACCAFC350D479002755 /* CustomGroupChannel.ViewConverter.Header.leftView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomGroupChannel.ViewConverter.Header.leftView.swift; sourceTree = ""; }; E85ED75EA5E22BAA62EBFB36 /* SBUInviteUserModule.Deprecated.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SBUInviteUserModule.Deprecated.swift; sourceTree = ""; }; E88D2D612D550B0EA07F62CD /* SBUMessageTemplate.ErrorMessages.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SBUMessageTemplate.ErrorMessages.swift; sourceTree = ""; }; @@ -2123,6 +2154,7 @@ FB5A989D9069803A11632F6A /* OpenChannelView+SubViewBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OpenChannelView+SubViewBuilder.swift"; sourceTree = ""; }; FB93B3DEC8E153D6A8BFC160 /* CustomCreateOpenChannel.SwiftUI.View.Main.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomCreateOpenChannel.SwiftUI.View.Main.swift; sourceTree = ""; }; FBC4A6592CF9264B9334EC0C /* CustomMessageThread.ViewConverter.Header.rightView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomMessageThread.ViewConverter.Header.rightView.swift; sourceTree = ""; }; + FCD53856699176A2A465111B /* SBUFormFieldView.Deprecated.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SBUFormFieldView.Deprecated.swift; sourceTree = ""; }; FD3B3C58D3785437EE1411F1 /* SBUColorSet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SBUColorSet.swift; sourceTree = ""; }; FD91A7FA49003653EAC40E13 /* OpenParticipantListViewConverter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenParticipantListViewConverter.swift; sourceTree = ""; }; FD96D51055C0724028FBB802 /* InviteUserView+ViewConverter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "InviteUserView+ViewConverter.swift"; sourceTree = ""; }; @@ -2165,15 +2197,6 @@ path = SwiftUI; sourceTree = ""; }; - 009DCA0B8DD2BCEE3C7A1B61 /* Forms */ = { - isa = PBXGroup; - children = ( - 1D9417F32EBBBF077BBFFD8B /* ViewParams */, - 5AA980813D5ABC96D7D2002A /* Views */, - ); - path = Forms; - sourceTree = ""; - }; 015143E27CE775FB5F5CEF12 /* ViewConverter */ = { isa = PBXGroup; children = ( @@ -2265,6 +2288,14 @@ path = ViewConverter; sourceTree = ""; }; + 06BA19501C8B082AE6526FD4 /* ViewParams */ = { + isa = PBXGroup; + children = ( + 09E6606143A887F262F79FA9 /* SBUMessageFormViewParams.swift */, + ); + path = ViewParams; + sourceTree = ""; + }; 09C770197FC73C6111FF9983 /* ViewConverters */ = { isa = PBXGroup; children = ( @@ -2590,6 +2621,7 @@ children = ( F566B012103F8EEBE03AECA3 /* BaseMessage+SBUIKit.MessageTemplate.swift */, C589684819C0917EE5FC82DE /* BaseMessage+SBUIKit.swift */, + 86D998504A47DE9BAAC543C1 /* MessageForm+SBUIKit.swift */, 5CB7111297AFFD4F44521579 /* MultipleFilesMessage+SBUIKit.swift */, ); path = ChatSDK; @@ -2677,14 +2709,6 @@ path = Syntax; sourceTree = ""; }; - 1D9417F32EBBBF077BBFFD8B /* ViewParams */ = { - isa = PBXGroup; - children = ( - CA500BA4D05A2F6897095A67 /* SBUFormViewParams.swift */, - ); - path = ViewParams; - sourceTree = ""; - }; 1DB640D00845527888B91E08 /* Params */ = { isa = PBXGroup; children = ( @@ -3167,6 +3191,7 @@ isa = PBXGroup; children = ( 563DF68B8CFC74728183C069 /* SBUMessageTemplate.Binder.swift */, + 130BDF243558344563BDD04F /* SBUMessageTemplate.Container.swift */, 01554179DBF097DA7646B2AE /* SBUMessageTemplate.Coordinator.swift */, 26E081F853537DD71E0BC25F /* SBUMessageTemplate.Payload.swift */, 54C28728C9C8AC31A2BFBAD1 /* SBUMessageTemplate.PayloadType.swift */, @@ -3202,6 +3227,7 @@ CB0B81E6414A56CEC58EB58E /* SBUBaseMessageCellParams.swift */, 8220E4F1F2BE2C124ED8B73A /* SBUFeedNotificationCellParams.swift */, 7AD2BD448D02C9A9B551AA86 /* SBUFileMessageCellParams.swift */, + 4E542BCE47135E2CF12B69A8 /* SBUMessageTemplateCellParams.swift */, 63888E8140CC9FF7B2967F6E /* SBUMultipleFilesMessageCellParams.swift */, A2B05FBDB7E619A9A48FA28F /* SBUTypingMessageCellParams.swift */, 3D5EA770115CE527CEF39044 /* SBUUnknownMessageCellParams.swift */, @@ -3376,6 +3402,9 @@ 0B26FE8DE186569C5BEFC548 /* SBUCoverImageView.Deprecated.swift */, 8A8F43AB90915B61D475F48A /* SBUEnums.Deprecated.swift */, 055A3B07C1A7654608413474 /* SBUForm.Deprecated.swift */, + FCD53856699176A2A465111B /* SBUFormFieldView.Deprecated.swift */, + 515A6FCA6E34F58EB62146AD /* SBUFormView.Deprecated.swift */, + 1DB8F69105F07B296E8C63F7 /* SBUFormViewParams.Deprecated.swift */, 505C2553E67C0C1CDDE626B2 /* SBUGlobals.Deprecated.swift */, B48EFC9732F79D6C644ABE86 /* SBUTableViewCell.Unavailable.swift */, F3D7ECABBD13ECA6EDB56017 /* SBUTheme.Deprecated.swift */, @@ -3422,6 +3451,20 @@ path = SwiftUI; sourceTree = ""; }; + 473E43FA14C87BF26F51A9FA /* Views */ = { + isa = PBXGroup; + children = ( + 5E7E39A804B558F66F7EB947 /* SBUMessageFormChipsItemView.swift */, + D23CE29CE2785C1F89E0FE88 /* SBUMessageFormFallbackView.swift */, + A4FE9D61A27CDE2A743D3CFE /* SBUMessageFormItemView.swift */, + 52F1328AE03EAAF1AD426D08 /* SBUMessageFormMultiTextItemView.swift */, + BE7259CA31206A469BB51C25 /* SBUMessageFormSingleTextItemView.swift */, + 384A0D8AB5B526308F89634E /* SBUMessageFormView.swift */, + D743D1ED45AAA2315FDB6614 /* SubViews */, + ); + path = Views; + sourceTree = ""; + }; 47C39D2BE9C14140C12B50E5 /* Protocol */ = { isa = PBXGroup; children = ( @@ -3681,15 +3724,6 @@ path = ViewConverters; sourceTree = ""; }; - 5AA980813D5ABC96D7D2002A /* Views */ = { - isa = PBXGroup; - children = ( - CC1973CFB8CDEF2249804990 /* SBUFormFieldView.swift */, - 4E9A72E3F57CDEED75481704 /* SBUFormView.swift */, - ); - path = Views; - sourceTree = ""; - }; 5B71D0B9AB321FE785DF7693 /* Channel */ = { isa = PBXGroup; children = ( @@ -3870,16 +3904,18 @@ 66E8A3A8B613A74979FB0FD2 /* SBUBaseMessageCell.swift */, 9FF73F1AC1586488A59B9BB9 /* SBUContentBaseMessageCell.swift */, 27F66C34721B8052037668EF /* SBUFileMessageCell.swift */, + E7F4A5C2DE942F918F081586 /* SBUMessageTemplateCell.MessageTemplateLayer.swift */, + 2F7CFF8FDC3CA84CA743B250 /* SBUMessageTemplateCell.swift */, + 374258FE63226944670893DB /* SBUMessageTemplateCellLayout.swift */, A0B27BB4D8B2F159844A77BF /* SBUTypingIndicatorMessageCell.swift */, 1CD45E3398D54B7995E3867F /* SBUUnknownMessageCell.swift */, - 8FDAE9FA5128686B5ABA8011 /* SBUUserMessageCell.MessageTemplate.swift */, 69FA8142D379C935F954206C /* SBUUserMessageCell.swift */, F8A9CA098992AC9A397860E6 /* CarouselView */, 26A47D9B3830215D4BA69EF6 /* CustomView */, C4C67B3ED5AFC52073C01656 /* Feedback */, D85A179C310192313FBAC03D /* FileMessageContentView */, - 009DCA0B8DD2BCEE3C7A1B61 /* Forms */, 3BAED34AF2354C77A3167F4E /* MessageCellParams */, + C9BF1ADC893574C285E9743D /* MessageForm */, 2CF9F5283FE2A1D0DCAB7BEA /* MultipleFilesMessage */, 91FF8520C435D3CACA86F75A /* NotificationChannel */, D82AB513C032DD5BF769BC9C /* OpenChannel */, @@ -5356,6 +5392,15 @@ path = OpenChannelList; sourceTree = ""; }; + C9BF1ADC893574C285E9743D /* MessageForm */ = { + isa = PBXGroup; + children = ( + 06BA19501C8B082AE6526FD4 /* ViewParams */, + 473E43FA14C87BF26F51A9FA /* Views */, + ); + path = MessageForm; + sourceTree = ""; + }; CC90673304787B93F2D1652A /* ScrollBottomView */ = { isa = PBXGroup; children = ( @@ -5578,6 +5623,14 @@ path = OpenParticipantList; sourceTree = ""; }; + D743D1ED45AAA2315FDB6614 /* SubViews */ = { + isa = PBXGroup; + children = ( + 904618EB6B0A591FC15D1542 /* SBUMessageFormChipView.swift */, + ); + path = SubViews; + sourceTree = ""; + }; D7DA8666256ADB2BE36019B1 /* ViewConverter */ = { isa = PBXGroup; children = ( @@ -5668,6 +5721,7 @@ 6AEF8D4FF705BB662A4BDC7A /* Thread+SBUIKit.swift */, 463AAB7723F705DF887E76BE /* UIApplication+SBUIKit.swift */, 2435FE6370AF68E18F345DD8 /* UIButton+SBUIKit.swift */, + 3064CE17D9A94272F21E223E /* UICollectionView+SBUIKit.swift */, A3CF699C92A450F181836CD9 /* UIColor+SBUIKit.swift */, 1286E9C8A5FDC69006356104 /* UIImage+SBUIKit.swift */, BD7AC31657ED213E32ECBD42 /* UIImageView+SBUIKit.swift */, @@ -5854,6 +5908,7 @@ E6C3AAB9BFE9E35C5F63F819 /* Model */ = { isa = PBXGroup; children = ( + 2C15ED108FD8018112C6E8B7 /* SBUError.swift */, 19A3BDA35036947D712CB9E1 /* SBUExtendedMessagePayload.swift */, 80EA4D44B6C5B0BF0C7543E6 /* SBUExtendedMessagePayloadForUI.swift */, 89F0ABB7E53466D01AC02983 /* SBUFeedbackAction.swift */, @@ -6132,6 +6187,7 @@ F680D037CDE9E80938B09CC6 /* SBUQuotedMessageViewProtocol.swift */, 55425279F87879EA393619E1 /* SBUQuoteMessageInputViewProtocol.swift */, E1D3679B4B2C92ABF5A1CF56 /* SBUTableViewCell.swift */, + 2752645C33797142AEE30A20 /* SBUTextView.swift */, 3A9D4CF5A5EE6E8646E37865 /* SBUView.swift */, 13C1A9FF2EF8EC07A88DF01C /* SBUViewLifeCycle.swift */, ); @@ -6295,62 +6351,804 @@ path = List; sourceTree = ""; }; - "TEMP_3705E1FC-1BDB-45D5-84B5-52BFDC494EA9" /* SubView */ = { + "TEMP_00C1DC87-BEB4-4D0B-9289-B424A4AC28A2" /* ViewModel */ = { + isa = PBXGroup; + children = ( + ); + path = ViewModel; + sourceTree = ""; + }; + "TEMP_0404CB60-76F7-439A-8B2A-DA7F12636F93" /* Common */ = { + isa = PBXGroup; + children = ( + ); + path = Common; + sourceTree = ""; + }; + "TEMP_04FCD1AD-6A23-4FF7-A3E3-35E5CD230391" /* Header */ = { + isa = PBXGroup; + children = ( + ); + path = Header; + sourceTree = ""; + }; + "TEMP_05FD2283-B64E-405D-92DC-C939B84672AF" /* Header */ = { + isa = PBXGroup; + children = ( + ); + path = Header; + sourceTree = ""; + }; + "TEMP_08DE343F-BAF9-40A1-BF6D-228FC4BD3648" /* Module */ = { + isa = PBXGroup; + children = ( + ); + path = Module; + sourceTree = ""; + }; + "TEMP_0A029107-6939-432B-906A-AB34D86DEEFF" /* ViewController */ = { + isa = PBXGroup; + children = ( + ); + path = ViewController; + sourceTree = ""; + }; + "TEMP_0AA039BD-811C-45E1-A472-274C89A41632" /* Media */ = { + isa = PBXGroup; + children = ( + ); + path = Media; + sourceTree = ""; + }; + "TEMP_10D1428C-6681-4961-BFAD-FAD7DA5DAC8F" /* Header */ = { + isa = PBXGroup; + children = ( + ); + path = Header; + sourceTree = ""; + }; + "TEMP_144D84CD-8E3A-49AD-A83D-85B5B44E701E" /* List */ = { + isa = PBXGroup; + children = ( + ); + path = List; + sourceTree = ""; + }; + "TEMP_1662EB54-C9CC-4AA4-8965-D861FF82E761" /* List */ = { + isa = PBXGroup; + children = ( + ); + path = List; + sourceTree = ""; + }; + "TEMP_1818B6C0-765F-42B9-9F02-2A58FB44B89E" /* SubView */ = { + isa = PBXGroup; + children = ( + ); + path = SubView; + sourceTree = ""; + }; + "TEMP_1E0BDA61-6E72-4B19-8280-D6CF545098D0" /* OpenUserList */ = { + isa = PBXGroup; + children = ( + ); + path = OpenUserList; + sourceTree = ""; + }; + "TEMP_245B285A-BE8F-4282-8434-8216E81C8308" /* Common */ = { + isa = PBXGroup; + children = ( + ); + path = Common; + sourceTree = ""; + }; + "TEMP_296D61AF-FE5D-4CFC-80FF-009795641A5F" /* Module */ = { + isa = PBXGroup; + children = ( + ); + path = Module; + sourceTree = ""; + }; + "TEMP_2AD70795-29AA-4FB1-AB50-FDC8AE092B4A" /* Module */ = { + isa = PBXGroup; + children = ( + ); + path = Module; + sourceTree = ""; + }; + "TEMP_2B60769F-A272-4C6C-8D4C-E0CC8AC33817" /* Module */ = { + isa = PBXGroup; + children = ( + ); + path = Module; + sourceTree = ""; + }; + "TEMP_2C51EA66-D974-47EC-B57C-9C2F3A1DCC54" /* ViewController */ = { + isa = PBXGroup; + children = ( + ); + path = ViewController; + sourceTree = ""; + }; + "TEMP_2DC9408B-D809-4797-AB73-F40775722A8B" /* GroupUserList */ = { + isa = PBXGroup; + children = ( + ); + path = GroupUserList; + sourceTree = ""; + }; + "TEMP_2E7536C1-72FC-4084-B589-C6D0C7C904B6" /* ViewController */ = { + isa = PBXGroup; + children = ( + ); + path = ViewController; + sourceTree = ""; + }; + "TEMP_2F15C2BA-5E57-445D-A776-8F9966E7C302" /* Module */ = { + isa = PBXGroup; + children = ( + ); + path = Module; + sourceTree = ""; + }; + "TEMP_30050B9F-2AF2-4B30-824E-2B5A4A1C1DED" /* SubView */ = { + isa = PBXGroup; + children = ( + ); + path = SubView; + sourceTree = ""; + }; + "TEMP_31D20EF7-4E0F-4402-B728-C181900551BD" /* SubView */ = { isa = PBXGroup; children = ( ); path = SubView; sourceTree = ""; }; - "TEMP_3FBA214E-C4B9-4EA0-B35C-E3EC9B9F2AE8" /* SubView */ = { + "TEMP_383E9268-41D2-4EE9-85C2-FBE797E3D292" /* SubView */ = { isa = PBXGroup; children = ( ); path = SubView; sourceTree = ""; }; - "TEMP_47BF45C9-E1D6-4691-BF7D-D8E499DE653E" /* SubView */ = { + "TEMP_3C8D6724-A1A3-465E-9652-020FEBD9E866" /* List */ = { + isa = PBXGroup; + children = ( + ); + path = List; + sourceTree = ""; + }; + "TEMP_3D1585FA-B322-40D7-87AD-AE2F92F4423F" /* ViewController */ = { + isa = PBXGroup; + children = ( + ); + path = ViewController; + sourceTree = ""; + }; + "TEMP_4184736E-FCF8-4EA1-ACA3-FE2DCEF0ED34" /* SubView */ = { isa = PBXGroup; children = ( ); path = SubView; sourceTree = ""; }; - "TEMP_4A683376-538F-4CD6-8FEF-FBA5E071A274" /* SubView */ = { + "TEMP_42051311-2B6D-4EC0-8F23-0F7FD688C814" /* Common */ = { + isa = PBXGroup; + children = ( + ); + path = Common; + sourceTree = ""; + }; + "TEMP_42CC7C17-D9E1-47A7-9377-4930F330536C" /* ViewModel */ = { + isa = PBXGroup; + children = ( + ); + path = ViewModel; + sourceTree = ""; + }; + "TEMP_49112FA1-1995-45C3-8B9B-229141A85527" /* ViewModel */ = { + isa = PBXGroup; + children = ( + ); + path = ViewModel; + sourceTree = ""; + }; + "TEMP_4AAF17E7-4293-4C8B-AAE4-81EC771000C1" /* Module */ = { + isa = PBXGroup; + children = ( + ); + path = Module; + sourceTree = ""; + }; + "TEMP_4AAF3B6E-2757-4758-88E9-5AD52BA5AC4F" /* List */ = { + isa = PBXGroup; + children = ( + ); + path = List; + sourceTree = ""; + }; + "TEMP_52EA991C-5361-46C1-A221-6BE439B52F8F" /* ViewController */ = { + isa = PBXGroup; + children = ( + ); + path = ViewController; + sourceTree = ""; + }; + "TEMP_54AA0A9E-47D5-4C41-8557-3FF0D4984346" /* Header */ = { + isa = PBXGroup; + children = ( + ); + path = Header; + sourceTree = ""; + }; + "TEMP_5577CA23-D791-45FB-8327-758BC0A8C58C" /* ViewModel */ = { + isa = PBXGroup; + children = ( + ); + path = ViewModel; + sourceTree = ""; + }; + "TEMP_562E31C6-43CD-46ED-868D-B0615600AE38" /* SubView */ = { isa = PBXGroup; children = ( ); path = SubView; sourceTree = ""; }; - "TEMP_4BB4B028-14B1-4884-B540-D65223CB49E4" /* SubView */ = { + "TEMP_576453DE-77A7-4D7E-986A-9FA77959D1B3" /* SubView */ = { isa = PBXGroup; children = ( ); path = SubView; sourceTree = ""; }; - "TEMP_74DF3411-7897-4498-B706-251373F04400" /* SubView */ = { + "TEMP_5CA73E82-B314-4C57-A5D7-6F1AE722873F" /* ViewModel */ = { + isa = PBXGroup; + children = ( + ); + path = ViewModel; + sourceTree = ""; + }; + "TEMP_5CBD6818-0609-47FC-AC91-5F8714EC3111" /* SubView */ = { isa = PBXGroup; children = ( ); path = SubView; sourceTree = ""; }; - "TEMP_981D7B83-1B32-4EB7-A883-F6E4FCDB761C" /* SubView */ = { + "TEMP_5D21DAEA-4BC5-4FF4-A0E7-4AA36D6B6C29" /* ViewController */ = { + isa = PBXGroup; + children = ( + ); + path = ViewController; + sourceTree = ""; + }; + "TEMP_5E8266E8-FDD5-4886-A471-240A51A63F36" /* Module */ = { + isa = PBXGroup; + children = ( + ); + path = Module; + sourceTree = ""; + }; + "TEMP_5F861CE4-1D33-4C88-A34D-00196944137A" /* ViewController */ = { + isa = PBXGroup; + children = ( + ); + path = ViewController; + sourceTree = ""; + }; + "TEMP_6104823B-E019-4458-81EA-F4206F19CD5B" /* ViewModel */ = { + isa = PBXGroup; + children = ( + ); + path = ViewModel; + sourceTree = ""; + }; + "TEMP_62E49CFE-50C3-420B-8B76-8662D0A1A788" /* SubView */ = { isa = PBXGroup; children = ( ); path = SubView; sourceTree = ""; }; - "TEMP_D3C1CDFE-3C33-4152-950B-25F8BD3F4058" /* SubView */ = { + "TEMP_661D5A7F-D4E2-4412-B17C-2608708337B7" /* ViewModel */ = { + isa = PBXGroup; + children = ( + ); + path = ViewModel; + sourceTree = ""; + }; + "TEMP_68C201DB-B03D-4770-B360-E34917A3F113" /* SubView */ = { isa = PBXGroup; children = ( ); path = SubView; sourceTree = ""; }; + "TEMP_6BEA445B-3AF8-49C4-8AC3-7D38FCFC8383" /* ViewController */ = { + isa = PBXGroup; + children = ( + ); + path = ViewController; + sourceTree = ""; + }; + "TEMP_6CF9F235-DAA3-41C4-AD13-0DA026ADE66E" /* Common */ = { + isa = PBXGroup; + children = ( + ); + path = Common; + sourceTree = ""; + }; + "TEMP_72157775-FA81-49FA-80E2-054E8BBBE81C" /* Header */ = { + isa = PBXGroup; + children = ( + ); + path = Header; + sourceTree = ""; + }; + "TEMP_74ABE359-6AA8-498B-91A2-C69F8CE3FA54" /* List */ = { + isa = PBXGroup; + children = ( + ); + path = List; + sourceTree = ""; + }; + "TEMP_74FCEFC7-D168-4601-98D7-7C9E949DAA1A" /* Theme */ = { + isa = PBXGroup; + children = ( + ); + path = Theme; + sourceTree = ""; + }; + "TEMP_74FF0044-1511-47B3-8DD2-EBAF64765221" /* Common */ = { + isa = PBXGroup; + children = ( + ); + path = Common; + sourceTree = ""; + }; + "TEMP_76A41946-8088-45F8-B4EA-9AA5228DA3C7" /* ViewController */ = { + isa = PBXGroup; + children = ( + ); + path = ViewController; + sourceTree = ""; + }; + "TEMP_76CBE126-EB17-42B0-BD92-430B3560779F" /* Header */ = { + isa = PBXGroup; + children = ( + ); + path = Header; + sourceTree = ""; + }; + "TEMP_76FE897C-1762-4E70-AF43-1CAD37F7ED69" /* Module */ = { + isa = PBXGroup; + children = ( + ); + path = Module; + sourceTree = ""; + }; + "TEMP_7A9EDAA3-83F4-499E-81EA-BE4AA08E69A6" /* Module */ = { + isa = PBXGroup; + children = ( + ); + path = Module; + sourceTree = ""; + }; + "TEMP_815C82E3-1D63-4D60-99CD-69828F21EE4C" /* ViewController */ = { + isa = PBXGroup; + children = ( + ); + path = ViewController; + sourceTree = ""; + }; + "TEMP_82455EB7-5123-4E9B-839E-C2F31EC35436" /* Header */ = { + isa = PBXGroup; + children = ( + ); + path = Header; + sourceTree = ""; + }; + "TEMP_87DA4E5E-7FBB-40D6-BD08-2FD66F01B001" /* List */ = { + isa = PBXGroup; + children = ( + ); + path = List; + sourceTree = ""; + }; + "TEMP_87E95E60-B55D-4826-AF8A-59BF1F9916C3" /* ViewModel */ = { + isa = PBXGroup; + children = ( + ); + path = ViewModel; + sourceTree = ""; + }; + "TEMP_8A9C3F53-96CB-4140-8D00-AC6CC09832A1" /* ViewModel */ = { + isa = PBXGroup; + children = ( + ); + path = ViewModel; + sourceTree = ""; + }; + "TEMP_8D892819-8345-49B0-BDD0-8387309A46A1" /* ViewController */ = { + isa = PBXGroup; + children = ( + ); + path = ViewController; + sourceTree = ""; + }; + "TEMP_90F9FCB1-D88B-40FF-8A19-63E36CEF714E" /* Module */ = { + isa = PBXGroup; + children = ( + ); + path = Module; + sourceTree = ""; + }; + "TEMP_91D842A5-8B13-494C-A8B1-BA66068AB790" /* Header */ = { + isa = PBXGroup; + children = ( + ); + path = Header; + sourceTree = ""; + }; + "TEMP_95C8899C-ABD6-434B-B6F4-F4EBF49A506E" /* ProfileInput */ = { + isa = PBXGroup; + children = ( + ); + path = ProfileInput; + sourceTree = ""; + }; + "TEMP_98020E12-E744-460C-850B-3184CFDDEB58" /* SubView */ = { + isa = PBXGroup; + children = ( + ); + path = SubView; + sourceTree = ""; + }; + "TEMP_992CFAA0-56DA-4CB2-9397-4E1F4C1A4EFC" /* Module */ = { + isa = PBXGroup; + children = ( + ); + path = Module; + sourceTree = ""; + }; + "TEMP_997F9278-80B4-47FB-A24F-33FE46F51BF7" /* Header */ = { + isa = PBXGroup; + children = ( + ); + path = Header; + sourceTree = ""; + }; + "TEMP_9B11E5C4-9BEA-45FC-AC31-55B9F69CDBC8" /* Input */ = { + isa = PBXGroup; + children = ( + ); + path = Input; + sourceTree = ""; + }; + "TEMP_9C11B0D7-B44B-4D95-88FA-5A777A879584" /* Module */ = { + isa = PBXGroup; + children = ( + ); + path = Module; + sourceTree = ""; + }; + "TEMP_9F9F2020-08A0-4791-9EC0-99B3585A3E1F" /* Input */ = { + isa = PBXGroup; + children = ( + ); + path = Input; + sourceTree = ""; + }; + "TEMP_A31DB68B-4D46-42DD-9E04-28B53A258799" /* ChannelList */ = { + isa = PBXGroup; + children = ( + ); + path = ChannelList; + sourceTree = ""; + }; + "TEMP_A5BA7361-190C-419F-8D31-FABE40024911" /* List */ = { + isa = PBXGroup; + children = ( + ); + path = List; + sourceTree = ""; + }; + "TEMP_A7214DC0-2F84-4F5E-9E31-880BE8FFBDF3" /* ViewModel */ = { + isa = PBXGroup; + children = ( + ); + path = ViewModel; + sourceTree = ""; + }; + "TEMP_AA64EDA0-2EDF-4D87-98AB-5C932F126677" /* Module */ = { + isa = PBXGroup; + children = ( + ); + path = Module; + sourceTree = ""; + }; + "TEMP_ACD89950-FCEF-4A25-BE9A-0D5DB219DFA7" /* ViewController */ = { + isa = PBXGroup; + children = ( + ); + path = ViewController; + sourceTree = ""; + }; + "TEMP_AEB6E47A-78E6-40BD-B081-2CB837E44F6A" /* ViewModel */ = { + isa = PBXGroup; + children = ( + ); + path = ViewModel; + sourceTree = ""; + }; + "TEMP_AFF5B430-C45B-4F00-B439-79BEDBB840E0" /* Header */ = { + isa = PBXGroup; + children = ( + ); + path = Header; + sourceTree = ""; + }; + "TEMP_B2FD5813-91EC-4DA4-B00E-B49ADF33142A" /* Module */ = { + isa = PBXGroup; + children = ( + ); + path = Module; + sourceTree = ""; + }; + "TEMP_B890AC70-9D31-4535-BECD-EB6D7BF74AB2" /* ViewModel */ = { + isa = PBXGroup; + children = ( + ); + path = ViewModel; + sourceTree = ""; + }; + "TEMP_B9D481C6-FDA3-41CC-949A-850C1B67B0A7" /* SubView */ = { + isa = PBXGroup; + children = ( + ); + path = SubView; + sourceTree = ""; + }; + "TEMP_BB57748F-09BF-4D90-8D67-39DE285DBA13" /* List */ = { + isa = PBXGroup; + children = ( + ); + path = List; + sourceTree = ""; + }; + "TEMP_BBBAC5A0-F3FB-4656-AA23-2245498622DB" /* ViewController */ = { + isa = PBXGroup; + children = ( + ); + path = ViewController; + sourceTree = ""; + }; + "TEMP_BEDC1B44-6E2B-4882-8A75-4C30CA414909" /* Module */ = { + isa = PBXGroup; + children = ( + ); + path = Module; + sourceTree = ""; + }; + "TEMP_C1E947F7-CFDA-4D91-B9A3-E7C3E4B9FE15" /* List */ = { + isa = PBXGroup; + children = ( + ); + path = List; + sourceTree = ""; + }; + "TEMP_C1F8D1E0-CAC1-4DE9-9014-B21D0B178E53" /* ViewModel */ = { + isa = PBXGroup; + children = ( + ); + path = ViewModel; + sourceTree = ""; + }; + "TEMP_C4A63F1F-73FB-4F40-8F24-51DF96378203" /* List */ = { + isa = PBXGroup; + children = ( + ); + path = List; + sourceTree = ""; + }; + "TEMP_C9C5B29B-8BA6-4B56-9809-A14933C51F1D" /* ViewModel */ = { + isa = PBXGroup; + children = ( + ); + path = ViewModel; + sourceTree = ""; + }; + "TEMP_CAE1A252-0C53-43B4-80A5-FAFF6ADD2533" /* ViewModel */ = { + isa = PBXGroup; + children = ( + ); + path = ViewModel; + sourceTree = ""; + }; + "TEMP_CD5F7C5E-A92C-4C91-BC19-CC19F35A30E7" /* ViewModel */ = { + isa = PBXGroup; + children = ( + ); + path = ViewModel; + sourceTree = ""; + }; + "TEMP_CE286E35-2931-4AE4-8988-0672735D0E6F" /* ViewController */ = { + isa = PBXGroup; + children = ( + ); + path = ViewController; + sourceTree = ""; + }; + "TEMP_CE4C14FB-9E46-4180-8E3C-B86847A15AB1" /* Header */ = { + isa = PBXGroup; + children = ( + ); + path = Header; + sourceTree = ""; + }; + "TEMP_CEBDB265-CD75-4D34-9D25-B03A7A4181A2" /* Common */ = { + isa = PBXGroup; + children = ( + ); + path = Common; + sourceTree = ""; + }; + "TEMP_CF7607DC-BE68-443C-9B13-A6E79F58C11B" /* List */ = { + isa = PBXGroup; + children = ( + ); + path = List; + sourceTree = ""; + }; + "TEMP_D2F3675A-11E2-4EA6-A47F-F677AF8CEADE" /* SubView */ = { + isa = PBXGroup; + children = ( + ); + path = SubView; + sourceTree = ""; + }; + "TEMP_D3CD6D21-252C-43F3-A1CC-307975E53164" /* Header */ = { + isa = PBXGroup; + children = ( + ); + path = Header; + sourceTree = ""; + }; + "TEMP_D705BD99-C526-4B92-BFDF-268A4DAE2347" /* Input */ = { + isa = PBXGroup; + children = ( + ); + path = Input; + sourceTree = ""; + }; + "TEMP_D72F90F8-9E1E-4E7D-B70A-6F163CC32998" /* Module */ = { + isa = PBXGroup; + children = ( + ); + path = Module; + sourceTree = ""; + }; + "TEMP_D8BECCFD-2E13-49D8-81B7-70BF9399A7AD" /* List */ = { + isa = PBXGroup; + children = ( + ); + path = List; + sourceTree = ""; + }; + "TEMP_DA459AC6-F863-4A10-B472-9545CEBF223E" /* Common */ = { + isa = PBXGroup; + children = ( + ); + path = Common; + sourceTree = ""; + }; + "TEMP_DB46A9B5-D084-481F-9935-E6008406818B" /* List */ = { + isa = PBXGroup; + children = ( + ); + path = List; + sourceTree = ""; + }; + "TEMP_DFDE67C1-992D-4682-887A-83E6000A99F3" /* ViewController */ = { + isa = PBXGroup; + children = ( + ); + path = ViewController; + sourceTree = ""; + }; + "TEMP_E06E2802-B917-467D-B37C-BD3DFE1E6595" /* List */ = { + isa = PBXGroup; + children = ( + ); + path = List; + sourceTree = ""; + }; + "TEMP_EBA5522C-DFFB-4B3A-8D9E-9A36D1A0A70D" /* Header */ = { + isa = PBXGroup; + children = ( + ); + path = Header; + sourceTree = ""; + }; + "TEMP_ECB16E58-F71D-4A6F-9FE1-836981F07517" /* List */ = { + isa = PBXGroup; + children = ( + ); + path = List; + sourceTree = ""; + }; + "TEMP_ED85BD6B-C46D-4D54-A76E-F76CE4AE04EE" /* List */ = { + isa = PBXGroup; + children = ( + ); + path = List; + sourceTree = ""; + }; + "TEMP_EEEA0B52-7595-4F45-AF5F-DCEFAC655449" /* ViewModel */ = { + isa = PBXGroup; + children = ( + ); + path = ViewModel; + sourceTree = ""; + }; + "TEMP_EEEAA7B8-D9A7-4CAC-BF27-76FA5D32D828" /* Header */ = { + isa = PBXGroup; + children = ( + ); + path = Header; + sourceTree = ""; + }; + "TEMP_F15543C3-0193-4D63-B680-50D27B11E587" /* ViewController */ = { + isa = PBXGroup; + children = ( + ); + path = ViewController; + sourceTree = ""; + }; + "TEMP_F7CEC51D-0645-419A-BA55-CCC773953347" /* Header */ = { + isa = PBXGroup; + children = ( + ); + path = Header; + sourceTree = ""; + }; + "TEMP_F95CAD70-2D58-476B-851E-9C1F0101A56A" /* Header */ = { + isa = PBXGroup; + children = ( + ); + path = Header; + sourceTree = ""; + }; + "TEMP_FA22DEAA-20DC-4EC3-9D18-748081664003" /* ViewController */ = { + isa = PBXGroup; + children = ( + ); + path = ViewController; + sourceTree = ""; + }; + "TEMP_FC549662-1520-4980-8389-D3022EB5909D" /* Common */ = { + isa = PBXGroup; + children = ( + ); + path = Common; + sourceTree = ""; + }; + "TEMP_FCE49633-B863-468D-B9F8-376555B50B1D" /* Module */ = { + isa = PBXGroup; + children = ( + ); + path = Module; + sourceTree = ""; + }; + "TEMP_FE50123F-8117-47C9-9E01-4CB5FFA63591" /* Header */ = { + isa = PBXGroup; + children = ( + ); + path = Header; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -6894,6 +7692,7 @@ 02D7FB791629405E7B868FB5 /* InviteUserViewConverter.swift in Sources */, 6FEB53022DA164CA48ED8F6E /* LoginView.swift in Sources */, 786261EBAF78615F8AFEAF41 /* MainView.swift in Sources */, + 201120B35CFB5D19CCF88E89 /* MessageForm+SBUIKit.swift in Sources */, F16A0166E2F1F0B4B596C82A /* MessageSearchView+Item.swift in Sources */, 930F1FD0E06245D81E40DCB6 /* MessageSearchView+SubViewBuilder.swift in Sources */, 2EAA52382A0E8C5B2E941005 /* MessageSearchView+ViewConverter.swift in Sources */, @@ -7106,6 +7905,7 @@ 027F85F8D05965EA72E35649 /* SBUEmptyView.swift in Sources */, D2D3898FA489DEC8B003670B /* SBUEnums.Deprecated.swift in Sources */, 56A4448631C5FBAD4A47BA51 /* SBUEnums.swift in Sources */, + 3513E41C4A69F8983B75B7C1 /* SBUError.swift in Sources */, C92800E91681A1906102B807 /* SBUExtendedMessagePayload.swift in Sources */, DA6E618B26EF3A66E44CFE4B /* SBUExtendedMessagePayloadCustomViewFactory.swift in Sources */, F20DF8AC3F5E34ED6E8B7D79 /* SBUExtendedMessagePayloadForUI.swift in Sources */, @@ -7127,9 +7927,9 @@ 2DB6CA0CC0BB531B7E073BFE /* SBUFileViewController.swift in Sources */, 66ADA4F8CFC6EB6A77D60B1B /* SBUFontSet.swift in Sources */, E2F52C7E7ADB0B8114C5CDD3 /* SBUForm.Deprecated.swift in Sources */, - B0205DE7C34AD49E722B49E3 /* SBUFormFieldView.swift in Sources */, - D4E6406BF304B2FCC0AF2ADC /* SBUFormView.swift in Sources */, - 76DEE0DC4C4B264BEDC939AD /* SBUFormViewParams.swift in Sources */, + AF83514E03BDB74C06340303 /* SBUFormFieldView.Deprecated.swift in Sources */, + 2637DC0A0C8F965A37AE303E /* SBUFormView.Deprecated.swift in Sources */, + 2396C41E13FDB99A2EC97888 /* SBUFormViewParams.Deprecated.swift in Sources */, EF2DDFC6BA4286E5C6976A10 /* SBUGlobalCustomParams.swift in Sources */, B90B393ED6F9C556DC4ADE50 /* SBUGlobals.Deprecated.swift in Sources */, AF20C220160DCC107926F516 /* SBUGlobals.swift in Sources */, @@ -7227,6 +8027,14 @@ 6A06C04DE0DE920B35519A29 /* SBUMessageCellConfiguration.swift in Sources */, 7F315F24B4D8C1F258FCA8C1 /* SBUMessageCellProtocol.swift in Sources */, EFB6D9EBA3D6FE450C063CFC /* SBUMessageDateView.swift in Sources */, + 1EB0F0FB0921F42151B381F8 /* SBUMessageFormChipView.swift in Sources */, + D78B56FF68EC5CAF2F39F522 /* SBUMessageFormChipsItemView.swift in Sources */, + BDF9A8DE063A5D26AD903A5A /* SBUMessageFormFallbackView.swift in Sources */, + F0D502B07BE7FF2D8FDE7967 /* SBUMessageFormItemView.swift in Sources */, + 6F337F85F1EC26FB5747C0B0 /* SBUMessageFormMultiTextItemView.swift in Sources */, + D0B9BCABF4D8E0174A29C3C7 /* SBUMessageFormSingleTextItemView.swift in Sources */, + 1CDB503F30FC2AB8FF73576F /* SBUMessageFormView.swift in Sources */, + 6E56A4C38E8FB2A9ABFE5100 /* SBUMessageFormViewParams.swift in Sources */, 4B12AC423B1E57AFD2800C84 /* SBUMessageInputMode.swift in Sources */, 70D3891EF8F0F4988D1F82A6 /* SBUMessageInputView+SwiftUI.swift in Sources */, BD6080969BB3E78B1ED0C952 /* SBUMessageInputView.swift in Sources */, @@ -7245,6 +8053,7 @@ EE6F7874CA43EF4B1374DC47 /* SBUMessageStateView.swift in Sources */, 413DA6431A1DF4B2285ACC61 /* SBUMessageTemplate.Action.swift in Sources */, 2974135DF76DB03A6D24D1FF /* SBUMessageTemplate.Binder.swift in Sources */, + 790BDF268EE5BE9FF7D4C985 /* SBUMessageTemplate.Container.swift in Sources */, 9864666F137B4E39C81300D8 /* SBUMessageTemplate.Coordinator.swift in Sources */, 1623C20672C40F24BECBD4B0 /* SBUMessageTemplate.Decoders.swift in Sources */, 2AF12799601EBC9F368DCE25 /* SBUMessageTemplate.ErrorMessages.swift in Sources */, @@ -7268,6 +8077,10 @@ C6EE61C38C7751AC1FE367E5 /* SBUMessageTemplate.Syntax.Views.swift in Sources */, 19DD09C33C5BA282969E440D /* SBUMessageTemplate.TemplateList.swift in Sources */, E2A5C652B7FB5E678480D382 /* SBUMessageTemplate.swift in Sources */, + 73A07126956A34CDDF79C72B /* SBUMessageTemplateCell.MessageTemplateLayer.swift in Sources */, + 978C78CD1448E70538A47AB8 /* SBUMessageTemplateCell.swift in Sources */, + 0B873482FEFADB21E16A6EEE /* SBUMessageTemplateCellLayout.swift in Sources */, + 3352E799FEA119A35F28E0ED /* SBUMessageTemplateCellParams.swift in Sources */, 73CE000A9B2FA50F39AC6B1B /* SBUMessageTemplateManager.swift in Sources */, 602D59F11EFE41978C1901E7 /* SBUMessageThreadInputView+SwiftUI.swift in Sources */, 341397411E718FA1877632CE /* SBUMessageThreadModule.Deprecated.swift in Sources */, @@ -7417,6 +8230,7 @@ C5732121655B28735339013C /* SBUTableViewCell.swift in Sources */, C311F7EF246404831952FCDF /* SBUTemplateLabel.swift in Sources */, 5E6AF1F62A480310EAE344BE /* SBUTemplateType.swift in Sources */, + F8F0B40DE98424420598D32A /* SBUTextView.swift in Sources */, A24923CE6B533A8BF0480F8E /* SBUTheme+Type.swift in Sources */, B992CEB2B4C62A6A3FC62385 /* SBUTheme.Deprecated.swift in Sources */, 7FA8937FB07369062A7EBAFE /* SBUTheme.swift in Sources */, @@ -7443,7 +8257,6 @@ 01198926DBBD9D8D39D76595 /* SBUUserListViewController.swift in Sources */, 29BE03D00D366B85209BD197 /* SBUUserListViewModel.swift in Sources */, 21A67DAEA05E61BC99F5D0CB /* SBUUserMentionConfiguration.swift in Sources */, - 65C75B61D994D49E029BAF6A /* SBUUserMessageCell.MessageTemplate.swift in Sources */, 18157D39446334925AAAAF9B /* SBUUserMessageCell.swift in Sources */, 237893FFC266BAB3578B4489 /* SBUUserMessageCellParams.swift in Sources */, 921EFBA21BE951E3863281E8 /* SBUUserMessageTextView.swift in Sources */, @@ -7477,6 +8290,7 @@ D3775D72291D8243C42B000E /* Thread+SBUIKit.swift in Sources */, 629055AEB92A061CA912F275 /* UIApplication+SBUIKit.swift in Sources */, F2A5904C3A514379ED495E33 /* UIButton+SBUIKit.swift in Sources */, + F78D508B82FF6AF8575ECC04 /* UICollectionView+SBUIKit.swift in Sources */, D7531BC55DDC2FD53CFCF70A /* UIColor+SBUIKit.swift in Sources */, 265BA70DB6A018F1AE1D2ECE /* UIFont+Sendbird.swift in Sources */, ACB2B91137FA560631B8E21C /* UIImage+SBUIKit.swift in Sources */, @@ -7530,7 +8344,7 @@ "@executable_path/Frameworks", "@loader_path/Frameworks", ); - MARKETING_VERSION = "1.0.0-beta.1"; + MARKETING_VERSION = "1.0.0-beta.2"; PRODUCT_BUNDLE_IDENTIFIER = com.sendbird.swiftui.sample; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = iphoneos; @@ -7563,7 +8377,7 @@ "@executable_path/Frameworks", "@loader_path/Frameworks", ); - MARKETING_VERSION = "1.0.0-beta.1"; + MARKETING_VERSION = "1.0.0-beta.2"; PRODUCT_BUNDLE_IDENTIFIER = com.sendbird.swiftui.sample.SwiftUINotificationService; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = iphoneos; @@ -7654,7 +8468,7 @@ "@executable_path/Frameworks", "@loader_path/Frameworks", ); - MARKETING_VERSION = "1.0.0-beta.1"; + MARKETING_VERSION = "1.0.0-beta.2"; PRODUCT_BUNDLE_IDENTIFIER = com.sendbird.swiftui.sample; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = iphoneos; @@ -7742,7 +8556,7 @@ "@executable_path/Frameworks", "@loader_path/Frameworks", ); - MARKETING_VERSION = "1.0.0-beta.1"; + MARKETING_VERSION = "1.0.0-beta.2"; PRODUCT_BUNDLE_IDENTIFIER = com.sendbird.swiftui.sample.SwiftUINotificationService; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = iphoneos; @@ -7791,7 +8605,7 @@ repositoryURL = "https://github.com/sendbird/sendbird-chat-sdk-ios"; requirement = { kind = upToNextMajorVersion; - minimumVersion = 4.20.0; + minimumVersion = 4.21.1; }; }; /* End XCRemoteSwiftPackageReference section */ diff --git a/Sample/project.yml b/Sample/project.yml index 865e039..c7b102c 100644 --- a/Sample/project.yml +++ b/Sample/project.yml @@ -12,7 +12,7 @@ options: packages: SendbirdChatSDK: url: https://github.com/sendbird/sendbird-chat-sdk-ios - from: 4.20.0 + from: 4.21.1 schemes: QuickStartSwiftUI: @@ -41,7 +41,7 @@ settingGroups: FRAMEWORK_SEARCH_PATHS: '' IPHONEOS_DEPLOYMENT_TARGET: '15.0' LD_RUNPATH_SEARCH_PATHS: ["$(inherited)", "@executable_path/Frameworks", "@loader_path/Frameworks"] - MARKETING_VERSION: '1.0.0-beta.1' + MARKETING_VERSION: '1.0.0-beta.2' PRODUCT_NAME: "$(TARGET_NAME)" SDKROOT: iphoneos SWIFT_VERSION: '5.0' diff --git a/SendbirdSwiftUI.podspec b/SendbirdSwiftUI.podspec index 6b87d2f..1ba3c40 100644 --- a/SendbirdSwiftUI.podspec +++ b/SendbirdSwiftUI.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = "SendbirdSwiftUI" - s.version = "1.0.0-beta.1" + s.version = "1.0.0-beta.2" s.summary = "Sendbird SwiftUI SDK based on SendbirdChatSDK" s.description = "SendbirdSwiftUI is a framework composed of basic UI components based on SwiftUI and SendbirdChatSDK." s.homepage = "https://sendbird.com" @@ -16,10 +16,10 @@ Pod::Spec.new do |s| "Kai" => "kai.lee@sendbird.com" } s.platform = :ios, "15.0" - s.source = { :http => "https://github.com/sendbird/sendbird-swiftui-ios/releases/download/#{s.version}/SendbirdSwiftUI.zip", :sha1 => "b6615fddbfb97ceec73f837365724f467d28660b" } + s.source = { :http => "https://github.com/sendbird/sendbird-swiftui-ios/releases/download/#{s.version}/SendbirdSwiftUI.zip", :sha1 => "7707988dde5aa55cea4af0f46c4c76f869300699" } s.ios.vendored_frameworks = 'SendbirdSwiftUI/SendbirdSwiftUI.xcframework' s.ios.frameworks = ["UIKit", "SwiftUI", "Foundation", "CoreData", "SendbirdChatSDK"] s.requires_arc = true - s.dependency "SendbirdChatSDK", ">= 4.20.0" + s.dependency "SendbirdChatSDK", ">= 4.21.1" s.ios.library = "icucore" end diff --git a/Sources/Constant/SBUStringSet.swift b/Sources/Constant/SBUStringSet.swift index 1ac5311..a84815d 100644 --- a/Sources/Constant/SBUStringSet.swift +++ b/Sources/Constant/SBUStringSet.swift @@ -328,6 +328,10 @@ public class SBUStringSet { // MARK: - form type public static var FormType_Optional = "(optional)" // 3.11.0 public static var FormType_Error_Default = "Please check the value" // 3.11.0 + public static var FormType_Error_Required = "This field is required" // 3.27.0 + public static var FormType_Fallback_Message = "Form type messages are not available in this version." // 3.27.0 + public static var FormType_Submit_Done = "Submitted successfully" // 3.27.0 + public static var FormType_No_Reponse = "No response" // 3.27.0 // MARK: - Feedback public static var Feedback_Comment_Title = "Provide additional feedback (optional)" // 3.15.0 diff --git a/Sources/Deprecated/SBUForm.Deprecated.swift b/Sources/Deprecated/SBUForm.Deprecated.swift index e33aad9..62e7d0d 100644 --- a/Sources/Deprecated/SBUForm.Deprecated.swift +++ b/Sources/Deprecated/SBUForm.Deprecated.swift @@ -183,6 +183,7 @@ extension SBUUserMessageCell { public func updateFormView(with forms: [SBUForm]?, answers: [SBUForm.Answer]) -> Bool { false } } +@available(*, unavailable, message: "This model is no longer used internally.") extension SBUFormViewDelegate { /// Called when `form` is submitted. /// - Parameters: @@ -199,6 +200,7 @@ extension SBUFormViewDelegate { func formView(_ view: SBUFormView, didUpdate answer: SBUForm.Answer) { } } +@available(*, unavailable, message: "This model is no longer used internally.") extension SBUFormFieldViewDelegate { /// Called when `SBUForm.Field` is updated. /// - Parameters: @@ -208,6 +210,7 @@ extension SBUFormFieldViewDelegate { func formFieldView(_ fieldView: SBUFormFieldView, didUpdate updated: SBUForm.Field.Updated) { } } +@available(*, unavailable, message: "This method is deprecated in 3.27.0") extension SBUFormViewParams { @available(*, unavailable, message: "This model is no longer used internally. Changed to use `SendbirdChatSDK.Form`.") init(messageId: Int64, form: SBUForm) { @@ -215,6 +218,7 @@ extension SBUFormViewParams { } } +@available(*, unavailable, message: "This model is no longer used internally.") extension SBUFormView { /// Memory cached answer data. @available(*, unavailable, message: "This model is no longer used internally. Changed to use `SendbirdChatSDK.Form`.") @@ -232,6 +236,7 @@ extension SBUFormView { public func formFieldView(_ view: SBUFormFieldView, didUpdate updated: SBUForm.Field.Updated) { } } +@available(*, unavailable, message: "This model is no longer used internally.") extension SBUFormFieldView { // MARK: - Configure @@ -277,6 +282,7 @@ extension SBUGroupChannelViewController { public func groupChannelModule(_ listComponent: SBUGroupChannelModule.List, answersFor messageId: Int64?) -> [SBUForm.Answer]? { nil } } +@available(*, unavailable, message: "This model is no longer used internally.") extension SBUFormFieldView.StatusType { @available(*, unavailable, message: "This model is no longer used internally. Changed to use `SendbirdChatSDK.Form`.") public init(form: SBUForm, field: SBUForm.Field, value: String?) { diff --git a/Sources/View/Channel/MessageCell/Forms/Views/SBUFormFieldView.swift b/Sources/Deprecated/SBUFormFieldView.Deprecated.swift similarity index 95% rename from Sources/View/Channel/MessageCell/Forms/Views/SBUFormFieldView.swift rename to Sources/Deprecated/SBUFormFieldView.Deprecated.swift index 3555489..fffff09 100644 --- a/Sources/View/Channel/MessageCell/Forms/Views/SBUFormFieldView.swift +++ b/Sources/Deprecated/SBUFormFieldView.Deprecated.swift @@ -10,6 +10,7 @@ import UIKit import SendbirdChatSDK /// - Since: 3.11.0 +@available(*, deprecated, message: "This method is deprecated in 3.27.0") public protocol SBUFormFieldViewDelegate: AnyObject { /// Called when `SBUForm.Field` is updated. /// - Parameters: @@ -19,6 +20,7 @@ public protocol SBUFormFieldViewDelegate: AnyObject { } /// - Since: 3.11.0 +@available(*, deprecated, message: "This method is deprecated in 3.27.0", renamed: "SBUMessageFormItemView") public class SBUFormFieldView: SBUView, UITextFieldDelegate { // MARK: - Properties @@ -68,6 +70,7 @@ public class SBUFormFieldView: SBUView, UITextFieldDelegate { } /// - Since: 3.11.0 +@available(*, deprecated, message: "This method is deprecated in 3.27.0", renamed: "SBUMessageFormItemView") public class SBUSimpleFormFieldView: SBUFormFieldView { /// A vertical stack view to configure layouts of the fields. @@ -156,7 +159,7 @@ public class SBUSimpleFormFieldView: SBUFormFieldView { self.errorTitleView.sbu_constraint(height: 12) - self.topSpaceView.sbu_constraint(width: 1, height: 8) + self.topSpaceView.sbu_constraint(width: 1, height: 6) self.bottomSpaceView.sbu_constraint(width: 1, height: 4) self.inputFieldView @@ -261,9 +264,11 @@ public class SBUSimpleFormFieldView: SBUFormFieldView { } +@available(*, deprecated, message: "This method is deprecated in 3.27.0") extension SBUFormFieldView { /// Enum model to indicate the status of the value in the currently entered field. /// - Since: 3.11.0 + @available(*, deprecated, message: "This method is deprecated in 3.27.0") public enum StatusType { /// Represents a completed form field with a value. case done(value: String) @@ -341,6 +346,7 @@ extension SBUFormFieldView { /// Indicates the input type of the field. /// Can be used to specify the keyboard type. /// - Since: 3.16.0 +@available(*, deprecated, message: "This method is deprecated in 3.27.0") public enum SBUFormFieldInputType: String, Codable { case text // default value. case phone diff --git a/Sources/View/Channel/MessageCell/Forms/Views/SBUFormView.swift b/Sources/Deprecated/SBUFormView.Deprecated.swift similarity index 95% rename from Sources/View/Channel/MessageCell/Forms/Views/SBUFormView.swift rename to Sources/Deprecated/SBUFormView.Deprecated.swift index 8144c3d..251cb87 100644 --- a/Sources/View/Channel/MessageCell/Forms/Views/SBUFormView.swift +++ b/Sources/Deprecated/SBUFormView.Deprecated.swift @@ -10,6 +10,7 @@ import UIKit import SendbirdChatSDK /// - Since: 3.11.0 +@available(*, deprecated, message: "This method is deprecated in 3.27.0") public protocol SBUFormViewDelegate: AnyObject { /// Called when `form` is submitted. /// - Parameters: @@ -20,6 +21,7 @@ public protocol SBUFormViewDelegate: AnyObject { } /// - Since: 3.11.0 +@available(*, deprecated, message: "This method is deprecated in 3.27.0", renamed: "SBUMessageFormView") open class SBUFormView: SBUView, SBUFormFieldViewDelegate { public var theme: SBUMessageCellTheme = SBUTheme.messageCellTheme @@ -83,6 +85,7 @@ open class SBUFormView: SBUView, SBUFormFieldViewDelegate { } /// - Since: 3.11.0 +@available(*, deprecated, message: "This method is deprecated in 3.27.0", renamed: "SBUMessageFormView") open class SBUSimpleFormView: SBUFormView { // views diff --git a/Sources/View/Channel/MessageCell/Forms/ViewParams/SBUFormViewParams.swift b/Sources/Deprecated/SBUFormViewParams.Deprecated.swift similarity index 86% rename from Sources/View/Channel/MessageCell/Forms/ViewParams/SBUFormViewParams.swift rename to Sources/Deprecated/SBUFormViewParams.Deprecated.swift index b4d143c..0b4b5a5 100644 --- a/Sources/View/Channel/MessageCell/Forms/ViewParams/SBUFormViewParams.swift +++ b/Sources/Deprecated/SBUFormViewParams.Deprecated.swift @@ -11,6 +11,7 @@ import SendbirdChatSDK /// The data model used for configuring ``SBUFormView``. /// - Since: 3.11.0 +@available(*, deprecated, message: "This method is deprecated in 3.27.0") public struct SBUFormViewParams { // MARK: - Properties /// The ID of the message that provides form. diff --git a/Sources/Extension/Array+SBUIKit.swift b/Sources/Extension/Array+SBUIKit.swift index 97e9d5e..6b7ceda 100644 --- a/Sources/Extension/Array+SBUIKit.swift +++ b/Sources/Extension/Array+SBUIKit.swift @@ -69,8 +69,9 @@ extension Array where Element: BaseMessage { guard message.updatedAt == 0 else { return nil } guard message.sendingStatus == .succeeded else { return nil } guard message.sender?.userId != SBUGlobals.currentUser?.userId else { return nil } - - return message + if message.isStreamMessage == true { return message } + if latestMessage.isStreamMessage == true { return message } + return nil } } @@ -114,3 +115,35 @@ extension Array { return indices.contains(index) ? self[index] : nil } } + +extension Array where Element == String { + func toggle(_ value: String) -> [String] { + var copy = self + if let index = copy.firstIndex(of: value) { + copy.remove(at: index) + } else { + copy.append(value) + } + return copy + } +} + +extension Array where Element == BaseMessage { + /// A value that determines whether to disable the MessageInputView. + /// The values of sequential messages with `disable_chat_input` enabled are reviewed internally. + /// - Since: 3.27.2 + public func getChatInputDisableState(hasNext: Bool?) -> Bool { + if hasNext == true { return false } + + var types = [BaseMessage.ChatInputDisableType]() + + for element in self { + let type = element.getChatInputDisableType(hasNext: hasNext) + if type == .none { break } + types.append(type) + } + + // Component type must be included to exit the disable_chat_input state. + return types.contains(where: { $0 == .component }) + } +} diff --git a/Sources/Extension/ChatSDK/BaseMessage+SBUIKit.MessageTemplate.swift b/Sources/Extension/ChatSDK/BaseMessage+SBUIKit.MessageTemplate.swift index 3aa1ddc..8162050 100644 --- a/Sources/Extension/ChatSDK/BaseMessage+SBUIKit.MessageTemplate.swift +++ b/Sources/Extension/ChatSDK/BaseMessage+SBUIKit.MessageTemplate.swift @@ -9,27 +9,9 @@ import UIKit import SendbirdChatSDK -// only internal -fileprivate extension BaseMessage { - func setInMemoryUserInfo(key: String, data: Element) { - var memory = self.inMemoryUserInfo ?? [:] - memory[key] = data - self.inMemoryUserInfo = memory - } - - func getInMemoryUserInfo(key: String) -> Element? { - self.inMemoryUserInfo?[key] as? Element - } - - func getInMemoryUserInfo(key: String, defaultValue: Element) -> Element { - self.inMemoryUserInfo?[key] as? Element ?? defaultValue - } -} - extension BaseMessage { static let messageTemplateRetryStatusKey = "messageTemplateRetryStatusKey" static let messageTemplateImageRetryStatusKey = "messageTemplateImageRetryStatusKey" - static let messageTemplateHasCompositeType = "messageTemplateHasCompositeType" static let messageTemplateCarouselView = "messageTemplateCarouselView" } @@ -43,12 +25,7 @@ extension BaseMessage { get { self.getInMemoryUserInfo(key: Self.messageTemplateImageRetryStatusKey, defaultValue: .initialized) } set { self.setInMemoryUserInfo(key: Self.messageTemplateImageRetryStatusKey, data: newValue) } } - - var hasMessageTemplateCompositeType: Bool { - get { self.getInMemoryUserInfo(key: Self.messageTemplateHasCompositeType, defaultValue: false) } - set { self.setInMemoryUserInfo(key: Self.messageTemplateHasCompositeType, data: newValue) } - } - + var messageTemplateCarouselView: UIView? { get { self.getInMemoryUserInfo(key: Self.messageTemplateCarouselView, defaultValue: nil) } set { self.setInMemoryUserInfo(key: Self.messageTemplateCarouselView, data: newValue) } diff --git a/Sources/Extension/ChatSDK/BaseMessage+SBUIKit.swift b/Sources/Extension/ChatSDK/BaseMessage+SBUIKit.swift index c72e632..aa0aa4a 100644 --- a/Sources/Extension/ChatSDK/BaseMessage+SBUIKit.swift +++ b/Sources/Extension/ChatSDK/BaseMessage+SBUIKit.swift @@ -50,19 +50,82 @@ extension BaseMessage { /// container type of message template /// - Since: 3.21.0 - public var asUiSettingContainerType: SBUMessageContainerType { - if self.hasMessageTemplateCompositeType == true { return .full } - - switch self.asExtendedMessagePayload?.uiSettings?.containerType { - case .wide: return .wide - case .full: return self.hasMessageTemplate ? .full : .`default` - default: return .`default` - } - } + @available(*, deprecated, message: "`asUiSettingContainerType` has been deprecated since 3.27.2.") + public var asUiSettingContainerType: SBUMessageContainerType { .`default` } /// Function to decode to custom view data using genric type. /// - Since: 3.11.0 public func decodeCustomViewData() throws -> ViewData? { try self.asExtendedMessagePayload?.decodeCustomViewData() } + + /// Indicates if the message is a stream (being updated) message. + /// - Since: 3.26.0 + public var isStreamMessage: Bool { + StreamData.make(self).stream == true + } +} + +extension BaseMessage { + func setInMemoryUserInfo(key: String, data: Element) { + var memory = self.inMemoryUserInfo ?? [:] + memory[key] = data + self.inMemoryUserInfo = memory + } + + func getInMemoryUserInfo(key: String) -> Element? { + self.inMemoryUserInfo?[key] as? Element + } + + func getInMemoryUserInfo(key: String, defaultValue: Element) -> Element { + self.inMemoryUserInfo?[key] as? Element ?? defaultValue + } + + fileprivate struct StreamData: Codable { + let stream: Bool? + + static func make(_ message: BaseMessage) -> StreamData { + guard let jsonData = message.data.data(using: .utf8) else { return StreamData(stream: false) } + return (try? JSONDecoder().decode(StreamData.self, from: jsonData)) ?? StreamData(stream: false) + } + } +} + +extension BaseMessage { + /// A value that determines whether to disable the MessageInputView. + /// Additionally, other properties are checked as well. + /// - Since: 3.27.0 + @available(*, deprecated, message: "Use `getChatInputDisableState(hasNext:)` in [BaseMessage]") // 3.27.2 + public func getChatInputDisabledState(hasNext: Bool?) -> Bool { + getChatInputDisableType(hasNext: hasNext) != .none + } + + func getChatInputDisableType(hasNext: Bool?) -> ChatInputDisableType { + guard let extendedMessagePayload = self.asExtendedMessagePayload else { return .none } + guard extendedMessagePayload.disableChatInput == true else { return .none } + + if hasNext == true { return .none } + + if let form = self.messageForm, + form.isValidVersion == true, + form.isSubmitted == false, + SendbirdUI.config.groupChannel.channel.isFormTypeMessageEnabled == true { + return .component // message form => component + } + + if extendedMessagePayload.suggestedReplies?.hasElements == true, + SendbirdUI.config.groupChannel.channel.isSuggestedRepliesEnabled == true { + return .component // suggested replies => component + } + + // normal message => not component + return .message + } + + enum ChatInputDisableType { + case component + case message + case none + } + } diff --git a/Sources/Extension/ChatSDK/MessageForm+SBUIKit.swift b/Sources/Extension/ChatSDK/MessageForm+SBUIKit.swift new file mode 100644 index 0000000..8561060 --- /dev/null +++ b/Sources/Extension/ChatSDK/MessageForm+SBUIKit.swift @@ -0,0 +1,84 @@ +// +// MessageForm+SBUIKit.swift +// SendbirdUIKit +// +// Created by Damon Park on 7/3/24. +// + +import UIKit +import SendbirdChatSDK + +extension BaseMessage { + /// boolean to prevent duplicate message forms from being submitted + /// - Since: 3.27.0 + public var isFormSubmitting: Bool { + get { self.getInMemoryUserInfo(key: "isFormSubmitting", defaultValue: false) } + set { self.setInMemoryUserInfo(key: "isFormSubmitting", data: newValue) } + } + + /// Tracks validation status of each form item to prevent duplicate submissions. + /// (key: item id, value: validation status) + /// - Since: 3.27.0 + public var formItemValidationStatus: [Int64: Bool] { + get { self.getInMemoryUserInfo(key: "formItemValidationStatus", defaultValue: [:]) } + set { self.setInMemoryUserInfo(key: "formItemValidationStatus", data: newValue) } + } +} + +extension MessageForm { + /// boolean variable indicating whether the form has valid version. + /// - Since: 3.27.0 + public var isValidVersion: Bool { + version == 1 + } +} + +extension MessageFormItem.LayoutType { + /// Keyboard type + /// - Since: 3.27.0 + public var keyboardType: UIKeyboardType { + switch self { + case .text: return .default + case .textarea: return .default + case .number: return .numberPad + case .phone: return .phonePad + case .email: return .emailAddress + case .chip: return .default + case .unknown: return .default + } + } + + public var isTextInputType: Bool { + switch self { + case .text: return true + case .textarea: return true + case .number: return true + case .phone: return true + case .email: return true + case .chip: return false + case .unknown: return false + } + } +} + +extension MessageFormItem.ResultCount { + /// Method to check the max value of resultCount to see if values can be updated. + /// - Parameter values: draft values. + /// - Since: 3.27.0 + public func canUpdate(_ values: [String]) -> Bool { + guard let _ = min, let max = max else { return false } + return values.count <= max + } + + /// Method to check if values is a valid value by checking the min/max value of resultCount. + /// - Parameter values: draft values. + /// - Since: 3.27.0 + public func isValid(_ values: [String]) -> Bool { + guard let min = min, let max = max else { return false } + return values.count <= max && values.count >= min + } + + /// A boolean to indicate whether the resultCount is a single-value result. + /// - Since: 3.27.0 + public var isOnlyOne: Bool { max == 1 } +} diff --git a/Sources/Extension/UICollectionView+SBUIKit.swift b/Sources/Extension/UICollectionView+SBUIKit.swift new file mode 100644 index 0000000..5bf94e5 --- /dev/null +++ b/Sources/Extension/UICollectionView+SBUIKit.swift @@ -0,0 +1,56 @@ +// +// UICollectionView+SBUIKit.swift +// SendbirdUIKit +// +// Created by Damon Park on 7/3/24. +// + +import UIKit + +final class LeftAlignedCollectionViewFlowLayout: UICollectionViewFlowLayout { + override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? { + let attributes = super.layoutAttributesForElements(in: rect) + + var leftMargin = sectionInset.left + var rightMargin = collectionView?.bounds.width ?? 0 - sectionInset.right + var maxY: CGFloat = -1.0 + + let attribute = UIView.appearance().semanticContentAttribute + let isRTL = UIView.userInterfaceLayoutDirection(for: attribute) == .rightToLeft + + attributes?.forEach { layoutAttribute in + if layoutAttribute.frame.origin.y >= maxY { + leftMargin = sectionInset.left + rightMargin = collectionView?.bounds.width ?? 0 - sectionInset.right + } + + if isRTL { + layoutAttribute.frame.origin.x = rightMargin - layoutAttribute.frame.width + rightMargin -= layoutAttribute.frame.width + minimumInteritemSpacing + } else { + layoutAttribute.frame.origin.x = leftMargin + leftMargin += layoutAttribute.frame.width + minimumInteritemSpacing + } + + maxY = max(layoutAttribute.frame.maxY, maxY) + } + return attributes + } +} + +final class SBUWrappingCollectionView: UICollectionView { + override func reloadData() { + super.reloadData() + + invalidateIntrinsicContentSize() + superview?.layoutIfNeeded() + } + + override var intrinsicContentSize: CGSize { contentSize } + + override func layoutSubviews() { + super.layoutSubviews() + invalidateIntrinsicContentSize() + superview?.layoutIfNeeded() + } +} diff --git a/Sources/Extension/UIColor+SBUIKit.swift b/Sources/Extension/UIColor+SBUIKit.swift index 1f2dcde..aa92585 100644 --- a/Sources/Extension/UIColor+SBUIKit.swift +++ b/Sources/Extension/UIColor+SBUIKit.swift @@ -67,5 +67,25 @@ extension UIColor { } self.init(red: CGFloat(r) / 255, green: CGFloat(g) / 255, blue: CGFloat(b) / 255, alpha: CGFloat(a) / 255) } + + func toHexString() -> String? { + var red: CGFloat = 0 + var green: CGFloat = 0 + var blue: CGFloat = 0 + var alpha: CGFloat = 0 + + guard self.getRed(&red, green: &green, blue: &blue, alpha: &alpha) else { return nil } + + let r = Int(red * 255.0) + let g = Int(green * 255.0) + let b = Int(blue * 255.0) + let a = Int(alpha * 255.0) + + if alpha == 1.0 { + return String(format: "#%02X%02X%02X", r, g, b) + } else { + return String(format: "#%02X%02X%02X%02X", r, g, b, a) + } + } } // swiftlint:enable identifier_name diff --git a/Sources/Extension/UIView+SBUIKit.swift b/Sources/Extension/UIView+SBUIKit.swift index eb675d1..e1dc7ed 100644 --- a/Sources/Extension/UIView+SBUIKit.swift +++ b/Sources/Extension/UIView+SBUIKit.swift @@ -1369,3 +1369,19 @@ extension UIUserInterfaceLayoutDirection { var isRTL: Bool { self == .rightToLeft } var isLTR: Bool { self == .leftToRight } } + +extension UIView { + /// Methods for creating an empty view to use for layout construction + /// NOTE: width, height constraints are set automatically. + public static func spacing( + width: CGFloat = 0, + height: CGFloat = 0, + tag: Int? = nil + ) -> UIView { + let view = UIView() + view.backgroundColor = .clear + view.sbu_constraint(width: width, height: height) + if let tag = tag { view.tag = tag } + return view + } +} diff --git a/Sources/Manager/CacheManager/SBUCacheManager.Template.swift b/Sources/Manager/CacheManager/SBUCacheManager.Template.swift index 9ea4d80..065cc14 100644 --- a/Sources/Manager/CacheManager/SBUCacheManager.Template.swift +++ b/Sources/Manager/CacheManager/SBUCacheManager.Template.swift @@ -12,7 +12,7 @@ extension SBUCacheManager { static func template(with type: SBUTemplateType) -> SBUTemplateCacheType { switch type { case .notification: return NotificationMessageTemplate.shared - case .group: return GroupMessageTemplate.shared + case .message: return GroupMessageTemplate.shared } } @@ -25,7 +25,7 @@ extension SBUCacheManager { class GroupMessageTemplate: SBUTemplateCacheType { static let shared = GroupMessageTemplate() - static let type = SBUTemplateType.group + static let type = SBUTemplateType.message static let memoryCache = MemoryCacheForTemplate() static let diskCache = DiskCacheForTemplate(cacheType: type.cacheKey) } diff --git a/Sources/Manager/SBUEmojiManager.swift b/Sources/Manager/SBUEmojiManager.swift index 85a0e75..4b39b09 100644 --- a/Sources/Manager/SBUEmojiManager.swift +++ b/Sources/Manager/SBUEmojiManager.swift @@ -96,13 +96,35 @@ public class SBUEmojiManager { return category.emojis } + static func getEmojis(with categoryIds: [Int64]) -> [Emoji] { + guard let container = shared.container else { + SBULog.error("[Failed] Emojis with categoryIds") + return [] + } + + let categories = container.categories + guard !categories.isEmpty else { + return [] + } + + let filteredEmojiCategories = categories.filter { categoryIds.contains($0.cid) } + let filteredEmojis = filteredEmojiCategories.reduce([]) { $0 + $1.emojis} + + if filteredEmojis.isEmpty { + SBULog.warning("Emojis for emojiCategoryIds is empty.") + } + + return filteredEmojis + } + // MARK: - private function static func isReactionEnabled(channel: BaseChannel?) -> Bool { guard let groupChannel = channel as? GroupChannel else { return false } - return groupChannel.isSuper ? - SBUAvailable.isSupportReactions(for: .superGroup) : - SBUAvailable.isSupportReactions(for: .group) && !groupChannel.isBroadcast + return !groupChannel.isBroadcast && + (groupChannel.isSuper ? + SBUAvailable.isSupportReactions(for: .superGroup) : + SBUAvailable.isSupportReactions(for: .group)) } /// Decides whether to show the member list for each reaction upon long press on an emoji. @@ -168,4 +190,27 @@ public class SBUEmojiManager { UserDefaults.standard.setValue(serializedContainer, forKey: SBUEmojiManager.kEmojiCacheKey) } } + + /// Checks if an emoji is available in current app. + /// - Since: 3.27.0 + static func isEmojiAvailable( + emojiKey: String, + message: BaseMessage + ) -> Bool { + if let categoryIds = SBUGlobals.emojiCategoryFilter(message) { + let emojiKeys = getEmojis(with: categoryIds).map { $0.key } + if emojiKeys.contains(emojiKey) == false { + return false + } + } + return true + } + + static func getAvailableEmojis(message: BaseMessage?) -> [Emoji] { + if let message, let categoryIds = SBUGlobals.emojiCategoryFilter(message) { + return getEmojis(with: categoryIds) + } else { + return getAllEmojis() + } + } } diff --git a/Sources/Manager/SBUMessageTemplateManager.swift b/Sources/Manager/SBUMessageTemplateManager.swift index e78ec91..6124f1c 100644 --- a/Sources/Manager/SBUMessageTemplateManager.swift +++ b/Sources/Manager/SBUMessageTemplateManager.swift @@ -20,7 +20,7 @@ public class SBUMessageTemplateManager: NSObject { /// Resets message template cache /// - Since: 3.21.0 public static func resetMessageTemplateCache() { - SBUCacheManager.template(with: .group).resetCache() + SBUCacheManager.template(with: .message).resetCache() } static let exeucuteQueue = DispatchQueue(label: "com.sendbird.message_template.images") @@ -35,7 +35,7 @@ extension SBUMessageTemplateManager { subData: String?, themeMode: String? = nil, newTemplateResponseHandler: ((_ success: Bool) -> Void)? = nil - ) -> (String?, Bool) { // bindedTemplate, isNewTemplate + ) -> (BindedTemplate?, Bool) { // bindedTemplate, isNewTemplate guard let subData = subData else { return (nil, false) } // data scheme @@ -109,11 +109,15 @@ extension SBUMessageTemplateManager { var uiTemplate = template.uiTemplate uiTemplate = uiTemplate.replacingOccurrences(of: "\\n", with: "\n") + var dataTemplate = template.dataTemplate + dataTemplate = dataTemplate.replacingOccurrences(of: "\\n", with: "\n") + // bind switch SBUTheme.colorScheme { case .light: let result = bind( uiTemplate: uiTemplate, + dataTemplate: dataTemplate, templateVariables: templateVariables, colorVariable: colorVariablesForLight ) @@ -121,6 +125,7 @@ extension SBUMessageTemplateManager { case .dark: let result = bind( uiTemplate: uiTemplate, + dataTemplate: dataTemplate, templateVariables: templateVariables, colorVariable: colorVariablesForDark ) @@ -164,9 +169,10 @@ extension SBUMessageTemplateManager { // original fileprivate static func bind( uiTemplate: String, + dataTemplate: String, templateVariables: [String: String], colorVariable: [String: String] - ) -> String? { + ) -> BindedTemplate? { // {([^{}\n]+)} // \\{([^{}\\\"\\n]+)\\} guard let regex = try? NSRegularExpression( @@ -174,17 +180,39 @@ extension SBUMessageTemplateManager { options: [] ) else { return nil } let dictionary = templateVariables.merging(colorVariable) { (_, new) in new } - var resultTemplate = uiTemplate + + let resultUiTemplate = bindTemplate( + regex: regex, + template: uiTemplate, + variables: dictionary + ) + let resultDataTemplate = bindTemplate( + regex: regex, + template: dataTemplate, + variables: dictionary + ) - let matches = regex.matches( - in: uiTemplate, + return BindedTemplate( + resultUiTemplate: resultUiTemplate, + resultDataTemplate: resultDataTemplate + ) + } + + fileprivate static func bindTemplate( + regex: NSRegularExpression, + template: String, + variables: [String: String] + ) -> String { + var resultTemplate = template + let templateMatches = regex.matches( + in: template, options: [], - range: NSRange(location: 0, length: uiTemplate.utf16.count) + range: NSRange(location: 0, length: template.utf16.count) ) - for match in matches.reversed() { + for match in templateMatches.reversed() { let keyRange = match.range(at: 1) - let key = (uiTemplate as NSString).substring(with: keyRange) - if let value = dictionary[key] { + let key = (template as NSString).substring(with: keyRange) + if let value = variables[key] { let escapedValue = value.replacingOccurrences(of: "\"", with: "\\\"") resultTemplate = resultTemplate.replacingOccurrences( of: "{\(key)}", @@ -197,7 +225,6 @@ extension SBUMessageTemplateManager { return resultTemplate } - } // for view model diff --git a/Sources/Manager/SBUNotificationChannelManager.swift b/Sources/Manager/SBUNotificationChannelManager.swift index 9b682c7..8a56917 100644 --- a/Sources/Manager/SBUNotificationChannelManager.swift +++ b/Sources/Manager/SBUNotificationChannelManager.swift @@ -9,6 +9,34 @@ import UIKit import SendbirdChatSDK +struct BindedTemplate { + enum TemplateType { + case ui + case data + case unknown + } + + var type: TemplateType + var template: String + + /// Initializes a new BindedTemplate with UI or Data template. + /// - Parameters: + /// - resultUiTemplate: The UI template as a String. + /// - resultDataTemplate: The Data template as a String. + init(resultUiTemplate: String, resultDataTemplate: String) { + if resultUiTemplate != "{}" { + self.type = .ui + self.template = resultUiTemplate + } else if resultDataTemplate != "{}" { + self.type = .data + self.template = resultDataTemplate + } else { + self.type = .unknown + self.template = "{}" + } + } +} + public class SBUNotificationChannelManager: NSObject { static var notificationChannelThemeMode: String { SBUCacheManager.NotificationSetting.themeMode diff --git a/Sources/Manager/SBUTemplateType.swift b/Sources/Manager/SBUTemplateType.swift index d22100c..01fed84 100644 --- a/Sources/Manager/SBUTemplateType.swift +++ b/Sources/Manager/SBUTemplateType.swift @@ -11,33 +11,40 @@ import SendbirdChatSDK enum SBUTemplateType { case notification - case group + case message var cacheKey: String { switch self { case .notification: return "template" // NOTE: for backward - case .group: return "group-template" + case .message: return "message_template" } } var templateKey: String { switch self { case .notification: return "template_key" - case .group: return "key" + case .message: return "key" } } var dataVariable: String { switch self { case .notification: return "template_variables" - case .group: return "variables" + case .message: return "variables" } } var viewVariable: String { switch self { case .notification: return "template_view_variables" - case .group: return "view_variables" + case .message: return "view_variables" + } + } + + var containerOptions: String { + switch self { + case .notification: return "container_options" + case .message: return "container_options" } } } @@ -58,7 +65,7 @@ extension SBUTemplateType { case .notification: return Int64(SendbirdChat.getAppInfo()?.notificationInfo?.templateListToken ?? "0") ?? 0 - case .group: + case .message: return Int64(SendbirdChat.getAppInfo()?.messageTemplateInfo?.templateListToken ?? "0") ?? 0 } } @@ -73,7 +80,7 @@ extension SBUTemplateType { completionHandler(template?.jsonPayload, error) } - case .group: + case .message: SendbirdChat.getMessageTemplate(key: key) { template, error in completionHandler(template?.jsonPayload, error) } @@ -90,7 +97,7 @@ extension SBUTemplateType { SendbirdChat.getNotificationTemplateList(token: token, params: params) { templateList, _, token, _ in completionHandler(templateList?.jsonPayload, token) } - case .group: + case .message: let params = MessageTemplateListParams { $0.limit = 100 } SendbirdChat.getMessageTemplateList(token: token, params: params) { templateList, _, token, _ in completionHandler(templateList?.jsonPayload, token) diff --git a/Sources/MessageTemplate/Processor/SBUMessageTemplate.Container.swift b/Sources/MessageTemplate/Processor/SBUMessageTemplate.Container.swift new file mode 100644 index 0000000..d5cae44 --- /dev/null +++ b/Sources/MessageTemplate/Processor/SBUMessageTemplate.Container.swift @@ -0,0 +1,99 @@ +// +// SBUMessageTemplate.Container.swift +// SendbirdUIKit +// +// Created by Damon Park on 8/26/24. +// + +import Foundation + +extension SBUMessageTemplate { + /// The model struct that makes up the container of the MessageTemplate + /// - Since: 3.27.2 + public struct Container { + /// type value + public let type: ContainerType + /// container options + public let containerOptions: ContainerOptions + } +} + +extension SBUMessageTemplate.Container { + static func create(with data: [String: Any]?) -> SBUMessageTemplate.Container { + guard let data = data else { return .default } + let type = ContainerType(typeString: data["type"] as? String) + let options = ContainerOptions.create(with: data["container_options"] as? [String: Any]) + return SBUMessageTemplate.Container(type: type, containerOptions: options) + } +} + +extension SBUMessageTemplate.Container { + /// Enum value representing the ContainerType for layout configuration + /// - Since: 3.27.2 + public enum ContainerType: String { + case `default` + case unknown + } + + /// Model struct for ui configuration of subviews inside the container + /// - Since: 3.27.2 + public struct ContainerOptions { + /// Profile exposure enabled boolean value (default: true) + public let profile: Bool + /// Time exposure enabled boolean value (default: true) + public let time: Bool + /// Nickname exposure enabled boolean value (default: true) + public let nickname: Bool + } +} + +extension SBUMessageTemplate.Container { + static var `default`: SBUMessageTemplate.Container { + SBUMessageTemplate.Container(type: .default, containerOptions: .default) + } +} + +extension SBUMessageTemplate.Container.ContainerType { + /// A value indicating whether the container type is a valid type + public var isValid: Bool { self != .unknown } + + init(typeString: String?) { + self = .init(rawValue: typeString ?? "") ?? .unknown + } + + public static func isValidType(with template: [String: Any]) -> Bool { + SBUMessageTemplate.Container.ContainerType(typeString: template["type"] as? String).isValid + } +} + +extension SBUMessageTemplate.Container.ContainerOptions: Decodable { + static var `default`: SBUMessageTemplate.Container.ContainerOptions { + SBUMessageTemplate.Container.ContainerOptions( + profile: true, + time: true, + nickname: true + ) + } + + enum CodingKeys: String, CodingKey { + case profile, time, nickname + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + self.profile = try container.decodeIfPresent(Bool.self, forKey: .profile) ?? true + self.time = try container.decodeIfPresent(Bool.self, forKey: .time) ?? true + self.nickname = try container.decodeIfPresent(Bool.self, forKey: .nickname) ?? true + } + + static func create(with data: [String: Any]?) -> SBUMessageTemplate.Container.ContainerOptions { + guard let data = data else { return .default } + do { + let jsonData = try JSONSerialization.data(withJSONObject: data, options: []) + return try JSONDecoder().decode(SBUMessageTemplate.Container.ContainerOptions.self, from: jsonData) + } catch { + return .default + } + } +} diff --git a/Sources/MessageTemplate/Processor/SBUMessageTemplate.Coordinator.swift b/Sources/MessageTemplate/Processor/SBUMessageTemplate.Coordinator.swift index 1eeeef4..1f742f0 100644 --- a/Sources/MessageTemplate/Processor/SBUMessageTemplate.Coordinator.swift +++ b/Sources/MessageTemplate/Processor/SBUMessageTemplate.Coordinator.swift @@ -17,7 +17,6 @@ extension SBUMessageTemplate.Coordinator { enum ReloadType { case download(DownloadType) - case compositeType } enum DownloadType { @@ -75,11 +74,6 @@ extension SBUMessageTemplate { return .failed } - if template.hasCompositeType == true, message.asUiSettingContainerType != .full { - message.hasMessageTemplateCompositeType = template.hasCompositeType - return .reload(.compositeType) - } - // download cache image size // if let noHitData = template.identifierFactory.getUncachedData(), noHitData.hasElements { // if imageRetryStatus.isRetry { diff --git a/Sources/MessageTemplate/Processor/SBUMessageTemplate.TemplateList.swift b/Sources/MessageTemplate/Processor/SBUMessageTemplate.TemplateList.swift index 3268db5..66b5205 100644 --- a/Sources/MessageTemplate/Processor/SBUMessageTemplate.TemplateList.swift +++ b/Sources/MessageTemplate/Processor/SBUMessageTemplate.TemplateList.swift @@ -38,6 +38,7 @@ extension SBUMessageTemplate { let key: String // unique let name: String let uiTemplate: String // JSON_OBJECT + let dataTemplate: String // JSON_OBJECT let colorVariables: String // JSON_OBJECT let createdAt: Int64 let updatedAt: Int64 @@ -48,6 +49,7 @@ extension SBUMessageTemplate { case createdAt = "created_at" case updatedAt = "updated_at" case uiTemplate = "ui_template" + case dataTemplate = "data_template" case colorVariables = "color_variables" } @@ -88,10 +90,14 @@ extension SBUMessageTemplate.TemplateModel { let updatedAt = template.value(from: .updatedAt) as? Int64 { let uiTemplate = template.value(from: .uiTemplate) as? [String: Any] ?? [:] + let dataTemplate = template.value(from: .dataTemplate) as? [String: Any] ?? [:] let colorVariables = template.value(from: .colorVariables) as? [String: Any] ?? [:] let uiTemplateJson = try JSONSerialization.data(withJSONObject: uiTemplate, options: []) let uiTemplateJsonStr = String(data: uiTemplateJson, encoding: .utf8) + + let dataTemplateJson = try JSONSerialization.data(withJSONObject: dataTemplate, options: []) + let dataTemplateJsonStr = String(data: dataTemplateJson, encoding: .utf8) let colorVariablesJson = try JSONSerialization.data(withJSONObject: colorVariables, options: []) let colorVariablesJsonStr = String(data: colorVariablesJson, encoding: .utf8) @@ -100,6 +106,7 @@ extension SBUMessageTemplate.TemplateModel { key: key, name: name, uiTemplate: uiTemplateJsonStr ?? "", + dataTemplate: dataTemplateJsonStr ?? "", colorVariables: colorVariablesJsonStr ?? "", createdAt: createdAt, updatedAt: updatedAt diff --git a/Sources/MessageTemplate/Renderer/SBUMessageTemplate.Renderer+Events.swift b/Sources/MessageTemplate/Renderer/SBUMessageTemplate.Renderer+Events.swift index 8e813c1..cd5845c 100644 --- a/Sources/MessageTemplate/Renderer/SBUMessageTemplate.Renderer+Events.swift +++ b/Sources/MessageTemplate/Renderer/SBUMessageTemplate.Renderer+Events.swift @@ -24,7 +24,6 @@ protocol MessageTemplateRendererDataSource: AnyObject { extension SBUMessageTemplate.Renderer { enum EventSourceKeys: String { case templateFactory - case carouselProfileAreaSize case carouselRestoreView } } diff --git a/Sources/MessageTemplate/Renderer/SBUMessageTemplate.Renderer+RenderItems.swift b/Sources/MessageTemplate/Renderer/SBUMessageTemplate.Renderer+RenderItems.swift index 6893268..31f316d 100644 --- a/Sources/MessageTemplate/Renderer/SBUMessageTemplate.Renderer+RenderItems.swift +++ b/Sources/MessageTemplate/Renderer/SBUMessageTemplate.Renderer+RenderItems.swift @@ -114,6 +114,7 @@ extension SBUMessageTemplate.Renderer { item: carouselItem, parentView: self.bodyView, prevView: prevView, + prevItem: prevItem, isLastItem: isLastItem ) currentView = carouselView @@ -261,9 +262,18 @@ extension SBUMessageTemplate.Renderer { currentView = imageButton prevItem = imageButtonItem - case .carouselView: - // INFO: Carousel views are render only in root body. - break + case .carouselView(let carouselItem): + let carouselView = self.renderCarouselView( + item: carouselItem, + parentView: self.bodyView, + prevView: prevView, + prevItem: prevItem, + itemsAlign: itemsAlign, + layout: layout, + isLastItem: isLastItem + ) + currentView = carouselView + prevItem = carouselItem } if prevItem?.width.type == .flex, @@ -637,20 +647,25 @@ extension SBUMessageTemplate.Renderer { let carouselView = restoreView?.cacheKey?.isEqualCacheKey(factory) == true ? restoreView! : SBUBaseCarouselView(frame: .init(x: 0, y: 0, width: 1, height: 1)) if restoreView != carouselView { - let renderers = item.items?.compactMap { + let renderers = item.items?.compactMap { data in SBUMessageTemplate.Renderer.CarouselRenderer( - data: $0, + data: data, + maxWidth: item.carouselStyle.maxChildWidth, actionHandler: self.actionHandler ) } ?? [] - let profileArea = self.rendererValueFor(key: .carouselProfileAreaSize, defaultValue: CGFloat.zero) + let padding = UIEdgeInsets( + top: item.viewStyle.padding?.top ?? 0, + left: item.viewStyle.padding?.left ?? 0, + bottom: item.viewStyle.padding?.bottom ?? 0, + right: item.viewStyle.padding?.right ?? 0 + ) carouselView.configure( with: SBUBaseCarouselViewParams( - padding: .zero, - spacing: CGFloat(item.spacing), - profileArea: profileArea, + padding: padding, + spacing: item.carouselStyle.spacing, renderers: renderers ) ) @@ -666,6 +681,10 @@ extension SBUMessageTemplate.Renderer { baseView.addSubview(carouselView) parentView.addSubview(baseView) + // For `carousel view`, `padding` is used inside the carousel container (scroll view padding). + // `padding` is mapped to `nil` to avoid having to check the item type inside renderViewLayout(:) where it is used. + item.viewStyle.padding = nil + // View Style self.renderViewStyle(with: item, to: baseView) diff --git a/Sources/MessageTemplate/Renderer/SBUMessageTemplate.Renderer+RenderStyles.swift b/Sources/MessageTemplate/Renderer/SBUMessageTemplate.Renderer+RenderStyles.swift index d24179d..0fc307b 100644 --- a/Sources/MessageTemplate/Renderer/SBUMessageTemplate.Renderer+RenderStyles.swift +++ b/Sources/MessageTemplate/Renderer/SBUMessageTemplate.Renderer+RenderStyles.swift @@ -87,7 +87,7 @@ extension SBUMessageTemplate.Renderer { // default self.rendererConstraints += baseView.sbu_constraint_v2(equalTo: parentView, centerX: 0) self.rendererConstraints += baseView.sbu_constraint_v2(equalTo: parentView, leading: marginInsets.left) - self.rendererConstraints += baseView.sbu_constraint_v2(equalTo: parentView, trailing: marginInsets.right) + self.rendererConstraints += baseView.sbu_constraint_v2(equalTo: parentView, trailing: -marginInsets.right) } else { /** @@ -99,14 +99,14 @@ extension SBUMessageTemplate.Renderer { switch horizontalAlign { case .left: self.rendererConstraints += baseView.sbu_constraint_v2(equalTo: parentView, leading: marginInsets.left) - self.rendererConstraints += baseView.sbu_constraint_v2(lessThanOrEqualTo: parentView, trailing: marginInsets.right) + self.rendererConstraints += baseView.sbu_constraint_v2(lessThanOrEqualTo: parentView, trailing: -marginInsets.right) case .center: self.rendererConstraints += baseView.sbu_constraint_v2(equalTo: parentView, centerX: 0) self.rendererConstraints += baseView.sbu_constraint_v2(greaterThanOrEqualTo: parentView, leading: marginInsets.left) - self.rendererConstraints += baseView.sbu_constraint_v2(lessThanOrEqualTo: parentView, trailing: marginInsets.right) + self.rendererConstraints += baseView.sbu_constraint_v2(lessThanOrEqualTo: parentView, trailing: -marginInsets.right) case .right: self.rendererConstraints += baseView.sbu_constraint_v2(greaterThanOrEqualTo: parentView, leading: marginInsets.left) - self.rendererConstraints += baseView.sbu_constraint_v2(equalTo: parentView, trailing: marginInsets.right) + self.rendererConstraints += baseView.sbu_constraint_v2(equalTo: parentView, trailing: -marginInsets.right) } } @@ -120,7 +120,7 @@ extension SBUMessageTemplate.Renderer { // right anchor if isLastItem { - self.rendererConstraints += baseView.sbu_constraint_v2(equalTo: parentView, trailing: marginInsets.right) + self.rendererConstraints += baseView.sbu_constraint_v2(equalTo: parentView, trailing: -marginInsets.right) } } diff --git a/Sources/MessageTemplate/Renderer/SBUMessageTemplate.Renderer.RendererType.swift b/Sources/MessageTemplate/Renderer/SBUMessageTemplate.Renderer.RendererType.swift index 4b640af..ea8c176 100644 --- a/Sources/MessageTemplate/Renderer/SBUMessageTemplate.Renderer.RendererType.swift +++ b/Sources/MessageTemplate/Renderer/SBUMessageTemplate.Renderer.RendererType.swift @@ -57,35 +57,60 @@ extension SBUMessageTemplate.Renderer.RendererType { extension SBUMessageTemplate.Renderer { static func parsingErrorMesageTemplateBody( type: SBUTemplateType, - message: BaseMessage? + message: BaseMessage?, + containerViewStyle viewStyle: SBUMessageTemplate.Syntax.ViewStyle? = nil ) -> SBUMessageTemplate.Syntax.Body { if let defaultMessage = message?.message, defaultMessage.hasElements { - return .parsingError(text: defaultMessage) + return .parsingError( + text: defaultMessage, + containerViewStyle: viewStyle + ) } switch type { case .notification: - return .parsingError(text: SBUStringSet.Notification_Template_Error_Title, - subText: SBUStringSet.Notification_Template_Error_Subtitle) - case .group: - return .parsingError(text: SBUStringSet.Message_Template_Error_Title, - subText: SBUStringSet.Message_Template_Error_Subtitle) + return .parsingError( + text: SBUStringSet.Notification_Template_Error_Title, + subText: SBUStringSet.Notification_Template_Error_Subtitle, + containerViewStyle: viewStyle + ) + case .message: + return .parsingError( + text: SBUStringSet.Message_Template_Error_Title, + subText: SBUStringSet.Message_Template_Error_Subtitle, + containerViewStyle: viewStyle + ) } } static func errorRenderer( type: SBUTemplateType, - message: BaseMessage? + message: BaseMessage?, + viewStyle: SBUMessageTemplate.Syntax.ViewStyle? = nil ) -> SBUMessageTemplate.Renderer { - let body: SBUMessageTemplate.Syntax.Body = parsingErrorMesageTemplateBody(type: type, message: message) - return SBUMessageTemplate.Renderer(messageId: message?.messageId, body: body) + let body: SBUMessageTemplate.Syntax.Body = parsingErrorMesageTemplateBody( + type: type, + message: message, + containerViewStyle: viewStyle + ) + return SBUMessageTemplate.Renderer( + messageId: message?.messageId, + body: body + ) } static func downloadingRenderer( messageId: Int64?, - downloadingHeight: CGFloat + downloadingHeight: CGFloat, + viewStyle: SBUMessageTemplate.Syntax.ViewStyle? = nil ) -> SBUMessageTemplate.Renderer { - let body: SBUMessageTemplate.Syntax.Body = .downloadingTemplate( height: downloadingHeight ) - return SBUMessageTemplate.Renderer(messageId: messageId, body: body) + let body: SBUMessageTemplate.Syntax.Body = .downloadingTemplate( + height: downloadingHeight, + containerViewStyle: viewStyle + ) + return SBUMessageTemplate.Renderer( + messageId: messageId, + body: body + ) } } diff --git a/Sources/MessageTemplate/Renderer/SBUMessageTemplate.Renderer.Views.swift b/Sources/MessageTemplate/Renderer/SBUMessageTemplate.Renderer.Views.swift index 8cbfdf8..36a9b9b 100644 --- a/Sources/MessageTemplate/Renderer/SBUMessageTemplate.Renderer.Views.swift +++ b/Sources/MessageTemplate/Renderer/SBUMessageTemplate.Renderer.Views.swift @@ -152,26 +152,22 @@ extension SBUMessageTemplate.Renderer { init?( data: SBUMessageTemplate.Syntax.TemplateView, + maxWidth: CGFloat, actionHandler: ((SBUMessageTemplate.Action) -> Void)? ) { guard let renderer = SBUMessageTemplate.Renderer.generate( template: data, - maxWidth: data.itemsMaxWidth, + maxWidth: maxWidth, actionHandler: { actionHandler?($0) } ) else { return nil } self.data = data self.renderer = renderer - self.expectedWidth = data.itemsMaxWidth + self.expectedWidth = data.itemsMaxWidth(with: maxWidth) } func render() -> UIView { self.renderer.backgroundColor = .clear - - if self.expectedWidth != .infinity { - self.renderer.sbu_constraint(width: self.expectedWidth, priority: .required) - } else { - self.renderer.sbu_constraint_lessThan(width: SBUMessageContainerType.defaultMaxSize, priority: .required) - } + self.renderer.sbu_constraint_lessThan(width: self.expectedWidth, priority: .required) return renderer } diff --git a/Sources/MessageTemplate/SBUMessageTemplate.swift b/Sources/MessageTemplate/SBUMessageTemplate.swift index f26bd9c..29964f6 100644 --- a/Sources/MessageTemplate/SBUMessageTemplate.swift +++ b/Sources/MessageTemplate/SBUMessageTemplate.swift @@ -12,4 +12,5 @@ import UIKit /// `SBUMessageTemplate` is a class that handles message templates. public class SBUMessageTemplate { static let urlForTemplateDownload = "TEMPLATE_DOWNLOAD" + static var defaultMaxSize: CGFloat = 256.0 } diff --git a/Sources/MessageTemplate/Syntax/SBUMessageTemplate.ErrorMessages.swift b/Sources/MessageTemplate/Syntax/SBUMessageTemplate.ErrorMessages.swift index ff79f3e..7e72ccc 100644 --- a/Sources/MessageTemplate/Syntax/SBUMessageTemplate.ErrorMessages.swift +++ b/Sources/MessageTemplate/Syntax/SBUMessageTemplate.ErrorMessages.swift @@ -9,7 +9,13 @@ import UIKit extension SBUMessageTemplate.Syntax.Body { - static func parsingError(text: String, subText: String? = nil) -> SBUMessageTemplate.Syntax.Body { + static func parsingError( + text: String, + subText: String? = nil, + width: CGFloat? = nil, + height: CGFloat? = nil, + containerViewStyle viewStyle: SBUMessageTemplate.Syntax.ViewStyle? = nil + ) -> SBUMessageTemplate.Syntax.Body { var textItems: [SBUMessageTemplate.Syntax.Item] = [ .text(.init( text: text, @@ -49,6 +55,9 @@ extension SBUMessageTemplate.Syntax.Body { layout: .column, align: SBUMessageTemplate.Syntax.ItemsAlign(horizontal: .left, vertical: .center), type: .box, + viewStyle: viewStyle, + width: width != nil ? .init(type: .fixed, value: Int(width!)) : .wrapContent(), + height: height != nil ? .init(type: .fixed, value: Int(height!)) : .wrapContent(), items: [ .box(.init( layout: .column, @@ -65,7 +74,11 @@ extension SBUMessageTemplate.Syntax.Body { return body } - static func downloadingTemplate(height: CGFloat) -> SBUMessageTemplate.Syntax.Body { + static func downloadingTemplate( + width: CGFloat? = nil, + height: CGFloat, + containerViewStyle viewStyle: SBUMessageTemplate.Syntax.ViewStyle? = nil + ) -> SBUMessageTemplate.Syntax.Body { let spinnerItems: [SBUMessageTemplate.Syntax.Item] = [ .image(.init( imageUrl: SBUMessageTemplate.urlForTemplateDownload, @@ -83,6 +96,8 @@ extension SBUMessageTemplate.Syntax.Body { layout: .column, align: SBUMessageTemplate.Syntax.ItemsAlign(horizontal: .center, vertical: .center), type: .box, + viewStyle: viewStyle, + width: width != nil ? .init(type: .fixed, value: Int(width!)) : .wrapContent(), height: .init(type: .fixed, value: Int(height)), items: [ .box(.init( @@ -101,4 +116,60 @@ extension SBUMessageTemplate.Syntax.Body { ] return body } + + static func dataTemplate(text: String, subText: String? = nil) -> SBUMessageTemplate.Syntax.Body { + var textItems: [SBUMessageTemplate.Syntax.Item] = [ + .text(.init( + text: text, + maxTextLines: 10, + textStyle: .init( + size: 14, + color: SBUTheme.notificationTheme.notificationCell.fallbackMessageTitleHexColor, + weight: .normal + ), + type: .text, + viewStyle: .init( + padding: .init(top: 0, bottom: 0, left: 0, right: 0) + ) + )) + ] + if let subText = subText { + textItems.append( + .text(.init( + text: subText, + maxTextLines: 0, + textStyle: .init( + size: 14, + color: SBUTheme.notificationTheme.notificationCell.fallbackMessageSubtitleHexColor, + weight: .normal + ), + type: .text, + viewStyle: .init( + padding: .init(top: 0, bottom: 0, left: 0, right: 0) + ) + )) + ) + } + + let body = SBUMessageTemplate.Syntax.Body() + body.items = [ + .box(.init( + layout: .column, + align: SBUMessageTemplate.Syntax.ItemsAlign(horizontal: .left, vertical: .center), + type: .box, + items: [ + .box(.init( + layout: .column, + align: .init(horizontal: .left, vertical: .center), + type: .box, + viewStyle: .init( + padding: .init(top: 12, bottom: 12, left: 12, right: 12) + ), + items: textItems + )) + ] + )) + ] + return body + } } diff --git a/Sources/MessageTemplate/Syntax/SBUMessageTemplate.Syntax.Sizes.swift b/Sources/MessageTemplate/Syntax/SBUMessageTemplate.Syntax.Sizes.swift index 1179faf..b3d246b 100644 --- a/Sources/MessageTemplate/Syntax/SBUMessageTemplate.Syntax.Sizes.swift +++ b/Sources/MessageTemplate/Syntax/SBUMessageTemplate.Syntax.Sizes.swift @@ -117,6 +117,19 @@ extension SBUMessageTemplate.Syntax { from: container ) } + + /// Used for rendering internal template types (error, download) + init( + top: CGFloat, + bottom: CGFloat, + left: CGFloat, + right: CGFloat + ) { + self.top = top + self.bottom = bottom + self.left = left + self.right = right + } } class Padding: Decodable { @@ -157,3 +170,14 @@ extension SBUMessageTemplate.Syntax { } } } + +extension UIEdgeInsets { + var asMessageTemplatePadding: SBUMessageTemplate.Syntax.Padding { + SBUMessageTemplate.Syntax.Padding( + top: self.top, + bottom: self.bottom, + left: self.left, + right: self.right + ) + } +} diff --git a/Sources/MessageTemplate/Syntax/SBUMessageTemplate.Syntax.Styles.swift b/Sources/MessageTemplate/Syntax/SBUMessageTemplate.Syntax.Styles.swift index df6f2cc..be5a991 100644 --- a/Sources/MessageTemplate/Syntax/SBUMessageTemplate.Syntax.Styles.swift +++ b/Sources/MessageTemplate/Syntax/SBUMessageTemplate.Syntax.Styles.swift @@ -61,7 +61,7 @@ extension SBUMessageTemplate.Syntax { self.padding = padding } } - + class TextStyle: Decodable { let size: Int? let color: String? diff --git a/Sources/MessageTemplate/Syntax/SBUMessageTemplate.Syntax.Views.swift b/Sources/MessageTemplate/Syntax/SBUMessageTemplate.Syntax.Views.swift index 09cc8c7..baa877c 100644 --- a/Sources/MessageTemplate/Syntax/SBUMessageTemplate.Syntax.Views.swift +++ b/Sources/MessageTemplate/Syntax/SBUMessageTemplate.Syntax.Views.swift @@ -78,7 +78,7 @@ extension SBUMessageTemplate.Syntax { - 1: [____] <--- template max width - 2: [__] */ - var itemsMaxWidth: CGFloat { + func itemsMaxWidth(with limit: CGFloat) -> CGFloat { guard let items = self.body?.items?.compactMap({ $0.asView }) else { return .infinity } var maxWidth: CGFloat = 0 @@ -91,10 +91,10 @@ extension SBUMessageTemplate.Syntax { if maxWidth < item.widthValue { maxWidth = item.fullWidthValue } } - // If {fill_parent} is present, it will be drawn with {max_fixed_width} or {default_width} because it doesn't know how it will be drawn. - if hasFillParent == true { return max(maxWidth, SBUMessageContainerType.defaultMaxSize) } - // If {wrap_content} is present, make it smaller than {default_width} and allow it to have a wrap area. - if hasWrapContent == true { return .infinity } // NOTE: lessThan `default_max_width` in renderer. + // If {fill_parent} is present, it will be drawn with {max_fixed_width} or {limit} because it doesn't know how it will be drawn. + if hasFillParent == true { return max(maxWidth, limit) } + // If {wrap_content} is present, make it smaller than {limit} and allow it to have a wrap area. + if hasWrapContent == true { return max(maxWidth, limit) } // NOTE: lessThan `{limit}` in renderer. // If there are only fixed width values. return maxWidth } @@ -367,20 +367,19 @@ extension SBUMessageTemplate.Syntax { } class CarouselItem: View { - let spacing: Int // default: 8 let items: [TemplateView]? + let carouselStyle: CarouselStyle enum CodingKeys: String, CodingKey { - case spacing, items + case items, carouselStyle } - static var maxLimitOfItems: Int = 10 - required init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) - self.spacing = try container.decodeIfPresent(Int.self, forKey: .spacing) ?? 8 // default let items = try container.decodeIfPresent([TemplateView].self, forKey: .items) ?? [] - self.items = Array(items.prefix(CarouselItem.maxLimitOfItems)) + self.items = items + + self.carouselStyle = try container.decodeIfPresent(CarouselStyle.self, forKey: .carouselStyle) ?? .init() try super.init(from: decoder) } @@ -389,5 +388,33 @@ extension SBUMessageTemplate.Syntax { self.identifier = factory.generate(with: self) items?.forEach { $0.setIdentifier(with: factory) } } + + class CarouselStyle: Decodable { + let spacing: CGFloat + let maxChildWidth: CGFloat + + enum CodingKeys: String, CodingKey { + case spacing + case maxChildWidth + } + + init(spacing: CGFloat = 10, maxChildWidth: CGFloat = SBUMessageTemplate.defaultMaxSize) { + self.spacing = spacing + self.maxChildWidth = maxChildWidth + } + + required init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + self.spacing = SBUMessageTemplate.decodeMultipleTypeForCGFloat( + forKey: .spacing, + from: container + ) + self.maxChildWidth = SBUMessageTemplate.decodeIfPresentMultipleTypeForCGFloat( + forKey: .maxChildWidth, + from: container + ) ?? SBUMessageTemplate.defaultMaxSize + } + } } } diff --git a/Sources/MessageTemplate/Tester/MessageTemplateTestViewController.swift b/Sources/MessageTemplate/Tester/MessageTemplateTestViewController.swift index e2cb4dc..3602c2b 100644 --- a/Sources/MessageTemplate/Tester/MessageTemplateTestViewController.swift +++ b/Sources/MessageTemplate/Tester/MessageTemplateTestViewController.swift @@ -9,8 +9,7 @@ import UIKit public class MessageTemplateTestViewController: SBUBaseViewController { - - let baseView = UIView() + let scrollView = UIScrollView() var renderedView: UIView? var useParserTest: Bool = false @@ -41,6 +40,7 @@ public class MessageTemplateTestViewController: SBUBaseViewController { let mockJson = MessageTemplateParser.MockJson self.renderedView = SBUMessageTemplate.Renderer( with: mockJson, + messageId: 1, actionHandler: { action in SBULog.info(action.data) } @@ -49,20 +49,19 @@ public class MessageTemplateTestViewController: SBUBaseViewController { ) if let renderedView = self.renderedView { - self.baseView.addSubview(renderedView) + self.scrollView.addSubview(renderedView) } - self.baseView.roundCorners(corners: .allCorners, radius: 16.0) - self.baseView.clipsToBounds = true + self.scrollView.clipsToBounds = true self.view.backgroundColor = .gray - self.view.addSubview(self.baseView) + self.view.addSubview(self.scrollView) } public override func setupStyles() { super.setupStyles() - self.baseView.backgroundColor = .white + self.scrollView.backgroundColor = .white } public override func setupLayouts() { @@ -70,17 +69,17 @@ public class MessageTemplateTestViewController: SBUBaseViewController { // Must implement belows - self.baseView.sbu_constraint_equalTo( - leadingAnchor: self.view.safeAreaLayoutGuide.leadingAnchor, - leading: 20, - trailingAnchor: self.view.safeAreaLayoutGuide.trailingAnchor, - trailing: -20 + self.scrollView.sbu_constraint( + equalTo: self.view, + leading: 0, + trailing: 0, + top: 0, + bottom: 0 ) - self.baseView.sbu_constraint(equalTo: self.view, centerX: 0, centerY: 0) - if let renderedView = self.renderedView { - renderedView.sbu_constraint(equalTo: self.baseView, leading: 0, trailing: 0, top: 0, bottom: 0) + renderedView.sbu_constraint(equalTo: self.scrollView, leading: 0, trailing: 0, top: 0, bottom: 0) + renderedView.sbu_constraint(widthAnchor: self.scrollView.widthAnchor, width: 0) // renderedView.sbu_constraint_greater(bottomAnchor: self.baseView.bottomAnchor, bottom: 0) } } diff --git a/Sources/Model/SBUError.swift b/Sources/Model/SBUError.swift new file mode 100644 index 0000000..6d87b3d --- /dev/null +++ b/Sources/Model/SBUError.swift @@ -0,0 +1,33 @@ +// +// SBUError.swift +// SendbirdUIKit +// +// Created by Celine Moon on 8/27/24. +// + +import SendbirdChatSDK + +struct SBUError { + var domain: String? + var code: SBUErrorCode + var userInfo: [String: Any]? + + func asSBError() -> SBError { + SBError( + domain: self.domain ?? self.code.message, + code: self.code.rawValue, + userInfo: self.userInfo + ) + } +} + +enum SBUErrorCode: Int { + case emojiUnsupported = 10100 + + var message: String { + switch self { + case .emojiUnsupported: + return "The selected emoji is unsupported." + } + } +} diff --git a/Sources/Model/SBUExtendedMessagePayload.swift b/Sources/Model/SBUExtendedMessagePayload.swift index 88e1346..364c921 100644 --- a/Sources/Model/SBUExtendedMessagePayload.swift +++ b/Sources/Model/SBUExtendedMessagePayload.swift @@ -41,13 +41,14 @@ struct SBUExtendedMessagePayload { case suggestedReplies = "suggested_replies" case customView = "custom_view" case disableChatInput = "disable_chat_input" - case template + case template = "message_template" case uiSettings = "ui" } /// A value that determines whether to disable the MessageInputView. /// Additionally, other properties are checked as well. /// - Since: 3.16.0 + @available(*, deprecated, message: "This method is deprecated in 3.27.0. Use `BaseMessage.getChatInputDisabledState(hasNext:)` instead.") public func getDisabledChatInputState(hasNext: Bool?) -> Bool { if hasNext == true { return false } if SendbirdUI.config.groupChannel.channel.isSuggestedRepliesEnabled == false { return false } diff --git a/Sources/Model/SBUExtendedMessagePayloadForUI.swift b/Sources/Model/SBUExtendedMessagePayloadForUI.swift index f6120d8..a549deb 100644 --- a/Sources/Model/SBUExtendedMessagePayloadForUI.swift +++ b/Sources/Model/SBUExtendedMessagePayloadForUI.swift @@ -12,30 +12,16 @@ import UIKit /// UI Setting for MessageTemplate /// - Since: 3.21.0 public struct SBUExtendedMessagePayloadForUI: Decodable { - /// Type specifying the maximum width of the message view - public let containerType: SBUMessageContainerType - - enum CodingKeys: String, CodingKey { - case containerType = "container_type" - } + @available(*, deprecated, message: "`containerType` has been deprecated since 3.27.2.") + public let containerType: SBUMessageContainerType = .default - init(containerType: SBUMessageContainerType) { - self.containerType = containerType - } - - public init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) - - let containerType = try? container.decodeIfPresent(String.self, forKey: .containerType) - self.containerType = SBUMessageContainerType( - decodeRawValue: containerType ?? SBUMessageContainerType.default.rawValue - ) ?? .default - } + public init(from decoder: Decoder) throws { } } /// Type specifying the maximum width of the message view /// - Since: 3.21.0 +@available(*, deprecated, message: "`SBUMessageContainerType` has been deprecated since 3.27.2.") public enum SBUMessageContainerType: String, Decodable { /// The default size already used case `default` @@ -45,81 +31,6 @@ public enum SBUMessageContainerType: String, Decodable { case full public init?(decodeRawValue: String) { - self = SBUMessageContainerType(rawValue: decodeRawValue) ?? .default - if self == .full { self = .default } - } - - var isDefaultSize: Bool { self == .`default` } - var isBiggerWideSize: Bool { self != .`default` } - var isWide: Bool { self == .wide } - - static var screenWidth: CGFloat { - let windowBounds = (UIApplication.shared.currentWindow?.bounds ?? .zero) - return min(windowBounds.width, windowBounds.height) - } - - static var defaultMaxSize: CGFloat = 256.0 - - var maxWidth: CGFloat { - switch self { - case .`default`: return SBUMessageContainerType.defaultMaxSize - case .wide: return SBUMessageContainerType.screenWidth - case .full: return SBUMessageContainerType.screenWidth - } - } -} - -struct SBUMessageContainerSizeFactory { - let type: SBUMessageContainerType - let profileWidth: CGFloat? - let timpstampWidth: CGFloat? - - init( - type: SBUMessageContainerType, - profileWidth: CGFloat? = nil, - timpstampWidth: CGFloat? = nil - ) { - self.type = type - self.profileWidth = profileWidth - self.timpstampWidth = timpstampWidth - } - - static var `default` = SBUMessageContainerSizeFactory(type: .default) -} - -extension SBUMessageContainerSizeFactory { - struct `Default` { - static let margin: CGFloat = 12.0 - static let spacing: CGFloat = 4.0 - static let profile: CGFloat = 26 - static let timestamp: CGFloat = 55 - } - - func getProfileArea() -> CGFloat { - let profile = self.profileWidth ?? Default.profile - let margin = Default.margin - return margin + profile + margin - } - - func getTimestampArea() -> CGFloat { - let timestamp = self.timpstampWidth ?? Default.timestamp - let spacing = Default.spacing - let margin = Default.margin - return spacing + timestamp + margin - } - - func getWidth(type: SBUMessageContainerType? = nil) -> CGFloat { - let contents = CGFloat.zero - let margin = Default.margin - let screen = SBUMessageContainerType.screenWidth - - switch type ?? self.type { - case .`default`: - return min(self.type.maxWidth, screen - (getProfileArea() + contents + getTimestampArea())) - case .wide: - return min(self.type.maxWidth, screen - (getProfileArea() + contents + margin)) - case .full: - return screen - } + self = .default } } diff --git a/Sources/Module/Channel/GroupChannel/SBUGroupChannelModule.List.swift b/Sources/Module/Channel/GroupChannel/SBUGroupChannelModule.List.swift index 03ae5e9..6cbf95c 100644 --- a/Sources/Module/Channel/GroupChannel/SBUGroupChannelModule.List.swift +++ b/Sources/Module/Channel/GroupChannel/SBUGroupChannelModule.List.swift @@ -70,8 +70,17 @@ public protocol SBUGroupChannelModuleListDelegate: SBUBaseChannelModuleListDeleg /// - form: `SendbirdChatSDK.Form` object. /// - messageCell: Message cell object /// - Since: 3.16.0 + @available(*, deprecated, message: "This method is deprecated in 3.27.0.") func groupChannelModule(_ listComponent: SBUGroupChannelModule.List, didSubmit form: SendbirdChatSDK.Form, messageCell: SBUBaseMessageCell) + /// Called when submit the messageForm. + /// - Parameters: + /// - listComponent: `SBUGroupChannelModule.List` object. + /// - messageForm: Message Form object + /// - messageCell: Message cell object + /// - Since: 3.27.0 + func groupChannelModule(_ listComponent: SBUGroupChannelModule.List, didSubmitMessageForm messageForm: MessageForm, messageCell: SBUBaseMessageCell) + /// Called when updated the feedback answer. /// - Parameters: /// - answer: The answer of the feedback that is updated by user. @@ -168,6 +177,11 @@ extension SBUGroupChannelModule { /// - Since: 3.12.0 public private(set) var typingIndicatorMessageCell: SBUBaseMessageCell? + /// The message cell for `MessageTemplate` data in `extendedMessagePayload`. + /// Use `register(messageTemplateCell:nib:)` to update. + /// - Since: 3.27.2 + public private(set) var messageTemplateCell: SBUMessageTemplateCell? + /// The message cell for some unknown message which is not a type of `AdminMessage` | `UserMessage` | ` FileMessage`. Use `register(unknownMessageCell:nib:)` to update. public private(set) var unknownMessageCell: SBUBaseMessageCell? @@ -288,7 +302,9 @@ extension SBUGroupChannelModule { if self.unknownMessageCell == nil { self.register(unknownMessageCell: Self.UnknownMessageCell.init()) } - + if self.messageTemplateCell == nil { + self.register(messageTemplateCell: SBUMessageTemplateCell()) + } if let cellType = Self.CustomMessageCell { self.register(customMessageCell: cellType.init()) } @@ -528,6 +544,21 @@ extension SBUGroupChannelModule { self.register(messageCell: typingIndicatorMessageCell, nib: nib) } + /// Registers a custom cell as a message template cell based on `SBUMessageTemplateCell`. + /// - Parameters: + /// - messageTemplateCell: Customized message template cell + /// - nib: nib information. If the value is nil, the nib file is not used. + /// - Important: To register custom message cell, please use this function before calling `configure(delegate:dataSource:theme:)` + /// ```swift + /// listComponent.register(messageTemplateCell: MyMessageTemplateCell) + /// listComponent.configure(delegate: self, dataSource: self, theme: theme) + /// ``` + /// - Since: 3.27.2 + open func register(messageTemplateCell: SBUMessageTemplateCell, nib: UINib? = nil) { + self.messageTemplateCell = messageTemplateCell + self.register(messageCell: messageTemplateCell, nib: nib) + } + /// Registers a custom cell as a unknown message cell based on `SBUBaseMessageCell`. /// - Parameters: /// - unknownMessageCell: Customized unknown message cell @@ -698,6 +729,21 @@ extension SBUGroupChannelModule { ) typingMessageCell.configure(with: configuration) + // message template cell + case let (message, templateCell) as (BaseMessage, SBUMessageTemplateCell): + let shouldHideSuggestedReplies = + SendbirdUI.config.groupChannel.channel.showSuggestedRepliesFor + .shouldHideSuggestedReplies( + message: message, + fullMessageList: fullMessageList + ) + + let configuration = SBUMessageTemplateCellParams( + message: message, + shouldHideSuggestedReplies: shouldHideSuggestedReplies + ) + templateCell.configure(with: configuration) + default: let configuration = SBUBaseMessageCellParams( message: message, @@ -750,9 +796,10 @@ extension SBUGroupChannelModule { self.delegate?.groupChannelModule(self, didSelect: optionView) } - messageCell.submitFormHandler = { [weak self] form, cell in + messageCell.submitMessageFormHandler = { [weak self] form, cell in guard let self = self else { return } - self.delegate?.groupChannelModule(self, didSubmit: form, messageCell: cell) + guard let form = message.messageForm else { return } + self.delegate?.groupChannelModule(self, didSubmitMessageForm: form, messageCell: cell) } messageCell.updateFeedbackHandler = { [weak self] answer, cell in @@ -811,6 +858,11 @@ extension SBUGroupChannelModule { messageCell: messageCell ) } + + messageCell.errorHandler = { [weak self] error in + guard let self = self else { return } + self.delegate?.didReceiveError(error, isBlocker: false) + } } open override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { @@ -857,6 +909,15 @@ extension SBUGroupChannelModule { /// - Parameter message: Message object /// - Returns: The identifier of message cell. open func generateCellIdentifier(by message: BaseMessage) -> String { + if let template = message.asMessageTemplate { + if SBUMessageTemplate.Container.ContainerType.isValidType(with: template) == true { + return messageTemplateCell?.sbu_className ?? SBUMessageTemplateCell.sbu_className + } else { + SBULog.warning("Invalid `extended_message_paylod.template.type` of message template") + return unknownMessageCell?.sbu_className ?? SBUUnknownMessageCell.sbu_className + } + } + switch message { case is SBUTypingIndicatorMessage: return typingIndicatorMessageCell?.sbu_className ?? SBUTypingIndicatorMessageCell.sbu_className diff --git a/Sources/Module/MessageThread/SBUMessageThreadModule.List.swift b/Sources/Module/MessageThread/SBUMessageThreadModule.List.swift index d59fbdc..c26a1a1 100644 --- a/Sources/Module/MessageThread/SBUMessageThreadModule.List.swift +++ b/Sources/Module/MessageThread/SBUMessageThreadModule.List.swift @@ -343,6 +343,11 @@ extension SBUMessageThreadModule { guard let self = self else { return } self.delegate?.messageThreadModule(self, didTapMentionUser: user) } + + self.parentMessageInfoView.errorHandler = { [weak self] error in + guard let self = self else { return } + self.delegate?.didReceiveError(error, isBlocker: false) + } } // MARK: - EmptyView diff --git a/Sources/SBUGlobals.swift b/Sources/SBUGlobals.swift index b1a6ce2..5ee4bdc 100644 --- a/Sources/SBUGlobals.swift +++ b/Sources/SBUGlobals.swift @@ -8,6 +8,7 @@ import UIKit import Photos +import SendbirdChatSDK /// This class contains global variables and configurations for Sendbird UIKit. public class SBUGlobals { @@ -185,4 +186,33 @@ public class SBUGlobals { /// The API host URL as a string. This is optional and can be set to connect to a specific API server. /// - Since: 3.21.0 public static var apiHost: String? + + /// A static closure property that lets you define filter logic for determining which emoji categories to show for different messages. + /// + /// Override this closure to apply your custom logic. + /// By default, the closure returns `nil`, in which case all emojis defined in your application will be shown. + /// + /// - Parameter message: The `BaseMessage` object for which emoji categories are being filtered. + /// - Returns: An optional array of `Int64` representing the Ids of `EmojiCategory` instances to display, or `nil` if no filtering is applied. + /// + /// See the example below. + /// ```swift + /// // Define your custom filter logic before the emojis are shown. + /// + /// SBUGlobals.emojiCategoryFilter = { message in + /// switch message { + /// case is UserMessage: + /// return [1, 2] + /// case is FileMessage: + /// return [2, 3] + /// default: return [] + /// } + /// } + /// ``` + /// + /// - Note: If you wish to show all available emojis without filtering, return `nil`. + /// - Since: 3.27.0 + public static var emojiCategoryFilter: (BaseMessage) -> [Int64]? = { message in + return nil + } } diff --git a/Sources/SendbirdUI.swift b/Sources/SendbirdUI.swift index cee567f..a39d1fb 100644 --- a/Sources/SendbirdUI.swift +++ b/Sources/SendbirdUI.swift @@ -436,7 +436,7 @@ public class SendbirdUI { return } - SBUMessageTemplateManager.loadTemplateList(type: .group) { success in + SBUMessageTemplateManager.loadTemplateList(type: .message) { success in if !success { SBULog.error("[Failed] Load group message template list") } completionHandler(success) } diff --git a/Sources/SwiftUI/Util/SwiftUIViewController.swift b/Sources/SwiftUI/Util/SwiftUIViewController.swift index ea292bd..653784f 100644 --- a/Sources/SwiftUI/Util/SwiftUIViewController.swift +++ b/Sources/SwiftUI/Util/SwiftUIViewController.swift @@ -81,9 +81,23 @@ extension SwiftUIViewController: UIViewControllerRepresentable { private func findNavigationController() -> Bool { if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene { for window in windowScene.windows { - if let navigationController = window.rootViewController as? UINavigationController { + if window.rootViewController is UINavigationController { return true } else if let rootViewController = window.rootViewController { + if rootViewController.findNavigationControllerFromResponder() != nil { + return true + } + + // Check for modally presented view controllers (e.g., UIHostingController opened with sheet) + if let presentedViewController = rootViewController.presentedViewController { + return findNavigationController(from: presentedViewController) + } + + // Check if the current view controller is a UITabBarController + if let tabBarController = rootViewController as? UITabBarController { + return findNavigationController(in: tabBarController) + } + return findNavigationController(from: rootViewController) } } @@ -104,11 +118,46 @@ extension SwiftUIViewController: UIViewControllerRepresentable { } } + // Check for modally presented view controllers (e.g., UIHostingController opened with sheet) + if let presentedViewController = viewController.presentedViewController { + return findNavigationController(from: presentedViewController) + } + + // Check if the current view controller is a UITabBarController + if let tabBarController = viewController as? UITabBarController { + return findNavigationController(in: tabBarController) + } + + // Search the children of the current view controller for child in viewController.children { return findNavigationController(from: child) } return false } + + private func findNavigationController(in tabBarController: UITabBarController) -> Bool { + // Check each tab within the UITabBarController + if let viewControllers = tabBarController.viewControllers { + for viewController in viewControllers { + // Search the UINavigationController in each tab + return findNavigationController(from: viewController) + } + } + return false + } +} + +extension UIResponder { + func findNavigationControllerFromResponder() -> UINavigationController? { + var nextResponder: UIResponder? = self + while let responder = nextResponder { + if let navigationController = responder as? UINavigationController { + return navigationController + } + nextResponder = responder.next + } + return nil + } } extension SwiftUIViewController { diff --git a/Sources/SwiftUI/Util/View + ViewModifier.swift b/Sources/SwiftUI/Util/View + ViewModifier.swift index 9938c72..be379c4 100644 --- a/Sources/SwiftUI/Util/View + ViewModifier.swift +++ b/Sources/SwiftUI/Util/View + ViewModifier.swift @@ -27,5 +27,6 @@ extension View { .ignoresSafeArea() .navigationBarTitleDisplayMode(.inline) .navigationBarBackButtonHidden() + .navigationBarHidden(true) } } diff --git a/Sources/Theme/SBUColorSet.swift b/Sources/Theme/SBUColorSet.swift index f08d93a..5773eed 100644 --- a/Sources/Theme/SBUColorSet.swift +++ b/Sources/Theme/SBUColorSet.swift @@ -283,7 +283,7 @@ public extension SBUColorSet { get { onDarkTextLowEmphasis } set { onDarkTextLowEmphasis = newValue } } - + @available(*, deprecated, renamed: "onDarkTextDisabled", message: "") static var ondark04: UIColor { get { onDarkTextDisabled } diff --git a/Sources/Theme/SBUTheme.swift b/Sources/Theme/SBUTheme.swift index cf57998..b8db25c 100644 --- a/Sources/Theme/SBUTheme.swift +++ b/Sources/Theme/SBUTheme.swift @@ -1415,11 +1415,33 @@ public class SBUMessageCellTheme { theme.formInputTitleColor = SBUColorSet.onLightTextHighEmphasis theme.formInputIconColor = SBUColorSet.secondaryMain theme.formInputBorderNormalColor = SBUColorSet.onLightTextDisabled + theme.formInputBorderActiveColor = SBUColorSet.primaryMain + theme.formInputBorderErrorColor = SBUColorSet.errorMain theme.formInputErrorColor = SBUColorSet.errorMain theme.formInputPlaceholderColor = SBUColorSet.onLightTextLowEmphasis theme.formSubmitButtonBackgroundColor = SBUColorSet.primaryMain - theme.formSubmitButtonBackgroundDisabledColor = SBUColorSet.onLightTextDisabled + theme.formSubmitButtonBackgroundDisabledColor = SBUColorSet.background100 theme.formSubmitButtonTitleColor = SBUColorSet.onDarkTextHighEmphasis + theme.formSubmitButtonTitleDisabledColor = SBUColorSet.onLightTextDisabled + + theme.formChipBackgroundNormalColor = SBUColorSet.background50 + theme.formChipBackgroundSelectColor = SBUColorSet.primaryExtraLight + theme.formChipBackgroundDisableColor = SBUColorSet.onDarkTextMidEmphasis + theme.formChipBackgroundSubmittedColor = SBUColorSet.onDarkTextMidEmphasis + theme.formChipTitleNormalColor = SBUColorSet.onLightTextMidEmphasis + theme.formChipTitleSelectColor = SBUColorSet.primaryMain + theme.formChipTitleDisableColor = SBUColorSet.onLightTextMidEmphasis + theme.formChipTitleSubmittedColor = SBUColorSet.onLightTextHighEmphasis + theme.formChipBorderNormalColor = SBUColorSet.onLightTextDisabled + theme.formChipBorderSelectColor = SBUColorSet.primaryMain + theme.formChipBorderDisableColor = SBUColorSet.onDarkTextDisabled + theme.formChipBorderSubmittedColor = UIColor.clear + theme.formTitleFont = SBUFontSet.caption3 + theme.formOptionalTitleFont = SBUFontSet.caption3 + theme.formErrorTitleFont = SBUFontSet.caption4 + theme.formInputTextFont = SBUFontSet.body3 + theme.formChipTextFont = SBUFontSet.caption1 + theme.formSubmittButtonFont = SBUFontSet.button3 // Typing message theme.typingMessageProfileBorderColor = SBUColorSet.background50 @@ -1590,11 +1612,34 @@ public class SBUMessageCellTheme { theme.formInputTitleColor = SBUColorSet.onDarkTextHighEmphasis theme.formInputIconColor = SBUColorSet.secondaryLight theme.formInputBorderNormalColor = SBUColorSet.onDarkTextDisabled + theme.formInputBorderActiveColor = SBUColorSet.primaryLight + theme.formInputBorderErrorColor = SBUColorSet.errorMain theme.formInputErrorColor = SBUColorSet.errorLight theme.formInputPlaceholderColor = SBUColorSet.onDarkTextMidEmphasis theme.formSubmitButtonBackgroundColor = SBUColorSet.primaryLight - theme.formSubmitButtonBackgroundDisabledColor = SBUColorSet.onLightTextDisabled + theme.formSubmitButtonBackgroundDisabledColor = SBUColorSet.background500 theme.formSubmitButtonTitleColor = SBUColorSet.onLightTextHighEmphasis + theme.formSubmitButtonTitleDisabledColor = SBUColorSet.onDarkTextDisabled + + theme.formChipBackgroundNormalColor = SBUColorSet.onLightTextLowEmphasis + theme.formChipBackgroundSelectColor = SBUColorSet.background600 + theme.formChipBackgroundDisableColor = SBUColorSet.background500 + theme.formChipBackgroundSubmittedColor = SBUColorSet.onLightTextDisabled + theme.formChipTitleNormalColor = SBUColorSet.onDarkTextMidEmphasis + theme.formChipTitleSelectColor = SBUColorSet.primaryLight + theme.formChipTitleDisableColor = SBUColorSet.onDarkTextDisabled + theme.formChipTitleSubmittedColor = SBUColorSet.onDarkTextHighEmphasis + theme.formChipBorderNormalColor = SBUColorSet.onDarkTextDisabled + theme.formChipBorderSelectColor = SBUColorSet.primaryLight + theme.formChipBorderDisableColor = SBUColorSet.background500 + theme.formChipBorderSubmittedColor = UIColor.clear + + theme.formTitleFont = SBUFontSet.caption3 + theme.formOptionalTitleFont = SBUFontSet.caption3 + theme.formErrorTitleFont = SBUFontSet.caption4 + theme.formInputTextFont = SBUFontSet.body3 + theme.formChipTextFont = SBUFontSet.caption1 + theme.formSubmittButtonFont = SBUFontSet.button3 // Typing message theme.typingMessageProfileBorderColor = SBUColorSet.background600 @@ -1863,11 +1908,32 @@ public class SBUMessageCellTheme { formInputTitleColor: UIColor = SBUColorSet.onLightTextHighEmphasis, formInputIconColor: UIColor = SBUColorSet.secondaryMain, formInputBorderNormalColor: UIColor = SBUColorSet.onLightTextDisabled, + formInputBorderActiveColor: UIColor = SBUColorSet.primaryMain, + formInputBorderErrorColor: UIColor = SBUColorSet.errorMain, formInputErrorColor: UIColor = SBUColorSet.errorMain, formInputPlaceholderColor: UIColor = SBUColorSet.onLightTextLowEmphasis, formSubmitButtonBackgroundColor: UIColor = SBUColorSet.primaryMain, - formSubmitButtonBackgroundDisabledColor: UIColor = SBUColorSet.onLightTextDisabled, + formSubmitButtonBackgroundDisabledColor: UIColor = SBUColorSet.background100, formSubmitButtonTitleColor: UIColor = SBUColorSet.onDarkTextHighEmphasis, + formSubmitButtonTitleDisabledColor: UIColor = SBUColorSet.onLightTextDisabled, + formChipBackgroundNormalColor: UIColor = SBUColorSet.background50, + formChipBackgroundSelectColor: UIColor = SBUColorSet.primaryExtraLight, + formChipBackgroundDisableColor: UIColor = SBUColorSet.onDarkTextDisabled, + formChipBackgroundSubmittedColor: UIColor = SBUColorSet.onDarkTextDisabled, + formChipTitleNormalColor: UIColor = SBUColorSet.onLightTextMidEmphasis, + formChipTitleSelectColor: UIColor = SBUColorSet.primaryExtraLight, + formChipTitleDisableColor: UIColor = SBUColorSet.onLightTextMidEmphasis, + formChipTitleSubmittedColor: UIColor = SBUColorSet.onLightTextHighEmphasis, + formChipBorderNormalColor: UIColor = SBUColorSet.onLightTextDisabled, + formChipBorderSelectColor: UIColor = SBUColorSet.primaryMain, + formChipBorderDisableColor: UIColor = SBUColorSet.onDarkTextDisabled, + formChipBorderSubmittedColor: UIColor = UIColor.clear, + formTitleFont: UIFont = SBUFontSet.caption3, + formOptionalTitleFont: UIFont = SBUFontSet.caption3, + formErrorTitleFont: UIFont = SBUFontSet.caption4, + formInputTextFont: UIFont = SBUFontSet.body3, + formChipTextFont: UIFont = SBUFontSet.caption1, + formSubmittButtonFont: UIFont = SBUFontSet.button3, typingMessageProfileBorderColor: UIColor = SBUColorSet.background50, typingMessageDotColor: UIColor = SBUColorSet.onLightTextDisabled, typingMessageDotTransformColor: UIColor = SBUColorSet.onLightTextLowEmphasis, @@ -2002,11 +2068,33 @@ public class SBUMessageCellTheme { self.formInputTitleColor = formInputTitleColor self.formInputIconColor = formInputIconColor self.formInputBorderNormalColor = formInputBorderNormalColor + self.formInputBorderActiveColor = formInputBorderActiveColor + self.formInputBorderErrorColor = formInputBorderErrorColor self.formInputErrorColor = formInputErrorColor self.formInputPlaceholderColor = formInputPlaceholderColor self.formSubmitButtonBackgroundColor = formSubmitButtonBackgroundColor self.formSubmitButtonBackgroundDisabledColor = formSubmitButtonBackgroundDisabledColor self.formSubmitButtonTitleColor = formSubmitButtonTitleColor + self.formSubmitButtonTitleDisabledColor = formSubmitButtonTitleDisabledColor + self.formChipBackgroundNormalColor = formChipBackgroundNormalColor + self.formChipBackgroundSelectColor = formChipBackgroundSelectColor + self.formChipBackgroundDisableColor = formChipBackgroundDisableColor + self.formChipBackgroundSubmittedColor = formChipBackgroundSubmittedColor + self.formChipTitleNormalColor = formChipTitleNormalColor + self.formChipTitleSelectColor = formChipTitleSelectColor + self.formChipTitleDisableColor = formChipTitleDisableColor + self.formChipTitleSubmittedColor = formChipTitleSubmittedColor + self.formChipBorderNormalColor = formChipBorderNormalColor + self.formChipBorderSelectColor = formChipBorderSelectColor + self.formChipBorderDisableColor = formChipBorderDisableColor + self.formChipBorderSubmittedColor = formChipBorderSubmittedColor + + self.formTitleFont = formTitleFont + self.formOptionalTitleFont = formOptionalTitleFont + self.formErrorTitleFont = formErrorTitleFont + self.formInputTextFont = formInputTextFont + self.formChipTextFont = formChipTextFont + self.formSubmittButtonFont = formSubmittButtonFont self.typingMessageProfileBorderColor = typingMessageProfileBorderColor self.typingMessageDotColor = typingMessageDotColor @@ -2217,11 +2305,33 @@ public class SBUMessageCellTheme { public var formInputTitleColor: UIColor // 3.11.0 public var formInputIconColor: UIColor // 3.11.0 public var formInputBorderNormalColor: UIColor // 3.11.0 + public var formInputBorderActiveColor: UIColor // 3.27.0 + public var formInputBorderErrorColor: UIColor // 3.27.0 public var formInputErrorColor: UIColor // 3.11.0 public var formInputPlaceholderColor: UIColor // 3.11.0 public var formSubmitButtonBackgroundColor: UIColor // 3.11.0 public var formSubmitButtonBackgroundDisabledColor: UIColor // 3.11.0 public var formSubmitButtonTitleColor: UIColor // 3.11.0 + public var formSubmitButtonTitleDisabledColor: UIColor // 3.27.0 + public var formChipBackgroundNormalColor: UIColor // 3.27.0 + public var formChipBackgroundSelectColor: UIColor // 3.27.0 + public var formChipBackgroundDisableColor: UIColor // 3.27.0 + public var formChipBackgroundSubmittedColor: UIColor // 3.27.0 + public var formChipTitleNormalColor: UIColor // 3.27.0 + public var formChipTitleSelectColor: UIColor // 3.27.0 + public var formChipTitleDisableColor: UIColor // 3.27.0 + public var formChipTitleSubmittedColor: UIColor // 3.27.0 + public var formChipBorderNormalColor: UIColor // 3.27.0 + public var formChipBorderSelectColor: UIColor // 3.27.0 + public var formChipBorderDisableColor: UIColor // 3.27.0 + public var formChipBorderSubmittedColor: UIColor // 3.27.0 + + public var formTitleFont: UIFont // 3.27.0 + public var formOptionalTitleFont: UIFont // 3.27.0 + public var formErrorTitleFont: UIFont // 3.27.0 + public var formInputTextFont: UIFont // 3.27.0 + public var formChipTextFont: UIFont // 3.27.0 + public var formSubmittButtonFont: UIFont // 3.27.0 // MARK: Typing Message public var typingMessageProfileBorderColor: UIColor // 3.12.0 diff --git a/Sources/Util/SBUUtils.swift b/Sources/Util/SBUUtils.swift index 88f6616..c332e60 100644 --- a/Sources/Util/SBUUtils.swift +++ b/Sources/Util/SBUUtils.swift @@ -289,3 +289,25 @@ extension SBUUtils { } } + +extension SBUUtils { + /// Methods for determining if the first character of a string is an RTL language + /// - Since: 3.26.0 + public static func isRTLCharacter(with string: String?) -> Bool { + guard let scalar = string?.unicodeScalars.first else { return false } + return Self.rtlCharacterSet.contains(scalar) + } + + /// The characterset used to determine RTL language + public static var rtlCharacterSet: CharacterSet = { + var rtlCharacterSet = CharacterSet() + rtlCharacterSet.insert(charactersIn: "\u{0590}"..."\u{05FF}") // Hebrew + rtlCharacterSet.insert(charactersIn: "\u{0600}"..."\u{06FF}") // Arabic + rtlCharacterSet.insert(charactersIn: "\u{0750}"..."\u{077F}") // Arabic Supplement + rtlCharacterSet.insert(charactersIn: "\u{08A0}"..."\u{08FF}") // Arabic Extended-A + rtlCharacterSet.insert(charactersIn: "\u{FB1D}"..."\u{FB4F}") // Hebrew Presentation Forms + rtlCharacterSet.insert(charactersIn: "\u{FE70}"..."\u{FEFF}") // Arabic Presentation Forms-B + rtlCharacterSet.insert(charactersIn: "\u{1EE00}"..."\u{1EEFF}") // Arabic Mathematical Alphabetic Symbols + return rtlCharacterSet + }() +} diff --git a/Sources/View/Channel/CellView/SBUUserMessageTextView.swift b/Sources/View/Channel/CellView/SBUUserMessageTextView.swift index d10aaf8..398ec27 100644 --- a/Sources/View/Channel/CellView/SBUUserMessageTextView.swift +++ b/Sources/View/Channel/CellView/SBUUserMessageTextView.swift @@ -104,7 +104,7 @@ open class SBUUserMessageTextView: SBUView { if !self.needsToRemoveMargin { self.widthConstraint?.isActive = false self.widthConstraint = self.widthAnchor.constraint( - lessThanOrEqualToConstant: containerType.isWide ? containerType.maxWidth : Metric.textMaxWidth + lessThanOrEqualToConstant: Metric.textMaxWidth ) self.widthConstraint?.priority = .init(999) self.widthConstraint?.isActive = true @@ -181,6 +181,15 @@ open class SBUUserMessageTextView: SBUView { with: model.attributedText, isEnabled: model.isMarkdownEnabled ) + + if self.currentLayoutDirection == .rightToLeft { + if SBUUtils.isRTLCharacter(with: self.textView.attributedText.string) { + self.textView.textAlignment = .right + } else { + self.textView.textAlignment = .left + } + } + self.textView.linkTextAttributes = [ .foregroundColor: model.textColor, .underlineStyle: NSUnderlineStyle.single.rawValue diff --git a/Sources/View/Channel/MessageCell/CarouselView/SBUBaseCarouselView.swift b/Sources/View/Channel/MessageCell/CarouselView/SBUBaseCarouselView.swift index d57b7fb..919ac46 100644 --- a/Sources/View/Channel/MessageCell/CarouselView/SBUBaseCarouselView.swift +++ b/Sources/View/Channel/MessageCell/CarouselView/SBUBaseCarouselView.swift @@ -16,7 +16,6 @@ protocol SBUBaseCarouselCellRenderer { struct SBUBaseCarouselViewParams { let padding: UIEdgeInsets let spacing: CGFloat - let profileArea: CGFloat let renderers: [SBUBaseCarouselCellRenderer] } @@ -54,7 +53,7 @@ class SBUBaseCarouselView: UIView, UIScrollViewDelegate { // MARK: - Properties - var params = SBUBaseCarouselViewParams(padding: .zero, spacing: 0, profileArea: 0, renderers: []) + var params = SBUBaseCarouselViewParams(padding: .zero, spacing: 0, renderers: []) var contentViews = [UIView]() // MARK: - Initialization @@ -89,10 +88,10 @@ class SBUBaseCarouselView: UIView, UIScrollViewDelegate { self.scrollView.leftAnchor.constraint(equalTo: leftAnchor), self.scrollView.rightAnchor.constraint(equalTo: rightAnchor), - self.stackView.topAnchor.constraint(equalTo: self.scrollView.contentLayoutGuide.topAnchor), - self.stackView.bottomAnchor.constraint(equalTo: self.scrollView.contentLayoutGuide.bottomAnchor), - self.stackView.leftAnchor.constraint(equalTo: self.scrollView.contentLayoutGuide.leftAnchor, constant: self.params.profileArea), - self.stackView.rightAnchor.constraint(equalTo: self.scrollView.contentLayoutGuide.rightAnchor), + self.stackView.topAnchor.constraint(equalTo: self.scrollView.contentLayoutGuide.topAnchor, constant: self.params.padding.top), + self.stackView.bottomAnchor.constraint(equalTo: self.scrollView.contentLayoutGuide.bottomAnchor, constant: self.params.padding.bottom), + self.stackView.leftAnchor.constraint(equalTo: self.scrollView.contentLayoutGuide.leftAnchor, constant: self.params.padding.left), + self.stackView.rightAnchor.constraint(equalTo: self.scrollView.contentLayoutGuide.rightAnchor, constant: self.params.padding.right), self.stackView.heightAnchor.constraint(equalTo: self.scrollView.heightAnchor) ]) @@ -116,7 +115,7 @@ class SBUBaseCarouselView: UIView, UIScrollViewDelegate { let offsetX = UIScrollView.SBUScrollAdjustPosition.adjustContentOffsetX( in: scrollView, items: self.contentViews, - offset: self.params.profileArea, + offset: self.params.padding.left, velocityX: velocityX ) diff --git a/Sources/View/Channel/MessageCell/MessageCellParams/SBUMessageTemplateCellParams.swift b/Sources/View/Channel/MessageCell/MessageCellParams/SBUMessageTemplateCellParams.swift new file mode 100644 index 0000000..1488105 --- /dev/null +++ b/Sources/View/Channel/MessageCell/MessageCellParams/SBUMessageTemplateCellParams.swift @@ -0,0 +1,50 @@ +// +// SBUMessageTemplateCellParams.swift +// SendbirdUIKit +// +// Created by Damon Park on 9/2/24. +// + +import SendbirdChatSDK + +/// This is the message template parameter class +/// - Since: 3.27.2 +public class SBUMessageTemplateCellParams: SBUBaseMessageCellParams { + + /// Template data values + let messageTempalteData: [String: Any]? + + /// The boolean value to indicates that the message cell should hide suggested replies. + /// If it's `true`, never show the suggested replies view even the `BaseMessage/ExtendedMessagePayload` has the reply `option` values. + public let shouldHideSuggestedReplies: Bool + + /// Model struct for ui configuration of subviews inside the container + public let container: SBUMessageTemplate.Container + + public init( + message: BaseMessage, + hideDateView: Bool = false, + groupPosition: MessageGroupPosition = .none, + receiptState: SBUMessageReceiptState = .none, + isThreadMessage: Bool = false, + joinedAt: Int64 = 0, + messageOffsetTimestamp: Int64 = 0, + shouldHideSuggestedReplies: Bool = true + ) { + let templateData = message.asMessageTemplate + self.messageTempalteData = templateData + self.shouldHideSuggestedReplies = shouldHideSuggestedReplies + self.container = .create(with: templateData) + + super.init( + message: message, + hideDateView: hideDateView, + messagePosition: .left, + groupPosition: groupPosition, + receiptState: receiptState, + isThreadMessage: isThreadMessage, + joinedAt: joinedAt, + messageOffsetTimestamp: messageOffsetTimestamp + ) + } +} diff --git a/Sources/View/Channel/MessageCell/MessageForm/ViewParams/SBUMessageFormViewParams.swift b/Sources/View/Channel/MessageCell/MessageForm/ViewParams/SBUMessageFormViewParams.swift new file mode 100644 index 0000000..a2f411d --- /dev/null +++ b/Sources/View/Channel/MessageCell/MessageForm/ViewParams/SBUMessageFormViewParams.swift @@ -0,0 +1,27 @@ +// +// SBUFormViewParams.swift +// SendbirdUIKit +// +// Created by Damon Park on 2024/07/02. +// Copyright © 2023 Sendbird, Inc. All rights reserved. +// + +import Foundation +import SendbirdChatSDK + +/// The data model used for configuring ``SBUFormView``. +/// - Since: 3.27.0 +public struct SBUMessageFormViewParams { + // MARK: - Properties + /// The ID of the message that provides form. + public let messageId: Int64 + + /// The form. + public let messageForm: SendbirdChatSDK.MessageForm + + /// Boolean value to handle whether the submit is in progress on the UI + public let isSubmitting: Bool + + /// Tracks validation status of each form item to prevent duplicate submissions. + public let itemValidationStatus: [Int64: Bool] +} diff --git a/Sources/View/Channel/MessageCell/MessageForm/Views/SBUMessageFormChipsItemView.swift b/Sources/View/Channel/MessageCell/MessageForm/Views/SBUMessageFormChipsItemView.swift new file mode 100644 index 0000000..5e38ca9 --- /dev/null +++ b/Sources/View/Channel/MessageCell/MessageForm/Views/SBUMessageFormChipsItemView.swift @@ -0,0 +1,187 @@ +// +// SBUMessageFormChipsItemView.swift +// SendbirdUIKit +// +// Created by Damon Park on 7/2/24. +// + +import UIKit +import SendbirdChatSDK + +/// Item view of a message form with a chip design +/// - Since: 3.27.0 +public class SBUMessageFormChipsItemView: SBUMessageFormItemView, SBUMesageFormChipViewDelegate { + /// A vertical stack view to configure layouts of the fields. + public var stackView = SBUStackView(axis: .vertical, alignment: .leading, spacing: 0) + /// A horizontal stack view to configure layouts of `title` and `optional`. + public var titleStackView = SBUStackView(axis: .horizontal, alignment: .fill, spacing: 3) + /// The `UILabel` displaying form field title.` + public var titleView = UILabel() + /// Top space view. + public var topSpaceView = UIView() + /// A chip collection view. + public let chipView = SBUMesageFormChipView() + /// Bottom space view. can be hidden. Used only if there is an error message. + public var bottomSpaceView = UIView() + /// The `UILabel` displaying the error message. + public var errorTitleView = UILabel() + /// List of chips values. + public var chips: [String] { formItem?.style.options ?? [] } + + public override func setupViews() { + super.setupViews() + + // + -- stackView ------------------------- + + // | + --- titleStackView --------------- + | + // | | titleView | | + // | + ---------------------------------- + | + // + -------------------------------------- + + // | topSpaceView | + // + -------------------------------------- + + // | chipView | + // + -------------------------------------- + + // | bottomSpaceView | + // + -------------------------------------- + + // | errorTitleView | + // + -------------------------------------- + + + self.updateDefaultOptions() + self.titleView.text = self.formItem?.name + self.chipView.delegate = self + + self.titleStackView.setHStack([self.titleView]) + + self.stackView.setVStack([ + self.titleStackView, + self.topSpaceView, + self.chipView, + self.bottomSpaceView, + self.errorTitleView + ]) + + self.addSubview(stackView) + + self.titleView.attributedText = self.titleAttributedString + + self.errorTitleView.text = SBUStringSet.FormType_Error_Default + } + + public override func setupLayouts() { + super.setupLayouts() + + self.titleView.sbu_constraint_greaterThan(height: 12) + + self.errorTitleView.sbu_constraint(height: 12) + + self.topSpaceView.sbu_constraint(width: 1, height: 6, priority: .defaultHigh) + self.bottomSpaceView.sbu_constraint(width: 1, height: 4, priority: .defaultHigh) + + self.chipView + .sbu_constraint(equalTo: self.stackView, left: 0, right: 0) + + self.chipView.setContentCompressionResistancePriority(UILayoutPriority(751), for: .vertical) + self.chipView.setContentHuggingPriority(UILayoutPriority(249), for: .vertical) + + self.stackView + .sbu_constraint(equalTo: self, left: 0, top: 0) + .sbu_constraint(equalTo: self, right: 0, bottom: 0, priority: .defaultHigh) + .sbu_constraint_multiplier(widthAnchor: self.widthAnchor, widthMultiplier: 1) // NOTE: do not remove this constraints (AC-3258) + } + + public override func setupStyles() { + super.setupStyles() + + self.titleView.numberOfLines = 0 + + self.errorTitleView.font = theme.formErrorTitleFont + self.errorTitleView.textColor = theme.formInputErrorColor + + self.updateInputValidation() + self.updateInputData() + self.updateChips() + } + + func updateInputValidation() { + self.bottomSpaceView.isHidden = true + self.errorTitleView.isHidden = true + + let errorType = self.isValidInput() + + if errorType.hasError == true { + self.errorTitleView.isHidden = false + self.errorTitleView.text = errorType.errorMessage + self.bottomSpaceView.isHidden = false + } + } + + func updateInputData() { + // do nothing + } + + func updateChips() { + self.chipView.update(chips: self.chips, status: self.status) + } + + func updateDefaultOptions() { + guard let item = self.formItem, + let defaultOptions = item.style.defaultOptions, + self.status.isEditable == true, + item.draftValues == nil + else { return } + + item.draftValues = defaultOptions + self.status.edting(item: item) + } + + private func createHorizontalStackView() -> UIStackView { + let stackView = SBUStackView(axis: .horizontal, alignment: .leading, spacing: 8) + stackView.distribution = .fillProportionally + return stackView + } + + // MARK: - SBUMesageFormChipDelegate + func messageFormChipView(_ chip: SBUMesageFormChipView, didSelect value: String) { + guard self.status.isEditable == true else { return } + guard let item = self.formItem else { return } + guard let resultCount = item.style.resultCount else { return } + + let draftValues = item.draftValues ?? [] + + var values: [String] = [] + + if resultCount.isOnlyOne == true { + if item.required == true { + values = [value] + } else { + values = draftValues.contains(value) ? [] : [value] + } + } else { + values = draftValues.toggle(value) + } + + if resultCount.canUpdate(values) == false { return } + + item.draftValues = values + + self.status.edting(item: item) + self.updateChips() + self.updateInputValidation() + + self.delegate?.messageFormItemView(self, didUpdate: item) + } + + func isValidInput() -> InputErrorType { + guard let item = self.formItem else { return .none } + + let values = item.draftValues ?? [] + + if values.count == 0 { + if didValidation == false { return .none } + return item.required ? .required : .none + } + if item.style.resultCount?.isValid(values) == false { + return .invalid + } + return item.isValid(values) ? .none : .invalid + } +} diff --git a/Sources/View/Channel/MessageCell/MessageForm/Views/SBUMessageFormFallbackView.swift b/Sources/View/Channel/MessageCell/MessageForm/Views/SBUMessageFormFallbackView.swift new file mode 100644 index 0000000..cc77eb3 --- /dev/null +++ b/Sources/View/Channel/MessageCell/MessageForm/Views/SBUMessageFormFallbackView.swift @@ -0,0 +1,50 @@ +// +// SBUMessageFormFallbackView.swift +// SendbirdUIKit +// +// Created by Damon Park on 7/4/24. +// + +import UIKit + +/// The View exposed when the form message version does not valid +/// - Since: 3.27.0 +public class SBUMessageFormFallbackView: SBUMessageFormView { + let container = UIView() + let titleLabel = UILabel() + + public override func setupViews() { + super.setupViews() + + self.container.addSubview(self.titleLabel) + self.addSubview(self.container) + + let paragraphStyle = NSMutableParagraphStyle() + paragraphStyle.lineHeightMultiple = 1.2 + self.titleLabel.attributedText = NSMutableAttributedString( + string: SBUStringSet.FormType_Fallback_Message, + attributes: [NSAttributedString.Key.paragraphStyle: paragraphStyle] + ) + } + + public override func setupLayouts() { + super.setupLayouts() + + self.container + .sbu_constraint(equalTo: self, left: 0, right: 0, top: 0, bottom: 0) + .sbu_constraint_lessThan(width: 244) + + self.titleLabel.sbu_constraint(equalTo: self, left: 12, right: 12, top: 6, bottom: 6) + } + + public override func setupStyles() { + super.setupStyles() + + self.titleLabel.font = theme.userMessageFont + self.titleLabel.numberOfLines = 0 + self.titleLabel.textColor = theme.userMessageLeftTextColor + self.titleLabel.backgroundColor = .clear + self.container.layer.cornerRadius = 16 + self.container.backgroundColor = theme.leftBackgroundColor + } +} diff --git a/Sources/View/Channel/MessageCell/MessageForm/Views/SBUMessageFormItemView.swift b/Sources/View/Channel/MessageCell/MessageForm/Views/SBUMessageFormItemView.swift new file mode 100644 index 0000000..5543815 --- /dev/null +++ b/Sources/View/Channel/MessageCell/MessageForm/Views/SBUMessageFormItemView.swift @@ -0,0 +1,226 @@ +// +// SBUMessageFormItemView.swift +// SendbirdUIKit +// +// Created by Damon Park on 2024/07/02. +// Copyright © 2023 Sendbird, Inc. All rights reserved. +// + +import UIKit +import SendbirdChatSDK + +/// delegate for forwarding events from the form item +/// - Since: 3.27.0 +public protocol SBUMessageFormItemViewDelegate: AnyObject { + /// Called when `MessageFormItem` is updated. + /// - Parameters: + /// - itemView: The updated ``SBUMessageFormItemView`` object. + /// - formItem: The updated ``SendbirdChatSDK.MessageFormItem`` object. + func messageFormItemView(_ itemView: SBUMessageFormItemView, didUpdate formItem: SendbirdChatSDK.MessageFormItem) + + /// Called when `MessageFormItem` is validation checked. + /// - Parameters: + /// - itemView: The updated ``SBUMessageFormItemView`` object. + /// - formItem: The updated ``SendbirdChatSDK.MessageFormItem`` object. + func messageFormItemView(_ itemView: SBUMessageFormItemView, didCheckedValidation formItem: SendbirdChatSDK.MessageFormItem) +} + +/// The base view that holds the data for the form item. +/// - Since: 3.27.0 +open class SBUMessageFormItemView: SBUView { + // MARK: - Properties + + /// The theme for ``SBUMessageFormItemView`` that is type of ``SBUMessageCellTheme``. + public var theme: SBUMessageCellTheme = SBUTheme.messageCellTheme + + /// The id of the ``MessageForm``. + /// To update the data, use ``SBUMessageFormItemView/configure(form:item:didValidation:delegate:)``. + /// - Since: 3.27.0 + public private(set) var formId: Int64? + + /// The formItem of the ``MessageForm``. + /// To update the data, use ``SBUMessageFormItemView/configure(form:item:didValidation:delegate:)``. + /// - Since: 3.27.0 + public private(set) var formItem: SendbirdChatSDK.MessageFormItem? + + /// The validation status of the ``MessageForm``. + /// To update the data, use ``SBUMessageFormItemView/configure(form:item:didValidation:delegate:)``. + public var didValidation: Bool = false { + didSet { + guard didValidation == true else { return } + guard let item = self.formItem else { return } + self.delegate?.messageFormItemView(self, didCheckedValidation: item) + } + } + + /// The status of the ``MessageForm``. + /// Include value of item. + /// To update the data, use ``SBUMessageFormItemView/configure(form:item:didValidation:delegate:)``. + public var status: StatusType = .unknown + + /// The delegate that is type of ``SBUMessageFormItemViewDelegate`` + /// ```swift + /// view.delegate = self // `self` conforms to `SBUMessageFormItemViewDelegate` + /// ``` + public weak var delegate: SBUMessageFormItemViewDelegate? + + // MARK: - Configure + /// Configure ``SBUMessageFormItemView`` with `item`. + open func configure( + form: SendbirdChatSDK.MessageForm, + item: SendbirdChatSDK.MessageFormItem, + didValidation: Bool, + delegate: SBUMessageFormItemViewDelegate? = nil + ) { + self.formId = form.id + self.formItem = item + self.didValidation = didValidation + self.status = StatusType(form: form, item: item) + self.delegate = delegate + + self.setupViews() + self.setupLayouts() + self.setupStyles() + self.setupActions() + } +} + +extension SBUMessageFormItemView { + /// Attributed string for displaying the title of the item view. Also includes whether it is optional + public var titleAttributedString: NSAttributedString { + let title = NSMutableAttributedString() + title.append(NSAttributedString( + string: self.formItem?.name ?? "", + attributes: [ + .foregroundColor: theme.formTitleColor, + .font: theme.formTitleFont + ] + )) + title.append(NSAttributedString( + string: self.formItem?.required == false ? " " : "" + )) + title.append(NSAttributedString( + string: self.formItem?.required == false ? SBUStringSet.FormType_Optional : "", + attributes: [ + .foregroundColor: theme.formOptionalTitleColor, + .font: theme.formOptionalTitleFont + ] + )) + return title + } +} + +extension SBUMessageFormItemView { + /// Enum model to indicate the error type of the input value. + /// - Since: 3.27.0 + public enum InputErrorType { + /// Represents a required item error. + case required + /// Represents a invalid value error. + case invalid + /// none error. + case none + + var hasError: Bool { + switch self { + case .invalid: return true + case .required: return true + default: return false + } + } + + var errorMessage: String { + switch self { + case .invalid: return SBUStringSet.FormType_Error_Default + case .required: return SBUStringSet.FormType_Error_Required + default: return SBUStringSet.FormType_Error_Default + } + } + } +} + +extension SBUMessageFormItemView { + /// Enum model to indicate the status of the value in the currently entered item. + /// - Since: 3.27.0 + public enum StatusType { + /// Represents a completed form item with a value. + case done(values: [String]) + /// Represents an optional form item. + case optional + /// Represents a form item that is currently being edited with a value. + case editing(values: [String]?) + /// Represents an unknown form item status. + case unknown + + /// init + /// - Parameters: + /// - form: form data + /// - item: form item + /// - value: user input value + public init( + form: SendbirdChatSDK.MessageForm, + item: SendbirdChatSDK.MessageFormItem + ) { + guard form.isSubmitted == true else { + self = .editing(values: item.draftValues) + return + } + + if let values = item.submittedValues?.compactMap({ $0.hasElements ? $0 : nil }), values.hasElements { + self = .done(values: values) + } else { + self = .optional + } + } + + mutating func edting(item: MessageFormItem) { + guard isEditable == true else { return } + self = .editing(values: item.draftValues) + } + + /// The text property represents the current text value of the form item. + public var text: String? { + switch self { + case .done(let values): return values.first + case .editing(let values): return values?.first + case .optional: return SBUStringSet.FormType_No_Reponse + case .unknown: return nil + } + } + + /// The isDone property represents whether the form item has been completed. + public var isDone: Bool { + switch self { + case .done: return true + default: return false + } + } + + /// The didSubmit property represents whether the form has been submitted. + public var didSubmit: Bool { + switch self { + case .done: return true + case .optional: return true + default: return false + } + } + + /// The isOptional property represents whether the form item is optional. + public var isOptional: Bool { + switch self { + case .optional: return true + default: return false + } + } + + /// The isEditable property represents whether the form item is editable. + public var isEditable: Bool { + switch self { + case .editing: return true + case .unknown: return true + case .done: return false + case .optional: return false + } + } + } +} diff --git a/Sources/View/Channel/MessageCell/MessageForm/Views/SBUMessageFormMultiTextItemView.swift b/Sources/View/Channel/MessageCell/MessageForm/Views/SBUMessageFormMultiTextItemView.swift new file mode 100644 index 0000000..23b2976 --- /dev/null +++ b/Sources/View/Channel/MessageCell/MessageForm/Views/SBUMessageFormMultiTextItemView.swift @@ -0,0 +1,280 @@ +// +// SBUMessageFormMultiTextItemView.swift +// SendbirdUIKit +// +// Created by Damon Park on 7/2/24. +// + +import UIKit +import SendbirdChatSDK + +/// - Since: 3.27.0 +public class SBUMessageFormMultiTextItemView: SBUMessageFormItemView, UITextViewDelegate { + + /// A vertical stack view to configure layouts of the items. + public var stackView = SBUStackView(axis: .vertical, alignment: .leading, spacing: 0) + /// A horizontal stack view to configure layouts of `title` and `optional`. + public var titleStackView = SBUStackView(axis: .horizontal, alignment: .top, spacing: 3) + /// The `UILabel` displaying form item title.` + public var titleView = UILabel() + /// Top space view. + public var topSpaceView = UIView() + /// A container view to wrap `inputStackView`. + public var inputContainer = UIView() + /// A horizontal stack view to configure layouts of `valueStackView` and `input icon`. + public var inputStackView = SBUStackView(axis: .horizontal, alignment: .top, spacing: 0) + /// A vertical stack view to configure layouts of `input item` and `text label`. + public var valueStackView = SBUStackView(axis: .vertical, alignment: .fill, spacing: 0) + + /// The `UITextField` for displaying and interacting with the input form item. + public var inputTextView = SBUTextView() + /// The `UILabel` displaying the submitted values. + public var inputTextLabel = SBUPaddingLabel(3, 0, 6, 0) + /// A container view to wrap `inputIconView`. + public var iconContainer = UIView() + /// The `UIImageView` for displaying input completion icons. + public var inputIconView = UIImageView() + /// Bottom space view. can be hidden. Used only if there is an error message. + public var bottomSpaceView = UIView() + /// The `UILabel` displaying the error message. + public var errorTitleView = UILabel() + + private var isActive: Bool = false { + didSet { updateInputValidation() } + } + + // MARK: - Sendbird UIKit Life Cycle + public override func setupViews() { + super.setupViews() + + // + -- stackView ----------------------------- + + // | + --- titleStackView ------------------- + | + // | | titleView | | + // | + -------------------------------------- + | + // + ------------------------------------------ + + // | topSpaceView | + // + ------------------------------------------ + + // | + -- inputContainer -------------------- + | + // | | +---- inputStackView --------------- + | | + // | | | +- valueStackView -+ | | | + // | | | | inputTextView | | | | + // | | | +------------------+ inputIconView | | | + // | | | | inputTextLabel | | | | + // | | | +------------------+ | | | + // | | +----------------------------------- + | | + // | + -------------------------------------- + | + // + ------------------------------------------ + + // | bottomSpaceView | + // + ------------------------------------------ + + // | errorTitleView | + // + ------------------------------------------ + + + self.iconContainer.addSubview(self.inputIconView) + + self.titleStackView.setHStack([self.titleView]) + self.valueStackView.setVStack([self.inputTextView, self.inputTextLabel]) + self.inputStackView.setHStack([self.valueStackView, self.iconContainer]) + + self.inputContainer.addSubview(inputStackView) + + self.stackView.setVStack([ + self.titleStackView, + self.topSpaceView, + self.inputContainer, + self.bottomSpaceView, + self.errorTitleView + ]) + + self.inputTextView.delegate = self + + self.addSubview(stackView) + + self.titleView.attributedText = self.titleAttributedString + + self.errorTitleView.text = SBUStringSet.FormType_Error_Default + + self.inputTextView.placeholder = self.formItem?.placeholder + self.inputTextView.text = self.status.text + self.inputTextView.keyboardType = .default + + self.inputTextLabel.font = SBUFontSet.body3 + self.inputTextLabel.textColor = theme.formInputTitleColor + self.inputTextLabel.textAlignment = .left + self.inputTextLabel.numberOfLines = 0 + + self.inputIconView.isHidden = !self.status.didSubmit + } + + public override func setupLayouts() { + super.setupLayouts() + + self.titleView.sbu_constraint_greaterThan(height: 12) + + self.errorTitleView.sbu_constraint(height: 12) + + self.topSpaceView.sbu_constraint(width: 1, height: 6) + self.bottomSpaceView.sbu_constraint(width: 1, height: 4) + + self.inputTextView + .sbu_constraint_greaterThan(height: 80, priority: .defaultLow) + + self.inputContainer + .sbu_constraint_greaterThan(height: 96) + + self.iconContainer + .sbu_constraint(heightAnchor: self.inputStackView.heightAnchor, height: 0) + + self.inputIconView + .sbu_constraint(width: 20, height: 20, priority: UILayoutPriority(900)) + .sbu_constraint(equalTo: self.iconContainer, leading: 0, trailing: 0, bottom: 0) + + self.inputStackView + .sbu_constraint(equalTo: self.inputContainer, left: 6, top: 3) + .sbu_constraint(equalTo: self.inputContainer, right: 12, bottom: 8) + + self.inputContainer + .sbu_constraint(equalTo: self.stackView, left: 0, right: 0) + .sbu_constraint(height: 36, priority: .defaultLow) + + self.stackView + .sbu_constraint(equalTo: self, left: 0, top: 0) + .sbu_constraint(equalTo: self, right: 0, bottom: 0, priority: .defaultHigh) + .sbu_constraint_multiplier(widthAnchor: self.widthAnchor, widthMultiplier: 1) // NOTE: do not remove this constraints (AC-3258) + + self.inputStackView.spacing = 4 // NOTE: Set after layout setup due to auto layout warnings. + } + + public override func setupStyles() { + super.setupStyles() + + self.titleView.numberOfLines = 0 + + self.errorTitleView.font = theme.formErrorTitleFont + self.errorTitleView.textColor = theme.formInputErrorColor + + self.inputTextView.font = theme.formInputTextFont + self.inputTextView.textColor = theme.formInputTitleColor + self.inputTextView.placeholderColor = theme.formInputPlaceholderColor + self.inputTextView.textAlignment = .left + self.inputTextView.backgroundColor = .clear + self.inputTextView.autocorrectionType = .no + self.inputTextView.spellCheckingType = .no + self.inputTextView.alwaysBounceHorizontal = false + self.inputTextView.alwaysBounceVertical = true + self.inputTextView.isScrollEnabled = true + + self.inputTextLabel.font = theme.formInputTextFont + self.inputTextLabel.textColor = self.status.isOptional ? theme.formInputPlaceholderColor : theme.formInputTitleColor + self.inputTextLabel.textAlignment = .left + self.inputTextLabel.numberOfLines = 0 + self.inputTextLabel.backgroundColor = .clear + + self.inputContainer.layer.borderColor = theme.formInputBorderNormalColor.cgColor + self.inputContainer.layer.borderWidth = 1.0 + self.inputContainer.layer.cornerRadius = 6 + self.inputContainer.backgroundColor = self.status.didSubmit ? theme.formInputBackgroundDoneColor : theme.formInputBackgroundColor + + self.inputIconView.image = SBUIconSet.iconDone.sbu_with(tintColor: theme.formInputIconColor) + + self.updateInputValidation() + self.updateInputData() + } + + func updateInputValidation() { + if self.status.didSubmit == true { + self.inputContainer.layer.borderColor = UIColor.clear.cgColor + self.bottomSpaceView.isHidden = true + self.errorTitleView.isHidden = true + return + } + + let errorType = didValidation ? self.isValidInput() : .none + + if isActive == true { + // active: true + if errorTitleView.isHidden == false { + self.inputContainer.layer.borderColor = theme.formInputBorderErrorColor.cgColor + self.errorTitleView.isHidden = false + self.bottomSpaceView.isHidden = false + } else { + self.inputContainer.layer.borderColor = theme.formInputBorderActiveColor.cgColor + self.bottomSpaceView.isHidden = true + self.errorTitleView.isHidden = true + } + } else { + // active: false + if errorType.hasError == true { + self.inputContainer.layer.borderColor = theme.formInputBorderErrorColor.cgColor + self.errorTitleView.text = errorType.errorMessage + self.errorTitleView.isHidden = false + self.bottomSpaceView.isHidden = false + } else { + self.inputContainer.layer.borderColor = theme.formInputBorderNormalColor.cgColor + self.bottomSpaceView.isHidden = true + self.errorTitleView.isHidden = true + } + } + } + + /// Methods that update the view state and values + public func updateInputData() { + self.inputTextView.text = self.status.text + self.inputTextView.isEditable = self.status.isEditable + self.inputTextView.isHidden = self.status.didSubmit + + self.inputTextLabel.text = self.status.text + self.inputTextLabel.isHidden = !self.status.didSubmit + + self.iconContainer.isHidden = !self.status.didSubmit + } + + /// Text view delegate methods and called when text is changed + public func textViewDidChange(_ textView: UITextView) { + guard self.status.isEditable == true else { return } + guard let item = self.formItem else { return } + + item.draftValues = [textView.text].compactMap({ $0 }) + self.status.edting(item: item) + self.inputTextView.text = self.status.text + + self.delegate?.messageFormItemView(self, didUpdate: item) + + self.updateInputValidation() + + self.setNeedsLayout() + self.layoutIfNeeded() + } + + /// Text view delegate methods and called when editing starts + public func textViewDidBeginEditing(_ textView: UITextView) { + // NOTE: (AC-3323) + // fix scrolling issue + self.inputTextView.text = "" + self.inputTextView.text = self.formItem?.draftValues?.first + + self.isActive = true + } + + /// Text view delegate methods and called when editing end + public func textViewDidEndEditing(_ textView: UITextView) { + self.didValidation = true + self.isActive = false + } + + func isValidInput() -> InputErrorType { + guard let item = self.formItem else { return .none } + let value = item.draftValues?.first ?? "" + if value.isEmpty { + if didValidation == false { return .none } + return item.required ? .required : .none + } + return item.isValid([value]) ? .none : .invalid + } + + func scrollToBottom() { + if inputTextView.text.count > 0 { + let bottom = NSRange(location: inputTextView.text.count - 1, length: 1) + inputTextView.scrollRangeToVisible(bottom) + } + } +} diff --git a/Sources/View/Channel/MessageCell/MessageForm/Views/SBUMessageFormSingleTextItemView.swift b/Sources/View/Channel/MessageCell/MessageForm/Views/SBUMessageFormSingleTextItemView.swift new file mode 100644 index 0000000..32d4854 --- /dev/null +++ b/Sources/View/Channel/MessageCell/MessageForm/Views/SBUMessageFormSingleTextItemView.swift @@ -0,0 +1,255 @@ +// +// SBUMessageFormSingleTextItemView.swift +// SendbirdUIKit +// +// Created by Damon Park on 7/2/24. +// + +import UIKit +import SendbirdChatSDK + +/// - Since: 3.27.0 +public class SBUMessageFormSingleTextItemView: SBUMessageFormItemView, UITextFieldDelegate { + + /// A vertical stack view to configure layouts of the items. + public var stackView = SBUStackView(axis: .vertical, alignment: .leading, spacing: 0) + /// A horizontal stack view to configure layouts of `title` and `optional`. + public var titleStackView = SBUStackView(axis: .horizontal, alignment: .fill, spacing: 3) + /// The `UILabel` displaying form item title.` + public var titleView = UILabel() + /// Top space view. + public var topSpaceView = UIView() + /// A container view to wrap `inputStackView`. + public var inputContainer = UIView() + /// A horizontal stack view to configure layouts of `valueStackView` and `input icon`. + public var inputStackView = SBUStackView(axis: .horizontal, alignment: .fill, spacing: 4) + /// A vertical stack view to configure layouts of `input item` and `text label`. + public var valueStackView = SBUStackView(axis: .vertical, alignment: .fill, spacing: 0) + /// The `UITextField` for displaying and interacting with the input form item. + public var inputTextField = UITextField() + /// The `UILabel` displaying the submitted values. + public var inputTextLabel = UILabel() + /// A container view to wrap `inputIconView`. + public var iconContainer = UIView() + /// The `UIImageView` for displaying input completion icons. + public var inputIconView = UIImageView() + /// Bottom space view. can be hidden. Used only if there is an error message. + public var bottomSpaceView = UIView() + /// The `UILabel` displaying the error message. + public var errorTitleView = UILabel() + + private var isActive: Bool = false { + didSet { updateInputValidation() } + } + + // MARK: - Sendbird UIKit Life Cycle + public override func setupViews() { + super.setupViews() + + // + -- stackView ----------------------------- + + // | + --- titleStackView ------------------- + | + // | | titleView | | + // | + -------------------------------------- + | + // + ------------------------------------------ + + // | topSpaceView | + // + ------------------------------------------ + + // | + -- inputContainer -------------------- + | + // | | +---- inputStackView --------------- + | | + // | | | +- valueStackView -+ | | | + // | | | | inputTextField | | | | + // | | | +------------------+ inputIconView | | | + // | | | | inputTextLabel | | | | + // | | | +------------------+ | | | + // | | +----------------------------------- + | | + // | + -------------------------------------- + | + // + ------------------------------------------ + + // | bottomSpaceView | + // + ------------------------------------------ + + // | errorTitleView | + // + ------------------------------------------ + + + self.iconContainer.addSubview(self.inputIconView) + + self.titleStackView.setHStack([self.titleView]) + self.valueStackView.setVStack([self.inputTextField, self.inputTextLabel]) + self.inputStackView.setHStack([self.valueStackView, self.iconContainer]) + + self.inputContainer.addSubview(inputStackView) + + self.stackView.setVStack([ + self.titleStackView, + self.topSpaceView, + self.inputContainer, + self.bottomSpaceView, + self.errorTitleView + ]) + + self.inputTextField.delegate = self + + self.addSubview(stackView) + + self.titleView.attributedText = self.titleAttributedString + + self.errorTitleView.text = SBUStringSet.FormType_Error_Default + + self.inputTextField.placeholder = self.formItem?.placeholder + self.inputTextField.text = self.status.text + self.inputTextField.keyboardType = formItem?.style.layout.keyboardType ?? .default + + self.inputTextLabel.text = self.status.text + } + + public override func setupLayouts() { + super.setupLayouts() + + self.titleView.sbu_constraint_greaterThan(height: 12) + + self.errorTitleView.sbu_constraint(height: 12) + + self.topSpaceView.sbu_constraint(width: 1, height: 6) + self.bottomSpaceView.sbu_constraint(width: 1, height: 4) + + self.inputTextField + .sbu_constraint(height: 20) + + self.inputIconView + .sbu_constraint(width: 20, height: 20) + .sbu_constraint(equalTo: self.iconContainer, leading: 0, trailing: 0, bottom: 0) + + self.inputStackView + .sbu_constraint(equalTo: self.inputContainer, left: 12, top: 8) + .sbu_constraint(equalTo: self.inputContainer, right: 12, bottom: 8) + + self.inputContainer + .sbu_constraint(equalTo: self.stackView, left: 0, right: 0) + .sbu_constraint(height: 36, priority: .defaultLow) + + self.stackView + .sbu_constraint(equalTo: self, left: 0, top: 0) + .sbu_constraint(equalTo: self, right: 0, bottom: 0, priority: .defaultHigh) + .sbu_constraint_multiplier(widthAnchor: self.widthAnchor, widthMultiplier: 1) // NOTE: do not remove this constraints (AC-3258) + } + + public override func setupStyles() { + super.setupStyles() + + self.titleView.numberOfLines = 0 + + self.errorTitleView.font = theme.formErrorTitleFont + self.errorTitleView.textColor = theme.formInputErrorColor + + self.inputTextField.font = theme.formInputTextFont + self.inputTextField.textColor = theme.formInputTitleColor + self.inputTextField.setPlaceholderColor(theme.formInputPlaceholderColor) + self.inputTextField.textAlignment = .left + self.inputTextField.borderStyle = .none + self.inputTextField.autocorrectionType = .no + self.inputTextField.spellCheckingType = .no + + self.inputTextLabel.font = theme.formInputTextFont + self.inputTextLabel.textColor = self.status.isOptional ? theme.formInputPlaceholderColor : theme.formInputTitleColor + self.inputTextLabel.textAlignment = .left + self.inputTextLabel.numberOfLines = 0 + self.inputTextLabel.backgroundColor = .clear + + self.inputContainer.layer.borderColor = theme.formInputBorderNormalColor.cgColor + self.inputContainer.layer.borderWidth = 1.0 + self.inputContainer.layer.cornerRadius = 6 + self.inputContainer.backgroundColor = self.status.didSubmit ? theme.formInputBackgroundDoneColor : theme.formInputBackgroundColor + + self.inputIconView.image = SBUIconSet.iconDone.sbu_with(tintColor: theme.formInputIconColor) + + self.updateInputValidation() + self.updateInputData() + } + + public override func setupActions() { + super.setupActions() + + self.inputTextField.addTarget(self, action: #selector(onChangeFieldValue(textField:)), for: .editingChanged) + } + + @objc + func onChangeFieldValue(textField: UITextField) { + guard self.status.isEditable == true else { return } + guard let item = self.formItem else { return } + + item.draftValues = [textField.text].compactMap({ $0 }) + self.status.edting(item: item) + self.inputTextField.text = self.status.text + + self.delegate?.messageFormItemView(self, didUpdate: item) + + self.updateInputValidation() + + self.setNeedsLayout() + self.layoutIfNeeded() + } + + func updateInputValidation() { + if self.status.didSubmit == true { + self.inputContainer.layer.borderColor = UIColor.clear.cgColor + self.bottomSpaceView.isHidden = true + self.errorTitleView.isHidden = true + return + } + + let errorType = didValidation ? self.isValidInput() : .none + + if isActive == true { + if errorType.hasError == true { + self.inputContainer.layer.borderColor = theme.formInputBorderErrorColor.cgColor + self.errorTitleView.isHidden = false + self.bottomSpaceView.isHidden = false + } else { + self.inputContainer.layer.borderColor = theme.formInputBorderActiveColor.cgColor + self.bottomSpaceView.isHidden = true + self.errorTitleView.isHidden = true + } + } else { + if errorType.hasError == true { + self.inputContainer.layer.borderColor = theme.formInputBorderErrorColor.cgColor + self.errorTitleView.text = errorType.errorMessage + self.errorTitleView.isHidden = false + self.bottomSpaceView.isHidden = false + } else { + self.inputContainer.layer.borderColor = theme.formInputBorderNormalColor.cgColor + self.bottomSpaceView.isHidden = true + self.errorTitleView.isHidden = true + } + } + } + + /// Methods that update the view state and values + public func updateInputData() { + self.inputTextField.text = self.status.text + self.inputTextField.isEnabled = self.status.isEditable + self.inputTextField.isHidden = self.status.didSubmit + + self.inputTextLabel.text = self.status.text + self.inputTextLabel.isHidden = !self.status.didSubmit + + self.iconContainer.isHidden = !self.status.didSubmit + } + + /// Text view delegate methods and called when editing starts + public func textFieldDidBeginEditing(_ textField: UITextField) { + self.isActive = true + } + + /// Text view delegate methods and called when editing end + public func textFieldDidEndEditing(_ textField: UITextField) { + self.didValidation = true + self.isActive = false + } + + func isValidInput() -> InputErrorType { + guard let item = self.formItem else { return .none } + let value = item.draftValues?.first ?? "" + if value.isEmpty { + if didValidation == false { return .none } + return item.required ? .required : .none + } + return item.isValid([value]) ? .none : .invalid + } +} diff --git a/Sources/View/Channel/MessageCell/MessageForm/Views/SBUMessageFormView.swift b/Sources/View/Channel/MessageCell/MessageForm/Views/SBUMessageFormView.swift new file mode 100644 index 0000000..e552288 --- /dev/null +++ b/Sources/View/Channel/MessageCell/MessageForm/Views/SBUMessageFormView.swift @@ -0,0 +1,240 @@ +// +// SBUMessageFormView.swift +// SendbirdUIKit +// +// Created by Damon Park on 2024/07/02. +// Copyright © 2023 Sendbird, Inc. All rights reserved. +// + +import UIKit +import SendbirdChatSDK + +/// delegate for forwarding events from the form. +/// - Since: 3.27.0 +public protocol SBUMessageFormViewDelegate: AnyObject { + /// Called when `messageForm` is submitted. + /// - Parameters: + /// - view: ``SBUMessageFormView`` object. + /// - draft: the submitted ``SendbirdChatSDK.MessageForm`` object. + func messageFormView(_ view: SBUMessageFormView, didSubmit form: SendbirdChatSDK.MessageForm) + + /// Called the validation status of the `MessageFormItem` + /// - Parameters: + /// - view: ``SBUMessageFormView`` object. + /// - didUpdateValidationStatus: Validation status of form items (key: item number, value: validation status) + func messageFormView(_ view: SBUMessageFormView, didUpdateValidationStatus: [Int64: Bool]) + + /// Called when the view frame of the `MessageFormView` is changed. + /// - Parameters: + /// - view: ``SBUMessageFormView`` object. + /// - didUpdateLayoutSize: Updated form view frame. + func messageFormView(_ view: SBUMessageFormView, didUpdateViewFrame: CGRect) +} + +/// Basic message form view +/// - Since: 3.27.0 +open class SBUMessageFormView: SBUView, SBUMessageFormItemViewDelegate { + public var theme: SBUMessageCellTheme = SBUTheme.messageCellTheme + + /// (Read-only) The form from ``SBUMessageFormViewParams`` + /// - Since: 3.27.0 + public var messageForm: SendbirdChatSDK.MessageForm? { params?.messageForm } + + /// (Read-only) The message ID for quick reply which is from ``SBUMessageFormViewParams`` + public var messageId: Int64? { params?.messageId } + + /// (Read-only) The data structure for ``SBUMessageFormViewParams``. Please use ``configure(with:delegate:)`` to update ``params`` + public private(set) var params: SBUMessageFormViewParams? + + /// Instances of the created item views. Can be `nil`. + public var itemViews: [SBUMessageFormItemView]? + + /// Tracks validation status of each form item to prevent duplicate submissions. + public var itemValidationStatus: [Int64: Bool] = [:] + + /// The delegate that is type of ``SBUMessageFormViewDelegate`` + public weak var delegate: SBUMessageFormViewDelegate? + + var currentBounds: CGRect = .zero + + /// Updates UI with ``SBUMessageFormViewParams`` object and ``SBUMessageFormViewDelegate``. + /// - Parameters: + /// - configuration: ``SBUMessageFormViewParams`` object. + /// - delegate: ``SBUMessageFormViewDelegate``, the delegate object that handles the form item event sent by ``SBUMessageFormItemViewDelegate``. + /// - Note: This method updates ``params`` and ``delegate`` then, calls ``setupViews()``, ``setupLayouts()`` and ``setupStyles()`` + open func configure(with configuration: SBUMessageFormViewParams, delegate: SBUMessageFormViewDelegate? = nil) { + self.params = configuration + self.delegate = delegate + self.itemValidationStatus = configuration.itemValidationStatus + + self.setupViews() + self.setupLayouts() + self.setupStyles() + } + + /// Method to return a view that inherits from ``SBUMessageFormItemView``. + /// The parent class contains only data. + open func createItemView(_ item: MessageFormItem) -> SBUMessageFormItemView? { + switch item.style.layout { + case .chip: + return SBUMessageFormChipsItemView() + case .textarea: + return SBUMessageFormMultiTextItemView() + case .text, .email, .number, .phone: + return SBUMessageFormSingleTextItemView() + case .unknown: + return nil + } + } + + /// Creates ``SBUMessageFormItemView`` instances with ``SBUMessageFormViewParams``. + /// - Parameter forms: The array of ``SBUMessageForm``. + /// - Returns: The array of ``SBUMessageFormItemView`` instances. + /// - Since: 3.27.0 + open func createFormItemViews(with form: SendbirdChatSDK.MessageForm?) -> [SBUMessageFormItemView] { + guard let form = form else { return [] } + return form.items.compactMap { item in + let view = createItemView(item) + view?.configure( + form: form, + item: item, + didValidation: itemValidationStatus[item.id] ?? false, + delegate: self + ) + return view + } + } + + // MARK: `SBUMessageFormItemViewDelegate`` + + /// Called when a form item is updated. + /// It invokes ``SBUMessageFormItemViewDelegate/messageFormItemView(_:didUpdate:)` + open func messageFormItemView(_ itemView: SBUMessageFormItemView, didUpdate formItem: MessageFormItem) { + self.setupStyles() + } + + /// Called when `MessageFormItem` is validation checked. + /// It invokes ``SBUMessageFormItemViewDelegate/messageFormItemView(_:didUpdateValidationStatus:)` + open func messageFormItemView(_ itemView: SBUMessageFormItemView, didCheckedValidation formItem: MessageFormItem) { + self.itemValidationStatus.updateValue(true, forKey: formItem.id) + self.delegate?.messageFormView(self, didUpdateValidationStatus: self.itemValidationStatus) + } + + open override func layoutSubviews() { + super.layoutSubviews() + + guard self.currentBounds != self.bounds else { return } + + self.currentBounds = self.bounds + + self.delegate?.messageFormView(self, didUpdateViewFrame: self.bounds) + } + + /// Method called when the form is submitted. + /// If submit is not possible, treat all form items as having validation checked once + /// If submit is successful, proceed with the submit flow + /// - Returns: Boolean if submit went successfully + @objc + open func onSubmit() -> Bool { + defer { + self.setNeedsLayout() + self.layoutIfNeeded() + } + guard let form = self.messageForm else { return false } + + guard form.canSubmit == true else { + for item in form.items { + if let itemView = itemViews?.first(where: { $0.formId == item.id }) { + itemView.didValidation = true + itemView.setNeedsLayout() + } + self.itemValidationStatus.updateValue(true, forKey: item.id) + } + self.delegate?.messageFormView(self, didUpdateValidationStatus: self.itemValidationStatus) + self.setupViews() + return false + } + + self.delegate?.messageFormView(self, didSubmit: form) + return true + } +} + +/// - Since: 3.27.0 +public class SBUSimpleMessageFormView: SBUMessageFormView { + // views + + /// A container view to wrap `stackView`. + public var container: UIView = UIView() + /// A vertical stack view to configure layouts of the forms. + public var stackView: UIStackView = SBUStackView(axis: .vertical, alignment: .fill, spacing: 12) + /// The `UIButton` displaying the submit button. + public var submitButton: UIButton = UIButton() + + // MARK: - Sendbird UIKit Life Cycle + + open override func setupViews() { + super.setupViews() + + // + ---- stackView ---- + + // | [itemViews] | + // + ------------------- + + // | submitButton | + // + ------------------- + + + let itemViews = self.createFormItemViews(with: self.messageForm) + self.stackView.setVStack(itemViews) + self.itemViews = itemViews + + self.stackView.addArrangedSubview(self.submitButton) + self.container.addSubview(self.stackView) + self.addSubview(self.container) + } + + public override func setupLayouts() { + super.setupLayouts() + + self.stackView + .sbu_constraint(equalTo: self.container, left: 12, right: 12, top: 16, bottom: 16) + + self.submitButton + .sbu_constraint(height: 36) + + self.container + .sbu_constraint(width: 244) + .sbu_constraint(equalTo: self, leading: 0, trailing: 0, top: 0, bottom: 0) + } + + public override func setupStyles() { + super.setupStyles() + + self.container.backgroundColor = theme.formBackgroundColor + self.container.layer.cornerRadius = 16 + + self.submitButton.clipsToBounds = true + self.submitButton.layer.cornerRadius = 6 + self.submitButton.titleLabel?.font = theme.formSubmittButtonFont + self.submitButton.setTitle(SBUStringSet.Submit, for: .normal) + self.submitButton.setTitle(SBUStringSet.FormType_Submit_Done, for: .disabled) + self.submitButton.setTitleColor(theme.formSubmitButtonTitleColor, for: .normal) + self.submitButton.setTitleColor(theme.formSubmitButtonTitleDisabledColor, for: .disabled) + self.submitButton.setBackgroundImage(UIImage.from(color: theme.formSubmitButtonBackgroundColor), for: .normal) + self.submitButton.setBackgroundImage(UIImage.from(color: theme.formSubmitButtonBackgroundDisabledColor), for: .disabled) + + self.submitButton.isEnabled = (self.params?.isSubmitting == false && self.messageForm?.isSubmitted == false) + } + + public override func setupActions() { + super.setupActions() + + self.submitButton.addTarget(self, action: #selector(onSubmit), for: .touchUpInside) + } + + public override func onSubmit() -> Bool { + let success = super.onSubmit() + if success { + self.submitButton.isEnabled = false + } + return success + } +} diff --git a/Sources/View/Channel/MessageCell/MessageForm/Views/SubViews/SBUMessageFormChipView.swift b/Sources/View/Channel/MessageCell/MessageForm/Views/SubViews/SBUMessageFormChipView.swift new file mode 100644 index 0000000..c80628a --- /dev/null +++ b/Sources/View/Channel/MessageCell/MessageForm/Views/SubViews/SBUMessageFormChipView.swift @@ -0,0 +1,202 @@ +// +// SBUMesageFormChipView.swift +// SendbirdUIKit +// +// Created by Damon Park on 7/3/24. +// + +import UIKit +import SendbirdChatSDK + +/// - Since: 3.27.0 +protocol SBUMesageFormChipViewDelegate: AnyObject { + func messageFormChipView(_ chip: SBUMesageFormChipView, didSelect value: String) +} + +/// Chip view A view that displays items +/// - Since: 3.27.0 +public class SBUMesageFormChipView: SBUView, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout { + /// The theme for ``SBUMessageFormItemView`` that is type of ``SBUMessageCellTheme``. + var theme: SBUMessageCellTheme = SBUTheme.messageCellTheme + + /// chip items + public var chips: [String] = [] + + var status: SBUMessageFormItemView.StatusType = .unknown + + weak var delegate: SBUMesageFormChipViewDelegate? + + lazy var collectionView: UICollectionView = { + let layout = LeftAlignedCollectionViewFlowLayout() + layout.minimumInteritemSpacing = 4 + layout.minimumLineSpacing = 4 + layout.sectionInset = .zero + + let collectionView = SBUWrappingCollectionView(frame: .zero, collectionViewLayout: layout) + collectionView.dataSource = self + collectionView.delegate = self + collectionView.backgroundColor = .clear + collectionView.isScrollEnabled = false + collectionView.register( + SBUMesageFormChipCell.self, + forCellWithReuseIdentifier: SBUMesageFormChipCell.sbu_className + ) + + return collectionView + }() + + func update(chips: [String], status: SBUMessageFormItemView.StatusType) { + self.status = status + self.chips = chips + self.collectionView.reloadData() + } + + public override func setupViews() { + super.setupViews() + + self.addSubview(self.collectionView) + } + + public override func setupLayouts() { + super.setupLayouts() + + self.collectionView.sbu_constraint(equalTo: self, leading: 0, trailing: 0, top: 0, bottom: 0) + } + + public func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { + self.chips.count + } + + public func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { + guard let cell = collectionView.dequeueReusableCell( + withReuseIdentifier: SBUMesageFormChipCell.sbu_className, + for: indexPath + ) as? SBUMesageFormChipCell else { + return UICollectionViewCell() + } + let value = self.chips[indexPath.row] + let state = SBUMesageFormChipCell.ChipState(status: self.status, value: value) + cell.configure(value: value, state: state) + return cell + } + + public func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { + guard self.status.isEditable == true else { return } + self.delegate?.messageFormChipView(self, didSelect: self.chips[indexPath.row]) + } + + public func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { + let value = self.chips[indexPath.row] + let size = (value as NSString).size(withAttributes: [NSAttributedString.Key.font: theme.formChipTextFont]) + let state = SBUMesageFormChipCell.ChipState(status: self.status, value: value) + let maxWidth = collectionView.bounds.width + let iconViewWidth: CGFloat = state.isSubmitted ? 20 + 3 : 0 + let width = min(size.width + 24 + iconViewWidth, maxWidth) + return CGSize(width: width.rounded(.up), height: 32) + } +} + +/// - Since: 3.27.0 +class SBUMesageFormChipCell: SBUCollectionViewCell { + /// The theme for ``SBUMessageFormItemView`` that is type of ``SBUMessageCellTheme``. + var theme: SBUMessageCellTheme = SBUTheme.messageCellTheme + /// The `UILabel` displaying form field title.` + var titleView = UILabel() + /// The `UIImageView` for displaying input completion icons. + var inputIconView = UIImageView() + /// A horizontal stack view to configure layouts of `title` and `icon`. + var stackView = SBUStackView(axis: .horizontal, alignment: .center, spacing: 3) + + var state: ChipState = .submitted(true) { + didSet { updateAppearance() } + } + + func configure(value: String, state: ChipState) { + self.titleView.text = value + self.state = state + + self.updateAppearance() + } + + override func setupViews() { + self.stackView.setHStack([self.titleView, self.inputIconView]) + self.contentView.addSubview(self.stackView) + } + + override func setupLayouts() { + self.stackView + .sbu_constraint(height: 32, priority: .defaultLow) + .sbu_constraint(equalTo: self.contentView, left: 12, top: 0) + .sbu_constraint(equalTo: self.contentView, right: 12, bottom: 0) + + self.inputIconView + .sbu_constraint(width: 20, height: 20) + } + + override func setupStyles() { + self.titleView.textAlignment = .center + self.titleView.numberOfLines = 1 + self.titleView.font = theme.formChipTextFont + self.titleView.lineBreakMode = .byTruncatingTail + + self.layer.cornerRadius = 16 + + self.inputIconView.image = SBUIconSet.iconDone.sbu_with(tintColor: theme.formInputIconColor) + self.updateAppearance() + } + + private func updateAppearance() { + switch state { + case .selected(false): + self.layer.borderWidth = 1 + self.layer.borderColor = theme.formChipBorderNormalColor.cgColor + self.backgroundColor = theme.formChipBackgroundNormalColor + self.titleView.textColor = theme.formChipTitleNormalColor + self.inputIconView.isHidden = true + case .selected(true): + self.layer.borderWidth = 1 + self.layer.borderColor = theme.formChipBorderSelectColor.cgColor + self.backgroundColor = theme.formChipBackgroundSelectColor + self.titleView.textColor = theme.formChipTitleSelectColor + self.inputIconView.isHidden = true + case .submitted(false): + self.layer.borderWidth = 1 + self.layer.borderColor = theme.formChipBorderDisableColor.cgColor + self.backgroundColor = theme.formChipBackgroundDisableColor + self.titleView.textColor = theme.formChipTitleDisableColor + self.inputIconView.isHidden = true + case .submitted(true): + self.layer.borderWidth = 1 + self.layer.borderColor = theme.formChipBorderSubmittedColor.cgColor + self.backgroundColor = theme.formChipBackgroundSubmittedColor + self.titleView.textColor = theme.formChipTitleSubmittedColor + self.inputIconView.isHidden = false + } + } +} + +extension SBUMesageFormChipCell { + /// - Since: 3.27.0 + enum ChipState { + case selected(Bool) + case submitted(Bool) + + var isSubmitted: Bool { + switch self { + case .submitted(true): return true + default: return false + } + } + } +} + +extension SBUMesageFormChipCell.ChipState { + init(status: SBUMessageFormItemView.StatusType, value: String) { + switch status { + case .done(let values): self = .submitted(values.contains(value)) + case .editing(let values): self = .selected((values ?? []).contains(value)) + case .optional: self = .submitted(false) + case .unknown: self = .submitted(false) + } + } +} diff --git a/Sources/View/Channel/MessageCell/NotificationChannel/SBUNotificationCell.swift b/Sources/View/Channel/MessageCell/NotificationChannel/SBUNotificationCell.swift index 0099024..06d2511 100644 --- a/Sources/View/Channel/MessageCell/NotificationChannel/SBUNotificationCell.swift +++ b/Sources/View/Channel/MessageCell/NotificationChannel/SBUNotificationCell.swift @@ -299,7 +299,7 @@ class SBUNotificationCell: SBUBaseMessageCell { guard subType == 0 else { return } // subType: 0 is template type let subData = notification?.extendedMessage["sub_data"] as? String - var bindedTemplate: String? + var bindedTemplate: BindedTemplate? var isNewTemplateDownloading: Bool = false if !isTemplateDownloadFailed { @@ -315,12 +315,13 @@ class SBUNotificationCell: SBUBaseMessageCell { } } - bindedTemplate = bindedTemplate?.replacingOccurrences(of: "\\n", with: "\\\\n") - bindedTemplate = bindedTemplate?.replacingOccurrences(of: "\n", with: "\\n") + var escapedTemplate = bindedTemplate?.template.replacingOccurrences(of: "\\n", with: "\\\\n") ?? "{}" + escapedTemplate = escapedTemplate.replacingOccurrences(of: "\n", with: "\\n") + bindedTemplate?.template = escapedTemplate var template: SBUMessageTemplate.Syntax.TemplateView? do { - template = try JSONDecoder().decode(SBUMessageTemplate.Syntax.TemplateView.self, from: Data((bindedTemplate ?? "").utf8)) + template = try JSONDecoder().decode(SBUMessageTemplate.Syntax.TemplateView.self, from: Data((bindedTemplate?.template ?? "").utf8)) template?.setIdentifier(with: .init(messageId: message?.messageId)) } catch { SBULog.error(error) @@ -334,33 +335,47 @@ class SBUNotificationCell: SBUBaseMessageCell { } self.notificationTemplateRenderer = nil - if isNewTemplateDownloading { + switch bindedTemplate?.type { + case .ui: + if isNewTemplateDownloading { + self.notificationTemplateRenderer = SBUMessageTemplate.Renderer( + body: .downloadingTemplate( + height: (type == .chat) + ? chatNotificationDownloadingHeight + : feedNotificationDownloadingHeight + ), + fontFamily: SBUFontSet.FontFamily.notifications + ) + } else if let bindedTemplate = bindedTemplate, !showFallback, // 정상 케이스 + let notificationTemplateRenderer = SBUMessageTemplate.Renderer( + with: bindedTemplate.template, + messageId: message?.messageId, + delegate: self, + maxWidth: self.availableTemplateWidth, + fontFamily: SBUFontSet.FontFamily.notifications, + actionHandler: { [weak self] action in + self?.statisticsForAction(with: subData) + self?.messageTemplateActionHandler?(action) + } + ) { + self.notificationTemplateRenderer = notificationTemplateRenderer + self.isRendered = true + } else { + self.notificationTemplateRenderer = parsingErrorNotificationRenderer + } + case .data: self.notificationTemplateRenderer = SBUMessageTemplate.Renderer( - body: .downloadingTemplate( - height: (type == .chat) - ? chatNotificationDownloadingHeight - : feedNotificationDownloadingHeight + body: .dataTemplate( + text: "[This message is sent from data template.]", + subText: bindedTemplate?.template ?? "{}" ), fontFamily: SBUFontSet.FontFamily.notifications ) - } else if let bindedTemplate = bindedTemplate, !showFallback, // 정상 케이스 - let notificationTemplateRenderer = SBUMessageTemplate.Renderer( - with: bindedTemplate, - messageId: message?.messageId, - delegate: self, - maxWidth: self.availableTemplateWidth, - fontFamily: SBUFontSet.FontFamily.notifications, - actionHandler: { [weak self] action in - self?.statisticsForAction(with: subData) - self?.messageTemplateActionHandler?(action) - } - ) { - self.notificationTemplateRenderer = notificationTemplateRenderer self.isRendered = true - } else { + default: self.notificationTemplateRenderer = parsingErrorNotificationRenderer } - + self.notificationTemplateRenderer?.delegate = self guard let notificationTemplateRenderer = self.notificationTemplateRenderer else { return } notificationTemplateRenderer.backgroundColor = self.notificationCellTheme.backgroundColor diff --git a/Sources/View/Channel/MessageCell/SBUBaseMessageCell.swift b/Sources/View/Channel/MessageCell/SBUBaseMessageCell.swift index dcf5bce..05f2bdc 100644 --- a/Sources/View/Channel/MessageCell/SBUBaseMessageCell.swift +++ b/Sources/View/Channel/MessageCell/SBUBaseMessageCell.swift @@ -71,6 +71,7 @@ open class SBUBaseMessageCell: SBUTableViewCell, SBUMessageCellProtocol, SBUFeed var moreEmojiTapHandler: (() -> Void)? var emojiLongPressHandler: ((_ emojiKey: String) -> Void)? var mentionTapHandler: ((_ user: SBUUser) -> Void)? + var errorHandler: ((_ error: SBError) -> Void)? /// The action of ``SBUSuggestedReplyView`` that is called when a ``SBUSuggestedReplyOptionView`` is selected. /// - Parameter selectedOptionView: The selected ``SBUSuggestedReplyOptionView`` object. @@ -81,8 +82,16 @@ open class SBUBaseMessageCell: SBUTableViewCell, SBUMessageCellProtocol, SBUFeed /// - form: The ``SendbirdChatSDK.Form`` object that will be submitted. /// - messageCell: The current ``SBUBaseMessageCell`` object. /// - Since: 3.16.0 + @available(*, deprecated, message: "This method is deprecated in 3.27.0.") var submitFormHandler: ((_ form: SendbirdChatSDK.Form, _ messageCell: SBUBaseMessageCell) -> Void)? + /// The action of ``SBUMessageFormView`` that is called when a ``SendbirdChatSDK.MessageForm`` is submitted. + /// - Parameters: + /// - messageForm: The current ``MessageForm`` object. + /// - messageCell: The current ``SBUBaseMessageCell`` object. + /// - Since: 3.27.0 + var submitMessageFormHandler: ((_ messageForm: SendbirdChatSDK.MessageForm, _ messageCell: SBUBaseMessageCell) -> Void)? + /// The action of ``SBUFeedbackView`` that is called when a `Feedback` is updated. /// - Parameters: /// - feedbackAnswer: The ``SBUFeedbackAnswer`` object that will be submitted. diff --git a/Sources/View/Channel/MessageCell/SBUContentBaseMessageCell.swift b/Sources/View/Channel/MessageCell/SBUContentBaseMessageCell.swift index 95c14b1..0012d53 100644 --- a/Sources/View/Channel/MessageCell/SBUContentBaseMessageCell.swift +++ b/Sources/View/Channel/MessageCell/SBUContentBaseMessageCell.swift @@ -124,30 +124,33 @@ open class SBUContentBaseMessageCell: SBUBaseMessageCell { /// Type specifying the maximum width of the message view /// - Since: 3.21.0 - var containerType: SBUMessageContainerType { - self.message?.asUiSettingContainerType ?? .default - } + @available(*, deprecated, message: "`containerType` has been deprecated since 3.27.2.") + var containerType: SBUMessageContainerType { .default } /// Used when the containertype is wide, to place state view below the message bubble. /// - Since: 3.21.0 + @available(*, deprecated, message: "`wideSizeStateContainerView` has been deprecated since 3.27.2.") lazy var wideSizeStateContainerView: UIStackView = { return SBUStackView(axis: .horizontal, alignment: .center, spacing: 12) }() /// This is the view used to display the state view after spacing out the profile area in the `wideSizeStateContainerView`. /// - Since: 3.21.0 + @available(*, deprecated, message: "`wideSizeProfileSpaceView` has been deprecated since 3.27.2.") lazy var wideSizeProfileSpaceView: UIView = { return UIView() }() /// The fullSizeMessageContainerView is attached as an overlay. /// - Since: 3.21.0 + @available(*, deprecated, message: "`fullSizeMessageContainerView` has been deprecated since 3.27.2.") lazy var fullSizeMessageContainerView: UIStackView = { return SBUStackView(axis: .vertical, alignment: .fill, spacing: 4) }() // fullSizeMessageConstraints exists as a property to handle active/deactive. /// - Since: 3.21.0 + @available(*, deprecated, message: "`fullSizeMessageConstraints` has been deprecated since 3.27.2.") var fullSizeMessageConstraints: [NSLayoutConstraint] = [] { didSet { NSLayoutConstraint.deactivate(oldValue) @@ -243,7 +246,7 @@ open class SBUContentBaseMessageCell: SBUBaseMessageCell { self.mainContainerVStackView.setVStack([ self.mainContainerView, ]), - self.containerType.isDefaultSize ? self.stateView : nil, + self.stateView, self.messageSpacing ]) ]) @@ -251,16 +254,11 @@ open class SBUContentBaseMessageCell: SBUBaseMessageCell { self.threadHStackView.setHStack([ self.threadInfoSpacing, self.threadInfoView - ]), - self.wideSizeStateContainerView, + ]) ]) self.messageContentView .addSubview(self.userNameStackView) - - // NOTE: The fullSizeMessageContainerView is attached as an overlay. - self.fullSizeMessageContainerView.setVStack([]) - self.userNameStackView.addSubview(self.fullSizeMessageContainerView) } open override func setupLayouts() { @@ -271,22 +269,7 @@ open class SBUContentBaseMessageCell: SBUBaseMessageCell { self.userNameStackView .sbu_constraint(equalTo: self.messageContentView, left: 12, right: 12, bottom: 0) .sbu_constraint(equalTo: self.messageContentView, top: 0, priority: .defaultLow) - - self.wideSizeStateContainerView - .sbu_constraint_multiplier(widthAnchor: self.userNameStackView.widthAnchor, widthMultiplier: 1, priority: UILayoutPriority(1000) - ) - - self.fullSizeMessageContainerView.backgroundColor = .clear - self.fullSizeMessageConstraints = [ - self.fullSizeMessageContainerView - .sbu_constraint_v2( - equalTo: self.userNameStackView, top: 0, bottom: 0, centerX: 0, priority: UILayoutPriority(1000) - ), - self.fullSizeMessageContainerView.sbu_constraint_v2( - widthAnchor: self.contentView.widthAnchor, width: 0, priority: UILayoutPriority(1000) - ) - ].flatMap({ $0 }) - + super.setupLayouts() } @@ -317,6 +300,11 @@ open class SBUContentBaseMessageCell: SBUBaseMessageCell { guard let self = self else { return } self.moreEmojiTapHandler?() } + + self.reactionView.errorHandler = { [weak self] error in + guard let self = self else { return } + self.errorHandler?(error) + } } open override func setupStyles() { @@ -370,12 +358,14 @@ open class SBUContentBaseMessageCell: SBUBaseMessageCell { guard let message = self.message else { return } // MARK: Configure reaction view - self.reactionView.configure( + let params = SBUMessageReactionViewParams( maxWidth: SBUConstant.imageSize.width, useReaction: self.useReaction, reactions: message.reactions, - enableEmojiLongPress: self.enableEmojiLongPress + enableEmojiLongPress: self.enableEmojiLongPress, + message: message ) + self.reactionView.configure(configuration: params) // MARK: update UI with message position @@ -416,11 +406,11 @@ open class SBUContentBaseMessageCell: SBUBaseMessageCell { sendingState: message.sendingStatus, receiptState: self.receiptState, position: self.position, - isQuotedReplyMessage: isQuotedReplyMessage || self.containerType.isBiggerWideSize + isQuotedReplyMessage: isQuotedReplyMessage ) self.stateView.removeFromSuperview() self.stateView = SBUMessageStateView( - isQuotedReplyMessage: isQuotedReplyMessage || self.containerType.isBiggerWideSize + isQuotedReplyMessage: isQuotedReplyMessage ) (self.stateView as? SBUMessageStateView)?.configure(with: configuration) } @@ -568,12 +558,11 @@ open class SBUContentBaseMessageCell: SBUBaseMessageCell { case .left: self.userNameStackView.alignment = .leading self.mainContainerVStackView.alignment = .leading - self.wideSizeStateContainerView.alignment = .leading self.messageHStackView.setHStack([ self.mainContainerVStackView.setVStack([ self.mainContainerView, ]), - self.containerType.isDefaultSize ? self.stateView : nil, + self.stateView, self.messageSpacing ]) self.contentVStackView.setVStack([ @@ -596,26 +585,13 @@ open class SBUContentBaseMessageCell: SBUBaseMessageCell { self.threadInfoSpacing, self.threadInfoView ]) - self.wideSizeStateContainerView.setHStack([ - self.wideSizeProfileSpaceView, - self.containerType.isBiggerWideSize ? self.stateView : nil - ]) - - if self.profileView.hasSuperview == true { - self.wideSizeProfileSpaceView.sbu_constraint_multiplier( - widthAnchor: self.profileView.widthAnchor, - widthMultiplier: 1, - priority: .defaultLow - ) - } case .right: self.userNameStackView.alignment = .trailing self.mainContainerVStackView.alignment = .trailing - self.wideSizeStateContainerView.alignment = .trailing self.messageHStackView.setHStack([ self.messageSpacing, - self.containerType.isDefaultSize ? self.stateView : nil, + self.stateView, self.mainContainerVStackView.setVStack([ self.mainContainerView, ]), @@ -631,10 +607,6 @@ open class SBUContentBaseMessageCell: SBUBaseMessageCell { self.threadHStackView.setHStack([ self.threadInfoView ]) - self.wideSizeStateContainerView.setHStack([ - self.wideSizeProfileSpaceView, - self.containerType.isBiggerWideSize ? self.stateView : nil - ]) case .center: break diff --git a/Sources/View/Channel/MessageCell/SBUUserMessageCell.MessageTemplate.swift b/Sources/View/Channel/MessageCell/SBUMessageTemplateCell.MessageTemplateLayer.swift similarity index 55% rename from Sources/View/Channel/MessageCell/SBUUserMessageCell.MessageTemplate.swift rename to Sources/View/Channel/MessageCell/SBUMessageTemplateCell.MessageTemplateLayer.swift index a2acfb8..dc37fab 100644 --- a/Sources/View/Channel/MessageCell/SBUUserMessageCell.MessageTemplate.swift +++ b/Sources/View/Channel/MessageCell/SBUMessageTemplateCell.MessageTemplateLayer.swift @@ -1,5 +1,5 @@ // -// SBUUserMessageCell.MessageTemplate.swift +// SBUMessageTemplateCell.MessageTemplateLayer.swift // SendbirdUIKit // // Created by Damon Park on 2024/02/19. @@ -9,7 +9,7 @@ import UIKit import SendbirdChatSDK -extension SBUUserMessageCell { +extension SBUMessageTemplateCell { class MessageTemplateLayer { static let downloadingHeight: CGFloat = 274.0 @@ -21,7 +21,6 @@ extension SBUUserMessageCell { } var templateContainerView = UIStackView() - var spaceArea = SpaceArea() var templateView: SBUMessageTemplate.Syntax.TemplateView? var messageTemplateRenderer: SBUMessageTemplate.Renderer.RendererType = .inProgress @@ -35,55 +34,35 @@ extension SBUUserMessageCell { get { self.message?.templateImagesRetryStatus ?? .initialized } set { self.message?.templateImagesRetryStatus = newValue } } - - var containerSizeFactory: SBUMessageContainerSizeFactory = .default - } - - class SpaceArea { - var name = UIView() - var contents = UIView() - var time = UIView() - - init() { - self.name.backgroundColor = .clear - self.contents.backgroundColor = .clear - self.time.backgroundColor = .clear - } } } -extension SBUUserMessageCell.MessageTemplateLayer { +extension SBUMessageTemplateCell.MessageTemplateLayer { func renderError() { let renderer = SBUMessageTemplate.Renderer.errorRenderer( - type: .group, - message: self.message + type: .message, + message: self.message, + viewStyle: .init( + backgroundColor: SBUMessageTemplate.Renderer.defaultTheme.viewBackgroundColor.toHexString(), + radius: 16, + margin: .init(top: 0, bottom: 0, left: 50, right: 12) + ) ) - if message?.asUiSettingContainerType != .full { - renderer.layer.cornerRadius = 16 - renderer.layer.borderColor = UIColor.clear.cgColor - renderer.layer.borderWidth = 1 - renderer.clipsToBounds = true - renderer.backgroundColor = SBUMessageTemplate.Renderer.defaultTheme.viewBackgroundColor - } - self.messageTemplateRenderer = .error(renderer: renderer) } func renderDownload() { let renderer = SBUMessageTemplate.Renderer.downloadingRenderer( messageId: message?.messageId, - downloadingHeight: Self.downloadingHeight + downloadingHeight: Self.downloadingHeight, + viewStyle: .init( + backgroundColor: SBUMessageTemplate.Renderer.defaultTheme.viewBackgroundColor.toHexString(), + radius: 16, + margin: .init(top: 0, bottom: 0, left: 50, right: 12) + ) ) - if message?.asUiSettingContainerType != .full { - renderer.layer.cornerRadius = 16 - renderer.layer.borderColor = UIColor.clear.cgColor - renderer.layer.borderWidth = 1 - renderer.clipsToBounds = true - renderer.backgroundColor = SBUMessageTemplate.Renderer.defaultTheme.viewBackgroundColor - } - self.messageTemplateRenderer = .downloading(renderer: renderer) } @@ -97,20 +76,10 @@ extension SBUUserMessageCell.MessageTemplateLayer { func clear() { self.messageTemplateRenderer.clear() - self.containerSizeFactory = .default } } -extension SBUUserMessageCell { - - func updateTemplateSizeFactory() { - self.messageTemplateLayer.containerSizeFactory = SBUMessageContainerSizeFactory( - type: self.message?.asUiSettingContainerType ?? .default, - profileWidth: (self.profileView as? SBUMessageProfileView)?.imageSize, - timpstampWidth: (self.stateView as? SBUMessageStateView)?.timeLabelCustomSize?.width - ) - } - +extension SBUMessageTemplateCell { func setupMessageTemplate() { guard let message = self.message else { return } guard let data = message.asMessageTemplate else { return } @@ -128,10 +97,8 @@ extension SBUUserMessageCell { return } - self.updateTemplateSizeFactory() - let result = SBUMessageTemplate.Coordinator.execute( - type: .group, + type: .message, message: message, payloadJson: payloadJson, imageRetryStatus: self.messageTemplateLayer.imagesRetryStatus @@ -167,17 +134,6 @@ extension SBUUserMessageCell { self.reloadCell() } } - - case .reload(.compositeType): - guard let prev = self.configuration else { - self.messageTemplateLayer.renderDownload() - self.reloadCell() - return - } - - self.isMessyViewHierarchy = true - self.prepareForReuse() - self.configure(with: prev) case .template(let key, let template): self.messageTemplateLayer.templateView = template @@ -186,7 +142,7 @@ extension SBUUserMessageCell { template: template, delegate: self, dataSource: self, - maxWidth: self.messageTemplateLayer.containerSizeFactory.getWidth(), + maxWidth: self.bounds.width, actionHandler: { [weak self] in self?.messageTemplateActionHandler?($0) } ) { self.messageTemplateLayer.messageTemplateRenderer = .loaded(key: key, renderer: renderer) @@ -202,64 +158,20 @@ extension SBUUserMessageCell { func setupMessageTemplateLayouts() { guard let renderer = self.messageTemplateLayer.validRenderer else { return } - switch self.containerType { - case .`default`: - self.messageTextView.removeFromSuperview() - self.messageTemplateLayer.templateContainerView.setVStack([renderer]) - - case .wide: - self.messageTextView.removeFromSuperview() - self.messageTemplateLayer.templateContainerView.setVStack([renderer]) - - case .full: - self.messageTextView.removeFromSuperview() - - self.fullSizeMessageContainerView.setVStack([ - self.messageTemplateLayer.spaceArea.name, - renderer, - self.messageTemplateLayer.spaceArea.time, - ]) - self.messageTemplateLayer.templateContainerView.setVStack([self.messageTemplateLayer.spaceArea.contents]) - self.messageTemplateLayer.spaceArea.name.setHeightConstraints(with: userNameView) - self.messageTemplateLayer.spaceArea.contents.setHeightConstraints(with: renderer) - self.messageTemplateLayer.spaceArea.time.setHeightConstraints(with: wideSizeStateContainerView) - } - - self.isMessyViewHierarchy = true + self.messageTemplateLayer.templateContainerView.setVStack([renderer]) } func updateMessageTemplateLayouts() { - guard let renderer = self.messageTemplateLayer.validRenderer else { - NSLayoutConstraint.deactivate(self.fullSizeMessageConstraints) - return - } - - if self.containerType == .full { - NSLayoutConstraint.activate(self.fullSizeMessageConstraints) - } else { - NSLayoutConstraint.deactivate(self.fullSizeMessageConstraints) - } + guard let renderer = self.messageTemplateLayer.validRenderer else { return } renderer.sbu_constraint( - width: self.messageTemplateLayer.containerSizeFactory.getWidth(), - priority: self.messageTemplateLayer.messageTemplateRenderer.isError ? .defaultLow : UILayoutPriority(rawValue: 1000) + width: self.bounds.width, + priority: self.messageTemplateLayer.messageTemplateRenderer.isError ? .defaultLow : .required ) } - - func setupMesageTemplateStyles() { - guard self.messageTemplateLayer.hasValidRenderer == true else { return } - - self.mainContainerView.layer.cornerRadius = 0.0 - self.mainContainerView.layer.borderWidth = 0.0 - self.mainContainerView.setTransparentBackgroundColor() - } - - func clearMessageTemplateLayouts() { - NSLayoutConstraint.deactivate(self.fullSizeMessageConstraints) - } } -extension SBUUserMessageCell: MessageTemplateRendererDelegate { +extension SBUMessageTemplateCell: MessageTemplateRendererDelegate { func messageTemplateRender(_ renderer: SBUMessageTemplate.Renderer, didFinishLoadingImage imageView: UIImageView) { guard self.messageTemplateLayer.hasValidRenderer == true else { return } self.reloadCell() @@ -275,28 +187,19 @@ extension SBUUserMessageCell: MessageTemplateRendererDelegate { case .carouselRestoreView: guard let carouselView = value as? SBUBaseCarouselView else { return } self.message?.messageTemplateCarouselView = carouselView - default: break + default: + break } } } -extension SBUUserMessageCell: MessageTemplateRendererDataSource { +extension SBUMessageTemplateCell: MessageTemplateRendererDataSource { func messageTemplateRender(_ renderer: SBUMessageTemplate.Renderer, valueFor key: SBUMessageTemplate.Renderer.EventSourceKeys) -> Any? { switch key { case .carouselRestoreView: return self.message?.messageTemplateCarouselView - case .carouselProfileAreaSize: - return self.messageTemplateLayer.containerSizeFactory.getProfileArea() case .templateFactory: return self.messageTemplateLayer.templateView?.identifierFactory } } } - -fileprivate extension UIView { - func setHeightConstraints(with target: UIView) { - self.translatesAutoresizingMaskIntoConstraints = false - self.widthAnchor.constraint(equalToConstant: 0).isActive = true - self.heightAnchor.constraint(equalTo: target.heightAnchor, multiplier: 1).isActive = true - } -} diff --git a/Sources/View/Channel/MessageCell/SBUMessageTemplateCell.swift b/Sources/View/Channel/MessageCell/SBUMessageTemplateCell.swift new file mode 100644 index 0000000..d1f086a --- /dev/null +++ b/Sources/View/Channel/MessageCell/SBUMessageTemplateCell.swift @@ -0,0 +1,226 @@ +// +// SBUMessageTemplateCell.swift +// SendbirdUIKit +// +// Created by Damon Park on 8/22/24. +// + +import UIKit + +/// Cell for rendering the MessageTemplate in the GroupChannel List. +/// - Since: 3.27.2 +open class SBUMessageTemplateCell: SBUBaseMessageCell, SBUSuggestedReplyViewDelegate { + // MARK: - UI Layouts + lazy var layout: SBUMessageTemplateCellLayout = { self.createLayout() }() + + func createLayout() -> SBUMessageTemplateCellLayout { + let layout = SBUMessageTemplateCellLayout() + layout.target = self + return layout + } + + // MARK: - UI Views (Public) + /// profile view property + public lazy var profileView: UIView = { self.createProfileView() }() + /// user name view property + public lazy var userNameView: UIView = { self.createUserNameView() }() + /// state view property + public lazy var stateView: UIView = { self.createStateView() }() + + /// Methods for creating a profile view. Can be overridden. + open func createProfileView() -> SBUMessageProfileView { SBUMessageProfileView() } + /// Methods for creating a user name view. Can be overridden. + open func createUserNameView() -> SBUUserNameView { SBUUserNameView() } + /// Methods for creating a state view. Can be overridden. + open func createStateView() -> SBUMessageStateView { SBUMessageStateView() } + + private(set) var messageTemplateLayer = MessageTemplateLayer() + + /// message tempalte container view + public var messageTemplateContainer: UIView { + messageTemplateLayer.templateContainerView + } + + // MARK: - Suggested Replies + + /// ``SBUSuggestedReplyView`` instance. + /// If you want to override that view, override the ``createSuggestedReplyView()`` constructor function. + public private(set) var suggestedReplyView: SBUSuggestedReplyView? + + /// The boolean value whether the ``suggestedReplyView`` instance should appear or not. The default is `true` + /// - Important: If it's true, ``suggestedReplyView`` never appears even if the ``userMessage`` has quick reply options. + public private(set) var shouldHideSuggestedReplies: Bool = true + + // MARK: - UI Views (Private) + private var renderer: SBUMessageTemplate.Renderer? + + // MARK: - Sendbird Life cycle + /// Configures a cell with ``SBUBaseMessageCellParams`` object. + open override func configure(with configuration: SBUBaseMessageCellParams) { + guard let configuration = configuration as? SBUMessageTemplateCellParams else { return } + + self.shouldHideSuggestedReplies = configuration.shouldHideSuggestedReplies + + super.configure(with: configuration) + + self.configureProfileView() + self.configureUserNameView() + self.configureStateView() + + self.configureMessageTemplateContainer() + self.configureMessageTemplateLayer() + + self.updateSuggestedReplyView(with: configuration.message.suggestedReplies) + + self.setupLayouts() + self.setNeedsLayout() + } + + // MARK: - configure methods for subviews. + + /// Methods to configure profile view + open func configureProfileView() { + guard let profileView = self.profileView as? SBUMessageProfileView else { return } + + let urlString = self.message?.sender?.profileURL ?? "" + profileView.configure(urlString: urlString) + } + + /// Methods to configure userName view + open func configureUserNameView() { + guard let userNameView = self.userNameView as? SBUUserNameView else { return } + + if let sender = self.message?.sender { + let username = SBUUser(user: sender).refinedNickname() + userNameView.configure(username: username) + } else { + userNameView.configure(username: "") + } + } + + /// Methods to configure state view + open func configureStateView() { + guard let stateView = self.stateView as? SBUMessageStateView else { return } + guard let message = self.message else { return } + + let configuration = SBUMessageStateViewParams( + timestamp: message.createdAt, + sendingState: message.sendingStatus, + receiptState: .none, + position: .left, + isQuotedReplyMessage: false + ) + stateView.configure(with: configuration) + } + + // MARK: - message template configuration methods + + // Methods to configure the value of the container property of a message template + public func configureMessageTemplateContainer() { + guard let options = (self.configuration as? SBUMessageTemplateCellParams)?.container.containerOptions else { return } + + self.profileView.alpha = options.profile == false ? 0.0 : 1.0 + self.userNameView.alpha = options.nickname == false ? 0.0 : 1.0 + self.stateView.alpha = options.time == false ? 0.0 : 1.0 + } + + // Methods to configure the message template layer + public func configureMessageTemplateLayer() { + self.messageTemplateLayer.message = self.message + self.setupMessageTemplate() + self.setupMessageTemplateLayouts() + self.updateMessageTemplateLayouts() + } + + // MARK: - tableview cell life cycle methods + + open override func prepareForReuse() { + super.prepareForReuse() + + (self.profileView as? SBUMessageProfileView)?.configure(urlString: ReuseConstant.profileUrl) + (self.userNameView as? SBUUserNameView)?.configure(username: ReuseConstant.userName) + (self.stateView as? SBUMessageStateView)?.configure(with: ReuseConstant.stateParameter) + + self.profileView.alpha = 0.0 + self.userNameView.alpha = 0.0 + self.stateView.alpha = 0.0 + + self.layout.prepareForReuse() + } + + // MARK: - layout cycle methods + + open override func setupViews() { + super.setupViews() + self.layout.configureViews() + } + + open override func setupLayouts() { + super.setupLayouts() + self.layout.configureLayouts() + } + + open override func updateLayouts() { + super.updateLayouts() + } + + open override func setupStyles() { + self.backgroundColor = .clear + + (self.profileView as? SBUMessageProfileView)?.setupStyles() + (self.userNameView as? SBUUserNameView)?.setupStyles() + (self.stateView as? SBUMessageStateView)?.setupStyles() + } + + open override func setupActions() { + super.setupActions() + + self.profileView.addGestureRecognizer(UITapGestureRecognizer( + target: self, + action: #selector(self.onTapUserProfileView(sender:))) + ) + } + + // MARK: - Action + @objc + open func onTapUserProfileView(sender: UITapGestureRecognizer) { + self.userProfileTapHandler?() + } + + // MARK: - Suggested Replies view method + + open func updateSuggestedReplyView(with options: [String]?) { + self.suggestedReplyView = + SBUSuggestedReplyView.updateSuggestedReplyView( + with: options, + message: self.message, + shouldHide: shouldHideSuggestedReplies, + delegate: self + ) + } + + public func suggestedReplyView(_ view: SBUSuggestedReplyView, didSelectOption optionView: SBUSuggestedReplyOptionView) { + self.suggestedReplySelectHandler?(optionView) + + self.suggestedReplyView?.removeFromSuperview() + self.suggestedReplyView = nil + + self.layoutIfNeeded() + } +} + +extension SBUMessageTemplateCell { + struct ReuseConstant { + fileprivate static var profileUrl: String = "" + fileprivate static var userName: String = "" + fileprivate static var stateParameter: SBUMessageStateViewParams { + SBUMessageStateViewParams( + timestamp: 0, + sendingState: .none, + receiptState: .none, + position: .left + ) + } + + } +} diff --git a/Sources/View/Channel/MessageCell/SBUMessageTemplateCellLayout.swift b/Sources/View/Channel/MessageCell/SBUMessageTemplateCellLayout.swift new file mode 100644 index 0000000..fac8505 --- /dev/null +++ b/Sources/View/Channel/MessageCell/SBUMessageTemplateCellLayout.swift @@ -0,0 +1,141 @@ +// +// SBUMessageTemplateCellLayout.swift +// SendbirdUIKit +// +// Created by Damon Park on 8/23/24. +// + +import UIKit + +/// The `SBUViewLayoutConfigurable` protocol defines the layout cycle for a cell. +/// Classes that conform to this protocol are responsible for configuring the views, setting up layouts, updating layouts, and preparing for reuse. +protocol SBUViewLayoutConfigurable: AnyObject { + /// The associated cell type. Classes conforming to this protocol must be tied to a specific cell type. + associatedtype TargetCell: UIView + + /// The target cell instance that this layout is associated with. + var target: TargetCell? { get set } + + /// Configures the views of the target cell. This is typically where initial view setup occurs. + func configureViews() + + /// Sets up the initial layout constraints or frames for the views in the target cell. + func configureLayouts() + + /// Prepares the cell for reuse by resetting any state or views before the cell is reused. + func prepareForReuse() +} + +class SBUMessageTemplateCellLayout: SBUViewLayoutConfigurable { + // -> baseStackView + // + ------------------- + + // | topStackView | + // + ------------------- + + // | contentStackView | + // + ------------------- + + // | bottomStackView | + // + ------------------- + + // | additionalView | + // + ------------------- + + + var baseStackView = SBUStackView(axis: .vertical, alignment: .fill, spacing: 4) + var topStackView = SBUStackView(axis: .horizontal, alignment: .center, spacing: 0) + var contentStackView = SBUStackView(axis: .horizontal, alignment: .center, spacing: 0) + var bottomStackView = SBUStackView(axis: .horizontal, alignment: .center, spacing: 0) + var additionalView = SBUStackView(axis: .vertical, alignment: .center, spacing: 0) + + var isMessyViewHierarchy: Bool = false + + weak var target: SBUMessageTemplateCell? + + var constraints: [NSLayoutConstraint] = [] { + didSet { + oldValue.forEach { $0.isActive = false } + constraints.forEach { $0.isActive = true } + } + } + + func configureViews() { + guard let cell = self.target else { return } + + cell.messageContentView.addSubview(self.baseStackView) + + self.baseStackView.setVStack([ + // top + self.topStackView.setHStack([ + UIView.spacing(width: 12), + cell.profileView, + UIView.spacing(width: 24), + cell.userNameView, + ]), + + // contents + self.contentStackView.setHStack([ + cell.messageTemplateLayer.templateContainerView + ]), + + // bottom + self.bottomStackView.setHStack([ + UIView.spacing(width: 50), + cell.stateView + ]), + + self.additionalView + ]) + } + + func configureLayouts() { + guard let cell = self.target else { return } + + self.baseStackView.sbu_constraint( + equalTo: cell.contentView, + leading: 0, + trailing: 0, + priority: UILayoutPriority(1000) + ) + + self.baseStackView.sbu_constraint( + equalTo: cell.messageContentView, + top: 0, + bottom: 0, + priority: UILayoutPriority(1000) + ) + + self.topStackView.isHidden = self.topStackViewHidden() + self.bottomStackView.isHidden = self.topStackViewHidden() + + // layout for optional feature views + + if cell.messageTemplateLayer.messageTemplateRenderer.isLoaded { + self.additionalView.setVStack([ + cell.suggestedReplyView + ]) + + cell.suggestedReplyView?.sbu_constraint( + equalTo: self.additionalView, + left: 12, + right: 12 + ) + } else { + self.additionalView.setVStack([]) + } + + } + + func prepareForReuse() { + guard isMessyViewHierarchy == true else { return } + + self.configureViews() + self.configureLayouts() + } + + func topStackViewHidden() -> Bool { + guard let cell = self.target else { return false } + return cell.profileView.alpha == 0.0 && cell.userNameView.alpha == 0.0 + } + + func bottomStackViewHidden() -> Bool { + guard let cell = self.target else { return false } + return cell.stateView.alpha == 0.0 + } +} diff --git a/Sources/View/Channel/MessageCell/SBUUserMessageCell.swift b/Sources/View/Channel/MessageCell/SBUUserMessageCell.swift index 37e5bc8..720fb6f 100644 --- a/Sources/View/Channel/MessageCell/SBUUserMessageCell.swift +++ b/Sources/View/Channel/MessageCell/SBUUserMessageCell.swift @@ -10,16 +10,10 @@ import UIKit import SendbirdChatSDK @IBDesignable -open class SBUUserMessageCell: SBUContentBaseMessageCell, SBUUserMessageTextViewDelegate, SBUSuggestedReplyViewDelegate, SBUFormViewDelegate { +open class SBUUserMessageCell: SBUContentBaseMessageCell, SBUUserMessageTextViewDelegate, SBUSuggestedReplyViewDelegate, SBUMessageFormViewDelegate { // MARK: - Public property public lazy var messageTextView: UIView = SBUUserMessageTextView() - public override var message: BaseMessage? { - didSet { - self.messageTemplateLayer.message = message - } - } - public var userMessage: UserMessage? { self.message as? UserMessage } @@ -39,7 +33,7 @@ open class SBUUserMessageCell: SBUContentBaseMessageCell, SBUUserMessageTextView return webView }() - // MARK: - Quick Reply + // MARK: - Suggested Replies /// The boolean value whether the ``suggestedReplyView`` instance should appear or not. The default is `true` /// - Important: If it's true, ``suggestedReplyView`` never appears even if the ``userMessage`` has quick reply options. @@ -53,16 +47,22 @@ open class SBUUserMessageCell: SBUContentBaseMessageCell, SBUUserMessageTextView // MARK: - Form Type Message - /// The boolean value whether the ``formViews`` instance should appear or not. The default is `true` - /// - Important: If it's true, ``formViews`` never appears even if the ``userMessage`` has `forms`. + /// The boolean value whether the ``messageFormView`` instance should appear or not. The default is `true` + /// - Important: If it's true, ``messageFormView`` never appears even if the ``userMessage`` has `forms`. /// - Since: 3.11.0 public private(set) var shouldHideFormTypeMessage: Bool = true /// The array of ``SBUFormView`` instance. /// If you want to override that view, override the ``createFormView()`` constructor function. /// - Since: 3.11.0 + @available(*, deprecated, message: "This method is deprecated in 3.27.0.") public private(set) var formViews: [SBUFormView]? + /// The array of ``SBUMessageFormView`` instance. + /// If you want to override that view, override the ``createMessageFormView()`` constructor function. + /// - Since: 3.27.0 + public private(set) var messageFormView: SBUMessageFormView? + /// This is a user message custom cell factory type /// to support customization via the `custom_view` data in `BaseMessage.extendedMessage`. /// @@ -124,8 +124,6 @@ open class SBUUserMessageCell: SBUContentBaseMessageCell, SBUUserMessageTextView /// - Since: 3.11.0 open var extendedMessagePayloadCustomViewFactory: SBUExtendedMessagePayloadCustomViewFactoryInternal.Type? { nil } - private(set) var messageTemplateLayer = MessageTemplateLayer() - // MARK: - View Lifecycle open override func setupViews() { super.setupViews() @@ -139,7 +137,6 @@ open class SBUUserMessageCell: SBUContentBaseMessageCell, SBUUserMessageTextView // + --------------- + self.mainContainerView.setStack([ - self.messageTemplateLayer.templateContainerView.setVStack([]), self.messageTextView, self.additionContainerView.setStack([ self.reactionView @@ -147,12 +144,6 @@ open class SBUUserMessageCell: SBUContentBaseMessageCell, SBUUserMessageTextView ]) } - open override func setupLayouts() { - super.setupLayouts() - - self.updateMessageTemplateLayouts() - } - open override func setupActions() { super.setupActions() @@ -195,8 +186,6 @@ open class SBUUserMessageCell: SBUContentBaseMessageCell, SBUUserMessageTextView self.additionContainerView.layer.cornerRadius = 16 - self.setupMesageTemplateStyles() - #if SWIFTUI if self.viewConverter.userMessage.entireContent != nil { self.mainContainerView.setTransparentBackgroundColor() @@ -226,10 +215,10 @@ open class SBUUserMessageCell: SBUContentBaseMessageCell, SBUUserMessageTextView self.suggestedReplyView = nil self.updateSuggestedReplyView(with: message.suggestedReplies) - // MARK: Form Views - self.formViews?.forEach({ $0.removeFromSuperview() }) - self.formViews = nil - let hasForms = self.updateFormView(with: message) + // MARK: Message Form View + self.messageFormView?.removeFromSuperview() + self.messageFormView = nil + let hasForms = self.updateMessageFormView(with: message) self.mainContainerView.isHidden = hasForms && configuration.useOnlyFromView // Set up message position of additionContainerView(reactionView) @@ -273,9 +262,6 @@ open class SBUUserMessageCell: SBUContentBaseMessageCell, SBUUserMessageTextView let view = factory.makeCustomView(message: self.message) { factory.configure(with: view, cell: self) } - - // setup message template - self.updateMessageTemplate() } @available(*, deprecated, renamed: "configure(with:)") // 2.2.0 @@ -372,65 +358,77 @@ open class SBUUserMessageCell: SBUContentBaseMessageCell, SBUUserMessageTextView /// - Parameter options: The string array that configured the view list. /// - since: 3.11.0 open func updateSuggestedReplyView(with options: [String]?) { - guard SendbirdUI.config.groupChannel.channel.isSuggestedRepliesEnabled else { return } - guard shouldHideSuggestedReplies == false else { return } + let suggestedReplyView = + SBUSuggestedReplyView.updateSuggestedReplyView( + with: options, + message: self.message, + shouldHide: shouldHideSuggestedReplies, + delegate: self, + factory: createSuggestedReplyView + ) - guard let options = options else { return } - guard let messageId = self.message?.messageId else { return } + guard let suggestedReplyView = suggestedReplyView else { return } - let suggestedReplyView = createSuggestedReplyView() - let configuration = SBUSuggestedReplyViewParams( - messageId: messageId, - replyOptions: options - ) - suggestedReplyView.configure(with: configuration, delegate: self) self.userNameStackView.addArrangedSubview(suggestedReplyView) suggestedReplyView.sbu_constraint(equalTo: self.userNameStackView, leading: 0, trailing: 0) - self.suggestedReplyView = suggestedReplyView self.layoutIfNeeded() } - /// Methods to use when you want to fully customize the design of the ``SBUSuggestedReplyView``. + /// Override this method to return a custom view that inherits from SBUSuggestedReplyView. + /// + /// Method to use when you want to fully customize the design of the ``SBUSuggestedReplyView``. /// Create your own view that inherits from ``SBUSuggestedReplyView`` and return it. /// NOTE: The default view is ``SBUVerticalSuggestedReplyView``, which is a vertically organized option view. /// - Returns: Views that inherit from ``SBUSuggestedReplyView``. /// - since: 3.11.0 open func createSuggestedReplyView() -> SBUSuggestedReplyView { - switch SendbirdUI.config.groupChannel.channel.suggestedRepliesDirection { - case .vertical: return SBUVerticalSuggestedReplyView() - case .horizontal: return SBUHorizontalSuggestedReplyView() - } + SBUSuggestedReplyView.createDefaultSuggestedReplyView() } // MARK: - form view - /// This is function to create and set up the `[SBUFormView]`. - /// - Parameter forms: Form list data. - /// - Parameter answers: Cached form answer datas. + /// This is function to create and set up the `[SBUMessageFormView]`. + /// - Parameter message: base message. /// - Returns: If `true`, succeeds in creating a valid form view /// - since: 3.11.0 - public func updateFormView(with message: BaseMessage?) -> Bool { + @available(*, deprecated, message: "This method is deprecated in 3.27.0.") + public func updateFormView(with message: BaseMessage?) -> Bool { false } + + public func updateMessageFormView(with message: BaseMessage?) -> Bool { guard SendbirdUI.config.groupChannel.channel.isFormTypeMessageEnabled else { return false } guard shouldHideFormTypeMessage == false else { return false } - guard let forms = message?.forms else { return false } - guard let messageId = message?.messageId else { return false } - - let formViews = forms.reduce(into: [SBUFormView]()) { result, form in - let formView = createFormView() - let configuration = SBUFormViewParams(messageId: messageId, form: form) - formView.configure(with: configuration, delegate: self) - result.append(formView) + guard let message = message else { return false } + guard let messageForm = message.messageForm else { return false } + + let messageId = message.messageId + + if messageForm.isValidVersion == false { + let fallbackView = SBUMessageFormFallbackView() + self.mainContainerVStackView.addArrangedSubview(fallbackView) + self.messageFormView = fallbackView + self.layoutIfNeeded() + + return true } + + let messageFormView = createMessageFormView() + let configuration = SBUMessageFormViewParams( + messageId: messageId, + messageForm: messageForm, + isSubmitting: message.isFormSubmitting, + itemValidationStatus: message.formItemValidationStatus + ) + messageFormView.configure(with: configuration, delegate: self) - formViews.forEach { self.mainContainerVStackView.addArrangedSubview($0) } + self.mainContainerVStackView.addArrangedSubview(messageFormView) - self.formViews = formViews + self.messageFormView = messageFormView self.layoutIfNeeded() - return formViews.count > 0 + return true } /// Methods to use when you want to fully customize the design of the ``SBUFormView``. @@ -438,14 +436,19 @@ open class SBUUserMessageCell: SBUContentBaseMessageCell, SBUUserMessageTextView /// NOTE: The default view is ``SBUSimpleFormView``, which is a vertically organized form view. /// - Returns: Views that inherit from ``SBUFormView``. /// - since: 3.11.0 + @available(*, deprecated, message: "This method is deprecated in 3.27.0.") open func createFormView() -> SBUFormView { SBUSimpleFormView() } + + /// Methods to use when you want to fully customize the design of the ``SBUMessageFormView``. + /// Create your own view that inherits from ``SBUMessageFormView`` and return it. + /// NOTE: The default view is ``SBUSimpleMessageFormView``, which is a vertically organized form view. + /// - Returns: Views that inherit from ``SBUMessageFormView``. + /// - since: 3.27.0 + open func createMessageFormView() -> SBUMessageFormView { SBUSimpleMessageFormView() } /// - Since: 3.21.0 - public func updateMessageTemplate() { - self.setupMessageTemplate() - self.setupMessageTemplateLayouts() - self.updateMessageTemplateLayouts() - } + @available(*, deprecated, message: "`updateMessageTemplate` has been deprecated since 3.27.2.") + public func updateMessageTemplate() { } // MARK: - Mention /// As a default, it calls `groupChannelModule(_:didTapMentionUser:)` in ``SBUGroupChannelModuleListDelegate``. @@ -464,9 +467,22 @@ open class SBUUserMessageCell: SBUContentBaseMessageCell, SBUUserMessageTextView self.layoutIfNeeded() } - // MARK: - form view delegate + // MARK: - message form view delegate + + @available(*, deprecated, message: "This method is deprecated in 3.27.0.") + public func formView(_ view: SBUFormView, didSubmit form: SendbirdChatSDK.MessageForm) { } + + public func messageFormView(_ view: SBUMessageFormView, didSubmit form: SendbirdChatSDK.MessageForm) { + if self.message?.isFormSubmitting == true { return } + self.message?.isFormSubmitting = true + self.submitMessageFormHandler?(form, self) + } + + public func messageFormView(_ view: SBUMessageFormView, didUpdateValidationStatus status: [Int64: Bool]) { + self.message?.formItemValidationStatus = status + } - public func formView(_ view: SBUFormView, didSubmit form: SendbirdChatSDK.Form) { - self.submitFormHandler?(form, self) + public func messageFormView(_ view: SBUMessageFormView, didUpdateViewFrame: CGRect) { + self.invalidateIntrinsicContentSize() } } diff --git a/Sources/View/Channel/MessageCell/SuggestedReply/Views/SBUSuggestedReplyView.swift b/Sources/View/Channel/MessageCell/SuggestedReply/Views/SBUSuggestedReplyView.swift index cf87ac6..bc5f4b6 100644 --- a/Sources/View/Channel/MessageCell/SuggestedReply/Views/SBUSuggestedReplyView.swift +++ b/Sources/View/Channel/MessageCell/SuggestedReply/Views/SBUSuggestedReplyView.swift @@ -72,4 +72,46 @@ open class SBUSuggestedReplyView: SBUView, SBUSuggestedReplyOptionViewDelegate { } return optionViews } + + /// This is class method to create and set up the `SBUSuggestedReplyView`. + /// - Parameters: + /// - options: The string array that configured the view list + /// - message: base message + /// - shouldHide: optional value for hiding suggested replies + /// - delegate: delegate for event delivery + /// - factory: factory method closure for generating suggested replies + /// - since: 3.27.2 + open class func updateSuggestedReplyView( + with options: [String]?, + message: BaseMessage?, + shouldHide: Bool, + delegate: SBUSuggestedReplyViewDelegate? = nil, + factory: (() -> SBUSuggestedReplyView?)? = nil + ) -> SBUSuggestedReplyView? { + guard shouldHide == false else { return nil } + guard SendbirdUI.config.groupChannel.channel.isSuggestedRepliesEnabled else { return nil } + + guard let options = options else { return nil } + guard let messageId = message?.messageId else { return nil } + + let suggestedReplyView = factory?() ?? Self.createDefaultSuggestedReplyView() + let configuration = SBUSuggestedReplyViewParams( + messageId: messageId, + replyOptions: options + ) + suggestedReplyView.configure(with: configuration, delegate: delegate) + return suggestedReplyView + } + + /// Class method to use when you want to fully customize the design of the ``SBUSuggestedReplyView``. + /// Create your own view that inherits from ``SBUSuggestedReplyView`` and return it. + /// NOTE: The default view is ``SBUVerticalSuggestedReplyView``, which is a vertically organized option view. + /// - Returns: Views that inherit from ``SBUSuggestedReplyView``. + /// - since: 3.27.2 + open class func createDefaultSuggestedReplyView() -> SBUSuggestedReplyView { + switch SendbirdUI.config.groupChannel.channel.suggestedRepliesDirection { + case .vertical: return SBUVerticalSuggestedReplyView() + case .horizontal: return SBUHorizontalSuggestedReplyView() + } + } } diff --git a/Sources/View/Channel/MessageInput/SBUMentionLimitGuideCell.swift b/Sources/View/Channel/MessageInput/SBUMentionLimitGuideCell.swift index 1f6a7ec..ade0943 100644 --- a/Sources/View/Channel/MessageInput/SBUMentionLimitGuideCell.swift +++ b/Sources/View/Channel/MessageInput/SBUMentionLimitGuideCell.swift @@ -51,6 +51,8 @@ open class SBUMentionLimitGuideCell: SBUTableViewCell { open override func setupStyles() { super.setupStyles() + self.backgroundColor = theme.backgroundColor + self.iconImageView.image = SBUIconSetType.iconInfo.image( with: theme.mentionLimitGuideTextColor, to: SBUIconSetType.Metric.defaultIconSizeMedium diff --git a/Sources/View/Channel/MessageInput/SBUMessageInputView.swift b/Sources/View/Channel/MessageInput/SBUMessageInputView.swift index e412810..526f1a2 100644 --- a/Sources/View/Channel/MessageInput/SBUMessageInputView.swift +++ b/Sources/View/Channel/MessageInput/SBUMessageInputView.swift @@ -737,6 +737,15 @@ open class SBUMessageInputView: SBUView, SBUActionSheetDelegate, UITextViewDeleg self.textView?.layer.borderColor = theme.textFieldBorderColor.cgColor self.textView?.typingAttributes = defaultAttributes + // support rtl layout + if self.currentLayoutDirection == .rightToLeft { + if SBUUtils.isRTLCharacter(with: self.placeholderLabel.text) { + self.placeholderLabel.textAlignment = .right + } else { + self.placeholderLabel.textAlignment = .left + } + } + // addButton let iconAdd = SBUIconSetType.iconAdd .image(with: (self.isFrozen || self.isMuted || self.isDisabledByServer || self.isDisabled) @@ -854,20 +863,12 @@ open class SBUMessageInputView: SBUView, SBUActionSheetDelegate, UITextViewDeleg public func setFrozenModeState(_ isFrozen: Bool) { self.isFrozen = isFrozen - self.textView?.isEditable = !self.isFrozen - self.textView?.isUserInteractionEnabled = !self.isFrozen - self.addButton?.isEnabled = !self.isFrozen - self.voiceMessageButton?.isEnabled = !self.isFrozen - // SwiftUI #if SWIFTUI self.setFrozenModeStateForSwiftUI(isFrozen) #endif - - if self.isFrozen { - self.endTypingMode() - } - self.setupStyles() + + self.updateInputState() } /// Sets frozen mode state. @@ -875,41 +876,40 @@ open class SBUMessageInputView: SBUView, SBUActionSheetDelegate, UITextViewDeleg public func setMutedModeState(_ isMuted: Bool) { self.isMuted = isMuted - self.textView?.isEditable = !self.isMuted - self.textView?.isUserInteractionEnabled = !self.isMuted - self.addButton?.isEnabled = !self.isMuted - self.voiceMessageButton?.isEnabled = !self.isMuted - // SwiftUI #if SWIFTUI self.setMutedModeStateForSwiftUI(isMuted) #endif - - if self.isMuted { - self.endTypingMode() - } - self.setupStyles() + + self.updateInputState() } /// Sets disable chat input value /// - Parameter isDisable: `true` is disable mode, `false` is available mode func setDisableChatInputState(_ isDisabledByServer: Bool) { - if SendbirdUI.config.groupChannel.channel.isSuggestedRepliesEnabled == false { return } if self.isMuted || self.isFrozen { return } - self.isDisabledByServer = isDisabledByServer - - self.textView?.isEditable = !self.isDisabledByServer - self.textView?.isUserInteractionEnabled = !self.isDisabledByServer - self.addButton?.isEnabled = !self.isDisabledByServer - self.voiceMessageButton?.isEnabled = !self.isDisabledByServer - // SwiftUI #if SWIFTUI self.setDisableChatInputStateForSwiftUI(isDisabledByServer) #endif - if self.isDisabledByServer { + self.isDisabledByServer = isDisabledByServer + + self.updateInputState() + } + + /// Methods to update the inputView's input-enabled state by looking at all states + /// - Since: 3.27.0 + func updateInputState() { + let isDisabled = self.isDisabledByServer || self.isMuted || self.isFrozen + + self.textView?.isEditable = !isDisabled + self.textView?.isUserInteractionEnabled = !isDisabled + self.addButton?.isEnabled = !isDisabled + self.voiceMessageButton?.isEnabled = !isDisabled + + if isDisabled { self.endTypingMode() } self.setupStyles() @@ -1126,6 +1126,15 @@ open class SBUMessageInputView: SBUView, SBUActionSheetDelegate, UITextViewDeleg self.layoutIfNeeded() } + // support rtl layout + if self.currentLayoutDirection == .rightToLeft { + if SBUUtils.isRTLCharacter(with: text) { + self.textView?.textAlignment = .right + } else { + self.textView?.textAlignment = .left + } + } + self.delegate?.messageInputView(self, didChangeText: text) } diff --git a/Sources/View/Channel/Reaction/SBUEmojiListViewController.swift b/Sources/View/Channel/Reaction/SBUEmojiListViewController.swift index af1d652..a43db3f 100644 --- a/Sources/View/Channel/Reaction/SBUEmojiListViewController.swift +++ b/Sources/View/Channel/Reaction/SBUEmojiListViewController.swift @@ -18,7 +18,7 @@ open class SBUEmojiListViewController: SBUBaseViewController, UICollectionViewDe public private(set) lazy var collectionView = UICollectionView(frame: self.view.bounds, collectionViewLayout: layout) public let layout: UICollectionViewFlowLayout = SBUCollectionViewFlowLayout() - public let emojiList: [Emoji] = SBUEmojiManager.getAllEmojis() + public let emojiList: [Emoji] public let message: BaseMessage? @SBUThemeWrapper(theme: SBUTheme.componentTheme) @@ -48,6 +48,7 @@ open class SBUEmojiListViewController: SBUBaseViewController, UICollectionViewDe @available(*, unavailable, renamed: "SBUEmojiListViewController.init(message:)") required public init?(coder: NSCoder) { self.message = nil + self.emojiList = SBUEmojiManager.getAllEmojis() super.init(coder: coder) } @@ -55,6 +56,10 @@ open class SBUEmojiListViewController: SBUBaseViewController, UICollectionViewDe /// - Parameter message: BaseMessage required public init(message: BaseMessage) { self.message = message + + // Filter emojis if custom `SBUGlobals.emojiCategoryFilter` is defined. + emojiList = SBUEmojiManager.getAvailableEmojis(message: message) + super.init(nibName: nil, bundle: nil) } @@ -181,16 +186,21 @@ open class SBUEmojiListViewController: SBUBaseViewController, UICollectionViewDe guard let cell = collectionView.dequeueReusableCell( withReuseIdentifier: SBUReactionCollectionViewCell.sbu_className, for: indexPath - ) as? SBUReactionCollectionViewCell else { return .init() } + ) as? SBUReactionCollectionViewCell else { + return .init() + } let emoji = emojiList[indexPath.row] cell.configure(type: .messageMenu, url: emoji.url) - guard let currentUesr = SBUGlobals.currentUser else { return cell } + guard let currentUesr = SBUGlobals.currentUser else { + return cell + } let didSelect = self.message?.reactions .first { $0.key == emoji.key }? .userIds.contains(currentUesr.userId) ?? false cell.isSelected = didSelect + return cell } diff --git a/Sources/View/Channel/Reaction/SBUMessageReactionView.swift b/Sources/View/Channel/Reaction/SBUMessageReactionView.swift index 2846455..6aed640 100644 --- a/Sources/View/Channel/Reaction/SBUMessageReactionView.swift +++ b/Sources/View/Channel/Reaction/SBUMessageReactionView.swift @@ -9,12 +9,37 @@ import UIKit import SendbirdChatSDK +/// A set of properties that are passed onto `SBUMessageReactionView` and its subclasses. +/// - Since: 3.27.0 +public class SBUMessageReactionViewParams { + let maxWidth: CGFloat + let useReaction: Bool + let reactions: [Reaction] + let enableEmojiLongPress: Bool + let message: BaseMessage? + + public init( + maxWidth: CGFloat, + useReaction: Bool, + reactions: [Reaction], + enableEmojiLongPress: Bool, + message: BaseMessage? = nil + ) { + self.maxWidth = maxWidth + self.useReaction = useReaction + self.reactions = message?.reactions ?? reactions + self.enableEmojiLongPress = enableEmojiLongPress + self.message = message + } +} + /// Emoji reaction box open class SBUMessageReactionView: SBUView, UICollectionViewDelegate, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout { public lazy var collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout) public let layout: UICollectionViewFlowLayout = SBUCollectionViewFlowLayout() + public var message: BaseMessage? public var emojiList: [Emoji] = [] public var reactions: [Reaction] = [] public var maxWidth: CGFloat = SBUConstant.messageCellMaxWidth @@ -30,6 +55,7 @@ open class SBUMessageReactionView: SBUView, UICollectionViewDelegate, UICollecti var emojiTapHandler: ((_ emojiKey: String) -> Void)? var moreEmojiTapHandler: (() -> Void)? var emojiLongPressHandler: ((_ emojiKey: String) -> Void)? + var errorHandler: ((_ error: SBError) -> Void)? public private(set) var collectionViewHeightConstraint: NSLayoutConstraint? public private(set) var collectionViewMinWidthContraint: NSLayoutConstraint? @@ -121,27 +147,40 @@ open class SBUMessageReactionView: SBUView, UICollectionViewDelegate, UICollecti reactions: [Reaction], enableEmojiLongPress: Bool ) { - guard useReaction, !reactions.isEmpty else { + let params = SBUMessageReactionViewParams( + maxWidth: maxWidth, + useReaction: useReaction, + reactions: reactions, + enableEmojiLongPress: enableEmojiLongPress, + message: nil + ) + + self.configure(configuration: params) + } + + open func configure(configuration: SBUMessageReactionViewParams) { + guard configuration.useReaction, !configuration.reactions.isEmpty else { self.collectionViewMinWidthContraint?.isActive = false self.isHidden = true return } - + self.collectionViewMinWidthContraint?.isActive = true self.isHidden = false - self.maxWidth = maxWidth - self.reactions = reactions + self.maxWidth = configuration.maxWidth + self.message = configuration.message + self.reactions = configuration.message?.reactions ?? configuration.reactions self.emojiList = SBUEmojiManager.getAllEmojis() - self.enableEmojiLongPress = enableEmojiLongPress - - let hasMoreEmoji = self.reactions.count < emojiList.count + self.enableEmojiLongPress = configuration.enableEmojiLongPress + + let hasMoreEmoji = hasMoreEmoji() let cellSizes = reactions.reduce(0) { $0 + self.getCellSize(count: $1.userIds.count).width } - + var width: CGFloat = cellSizes - + CGFloat(reactions.count - 1) * layout.minimumLineSpacing - + layout.sectionInset.left + layout.sectionInset.right + + CGFloat(reactions.count - 1) * layout.minimumLineSpacing + + layout.sectionInset.left + layout.sectionInset.right if hasMoreEmoji { width += self.getCellSize(count: 0).width + layout.minimumLineSpacing } @@ -178,7 +217,7 @@ open class SBUMessageReactionView: SBUView, UICollectionViewDelegate, UICollecti ) -> Int { guard !reactions.isEmpty else { return 0 } - if self.reactions.count < emojiList.count { + if hasMoreEmoji() { return self.reactions.count + 1 } else { return self.reactions.count @@ -248,13 +287,30 @@ open class SBUMessageReactionView: SBUView, UICollectionViewDelegate, UICollecti _ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath ) { - guard !self.hasMoreEmoji(at: indexPath) else { return } guard !reactions.isEmpty else { return } let reaction = reactions[indexPath.row] self.emojiTapHandler?(reaction.key) } + + public func collectionView(_ collectionView: UICollectionView, shouldSelectItemAt indexPath: IndexPath) -> Bool { + guard let message = self.message else { + return false + } + + // Defense code for when different EmojiCategory had been applied in the past. + // Block reaction add/delete if the selected emoji is no longer supported due to being filtered by EmojiCategory. + let emojiKey = reactions[indexPath.row].key + if !SBUEmojiManager.isEmojiAvailable(emojiKey: emojiKey, message: message) { + let error = SBUError(code: .emojiUnsupported) + SBULog.info(error.code.message) + self.errorHandler?(error.asSBError()) // lets users handle the error. + return false + } + + return true + } open func collectionView( _ collectionView: UICollectionView, @@ -268,4 +324,15 @@ open class SBUMessageReactionView: SBUView, UICollectionViewDelegate, UICollecti let count = reactions[indexPath.row].userIds.count return self.getCellSize(count: count) } + + /// Computes whether there are emojis left to react to a message with. + /// - returns: `true` if there are more emojis, `false` if not. + /// - Since: 3.27.0 + public func hasMoreEmoji() -> Bool { + let availableEmojis = SBUEmojiManager.getAvailableEmojis(message: message) + let reactedEmojiKeys = reactions.map { $0.key } + let unreactedAvailableEmojis = availableEmojis.filter { !reactedEmojiKeys.contains($0.key) } + let hasMoreEmoji = !unreactedAvailableEmojis.isEmpty + return hasMoreEmoji + } } diff --git a/Sources/View/Channel/Reaction/SBUParentMessageInfoReactionView.swift b/Sources/View/Channel/Reaction/SBUParentMessageInfoReactionView.swift index 8fe8d54..75ea51a 100644 --- a/Sources/View/Channel/Reaction/SBUParentMessageInfoReactionView.swift +++ b/Sources/View/Channel/Reaction/SBUParentMessageInfoReactionView.swift @@ -21,21 +21,34 @@ open class SBUParentMessageInfoReactionView: SBUMessageReactionView { reactions: [Reaction], enableEmojiLongPress: Bool ) { - guard useReaction else { + let params = SBUMessageReactionViewParams( + maxWidth: maxWidth, + useReaction: useReaction, + reactions: reactions, + enableEmojiLongPress: enableEmojiLongPress, + message: nil + ) + + self.configure(configuration: params) + } + + open override func configure(configuration: SBUMessageReactionViewParams) { + guard configuration.useReaction else { self.collectionViewMinWidthContraint?.isActive = false self.isHidden = true return } self.collectionViewMinWidthContraint?.isActive = true - self.maxWidth = maxWidth - self.reactions = reactions + self.maxWidth = configuration.maxWidth + self.message = configuration.message + self.reactions = configuration.message?.reactions ?? configuration.reactions self.emojiList = SBUEmojiManager.getAllEmojis() - self.enableEmojiLongPress = enableEmojiLongPress + self.enableEmojiLongPress = configuration.enableEmojiLongPress self.sameCellWidth = true - let hasMoreEmoji = self.reactions.count < emojiList.count + let hasMoreEmoji = self.hasMoreEmoji() let cellSizes = reactions.reduce(0) { $0 + self.getCellSize(count: $1.userIds.count).width } @@ -62,8 +75,9 @@ open class SBUParentMessageInfoReactionView: SBUMessageReactionView { open override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { guard !reactions.isEmpty else { return 1 } - - if self.reactions.count < emojiList.count { + + let hasMoreEmoji = self.hasMoreEmoji() + if hasMoreEmoji { return self.reactions.count + 1 } else { return self.reactions.count diff --git a/Sources/View/Channel/SBUBaseChannelViewController.swift b/Sources/View/Channel/SBUBaseChannelViewController.swift index 44346c8..613b301 100644 --- a/Sources/View/Channel/SBUBaseChannelViewController.swift +++ b/Sources/View/Channel/SBUBaseChannelViewController.swift @@ -61,7 +61,7 @@ open class SBUBaseChannelViewController: SBUBaseViewController, SBUBaseChannelVi var isTransformedList: Bool = true - var isDisableChatInputState: Bool = false + var isChatInputDisabled: Bool = false // MARK: - Lifecycle @available(*, unavailable) @@ -682,8 +682,10 @@ open class SBUBaseChannelViewController: SBUBaseViewController, SBUBaseChannelVi } } - self.isDisableChatInputState = messages.first?.asExtendedMessagePayload?.getDisabledChatInputState(hasNext: self.baseViewModel?.hasNext()) ?? false - + self.isChatInputDisabled = messages.getChatInputDisableState( + hasNext: self.baseViewModel?.hasNext() + ) + guard needsToReload else { return } // Verify that the UIViewController is currently visible on the screen diff --git a/Sources/View/Channel/SBUGroupChannelViewController.swift b/Sources/View/Channel/SBUGroupChannelViewController.swift index 4e2ce59..2298139 100644 --- a/Sources/View/Channel/SBUGroupChannelViewController.swift +++ b/Sources/View/Channel/SBUGroupChannelViewController.swift @@ -48,9 +48,9 @@ open class SBUGroupChannelViewController: SBUBaseChannelViewController, SBUGroup public private(set) var newMessagesCount: Int = 0 - override var isDisableChatInputState: Bool { + override var isChatInputDisabled: Bool { didSet { - (self.baseInputComponent?.messageInputView as? SBUMessageInputView)?.setDisableChatInputState(isDisableChatInputState) + (self.baseInputComponent?.messageInputView as? SBUMessageInputView)?.setDisableChatInputState(isChatInputDisabled) } } @@ -997,14 +997,24 @@ open class SBUGroupChannelViewController: SBUBaseChannelViewController, SBUGroup open func groupChannelModule(_ listComponent: SBUGroupChannelModule.List, didSelect suggestedReplyOptionView: SBUSuggestedReplyOptionView) { guard let text = suggestedReplyOptionView.text else { return } - self.viewModel?.sendUserMessage(text: text) + + let messageParams = UserMessageCreateParams(message: text) + SBUGlobalCustomParams.userMessageParamsSendBuilder?(messageParams) + self.viewModel?.sendUserMessage(messageParams: messageParams) } + @available(*, deprecated, message: "This method is deprecated in 3.27.0.") open func groupChannelModule(_ listComponent: SBUGroupChannelModule.List, didSubmit form: SendbirdChatSDK.Form, messageCell: SBUBaseMessageCell) { guard let message = messageCell.message else { return } self.viewModel?.submitForm(message: message, form: form) } + + open func groupChannelModule(_ listComponent: SBUGroupChannelModule.List, didSubmitMessageForm form: SendbirdChatSDK.MessageForm, messageCell: SBUBaseMessageCell) { + guard let message = messageCell.message else { return } + + self.viewModel?.submitMessageForm(message: message) + } open func groupChannelModule(_ listComponent: SBUGroupChannelModule.List, didUpdate feedbackAnswer: SBUFeedbackAnswer, messageCell: SBUBaseMessageCell) { diff --git a/Sources/View/Common/Menu/SBUMenuSheetViewController.swift b/Sources/View/Common/Menu/SBUMenuSheetViewController.swift index 21d7323..d94cca5 100644 --- a/Sources/View/Common/Menu/SBUMenuSheetViewController.swift +++ b/Sources/View/Common/Menu/SBUMenuSheetViewController.swift @@ -24,7 +24,7 @@ open class SBUMenuSheetViewController: SBUBaseViewController, UITableViewDelegat public private(set) var tableView = UITableView() public let message: BaseMessage? public let items: [SBUMenuItem] - public let emojiList: [Emoji] = SBUEmojiManager.getAllEmojis() + public let emojiList: [Emoji] public private(set) var useReaction: Bool public let maxEmojiOneLine = 6 @@ -43,6 +43,7 @@ open class SBUMenuSheetViewController: SBUBaseViewController, UITableViewDelegat self.message = nil self.items = [] self.useReaction = false + self.emojiList = SBUEmojiManager.getAllEmojis() super.init(coder: coder) } @@ -52,6 +53,10 @@ open class SBUMenuSheetViewController: SBUBaseViewController, UITableViewDelegat self.message = message self.items = items self.useReaction = useReaction + + // Filter emojis if custom `SBUGlobals.emojiCategoryFilter` is defined. + emojiList = SBUEmojiManager.getAvailableEmojis(message: message) + super.init(nibName: nil, bundle: nil) } diff --git a/Sources/View/Life cycles/SBUTextView.swift b/Sources/View/Life cycles/SBUTextView.swift new file mode 100644 index 0000000..af46023 --- /dev/null +++ b/Sources/View/Life cycles/SBUTextView.swift @@ -0,0 +1,135 @@ +// +// SBUTextView.swift +// SendbirdChatLocalCachingTests +// +// Created by Damon Park on 7/2/24. +// + +import UIKit + +/// A TextView with a placeholder. +/// - Since: 3.27.0 +open class SBUTextView: UITextView, SBUViewLifeCycle { + /// placeholder label view + public let placeholderLabel: UILabel = UILabel() + /// vertical stackview containing a placeholder label + public let palceholderContainer = SBUStackView(axis: .vertical, alignment: .top, spacing: 0) + + /// placeholder string + open var placeholder: String? { + didSet { + self.placeholderLabel.text = placeholder + self.placeholderLabel.numberOfLines = 0 + } + } + + /// placeholder color + open var placeholderColor: UIColor = .lightGray { + didSet { self.placeholderLabel.textColor = placeholderColor } + } + + /// placeholder font + open override var font: UIFont? { + didSet { self.placeholderLabel.font = font } + } + + open override var text: String! { + didSet { self.textDidChange() } + } + + open override var attributedText: NSAttributedString! { + didSet { self.textDidChange() } + } + + open override var textAlignment: NSTextAlignment { + didSet { self.placeholderLabel.textAlignment = textAlignment } + } + + open override func layoutSubviews() { + super.layoutSubviews() + + self.updateLayouts() + self.updateStyles() + } + + public override init(frame: CGRect, textContainer: NSTextContainer?) { + super.init(frame: frame, textContainer: textContainer) + + self.setupViews() + self.setupLayouts() + self.setupActions() + } + + public required init?(coder: NSCoder) { + super.init(coder: coder) + + self.setupViews() + self.setupLayouts() + self.setupActions() + } + + deinit { + NotificationCenter.default.removeObserver( + self, + name: UITextView.textDidChangeNotification, + object: nil + ) + } + + open func setupViews() { + NotificationCenter.default.addObserver( + self, + selector: #selector(textDidChange), + name: UITextView.textDidChangeNotification, + object: nil + ) + + self.palceholderContainer.setVStack([self.placeholderLabel, UIView()]) + self.addSubview(self.palceholderContainer) + self.sendSubviewToBack(self.palceholderContainer) + + self.palceholderContainer.isUserInteractionEnabled = false + self.placeholderLabel.isUserInteractionEnabled = false + + self.textDidChange() + } + + open func setupLayouts() { + // Set Auto Layout constraints + self.palceholderContainer.sbu_constraint( + equalTo: self, + left: textContainerInset.left + 5, + right: textContainerInset.right + 5, + top: textContainerInset.top, + bottom: textContainerInset.bottom, + priority: .required + ) + + let widthDiff = textContainerInset.left + textContainerInset.right + 10 + let heightDiff = textContainerInset.top + textContainerInset.bottom + + self.palceholderContainer + .sbu_constraint(widthAnchor: self.widthAnchor, width: -widthDiff, priority: .defaultHigh) + .sbu_constraint(heightAnchor: self.heightAnchor, height: -heightDiff, priority: UILayoutPriority(500)) + } + + open func setupStyles() { + self.placeholderLabel.textColor = self.placeholderColor + self.placeholderLabel.font = self.font + self.placeholderLabel.textAlignment = self.textAlignment + } + + open func updateLayouts() { } + + open func updateStyles() { } + + open func setupActions() { } + + /// Methods called when text changes + @objc + open func textDidChange() { + // NOTE: (AC-3275) + // Use alpha instead of hidden to prevent placelabel height from changing + self.placeholderLabel.alpha = self.text.isEmpty ? 1.0 : 0.0 + } +} diff --git a/Sources/View/MessageThread/SBUParentMessageInfoView.swift b/Sources/View/MessageThread/SBUParentMessageInfoView.swift index 0119c19..2d529b7 100644 --- a/Sources/View/MessageThread/SBUParentMessageInfoView.swift +++ b/Sources/View/MessageThread/SBUParentMessageInfoView.swift @@ -151,6 +151,8 @@ open class SBUParentMessageInfoView: SBUView, SBUUserMessageTextViewDelegate { /// The handler that set the logic to be called when a mention is tapped. public var mentionTapHandler: ((_ user: SBUUser) -> Void)? + var errorHandler: ((_ error: SBError) -> Void)? + // MARK: - LifeCycle @available(*, unavailable, renamed: "SBUParentMessageInfoView(frame:)") required convenience public init?(coder: NSCoder) { @@ -597,6 +599,15 @@ open class SBUParentMessageInfoView: SBUView, SBUUserMessageTextViewDelegate { // MARK: Configure reaction view let isReactionEnabled = self.isReactionAvailable && self.enablesReaction + let params = SBUMessageReactionViewParams( + maxWidth: SBUConstant.imageSize.width, + useReaction: isReactionEnabled, + reactions: message.reactions, + enableEmojiLongPress: enableEmojiLongPress, + message: message + ) + self.reactionView.configure(configuration: params) + var didApplyReactionViewViewConverter = false #if SWIFTUI didApplyReactionViewViewConverter = self.applyViewConverter(.reactionView) @@ -610,7 +621,6 @@ open class SBUParentMessageInfoView: SBUView, SBUUserMessageTextViewDelegate { ) } - #if SWIFTUI self.applyViewConverter(.replyLabel) #endif @@ -676,6 +686,11 @@ open class SBUParentMessageInfoView: SBUView, SBUUserMessageTextViewDelegate { guard let self = self else { return } self.moreEmojiTapHandler?() } + + self.reactionView.errorHandler = { [weak self] error in + guard let self = self else { return } + self.errorHandler?(error) + } } /// Calls the `userProfileTapHandler()` when the user profile is tapped. diff --git a/Sources/ViewModel/Channel/SBUBaseChannelViewModel.swift b/Sources/ViewModel/Channel/SBUBaseChannelViewModel.swift index 3076050..3e96835 100644 --- a/Sources/ViewModel/Channel/SBUBaseChannelViewModel.swift +++ b/Sources/ViewModel/Channel/SBUBaseChannelViewModel.swift @@ -663,6 +663,7 @@ open class SBUBaseChannelViewModel: NSObject { SBULog.info("First : \(String(describing: messages?.first)), Last : \(String(describing: messages?.last))") var needMarkAsRead = false + let myLastRead = (channel as? GroupChannel)?.myLastRead messages?.forEach { message in if let index = SBUUtils.findIndex(of: message, in: self.messageList) { @@ -693,7 +694,9 @@ open class SBUBaseChannelViewModel: NSObject { requestId: message.requestId ) - needMarkAsRead = true + if let myLastRead, myLastRead < message.createdAt { + needMarkAsRead = true + } } else if message.sendingStatus == .failed || message.sendingStatus == .pending { @@ -714,7 +717,7 @@ open class SBUBaseChannelViewModel: NSObject { let channel = self.channel as? GroupChannel, !self.isThreadMessageMode, SendbirdChat.getConnectState() == .open { - channel.markAsRead { _ in + channel.markAsRead { error in sortAllMessageListBlock() } } else { diff --git a/Sources/ViewModel/Channel/SBUFeedNotificationChannelViewModel.swift b/Sources/ViewModel/Channel/SBUFeedNotificationChannelViewModel.swift index 15f6eb3..88bae3f 100644 --- a/Sources/ViewModel/Channel/SBUFeedNotificationChannelViewModel.swift +++ b/Sources/ViewModel/Channel/SBUFeedNotificationChannelViewModel.swift @@ -688,7 +688,7 @@ class SBUFeedNotificationChannelViewModel: NSObject { repeats: false ) { [weak self] _ in guard let self = self else { return } - _ = self.channel?.markAsViewed(messages: messages) + _ = self.channel?.logViewed(messages: messages) } } diff --git a/Sources/ViewModel/Channel/SBUGroupChannelViewModel.swift b/Sources/ViewModel/Channel/SBUGroupChannelViewModel.swift index b4235aa..abeafa7 100644 --- a/Sources/ViewModel/Channel/SBUGroupChannelViewModel.swift +++ b/Sources/ViewModel/Channel/SBUGroupChannelViewModel.swift @@ -79,6 +79,16 @@ open class SBUGroupChannelViewModel: SBUBaseChannelViewModel { /// - Since: 3.3.5 var displaysLocalCachedListFirst: Bool = false + /// Variable to cache the latest stream message ( reset to nil after use) + /// - Since: 3.26.0 + weak var lastestStreamingMessage: BaseMessage? + + /// Computed property to get the most latest succeeded message based on the parameter settings in the message collection. + /// - Since: 3.26.0 + var latestSucceededMessage: BaseMessage? { + self.messageListParams.reverse ? self.messageCollection?.succeededMessages.first : self.messageCollection?.succeededMessages.last + } + // MARK: - LifeCycle required public init( channel: BaseChannel? = nil, @@ -695,9 +705,13 @@ extension SBUGroupChannelViewModel: MessageCollectionDelegate { updatedMessages messages: [BaseMessage] ) { // pending -> failed, pending -> succeded, failed -> Pending + + let hasAnyBots = channel.hasBot || channel.hasAIBot + let latestMessage = self.lastestStreamingMessage ?? self.latestSucceededMessage // NOTE: stream message for gen-ai bot. - if let streamMessage = messages.hasStreamMessageOnly(with: self.messageCollection?.succeededMessages.first) { + if hasAnyBots == true, let streamMessage = messages.hasStreamMessageOnly(with: latestMessage) { + self.lastestStreamingMessage = streamMessage DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { self.delegate?.groupChannelViewModel( self, @@ -715,6 +729,7 @@ extension SBUGroupChannelViewModel: MessageCollectionDelegate { messages: [streamMessage], needReload: false ) + self.lastestStreamingMessage = nil } return } @@ -812,6 +827,7 @@ extension SBUGroupChannelViewModel: MessageCollectionDelegate { /// - message: `BaseMessage` object to submit form. /// - answer: `SendbirdChatSDK.Form` object. /// - Since: 3.16.0 + @available(*, deprecated, message: "This method is deprecated in 3.27.0.") public func submitForm(message: BaseMessage, form: SendbirdChatSDK.Form) { SBULog.info("[Request] Submit Form") message.submitForm(form: form) { error in @@ -823,6 +839,23 @@ extension SBUGroupChannelViewModel: MessageCollectionDelegate { } } + // MARK: - Submit Message Form. + /// This function is used to submit form data. + /// - Parameters: + /// - message: `BaseMessage` object to submit form. + /// - Since: 3.27.0 + public func submitMessageForm(message: BaseMessage) { + SBULog.info("[Request] Submit Message Form") + message.submitMessageForm { error in + if let error = error { + message.isFormSubmitting = false + SBULog.error("[Request] Submit Message Form - error: \(error.localizedDescription)") + self.delegate?.didReceiveError(error) + return + } + } + } + // MARK: - Request feedback. /// This function is used to submit feedback data. @@ -902,7 +935,7 @@ extension SBUGroupChannelViewModel: MessageCollectionDelegate { return } SBUGroupChannelViewModel.nowLoadingTemplate = true - SBUMessageTemplateManager.loadTemplateList(type: .group) { success in + SBUMessageTemplateManager.loadTemplateList(type: .message) { success in SBULog.info("[Request] load missing templates - success: \(success)") SBUGroupChannelViewModel.nowLoadingTemplate = false completionHandler(success) @@ -913,7 +946,7 @@ extension SBUGroupChannelViewModel: MessageCollectionDelegate { data: [String: String], completionHandler: @escaping (Bool) -> Void ) { - SBUMessageTemplateManager.loadTemplateImages(type: .group, cacheData: data) { success in + SBUMessageTemplateManager.loadTemplateImages(type: .message, cacheData: data) { success in SBULog.info("[Request] load missing templates images - success: \(success)") completionHandler(success) }