diff --git a/Monal/Monal.xcodeproj/project.pbxproj b/Monal/Monal.xcodeproj/project.pbxproj index c84a404..ccccdc3 100644 --- a/Monal/Monal.xcodeproj/project.pbxproj +++ b/Monal/Monal.xcodeproj/project.pbxproj @@ -136,6 +136,38 @@ 7E71758E2CECC5C70059F30B /* server_features.plist in Resources */ = {isa = PBXBuildFile; fileRef = 7E71758A2CECC5C70059F30B /* server_features.plist */; }; 7E71758F2CECC5C70059F30B /* launchscreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 7E7175892CECC5C70059F30B /* launchscreen.storyboard */; }; 7E8D7AE32CECD011009AD3DF /* SwiftfulRouting in Frameworks */ = {isa = PBXBuildFile; productRef = 7E8D7AE22CECD011009AD3DF /* SwiftfulRouting */; }; + 7E8D7AEB2CECE9A8009AD3DF /* LoginScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7E8D7AE82CECE9A8009AD3DF /* LoginScreen.swift */; }; + 7E8D7AEC2CECE9A8009AD3DF /* RegistrationScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7E8D7AE92CECE9A8009AD3DF /* RegistrationScreen.swift */; }; + 7E8D7AED2CECE9A8009AD3DF /* WelcomeScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7E8D7AEA2CECE9A8009AD3DF /* WelcomeScreen.swift */; }; + 7E8D7AF12CECEB30009AD3DF /* Images+Generated.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7E8D7AEF2CECEB30009AD3DF /* Images+Generated.swift */; }; + 7E8D7AF22CECEB30009AD3DF /* Strings+Generated.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7E8D7AF02CECEB30009AD3DF /* Strings+Generated.swift */; }; + 7E8D7AF32CECEB30009AD3DF /* Colors+Generated.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7E8D7AEE2CECEB30009AD3DF /* Colors+Generated.swift */; }; + 7E8D7AFA2CECEDB3009AD3DF /* SharedListRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7E8D7AF52CECEDB3009AD3DF /* SharedListRow.swift */; }; + 7E8D7AFB2CECEDB3009AD3DF /* SharedSectionTitle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7E8D7AF72CECEDB3009AD3DF /* SharedSectionTitle.swift */; }; + 7E8D7AFC2CECEDB3009AD3DF /* SharedNavigationBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7E8D7AF62CECEDB3009AD3DF /* SharedNavigationBar.swift */; }; + 7E8D7AFD2CECEDB3009AD3DF /* UniversalInputCollection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7E8D7AF82CECEDB3009AD3DF /* UniversalInputCollection.swift */; }; + 7E8D7AFE2CECEDB3009AD3DF /* LoadingScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7E8D7AF42CECEDB3009AD3DF /* LoadingScreen.swift */; }; + 7E8D7B162CECEE79009AD3DF /* Colors+Tappable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7E8D7B042CECEE79009AD3DF /* Colors+Tappable.swift */; }; + 7E8D7B172CECEE79009AD3DF /* Vibration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7E8D7B0F2CECEE79009AD3DF /* Vibration.swift */; }; + 7E8D7B182CECEE79009AD3DF /* ButtonStyles.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7E8D7B032CECEE79009AD3DF /* ButtonStyles.swift */; }; + 7E8D7B192CECEE79009AD3DF /* AVAsset+Thumbnail.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7E8D7B012CECEE79009AD3DF /* AVAsset+Thumbnail.swift */; }; + 7E8D7B1A2CECEE79009AD3DF /* Const.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7E8D7B052CECEE79009AD3DF /* Const.swift */; }; + 7E8D7B1B2CECEE79009AD3DF /* Typography.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7E8D7B0B2CECEE79009AD3DF /* Typography.swift */; }; + 7E8D7B1C2CECEE79009AD3DF /* TimeInterval+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7E8D7B0A2CECEE79009AD3DF /* TimeInterval+Extensions.swift */; }; + 7E8D7B1D2CECEE79009AD3DF /* EdgeInsets+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7E8D7B062CECEE79009AD3DF /* EdgeInsets+Extensions.swift */; }; + 7E8D7B1E2CECEE79009AD3DF /* Binding+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7E8D7B022CECEE79009AD3DF /* Binding+Extensions.swift */; }; + 7E8D7B1F2CECEE79009AD3DF /* UIApplication+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7E8D7B0C2CECEE79009AD3DF /* UIApplication+Extensions.swift */; }; + 7E8D7B202CECEE79009AD3DF /* View+Debug.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7E8D7B102CECEE79009AD3DF /* View+Debug.swift */; }; + 7E8D7B212CECEE79009AD3DF /* Map+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7E8D7B072CECEE79009AD3DF /* Map+Extensions.swift */; }; + 7E8D7B222CECEE79009AD3DF /* View+Flip.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7E8D7B112CECEE79009AD3DF /* View+Flip.swift */; }; + 7E8D7B232CECEE79009AD3DF /* View+TappableArea.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7E8D7B142CECEE79009AD3DF /* View+TappableArea.swift */; }; + 7E8D7B242CECEE79009AD3DF /* URL+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7E8D7B0E2CECEE79009AD3DF /* URL+Extensions.swift */; }; + 7E8D7B252CECEE79009AD3DF /* PHImageManager+Fetch.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7E8D7B082CECEE79009AD3DF /* PHImageManager+Fetch.swift */; }; + 7E8D7B262CECEE79009AD3DF /* View+If.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7E8D7B122CECEE79009AD3DF /* View+If.swift */; }; + 7E8D7B272CECEE79009AD3DF /* UIImage+Crop.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7E8D7B0D2CECEE79009AD3DF /* UIImage+Crop.swift */; }; + 7E8D7B282CECEE79009AD3DF /* String+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7E8D7B092CECEE79009AD3DF /* String+Extensions.swift */; }; + 7E8D7B292CECEE79009AD3DF /* View+OnLoad.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7E8D7B132CECEE79009AD3DF /* View+OnLoad.swift */; }; + 7E8D7B2B2CECEEDF009AD3DF /* AppError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7E8D7B2A2CECEEDF009AD3DF /* AppError.swift */; }; 7E995F242CEAC5D2005B30EE /* AnotherIMApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7E995F202CEAC5D2005B30EE /* AnotherIMApp.swift */; }; 7E995F252CEAC5D2005B30EE /* RootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7E995F222CEAC5D2005B30EE /* RootView.swift */; }; 7E995F2B2CEAC9A0005B30EE /* monalxmpp.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 26CC579223A0867400ABB92A /* monalxmpp.framework */; }; @@ -606,6 +638,38 @@ 7E7175892CECC5C70059F30B /* launchscreen.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = launchscreen.storyboard; sourceTree = ""; }; 7E71758A2CECC5C70059F30B /* server_features.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = server_features.plist; sourceTree = ""; }; 7E71758B2CECC5C70059F30B /* Localizable.strings */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; path = Localizable.strings; sourceTree = ""; }; + 7E8D7AE82CECE9A8009AD3DF /* LoginScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginScreen.swift; sourceTree = ""; }; + 7E8D7AE92CECE9A8009AD3DF /* RegistrationScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RegistrationScreen.swift; sourceTree = ""; }; + 7E8D7AEA2CECE9A8009AD3DF /* WelcomeScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WelcomeScreen.swift; sourceTree = ""; }; + 7E8D7AEE2CECEB30009AD3DF /* Colors+Generated.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Colors+Generated.swift"; sourceTree = ""; }; + 7E8D7AEF2CECEB30009AD3DF /* Images+Generated.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Images+Generated.swift"; sourceTree = ""; }; + 7E8D7AF02CECEB30009AD3DF /* Strings+Generated.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Strings+Generated.swift"; sourceTree = ""; }; + 7E8D7AF42CECEDB3009AD3DF /* LoadingScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadingScreen.swift; sourceTree = ""; }; + 7E8D7AF52CECEDB3009AD3DF /* SharedListRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SharedListRow.swift; sourceTree = ""; }; + 7E8D7AF62CECEDB3009AD3DF /* SharedNavigationBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SharedNavigationBar.swift; sourceTree = ""; }; + 7E8D7AF72CECEDB3009AD3DF /* SharedSectionTitle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SharedSectionTitle.swift; sourceTree = ""; }; + 7E8D7AF82CECEDB3009AD3DF /* UniversalInputCollection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UniversalInputCollection.swift; sourceTree = ""; }; + 7E8D7B012CECEE79009AD3DF /* AVAsset+Thumbnail.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AVAsset+Thumbnail.swift"; sourceTree = ""; }; + 7E8D7B022CECEE79009AD3DF /* Binding+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Binding+Extensions.swift"; sourceTree = ""; }; + 7E8D7B032CECEE79009AD3DF /* ButtonStyles.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ButtonStyles.swift; sourceTree = ""; }; + 7E8D7B042CECEE79009AD3DF /* Colors+Tappable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Colors+Tappable.swift"; sourceTree = ""; }; + 7E8D7B052CECEE79009AD3DF /* Const.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Const.swift; sourceTree = ""; }; + 7E8D7B062CECEE79009AD3DF /* EdgeInsets+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "EdgeInsets+Extensions.swift"; sourceTree = ""; }; + 7E8D7B072CECEE79009AD3DF /* Map+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Map+Extensions.swift"; sourceTree = ""; }; + 7E8D7B082CECEE79009AD3DF /* PHImageManager+Fetch.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PHImageManager+Fetch.swift"; sourceTree = ""; }; + 7E8D7B092CECEE79009AD3DF /* String+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+Extensions.swift"; sourceTree = ""; }; + 7E8D7B0A2CECEE79009AD3DF /* TimeInterval+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TimeInterval+Extensions.swift"; sourceTree = ""; }; + 7E8D7B0B2CECEE79009AD3DF /* Typography.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Typography.swift; sourceTree = ""; }; + 7E8D7B0C2CECEE79009AD3DF /* UIApplication+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIApplication+Extensions.swift"; sourceTree = ""; }; + 7E8D7B0D2CECEE79009AD3DF /* UIImage+Crop.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIImage+Crop.swift"; sourceTree = ""; }; + 7E8D7B0E2CECEE79009AD3DF /* URL+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "URL+Extensions.swift"; sourceTree = ""; }; + 7E8D7B0F2CECEE79009AD3DF /* Vibration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Vibration.swift; sourceTree = ""; }; + 7E8D7B102CECEE79009AD3DF /* View+Debug.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "View+Debug.swift"; sourceTree = ""; }; + 7E8D7B112CECEE79009AD3DF /* View+Flip.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "View+Flip.swift"; sourceTree = ""; }; + 7E8D7B122CECEE79009AD3DF /* View+If.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "View+If.swift"; sourceTree = ""; }; + 7E8D7B132CECEE79009AD3DF /* View+OnLoad.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "View+OnLoad.swift"; sourceTree = ""; }; + 7E8D7B142CECEE79009AD3DF /* View+TappableArea.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "View+TappableArea.swift"; sourceTree = ""; }; + 7E8D7B2A2CECEEDF009AD3DF /* AppError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppError.swift; sourceTree = ""; }; 7E995F062CEAC4B8005B30EE /* another.im.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = another.im.app; sourceTree = BUILT_PRODUCTS_DIR; }; 7E995F202CEAC5D2005B30EE /* AnotherIMApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnotherIMApp.swift; sourceTree = ""; }; 7E995F222CEAC5D2005B30EE /* RootView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RootView.swift; sourceTree = ""; }; @@ -1397,7 +1461,11 @@ 7E8D7AE42CECD037009AD3DF /* Views */ = { isa = PBXGroup; children = ( + 7E8D7AE82CECE9A8009AD3DF /* LoginScreen.swift */, + 7E8D7AE92CECE9A8009AD3DF /* RegistrationScreen.swift */, 7E995F222CEAC5D2005B30EE /* RootView.swift */, + 7E8D7AEA2CECE9A8009AD3DF /* WelcomeScreen.swift */, + 7E8D7AF92CECEDB3009AD3DF /* SharedComponents */, ); path = Views; sourceTree = ""; @@ -1406,15 +1474,56 @@ isa = PBXGroup; children = ( 7E6AF38E2CEB9110004328B5 /* MonalXmppWrapper.swift */, + 7E8D7B2A2CECEEDF009AD3DF /* AppError.swift */, ); path = XMPP; sourceTree = ""; }; + 7E8D7AF92CECEDB3009AD3DF /* SharedComponents */ = { + isa = PBXGroup; + children = ( + 7E8D7AF42CECEDB3009AD3DF /* LoadingScreen.swift */, + 7E8D7AF52CECEDB3009AD3DF /* SharedListRow.swift */, + 7E8D7AF62CECEDB3009AD3DF /* SharedNavigationBar.swift */, + 7E8D7AF72CECEDB3009AD3DF /* SharedSectionTitle.swift */, + 7E8D7AF82CECEDB3009AD3DF /* UniversalInputCollection.swift */, + ); + path = SharedComponents; + sourceTree = ""; + }; + 7E8D7B152CECEE79009AD3DF /* Helpers */ = { + isa = PBXGroup; + children = ( + 7E8D7B012CECEE79009AD3DF /* AVAsset+Thumbnail.swift */, + 7E8D7B022CECEE79009AD3DF /* Binding+Extensions.swift */, + 7E8D7B032CECEE79009AD3DF /* ButtonStyles.swift */, + 7E8D7B042CECEE79009AD3DF /* Colors+Tappable.swift */, + 7E8D7B052CECEE79009AD3DF /* Const.swift */, + 7E8D7B062CECEE79009AD3DF /* EdgeInsets+Extensions.swift */, + 7E8D7B072CECEE79009AD3DF /* Map+Extensions.swift */, + 7E8D7B082CECEE79009AD3DF /* PHImageManager+Fetch.swift */, + 7E8D7B092CECEE79009AD3DF /* String+Extensions.swift */, + 7E8D7B0A2CECEE79009AD3DF /* TimeInterval+Extensions.swift */, + 7E8D7B0B2CECEE79009AD3DF /* Typography.swift */, + 7E8D7B0C2CECEE79009AD3DF /* UIApplication+Extensions.swift */, + 7E8D7B0D2CECEE79009AD3DF /* UIImage+Crop.swift */, + 7E8D7B0E2CECEE79009AD3DF /* URL+Extensions.swift */, + 7E8D7B0F2CECEE79009AD3DF /* Vibration.swift */, + 7E8D7B102CECEE79009AD3DF /* View+Debug.swift */, + 7E8D7B112CECEE79009AD3DF /* View+Flip.swift */, + 7E8D7B122CECEE79009AD3DF /* View+If.swift */, + 7E8D7B132CECEE79009AD3DF /* View+OnLoad.swift */, + 7E8D7B142CECEE79009AD3DF /* View+TappableArea.swift */, + ); + path = Helpers; + sourceTree = ""; + }; 7E995F232CEAC5D2005B30EE /* another.im */ = { isa = PBXGroup; children = ( 7E995F202CEAC5D2005B30EE /* AnotherIMApp.swift */, EA534340732BF66B533E4C0B /* Generated */, + 7E8D7B152CECEE79009AD3DF /* Helpers */, 7ED6F01A2CECC43D0035B3B7 /* Resources */, 7E8D7AE42CECD037009AD3DF /* Views */, 7E8D7AE52CECD05C009AD3DF /* XMPP */, @@ -1609,6 +1718,9 @@ EA534340732BF66B533E4C0B /* Generated */ = { isa = PBXGroup; children = ( + 7E8D7AEE2CECEB30009AD3DF /* Colors+Generated.swift */, + 7E8D7AEF2CECEB30009AD3DF /* Images+Generated.swift */, + 7E8D7AF02CECEB30009AD3DF /* Strings+Generated.swift */, ); path = Generated; sourceTree = ""; @@ -2474,6 +2586,38 @@ buildActionMask = 2147483647; files = ( 7E995F242CEAC5D2005B30EE /* AnotherIMApp.swift in Sources */, + 7E8D7AEB2CECE9A8009AD3DF /* LoginScreen.swift in Sources */, + 7E8D7AF12CECEB30009AD3DF /* Images+Generated.swift in Sources */, + 7E8D7AF22CECEB30009AD3DF /* Strings+Generated.swift in Sources */, + 7E8D7AF32CECEB30009AD3DF /* Colors+Generated.swift in Sources */, + 7E8D7B162CECEE79009AD3DF /* Colors+Tappable.swift in Sources */, + 7E8D7B172CECEE79009AD3DF /* Vibration.swift in Sources */, + 7E8D7B182CECEE79009AD3DF /* ButtonStyles.swift in Sources */, + 7E8D7B192CECEE79009AD3DF /* AVAsset+Thumbnail.swift in Sources */, + 7E8D7B1A2CECEE79009AD3DF /* Const.swift in Sources */, + 7E8D7B1B2CECEE79009AD3DF /* Typography.swift in Sources */, + 7E8D7B1C2CECEE79009AD3DF /* TimeInterval+Extensions.swift in Sources */, + 7E8D7B1D2CECEE79009AD3DF /* EdgeInsets+Extensions.swift in Sources */, + 7E8D7B1E2CECEE79009AD3DF /* Binding+Extensions.swift in Sources */, + 7E8D7B1F2CECEE79009AD3DF /* UIApplication+Extensions.swift in Sources */, + 7E8D7B202CECEE79009AD3DF /* View+Debug.swift in Sources */, + 7E8D7B212CECEE79009AD3DF /* Map+Extensions.swift in Sources */, + 7E8D7B222CECEE79009AD3DF /* View+Flip.swift in Sources */, + 7E8D7B232CECEE79009AD3DF /* View+TappableArea.swift in Sources */, + 7E8D7B242CECEE79009AD3DF /* URL+Extensions.swift in Sources */, + 7E8D7B252CECEE79009AD3DF /* PHImageManager+Fetch.swift in Sources */, + 7E8D7B262CECEE79009AD3DF /* View+If.swift in Sources */, + 7E8D7B272CECEE79009AD3DF /* UIImage+Crop.swift in Sources */, + 7E8D7B282CECEE79009AD3DF /* String+Extensions.swift in Sources */, + 7E8D7B292CECEE79009AD3DF /* View+OnLoad.swift in Sources */, + 7E8D7AEC2CECE9A8009AD3DF /* RegistrationScreen.swift in Sources */, + 7E8D7AFA2CECEDB3009AD3DF /* SharedListRow.swift in Sources */, + 7E8D7B2B2CECEEDF009AD3DF /* AppError.swift in Sources */, + 7E8D7AFB2CECEDB3009AD3DF /* SharedSectionTitle.swift in Sources */, + 7E8D7AFC2CECEDB3009AD3DF /* SharedNavigationBar.swift in Sources */, + 7E8D7AFD2CECEDB3009AD3DF /* UniversalInputCollection.swift in Sources */, + 7E8D7AFE2CECEDB3009AD3DF /* LoadingScreen.swift in Sources */, + 7E8D7AED2CECE9A8009AD3DF /* WelcomeScreen.swift in Sources */, 7E995F252CEAC5D2005B30EE /* RootView.swift in Sources */, 7E6AF38F2CEB9110004328B5 /* MonalXmppWrapper.swift in Sources */, ); @@ -3415,8 +3559,7 @@ baseConfigurationReference = 1936866375CABF471D3CE238 /* Pods-another.im.debug.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; - ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = NO; CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; CLANG_WARN_FRAMEWORK_INCLUDE_PRIVATE_FROM_PUBLIC = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; @@ -3475,8 +3618,7 @@ baseConfigurationReference = F8ACC07B95446BB8052933BF /* Pods-another.im.adhoc.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; - ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = NO; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; @@ -3567,8 +3709,7 @@ baseConfigurationReference = BC9E05245CF07072A35AE126 /* Pods-another.im.alpha.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; - ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = NO; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; @@ -3659,8 +3800,7 @@ baseConfigurationReference = F2082C5D72E8D7D49B31FBEE /* Pods-another.im.appstore.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; - ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = NO; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; @@ -3751,8 +3891,7 @@ baseConfigurationReference = 7D281334DB441077E42E3E89 /* Pods-another.im.appstore-quicksy.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; - ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = NO; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; @@ -3843,8 +3982,7 @@ baseConfigurationReference = 3D8AAFBF5B865907983E9F59 /* Pods-another.im.beta.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; - ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = NO; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; diff --git a/Monal/another.im/Helpers/AVAsset+Thumbnail.swift b/Monal/another.im/Helpers/AVAsset+Thumbnail.swift new file mode 100644 index 0000000..3bccc0f --- /dev/null +++ b/Monal/another.im/Helpers/AVAsset+Thumbnail.swift @@ -0,0 +1,16 @@ +import AVFoundation +import UIKit + +extension AVAsset { + func generateVideoThumbnail(_ size: CGSize) async throws -> UIImage { + try await Task { + let assetImgGenerate = AVAssetImageGenerator(asset: self) + assetImgGenerate.appliesPreferredTrackTransform = true + let time = CMTimeMakeWithSeconds(Float64(1), preferredTimescale: 600) + let cgImage = try assetImgGenerate.copyCGImage(at: time, actualTime: nil) + let image = UIImage(cgImage: cgImage) + let result = try await image.scaleAndCropImage(size) + return result + }.value + } +} diff --git a/Monal/another.im/Helpers/Binding+Extensions.swift b/Monal/another.im/Helpers/Binding+Extensions.swift new file mode 100644 index 0000000..6a2a796 --- /dev/null +++ b/Monal/another.im/Helpers/Binding+Extensions.swift @@ -0,0 +1,12 @@ +import SwiftUI + +extension Binding where Value == String { + func max(_ limit: Int) -> Self { + if wrappedValue.count > limit { + DispatchQueue.main.async { + wrappedValue = String(wrappedValue.dropLast()) + } + } + return self + } +} diff --git a/Monal/another.im/Helpers/ButtonStyles.swift b/Monal/another.im/Helpers/ButtonStyles.swift new file mode 100644 index 0000000..62c59ab --- /dev/null +++ b/Monal/another.im/Helpers/ButtonStyles.swift @@ -0,0 +1,50 @@ +import SwiftUI + +private enum ButtonSizes { + static let padding = 16.0 + static let cornerRadius = 4.0 + static let scaleEffect: CGFloat = 0.9 + static let opacity: Double = 0.6 +} + +struct PrimaryButtonStyle: ButtonStyle { + @Environment(\.isEnabled) private var isEnabled + + func makeBody(configuration: Configuration) -> some View { + configuration + .label + .font(.head2) + .padding(ButtonSizes.padding) + .frame(maxWidth: .infinity) + .foregroundColor(.Material.Shape.white) + .background { + RoundedRectangle(cornerRadius: ButtonSizes.cornerRadius) + .foregroundColor(isEnabled ? .Material.Elements.active : .Material.Shape.separator) + } + .contentShape(Rectangle()) + .scaleEffect(configuration.isPressed ? ButtonSizes.scaleEffect : 1.0) + .opacity(configuration.isPressed ? ButtonSizes.opacity : 1.0) + .animation(.easeInOut(duration: 0.1), value: configuration.isPressed) + } +} + +struct SecondaryButtonStyle: ButtonStyle { + @Environment(\.isEnabled) private var isEnabled + + func makeBody(configuration: Configuration) -> some View { + configuration + .label + .font(.head2) + .padding(ButtonSizes.padding) + .frame(maxWidth: .infinity) + .foregroundColor(isEnabled ? .Material.Elements.active : .Material.Shape.separator) + .background { + RoundedRectangle(cornerRadius: ButtonSizes.cornerRadius) + .stroke(isEnabled ? Color.Material.Elements.active : Color.Material.Shape.separator) + } + .contentShape(Rectangle()) + .scaleEffect(configuration.isPressed ? ButtonSizes.scaleEffect : 1.0) + .opacity(configuration.isPressed ? ButtonSizes.opacity : 1.0) + .animation(.easeInOut(duration: 0.1), value: configuration.isPressed) + } +} diff --git a/Monal/another.im/Helpers/Colors+Tappable.swift b/Monal/another.im/Helpers/Colors+Tappable.swift new file mode 100644 index 0000000..718a3df --- /dev/null +++ b/Monal/another.im/Helpers/Colors+Tappable.swift @@ -0,0 +1,13 @@ +import SwiftUI + +public extension Color { + static let clearTappable = Color.white.opacity(0.0001) + // static func random(randomOpacity: Bool = false) -> Color { + // Color( + // red: .random(in: 0 ... 1), + // green: .random(in: 0 ... 1), + // blue: .random(in: 0 ... 1), + // opacity: randomOpacity ? .random(in: 0 ... 1) : 1 + // ) + // } +} diff --git a/Monal/another.im/Helpers/Const.swift b/Monal/another.im/Helpers/Const.swift new file mode 100644 index 0000000..8af2571 --- /dev/null +++ b/Monal/another.im/Helpers/Const.swift @@ -0,0 +1,51 @@ +import Foundation +import UIKit + +enum Const { + // App + static var appVersion: String { + let info = Bundle.main.infoDictionary + let appVersion = info?["CFBundleShortVersionString"] as? String ?? "Unknown" + let appBuild = info?[kCFBundleVersionKey as String] as? String ?? "Unknown" + return "v \(appVersion)(\(appBuild))" + } + + static var appName: String { + Bundle.main.bundleIdentifier ?? "another.im" + } + + // Trusted servers + enum TrustedServers: String { + case narayana = "narayana.im" + case conversations = "conversations.im" + } + + // Limit for video for sharing + static let videoDurationLimit = 60.0 + + // Grid size for gallery preview (3 in a row) + static let galleryGridSize = UIScreen.main.bounds.width / 3 + + // Size for map preview for location messages + static let mapPreviewSize = UIScreen.main.bounds.width * 0.5 + + // Size for attachment preview + static let attachmentPreviewSize = UIScreen.main.bounds.width * 0.5 + + // MAM request page size + static let mamRequestPageSize = 50 +} + +final class FolderWrapper { + static let shared = FolderWrapper() + let fileFolder: URL + private init() { + // swiftlint:disable:next force_unwrapping + let documentsURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first! + let subdirectoryURL = documentsURL.appendingPathComponent("Downloads") + if !FileManager.default.fileExists(atPath: subdirectoryURL.path) { + try? FileManager.default.createDirectory(at: subdirectoryURL, withIntermediateDirectories: true, attributes: nil) + } + fileFolder = subdirectoryURL + } +} diff --git a/Monal/another.im/Helpers/EdgeInsets+Extensions.swift b/Monal/another.im/Helpers/EdgeInsets+Extensions.swift new file mode 100644 index 0000000..65f5311 --- /dev/null +++ b/Monal/another.im/Helpers/EdgeInsets+Extensions.swift @@ -0,0 +1,15 @@ +import SwiftUI + +extension EdgeInsets { + var inverted: EdgeInsets { + .init(top: -top, leading: -leading, bottom: -bottom, trailing: -trailing) + } + + static var zero: EdgeInsets { + .init(top: 0, leading: 0, bottom: 0, trailing: 0) + } + + static func symmetric(_ value: CGFloat) -> EdgeInsets { + .init(top: value, leading: value, bottom: value, trailing: value) + } +} diff --git a/Monal/another.im/Helpers/Map+Extensions.swift b/Monal/another.im/Helpers/Map+Extensions.swift new file mode 100644 index 0000000..4c25921 --- /dev/null +++ b/Monal/another.im/Helpers/Map+Extensions.swift @@ -0,0 +1,16 @@ +import MapKit + +extension MKCoordinateRegion: Equatable { + public static func == (lhs: MKCoordinateRegion, rhs: MKCoordinateRegion) -> Bool { + lhs.center.latitude == rhs.center.latitude && + lhs.center.longitude == rhs.center.longitude && + lhs.span.latitudeDelta == rhs.span.latitudeDelta && + lhs.span.longitudeDelta == rhs.span.longitudeDelta + } +} + +extension CLLocationCoordinate2D: Identifiable { + public var id: String { + "\(latitude)-\(longitude)" + } +} diff --git a/Monal/another.im/Helpers/PHImageManager+Fetch.swift b/Monal/another.im/Helpers/PHImageManager+Fetch.swift new file mode 100644 index 0000000..5bec4a8 --- /dev/null +++ b/Monal/another.im/Helpers/PHImageManager+Fetch.swift @@ -0,0 +1,43 @@ +import Photos +import UIKit + +extension PHImageManager { + func getPhoto(for asset: PHAsset) async throws -> UIImage { + let options = PHImageRequestOptions() + options.version = .original + options.isSynchronous = true + return try await withCheckedThrowingContinuation { continuation in + requestImage( + for: asset, + targetSize: PHImageManagerMaximumSize, + contentMode: .aspectFill, + options: options + ) { image, _ in + if let image { + continuation.resume(returning: image) + } else { + continuation.resume(throwing: AppError.imageNotFound) + } + } + } + } + + func getVideo(for asset: PHAsset) async throws -> AVAsset { + let options = PHVideoRequestOptions() + options.version = .original + options.deliveryMode = .highQualityFormat + options.isNetworkAccessAllowed = true + return try await withCheckedThrowingContinuation { continuation in + requestAVAsset( + forVideo: asset, + options: options + ) { avAsset, _, _ in + if let avAsset { + continuation.resume(returning: avAsset) + } else { + continuation.resume(throwing: AppError.videoNotFound) + } + } + } + } +} diff --git a/Monal/another.im/Helpers/String+Extensions.swift b/Monal/another.im/Helpers/String+Extensions.swift new file mode 100644 index 0000000..17557ea --- /dev/null +++ b/Monal/another.im/Helpers/String+Extensions.swift @@ -0,0 +1,98 @@ +import CoreLocation +import Foundation +import SwiftUI + +extension String { + var firstLetter: String { + String(prefix(1)).uppercased() + } + + var makeReply: String { + let allLines = components(separatedBy: .newlines) + let nonBlankLines = allLines.filter { !$0.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty } + var result = nonBlankLines.joined(separator: "\n") + result = "> \(result)" + return result + } + + var isLocation: Bool { + hasPrefix("geo:") + } + + var getLatLon: CLLocationCoordinate2D { + let geo = components(separatedBy: ":")[1] + let parts = geo.components(separatedBy: ",") + let lat = Double(parts[0]) ?? 0.0 + let lon = Double(parts[1]) ?? 0.0 + return CLLocationCoordinate2D(latitude: lat, longitude: lon) + } + + var isContact: Bool { + hasPrefix("contact:") + } + + var getContactJid: String { + components(separatedBy: ":")[1] + } +} + +extension String { +// var attachmentType: AttachmentType { +// let ext = (self as NSString).pathExtension.lowercased() +// if ext.contains("jpeg") || ext.contains("jpg") || ext.contains("png") || ext.contains("gif") { +// return .image +// } else if ext.contains("mov") || ext.contains("mp4") || ext.contains("avi") { +// return .video +// } else if ext.contains("mp3") || ext.contains("wav") || ext.contains("m4a") { +// return .audio +// } else { +// return .file +// } +// } +} + +extension String { + var firstLetterColor: Color { + let firstLetter = self.firstLetter + switch firstLetter { + case "A", "M", "Y": + return Color.Rainbow.tortoiseLight500 + + case "B", "N", "Z": + return Color.Rainbow.orangeLight500 + + case "C", "O": + return Color.Rainbow.yellowLight500 + + case "D", "P": + return Color.Rainbow.greenLight500 + + case "E", "Q": + return Color.Rainbow.blueLight500 + + case "F", "R": + return Color.Rainbow.magentaLight500 + + case "G", "S": + return Color.Rainbow.tortoiseDark500 + + case "H", "T": + return Color.Rainbow.orangeDark500 + + case "I", "U": + return Color.Rainbow.yellowDark500 + + case "J", "V": + return Color.Rainbow.greenDark500 + + case "K", "W": + return Color.Rainbow.blueDark500 + + case "L", "X": + return Color.Rainbow.magentaDark500 + + default: + return Color.Rainbow.tortoiseLight500 + } + } +} diff --git a/Monal/another.im/Helpers/TimeInterval+Extensions.swift b/Monal/another.im/Helpers/TimeInterval+Extensions.swift new file mode 100644 index 0000000..0dde77a --- /dev/null +++ b/Monal/another.im/Helpers/TimeInterval+Extensions.swift @@ -0,0 +1,9 @@ +import Foundation + +extension TimeInterval { + var minAndSec: String { + let minutes = Int(self) / 60 + let seconds = Int(self) % 60 + return String(format: "%02d:%02d", minutes, seconds) + } +} diff --git a/Monal/another.im/Helpers/Typography.swift b/Monal/another.im/Helpers/Typography.swift new file mode 100644 index 0000000..db856f3 --- /dev/null +++ b/Monal/another.im/Helpers/Typography.swift @@ -0,0 +1,13 @@ +import Foundation +import SwiftUI + +extension Font { + static let head1l = Font.system(size: 34, weight: .light, design: .rounded) + static let head1r = Font.system(size: 34, weight: .regular, design: .rounded) + static let head2 = Font.system(size: 20, weight: .regular, design: .rounded) + static let body1 = Font.system(size: 18, weight: .regular, design: .rounded) + static let body2 = Font.system(size: 16, weight: .regular, design: .rounded) + static let body3 = Font.system(size: 14, weight: .regular, design: .rounded) + static let sub1 = Font.system(size: 10, weight: .regular, design: .rounded) + static let sub2 = Font.system(size: 8, weight: .regular, design: .rounded) +} diff --git a/Monal/another.im/Helpers/UIApplication+Extensions.swift b/Monal/another.im/Helpers/UIApplication+Extensions.swift new file mode 100644 index 0000000..63550f5 --- /dev/null +++ b/Monal/another.im/Helpers/UIApplication+Extensions.swift @@ -0,0 +1,10 @@ +import UIKit + +func openAppSettings() { + if + let appSettingsUrl = URL(string: UIApplication.openSettingsURLString), + UIApplication.shared.canOpenURL(appSettingsUrl) + { + UIApplication.shared.open(appSettingsUrl, completionHandler: nil) + } +} diff --git a/Monal/another.im/Helpers/UIImage+Crop.swift b/Monal/another.im/Helpers/UIImage+Crop.swift new file mode 100644 index 0000000..239a1e8 --- /dev/null +++ b/Monal/another.im/Helpers/UIImage+Crop.swift @@ -0,0 +1,30 @@ +import Foundation +import UIKit + +extension UIImage { + func scaleAndCropImage(_ size: CGSize) async throws -> UIImage { + try await Task { + let aspect = self.size.width / self.size.height + let targetAspect = size.width / size.height + var newWidth: CGFloat + var newHeight: CGFloat + if aspect < targetAspect { + newWidth = size.width + newHeight = size.width / aspect + } else { + newHeight = size.height + newWidth = size.height * aspect + } + + UIGraphicsBeginImageContextWithOptions(size, false, 0.0) + self.draw(in: CGRect(x: (size.width - newWidth) / 2, y: (size.height - newHeight) / 2, width: newWidth, height: newHeight)) + let newImage = UIGraphicsGetImageFromCurrentImageContext() + UIGraphicsEndImageContext() + if let newImage = newImage { + return newImage + } else { + throw NSError(domain: "UIImage", code: -900, userInfo: nil) + } + }.value + } +} diff --git a/Monal/another.im/Helpers/URL+Extensions.swift b/Monal/another.im/Helpers/URL+Extensions.swift new file mode 100644 index 0000000..c84c26a --- /dev/null +++ b/Monal/another.im/Helpers/URL+Extensions.swift @@ -0,0 +1,13 @@ +import UniformTypeIdentifiers + +extension URL { + var mimeType: String { + let pathExtension = self.pathExtension + + if let uti = UTType(filenameExtension: pathExtension) { + return uti.preferredMIMEType ?? "application/octet-stream" + } else { + return "application/octet-stream" + } + } +} diff --git a/Monal/another.im/Helpers/Vibration.swift b/Monal/another.im/Helpers/Vibration.swift new file mode 100644 index 0000000..3eee66b --- /dev/null +++ b/Monal/another.im/Helpers/Vibration.swift @@ -0,0 +1,17 @@ +import SwiftUI +import UIKit + +enum Vibration: String { + case error + case success + + public func vibrate() { + switch self { + case .error: + UINotificationFeedbackGenerator().notificationOccurred(.error) + + case .success: + UINotificationFeedbackGenerator().notificationOccurred(.success) + } + } +} diff --git a/Monal/another.im/Helpers/View+Debug.swift b/Monal/another.im/Helpers/View+Debug.swift new file mode 100644 index 0000000..34f7c65 --- /dev/null +++ b/Monal/another.im/Helpers/View+Debug.swift @@ -0,0 +1,35 @@ +import SwiftUI + +#if DEBUG + private let rainbowDebugColors = [ + Color.purple, + Color.blue, + Color.green, + Color.yellow, + Color.orange, + Color.red, + Color.pink, + Color.black.opacity(0.5), + Color.teal, + Color.gray, + Color.mint, + Color.cyan + ] + + public extension Color { + static func random(randomOpacity: Bool = false) -> Color { + Color( + red: .random(in: 0 ... 1), + green: .random(in: 0 ... 1), + blue: .random(in: 0 ... 1), + opacity: randomOpacity ? .random(in: 0 ... 1) : 1 + ) + } + } + + extension View { + func rainbowDebug() -> some View { + background(Color.random()) + } + } +#endif diff --git a/Monal/another.im/Helpers/View+Flip.swift b/Monal/another.im/Helpers/View+Flip.swift new file mode 100644 index 0000000..e0d0df2 --- /dev/null +++ b/Monal/another.im/Helpers/View+Flip.swift @@ -0,0 +1,15 @@ +import SwiftUI + +struct FlipView: ViewModifier { + func body(content: Content) -> some View { + content + .rotationEffect(.radians(Double.pi)) + .scaleEffect(x: -1, y: 1, anchor: .center) + } +} + +extension View { + func flip() -> some View { + modifier(FlipView()) + } +} diff --git a/Monal/another.im/Helpers/View+If.swift b/Monal/another.im/Helpers/View+If.swift new file mode 100644 index 0000000..f6611ca --- /dev/null +++ b/Monal/another.im/Helpers/View+If.swift @@ -0,0 +1,11 @@ +import SwiftUI + +public extension View { + @ViewBuilder func `if`(_ condition: @autoclosure () -> Bool, transform: (Self) -> Content) -> some View { + if condition() { + transform(self) + } else { + self + } + } +} diff --git a/Monal/another.im/Helpers/View+OnLoad.swift b/Monal/another.im/Helpers/View+OnLoad.swift new file mode 100644 index 0000000..4206bff --- /dev/null +++ b/Monal/another.im/Helpers/View+OnLoad.swift @@ -0,0 +1,27 @@ +import SwiftUI + +// MARK: - On load +extension View { + func onLoad(_ action: @escaping () -> Void) -> some View { + modifier(ViewDidLoadModifier(action)) + } +} + +private struct ViewDidLoadModifier: ViewModifier { + private let action: () -> Void + + @State private var didLoad = false + + init(_ action: @escaping () -> Void) { + self.action = action + } + + func body(content: Content) -> some View { + content.onAppear { + if !didLoad { + didLoad.toggle() + action() + } + } + } +} diff --git a/Monal/another.im/Helpers/View+TappableArea.swift b/Monal/another.im/Helpers/View+TappableArea.swift new file mode 100644 index 0000000..8c659d4 --- /dev/null +++ b/Monal/another.im/Helpers/View+TappableArea.swift @@ -0,0 +1,27 @@ +import SwiftUI + +extension View { + func tappablePadding(_ insets: EdgeInsets, onTap: @escaping () -> Void) -> some View { + modifier(TappablePadding(insets: insets, onTap: onTap)) + } +} + +struct TappablePadding: ViewModifier { + let insets: EdgeInsets + let onTap: () -> Void + + public init(insets: EdgeInsets, onTap: @escaping () -> Void) { + self.insets = insets + self.onTap = onTap + } + + public func body(content: Content) -> some View { + content + .padding(insets) + .contentShape(Rectangle()) + .onTapGesture { + onTap() + } + .padding(insets.inverted) + } +} diff --git a/Monal/another.im/Views/LoginScreen.swift b/Monal/another.im/Views/LoginScreen.swift new file mode 100644 index 0000000..09de1c6 --- /dev/null +++ b/Monal/another.im/Views/LoginScreen.swift @@ -0,0 +1,115 @@ +import Combine +import SwiftUI + +struct LoginScreen: View { + @EnvironmentObject var wrapper: MonalXmppWrapper + @Environment(\.router) var router + + enum Field { + case userJid + case password + } + + @FocusState private var focus: Field? + + @State private var jidStr: String = "" + @State private var pass: String = "" + + public var body: some View { + ZStack { + // background + Color.Material.Background.light + .ignoresSafeArea() + + // content + VStack(spacing: 32) { + // icon + Image(.aimLogo) + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: 120, height: 120) + + // texts + VStack(spacing: 10) { + Text(L10n.Login.title) + .font(.head1l) + .foregroundColor(.Material.Text.main) + .fixedSize(horizontal: true, vertical: false) + Text(L10n.Login.subtitle) + .font(.body2) + .foregroundColor(.Material.Text.sub) + .multilineTextAlignment(.center) + .fixedSize(horizontal: false, vertical: true) + } + + VStack(spacing: 16) { + UniversalInputCollection.TextField( + prompt: L10n.Login.Hint.jid, + text: $jidStr, + focus: $focus, + fieldType: .userJid, + contentType: .emailAddress, + keyboardType: .emailAddress, + submitLabel: .next, + action: { + focus = .password + } + ) + UniversalInputCollection.SecureField( + prompt: L10n.Login.Hint.password, + text: $pass, + focus: $focus, + fieldType: .password, + submitLabel: .go, + action: { + focus = nil + } + ) + + Button { + router.showModal { + LoadingScreen() + } + Task(priority: .background) { + await tryLogin() + } + } label: { + Text(L10n.Login.btn) + } + .buttonStyle(PrimaryButtonStyle()) + .disabled(!loginInputValid) + + Button { + router.dismissScreen() + } label: { + Text("\(Image(systemName: "chevron.left")) \(L10n.Global.back)") + .foregroundColor(.Material.Elements.active) + .font(.body2) + } + } + } + .padding(.horizontal, 32) + } + } + + private var loginInputValid: Bool { + !jidStr.isEmpty && !pass.isEmpty && UniversalInputCollection.Validators.isEmail(jidStr) + } + + private func tryLogin() async { + do { + // try await clientsStore.tryLogin(jidStr, pass) + router.dismissScreen() + } catch { + router.showAlert( + .alert, + title: L10n.Global.Error.title, + subtitle: L10n.Login.error + ) { + Button(L10n.Global.ok, role: .cancel) { + router.dismissModal() + } + } + } + } +} diff --git a/Monal/another.im/Views/RegistrationScreen.swift b/Monal/another.im/Views/RegistrationScreen.swift new file mode 100644 index 0000000..16b095d --- /dev/null +++ b/Monal/another.im/Views/RegistrationScreen.swift @@ -0,0 +1,20 @@ +import SwiftUI + +struct RegistrationScreen: View { + @Environment(\.router) var router + + public var body: some View { + ZStack { + Color.Material.Background.light + Button { + router.dismissScreen() + } label: { + VStack { + Text("Not yet implemented") + Text(L10n.Global.back) + } + } + } + .ignoresSafeArea() + } +} diff --git a/Monal/another.im/Views/RootView.swift b/Monal/another.im/Views/RootView.swift index 9a94353..8c34747 100644 --- a/Monal/another.im/Views/RootView.swift +++ b/Monal/another.im/Views/RootView.swift @@ -14,14 +14,12 @@ struct RootView: View { .ignoresSafeArea() .onAppear { switch wrapper.accountsAvailability { - case .noAccounts: - break + case .noAccounts, .allDisabled: + WelcomeScreen() case .someEnabled: - break - - case .allDisabled: - break + // here will be main flow + EmptyView() } } } diff --git a/Monal/another.im/Views/SharedComponents/LoadingScreen.swift b/Monal/another.im/Views/SharedComponents/LoadingScreen.swift new file mode 100644 index 0000000..154987a --- /dev/null +++ b/Monal/another.im/Views/SharedComponents/LoadingScreen.swift @@ -0,0 +1,23 @@ +import SwiftUI + +struct LoadingScreen: View { + var body: some View { + GeometryReader { geo in + ZStack { + // background with opacity + Color.Material.Elements.active.opacity(0.3) + .frame(maxWidth: .infinity, maxHeight: .infinity) + + // loader + ProgressView() + .progressViewStyle( + CircularProgressViewStyle(tint: .Material.Elements.active) + ) + .position(x: geo.size.width / 2, y: geo.size.height / 2) + .controlSize(.large) + } + } + .ignoresSafeArea() + .transition(AnyTransition.opacity.animation(.easeInOut(duration: 0.1))) + } +} diff --git a/Monal/another.im/Views/SharedComponents/SharedListRow.swift b/Monal/another.im/Views/SharedComponents/SharedListRow.swift new file mode 100644 index 0000000..a453172 --- /dev/null +++ b/Monal/another.im/Views/SharedComponents/SharedListRow.swift @@ -0,0 +1,84 @@ +import SwiftUI + +enum SharedListRowIconType { + case charCircle(String) + case image(Image, Color) + case none +} + +enum SharedListRowControlType { + case none + case switcher(isOn: Binding) +} + +struct SharedListRow: View { + let iconType: SharedListRowIconType + let text: String + let controlType: SharedListRowControlType + + var body: some View { + VStack(spacing: 0) { + HStack(spacing: 8) { + // Icon + switch iconType { + case .charCircle(let str): + let char = str.firstLetter + let color = str.firstLetterColor + ZStack { + Circle() + .frame(width: 44, height: 44) + .foregroundColor(color) + Text(char) + .foregroundColor(.white) + .font(.body1) + } + + case .image(let image, let color): + ZStack { + Circle() + .frame(width: 44, height: 44) + .foregroundColor(.clearTappable) + .overlay { + image + .foregroundColor(color) + } + } + + case .none: + Rectangle() + .fill(Color.clear) + .frame(width: 0.1, height: 44) + } + + // Text + Text(text) + .foregroundColor(Color.Material.Text.main) + .font(.body2) + Spacer() + + // If control is needed + switch controlType { + case .none: + Rectangle() + .fill(Color.clear) + .frame(width: 0.1, height: 44) + + case .switcher(let isOn): + Toggle("", isOn: isOn) + .toggleStyle(SwitchToggleStyle(tint: .Material.Elements.active)) + .frame(width: 49, height: 31) + } + } + .padding(.horizontal, 16) + .padding(.vertical, 4) + Rectangle() + .frame(maxWidth: .infinity) + .frame(height: 1) + .foregroundColor(.Material.Background.dark) + } + .listRowInsets(.zero) + .listRowSeparator(.hidden) + .frame(maxWidth: .infinity, maxHeight: .infinity) + .background(Color.Material.Background.light) + } +} diff --git a/Monal/another.im/Views/SharedComponents/SharedNavigationBar.swift b/Monal/another.im/Views/SharedComponents/SharedNavigationBar.swift new file mode 100644 index 0000000..5587122 --- /dev/null +++ b/Monal/another.im/Views/SharedComponents/SharedNavigationBar.swift @@ -0,0 +1,90 @@ +import SwiftUI + +struct SharedNavBarButton: View { + let image: Image? + let action: () -> Void + var isEnabled: Bool = true + + init( + image: Image, + action: @escaping () -> Void, + isEnabled: Bool = true + ) { + self.image = image + self.action = action + self.isEnabled = isEnabled + } + + var body: some View { + Button { + action() + } label: { + image + .foregroundColor(isEnabled ? .Material.Elements.active : .Material.Elements.inactive) + .tappablePadding(.symmetric(12)) { + action() + } + } + .disabled(!isEnabled) + } +} + +struct SharedNavBarText: View { + let text: String + let action: (() -> Void)? + + init( + text: String, + action: (() -> Void)? = nil + ) { + self.text = text + self.action = action + } + + var body: some View { + Text(text) + .font(.head2) + .foregroundColor(.Material.Text.main) + .tappablePadding(.init(top: 8, leading: 24, bottom: 8, trailing: 24)) { + action?() + } + } +} + +struct SharedNavigationBar: View { + var leftButton: SharedNavBarButton? + var centerText: SharedNavBarText? + var rightButton: SharedNavBarButton? + + var body: some View { + ZStack { + Color.Material.Background.dark + .ignoresSafeArea() + + VStack { + if centerText != nil { + centerText + } + } + Spacer() + + HStack(alignment: .center) { + VStack { + if leftButton != nil { + leftButton?.padding() + } + } + .frame(minWidth: 40) + Spacer() + VStack { + if rightButton != nil { + rightButton? + .padding() + } + } + .frame(minWidth: 40) + } + } + .frame(height: 44) + } +} diff --git a/Monal/another.im/Views/SharedComponents/SharedSectionTitle.swift b/Monal/another.im/Views/SharedComponents/SharedSectionTitle.swift new file mode 100644 index 0000000..128afe9 --- /dev/null +++ b/Monal/another.im/Views/SharedComponents/SharedSectionTitle.swift @@ -0,0 +1,21 @@ +import SwiftUI + +struct SharedSectionTitle: View { + let text: String + + var body: some View { + HStack(spacing: 0) { + // Text + Text(text) + .foregroundColor(Color.Material.Text.sub) + .font(.body3) + .padding(.leading, 16) + .padding(.top, 16) + Spacer() + } + .listRowInsets(.zero) + .listRowSeparator(.hidden) + .frame(maxWidth: .infinity, maxHeight: .infinity) + .background(Color.Material.Background.light) + } +} diff --git a/Monal/another.im/Views/SharedComponents/UniversalInputCollection.swift b/Monal/another.im/Views/SharedComponents/UniversalInputCollection.swift new file mode 100644 index 0000000..fb8b5f8 --- /dev/null +++ b/Monal/another.im/Views/SharedComponents/UniversalInputCollection.swift @@ -0,0 +1,203 @@ +import SwiftUI + +// MARK: Public +protocol UniversalInputSelectionElement: Identifiable, Equatable, Hashable { + var icon: Image? { get } + var text: String? { get } +} + +public enum UniversalInputCollection { + struct TextField { + let prompt: String + @Binding var text: String + var focus: FocusState.Binding + var fieldType: T + let contentType: UITextContentType + let keyboardType: UIKeyboardType + let submitLabel: SubmitLabel + let action: () -> Void + } + + struct SecureField { + let prompt: String + @Binding var text: String + var focus: FocusState.Binding + var fieldType: T + let submitLabel: SubmitLabel + let action: () -> Void + } + + struct DropDownMenu { + let prompt: String + let elements: [E] + @Binding var selected: E? + var focus: FocusState.Binding + var fieldType: T + } +} + +// MARK: Inputs implementations +extension UniversalInputCollection.TextField: View { + var body: some View { + TextField("", text: $text) + .padding(.horizontal, 8) + .focused(focus, equals: fieldType) + .font(.body2) + .foregroundColor(.Material.Text.main) + .autocorrectionDisabled(true) + .autocapitalization(.none) + .textContentType(contentType) + .keyboardType(keyboardType) + .submitLabel(submitLabel) + .textSelection(.enabled) + .onSubmit { + action() + } + .modifier(UniversalInputModifier( + prompt: prompt, + focus: focus, + fieldType: fieldType, + isActive: isFilled + )) + } + + var isFilled: Bool { + !text.isEmpty || focus.wrappedValue == fieldType + } +} + +extension UniversalInputCollection.SecureField: View { + var body: some View { + SecureField("", text: $text) + .padding(.horizontal, 8) + .focused(focus, equals: fieldType) + .font(.body2) + .foregroundColor(.Material.Text.main) + .autocorrectionDisabled(true) + .autocapitalization(.none) + .textContentType(.password) + .submitLabel(submitLabel) + .textSelection(.disabled) + .onSubmit { + action() + } + .modifier(UniversalInputModifier( + prompt: prompt, + focus: focus, + fieldType: fieldType, + isActive: isFilled + )) + } + + var isFilled: Bool { + !text.isEmpty || focus.wrappedValue == fieldType + } +} + +extension UniversalInputCollection.DropDownMenu: View { + var body: some View { + ZStack { + HStack { + Text(text) + .font(.body2) + .foregroundColor(.Material.Text.main) + .padding(.leading, 8) + Spacer() + } + .modifier(UniversalInputModifier( + prompt: prompt, + focus: focus, + fieldType: fieldType, + isActive: selected != nil + )) + + Menu { + ForEach(elements, id: \.self.id) { element in + Button { + selected = element + } label: { + Text(element.text ?? "") + } + } + } label: { + Label("", image: "") + .labelStyle(TitleOnlyLabelStyle()) + .padding(.vertical) + .frame(height: 48) + .frame(maxWidth: .infinity) + } + } + } + + var text: String { + if let text = selected?.text { + return text + } else { + return "" + } + } +} + +// MARK: Modifiers +private struct UniversalInputModifier: ViewModifier { + let prompt: String + var focus: FocusState.Binding + var fieldType: T + let isActive: Bool + var promptBackground: Color? + var isCentered: Bool? + var customTapAction: (() -> Void)? + + func body(content: Content) -> some View { + VStack(spacing: 0) { + ZStack { + HStack { + Text(isActive ? "" : prompt) + .font(.body2) + .foregroundColor(.Material.Shape.separator) + .padding(8) + Spacer() + } + content + .frame(height: 48) + } + } + .frame(height: 48) + .background { + ZStack { + RoundedRectangle(cornerRadius: 4) + .foregroundColor(.Material.Shape.white) + RoundedRectangle(cornerRadius: 4) + .stroke(Color.Material.Shape.separator) + } + } + .contentShape(Rectangle()) + .onTapGesture { + if let customTapAction { + customTapAction() + } else { + if focus.wrappedValue != fieldType { + focus.wrappedValue = fieldType + } + } + } + } +} + +// MARK: Validators +extension UniversalInputCollection { + enum Validators { + static func isEmail(_ input: String) -> Bool { + if !input.isEmpty { + let mailRegex = "[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}" + if !NSPredicate(format: "SELF MATCHES %@", mailRegex).evaluate(with: input) { + return false + } else { + return true + } + } else { + return true + } + } + } +} diff --git a/Monal/another.im/Views/WelcomeScreen.swift b/Monal/another.im/Views/WelcomeScreen.swift new file mode 100644 index 0000000..bc6201d --- /dev/null +++ b/Monal/another.im/Views/WelcomeScreen.swift @@ -0,0 +1,79 @@ +import SwiftUI + +struct WelcomeScreen: View { + @EnvironmentObject var wrapper: MonalXmppWrapper + @Environment(\.router) var router + + var body: some View { + ZStack { + // background + Color.Material.Background.light + .ignoresSafeArea() + + if wrapper.accountsAvailability == .allDisabled { + VStack { + HStack { + Spacer() + Image(systemName: "gear") + .foregroundColor(.Material.Elements.active) + .tappablePadding(.symmetric(10)) { + router.showScreen(.push) { _ in + EmptyView() + // SettingsScreen() + // .environment(\.settingsParent, .welcome) + // .navigationBarHidden(true) + } + } + } + .padding() + Spacer() + } + } + + // content + VStack(spacing: 32) { + // icon + Image.aimLogo + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: 120, height: 120) + + // texts + VStack(spacing: 10) { + Text(L10n.Global.name) + .font(.head1r) + .foregroundColor(.Material.Text.main) + .fixedSize(horizontal: true, vertical: false) + Text(L10n.Start.subtitle) + .font(.body2) + .foregroundColor(.Material.Text.sub) + .fixedSize(horizontal: false, vertical: true) + .multilineTextAlignment(.center) + } + + // buttons + VStack(spacing: 16) { + Button { + router.showScreen(.push) { _ in + LoginScreen() + .navigationBarBackButtonHidden(true) + } + } label: { + Text(L10n.Start.Btn.login) + } + .buttonStyle(SecondaryButtonStyle()) + Button { + router.showScreen(.push) { _ in + RegistrationScreen() + .navigationBarBackButtonHidden(true) + } + } label: { + Text(L10n.Start.Btn.register) + } + .buttonStyle(PrimaryButtonStyle()) + } + } + .padding(.horizontal, 32) + } + } +} diff --git a/Monal/another.im/XMPP/AppError.swift b/Monal/another.im/XMPP/AppError.swift new file mode 100644 index 0000000..bacb47a --- /dev/null +++ b/Monal/another.im/XMPP/AppError.swift @@ -0,0 +1,12 @@ +enum AppError: Error { + case clientNotFound + case rosterNotFound + case imageNotFound + case videoNotFound + case noData + case fileTooBig + case invalidContentType + case invalidLocalName + case featureNotSupported + case securityError +}