diff --git a/enzevalos_iphone.xcodeproj/project.pbxproj b/enzevalos_iphone.xcodeproj/project.pbxproj index 4493742a874316648e46b389088a291aa4646a88..e7a7314a35eaacc0ea0e6a3018bb5f205ddafd4b 100644 --- a/enzevalos_iphone.xcodeproj/project.pbxproj +++ b/enzevalos_iphone.xcodeproj/project.pbxproj @@ -233,6 +233,45 @@ A1EB05881D956879008659C1 /* ContactHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1EB05871D956879008659C1 /* ContactHandler.swift */; }; A1EB05A41D956E32008659C1 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = A1EB05A31D956E32008659C1 /* Assets.xcassets */; }; A5E303D824110F6400310264 /* smime-helpers.c in Sources */ = {isa = PBXBuildFile; fileRef = A5E303D724110F6400310264 /* smime-helpers.c */; }; + AD39D6B5264BF316001EC5C2 /* ChatCategoryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD39D683264BF315001EC5C2 /* ChatCategoryView.swift */; }; + AD39D6B6264BF316001EC5C2 /* ChatCategoryModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD39D684264BF315001EC5C2 /* ChatCategoryModel.swift */; }; + AD39D6B7264BF316001EC5C2 /* ChatCategory.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD39D685264BF315001EC5C2 /* ChatCategory.swift */; }; + AD39D6B8264BF316001EC5C2 /* Chat_PeopleRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD39D687264BF315001EC5C2 /* Chat_PeopleRow.swift */; }; + AD39D6B9264BF316001EC5C2 /* Bubble.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD39D689264BF315001EC5C2 /* Bubble.swift */; }; + AD39D6BA264BF316001EC5C2 /* Readme.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD39D68A264BF315001EC5C2 /* Readme.swift */; }; + AD39D6BB264BF316001EC5C2 /* ChatView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD39D68B264BF315001EC5C2 /* ChatView.swift */; }; + AD39D6BC264BF316001EC5C2 /* SampleCategoryV2.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD39D68C264BF315001EC5C2 /* SampleCategoryV2.swift */; }; + AD39D6BD264BF316001EC5C2 /* UnsortedCategory.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD39D68D264BF315001EC5C2 /* UnsortedCategory.swift */; }; + AD39D6BE264BF316001EC5C2 /* Category.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD39D68E264BF315001EC5C2 /* Category.swift */; }; + AD39D6BF264BF316001EC5C2 /* FilesCategoryModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD39D690264BF315001EC5C2 /* FilesCategoryModel.swift */; }; + AD39D6C0264BF316001EC5C2 /* FilesCategory.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD39D691264BF315001EC5C2 /* FilesCategory.swift */; }; + AD39D6C1264BF316001EC5C2 /* FilesPreviewer.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD39D693264BF315001EC5C2 /* FilesPreviewer.swift */; }; + AD39D6C2264BF316001EC5C2 /* FilesCategoryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD39D694264BF315001EC5C2 /* FilesCategoryView.swift */; }; + AD39D6C3264BF316001EC5C2 /* CategoryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD39D697264BF315001EC5C2 /* CategoryView.swift */; }; + AD39D6C4264BF316001EC5C2 /* TypePicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD39D699264BF315001EC5C2 /* TypePicker.swift */; }; + AD39D6C5264BF316001EC5C2 /* UnreadCountIcon.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD39D69A264BF315001EC5C2 /* UnreadCountIcon.swift */; }; + AD39D6C6264BF316001EC5C2 /* MailOptionsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD39D69B264BF315001EC5C2 /* MailOptionsView.swift */; }; + AD39D6C7264BF316001EC5C2 /* newMailRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD39D69C264BF315001EC5C2 /* newMailRow.swift */; }; + AD39D6C8264BF316001EC5C2 /* CategoryOverViewBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD39D69D264BF315001EC5C2 /* CategoryOverViewBar.swift */; }; + AD39D6C9264BF316001EC5C2 /* Searchview_2.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD39D69E264BF315001EC5C2 /* Searchview_2.swift */; }; + AD39D6CA264BF316001EC5C2 /* CategoryViewInner.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD39D69F264BF315001EC5C2 /* CategoryViewInner.swift */; }; + AD39D6CB264BF316001EC5C2 /* ViewModelProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD39D6A1264BF315001EC5C2 /* ViewModelProvider.swift */; }; + AD39D6CC264BF316001EC5C2 /* CategoryViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD39D6A2264BF315001EC5C2 /* CategoryViewModel.swift */; }; + AD39D6CD264BF316001EC5C2 /* HomeModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD39D6A3264BF315001EC5C2 /* HomeModel.swift */; }; + AD39D6CE264BF316001EC5C2 /* ReasonedObservers.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD39D6A5264BF315001EC5C2 /* ReasonedObservers.swift */; }; + AD39D6CF264BF316001EC5C2 /* CustomObservers.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD39D6A6264BF315001EC5C2 /* CustomObservers.swift */; }; + AD39D6D0264BF316001EC5C2 /* SearchFilters.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD39D6A8264BF315001EC5C2 /* SearchFilters.swift */; }; + AD39D6D1264BF316001EC5C2 /* Definitions.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD39D6A9264BF315001EC5C2 /* Definitions.swift */; }; + AD39D6D2264BF316001EC5C2 /* SearchFilterCollections.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD39D6AA264BF315001EC5C2 /* SearchFilterCollections.swift */; }; + AD39D6D3264BF316001EC5C2 /* PersistentDataController.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD39D6AB264BF315001EC5C2 /* PersistentDataController.swift */; }; + AD39D6D4264BF316001EC5C2 /* Home.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD39D6AC264BF315001EC5C2 /* Home.swift */; }; + AD39D6D5264BF316001EC5C2 /* README.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD39D6AD264BF315001EC5C2 /* README.swift */; }; + AD39D6D6264BF316001EC5C2 /* StatefulPreviewWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD39D6AF264BF315001EC5C2 /* StatefulPreviewWrapper.swift */; }; + AD39D6D7264BF316001EC5C2 /* DragResizableCard.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD39D6B0264BF315001EC5C2 /* DragResizableCard.swift */; }; + AD39D6D8264BF316001EC5C2 /* ToggleableTag.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD39D6B1264BF315001EC5C2 /* ToggleableTag.swift */; }; + AD39D6D9264BF316001EC5C2 /* Card.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD39D6B2264BF315001EC5C2 /* Card.swift */; }; + AD39D6DA264BF316001EC5C2 /* Wrap.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD39D6B3264BF315001EC5C2 /* Wrap.swift */; }; + AD39D6DB264BF316001EC5C2 /* Blur.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD39D6B4264BF315001EC5C2 /* Blur.swift */; }; AD97DFBE241F97A300C35B95 /* OnboardingIntroInfoSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD97DFBD241F97A300C35B95 /* OnboardingIntroInfoSection.swift */; }; E65CF5BC56030A9EC9275E90 /* Pods_enzevalos_iphoneTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DD408000605287A0BBEFB8CE /* Pods_enzevalos_iphoneTests.framework */; }; F14239C11F30A99C00998A83 /* QRCodeGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = F14239C01F30A99C00998A83 /* QRCodeGenerator.swift */; }; @@ -537,7 +576,7 @@ 47C112C92531E9B000621A07 /* AttachmentRecord.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttachmentRecord.swift; sourceTree = "<group>"; }; 47C22280218AFD6300BD2C2B /* AutocryptTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutocryptTest.swift; sourceTree = "<group>"; }; 47C22282218B02C700BD2C2B /* autocryptSimpleExample1.eml */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = autocryptSimpleExample1.eml; sourceTree = "<group>"; }; - 47C3490125489F52008D290C /* MailRowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = MailRowView.swift; path = enzevalos_iphone/SwiftUI/Inbox/MailRowView.swift; sourceTree = SOURCE_ROOT; }; + 47C3490125489F52008D290C /* MailRowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = MailRowView.swift; path = enzevalos_iphone/SwiftUI/Inbox/simple/MailRowView.swift; sourceTree = SOURCE_ROOT; }; 47C8224324379EAE005BCE73 /* AttachmentsViewMain.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AttachmentsViewMain.swift; sourceTree = "<group>"; }; 47C8224924379EAE005BCE73 /* DialogView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DialogView.swift; sourceTree = "<group>"; }; 47C8224A24379EAE005BCE73 /* FloatingActionButton.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FloatingActionButton.swift; sourceTree = "<group>"; }; @@ -624,6 +663,45 @@ A1EB05A31D956E32008659C1 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; }; A5E303D624110F6400310264 /* smime-helpers.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "smime-helpers.h"; sourceTree = "<group>"; }; A5E303D724110F6400310264 /* smime-helpers.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = "smime-helpers.c"; sourceTree = "<group>"; }; + AD39D683264BF315001EC5C2 /* ChatCategoryView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatCategoryView.swift; sourceTree = "<group>"; }; + AD39D684264BF315001EC5C2 /* ChatCategoryModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatCategoryModel.swift; sourceTree = "<group>"; }; + AD39D685264BF315001EC5C2 /* ChatCategory.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatCategory.swift; sourceTree = "<group>"; }; + AD39D687264BF315001EC5C2 /* Chat_PeopleRow.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Chat_PeopleRow.swift; sourceTree = "<group>"; }; + AD39D689264BF315001EC5C2 /* Bubble.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Bubble.swift; sourceTree = "<group>"; }; + AD39D68A264BF315001EC5C2 /* Readme.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Readme.swift; sourceTree = "<group>"; }; + AD39D68B264BF315001EC5C2 /* ChatView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatView.swift; sourceTree = "<group>"; }; + AD39D68C264BF315001EC5C2 /* SampleCategoryV2.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SampleCategoryV2.swift; sourceTree = "<group>"; }; + AD39D68D264BF315001EC5C2 /* UnsortedCategory.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UnsortedCategory.swift; sourceTree = "<group>"; }; + AD39D68E264BF315001EC5C2 /* Category.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Category.swift; sourceTree = "<group>"; }; + AD39D690264BF315001EC5C2 /* FilesCategoryModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FilesCategoryModel.swift; sourceTree = "<group>"; }; + AD39D691264BF315001EC5C2 /* FilesCategory.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FilesCategory.swift; sourceTree = "<group>"; }; + AD39D693264BF315001EC5C2 /* FilesPreviewer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FilesPreviewer.swift; sourceTree = "<group>"; }; + AD39D694264BF315001EC5C2 /* FilesCategoryView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FilesCategoryView.swift; sourceTree = "<group>"; }; + AD39D697264BF315001EC5C2 /* CategoryView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CategoryView.swift; sourceTree = "<group>"; }; + AD39D699264BF315001EC5C2 /* TypePicker.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TypePicker.swift; sourceTree = "<group>"; }; + AD39D69A264BF315001EC5C2 /* UnreadCountIcon.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UnreadCountIcon.swift; sourceTree = "<group>"; }; + AD39D69B264BF315001EC5C2 /* MailOptionsView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MailOptionsView.swift; sourceTree = "<group>"; }; + AD39D69C264BF315001EC5C2 /* newMailRow.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = newMailRow.swift; sourceTree = "<group>"; }; + AD39D69D264BF315001EC5C2 /* CategoryOverViewBar.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CategoryOverViewBar.swift; sourceTree = "<group>"; }; + AD39D69E264BF315001EC5C2 /* Searchview_2.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Searchview_2.swift; sourceTree = "<group>"; }; + AD39D69F264BF315001EC5C2 /* CategoryViewInner.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CategoryViewInner.swift; sourceTree = "<group>"; }; + AD39D6A1264BF315001EC5C2 /* ViewModelProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ViewModelProvider.swift; sourceTree = "<group>"; }; + AD39D6A2264BF315001EC5C2 /* CategoryViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CategoryViewModel.swift; sourceTree = "<group>"; }; + AD39D6A3264BF315001EC5C2 /* HomeModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HomeModel.swift; sourceTree = "<group>"; }; + AD39D6A5264BF315001EC5C2 /* ReasonedObservers.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReasonedObservers.swift; sourceTree = "<group>"; }; + AD39D6A6264BF315001EC5C2 /* CustomObservers.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CustomObservers.swift; sourceTree = "<group>"; }; + AD39D6A8264BF315001EC5C2 /* SearchFilters.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SearchFilters.swift; sourceTree = "<group>"; }; + AD39D6A9264BF315001EC5C2 /* Definitions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Definitions.swift; sourceTree = "<group>"; }; + AD39D6AA264BF315001EC5C2 /* SearchFilterCollections.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SearchFilterCollections.swift; sourceTree = "<group>"; }; + AD39D6AB264BF315001EC5C2 /* PersistentDataController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PersistentDataController.swift; sourceTree = "<group>"; }; + AD39D6AC264BF315001EC5C2 /* Home.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Home.swift; sourceTree = "<group>"; }; + AD39D6AD264BF315001EC5C2 /* README.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = README.swift; sourceTree = "<group>"; }; + AD39D6AF264BF315001EC5C2 /* StatefulPreviewWrapper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StatefulPreviewWrapper.swift; sourceTree = "<group>"; }; + AD39D6B0264BF315001EC5C2 /* DragResizableCard.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DragResizableCard.swift; sourceTree = "<group>"; }; + AD39D6B1264BF315001EC5C2 /* ToggleableTag.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ToggleableTag.swift; sourceTree = "<group>"; }; + AD39D6B2264BF315001EC5C2 /* Card.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Card.swift; sourceTree = "<group>"; }; + AD39D6B3264BF315001EC5C2 /* Wrap.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Wrap.swift; sourceTree = "<group>"; }; + AD39D6B4264BF315001EC5C2 /* Blur.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Blur.swift; sourceTree = "<group>"; }; AD97DFBD241F97A300C35B95 /* OnboardingIntroInfoSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingIntroInfoSection.swift; sourceTree = "<group>"; }; C45B00D77A89D61A56C33242 /* Pods-enzevalos_iphoneUITests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-enzevalos_iphoneUITests.debug.xcconfig"; path = "Target Support Files/Pods-enzevalos_iphoneUITests/Pods-enzevalos_iphoneUITests.debug.xcconfig"; sourceTree = "<group>"; }; C675EC8D68A3FFEB0087F5A9 /* Pods-enzevalos_iphoneUITests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-enzevalos_iphoneUITests.release.xcconfig"; path = "Target Support Files/Pods-enzevalos_iphoneUITests/Pods-enzevalos_iphoneUITests.release.xcconfig"; sourceTree = "<group>"; }; @@ -972,12 +1050,8 @@ 476406882416B54D00C7D426 /* Inbox */ = { isa = PBXGroup; children = ( - 476406892416B54D00C7D426 /* KeyRecordRow.swift */, - 4764068B2416B54D00C7D426 /* InboxCoordinator.swift */, - 4750BDE02539C5FC00F6D5AB /* InboxView.swift */, - 47FA8EAB254D77DE006883D0 /* MailListView.swift */, - 47C3490125489F52008D290C /* MailRowView.swift */, - 3F071ED22611BF6900E121F0 /* InboxRefreshModel.swift */, + AD39D680264BF0DA001EC5C2 /* categorized */, + AD39D67D264BF0C1001EC5C2 /* simple */, ); path = Inbox; sourceTree = "<group>"; @@ -1504,6 +1578,169 @@ path = c; sourceTree = "<group>"; }; + AD39D67D264BF0C1001EC5C2 /* simple */ = { + isa = PBXGroup; + children = ( + 476406892416B54D00C7D426 /* KeyRecordRow.swift */, + 4764068B2416B54D00C7D426 /* InboxCoordinator.swift */, + 4750BDE02539C5FC00F6D5AB /* InboxView.swift */, + 47FA8EAB254D77DE006883D0 /* MailListView.swift */, + 47C3490125489F52008D290C /* MailRowView.swift */, + 3F071ED22611BF6900E121F0 /* InboxRefreshModel.swift */, + ); + path = simple; + sourceTree = "<group>"; + }; + AD39D680264BF0DA001EC5C2 /* categorized */ = { + isa = PBXGroup; + children = ( + AD39D681264BF315001EC5C2 /* Categories */, + AD39D695264BF315001EC5C2 /* Helpers */, + AD39D6AC264BF315001EC5C2 /* Home.swift */, + AD39D6AD264BF315001EC5C2 /* README.swift */, + AD39D6AE264BF315001EC5C2 /* UI global */, + ); + path = categorized; + sourceTree = "<group>"; + }; + AD39D681264BF315001EC5C2 /* Categories */ = { + isa = PBXGroup; + children = ( + AD39D682264BF315001EC5C2 /* Chat */, + AD39D68C264BF315001EC5C2 /* SampleCategoryV2.swift */, + AD39D68D264BF315001EC5C2 /* UnsortedCategory.swift */, + AD39D68E264BF315001EC5C2 /* Category.swift */, + AD39D68F264BF315001EC5C2 /* Files */, + ); + path = Categories; + sourceTree = "<group>"; + }; + AD39D682264BF315001EC5C2 /* Chat */ = { + isa = PBXGroup; + children = ( + AD39D683264BF315001EC5C2 /* ChatCategoryView.swift */, + AD39D684264BF315001EC5C2 /* ChatCategoryModel.swift */, + AD39D685264BF315001EC5C2 /* ChatCategory.swift */, + AD39D686264BF315001EC5C2 /* Children */, + ); + path = Chat; + sourceTree = "<group>"; + }; + AD39D686264BF315001EC5C2 /* Children */ = { + isa = PBXGroup; + children = ( + AD39D687264BF315001EC5C2 /* Chat_PeopleRow.swift */, + AD39D688264BF315001EC5C2 /* chat */, + ); + path = Children; + sourceTree = "<group>"; + }; + AD39D688264BF315001EC5C2 /* chat */ = { + isa = PBXGroup; + children = ( + AD39D689264BF315001EC5C2 /* Bubble.swift */, + AD39D68A264BF315001EC5C2 /* Readme.swift */, + AD39D68B264BF315001EC5C2 /* ChatView.swift */, + ); + path = chat; + sourceTree = "<group>"; + }; + AD39D68F264BF315001EC5C2 /* Files */ = { + isa = PBXGroup; + children = ( + AD39D690264BF315001EC5C2 /* FilesCategoryModel.swift */, + AD39D691264BF315001EC5C2 /* FilesCategory.swift */, + AD39D692264BF315001EC5C2 /* Children */, + AD39D694264BF315001EC5C2 /* FilesCategoryView.swift */, + ); + path = Files; + sourceTree = "<group>"; + }; + AD39D692264BF315001EC5C2 /* Children */ = { + isa = PBXGroup; + children = ( + AD39D693264BF315001EC5C2 /* FilesPreviewer.swift */, + ); + path = Children; + sourceTree = "<group>"; + }; + AD39D695264BF315001EC5C2 /* Helpers */ = { + isa = PBXGroup; + children = ( + AD39D696264BF315001EC5C2 /* UI */, + AD39D6A0264BF315001EC5C2 /* ViewModels */, + AD39D6A4264BF315001EC5C2 /* CustumObserving */, + AD39D6A7264BF315001EC5C2 /* Search */, + AD39D6AB264BF315001EC5C2 /* PersistentDataController.swift */, + ); + path = Helpers; + sourceTree = "<group>"; + }; + AD39D696264BF315001EC5C2 /* UI */ = { + isa = PBXGroup; + children = ( + AD39D697264BF315001EC5C2 /* CategoryView.swift */, + AD39D698264BF315001EC5C2 /* SupportingViews */, + AD39D69F264BF315001EC5C2 /* CategoryViewInner.swift */, + ); + path = UI; + sourceTree = "<group>"; + }; + AD39D698264BF315001EC5C2 /* SupportingViews */ = { + isa = PBXGroup; + children = ( + AD39D699264BF315001EC5C2 /* TypePicker.swift */, + AD39D69A264BF315001EC5C2 /* UnreadCountIcon.swift */, + AD39D69B264BF315001EC5C2 /* MailOptionsView.swift */, + AD39D69C264BF315001EC5C2 /* newMailRow.swift */, + AD39D69D264BF315001EC5C2 /* CategoryOverViewBar.swift */, + AD39D69E264BF315001EC5C2 /* Searchview_2.swift */, + ); + path = SupportingViews; + sourceTree = "<group>"; + }; + AD39D6A0264BF315001EC5C2 /* ViewModels */ = { + isa = PBXGroup; + children = ( + AD39D6A1264BF315001EC5C2 /* ViewModelProvider.swift */, + AD39D6A2264BF315001EC5C2 /* CategoryViewModel.swift */, + AD39D6A3264BF315001EC5C2 /* HomeModel.swift */, + ); + path = ViewModels; + sourceTree = "<group>"; + }; + AD39D6A4264BF315001EC5C2 /* CustumObserving */ = { + isa = PBXGroup; + children = ( + AD39D6A5264BF315001EC5C2 /* ReasonedObservers.swift */, + AD39D6A6264BF315001EC5C2 /* CustomObservers.swift */, + ); + path = CustumObserving; + sourceTree = "<group>"; + }; + AD39D6A7264BF315001EC5C2 /* Search */ = { + isa = PBXGroup; + children = ( + AD39D6A8264BF315001EC5C2 /* SearchFilters.swift */, + AD39D6A9264BF315001EC5C2 /* Definitions.swift */, + AD39D6AA264BF315001EC5C2 /* SearchFilterCollections.swift */, + ); + path = Search; + sourceTree = "<group>"; + }; + AD39D6AE264BF315001EC5C2 /* UI global */ = { + isa = PBXGroup; + children = ( + AD39D6AF264BF315001EC5C2 /* StatefulPreviewWrapper.swift */, + AD39D6B0264BF315001EC5C2 /* DragResizableCard.swift */, + AD39D6B1264BF315001EC5C2 /* ToggleableTag.swift */, + AD39D6B2264BF315001EC5C2 /* Card.swift */, + AD39D6B3264BF315001EC5C2 /* Wrap.swift */, + AD39D6B4264BF315001EC5C2 /* Blur.swift */, + ); + path = "UI global"; + sourceTree = "<group>"; + }; F113C3831F30D01A00E7F1D6 /* QRCode */ = { isa = PBXGroup; children = ( @@ -1895,9 +2132,11 @@ files = ( 476801DE21846A5A00F7F259 /* OutgoingMail.swift in Sources */, 47B71AAF2538354A00CA87C6 /* OnboardingValueState.swift in Sources */, + AD39D6D9264BF316001EC5C2 /* Card.swift in Sources */, 47BCAF2A259CD5CE0008FE4B /* KeyManagementModel.swift in Sources */, 47C09C76243B3395007F74A2 /* SmallContactListView.swift in Sources */, 47EABF272423BFDD00774A93 /* AuthenticationScreen.swift in Sources */, + AD39D6BC264BF316001EC5C2 /* SampleCategoryV2.swift in Sources */, 477120AE254C28F900B28C64 /* TabSupport.swift in Sources */, 47BCAF74259FBC810008FE4B /* KeyManagementOverview.swift in Sources */, 4764069A2416B54D00C7D426 /* MailView.swift in Sources */, @@ -1918,19 +2157,25 @@ 476406992416B54D00C7D426 /* SearchView.swift in Sources */, 47D1302B1F7CEE6D007B14DF /* DebugSettings.swift in Sources */, A5E303D824110F6400310264 /* smime-helpers.c in Sources */, + AD39D6C6264BF316001EC5C2 /* MailOptionsView.swift in Sources */, 47FA8EC3254D9E01006883D0 /* RecipientFieldModel.swift in Sources */, 4775D7A8243F0D630052F2CC /* DisplayProtocols.swift in Sources */, F1984D721E1D327200804E1E /* IconsStyleKit.swift in Sources */, + AD39D6C5264BF316001EC5C2 /* UnreadCountIcon.swift in Sources */, 47B71AAE2538354A00CA87C6 /* OnboardingIntro.swift in Sources */, 47C822692438A85C005BCE73 /* PhishingView.swift in Sources */, F1737ACB2031D7D70000312B /* StudySettings.swift in Sources */, 0ECA5798240D496800B0F231 /* SMIME.swift in Sources */, 47C8225A24379EAE005BCE73 /* CardWithTitle.swift in Sources */, 6789425F2430C3B300C746D1 /* MailComparison.swift in Sources */, + AD39D6C3264BF316001EC5C2 /* CategoryView.swift in Sources */, + AD39D6D8264BF316001EC5C2 /* ToggleableTag.swift in Sources */, A114E4321FACB23000E40243 /* StringExtension.swift in Sources */, 472F398C1E2519C8009260FB /* CNContactExtension.swift in Sources */, 7FE3F9FA260F467A0025340B /* Attachment.swift in Sources */, + AD39D6B8264BF316001EC5C2 /* Chat_PeopleRow.swift in Sources */, 476406972416B54D00C7D426 /* InboxCoordinator.swift in Sources */, + AD39D6D0264BF316001EC5C2 /* SearchFilters.swift in Sources */, 0EF148082422572500B3C198 /* general-helpers.c in Sources */, 97BDE0432429188500B0BF03 /* BadgeProgressView.swift in Sources */, 47C822682438A85C005BCE73 /* SenderDetails.swift in Sources */, @@ -1938,18 +2183,23 @@ 3FEFF41E260A899100A7F9CC /* ComposeHeaderView.swift in Sources */, 47B71AAD2538354A00CA87C6 /* NewOnboardingView.swift in Sources */, AD97DFBE241F97A300C35B95 /* OnboardingIntroInfoSection.swift in Sources */, + AD39D6C7264BF316001EC5C2 /* newMailRow.swift in Sources */, 47BCAF40259CFE110008FE4B /* AddButton.swift in Sources */, 47EABF2B2423C20C00774A93 /* LoadingBlocker.swift in Sources */, 475B00351F7B9565006CDD41 /* CryptoObject.swift in Sources */, 4706D661225CD21D00B3F1D3 /* ExportKeyHelper.swift in Sources */, 32261743260C7CE60068CBD4 /* ContactMailListView.swift in Sources */, 4775D7B02443539E0052F2CC /* DialogProtocols.swift in Sources */, + AD39D6D1264BF316001EC5C2 /* Definitions.swift in Sources */, 475B00331F7B9565006CDD41 /* SwiftPGP.swift in Sources */, + AD39D6C2264BF316001EC5C2 /* FilesCategoryView.swift in Sources */, 477548E421F77BA0000B22A8 /* StudyParameterProtocol.swift in Sources */, 47BCAF38259CE3930008FE4B /* FavoriteButtonView.swift in Sources */, 47A5D6E42294BFF50084F81D /* Logger.swift in Sources */, + AD39D6CE264BF316001EC5C2 /* ReasonedObservers.swift in Sources */, 3EB4FAA420120096001D0625 /* DialogOption.swift in Sources */, F14239C11F30A99C00998A83 /* QRCodeGenerator.swift in Sources */, + AD39D6D2264BF316001EC5C2 /* SearchFilterCollections.swift in Sources */, 4764069C2416B54D00C7D426 /* VCSwiftUIView.swift in Sources */, 47BCAF26259CD52F0008FE4B /* PublicKeyListView.swift in Sources */, 7FE3F9D6260E76A40025340B /* ImagePicker.swift in Sources */, @@ -1957,6 +2207,8 @@ 4733B1E52527196100AB5600 /* PersistentDataProvider.swift in Sources */, F1866C86201F707200B72453 /* EmailHelper.m in Sources */, 476142081E07E52B00FD5E4F /* Theme.swift in Sources */, + AD39D6CF264BF316001EC5C2 /* CustomObservers.swift in Sources */, + AD39D6CA264BF316001EC5C2 /* CategoryViewInner.swift in Sources */, 4750BDBC25399D8200F6D5AB /* AddressRecord+CoreDataClass.swift in Sources */, 476801DB218436B600F7F259 /* Autocrypt.swift in Sources */, 4780C8C225E932AA00C1636F /* ReadModel.swift in Sources */, @@ -1964,13 +2216,19 @@ 47FAE31C2524C07B005A1BCB /* MailRecord.swift in Sources */, 477548DE21F5DABE000B22A8 /* MailServerConnectionError.swift in Sources */, 47BCAF34259CD64D0008FE4B /* KeyRowView.swift in Sources */, + AD39D6B7264BF316001EC5C2 /* ChatCategory.swift in Sources */, + AD39D6BD264BF316001EC5C2 /* UnsortedCategory.swift in Sources */, 47C112C62531DBDD00621A07 /* SecretKeyRecord.swift in Sources */, 4750BDF02539DF8200F6D5AB /* LetterboxApp.swift in Sources */, + AD39D6C4264BF316001EC5C2 /* TypePicker.swift in Sources */, + AD39D6D5264BF316001EC5C2 /* README.swift in Sources */, 47CEAC98222541B40075B7DC /* MailSession.swift in Sources */, 47EABF292423C1FB00774A93 /* KeyboardChecker.swift in Sources */, 477548E221F77466000B22A8 /* SecurityIndicator.swift in Sources */, 476373C21E09BA88004D5EFE /* UserData.swift in Sources */, 47BCAF64259E30280008FE4B /* MixedKeyListView.swift in Sources */, + AD39D6CD264BF316001EC5C2 /* HomeModel.swift in Sources */, + AD39D6C8264BF316001EC5C2 /* CategoryOverViewBar.swift in Sources */, 47A2A57223599D180013883D /* FeedbackButtonHelper.swift in Sources */, 3EC35F2420037651008BDF95 /* InvitationHelper.swift in Sources */, 47C112C22531D72E00621A07 /* PublicKeyRecord.swift in Sources */, @@ -1983,20 +2241,31 @@ 4709769626220659002436E2 /* SimpleMailRowView.swift in Sources */, 678942612430C3D600C746D1 /* Typosquatting.swift in Sources */, 47C112CA2531E9B000621A07 /* AttachmentRecord.swift in Sources */, + AD39D6D3264BF316001EC5C2 /* PersistentDataController.swift in Sources */, 476406982416B54D00C7D426 /* CircleImage.swift in Sources */, A111F6AD1FA77B170060AFDE /* LoggerDetail.swift in Sources */, 47358D92244A5AEA000116D7 /* SelectableTextView.swift in Sources */, 0EF148052422543E00B3C198 /* Certificate.swift in Sources */, + AD39D6CC264BF316001EC5C2 /* CategoryViewModel.swift in Sources */, 47BCAF5A259DE6770008FE4B /* SecretKeyListView.swift in Sources */, + AD39D6CB264BF316001EC5C2 /* ViewModelProvider.swift in Sources */, A15D215B223BE5F4003E0CE0 /* TempAttachment.swift in Sources */, 4706D65F225B7B6B00B3F1D3 /* ItunesHandler.swift in Sources */, 47EABF0A241A9C8700774A93 /* AuthenticationViewModel.swift in Sources */, 47FA8EAC254D77DE006883D0 /* MailListView.swift in Sources */, 47C8225924379EAE005BCE73 /* AttPreview.swift in Sources */, 7FE3F9E9260F45600025340B /* AddAttachmentsView.swift in Sources */, + AD39D6C1264BF316001EC5C2 /* FilesPreviewer.swift in Sources */, + AD39D6B6264BF316001EC5C2 /* ChatCategoryModel.swift in Sources */, 475B00341F7B9565006CDD41 /* Cryptography.swift in Sources */, + AD39D6BE264BF316001EC5C2 /* Category.swift in Sources */, + AD39D6C9264BF316001EC5C2 /* Searchview_2.swift in Sources */, + AD39D6B9264BF316001EC5C2 /* Bubble.swift in Sources */, + AD39D6C0264BF316001EC5C2 /* FilesCategory.swift in Sources */, A1EB057C1D956838008659C1 /* MailHandler.swift in Sources */, + AD39D6BF264BF316001EC5C2 /* FilesCategoryModel.swift in Sources */, 0EFEF0952417C0B400BB2FF7 /* CHelpers.swift in Sources */, + AD39D6DB264BF316001EC5C2 /* Blur.swift in Sources */, 4764069D2416B54D00C7D426 /* Stroke.swift in Sources */, 47BCAF6C259F9BC50008FE4B /* PasswordView.swift in Sources */, 3FB75DC525FFA75C00919925 /* ComposeView.swift in Sources */, @@ -2005,9 +2274,13 @@ 477120C2254C676000B28C64 /* ContactView.swift in Sources */, 47EABF09241A9C8700774A93 /* AuthenticationModel.swift in Sources */, 47FA8EA8254C7E5B006883D0 /* FolderRowView.swift in Sources */, + AD39D6DA264BF316001EC5C2 /* Wrap.swift in Sources */, A1EB05881D956879008659C1 /* ContactHandler.swift in Sources */, + AD39D6BB264BF316001EC5C2 /* ChatView.swift in Sources */, + AD39D6D4264BF316001EC5C2 /* Home.swift in Sources */, 47EABF2D2423C65F00774A93 /* AuthenticationView.swift in Sources */, 47BCAF68259F48840008FE4B /* TempKeyRow.swift in Sources */, + AD39D6D7264BF316001EC5C2 /* DragResizableCard.swift in Sources */, 479011492289975D0057AB04 /* NoSecIconStyleKit.swift in Sources */, 3FB75DC925FFA77800919925 /* RecipientRowView.swift in Sources */, 47C822622438A81C005BCE73 /* MapView.swift in Sources */, @@ -2016,11 +2289,14 @@ 479E15D82539E3CD0040142A /* LetterboxModel.swift in Sources */, 47EABF0D241A9CA800774A93 /* MailAccount.swift in Sources */, A18E7D771FBDE5D9002F7CC9 /* LoggingEventType.swift in Sources */, + AD39D6D6264BF316001EC5C2 /* StatefulPreviewWrapper.swift in Sources */, F1984D741E1E92B300804E1E /* LabelStyleKit.swift in Sources */, 47C8225724379EAE005BCE73 /* DialogView.swift in Sources */, + AD39D6B5264BF316001EC5C2 /* ChatCategoryView.swift in Sources */, 47BCAF56259DE23A0008FE4B /* KeyView.swift in Sources */, 478154A721FF3F0900A931EC /* Warning.swift in Sources */, 47E04DDF255D3CF600189320 /* ComposeModel.swift in Sources */, + AD39D6BA264BF316001EC5C2 /* Readme.swift in Sources */, 0EF73F4324237E6500932FA0 /* SMIMEHelpers.swift in Sources */, 47C8225E24379EAE005BCE73 /* ReadMainView.swift in Sources */, 47C3490225489F52008D290C /* MailRowView.swift in Sources */, diff --git a/enzevalos_iphone/SwiftUI/Inbox/categorized/Categories/Category.swift b/enzevalos_iphone/SwiftUI/Inbox/categorized/Categories/Category.swift new file mode 100644 index 0000000000000000000000000000000000000000..91791d591357f95303613ae77a24bca995326845 --- /dev/null +++ b/enzevalos_iphone/SwiftUI/Inbox/categorized/Categories/Category.swift @@ -0,0 +1,57 @@ +// +// Category.swift +// enzevalos_iphone +// +// Created by hanneh00 on 18.01.21. +// Copyright © 2021 fu-berlin. All rights reserved. +// + +import SwiftUI + +typealias CategoryV = Category + +///what a category has (and needs) for values +protocol Category { + var name : String {get} + var description : String {get} + var icon : Image {get} + var isBigger : Bool {get} + var isResizable : Bool {get} + var searchFields : [SearchFilter] {get} + var filter : MailFilter {get} + var andPredicate : NSPredicate {get} + + func body(model : CategoryViewModel)->AnyView +} + +///standart values for a category if not specified otherways +extension CategoryV { + var description : String {""} + var icon : Image {Image(systemName: "envelope.fill")} + var isBigger : Bool {false} + var isResizable : Bool {true} + var searchFields : [SearchFilter] {SearchFilterCollections.Standarts} + var andPredicate : NSPredicate {NSPredicate(value: true)} +} + +///to which extend the category will open a specific mail +enum MailCategoryOpenability : Int16, Comparable{ + static func < (lhs: MailCategoryOpenability, rhs: MailCategoryOpenability) -> Bool { + lhs.rawValue < rhs.rawValue + } + + case cannotOpen = 0, ///: means this category wont open this email, wont display it and if forced to might cause undefined behavior + canOpen = 1, ///: the category can display this mail , but the category wont float to the top of the inbox + shouldOpen = 2, ///: the category displays this mails and floats to the top to show that it has new mail + mustOpen = 3 ///: overrides all other categories to force them to not open this mail (e.g. cause it is dangerous phishing) + +} + + +typealias eMail = MailRecord + +///each category needs a filter to return to which extend the category will open a specific mail +typealias MailFilter = (eMail)->MailCategoryOpenability + +///what all the ViewModels use to compare to categories to see if they are the same (also the Database) +typealias CategoryIDType = String diff --git a/enzevalos_iphone/SwiftUI/Inbox/categorized/Categories/Chat/ChatCategory.swift b/enzevalos_iphone/SwiftUI/Inbox/categorized/Categories/Chat/ChatCategory.swift new file mode 100644 index 0000000000000000000000000000000000000000..f373112a19dfcc70cd5ba2696a1169425e5042e1 --- /dev/null +++ b/enzevalos_iphone/SwiftUI/Inbox/categorized/Categories/Chat/ChatCategory.swift @@ -0,0 +1,30 @@ +// +// ChatCategory.swift +// enzevalos_iphone +// +// Created by hanneh00 on 23.01.21. +// Copyright © 2021 fu-berlin. All rights reserved. +// + +import SwiftUI + +let ChatCategory = CategoryView( + name : NSLocalizedString("Category.Chats.name", comment: "People"), + icon : Image(systemName: "message.fill"), + searchFields : SearchFilterCollections.Files, + filter : filterForShortMail //TODO: create an actually good filter +){model in + ChatCategoryView(model:model) +} + + + +fileprivate func filterForShortMail(_ email : eMail)->MailCategoryOpenability{ + + let maxLength:Int = 200 + + guard email.body.count < maxLength else { + return .cannotOpen + } + return .shouldOpen +} diff --git a/enzevalos_iphone/SwiftUI/Inbox/categorized/Categories/Chat/ChatCategoryModel.swift b/enzevalos_iphone/SwiftUI/Inbox/categorized/Categories/Chat/ChatCategoryModel.swift new file mode 100644 index 0000000000000000000000000000000000000000..1eb382bd892f3c497ba0de111ca96564afc9b497 --- /dev/null +++ b/enzevalos_iphone/SwiftUI/Inbox/categorized/Categories/Chat/ChatCategoryModel.swift @@ -0,0 +1,79 @@ +// +// ChatCategoryModel.swift +// enzevalos_iphone +// +// Created by hanneh00 on 26.01.21. +// Copyright © 2021 fu-berlin. All rights reserved. +// + +import SwiftUI + +///some extra functionality on top of the standort category functionality +///for the ChatCategory obv. +class ChatCategoryVM: ObservableObject { + + init(_ categoryModel : CategoryViewModel) { + categoryBaseModel = categoryModel + } + + @ObservedObject var categoryBaseModel : CategoryViewModel + + var mails : [eMail] { + categoryBaseModel.allMails //group by sender? + } + + var singleChats : [SingleChat] { + //would probably be cooler to fetch the users directly and filter their mails with an NSPredicate ? + var chatsByUser : [eMail.C : SingleChat] = [:] + for mail in mails { + + if mail.addresses.count > 2 {continue} //group chat + + guard let candidat1 = mail.addresses[safe: 0] else{continue} + guard let candidat2 = mail.addresses.last else{continue} + + let _/*user*/ = !candidat1.isUser ? candidat1 : candidat2 + let partner = !candidat1.isUser ? candidat2 : candidat1 + + if var alreadyCreatedChat = chatsByUser[partner]{ + alreadyCreatedChat.messages.append(mail) + }else{ + chatsByUser[partner]=SingleChat( + timeStamp: mail.date, + partner: partner, + messages: [mail] + ) + } + } + //turn to array and sort by timeStamp + return chatsByUser.map{$1}.sorted(by: {$0.timeStamp >= $1.timeStamp}) + } + + //future work + var groupChatUsers : [GroupChat] { + //TODO + [] + } + + /* + var isFullscreen : Bool {categoryBaseModel.size == nil} + func attachmentsize(_ attachmentCount:Int)->CGFloat { + return isFullscreen ? 250 : ((categoryBaseModel.size! - 50) / CGFloat(attachmentCount)) + } + */ + +} + +struct SingleChat:Hashable{ + var timeStamp : Date + var partner : eMail.C + var messages : [eMail] +} + +//TODO future work +struct GroupChat{ + var timeStamp : Date + var users : [eMail.C] + var messages : [eMail] +} + diff --git a/enzevalos_iphone/SwiftUI/Inbox/categorized/Categories/Chat/ChatCategoryView.swift b/enzevalos_iphone/SwiftUI/Inbox/categorized/Categories/Chat/ChatCategoryView.swift new file mode 100644 index 0000000000000000000000000000000000000000..e8c953edda0537f28b47175d197e1f8622293dae --- /dev/null +++ b/enzevalos_iphone/SwiftUI/Inbox/categorized/Categories/Chat/ChatCategoryView.swift @@ -0,0 +1,43 @@ +// +// ChatCategoryView.swift +// enzevalos_iphone +// +// Created by hanneh00 on 23.01.21. +// Copyright © 2021 fu-berlin. All rights reserved. +// + +import SwiftUI + +/// how the home of the chat category looks (with all the contacts in a list) +struct ChatCategoryView: View { + + init(model : CategoryViewModel) { + baseModel = model + chatModel = ChatCategoryVM(model) + } + + var baseModel : CategoryViewModel + @ObservedObject var chatModel : ChatCategoryVM + + var body: some View { + VStack{ + HStack{Spacer()}//make it use full width + if let size = baseModel.size { + let optimalAmount = min(Int(size)/70 , chatModel.singleChats.count) + //Preview + ForEach(0 ..< optimalAmount,id:\.self){i in if let chat = chatModel.singleChats[safe: i]{ + Chat_PeopleRow(chat: chat) + }} + + }else{ + //FullScreen + ScrollView{ + ForEach(chatModel.singleChats, id: \.self){chat in + Chat_PeopleRow(chat: chat) + } + } + } + } + .frame(height:baseModel.size).clipped() + } +} diff --git a/enzevalos_iphone/SwiftUI/Inbox/categorized/Categories/Chat/Children/Chat_PeopleRow.swift b/enzevalos_iphone/SwiftUI/Inbox/categorized/Categories/Chat/Children/Chat_PeopleRow.swift new file mode 100644 index 0000000000000000000000000000000000000000..c2e159b76bf45de4bba7ffabdf99619a9c695245 --- /dev/null +++ b/enzevalos_iphone/SwiftUI/Inbox/categorized/Categories/Chat/Children/Chat_PeopleRow.swift @@ -0,0 +1,58 @@ +// +// Chat_PeopleRow.swift +// enzevalos_iphone +// +// Created by hanneh00 on 25.01.21. +// Copyright © 2021 fu-berlin. All rights reserved. +// + +import SwiftUI +import TagNavVi + +struct Chat_PeopleRow: View { + + let chat:SingleChat + + @State var opens:Bool = false + + var body : some View { + HStack{ + profile + VStack { + nameView + Spacer().frame(height: 5) + HStack{ + Text(chat.messages[0].body) + .font(.caption) + .lineLimit(2) + Spacer() + Text(chat.timeStamp.timeAgoText()) + .font(.caption) + } + }.onTapGesture { + opens.toggle() + } + TagNavigationLink(parentTag: 1, destination: ChatView(chat:chat), isActive: $opens) { + EmptyView() + } + } + } + + private var profile : some View { + chat.partner.myImage + .resizable() + .frame(width: 50, height: 50) + } + private var nameView : some View { + Text(chat.partner.name.split(separator: "@")[0]) + .frame(maxWidth: 300, alignment: .leading) + .lineLimit(1) + .font(.subheadline) + } +} + +struct Chat_PeopleRow_Previews: PreviewProvider { + static var previews: some View { + EmptyView()//Chat_PeopleRow() + } +} diff --git a/enzevalos_iphone/SwiftUI/Inbox/categorized/Categories/Chat/Children/chat/Bubble.swift b/enzevalos_iphone/SwiftUI/Inbox/categorized/Categories/Chat/Children/chat/Bubble.swift new file mode 100644 index 0000000000000000000000000000000000000000..44e1c368e147a9e2907915b1135a450d14a1d28f --- /dev/null +++ b/enzevalos_iphone/SwiftUI/Inbox/categorized/Categories/Chat/Children/chat/Bubble.swift @@ -0,0 +1,50 @@ +// +// Bubble.swift +// enzevalos_iphone +// +// Created by hanneh00 on 23.01.21. +// Copyright © 2021 fu-berlin. All rights reserved. +// + +import SwiftUI + + +///shows a single message in a chat (todo needs some refinement) +struct ChatMessageView : View { + + var checkedChatMessage: eMail + var body: some View { + HStack(alignment: .bottom, spacing: 15) { + if !checkedChatMessage.sender.isUser{ + checkedChatMessage.sender.myImage + .resizable() + .frame(width: 40, height: 40, alignment: .center) + .cornerRadius(20) + } else { + Spacer() + } + Bubble(contentChatMessage: checkedChatMessage.body, isCurrentChatUser: checkedChatMessage.sender.isUser) + .capsuledMailOptions(mail: checkedChatMessage) + } + } +} + +struct Bubble: View { + var contentChatMessage: String + var isCurrentChatUser: Bool + + var body: some View { + Text(contentChatMessage) + .padding(10) + .foregroundColor(isCurrentChatUser ? Color.white : Color.black) + .background(isCurrentChatUser ? Color.blue : Color(UIColor(red: 240/255, green: 240/255, blue: 240/255, alpha: 1.0))) + .cornerRadius(10) + } +} + +struct Bubble_Previews: PreviewProvider { + static var previews: some View { + Bubble(contentChatMessage: "Hi, I am your friend", isCurrentChatUser: false) + } +} + diff --git a/enzevalos_iphone/SwiftUI/Inbox/categorized/Categories/Chat/Children/chat/ChatView.swift b/enzevalos_iphone/SwiftUI/Inbox/categorized/Categories/Chat/Children/chat/ChatView.swift new file mode 100644 index 0000000000000000000000000000000000000000..0f072c91e5b12c5bb275016bccff4072b4a8948a --- /dev/null +++ b/enzevalos_iphone/SwiftUI/Inbox/categorized/Categories/Chat/Children/chat/ChatView.swift @@ -0,0 +1,40 @@ +// +// ChatView.swift +// enzevalos_iphone +// +// Created by hanneh00 on 23.01.21. +// Copyright © 2021 fu-berlin. All rights reserved. +// + +import SwiftUI + +///a complete chat with all its messages +struct ChatView: View { + + @State var typingChatMessage: String = "" + var chat : SingleChat + + var body: some View { + VStack { + ScrollView { + ForEach(chat.messages) { msg in + ChatMessageView(checkedChatMessage: msg) + } + } + HStack { + TextField("ChatMessage...", text: $typingChatMessage) + .textFieldStyle(RoundedBorderTextFieldStyle()) + .frame(minHeight: CGFloat(30)) + Button(action: {/*TODO*/}) { + Text("Send") + } + }.frame(minHeight: CGFloat(50)).padding() + } + } +} + +struct ChatView_Previews: PreviewProvider { + static var previews: some View { + EmptyView()//ChatView() + } +} diff --git a/enzevalos_iphone/SwiftUI/Inbox/categorized/Categories/Chat/Children/chat/Readme.swift b/enzevalos_iphone/SwiftUI/Inbox/categorized/Categories/Chat/Children/chat/Readme.swift new file mode 100644 index 0000000000000000000000000000000000000000..4f0d7273b831c3670eea2c89bb84633652a8d42c --- /dev/null +++ b/enzevalos_iphone/SwiftUI/Inbox/categorized/Categories/Chat/Children/chat/Readme.swift @@ -0,0 +1,3 @@ +/* + small parts of the contents of this folder are cmd-c-cmd-v'd from https://www.iosapptemplates.com/blog/swiftui/swiftui-chat + */ diff --git a/enzevalos_iphone/SwiftUI/Inbox/categorized/Categories/Files/Children/FilesPreviewer.swift b/enzevalos_iphone/SwiftUI/Inbox/categorized/Categories/Files/Children/FilesPreviewer.swift new file mode 100644 index 0000000000000000000000000000000000000000..7a5d5a7c079afd75a142db7da5984fef99bfd20a --- /dev/null +++ b/enzevalos_iphone/SwiftUI/Inbox/categorized/Categories/Files/Children/FilesPreviewer.swift @@ -0,0 +1,55 @@ +// +// FilesPreviewer.swift +// enzevalos_iphone +// +// Created by hanneh00 on 22.01.21. +// Copyright © 2021 fu-berlin. All rights reserved. +// + +import SwiftUI +import TagNavVi + + +///shows all the files of each and every email +struct FilesPreviewer: View { + + var height: CGFloat? + @ObservedObject var filesCategoryVM : FilesCategoryVM + + @State var openEmail = false + + var body: some View { + ScrollView(.horizontal,showsIndicators:false){HStack{ForEach(0..<filesCategoryVM.mailsAttachments.count){ mailIndex in + if let mailAttachments = filesCategoryVM.mailsAttachments[safe: mailIndex] { + if filesCategoryVM.isFullscreen { + ScrollView(.vertical, showsIndicators:false){attachmentColumn(filesCategoryVM.mails[mailIndex],attachments:mailAttachments)} + + } else { + attachmentColumn(filesCategoryVM.mails[mailIndex],attachments:mailAttachments) + } + } + }}.padding(.horizontal)} + } + + ///the attachments of a single mail + @ViewBuilder func attachmentColumn(_ mail: eMail, attachments:[DisplayAttachment])->some View{ + let height = filesCategoryVM.attachmentsize(attachments.count) + VStack{ + Text(mail.subject) + .capsuledMailOptions(mail: mail) + ForEach(0..<attachments.count){ attachmentIndex in if let attachment = attachments[safe: attachmentIndex] { + AttPrev( + attachment: attachment + ) //TODO: make this have the exact aspect ratio ? + .frame(width: 200).frame(height: height) + .padding(.horizontal, 40).padding(.bottom, -25).padding(.top, -7) + }} + } + } +} + +struct FilesPreviewer_Previews: PreviewProvider { + static var previews: some View { + EmptyView()//FilesPreviewer() + } +} diff --git a/enzevalos_iphone/SwiftUI/Inbox/categorized/Categories/Files/FilesCategory.swift b/enzevalos_iphone/SwiftUI/Inbox/categorized/Categories/Files/FilesCategory.swift new file mode 100644 index 0000000000000000000000000000000000000000..6a60e5f5b5ff08ffd85e8cfad07f483504d884fd --- /dev/null +++ b/enzevalos_iphone/SwiftUI/Inbox/categorized/Categories/Files/FilesCategory.swift @@ -0,0 +1,29 @@ +// +// FilesCategory.swift +// enzevalos_iphone +// +// Created by hanneh00 on 21.01.21. +// Copyright © 2021 fu-berlin. All rights reserved. +// + +import SwiftUI + +let FilesCategory = CategoryView( + name : NSLocalizedString("Category.Files.name", comment: "Dateien"), + icon : Image(systemName: "archivebox.fill"), + searchFields : SearchFilterCollections.Files, + filter : filterForWithAttachment +){model in + FilesCategoryView(model:model) + +} + + + +fileprivate func filterForWithAttachment(_ email : eMail)->MailCategoryOpenability{ + guard email.displayAttachments.count > 0 else { + return .cannotOpen + } + //TODO: filter for actually displayable attachments + return .canOpen +} diff --git a/enzevalos_iphone/SwiftUI/Inbox/categorized/Categories/Files/FilesCategoryModel.swift b/enzevalos_iphone/SwiftUI/Inbox/categorized/Categories/Files/FilesCategoryModel.swift new file mode 100644 index 0000000000000000000000000000000000000000..3ad6bb12aab1ddebbd77214edb02907bb5429953 --- /dev/null +++ b/enzevalos_iphone/SwiftUI/Inbox/categorized/Categories/Files/FilesCategoryModel.swift @@ -0,0 +1,45 @@ +// +// FilesCategoryModel.swift +// enzevalos_iphone +// +// Created by hanneh00 on 22.01.21. +// Copyright © 2021 fu-berlin. All rights reserved. +// + +import SwiftUI + +class FilesCategoryVM: ObservableObject { + + init(_ categoryModel : CategoryViewModel) { + categoryBaseModel = categoryModel + } + @ObservedObject var categoryBaseModel : CategoryViewModel + + @Published var filteredFileTypes: [String] = [] + + func filterByTypes(_ email : eMail)->Bool{ + filteredFileTypes.count == 0 || + email.displayAttachments.map{filteredFileTypes.contains($0.myName.components(separatedBy: ".").last ?? "¿")}.contains(true) + } + + var mails : [eMail] { + categoryBaseModel.allMails.filter(filterByTypes) + } + + var mailsAttachments : [[DisplayAttachment]] { + mails.map{ + $0.displayAttachments + .filter{attachment in + filteredFileTypes.count == 0 || + filteredFileTypes.contains(attachment.myName.components(separatedBy: ".").last ?? "¿") + } + } + } + + + var isFullscreen : Bool {categoryBaseModel.size == nil} + func attachmentsize(_ attachmentCount:Int)->CGFloat { + return isFullscreen ? 250 : ((categoryBaseModel.size! - 50) / CGFloat(attachmentCount)) + } + +} diff --git a/enzevalos_iphone/SwiftUI/Inbox/categorized/Categories/Files/FilesCategoryView.swift b/enzevalos_iphone/SwiftUI/Inbox/categorized/Categories/Files/FilesCategoryView.swift new file mode 100644 index 0000000000000000000000000000000000000000..af97cf404146865e6b7c776bcda49393e927c6f0 --- /dev/null +++ b/enzevalos_iphone/SwiftUI/Inbox/categorized/Categories/Files/FilesCategoryView.swift @@ -0,0 +1,37 @@ +// +// FilesCategoryView.swift +// enzevalos_iphone +// +// Created by hanneh00 on 22.01.21. +// Copyright © 2021 fu-berlin. All rights reserved. +// + +import SwiftUI + +struct FilesCategoryView : View{ + + init(model : CategoryViewModel) { + baseModel = model + filesModel = FilesCategoryVM(model) + } + + var baseModel : CategoryViewModel + @ObservedObject var filesModel : FilesCategoryVM + + var body: some View{ + VStack{ + TypePicker(selectedTypes: $filesModel.filteredFileTypes).padding(.horizontal, -20).padding(.bottom , 5) + FilesPreviewer(height: baseModel.size, filesCategoryVM: filesModel).padding(.horizontal, -20) + } + } +} + + + + +struct FilesCategory_Previews: PreviewProvider { + static var previews: some View { + FilesCategory + } +} + diff --git a/enzevalos_iphone/SwiftUI/Inbox/categorized/Categories/SampleCategoryV2.swift b/enzevalos_iphone/SwiftUI/Inbox/categorized/Categories/SampleCategoryV2.swift new file mode 100644 index 0000000000000000000000000000000000000000..ab80bc64ffffd706eca96d98b907e8095e18f729 --- /dev/null +++ b/enzevalos_iphone/SwiftUI/Inbox/categorized/Categories/SampleCategoryV2.swift @@ -0,0 +1,33 @@ +// +// SampleCategoryV2.swift +// enzevalos_iphone +// +// Created by hanneh00 on 18.01.21. +// Copyright © 2021 fu-berlin. All rights reserved. +// + +import SwiftUI + +///just a sample category used for debugging play around as you wish +struct SampleCategoryV2: CategoryV { + + let name: String = "Sample Category" + + var filter: MailFilter = {_ in .canOpen} + + + func body(model:CategoryViewModel) -> AnyView {AnyView( + VStack{ + HStack{Spacer()} + if let size = model.size { + let optimalAmount = min(Int(size)/40 , model.allMails.count) + ForEach(0 ..< optimalAmount,id:\.self){i in Text(model.allMails[i].subject).font(.footnote)} + }else{ + Text("\(model.allMails.count)") + Text("nothing to see here.. this is just a sample, you know?") + } + }.frame(height:model.size) + .withUnreadCounter(model.mailAmount, onPress: {model.isActive=true}) + )} +} + diff --git a/enzevalos_iphone/SwiftUI/Inbox/categorized/Categories/UnsortedCategory.swift b/enzevalos_iphone/SwiftUI/Inbox/categorized/Categories/UnsortedCategory.swift new file mode 100644 index 0000000000000000000000000000000000000000..d56dcf1adf82554ad4ef240001554701f7757cbc --- /dev/null +++ b/enzevalos_iphone/SwiftUI/Inbox/categorized/Categories/UnsortedCategory.swift @@ -0,0 +1,36 @@ +// +// ScreenerCategory.swift +// enzevalos_iphone +// +// Created by hanneh00 on 31.01.21. +// Copyright © 2021 fu-berlin. All rights reserved. +// + +import SwiftUI + +/// The classic category with nothing special UI-wise , but it shows all the mails the other categories couldnt show (and depending on the user preferences just all mails) +struct UnsortedCategory : CategoryV { + + var showAllMails : Bool = false + + var name: String = NSLocalizedString("Category.Screener.name", comment: "No other cateogy is able to open this") + + var icon: Image = Image(systemName: "questionmark.folder.fill") + + //open all mails , and stay on top + var filter: MailFilter = {_ in .shouldOpen} + + var andPredicate: NSPredicate { + showAllMails + ? NSPredicate(value: true) + //query just the emails the other categories couldn't open (openability <= 0) + : NSPredicate(format: "SUBQUERY(inCategory, $category, $category.categoryName != %@ AND $category.openability > 0).@count == 0",name) + } + + func body(model: CategoryViewModel) -> AnyView {AnyView( + ScrollView{ForEach(model.allMails){ + OptionsMailRow(mail: $0) + }} + )} + +} diff --git a/enzevalos_iphone/SwiftUI/Inbox/categorized/Helpers/CustumObserving/CustomObservers.swift b/enzevalos_iphone/SwiftUI/Inbox/categorized/Helpers/CustumObserving/CustomObservers.swift new file mode 100644 index 0000000000000000000000000000000000000000..9029f650090565b4013ca14bd7140862ac1c031a --- /dev/null +++ b/enzevalos_iphone/SwiftUI/Inbox/categorized/Helpers/CustumObserving/CustomObservers.swift @@ -0,0 +1,69 @@ +// +// CustomObservers.swift +// enzevalos_iphone +// +// Created by hanneh00 on 11.02.21. +// Copyright © 2021 fu-berlin. All rights reserved. +// + +import Foundation +import Combine + +protocol CustomObservableObjectP { + associatedtype SendValue + var objectWillChangeR : PassthroughSubject<SendValue, Never> { get set } +} + +class CustomObservableObject<T> : CustomObservableObjectP{ + typealias SendValue = T + var objectWillChangeR = PassthroughSubject<SendValue, Never>() + init() { + //this is how we make all the CPublished vars send updates + Mirror(reflecting: self).children.forEach { child in + if let observedProperty = child.value as? CustomObservableObject<T> { + observedProperty.objectWillChangeR = self.objectWillChangeR + } + } + } +} + +@propertyWrapper class CPublished<Wrapped,Send> : CustomObservableObject<Send> { + + var wrappedValue: Wrapped { + didSet { + objectWillChangeR.send(reason) + } + } + + let reason : SendValue + init(wrappedValue: Wrapped, _ reason: SendValue) { + self.reason = reason + self.wrappedValue = wrappedValue + } +} + + + + + + + +//TODO: make this rebuild a View +//otherwise it is useless +@propertyWrapper class CustomObservedObject<Wrapped : CustomObservableObjectP> { + + typealias SendValue = Wrapped.SendValue + + var wrappedValue: Wrapped { + didSet { + wrappedValue.objectWillChangeR.send(reason) + } + } + + let reason : SendValue + init(wrappedValue: Wrapped, _ reason: SendValue) { + self.reason = reason + self.wrappedValue = wrappedValue + } + +} diff --git a/enzevalos_iphone/SwiftUI/Inbox/categorized/Helpers/CustumObserving/ReasonedObservers.swift b/enzevalos_iphone/SwiftUI/Inbox/categorized/Helpers/CustumObserving/ReasonedObservers.swift new file mode 100644 index 0000000000000000000000000000000000000000..23d778f26a534ac25f859d494d9d3ff1f834e244 --- /dev/null +++ b/enzevalos_iphone/SwiftUI/Inbox/categorized/Helpers/CustumObserving/ReasonedObservers.swift @@ -0,0 +1,39 @@ +// +// ReasonedObservers.swift +// enzevalos_iphone +// +// Created by hanneh00 on 12.02.21. +// Copyright © 2021 fu-berlin. All rights reserved. +// + +import Foundation +import Combine + +typealias ReasonedObservableObject = ReasonedObservableObject_ & ObservableObject + +class ReasonedObservableObject_ : CustomObservableObject<PublishedReason> {} + +enum PublishedReason : Equatable { + case + silent, + normal, + urgent, + custom(_ reason : String) +} + +//like @Published but with an additional reason (used to not update things not needing an update, while other subscribe to the same object need to update) +@propertyWrapper class PPublished<Wrapped> : CPublished<Wrapped,PublishedReason>{ + //why is the compiling not checking the Inheritance :/ + override var wrappedValue: Wrapped { + get { super.wrappedValue } + set(v) { super.wrappedValue = v } + } +} + +@propertyWrapper class ReasonedObservedObject<Wrapped : ReasonedObservableObject> : CustomObservedObject<Wrapped> { + //same here + override var wrappedValue: Wrapped { + get { super.wrappedValue } + set(v) { super.wrappedValue = v } + } +} diff --git a/enzevalos_iphone/SwiftUI/Inbox/categorized/Helpers/PersistentDataController.swift b/enzevalos_iphone/SwiftUI/Inbox/categorized/Helpers/PersistentDataController.swift new file mode 100644 index 0000000000000000000000000000000000000000..4debe269494c2d32285a671b0f8817f7167e030b --- /dev/null +++ b/enzevalos_iphone/SwiftUI/Inbox/categorized/Helpers/PersistentDataController.swift @@ -0,0 +1,55 @@ +// +// PersistentDataController.swift +// enzevalos_iphone +// +// Created by hanneh00 on 31.01.21. +// Copyright © 2021 fu-berlin. All rights reserved. +// +// https://www.iosapptemplates.com/blog/ios-development/data-persistence-ios-swift + + +import Foundation + +//TODO this is not working yet + +class PersistentDataController { + enum Key: String, CaseIterable { + case classicActive, sort + } + + let userDefaults: UserDefaults + // MARK: - Lifecycle + init(userDefaults: UserDefaults = .standard) { + self.userDefaults = userDefaults + } + // MARK: - API + + var inboxSettings:(Bool?,SortCategoriesBy?){ + get{ + return getInboxSettings() + } + set(new){ + setInboxSettings(isClassicActive: new.0,sortBy: new.1) + } + } + + func setInboxSettings(isClassicActive: Bool?, sortBy: SortCategoriesBy?) { + saveValue(forKey: .classicActive, value: isClassicActive) + saveValue(forKey: .sort, value: sortBy) + } + + func getInboxSettings() -> (isClassicActive: Bool?, sortBy: SortCategoriesBy?) { + let isClassicActive: Bool? = readValue(forKey: .classicActive) + let sortBy: SortCategoriesBy? = readValue(forKey: .sort) + return (isClassicActive, sortBy) + } + + + // MARK: - Private + private func saveValue(forKey key: Key, value: Any?) { + userDefaults.set(value, forKey: key.rawValue) + } + private func readValue<T>(forKey key: Key) -> T? { + return userDefaults.value(forKey: key.rawValue) as? T + } +} diff --git a/enzevalos_iphone/SwiftUI/Inbox/categorized/Helpers/Search/Definitions.swift b/enzevalos_iphone/SwiftUI/Inbox/categorized/Helpers/Search/Definitions.swift new file mode 100644 index 0000000000000000000000000000000000000000..18ff4e859c0d0d0d46001a2cf43af8df443154a9 --- /dev/null +++ b/enzevalos_iphone/SwiftUI/Inbox/categorized/Helpers/Search/Definitions.swift @@ -0,0 +1,42 @@ +// +// Definitions.swift +// enzevalos_iphone +// +// Created by hanneh00 on 09.01.21. +// Copyright © 2021 fu-berlin. All rights reserved. +// + +import Foundation + +struct Search{ + var text = "" + var fields:[SearchFilter] = SearchFilterCollections.Standarts + var activeIndex:Int = 0 + var now = false + var isExpanded = false + var activeField:SearchFilter { + fields[activeIndex] + } + func filter(eMail:eMail)->Bool{ + if now == false{ + return true + } + return self.activeField.filter(eMail,self.text) + } +} + +struct SearchFilter : Equatable{ + static func == (lhs: SearchFilter, rhs: SearchFilter) -> Bool { + lhs.text==rhs.text + } + + typealias Filter = (eMail,String)->Bool + var text:String + var filter: Filter +} + + +//Depricated +protocol SearchTypes:CaseIterable,Hashable where AllCases:RandomAccessCollection { + var text:String {get} +} diff --git a/enzevalos_iphone/SwiftUI/Inbox/categorized/Helpers/Search/SearchFilterCollections.swift b/enzevalos_iphone/SwiftUI/Inbox/categorized/Helpers/Search/SearchFilterCollections.swift new file mode 100644 index 0000000000000000000000000000000000000000..344036292eeb86d2a1ae9a99e643cdc6c9e6cc71 --- /dev/null +++ b/enzevalos_iphone/SwiftUI/Inbox/categorized/Helpers/Search/SearchFilterCollections.swift @@ -0,0 +1,59 @@ +// +// SearchFilterCollections.swift +// enzevalos_iphone +// +// Created by hanneh00 on 09.01.21. +// Copyright © 2021 fu-berlin. All rights reserved. +// + +import Foundation + + +class SearchFilterCollections{ + + + +//MARK: - Standart Search filters : Sender, Subject, Body, All +static let Standarts:[SearchFilter] = [ + SearchFilter( + text: NSLocalizedString("All", comment: ""), + filter: SearchFilters.AllStandarts + ), + SearchFilter( + text: NSLocalizedString("Subject", comment: ""), + filter: SearchFilters.Subject + ), + SearchFilter( + text: NSLocalizedString("Body", comment: ""), + filter: SearchFilters.Body + ), + SearchFilter( + text: NSLocalizedString("Sender", comment: ""), + filter: SearchFilters.Sender + ) + ] + + + +//MARK: - Filters for Files +static let Files:[SearchFilter] = [ + SearchFilter( + text: NSLocalizedString("Filename", comment: ""), + filter: SearchFilters.Filename + ), + SearchFilter( + text: NSLocalizedString("Subject", comment: ""), + filter: SearchFilters.Subject + ), + SearchFilter( + text: NSLocalizedString("Body", comment: ""), + filter: SearchFilters.Body + ), + SearchFilter( + text: NSLocalizedString("Sender", comment: ""), + filter: SearchFilters.Sender + ) + ] + + +} diff --git a/enzevalos_iphone/SwiftUI/Inbox/categorized/Helpers/Search/SearchFilters.swift b/enzevalos_iphone/SwiftUI/Inbox/categorized/Helpers/Search/SearchFilters.swift new file mode 100644 index 0000000000000000000000000000000000000000..9babd737a660e8c4e3ab07b740d0f2102042e1ba --- /dev/null +++ b/enzevalos_iphone/SwiftUI/Inbox/categorized/Helpers/Search/SearchFilters.swift @@ -0,0 +1,40 @@ +// +// SearchFilters.swift +// enzevalos_iphone +// +// Created by hanneh00 on 09.01.21. +// Copyright © 2021 fu-berlin. All rights reserved. +// + +import Foundation + +///some possibilities to filter for specific mails +class SearchFilters { + +///search in sender names +static let Sender:SearchFilter.Filter={eMail, searchText in + containsSearchTerms(content: eMail.sender.displayname, searchText: searchText) || + containsSearchTerms(content: eMail.sender.email, searchText: searchText) || + eMail.addresses.filter({containsSearchTerms(content: $0.mailAddress, searchText: searchText)}).count > 0 +} + +///search the subjects +static let Subject:SearchFilter.Filter={eMail, searchText in containsSearchTerms(content: eMail.subject, searchText: searchText)} + +static let Body:SearchFilter.Filter={eMail, searchText in containsSearchTerms(content: eMail.subject, searchText: searchText)} + +static let AllStandarts:SearchFilter.Filter={eMail, searchText in + Sender(eMail,searchText) || Subject(eMail,searchText) || Body(eMail,searchText) +} + +///search in the attachments +static let Filename:SearchFilter.Filter={eMail, searchText in + for attachment in eMail.displayAttachments { + if attachment.myName.contains(searchText){ + return true + } + } + return false +} + +} diff --git a/enzevalos_iphone/SwiftUI/Inbox/categorized/Helpers/UI/CategoryView.swift b/enzevalos_iphone/SwiftUI/Inbox/categorized/Helpers/UI/CategoryView.swift new file mode 100644 index 0000000000000000000000000000000000000000..d805336c84181c8f5b61a68d75f052485bcdc263 --- /dev/null +++ b/enzevalos_iphone/SwiftUI/Inbox/categorized/Helpers/UI/CategoryView.swift @@ -0,0 +1,87 @@ +// +// SampleCategoryV2.swift +// enzevalos_iphone +// +// Created by hanneh00 on 17.01.21. +// Copyright © 2021 fu-berlin. All rights reserved. +// + +import SwiftUI +import TagNavVi + +let biggerPreviewSize:CGFloat = 400 , smallerPreviewSize:CGFloat = 200 + +///this manages to create the logic a category needs with aut other devs needing to worry. just use this struct for all your categories +struct CategoryView<UI:View>:View,CategoryViewToAnyView,Identifiable{ + + typealias Content = (CategoryViewModel)->UI + + init( + name : String , + description : String = "", + icon : Image = Image(systemName: "envelope.fill"), + isBigger : Bool = false, + isResizable : Bool = true, + searchFields : [SearchFilter] = SearchFilterCollections.Standarts, + filter : @escaping MailFilter, + andPredicate : NSPredicate = NSPredicate(value: true), + _ content : @escaping Content + ) { + self.id = name + self.name = name + self.description = description + self.icon = icon + self.content = content + self.categoryVM = CategoryViewModelProvider.s.createViewModel( + for : name, + with : filter, + and : andPredicate, + data : CategoryData( + icon : icon, + description : description, + size : isBigger ? biggerPreviewSize : smallerPreviewSize, + isResizable : isResizable, + searchFields : searchFields + ) + ) + } + + @ObservedObject + var categoryVM : CategoryViewModel + var id : CategoryIDType + var name : String + private var description : String + private var icon : Image + private var content : Content + + var body: some View { + CategoryViewInner<UI>( + category: categoryVM + ){ + content(categoryVM) + } + .home(0) + } + + + + + func toAnyView()->CategoryView<AnyView>{ + CategoryView<AnyView>(vm: categoryVM){_ in + AnyView(content(categoryVM)) + } + } + private init( vm : CategoryViewModel, _ content : @escaping Content) { + self.id = vm.name + self.name = vm.name + self.description = vm.constData.description + self.icon = vm.constData.icon + self.content = content + self.categoryVM = vm + } + +} + +protocol CategoryViewToAnyView { + func toAnyView()->CategoryView<AnyView> +} diff --git a/enzevalos_iphone/SwiftUI/Inbox/categorized/Helpers/UI/CategoryViewInner.swift b/enzevalos_iphone/SwiftUI/Inbox/categorized/Helpers/UI/CategoryViewInner.swift new file mode 100644 index 0000000000000000000000000000000000000000..0b142b55991acd15176179c7501880b8a9652713 --- /dev/null +++ b/enzevalos_iphone/SwiftUI/Inbox/categorized/Helpers/UI/CategoryViewInner.swift @@ -0,0 +1,161 @@ +// +// CategoryView.swift +// enzevalos_iphone +// +// Created by hanneh00 on 01.01.21. +// Copyright © 2021 fu-berlin. All rights reserved. +// + +import SwiftUI +import TagNavVi + +/** +A View that should be used for the Categories + +- parameters + - category: the categorie that this view draws + - description: a short discription for the ChatUser + - icon: an Image that describes this kind of eMail +*/ +struct CategoryViewInner<Content:View>: View { + + @ObservedObject var category : CategoryViewModel + + var content : ()->Content + + var body: some View { + NavigatorCard( + icon : category.constData.icon, + headline : category.name, + timestmp : category.lastMailTimestamp, + isResizable : category.constData.isResizable, + size : category.Ssize, + onOpen : {category.isActive.toggle()}, + isOpen : category.isActive + ){ + content() + }.padding(.horizontal, 15).padding(.top, 10) + .animation(.easeInOut) + } +} + + +/** + A View that should be used for the Categories + + Providing a navigationStack usable by pushing views via @EnvironmentObject var nav: NavigationStack + nav.advance(newView()) + +- parameters + - headline: the title + - description: a short discription for the ChatUser + - icon: an Image that describes this kind of eMail + - tmestamp: a Date object of the last usage + - isResizable: a Bool whether to use a resizable Card + - size : a Binding of the current Size of the content + */ +struct NavigatorCard<Content:View> : View { + + init( + icon : Image, + headline : String, + timestmp : Date? = nil, + isResizable:Bool = true, + size:Binding<CGFloat?> = Binding.constant(nil), + onOpen : @escaping ()->() = {}, + isOpen : Bool=false, + @ViewBuilder _ content: @escaping () -> Content + ) { + self.icon=icon + self.headline=headline + self.timestmp=timestmp + self.isResizable=isResizable + self.size=size + self.onOpen=onOpen + self.isOpen=isOpen + self.content = content + } + + var icon : Image + var headline : String + var timestmp : Date? + + var isResizable:Bool = true + + var size:Binding<CGFloat?> = Binding.constant(nil) + private var _size : CGFloat? {size.wrappedValue} + + var onOpen : ()->() = {} + var isOpen : Bool = false + + var content: ()->Content + + + var body: some View { + if _size != nil && isResizable { + DragResizableCard(shadowElevation: 15, padding:0, height:size){ + inner + .padding(.bottom,5) + } + }else{ + if _size != nil { + Card(padding:0){ + inner + } + }else{ + Card(padding:0){ + inner + } + .padding(.bottom,-20) + .edgesIgnoringSafeArea(.bottom) + } + } + + } + + private var inner:some View{ + TagNavigationView(tag: 1){ + content() + }.withTitle(withHomeButton: true){ + HStack{ + icon + .sizeTo(25) + .padding(.trailing,5) + headline.font(.title3) + Spacer() + Text(timestmp?.timeAgoText() ?? "") + .font(.system(size: 15)) + .lineLimit(2) + .frame(width: 70) + .foregroundColor(.gray) + Button(action: onOpen){ + Image(systemName: isOpen ? + "arrow.down.right.and.arrow.up.left" : + "arrow.up.left.and.arrow.down.right" + ) + } + } + } + .frame(height: _size != nil ? (_size ?? 0) + 50 : nil) + .padding() + //TODO disable scrolling + .gesture(DragGesture()) + } +} + +struct CategoryView2_Previews: PreviewProvider { + static var previews: some View { + Group{ + ScrollView{ + VStack{ + StatefulPreviewWrapper(200){value in + NavigatorCard(icon: Image(systemName: "envelope.fill"), headline: "Test-Kategorie",size: value){ + Text("mois") + }.padding() + } + Spacer() + } + } + } + } +} diff --git a/enzevalos_iphone/SwiftUI/Inbox/categorized/Helpers/UI/SupportingViews/CategoryOverViewBar.swift b/enzevalos_iphone/SwiftUI/Inbox/categorized/Helpers/UI/SupportingViews/CategoryOverViewBar.swift new file mode 100644 index 0000000000000000000000000000000000000000..0bf73bf436a7cf0eb96b3be111d7cdd5c2390a0a --- /dev/null +++ b/enzevalos_iphone/SwiftUI/Inbox/categorized/Helpers/UI/SupportingViews/CategoryOverViewBar.swift @@ -0,0 +1,67 @@ +// +// CategoryWrap.swift +// enzevalos_iphone +// +// Created by hanneh00 on 10.01.21. +// Copyright © 2021 fu-berlin. All rights reserved. +// + +import SwiftUI +import TagNavVi + + +/** +The Overview of the Available categories + +- Parameters: + - controller: The InboxModelcontroller used to set and retrieve the active categoriy aswell as retrieving all categories +*/ +struct CategoryOverviewBar: View { + + @ObservedObject var controller: HomeModel + + var iconSize : CGFloat? + var usesWrap : Bool = false + var switchesToHStackIfACategoryIsActive:Bool = true + + let innerPadding:CGFloat = 10 + + private var allCats : [CategoryViewModel] {controller.allCatsSorted} + var body: some View { + if usesWrap && (controller.isHome || !switchesToHStackIfACategoryIsActive){ + DynamicSmartWrap(items: allCats , viewGenerator: ui).padding(.horizontal) + }else{ + ScrollView(.horizontal, showsIndicators : false){ + HStack{ + Spacer().frame(width: 20) + ForEach(allCats){catModel in + ui(catModel).padding(.vertical,innerPadding) + } + Spacer().frame(width: 15) + } + }.padding(.vertical, -15) + } + } + + ///to generate a single category entry + private func ui(_ categoryVM : CategoryViewModel)-> some View { + categoryButton(categoryVM) + } + private func categoryButton(_ categoryVM : CategoryViewModel) -> some View { + var isActive : Bool{ + return categoryVM.isActive + } + func switchActive(){ + // categoryVM.isActive.toggle() + controller.setActiveCategory(categoryVM) + } + return Button( + action: switchActive + ){ + categoryVM.constData.icon + .sizeTo(iconSize) + .padding(innerPadding) + .background(isActive ? Circle().fill(Color.gray).opacity(0.2).padding(-5) : nil) + } + } +} diff --git a/enzevalos_iphone/SwiftUI/Inbox/categorized/Helpers/UI/SupportingViews/MailOptionsView.swift b/enzevalos_iphone/SwiftUI/Inbox/categorized/Helpers/UI/SupportingViews/MailOptionsView.swift new file mode 100644 index 0000000000000000000000000000000000000000..b7d6ce8a389b67f8f94d00dac5f1a28ef63a04cf --- /dev/null +++ b/enzevalos_iphone/SwiftUI/Inbox/categorized/Helpers/UI/SupportingViews/MailOptionsView.swift @@ -0,0 +1,147 @@ +// +// MailOptionsView.swift +// enzevalos_iphone +// +// Created by hanneh00 on 08.02.21. +// Copyright © 2021 fu-berlin. All rights reserved. +// + +import SwiftUI + +///the mechanics for the extension at the bottom +fileprivate struct MailOptionsWrapper<Content:View> : View { + let mail : eMail + let extraOptions : [Button<Label<Image,Text>>] + let content: ()->Content + + @State var showPossibleCategories : Bool = false + + var body: some View { + ZStack{ + NavigationLink(destination: dst, isActive: $opens) { EmptyView() } + content() + .contextMenu{menu} + } + } + + @ViewBuilder private var menu : some View { + Button(action: { + mail.isRead.toggle() + }){ + Label(NSLocalizedString("Mark as unread", comment: ""), systemImage: "envelope.badge") + } + Button(action: { + setDst(ReadMainView(mail: mail)) + open() + }){ + Text(NSLocalizedString("Info", comment: "")) + Image(systemName: "info.circle.fill") + } + + Divider() + + if (!mail.isSingle){ + Button(action: { + newMail(to: mail.ccs) + }){ + Text(NSLocalizedString("Reply all", comment: "")) + Image(systemName: "arrowshape.turn.up.left.2.fill") + } + } + Button(action: { + newMail(to: [mail.fromAddress]) + }){ + Text(NSLocalizedString("Reply", comment: "")) + Image(systemName: "arrowshape.turn.up.left.fill") + } + Button(action: { + newMail(to: []) + }){ + Text(NSLocalizedString("Forward", comment: "")) + Image(systemName: "arrowshape.turn.up.right.fill") + } + + Divider() + + Button(action: { + showPossibleCategories.toggle() + //TODO: sadly this instantly collapses (not only this but every press on a contextMenu makes it collapse) + }){ + Text(NSLocalizedString("Open in", comment: "")) + Image(systemName: showPossibleCategories ? "chevron.up" : "chevron.down") + } + + if showPossibleCategories { + ForEach(CategoryViewModelProvider.s.viewModelList.array){ vm in + if vm.filter(mail) >= .canOpen && !vm.isActive { + Button(action: { + vm.isActive = true + //TODO: open this specific mail or at least highlight it + }){ + Text(vm.name) + vm.constData.icon + } + } + } + } + + + } + + func open() { opens = true } + @State private var opens : Bool = false + @State private var dst : AnyView = AnyView() + func newMail(to receivers : [AddressRecord?]){ + var receivers = receivers.compactMap{$0} + let to = receivers.removeFirst() + setDst( + ComposeView(model: + ComposeModel(model: + SelectReceiverModel( + to: to, + ccs: receivers + ) + ) + ) + ) + open() + } + private func setDst<V:View>(_ dst : V) { self.dst = AnyView(dst) } + +} + + + +// MARK: - PREVIEW +struct ContentView: View { + @State var showsAlert = false + var body: some View { + VStack { + Text("Man stelle sich vor das hier ist eine eMail") + } + //.mailOptions(mail: mail) + } +} + +struct ContentView_Previews: PreviewProvider { + static var previews: some View { + ContentView() + } +} + +// MARK: - extensions +extension View { + func mailOptions(mail: eMail, extraOptions: [Button<Label<Image,Text>>] = [] ) -> some View { + MailOptionsWrapper(mail: mail, extraOptions: extraOptions , content:{self}) + } +} + +extension View { + ///if the user holds a mail some mail options pop up (like reply, info etc.) + func capsuledMailOptions(mail: eMail, extraOptions: [Button<Label<Image,Text>>] = [] ) -> some View { + self.padding(10) + .background(Capsule().fill(Color.gray.opacity(0.0001))) + .mailOptions(mail: mail, extraOptions: extraOptions) + .padding(-10) + } +} diff --git a/enzevalos_iphone/SwiftUI/Inbox/categorized/Helpers/UI/SupportingViews/Searchview_2.swift b/enzevalos_iphone/SwiftUI/Inbox/categorized/Helpers/UI/SupportingViews/Searchview_2.swift new file mode 100644 index 0000000000000000000000000000000000000000..c2d34b83d72fdc7c802366ba2201e5021cef696c --- /dev/null +++ b/enzevalos_iphone/SwiftUI/Inbox/categorized/Helpers/UI/SupportingViews/Searchview_2.swift @@ -0,0 +1,73 @@ +// +// Searchview_2.swift +// enzevalos_iphone +// +// Created by hanneh00 on 09.01.21. +// Copyright © 2021 fu-berlin. All rights reserved. +// + +import SwiftUI + +/** + A SearchView for mails with a search segmented Picker to choose the search area. + Open Problems: Deplay the search, s.t. we do not start a search after each input. + + */ +struct CustomizableSearchView: View { + + @Binding var search:Search + + var body: some View { + VStack{ + HStack { + searchFieldView + if search.isExpanded { + Button("Cancel") { + UIApplication.shared.endEditing(true) + self.search.text = "" + self.search.isExpanded = false + self.search.now = false + } + .foregroundColor(Color(.systemBlue)) + } + } + if search.isExpanded { + searchSegment + } + } + .padding(.horizontal) + } + + var searchFieldView: some View { + HStack { + Image(systemName: "magnifyingglass") + TextField(NSLocalizedString("Searchbar.Title", comment: "Search"), text: $search.text, onEditingChanged: { isEditing in + self.search.isExpanded = true + }, onCommit: { + self.search.now = true + }) + .foregroundColor(.primary) + Button(action: { + self.search.text = "" + }) { + Image(systemName: "xmark.circle.fill").opacity(search.text == "" ? 0 : 1) + } + } + .padding(EdgeInsets(top: 8, leading: 6, bottom: 8, trailing: 6)) + .foregroundColor(.secondary) + .background(Color(.secondarySystemBackground)) + .cornerRadius(10.0) + } + + var searchSegment: some View { + Picker(selection: $search.activeIndex, label: EmptyView()) { + ForEach(0 ..< search.fields.count) { index in + Text(search.fields[safe: index]?.text ?? "").tag(index) + .onTapGesture { + self.search.activeIndex = index + } + } + } + .pickerStyle(SegmentedPickerStyle()) + } +} diff --git a/enzevalos_iphone/SwiftUI/Inbox/categorized/Helpers/UI/SupportingViews/TypePicker.swift b/enzevalos_iphone/SwiftUI/Inbox/categorized/Helpers/UI/SupportingViews/TypePicker.swift new file mode 100644 index 0000000000000000000000000000000000000000..759ec384688bb7ce971c2f3ecb5e5746a7faca01 --- /dev/null +++ b/enzevalos_iphone/SwiftUI/Inbox/categorized/Helpers/UI/SupportingViews/TypePicker.swift @@ -0,0 +1,45 @@ +// +// TypePicker.swift +// enzevalos_iphone +// +// Created by hanneh00 on 22.01.21. +// Copyright © 2021 fu-berlin. All rights reserved. +// + +import SwiftUI + +/// a row of possible file extensions the user can choose and we can bind to +/// currently mostly used by the filescategory +struct TypePicker : View{ + @Binding var selectedTypes : [String] + + var selectableTypes : [String] = ["jpg","png","pdf","txt","mp4"] + + var body: some View{ + ScrollView(.horizontal){HStack{ + ForEach(0..<selectableTypes.count){i -> ToggleableTag<Text> in + let boolToArrayBinder = Binding<Bool>( + get: {selectedTypes.contains(selectableTypes[i])}, + set: { + if $0 { + selectedTypes.append(selectableTypes[i]) + }else{ + selectedTypes.removeAll(where: {$0==selectableTypes[i]}) + } + } + ) + return ToggleableTag(isActive: boolToArrayBinder){ + Text(selectableTypes[i]) + } + } + }.padding(.horizontal,20)} + } +} + +struct TypePicker_Previews: PreviewProvider { + static var previews: some View { + StatefulPreviewWrapper(["Tag 1", "Tag 2"]){ + TypePicker(selectedTypes: $0) + } + } +} diff --git a/enzevalos_iphone/SwiftUI/Inbox/categorized/Helpers/UI/SupportingViews/UnreadCountIcon.swift b/enzevalos_iphone/SwiftUI/Inbox/categorized/Helpers/UI/SupportingViews/UnreadCountIcon.swift new file mode 100644 index 0000000000000000000000000000000000000000..0cdff4f9b49e761349cdf10794480bfc0dbcaf4d --- /dev/null +++ b/enzevalos_iphone/SwiftUI/Inbox/categorized/Helpers/UI/SupportingViews/UnreadCountIcon.swift @@ -0,0 +1,110 @@ +// +// UnreadCountIcon.swift +// enzevalos_iphone +// +// Created by hanneh00 on 09.01.21. +// Copyright © 2021 fu-berlin. All rights reserved. +// + +import SwiftUI + +/// can be used by categories to display the amount of unread emails +struct UnreadCountIcon: View { + + var unreadCount = 0 + + ///whether it only shows the amound of unread mails or also the total amount of mails in this category + var totalCount:Int? + + var size:CGFloat = 10 + + var withPlus = false + + var onPress = {} + + var body: some View { + FloatingActionButton( + radius:5, + elevation:0, + onShortPress: onPress + ){ + VStack{ + Text(withPlus ? "+" : "" + "\(unreadCount)").font(.system(size: 18)) + if let tot=totalCount{ + Text("/ \(tot)").font(.system(size: 10)).opacity(0.8) + } + }.frame(width: size, height: size) + } + } +} + +struct UnreadCountIcon_Previews: PreviewProvider { + static var previews: some View { + VStack{ + Spacer() + UnreadCountIcon(unreadCount: 10, totalCount: 15, size: 29) + Spacer() + Rectangle().frame(width: 100, height: 100, alignment: .center) + .withUnreadCounter(5) + Spacer() + } + } +} + + +///applying the unreadcounter to a view +extension View { + func withUnreadCounter( + _ unreadCount : Int, + totalCount:Int? = nil, + size:CGFloat = 10, + offset:CGSize = CGSize(width:10,height:10), + withPlus:Bool = false, + + onPress:@escaping ()->Void = {} + ) -> some View { + self.modifier(WithUnreadCounter( + unreadCount: unreadCount, totalCount: totalCount, size: size, withPlus: withPlus, offset: offset, onPress: onPress + )) + } + func withUnreadCounter( + _ amount : (Int,Int), + size:CGFloat = 35, + offset:CGSize = CGSize(width:5,height:8), + withPlus:Bool = false, + withoutTotal:Bool = false, + + onPress:@escaping ()->Void = {} + ) -> some View { + self.withUnreadCounter( + amount.0, totalCount: withoutTotal ? nil : amount.1, size: size, offset: offset, withPlus: withPlus, onPress: onPress + ) + } +} + + +///this is kinda of an 'umweg' , but i wanted to try the ViewModifier +fileprivate struct WithUnreadCounter: ViewModifier { + + var unreadCount = 0 + var totalCount:Int? + + var size:CGFloat = 10 + + var withPlus = false + + var offset:CGSize = CGSize(width:10,height:10) + + var onPress = {} + + func body(content: Content) -> some View { + ZStack(alignment: .bottomTrailing){ + content + UnreadCountIcon( + unreadCount: unreadCount, totalCount: totalCount, size: size, withPlus: withPlus, onPress: onPress + ) + .offset(offset) + } + + } +} diff --git a/enzevalos_iphone/SwiftUI/Inbox/categorized/Helpers/UI/SupportingViews/newMailRow.swift b/enzevalos_iphone/SwiftUI/Inbox/categorized/Helpers/UI/SupportingViews/newMailRow.swift new file mode 100644 index 0000000000000000000000000000000000000000..0d1ddcf6631df19931e0d663d254cd7787544e94 --- /dev/null +++ b/enzevalos_iphone/SwiftUI/Inbox/categorized/Helpers/UI/SupportingViews/newMailRow.swift @@ -0,0 +1,69 @@ +// +// SimpleMailRow.swift +// enzevalos_iphone +// +// Created by hanneh00 on 05.02.21. +// Copyright © 2021 fu-berlin. All rights reserved. +// + +import SwiftUI + +///just a simple mail row displaying the title and first lines of the content next to a small sender-usericon +struct SimpleMailRow: View { + + let mail : eMail + + var body: some View { + HStack(alignment: .center) { + icon + VStack { + HStack{ + Text(mail.subject) + .font(.caption) + .lineLimit(1) + Spacer() + Text(mail.date.timeAgoText()) + .font(.caption).foregroundColor(.gray) + } + nameView + } + } + } + + private var icon: some View{ + mail.sender.myImage + .sizeTo(30) + .openOnTap(tag:1){ + ContactView( + contact: mail.sender, + fromMail: mail, + derivedFromKey: true + ) + } + + } + + private var nameView: some View { + Text(mail.sender.name) + .frame(maxWidth: 300, alignment: .leading) + .lineLimit(1) + .font(.caption) + } +} + + +struct OptionsMailRow : View { + + let mail : eMail + + var body : some View { + SimpleMailRow(mail: mail) + .capsuledMailOptions(mail: mail) + } +} + +struct SimpleMailRow_Previews: PreviewProvider { + static var previews: some View { + Text("T0D0") + } +} diff --git a/enzevalos_iphone/SwiftUI/Inbox/categorized/Helpers/ViewModels/CategoryViewModel.swift b/enzevalos_iphone/SwiftUI/Inbox/categorized/Helpers/ViewModels/CategoryViewModel.swift new file mode 100644 index 0000000000000000000000000000000000000000..ea6ecc421548cdae7d8c6e4e07f660cd696140cb --- /dev/null +++ b/enzevalos_iphone/SwiftUI/Inbox/categorized/Helpers/ViewModels/CategoryViewModel.swift @@ -0,0 +1,172 @@ +// +// CategoryViewModel.swift +// enzevalos_iphone +// +// Created by hanneh00 on 17.01.21. +// Copyright © 2021 fu-berlin. All rights reserved. +// + +import SwiftUI +import Combine +import CoreData + +/** + The ViewModel every category gets provided + */ +class CategoryViewModel : ReasonedObservableObject,Identifiable,Equatable{ + + + let id : String + let name:String + + // MARK: - Accessors + + @PPublished(.custom("For HomeModel")) var isActive = false + + @Published var constData = CategoryData() + + var Ssize : Binding<CGFloat?> { Binding( + get:{ + self.isActive ? nil : self.constData.size + }, + set:{ + if let newSize = $0{ + self.constData.size = newSize + }else{ + self.isActive = false + } + } + )} + var size : CGFloat? {Ssize.wrappedValue} + + ///- returns: all the Mails in this category in this folder matching the current search + var allMails : [eMail]{ + let mailsInFolder = mailsInCategoryController.fetchedObjects ?? [] + return mailsInFolder.filter(search.filter) + } + + ///- returns: all the .shouldOpen Mails in this category in this folder matching the current search + var shouldOpenMails : [eMail] { + mailsInCategoryController.fetchedObjects?.filter(search.filter) ?? [] + } + + ///- returns: the unread and the total amount of mail in this category as a Tuple + var mailAmount : (Int,Int) { + return(allMails.filter({mail in mail.isRead}).count, allMails.count) + } + + ///- returns: the timestamp when this categor was last active + var lastMailTimestamp : Date? { + guard shouldOpenMails.count > 0 else{ + return nil + } + return shouldOpenMails[0].date + } + +// MARK: - helper/private vars + let filter:MailFilter + let andPred:NSPredicate + private let dataProvider = ProcessInfo.processInfo.environment["XCODE_RUNNING_FOR_PREVIEWS"] != "1" ? PersistentDataProvider.dataProvider : PersistentDataProvider.proxyPersistentDataProvider + + @Published private var mailsInCategoryController : NSFetchedResultsController<MailRecord> = PersistentDataProvider.dataProvider.generateNoMailsController() + @Published private var importantMailsInCategoryController : NSFetchedResultsController<MailRecord> = PersistentDataProvider.dataProvider.generateNoMailsController() + + + @Published var search = Search() + private var folder = MailHandler.INBOX + +// MARK: - helper functions + func hasThisData( + name : String, + folder : String, + consts : CategoryData + )->Bool{ + self.name==name && self.folder==folder && self.constData==consts + } + + + func updateSearch(_ search : Search){ + self.search=search + objectWillChange.send() + } + + func updateFolder(_ folderpath : String){ + mailsInCategoryController = dataProvider.generateFetchedMailsInFolderAndCategoryResultsController( + folderpath: folderpath, + category: self.name, + andPredicate: andPred + ) + mailsInCategoryController = dataProvider.generateFetchedMailsInFolderAndCategoryResultsController( + folderpath: folderpath, + category: self.name, + andPredicate: andPred, + important: true + ) + refresh() + } + + ///refresh the database to accomondate for new mails / new category + func deepRefresh(){ + do { + try self.dataProvider.setCategoryOpenability(category:name, filter: self.filter) + }catch{ + //mmm handle error + } + refresh() + } + + func refresh(){ + //refetch + try? mailsInCategoryController.performFetch() + objectWillChange.send() + } + + + + //MARK: - initialization + + init( + name : String, + filter : @escaping MailFilter, + andPredicate: NSPredicate + ) { + let standardFolder = MailHandler.INBOX + + self.id = name + self.name = name + self.filter = filter + self.andPred = andPredicate + + self.mailsInCategoryController = + dataProvider.generateFetchedMailsInFolderAndCategoryResultsController( + folderpath: standardFolder, + category: self.name, + andPredicate: andPredicate + ) + + self.importantMailsInCategoryController = + dataProvider.generateFetchedMailsInFolderAndCategoryResultsController( + folderpath: standardFolder, + category: self.name, + andPredicate: andPredicate, + important: true + ) + + + super.init() + + deepRefresh() + } + static func == (lhs: CategoryViewModel, rhs: CategoryViewModel) -> Bool { + lhs.id == rhs.id + } +} + + +struct CategoryData:Equatable{ + var icon : Image = Image(systemName: "envelope.fill") + var description : String = "" + var size : CGFloat = smallerPreviewSize + var isResizable : Bool = true + var searchFields: [SearchFilter] = SearchFilterCollections.Standarts +} diff --git a/enzevalos_iphone/SwiftUI/Inbox/categorized/Helpers/ViewModels/HomeModel.swift b/enzevalos_iphone/SwiftUI/Inbox/categorized/Helpers/ViewModels/HomeModel.swift new file mode 100644 index 0000000000000000000000000000000000000000..76bab250799a3465b3edc18d97d96fa95f021891 --- /dev/null +++ b/enzevalos_iphone/SwiftUI/Inbox/categorized/Helpers/ViewModels/HomeModel.swift @@ -0,0 +1,197 @@ +// +// HomeModel.swift +// enzevalos_iphone +// +// Created by hanneh00 on 17.01.21. +// Copyright © 2021 fu-berlin. All rights reserved. +// + +import SwiftUI +import Combine + +class HomeModel : ObservableObject{ + + + + ///how far its scrolled (currently not in use + @Published var scrollAmount:CGFloat = 0 + + ///whether the settings are opened + @Published var settingsEnabled:Bool = false + + ///if the user whiches to show all mail in an old-fashioned inbox + @Published var displayClassicalInbox : Bool = false + + /** + Other API calls: + - allCats : (better: allCatsSorted) all the categories used (their ViewModels to be precise) + - activeCategory : the currently opened Category + - search : the current search state including filters, searchtext etc + - folderpath : the current folderpath (can be used to implement tags later on) + - etc. + */ + + + + + + + + + init() { + _ = observe() + let settings = persistent.getInboxSettings() + self.displayClassicalInbox = settings.isClassicActive ?? false + self.sortBy = settings.sortBy ?? .recent + } + + + //MARK: - persistent settings and publishing Coategory state + //TODO this is not working yet + + + deinit {saveSettings()} + private var persistent = PersistentDataController() + private func saveSettings(){ + persistent.inboxSettings = (self.displayClassicalInbox, self.sortBy) + } + + private var cancellable:(AnyCancellable,AnyCancellable) = (AnyCancellable({}),AnyCancellable({})) + func observe()->HomeModel{ + + func update(_ reason : PublishedReason? ){ + if reason == .custom("For HomeModel") { self.objectWillChange.send() } + } + + cancellable = ( vmProv.objectWillChangeR.sink{update($0)} , + vmProv.objectWillChange.sink{update(nil)} ) + return self + } + + + //MARK: - getting the categories + + @ObservedObject private var vmProv = CategoryViewModelProvider.s.observe() + var allCategorieModels : ObservableArray<CategoryViewModel> {vmProv.viewModelList} + + var allCats : [CategoryViewModel] { + return allCategorieModels.array + } + + // MARK: - the Search state + var search : Search{ + get{ + _search.fields=activeCategory?.constData.searchFields ?? SearchFilterCollections.Standarts + return _search + } + set(newSearch){ + _search=newSearch + updateSearch() + objectWillChange.send() + } + } + + private var _search = Search(fields: SearchFilterCollections.Standarts) + private func updateSearch(){ + allCats.forEach{category in + category.updateSearch(self._search) + } + } + + // MARK: - current folder + @Published var folderpath = MailHandler.INBOX + func updateFolder(_ folderpath : String){ + self.folderpath=folderpath + allCats.forEach{category in + category.updateFolder(folderpath) + } + } + + //MARK: - the active Category + + ///nil if no category is maximized otherwise the maximized category + var activeCategory : CategoryViewModel?{ + get{ + //okay this only takes <.2 milliseconds + activeOne + } + set(newCat){ + setActiveCategory_(newCat) + objectWillChange.send() + } + } + func setActiveCategory(_ cat : CategoryViewModel){ + activeCategory = cat.isActive ? nil : cat + } + private func setActiveCategory_(_ newCat:CategoryViewModel?){ + allCats.forEach{category in + category.isActive = false + } + newCat?.isActive = true + } + private var activeOne : CategoryViewModel?{ + for cat in allCats{ + if cat.isActive{ + return cat + } + } + return nil + } + + var isHome:Bool{activeCategory == nil} + + func goBack(){ + if isHome{ + scrollAmount = 0 //this sadly doesnt do anything + } + activeCategory = nil + } + + + //MARK: - Sorting the Categories + + @Published var sortBy : SortCategoriesBy = .recent + + var allCatsSorted : [CategoryViewModel] { + return sortCategories(allCats) + } + func sortViews(_ views : [CategoryView<AnyView>])->[CategoryView<AnyView>]{ + views.sorted{catSorter($0.categoryVM, $1.categoryVM)} + } + func sortCategories(_ cats : [CategoryViewModel])->[CategoryViewModel]{ + cats.sorted(by: catSorter) + } + private func catSorter(_ lhs: CategoryViewModel, _ rhs: CategoryViewModel)->Bool{ + switch sortBy{ + case .non: + return true + case .recent: + return (lhs.lastMailTimestamp ?? Date.distantPast) > (rhs.lastMailTimestamp ?? Date.distantPast) + + } + } + +} + + +enum SortCategoriesBy : Int,CaseIterable,Identifiable { + + case recent = 0, non = 1 + + var name: String{ + get{ + switch self { + case .recent: + return NSLocalizedString("sort.recent", comment: "") + case .non: + return NSLocalizedString("sort.static", comment: "") + + } + } + } + + + var id: Int { + self.rawValue + } +} diff --git a/enzevalos_iphone/SwiftUI/Inbox/categorized/Helpers/ViewModels/ViewModelProvider.swift b/enzevalos_iphone/SwiftUI/Inbox/categorized/Helpers/ViewModels/ViewModelProvider.swift new file mode 100644 index 0000000000000000000000000000000000000000..215c2ae78581c33e0bd1f823417c8ef46c86c520 --- /dev/null +++ b/enzevalos_iphone/SwiftUI/Inbox/categorized/Helpers/ViewModels/ViewModelProvider.swift @@ -0,0 +1,171 @@ +// +// ViewModelProvider.swift +// enzevalos_iphone +// +// Created by hanneh00 on 17.01.21. +// Copyright © 2021 fu-berlin. All rights reserved. +// + +import SwiftUI + +/** + A singleton used to create all the Category ViewModels without unecessarily createing them to often + */ +class CategoryViewModelProvider : ReasonedObservableObject{ + + static let s = CategoryViewModelProvider() + private override init() {} + + ///this is used to let all subscribers know if any categoryVM published changes , the cool thing is, it also supports custom publishers + private var cancellable:(AnyCancellable,AnyCancellable) = (AnyCancellable({}),AnyCancellable({})) + func observe()->CategoryViewModelProvider{ + cancellable = ( viewModels.objectWillChangeR.sink{ reason in self.objectWillChangeR.send(reason) } , + viewModels.objectWillChange.sink { _ in self.objectWillChange.send () } ) + return self + } + + ///the ViewModel Cache + @/*Reasoned*/ObservedObject private var viewModels : ReasonedObservableDict<String , CategoryViewModel> = ReasonedObservableDict(Dictionary<String,CategoryViewModel>()).observeChildrenChanges() + + var viewModelList : ObservableArray<CategoryViewModel> {ObservableArray(array: viewModels.dict.map{$0.value}).observeChildrenChanges()} + + func createViewModel( + for name : String, + with filter : @escaping MailFilter, + and andPred : NSPredicate, + in folder : String = MailHandler.INBOX, + data consts : CategoryData = CategoryData() + )->CategoryViewModel{ + + var viewModel:CategoryViewModel + var shouldedit:Bool = false + + let alreadyCreated = viewModels.dict[name] + if (alreadyCreated != nil && alreadyCreated?.andPred == andPred) { + viewModel = alreadyCreated! + if !viewModel.hasThisData(name: name, folder: folder, consts: consts){shouldedit=true} + }else{ + let newVM = CategoryViewModel( + name : name, + filter : filter, + andPredicate: andPred + ) + viewModels = viewModels.append(k:name,v:newVM).observeChildrenChanges() + viewModel = newVM + shouldedit = true + } + + //everything modifying the VM must go inside this area. otherwise it results in a loop + if shouldedit{ + viewModel.updateFolder(folder) + viewModel.constData = consts + } + return viewModel + } +} + + + + + + + + + + + + +class ReasonedObservableDict<S:Hashable,T>: ReasonedObservableObject { + + @Published var dict:[S:T] = [:] + var cancellables = [S:AnyCancellable]() + var cancellables2 = [S:AnyCancellable]() + + init(_ dict: [S:T]) { + self.dict = dict + } + + func append(k:S,v:T) -> ReasonedObservableDict<S,T> { + self.dict[k]=v + return self + } + + func observeChildrenChanges<T: ReasonedObservableObject>() -> ReasonedObservableDict<S,T> { + let dict2 = dict as! [S:T] + dict2.forEach({ + let v = $0.value.objectWillChangeR.sink{ reason in self.objectWillChangeR.send(reason) } + let v2 = $0.value.objectWillChange.sink{ _ in self.objectWillChange.send () } + let k = $0.key + // Important: You have to keep the returned value allocated, + // otherwise the sink subscription gets cancelled + self.cancellables[k]=v + self.cancellables2[k]=v2 + }) + return self as! ReasonedObservableDict<S,T> + } +} + + + + + + + + + + + +//see https://stackoverflow.com/a/57920136 + + +import Combine +class ObservableDict<S:Hashable,T>: ObservableObject { + + @Published var dict:[S:T] = [:] + var cancellables = [S:AnyCancellable]() + + init(_ dict: [S:T]) { + self.dict = dict + } + + func append(k:S,v:T) -> ObservableDict<S,T> { + self.dict[k]=v + return self + } + + func observeChildrenChanges<T: ObservableObject>() -> ObservableDict<S,T> { + let dict2 = dict as! [S:T] + dict2.forEach({ + let v = $0.value.objectWillChange.sink(receiveValue: { _ in self.objectWillChange.send() }) + let k = $0.key + // Important: You have to keep the returned value allocated, + // otherwise the sink subscription gets cancelled + self.cancellables[k]=v + }) + return self as! ObservableDict<S,T> + } +} + +class ObservableArray<T>: ObservableObject { + + @Published var array:[T] = [] + var cancellables = [AnyCancellable]() + + init(array: [T]) { + self.array = array + + } + + func observeChildrenChanges<T: ObservableObject>() -> ObservableArray<T> { + let array2 = array as! [T] + array2.forEach({ + let c = $0.objectWillChange.sink(receiveValue: { _ in self.objectWillChange.send() }) + + // Important: You have to keep the returned value allocated, + // otherwise the sink subscription gets cancelled + self.cancellables.append(c) + }) + return self as! ObservableArray<T> + } + +} diff --git a/enzevalos_iphone/SwiftUI/Inbox/categorized/Home.swift b/enzevalos_iphone/SwiftUI/Inbox/categorized/Home.swift new file mode 100644 index 0000000000000000000000000000000000000000..edfe86eca84aff2b0b604704d5502d02910f3c5c --- /dev/null +++ b/enzevalos_iphone/SwiftUI/Inbox/categorized/Home.swift @@ -0,0 +1,215 @@ +// +// Home.swift +// enzevalos_iphone +// +// Created by hanneh00 on 17.01.21. +// Copyright © 2021 fu-berlin. All rights reserved. +// + +import SwiftUI +import TagNavVi + +struct InboxHome: View { + + + //Put the categories here + fileprivate var categories: CategoryCollection { + CategoryCollection( + UnsortedCategory(showAllMails: controller.displayClassicalInbox), + ChatCategory, + FilesCategory, + "swang" //objekte, die keine CategoryViews sind, werden ignoriert + //,SampleCategoryV2() //Other Categories must conform to CategoryV + ) + } + + + + + + //MARK: - HomeView + init(folderpath:String=MailHandler.INBOX,controller:HomeModel) { + controller.updateFolder(folderpath) + self.controller=controller + + _ = categories //initialize all Categories + } + + + + + + @ObservedObject var controller : HomeModel + + private let overviewBar_iconsPerRow:Int = 5 + + + + @State var isNavigationBarHidden: Bool = true + + + var body: some View { + NavigationView{ + VStack{ + topSection + if controller.isHome{ whenHome }else{ whenNotHome } + }//.animation(.easeInOut) + .navigationBarTitle(NSLocalizedString("Home", comment: "")) + .navigationBarHidden(self.isNavigationBarHidden) + .onAppear { + self.isNavigationBarHidden = true + } + } + } + @ViewBuilder private var topSection : some View { + VStack{ + HStack{ + backButton + .padding(.leading) + CustomizableSearchView(search: $controller.search) + + #if DEBUG && false + Button(action: {PersistentDataProvider.dataProvider.resetCategories()}, label: {Text("reset")}) + #endif + + } .padding(6) + HStack{ + if !controller.search.isExpanded{ + CategoryOverviewBar(controller: controller) + //.padding(.top, 10) + if controller.isHome {settingsButton} + } + } + if controller.isHome { settings.padding(.horizontal) } + } + } + + @ViewBuilder private var whenHome : some View { + ScrollView(){ + ForEach( controller.allCatsSorted ) { cat in + viewFromID(cat.id) + } + } + } + + @ViewBuilder private var whenNotHome : some View { + viewFromID(controller.activeCategory?.id ?? "¿") + } + + @ViewBuilder private func viewFromID(_ id : CategoryIDType)-> some View{ + if let cate = categories[id] {cate} + } + + @ViewBuilder private var backButton: some View{ + if !controller.isHome{ + Button(action: controller.goBack){ + Image(systemName: "house.fill") + .sizeTo() + } + } + } + + @ViewBuilder private var settingsButton : some View { + Button(action: {controller.settingsEnabled.toggle()}){ + Image(systemName: controller.settingsEnabled ? "gearshape" : "gearshape.fill") + }.foregroundColor(.gray).padding(.horizontal).padding(.trailing) + } + + @ViewBuilder private var settings : some View { + if controller.settingsEnabled{ + VStack{ + + HStack{ + Text(NSLocalizedString("Sort By", comment: "")) + Spacer() + Picker( selection: $controller.sortBy, label: Text(NSLocalizedString("Sort By", comment: ""))){ + ForEach(SortCategoriesBy.allCases) { sort in + return Text(sort.name).tag(sort) + } + } + .pickerStyle(SegmentedPickerStyle()) + } + + + Toggle(isOn: $controller.displayClassicalInbox, label: { + Text(NSLocalizedString("Classic Inbox", comment: "")) + }) + + }.padding() + } + } +} + + + +// MARK: - helper + +fileprivate class CategoryCollection : ObservableObject , ExpressibleByArrayLiteral , ExpressibleByDictionaryLiteral { + typealias DType = [CategoryIDType:CategoryView<AnyView>] + private var dict = DType() + + typealias Key = DType.Key + typealias Value = DType.Value + typealias ArrayLiteralElement = DType.Value + + required init(arrayLiteral elements: ArrayLiteralElement...) { + for cat in elements {dict[cat.id]=cat} + } + + required init(dictionaryLiteral elements: (Key, Value)...) { + for (key, val) in elements {dict[key]=val} + } + + convenience init(arrayLiteral elements: Any...) { + self.init(array: elements) + } + convenience init(_ elements: Any...) { + self.init(array: elements) + } + + private init(array elems:[Any]){ + for elem in elems { + if let cat = (elem as? CategoryViewToAnyView)?.toAnyView() { + dict[cat.id]=cat + }else if let cat = (elem as? CategoryV){ + dict[cat.name] = + CategoryView( + name: cat.name, + description: cat.description, + icon: cat.icon, + isBigger: cat.isBigger, + isResizable: cat.isResizable, + searchFields: cat.searchFields, + filter: cat.filter, andPredicate: + cat.andPredicate, + cat.body) + + } + } + } + + subscript(index: Key)->Value?{ + get { + return dict[index] + } + set(val){ + dict[index]=val + } + } + +} + + +extension CategoryCollection : Collection { + typealias Index = DType.Index + typealias Element = DType.Element + var startIndex: Index { return dict.startIndex} + var endIndex: Index { return dict.endIndex } + subscript(index: Index) -> Iterator.Element { + get { return dict[index] } + } + func index(after i: Index) -> Index { + return dict.index(after: i) + } +} + diff --git a/enzevalos_iphone/SwiftUI/Inbox/categorized/README.swift b/enzevalos_iphone/SwiftUI/Inbox/categorized/README.swift new file mode 100644 index 0000000000000000000000000000000000000000..62eb8f0939f8a212c9c9a03f08ad214b2acd8cea --- /dev/null +++ b/enzevalos_iphone/SwiftUI/Inbox/categorized/README.swift @@ -0,0 +1,16 @@ +// +// README.swift +// enzevalos_iphone +// +// Created by hanneh00 on 12.02.21. +// Copyright © 2021 fu-berlin. All rights reserved. +// + +/** + + You can create new Categories in the categoryfolder, but to use them, you need to add them in the Home.swift CategoryCollection + + + + + */ diff --git a/enzevalos_iphone/SwiftUI/Inbox/categorized/UI global/Blur.swift b/enzevalos_iphone/SwiftUI/Inbox/categorized/UI global/Blur.swift new file mode 100644 index 0000000000000000000000000000000000000000..e7cc7a92f1a426ed2ea7510c37a6d32db7838e89 --- /dev/null +++ b/enzevalos_iphone/SwiftUI/Inbox/categorized/UI global/Blur.swift @@ -0,0 +1,34 @@ +// +// Blur.swift +// enzevalos_iphone +// +// Created by hanneh00 on 08.01.21. +// Copyright © 2021 fu-berlin. All rights reserved. +// + +import SwiftUI + + +///this currently does not work +struct Blur: UIViewRepresentable { + let style: UIBlurEffect.Style = .systemThickMaterial + + func makeUIView(context: Context) -> UIVisualEffectView { + return UIVisualEffectView(effect: UIBlurEffect(style: style)) + } + + func updateUIView(_ uiView: UIVisualEffectView, context: Context) { + uiView.effect = UIBlurEffect(style: style) + } +} + + + +struct Blur_Previews: PreviewProvider { + static var previews: some View { + ZStack{ + Image("friendly").resizable().frame(width: 100, height: 100, alignment: .center) + Text("Blur Example").background(Blur().opacity(0.5)) + } + } +} diff --git a/enzevalos_iphone/SwiftUI/Inbox/categorized/UI global/Card.swift b/enzevalos_iphone/SwiftUI/Inbox/categorized/UI global/Card.swift new file mode 100644 index 0000000000000000000000000000000000000000..5e182135840e461c2b3c7cd1a356d5c024938a12 --- /dev/null +++ b/enzevalos_iphone/SwiftUI/Inbox/categorized/UI global/Card.swift @@ -0,0 +1,57 @@ +// +// Card.swift +// enzevalos_iphone +// +// Created by hanneh00 on 08.01.21. +// Copyright © 2021 fu-berlin. All rights reserved. +// + +import SwiftUI + +/// a beautiful Card with a slight, customizable 3D effect +struct Card<Content: View> : View { + + var shadowElevation:CGFloat = 30 + var cornerRadius:CGFloat = 35 + var fade:CGFloat = 0 + var effect3D: CGFloat = 4 + var padding : CGFloat? + + var content: () -> Content + + var body: some View { + ZStack{ + shadow + ZStack{ + Rectangle().fill(Color(UIColor.systemBackground)) + .cornerRadius(cornerRadius) + .blur(radius: min(effect3D,(cornerRadius+shadowElevation)/10)) + Rectangle().fill(Color.gray.opacity(0.2)) + .cornerRadius(cornerRadius) + .blur(radius: fade).clipped() + content() + .padding(padding ?? 20) + }.cornerRadius(cornerRadius) + + } + } + + var shadow : some View { + Rectangle().fill(Color.gray) + .cornerRadius(cornerRadius) + .padding((shadowElevation+effect3D/2)/5*3) + .shadow(radius:shadowElevation,y:shadowElevation/5*4) + } + +} + + +struct Card_Previews: PreviewProvider { + static var previews: some View { + Group{ + Card(){ + Text("Sample Content") + }.scaledToFit().padding() + } + } +} diff --git a/enzevalos_iphone/SwiftUI/Inbox/categorized/UI global/DragResizableCard.swift b/enzevalos_iphone/SwiftUI/Inbox/categorized/UI global/DragResizableCard.swift new file mode 100644 index 0000000000000000000000000000000000000000..139d2032b3cef2946a0a40f40535eac0aee0fee1 --- /dev/null +++ b/enzevalos_iphone/SwiftUI/Inbox/categorized/UI global/DragResizableCard.swift @@ -0,0 +1,89 @@ +// +// DragResizableCard.swift +// enzevalos_iphone +// +// Created by hanneh00 on 08.01.21. +// Copyright © 2021 fu-berlin. All rights reserved. +// + +import SwiftUI + +/// A card that has a small handle to dynamically resize +struct DragResizableCard<Content: View> : View { + + var shadowElevation:CGFloat = 30 + var cornerRadius:CGFloat = 35 + var fade:CGFloat = 0 + var effect3D: CGFloat = 4 + var padding: CGFloat? + + @Binding var height:CGFloat? + + var minHeight:CGFloat = 100 + + var content: () -> Content + + var body: some View { + Card( + shadowElevation: shadowElevation, + cornerRadius: cornerRadius, + fade: fade, + effect3D: effect3D, + padding:padding + ){ + ZStack{ + content() + VStack{ + Spacer() + Dragbar + } + } + } + } + + ///the handle on which the user drags to resize the card + private var Dragbar:some View{ + ZStack{ + Rectangle().fill(Color.gray.opacity(0.0001)).frame(width: 100, height: 25, alignment: .center) + Capsule() + .fill(Color.gray) + .frame(width: 100, height: 5) + .padding(.bottom,5) + } + .gesture( + DragGesture(minimumDistance: 0.0, coordinateSpace: .global) + .onChanged { gesture in + self.updateSize(gesture) + } + .onEnded { gesture in + self.endDrag(gesture) + } + ) + + } + + // this just buffers the latest location and calculates the dragoffset + @State private var latestX : CGFloat? + private func updateSize(_ dragGesture: DragGesture.Value){ + let updateBy : CGFloat = dragGesture.translation.height - (latestX ?? 0) + let newHeight = (self.height ?? 0) + updateBy + self.height = max(newHeight, minHeight) + latestX = (latestX ?? 0) + updateBy + } + private func endDrag(_ dragGesture: DragGesture.Value){ + self.latestX = nil + } + + +} + + +struct DragResizableCard_Previews: PreviewProvider { + static var previews: some View { + StatefulPreviewWrapper(200){size in + DragResizableCard(height: size){ + Text("Sample Content") + }.scaledToFit().padding() + } + } +} diff --git a/enzevalos_iphone/SwiftUI/Inbox/categorized/UI global/StatefulPreviewWrapper.swift b/enzevalos_iphone/SwiftUI/Inbox/categorized/UI global/StatefulPreviewWrapper.swift new file mode 100644 index 0000000000000000000000000000000000000000..a2f0941beb8d7b5c33c5650c86a1f5ca96441479 --- /dev/null +++ b/enzevalos_iphone/SwiftUI/Inbox/categorized/UI global/StatefulPreviewWrapper.swift @@ -0,0 +1,30 @@ +// +// StatefulPreviewWrapper.swift +// enzevalos_iphone +// +// Created by hanneh00 on 08.01.21. +// Copyright © 2021 fu-berlin. All rights reserved. +// + +import SwiftUI + +///Wrap ure Stateful Previews with this +struct StatefulPreviewWrapper<Value, Content: View>: View { + @State var value: Value + var content: (Binding<Value>) -> Content + + var body: some View { + content($value) + } + + init(_ value: Value, content: @escaping (Binding<Value>) -> Content) { + self._value = State(wrappedValue: value) + self.content = content + } +} + +struct StatefulPreviewWrapper_Previews: PreviewProvider { + static var previews: some View { + Text("this is only a wrapper to help show views that have a @Binding var, so no useful Preview") + } +} diff --git a/enzevalos_iphone/SwiftUI/Inbox/categorized/UI global/ToggleableTag.swift b/enzevalos_iphone/SwiftUI/Inbox/categorized/UI global/ToggleableTag.swift new file mode 100644 index 0000000000000000000000000000000000000000..16551f3f0e38fb99198355ee109a82a9b6a8ddf8 --- /dev/null +++ b/enzevalos_iphone/SwiftUI/Inbox/categorized/UI global/ToggleableTag.swift @@ -0,0 +1,38 @@ +// +// ToggleableTag.swift +// enzevalos_iphone +// +// Created by hanneh00 on 22.01.21. +// Copyright © 2021 fu-berlin. All rights reserved. +// + +import SwiftUI + +/// a tag on which the user can tap to (de-)activate it +/// mostly used for filters +struct ToggleableTag<UI:View>: View { + + @Binding var isActive : Bool + + var content : ()->UI + + var body: some View { + Button(action: {isActive.toggle()}){ + content() + .foregroundColor(isActive ? (Color(UIColor.systemBackground)) : .accentColor ) + .padding(.horizontal).padding(.vertical, 7) + }.background( + Capsule() + .fill(isActive ? Color.accentColor : Color.gray) + .opacity(isActive ? 0.9 : 0.1) + ) + } +} + +struct ToggleableTag_Previews: PreviewProvider { + static var previews: some View { + StatefulPreviewWrapper(true){ + ToggleableTag(isActive:$0){Text("pdf")} + } + } +} diff --git a/enzevalos_iphone/SwiftUI/Inbox/categorized/UI global/Wrap.swift b/enzevalos_iphone/SwiftUI/Inbox/categorized/UI global/Wrap.swift new file mode 100644 index 0000000000000000000000000000000000000000..a704c0777033d2d4ab217762f14331b40bdc9c36 --- /dev/null +++ b/enzevalos_iphone/SwiftUI/Inbox/categorized/UI global/Wrap.swift @@ -0,0 +1,168 @@ +// +// Wrap.swift +// enzevalos_iphone +// +// Created by hanneh00 on 10.01.21. +// Copyright © 2021 fu-berlin. All rights reserved. +// + +import SwiftUI + +/// a hybrid between HStack and VStack that orders its items along the main axis and overflows into the other stack +struct Wrap<V:View>: View { + + let views: [V] + + var mainAxisItemAmount : Int = 5 + var axis : Axis = .horizontal + var alignment : WrapAlignment = .spaceEqually + + var body: some View{ + DynamicWrap( + items: views, viewGenerator: {v in v}, + mainAxisItemAmount: mainAxisItemAmount, + axis:axis, + alignment:alignment + ) + } +} + +/// a wrap that automatically finds the best amount of items in a single row ( or column) +struct SmartWrap<V:View>: View { + + let views: [V] + + var minItemAmount : Int = 3 + var maxItemAmount : Int = 10 + + var axis : Axis = .horizontal + var alignment : WrapAlignment = .spaceEqually + + var body: some View{ + DynamicSmartWrap( + items: views, viewGenerator: {v in v}, + minItemAmount : minItemAmount, + maxItemAmount : maxItemAmount, + axis:axis, + alignment:alignment + ) + } +} + +/// same as smartwrap but with viewbuilder +struct DynamicSmartWrap<T,V:View>: View { + + let items : [T] + let viewGenerator: (T)->V + + var minItemAmount : Int = 3 + var maxItemAmount : Int = 10 + + var axis : Axis = .horizontal + var alignment : WrapAlignment = .spaceEqually + + var body: some View { + + //calc optimal length to have the least free space + let length = items.count + var optimalRelation:CGFloat = 0 + var optimalLength:Int=maxItemAmount + for i in (minItemAmount ... maxItemAmount){ + let itemsInLastRowAmount:CGFloat = CGFloat(length % i) + let relation:CGFloat = itemsInLastRowAmount / CGFloat(i) + if relation >= optimalRelation { + optimalRelation = relation + optimalLength = i + } + } + + return DynamicWrap( + items: items, + viewGenerator: viewGenerator, + mainAxisItemAmount: optimalLength, //pass optimal Length + axis:axis, + alignment:alignment + ) + } +} + +/// same as smartwrap but with viewbuilder +struct DynamicWrap<T,V:View>: View { + + let items : [T] + let viewGenerator: (T)->V + + var mainAxisItemAmount : Int = 5 + var axis : Axis = .horizontal + var alignment : WrapAlignment = .spaceEqually + + var body: some View { + + ///places as much items as specified in the main axis and overflows the rest into the next stack + @ViewBuilder func innerGenerator(_ outerIndex:Int)->some View{ + var isFirstOutOfIndex=true + ForEach(0 ..< mainAxisItemAmount){innerIndex -> AnyView in + let index = outerIndex * mainAxisItemAmount+innerIndex + if index >= items.count{ //if the amount of categories isnt a multiple of iconsPerRow + switch alignment{ + case .start, .center, .spaceEqually: + if isFirstOutOfIndex { + isFirstOutOfIndex = false + return AnyView(Spacer()) + } + fallthrough + default: + return AnyView(EmptyView()) + } + } + return AnyView(VHStack(true){ + if(alignment == .spaceEqually || alignment == .spaceBetween){Spacer()} + viewGenerator(items[index]) + if(alignment == .spaceEqually || alignment == .spaceBetween){Spacer()} + }) + } + } + + ///creates a stack perpendicular to the main axis in which all the inner stack come + let outerGenerator = + ForEach (0 ..< items.count % mainAxisItemAmount){outerIndex in + VHStack(true){ + switch alignment{ + case .end, .center, .spaceEqually: + AnyView(Spacer()) + default: + AnyView(EmptyView()) + } + innerGenerator(outerIndex) + } + } + + return VHStack(false){outerGenerator} + } + + ///chooses Axis depending on mainaxis and whether this is the inner or the outer stack + @ViewBuilder private func VHStack<V2:View>(_ isMain:Bool = true, @ViewBuilder content: @escaping ()->V2)-> some View{ + if ((axis == .horizontal && isMain) || (axis == .vertical && !isMain)) { + HStack{content()} + }else{ + VStack{content()} + } + } +} + + +/// how to space the items inside a wrap +enum WrapAlignment{ + case start, spaceEqually, spaceBetween, center, end +} + + + + +struct Wrap_Previews: PreviewProvider { + + static var previews: some View { + let views:[Text] = (0..<12).map{ i in Text("item \(i)") } + return Wrap(views: views) + } +} diff --git a/enzevalos_iphone/SwiftUI/Inbox/InboxCoordinator.swift b/enzevalos_iphone/SwiftUI/Inbox/simple/InboxCoordinator.swift similarity index 100% rename from enzevalos_iphone/SwiftUI/Inbox/InboxCoordinator.swift rename to enzevalos_iphone/SwiftUI/Inbox/simple/InboxCoordinator.swift diff --git a/enzevalos_iphone/SwiftUI/Inbox/InboxRefreshModel.swift b/enzevalos_iphone/SwiftUI/Inbox/simple/InboxRefreshModel.swift similarity index 100% rename from enzevalos_iphone/SwiftUI/Inbox/InboxRefreshModel.swift rename to enzevalos_iphone/SwiftUI/Inbox/simple/InboxRefreshModel.swift diff --git a/enzevalos_iphone/SwiftUI/Inbox/InboxView.swift b/enzevalos_iphone/SwiftUI/Inbox/simple/InboxView.swift similarity index 100% rename from enzevalos_iphone/SwiftUI/Inbox/InboxView.swift rename to enzevalos_iphone/SwiftUI/Inbox/simple/InboxView.swift diff --git a/enzevalos_iphone/SwiftUI/Inbox/KeyRecordRow.swift b/enzevalos_iphone/SwiftUI/Inbox/simple/KeyRecordRow.swift similarity index 100% rename from enzevalos_iphone/SwiftUI/Inbox/KeyRecordRow.swift rename to enzevalos_iphone/SwiftUI/Inbox/simple/KeyRecordRow.swift diff --git a/enzevalos_iphone/SwiftUI/Inbox/MailListView.swift b/enzevalos_iphone/SwiftUI/Inbox/simple/MailListView.swift similarity index 100% rename from enzevalos_iphone/SwiftUI/Inbox/MailListView.swift rename to enzevalos_iphone/SwiftUI/Inbox/simple/MailListView.swift diff --git a/enzevalos_iphone/SwiftUI/Inbox/MailRowView.swift b/enzevalos_iphone/SwiftUI/Inbox/simple/MailRowView.swift similarity index 100% rename from enzevalos_iphone/SwiftUI/Inbox/MailRowView.swift rename to enzevalos_iphone/SwiftUI/Inbox/simple/MailRowView.swift