diff --git a/.gitignore b/.gitignore index 22bd55b..543f622 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,18 @@ target/ /target Cargo.lock + +## iOS +## User settings +xcuserdata/ + +# Xcode +*.xcworkspace +*.xcuserstate +*.xccheckout + +# Swift Package Manager +/.build + +# CocoaPods +Pods/ \ No newline at end of file diff --git a/iroh-console-ios/IrohConsole.xcodeproj/project.pbxproj b/iroh-console-ios/IrohConsole.xcodeproj/project.pbxproj new file mode 100644 index 0000000..92a68a5 --- /dev/null +++ b/iroh-console-ios/IrohConsole.xcodeproj/project.pbxproj @@ -0,0 +1,956 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 60; + objects = { + +/* Begin PBXBuildFile section */ + 4226587D2AB100D5000BCD34 /* UserSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4226587C2AB100D5000BCD34 /* UserSession.swift */; }; + 4226587F2AB1EE52000BCD34 /* ImagePicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4226587E2AB1EE52000BCD34 /* ImagePicker.swift */; }; + 422658812AB24925000BCD34 /* AnchorEgressSummaryStats.swift in Sources */ = {isa = PBXBuildFile; fileRef = 422658802AB24925000BCD34 /* AnchorEgressSummaryStats.swift */; }; + 422658832AB24D8A000BCD34 /* Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 422658822AB24D8A000BCD34 /* Extensions.swift */; }; + 422658852AB25C4E000BCD34 /* MockData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 422658842AB25C4E000BCD34 /* MockData.swift */; }; + 422658872AB25E6A000BCD34 /* UserProfileRound.swift in Sources */ = {isa = PBXBuildFile; fileRef = 422658862AB25E6A000BCD34 /* UserProfileRound.swift */; }; + 422658892AB38399000BCD34 /* NodeHome.swift in Sources */ = {isa = PBXBuildFile; fileRef = 422658882AB38399000BCD34 /* NodeHome.swift */; }; + 4226588B2AB3EECF000BCD34 /* ShareDoc.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4226588A2AB3EECF000BCD34 /* ShareDoc.swift */; }; + 4226588D2AB3EFBA000BCD34 /* PickDocument.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4226588C2AB3EFBA000BCD34 /* PickDocument.swift */; }; + 422658932ABA2441000BCD34 /* UserProfile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 422658922ABA2441000BCD34 /* UserProfile.swift */; }; + 42344C2D2A71B25D001CE259 /* SpaceMono-Regular.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 42344C242A71B25D001CE259 /* SpaceMono-Regular.ttf */; }; + 42344C2E2A71B25D001CE259 /* SpaceGrotesk-Bold.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 42344C252A71B25D001CE259 /* SpaceGrotesk-Bold.ttf */; }; + 42344C2F2A71B25D001CE259 /* SpaceMono-BoldItalic.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 42344C262A71B25D001CE259 /* SpaceMono-BoldItalic.ttf */; }; + 42344C302A71B25D001CE259 /* SpaceMono-Bold.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 42344C272A71B25D001CE259 /* SpaceMono-Bold.ttf */; }; + 42344C312A71B25D001CE259 /* SpaceGrotesk-Light.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 42344C282A71B25D001CE259 /* SpaceGrotesk-Light.ttf */; }; + 42344C322A71B25D001CE259 /* SpaceMono-Italic.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 42344C292A71B25D001CE259 /* SpaceMono-Italic.ttf */; }; + 42344C332A71B25D001CE259 /* SpaceGrotesk-Medium.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 42344C2A2A71B25D001CE259 /* SpaceGrotesk-Medium.ttf */; }; + 42344C342A71B25D001CE259 /* SpaceGrotesk-SemiBold.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 42344C2B2A71B25D001CE259 /* SpaceGrotesk-SemiBold.ttf */; }; + 42344C352A71B25D001CE259 /* SpaceGrotesk-Regular.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 42344C2C2A71B25D001CE259 /* SpaceGrotesk-Regular.ttf */; }; + 42344C372A71C213001CE259 /* Document.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42344C362A71C213001CE259 /* Document.swift */; }; + 42344C3A2A71E9F6001CE259 /* TestHome.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42344C392A71E9F6001CE259 /* TestHome.swift */; }; + 42436ADD2A8321AB00D5EB53 /* IrohNodeManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42436ADC2A8321AB00D5EB53 /* IrohNodeManager.swift */; }; + 4256A1852A9C447300D7758E /* DocEntry.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4256A1842A9C447300D7758E /* DocEntry.swift */; }; + 425A9E992A7DC4DF00268BC9 /* Welcome.swift in Sources */ = {isa = PBXBuildFile; fileRef = 425A9E982A7DC4DF00268BC9 /* Welcome.swift */; }; + 425A9E9B2A7DC59000268BC9 /* Login.swift in Sources */ = {isa = PBXBuildFile; fileRef = 425A9E9A2A7DC59000268BC9 /* Login.swift */; }; + 425A9E9D2A7DC59A00268BC9 /* Signup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 425A9E9C2A7DC59A00268BC9 /* Signup.swift */; }; + 426195322ABD033B00693CB5 /* NewDoc.swift in Sources */ = {isa = PBXBuildFile; fileRef = 426195312ABD033B00693CB5 /* NewDoc.swift */; }; + 4263A46C2A83DEE9004D8A49 /* IrohAnchorAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4263A46B2A83DEE9004D8A49 /* IrohAnchorAPI.swift */; }; + 4263A46F2A83DFF8004D8A49 /* IrohAnchorAPITypes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4263A46E2A83DFF8004D8A49 /* IrohAnchorAPITypes.swift */; }; + 4263A4712A83E0C6004D8A49 /* IrohAnchorAPIClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4263A4702A83E0C6004D8A49 /* IrohAnchorAPIClient.swift */; }; + 4263A4732A843C85004D8A49 /* JoinDocSyncTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4263A4722A843C85004D8A49 /* JoinDocSyncTest.swift */; }; + 426572562A72000100D96F8D /* EditConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 426572552A72000100D96F8D /* EditConfig.swift */; }; + 426572582A72045100D96F8D /* DocEvents.json in Resources */ = {isa = PBXBuildFile; fileRef = 426572572A72045100D96F8D /* DocEvents.json */; }; + 4265725A2A720F8B00D96F8D /* DocSyncTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 426572592A720F8B00D96F8D /* DocSyncTest.swift */; }; + 4265725C2A72160500D96F8D /* NewDocSyncTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4265725B2A72160500D96F8D /* NewDocSyncTest.swift */; }; + 4265725E2A72198B00D96F8D /* TestLobby.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4265725D2A72198B00D96F8D /* TestLobby.swift */; }; + 426572612A72E71100D96F8D /* Topbar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 426572602A72E71100D96F8D /* Topbar.swift */; }; + 426572632A72E85200D96F8D /* DailyEgressBarChart.swift in Sources */ = {isa = PBXBuildFile; fileRef = 426572622A72E85200D96F8D /* DailyEgressBarChart.swift */; }; + 426572652A72F90400D96F8D /* Images in Resources */ = {isa = PBXBuildFile; fileRef = 426572642A72F90400D96F8D /* Images */; }; + 4266B64B2B5B1F28006DE0D5 /* IrohLib in Frameworks */ = {isa = PBXBuildFile; productRef = 4266B64A2B5B1F28006DE0D5 /* IrohLib */; }; + 42A4F3FC2ACCF96400F4A9F4 /* CodeScanner in Frameworks */ = {isa = PBXBuildFile; productRef = 42A4F3FB2ACCF96400F4A9F4 /* CodeScanner */; }; + 42B934382A5504A60085D27D /* AnchorsList.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42B934372A5504A60085D27D /* AnchorsList.swift */; }; + 42B9343A2A5505820085D27D /* Network.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42B934392A5505820085D27D /* Network.swift */; }; + 42B9343C2A5506910085D27D /* ModelData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42B9343B2A5506910085D27D /* ModelData.swift */; }; + 42B9343E2A5507150085D27D /* Networks.json in Resources */ = {isa = PBXBuildFile; fileRef = 42B9343D2A5507150085D27D /* Networks.json */; }; + 42B934432A5509580085D27D /* AnchorHome.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42B934422A5509580085D27D /* AnchorHome.swift */; }; + 42B934452A55098F0085D27D /* Modes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42B934442A55098F0085D27D /* Modes.swift */; }; + 42B934582A5DB5730085D27D /* AddAnchor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42B934572A5DB5730085D27D /* AddAnchor.swift */; }; + 42BFCCA62A984949006CC2AB /* Console.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42BFCCA52A984949006CC2AB /* Console.swift */; }; + 42F69E422A1C5002006F772D /* irohApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42F69E412A1C5002006F772D /* irohApp.swift */; }; + 42F69E442A1C5002006F772D /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42F69E432A1C5002006F772D /* ContentView.swift */; }; + 42F69E462A1C5003006F772D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 42F69E452A1C5003006F772D /* Assets.xcassets */; }; + 42F69E492A1C5003006F772D /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 42F69E482A1C5003006F772D /* Preview Assets.xcassets */; }; + 42F69E532A1C5003006F772D /* irohTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42F69E522A1C5003006F772D /* irohTests.swift */; }; + 42F69E5D2A1C5003006F772D /* irohUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42F69E5C2A1C5003006F772D /* irohUITests.swift */; }; + 42F69E5F2A1C5003006F772D /* irohUITestsLaunchTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42F69E5E2A1C5003006F772D /* irohUITestsLaunchTests.swift */; }; + 42FE7F332A98B664007DE329 /* SystemConfiguration.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 42F69E762A1C7238006F772D /* SystemConfiguration.framework */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 42F69E4F2A1C5003006F772D /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 42F69E362A1C5002006F772D /* Project object */; + proxyType = 1; + remoteGlobalIDString = 42F69E3D2A1C5002006F772D; + remoteInfo = iroh; + }; + 42F69E592A1C5003006F772D /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 42F69E362A1C5002006F772D /* Project object */; + proxyType = 1; + remoteGlobalIDString = 42F69E3D2A1C5002006F772D; + remoteInfo = iroh; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXFileReference section */ + 4226587C2AB100D5000BCD34 /* UserSession.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserSession.swift; sourceTree = ""; }; + 4226587E2AB1EE52000BCD34 /* ImagePicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImagePicker.swift; sourceTree = ""; }; + 422658802AB24925000BCD34 /* AnchorEgressSummaryStats.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnchorEgressSummaryStats.swift; sourceTree = ""; }; + 422658822AB24D8A000BCD34 /* Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Extensions.swift; sourceTree = ""; }; + 422658842AB25C4E000BCD34 /* MockData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockData.swift; sourceTree = ""; }; + 422658862AB25E6A000BCD34 /* UserProfileRound.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserProfileRound.swift; sourceTree = ""; }; + 422658882AB38399000BCD34 /* NodeHome.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NodeHome.swift; sourceTree = ""; }; + 4226588A2AB3EECF000BCD34 /* ShareDoc.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareDoc.swift; sourceTree = ""; }; + 4226588C2AB3EFBA000BCD34 /* PickDocument.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PickDocument.swift; sourceTree = ""; }; + 422658922ABA2441000BCD34 /* UserProfile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserProfile.swift; sourceTree = ""; }; + 42344C242A71B25D001CE259 /* SpaceMono-Regular.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "SpaceMono-Regular.ttf"; sourceTree = ""; }; + 42344C252A71B25D001CE259 /* SpaceGrotesk-Bold.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "SpaceGrotesk-Bold.ttf"; sourceTree = ""; }; + 42344C262A71B25D001CE259 /* SpaceMono-BoldItalic.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "SpaceMono-BoldItalic.ttf"; sourceTree = ""; }; + 42344C272A71B25D001CE259 /* SpaceMono-Bold.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "SpaceMono-Bold.ttf"; sourceTree = ""; }; + 42344C282A71B25D001CE259 /* SpaceGrotesk-Light.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "SpaceGrotesk-Light.ttf"; sourceTree = ""; }; + 42344C292A71B25D001CE259 /* SpaceMono-Italic.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "SpaceMono-Italic.ttf"; sourceTree = ""; }; + 42344C2A2A71B25D001CE259 /* SpaceGrotesk-Medium.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "SpaceGrotesk-Medium.ttf"; sourceTree = ""; }; + 42344C2B2A71B25D001CE259 /* SpaceGrotesk-SemiBold.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "SpaceGrotesk-SemiBold.ttf"; sourceTree = ""; }; + 42344C2C2A71B25D001CE259 /* SpaceGrotesk-Regular.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "SpaceGrotesk-Regular.ttf"; sourceTree = ""; }; + 42344C362A71C213001CE259 /* Document.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Document.swift; sourceTree = ""; }; + 42344C392A71E9F6001CE259 /* TestHome.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestHome.swift; sourceTree = ""; }; + 42436ADC2A8321AB00D5EB53 /* IrohNodeManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IrohNodeManager.swift; sourceTree = ""; }; + 4256A1842A9C447300D7758E /* DocEntry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DocEntry.swift; sourceTree = ""; }; + 425A9E982A7DC4DF00268BC9 /* Welcome.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Welcome.swift; sourceTree = ""; }; + 425A9E9A2A7DC59000268BC9 /* Login.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Login.swift; sourceTree = ""; }; + 425A9E9C2A7DC59A00268BC9 /* Signup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Signup.swift; sourceTree = ""; }; + 426195312ABD033B00693CB5 /* NewDoc.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewDoc.swift; sourceTree = ""; }; + 4263A46B2A83DEE9004D8A49 /* IrohAnchorAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IrohAnchorAPI.swift; sourceTree = ""; }; + 4263A46E2A83DFF8004D8A49 /* IrohAnchorAPITypes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IrohAnchorAPITypes.swift; sourceTree = ""; }; + 4263A4702A83E0C6004D8A49 /* IrohAnchorAPIClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IrohAnchorAPIClient.swift; sourceTree = ""; }; + 4263A4722A843C85004D8A49 /* JoinDocSyncTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JoinDocSyncTest.swift; sourceTree = ""; }; + 426572552A72000100D96F8D /* EditConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditConfig.swift; sourceTree = ""; }; + 426572572A72045100D96F8D /* DocEvents.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = DocEvents.json; sourceTree = ""; }; + 426572592A720F8B00D96F8D /* DocSyncTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DocSyncTest.swift; sourceTree = ""; }; + 4265725B2A72160500D96F8D /* NewDocSyncTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewDocSyncTest.swift; sourceTree = ""; }; + 4265725D2A72198B00D96F8D /* TestLobby.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestLobby.swift; sourceTree = ""; }; + 426572602A72E71100D96F8D /* Topbar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Topbar.swift; sourceTree = ""; }; + 426572622A72E85200D96F8D /* DailyEgressBarChart.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DailyEgressBarChart.swift; sourceTree = ""; }; + 426572642A72F90400D96F8D /* Images */ = {isa = PBXFileReference; lastKnownFileType = folder; path = Images; sourceTree = ""; }; + 426572662A72FEA400D96F8D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; }; + 42B934372A5504A60085D27D /* AnchorsList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnchorsList.swift; sourceTree = ""; }; + 42B934392A5505820085D27D /* Network.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Network.swift; sourceTree = ""; }; + 42B9343B2A5506910085D27D /* ModelData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ModelData.swift; sourceTree = ""; }; + 42B9343D2A5507150085D27D /* Networks.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = Networks.json; sourceTree = ""; }; + 42B934422A5509580085D27D /* AnchorHome.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnchorHome.swift; sourceTree = ""; }; + 42B934442A55098F0085D27D /* Modes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Modes.swift; sourceTree = ""; }; + 42B934572A5DB5730085D27D /* AddAnchor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddAnchor.swift; sourceTree = ""; }; + 42BFCCA52A984949006CC2AB /* Console.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Console.swift; sourceTree = ""; }; + 42F69E3E2A1C5002006F772D /* IrohConsole.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = IrohConsole.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 42F69E412A1C5002006F772D /* irohApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = irohApp.swift; sourceTree = ""; }; + 42F69E432A1C5002006F772D /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; + 42F69E452A1C5003006F772D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 42F69E482A1C5003006F772D /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; + 42F69E4E2A1C5003006F772D /* IrohConsoleTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = IrohConsoleTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 42F69E522A1C5003006F772D /* irohTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = irohTests.swift; sourceTree = ""; }; + 42F69E582A1C5003006F772D /* IrohConsoleUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = IrohConsoleUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 42F69E5C2A1C5003006F772D /* irohUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = irohUITests.swift; sourceTree = ""; }; + 42F69E5E2A1C5003006F772D /* irohUITestsLaunchTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = irohUITestsLaunchTests.swift; sourceTree = ""; }; + 42F69E762A1C7238006F772D /* SystemConfiguration.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SystemConfiguration.framework; path = System/Library/Frameworks/SystemConfiguration.framework; sourceTree = SDKROOT; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 42F69E3B2A1C5002006F772D /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 4266B64B2B5B1F28006DE0D5 /* IrohLib in Frameworks */, + 42A4F3FC2ACCF96400F4A9F4 /* CodeScanner in Frameworks */, + 42FE7F332A98B664007DE329 /* SystemConfiguration.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 42F69E4B2A1C5003006F772D /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 42F69E552A1C5003006F772D /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 42344C232A71B1D6001CE259 /* Fonts */ = { + isa = PBXGroup; + children = ( + 42344C252A71B25D001CE259 /* SpaceGrotesk-Bold.ttf */, + 42344C282A71B25D001CE259 /* SpaceGrotesk-Light.ttf */, + 42344C2A2A71B25D001CE259 /* SpaceGrotesk-Medium.ttf */, + 42344C2C2A71B25D001CE259 /* SpaceGrotesk-Regular.ttf */, + 42344C2B2A71B25D001CE259 /* SpaceGrotesk-SemiBold.ttf */, + 42344C272A71B25D001CE259 /* SpaceMono-Bold.ttf */, + 42344C262A71B25D001CE259 /* SpaceMono-BoldItalic.ttf */, + 42344C292A71B25D001CE259 /* SpaceMono-Italic.ttf */, + 42344C242A71B25D001CE259 /* SpaceMono-Regular.ttf */, + ); + path = Fonts; + sourceTree = ""; + }; + 42344C382A71E9A7001CE259 /* Tests */ = { + isa = PBXGroup; + children = ( + 42344C392A71E9F6001CE259 /* TestHome.swift */, + 426572592A720F8B00D96F8D /* DocSyncTest.swift */, + 4265725B2A72160500D96F8D /* NewDocSyncTest.swift */, + 4265725D2A72198B00D96F8D /* TestLobby.swift */, + 4263A4722A843C85004D8A49 /* JoinDocSyncTest.swift */, + ); + path = Tests; + sourceTree = ""; + }; + 425A9EA12A8291FE00268BC9 /* Packages */ = { + isa = PBXGroup; + children = ( + ); + name = Packages; + sourceTree = ""; + }; + 4263A46D2A83DFB6004D8A49 /* iroh_dot_network_api */ = { + isa = PBXGroup; + children = ( + 4263A46B2A83DEE9004D8A49 /* IrohAnchorAPI.swift */, + 4263A46E2A83DFF8004D8A49 /* IrohAnchorAPITypes.swift */, + 4263A4702A83E0C6004D8A49 /* IrohAnchorAPIClient.swift */, + ); + path = iroh_dot_network_api; + sourceTree = ""; + }; + 426572542A71FFE900D96F8D /* Config */ = { + isa = PBXGroup; + children = ( + 426572552A72000100D96F8D /* EditConfig.swift */, + ); + path = Config; + sourceTree = ""; + }; + 4265725F2A72E6F300D96F8D /* Subviews */ = { + isa = PBXGroup; + children = ( + 426572602A72E71100D96F8D /* Topbar.swift */, + 426572622A72E85200D96F8D /* DailyEgressBarChart.swift */, + 4226587E2AB1EE52000BCD34 /* ImagePicker.swift */, + 422658802AB24925000BCD34 /* AnchorEgressSummaryStats.swift */, + 422658862AB25E6A000BCD34 /* UserProfileRound.swift */, + ); + path = Subviews; + sourceTree = ""; + }; + 42B9343F2A55085A0085D27D /* Model */ = { + isa = PBXGroup; + children = ( + 42B934392A5505820085D27D /* Network.swift */, + 42B9343B2A5506910085D27D /* ModelData.swift */, + 42B934442A55098F0085D27D /* Modes.swift */, + 42344C362A71C213001CE259 /* Document.swift */, + 4226587C2AB100D5000BCD34 /* UserSession.swift */, + 422658842AB25C4E000BCD34 /* MockData.swift */, + ); + path = Model; + sourceTree = ""; + }; + 42B934402A5508640085D27D /* Views */ = { + isa = PBXGroup; + children = ( + 42BFCCA42A98492D006CC2AB /* Console */, + 4265725F2A72E6F300D96F8D /* Subviews */, + 426572542A71FFE900D96F8D /* Config */, + 42344C382A71E9A7001CE259 /* Tests */, + 42B934562A5DB3940085D27D /* Onboarding */, + 42B934512A5AD67F0085D27D /* Anchors */, + 425A9E9A2A7DC59000268BC9 /* Login.swift */, + 422658882AB38399000BCD34 /* NodeHome.swift */, + 422658922ABA2441000BCD34 /* UserProfile.swift */, + ); + path = Views; + sourceTree = ""; + }; + 42B934412A55087C0085D27D /* Data */ = { + isa = PBXGroup; + children = ( + 426572572A72045100D96F8D /* DocEvents.json */, + 42B9343D2A5507150085D27D /* Networks.json */, + ); + path = Data; + sourceTree = ""; + }; + 42B934512A5AD67F0085D27D /* Anchors */ = { + isa = PBXGroup; + children = ( + 42B934372A5504A60085D27D /* AnchorsList.swift */, + 42B934422A5509580085D27D /* AnchorHome.swift */, + 42B934572A5DB5730085D27D /* AddAnchor.swift */, + ); + path = Anchors; + sourceTree = ""; + }; + 42B934562A5DB3940085D27D /* Onboarding */ = { + isa = PBXGroup; + children = ( + 425A9E982A7DC4DF00268BC9 /* Welcome.swift */, + 425A9E9C2A7DC59A00268BC9 /* Signup.swift */, + ); + path = Onboarding; + sourceTree = ""; + }; + 42BFCCA42A98492D006CC2AB /* Console */ = { + isa = PBXGroup; + children = ( + 42BFCCA52A984949006CC2AB /* Console.swift */, + 4256A1842A9C447300D7758E /* DocEntry.swift */, + 4226588A2AB3EECF000BCD34 /* ShareDoc.swift */, + 4226588C2AB3EFBA000BCD34 /* PickDocument.swift */, + 426195312ABD033B00693CB5 /* NewDoc.swift */, + ); + path = Console; + sourceTree = ""; + }; + 42F69E352A1C5002006F772D = { + isa = PBXGroup; + children = ( + 425A9EA12A8291FE00268BC9 /* Packages */, + 42F69E402A1C5002006F772D /* iroh */, + 42F69E512A1C5003006F772D /* irohTests */, + 42F69E5B2A1C5003006F772D /* irohUITests */, + 42F69E3F2A1C5002006F772D /* Products */, + 42F69E6D2A1C54BF006F772D /* Frameworks */, + ); + sourceTree = ""; + }; + 42F69E3F2A1C5002006F772D /* Products */ = { + isa = PBXGroup; + children = ( + 42F69E3E2A1C5002006F772D /* IrohConsole.app */, + 42F69E4E2A1C5003006F772D /* IrohConsoleTests.xctest */, + 42F69E582A1C5003006F772D /* IrohConsoleUITests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + 42F69E402A1C5002006F772D /* iroh */ = { + isa = PBXGroup; + children = ( + 426572662A72FEA400D96F8D /* Info.plist */, + 42F69E412A1C5002006F772D /* irohApp.swift */, + 42F69E432A1C5002006F772D /* ContentView.swift */, + 42436ADC2A8321AB00D5EB53 /* IrohNodeManager.swift */, + 42B934402A5508640085D27D /* Views */, + 42B9343F2A55085A0085D27D /* Model */, + 42B934412A55087C0085D27D /* Data */, + 4263A46D2A83DFB6004D8A49 /* iroh_dot_network_api */, + 42F69E452A1C5003006F772D /* Assets.xcassets */, + 426572642A72F90400D96F8D /* Images */, + 42344C232A71B1D6001CE259 /* Fonts */, + 42F69E472A1C5003006F772D /* Preview Content */, + 422658822AB24D8A000BCD34 /* Extensions.swift */, + ); + path = iroh; + sourceTree = ""; + }; + 42F69E472A1C5003006F772D /* Preview Content */ = { + isa = PBXGroup; + children = ( + 42F69E482A1C5003006F772D /* Preview Assets.xcassets */, + ); + path = "Preview Content"; + sourceTree = ""; + }; + 42F69E512A1C5003006F772D /* irohTests */ = { + isa = PBXGroup; + children = ( + 42F69E522A1C5003006F772D /* irohTests.swift */, + ); + path = irohTests; + sourceTree = ""; + }; + 42F69E5B2A1C5003006F772D /* irohUITests */ = { + isa = PBXGroup; + children = ( + 42F69E5C2A1C5003006F772D /* irohUITests.swift */, + 42F69E5E2A1C5003006F772D /* irohUITestsLaunchTests.swift */, + ); + path = irohUITests; + sourceTree = ""; + }; + 42F69E6D2A1C54BF006F772D /* Frameworks */ = { + isa = PBXGroup; + children = ( + 42F69E762A1C7238006F772D /* SystemConfiguration.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 42F69E3D2A1C5002006F772D /* IrohConsole */ = { + isa = PBXNativeTarget; + buildConfigurationList = 42F69E622A1C5003006F772D /* Build configuration list for PBXNativeTarget "IrohConsole" */; + buildPhases = ( + 42F69E3A2A1C5002006F772D /* Sources */, + 42F69E3B2A1C5002006F772D /* Frameworks */, + 42F69E3C2A1C5002006F772D /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = IrohConsole; + packageProductDependencies = ( + 42A4F3FB2ACCF96400F4A9F4 /* CodeScanner */, + 4266B64A2B5B1F28006DE0D5 /* IrohLib */, + ); + productName = iroh; + productReference = 42F69E3E2A1C5002006F772D /* IrohConsole.app */; + productType = "com.apple.product-type.application"; + }; + 42F69E4D2A1C5003006F772D /* IrohConsoleTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 42F69E652A1C5003006F772D /* Build configuration list for PBXNativeTarget "IrohConsoleTests" */; + buildPhases = ( + 42F69E4A2A1C5003006F772D /* Sources */, + 42F69E4B2A1C5003006F772D /* Frameworks */, + 42F69E4C2A1C5003006F772D /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 42F69E502A1C5003006F772D /* PBXTargetDependency */, + ); + name = IrohConsoleTests; + productName = irohTests; + productReference = 42F69E4E2A1C5003006F772D /* IrohConsoleTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + 42F69E572A1C5003006F772D /* IrohConsoleUITests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 42F69E682A1C5003006F772D /* Build configuration list for PBXNativeTarget "IrohConsoleUITests" */; + buildPhases = ( + 42F69E542A1C5003006F772D /* Sources */, + 42F69E552A1C5003006F772D /* Frameworks */, + 42F69E562A1C5003006F772D /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 42F69E5A2A1C5003006F772D /* PBXTargetDependency */, + ); + name = IrohConsoleUITests; + productName = irohUITests; + productReference = 42F69E582A1C5003006F772D /* IrohConsoleUITests.xctest */; + productType = "com.apple.product-type.bundle.ui-testing"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 42F69E362A1C5002006F772D /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = 1; + LastSwiftUpdateCheck = 1430; + LastUpgradeCheck = 1500; + TargetAttributes = { + 42F69E3D2A1C5002006F772D = { + CreatedOnToolsVersion = 14.3; + }; + 42F69E4D2A1C5003006F772D = { + CreatedOnToolsVersion = 14.3; + TestTargetID = 42F69E3D2A1C5002006F772D; + }; + 42F69E572A1C5003006F772D = { + CreatedOnToolsVersion = 14.3; + TestTargetID = 42F69E3D2A1C5002006F772D; + }; + }; + }; + buildConfigurationList = 42F69E392A1C5002006F772D /* Build configuration list for PBXProject "IrohConsole" */; + compatibilityVersion = "Xcode 14.0"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 42F69E352A1C5002006F772D; + packageReferences = ( + 42A4F3FA2ACCF96400F4A9F4 /* XCRemoteSwiftPackageReference "CodeScanner" */, + 4266B6492B5B1F28006DE0D5 /* XCLocalSwiftPackageReference "../iroh-ffi/IrohLib" */, + ); + productRefGroup = 42F69E3F2A1C5002006F772D /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 42F69E3D2A1C5002006F772D /* IrohConsole */, + 42F69E4D2A1C5003006F772D /* IrohConsoleTests */, + 42F69E572A1C5003006F772D /* IrohConsoleUITests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 42F69E3C2A1C5002006F772D /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 42344C332A71B25D001CE259 /* SpaceGrotesk-Medium.ttf in Resources */, + 42F69E492A1C5003006F772D /* Preview Assets.xcassets in Resources */, + 426572652A72F90400D96F8D /* Images in Resources */, + 42344C352A71B25D001CE259 /* SpaceGrotesk-Regular.ttf in Resources */, + 42344C2F2A71B25D001CE259 /* SpaceMono-BoldItalic.ttf in Resources */, + 42344C312A71B25D001CE259 /* SpaceGrotesk-Light.ttf in Resources */, + 42344C2D2A71B25D001CE259 /* SpaceMono-Regular.ttf in Resources */, + 42344C2E2A71B25D001CE259 /* SpaceGrotesk-Bold.ttf in Resources */, + 42B9343E2A5507150085D27D /* Networks.json in Resources */, + 42344C322A71B25D001CE259 /* SpaceMono-Italic.ttf in Resources */, + 426572582A72045100D96F8D /* DocEvents.json in Resources */, + 42344C302A71B25D001CE259 /* SpaceMono-Bold.ttf in Resources */, + 42F69E462A1C5003006F772D /* Assets.xcassets in Resources */, + 42344C342A71B25D001CE259 /* SpaceGrotesk-SemiBold.ttf in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 42F69E4C2A1C5003006F772D /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 42F69E562A1C5003006F772D /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 42F69E3A2A1C5002006F772D /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 42B934432A5509580085D27D /* AnchorHome.swift in Sources */, + 42BFCCA62A984949006CC2AB /* Console.swift in Sources */, + 422658892AB38399000BCD34 /* NodeHome.swift in Sources */, + 422658872AB25E6A000BCD34 /* UserProfileRound.swift in Sources */, + 4226587F2AB1EE52000BCD34 /* ImagePicker.swift in Sources */, + 4256A1852A9C447300D7758E /* DocEntry.swift in Sources */, + 426572612A72E71100D96F8D /* Topbar.swift in Sources */, + 42344C3A2A71E9F6001CE259 /* TestHome.swift in Sources */, + 425A9E9D2A7DC59A00268BC9 /* Signup.swift in Sources */, + 4263A4712A83E0C6004D8A49 /* IrohAnchorAPIClient.swift in Sources */, + 426572562A72000100D96F8D /* EditConfig.swift in Sources */, + 426572632A72E85200D96F8D /* DailyEgressBarChart.swift in Sources */, + 42B9343A2A5505820085D27D /* Network.swift in Sources */, + 4265725C2A72160500D96F8D /* NewDocSyncTest.swift in Sources */, + 42B934382A5504A60085D27D /* AnchorsList.swift in Sources */, + 425A9E992A7DC4DF00268BC9 /* Welcome.swift in Sources */, + 4265725E2A72198B00D96F8D /* TestLobby.swift in Sources */, + 4263A46C2A83DEE9004D8A49 /* IrohAnchorAPI.swift in Sources */, + 4226587D2AB100D5000BCD34 /* UserSession.swift in Sources */, + 42B9343C2A5506910085D27D /* ModelData.swift in Sources */, + 422658812AB24925000BCD34 /* AnchorEgressSummaryStats.swift in Sources */, + 4226588B2AB3EECF000BCD34 /* ShareDoc.swift in Sources */, + 422658932ABA2441000BCD34 /* UserProfile.swift in Sources */, + 42F69E442A1C5002006F772D /* ContentView.swift in Sources */, + 4226588D2AB3EFBA000BCD34 /* PickDocument.swift in Sources */, + 422658832AB24D8A000BCD34 /* Extensions.swift in Sources */, + 42436ADD2A8321AB00D5EB53 /* IrohNodeManager.swift in Sources */, + 42B934582A5DB5730085D27D /* AddAnchor.swift in Sources */, + 4263A4732A843C85004D8A49 /* JoinDocSyncTest.swift in Sources */, + 425A9E9B2A7DC59000268BC9 /* Login.swift in Sources */, + 4265725A2A720F8B00D96F8D /* DocSyncTest.swift in Sources */, + 4263A46F2A83DFF8004D8A49 /* IrohAnchorAPITypes.swift in Sources */, + 426195322ABD033B00693CB5 /* NewDoc.swift in Sources */, + 42B934452A55098F0085D27D /* Modes.swift in Sources */, + 422658852AB25C4E000BCD34 /* MockData.swift in Sources */, + 42F69E422A1C5002006F772D /* irohApp.swift in Sources */, + 42344C372A71C213001CE259 /* Document.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 42F69E4A2A1C5003006F772D /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 42F69E532A1C5003006F772D /* irohTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 42F69E542A1C5003006F772D /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 42F69E5F2A1C5003006F772D /* irohUITestsLaunchTests.swift in Sources */, + 42F69E5D2A1C5003006F772D /* irohUITests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 42F69E502A1C5003006F772D /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 42F69E3D2A1C5002006F772D /* IrohConsole */; + targetProxy = 42F69E4F2A1C5003006F772D /* PBXContainerItemProxy */; + }; + 42F69E5A2A1C5003006F772D /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 42F69E3D2A1C5002006F772D /* IrohConsole */; + targetProxy = 42F69E592A1C5003006F772D /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin XCBuildConfiguration section */ + 42F69E602A1C5003006F772D /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + 42F69E612A1C5003006F772D /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 42F69E632A1C5003006F772D /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_ASSET_PATHS = "\"iroh/Preview Content\""; + DEVELOPMENT_TEAM = 7K8LD2955M; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + ENABLE_PREVIEWS = YES; + EXCLUDED_ARCHS = ""; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = iroh/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = Iroh; + INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.developer-tools"; + INFOPLIST_KEY_NSCameraUsageDescription = "Used to scan QR codes with network configuration info"; + INFOPLIST_KEY_NSPhotoLibraryUsageDescription = "Used for adding photos to documents"; + INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UIUserInterfaceStyle = Light; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.1.1; + OTHER_LDFLAGS = "-v"; + PRODUCT_BUNDLE_IDENTIFIER = computer.iroh.console; + PRODUCT_NAME = IrohConsole; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 42F69E642A1C5003006F772D /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_ASSET_PATHS = "\"iroh/Preview Content\""; + DEVELOPMENT_TEAM = 7K8LD2955M; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + ENABLE_PREVIEWS = YES; + EXCLUDED_ARCHS = ""; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = iroh/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = Iroh; + INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.developer-tools"; + INFOPLIST_KEY_NSCameraUsageDescription = "Used to scan QR codes with network configuration info"; + INFOPLIST_KEY_NSPhotoLibraryUsageDescription = "Used for adding photos to documents"; + INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UIUserInterfaceStyle = Light; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.1.1; + OTHER_LDFLAGS = "-v"; + PRODUCT_BUNDLE_IDENTIFIER = computer.iroh.console; + PRODUCT_NAME = IrohConsole; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; + 42F69E662A1C5003006F772D /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = 5A7G78D7YK; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 16.4; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = computer.iroh.irohTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/IrohConsole.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/IrohConsole"; + }; + name = Debug; + }; + 42F69E672A1C5003006F772D /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = 5A7G78D7YK; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 16.4; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = computer.iroh.irohTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/IrohConsole.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/IrohConsole"; + }; + name = Release; + }; + 42F69E692A1C5003006F772D /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = 5A7G78D7YK; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = computer.iroh.irohUITests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_TARGET_NAME = iroh; + }; + name = Debug; + }; + 42F69E6A2A1C5003006F772D /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = 5A7G78D7YK; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = computer.iroh.irohUITests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_TARGET_NAME = iroh; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 42F69E392A1C5002006F772D /* Build configuration list for PBXProject "IrohConsole" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 42F69E602A1C5003006F772D /* Debug */, + 42F69E612A1C5003006F772D /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 42F69E622A1C5003006F772D /* Build configuration list for PBXNativeTarget "IrohConsole" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 42F69E632A1C5003006F772D /* Debug */, + 42F69E642A1C5003006F772D /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 42F69E652A1C5003006F772D /* Build configuration list for PBXNativeTarget "IrohConsoleTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 42F69E662A1C5003006F772D /* Debug */, + 42F69E672A1C5003006F772D /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 42F69E682A1C5003006F772D /* Build configuration list for PBXNativeTarget "IrohConsoleUITests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 42F69E692A1C5003006F772D /* Debug */, + 42F69E6A2A1C5003006F772D /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + +/* Begin XCLocalSwiftPackageReference section */ + 4266B6492B5B1F28006DE0D5 /* XCLocalSwiftPackageReference "../iroh-ffi/IrohLib" */ = { + isa = XCLocalSwiftPackageReference; + relativePath = "../iroh-ffi/IrohLib"; + }; +/* End XCLocalSwiftPackageReference section */ + +/* Begin XCRemoteSwiftPackageReference section */ + 42A4F3FA2ACCF96400F4A9F4 /* XCRemoteSwiftPackageReference "CodeScanner" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/twostraws/CodeScanner"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 2.3.3; + }; + }; +/* End XCRemoteSwiftPackageReference section */ + +/* Begin XCSwiftPackageProductDependency section */ + 4266B64A2B5B1F28006DE0D5 /* IrohLib */ = { + isa = XCSwiftPackageProductDependency; + productName = IrohLib; + }; + 42A4F3FB2ACCF96400F4A9F4 /* CodeScanner */ = { + isa = XCSwiftPackageProductDependency; + package = 42A4F3FA2ACCF96400F4A9F4 /* XCRemoteSwiftPackageReference "CodeScanner" */; + productName = CodeScanner; + }; +/* End XCSwiftPackageProductDependency section */ + }; + rootObject = 42F69E362A1C5002006F772D /* Project object */; +} diff --git a/iroh-console-ios/IrohConsole.xcodeproj/xcshareddata/xcschemes/IrohApp.xcscheme b/iroh-console-ios/IrohConsole.xcodeproj/xcshareddata/xcschemes/IrohApp.xcscheme new file mode 100644 index 0000000..d2845e6 --- /dev/null +++ b/iroh-console-ios/IrohConsole.xcodeproj/xcshareddata/xcschemes/IrohApp.xcscheme @@ -0,0 +1,113 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/iroh-console-ios/README.md b/iroh-console-ios/README.md new file mode 100644 index 0000000..b6fd861 --- /dev/null +++ b/iroh-console-ios/README.md @@ -0,0 +1,6 @@ + +# Iroh Console + +This is the code for "iroh console", an iOS app for working directly with the iroh protocol. It's also an example of using [swift FFI bindings](https://github.com/n0-computer/iroh-ffi) + +This currently requires building & running iroh-ffi locally. \ No newline at end of file diff --git a/iroh-console-ios/iroh/Assets.xcassets/AccentColor.colorset/Contents.json b/iroh-console-ios/iroh/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 0000000..238a93e --- /dev/null +++ b/iroh-console-ios/iroh/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,20 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0xFF", + "green" : "0x7C", + "red" : "0x7C" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/iroh-console-ios/iroh/Assets.xcassets/AppIcon.appiconset/AppIcon.png b/iroh-console-ios/iroh/Assets.xcassets/AppIcon.appiconset/AppIcon.png new file mode 100644 index 0000000..18bf611 Binary files /dev/null and b/iroh-console-ios/iroh/Assets.xcassets/AppIcon.appiconset/AppIcon.png differ diff --git a/iroh-console-ios/iroh/Assets.xcassets/AppIcon.appiconset/Contents.json b/iroh-console-ios/iroh/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..cefcc87 --- /dev/null +++ b/iroh-console-ios/iroh/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,14 @@ +{ + "images" : [ + { + "filename" : "AppIcon.png", + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/iroh-console-ios/iroh/Assets.xcassets/Contents.json b/iroh-console-ios/iroh/Assets.xcassets/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/iroh-console-ios/iroh/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/iroh-console-ios/iroh/Assets.xcassets/IrohLogoPurple.imageset/Contents.json b/iroh-console-ios/iroh/Assets.xcassets/IrohLogoPurple.imageset/Contents.json new file mode 100644 index 0000000..86639c2 --- /dev/null +++ b/iroh-console-ios/iroh/Assets.xcassets/IrohLogoPurple.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "iroh logo purple.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "iroh logo purple@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "iroh logo purple@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/iroh-console-ios/iroh/Assets.xcassets/IrohLogoPurple.imageset/iroh logo purple.png b/iroh-console-ios/iroh/Assets.xcassets/IrohLogoPurple.imageset/iroh logo purple.png new file mode 100644 index 0000000..8f0ffe6 Binary files /dev/null and b/iroh-console-ios/iroh/Assets.xcassets/IrohLogoPurple.imageset/iroh logo purple.png differ diff --git a/iroh-console-ios/iroh/Assets.xcassets/IrohLogoPurple.imageset/iroh logo purple@2x.png b/iroh-console-ios/iroh/Assets.xcassets/IrohLogoPurple.imageset/iroh logo purple@2x.png new file mode 100644 index 0000000..62aa01d Binary files /dev/null and b/iroh-console-ios/iroh/Assets.xcassets/IrohLogoPurple.imageset/iroh logo purple@2x.png differ diff --git a/iroh-console-ios/iroh/Assets.xcassets/IrohLogoPurple.imageset/iroh logo purple@3x.png b/iroh-console-ios/iroh/Assets.xcassets/IrohLogoPurple.imageset/iroh logo purple@3x.png new file mode 100644 index 0000000..39eb69d Binary files /dev/null and b/iroh-console-ios/iroh/Assets.xcassets/IrohLogoPurple.imageset/iroh logo purple@3x.png differ diff --git a/iroh-console-ios/iroh/Assets.xcassets/launch_screen.imageset/Contents.json b/iroh-console-ios/iroh/Assets.xcassets/launch_screen.imageset/Contents.json new file mode 100644 index 0000000..2ebdf55 --- /dev/null +++ b/iroh-console-ios/iroh/Assets.xcassets/launch_screen.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "iroh_kv_1.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/iroh-console-ios/iroh/Assets.xcassets/launch_screen.imageset/iroh_kv_1.png b/iroh-console-ios/iroh/Assets.xcassets/launch_screen.imageset/iroh_kv_1.png new file mode 100644 index 0000000..1abc645 Binary files /dev/null and b/iroh-console-ios/iroh/Assets.xcassets/launch_screen.imageset/iroh_kv_1.png differ diff --git a/iroh-console-ios/iroh/ContentView.swift b/iroh-console-ios/iroh/ContentView.swift new file mode 100644 index 0000000..64a41a8 --- /dev/null +++ b/iroh-console-ios/iroh/ContentView.swift @@ -0,0 +1,58 @@ +// +// ContentView.swift +// iroh +// +// Created by Brendan O'Brien on 5/22/23. +// + +import SwiftUI +import IrohLib + +struct ContentView: View { + @StateObject private var sessionInfo = UserSession() + @ObservedObject var anchor: NetworkModelController = NetworkModelController(anchor: networks[0]) + + var body: some View { + TabView { + NodeHome() + .tabItem { + Label("Node", systemImage: "macpro.gen2.fill") + } + Console() + .tabItem { + Label("Console", systemImage: "terminal") + } +// AnchorHome() +// .tabItem { +// Label("Anchor", systemImage: "scalemass.fill") +// } + }.environmentObject(sessionInfo) + } +} + +extension UIApplication { + func handleKeyboard() { + guard let window = windows.first else { return } + let tapRecognizer = UITapGestureRecognizer(target: window, action: #selector(UIView.endEditing)) + tapRecognizer.cancelsTouchesInView = false + tapRecognizer.delegate = self + window.addGestureRecognizer(tapRecognizer) + } + } + +extension UIApplication: UIGestureRecognizerDelegate { + public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { + return false + } +} + + +struct ContentView_Previews: PreviewProvider { + static var previews: some View { + ContentView() + .environmentObject(IrohNodeManager.shared) + .onAppear() { + IrohNodeManager.shared.start() + } + } +} diff --git a/iroh-console-ios/iroh/Data/DocEvents.json b/iroh-console-ios/iroh/Data/DocEvents.json new file mode 100644 index 0000000..32960f8 --- /dev/null +++ b/iroh-console-ios/iroh/Data/DocEvents.json @@ -0,0 +1,2 @@ +[ +] \ No newline at end of file diff --git a/iroh-console-ios/iroh/Data/Networks.json b/iroh-console-ios/iroh/Data/Networks.json new file mode 100644 index 0000000..e4fcbaf --- /dev/null +++ b/iroh-console-ios/iroh/Data/Networks.json @@ -0,0 +1,29 @@ +[ + { + "network_id": "6e182172-95b8-4a0f-9e4e-f8f881eea7fd", + "created_by": "667ebcea-2d5a-402f-a15c-046134467a52", + "created_at": "2023-06-13T19:32:37.628630Z", + "updated_at": "2023-06-13T19:32:37.628630Z", + "pretty_name": "Default Iroh Network", + "name": "default", + "anchor_url": "https://default.iroh.network" + }, + { + "network_id": "778fbd90-4497-4f81-98be-2dab68ae6acb", + "created_by": "667ebcea-2d5a-402f-a15c-046134467a52", + "created_at": "2023-06-13T19:32:37.628630Z", + "updated_at": "2023-06-13T19:32:37.628630Z", + "pretty_name": "bananas", + "name": "bananas", + "anchor_url": "https://bananas.iroh.network" + }, + { + "network_id": "fc4e626c-787e-4cdf-8e29-d18375f0492b", + "created_by": "667ebcea-2d5a-402f-a15c-046134467a52", + "created_at": "2023-06-15T12:36:13.081136Z", + "updated_at": "2023-06-15T12:36:13.081136Z", + "pretty_name": "rust memes only", + "name": "rust-memes-only", + "anchor_url": "https://rust-memes-only.iroh.network" + } +] diff --git a/iroh-console-ios/iroh/Extensions.swift b/iroh-console-ios/iroh/Extensions.swift new file mode 100644 index 0000000..34d5251 --- /dev/null +++ b/iroh-console-ios/iroh/Extensions.swift @@ -0,0 +1,21 @@ +// +// Extensions.swift +// IrohApp +// +// Created by Brendan O'Brien on 9/13/23. +// + +import Foundation + +extension String { + /* + Truncates the string to the specified length number of characters and appends an optional trailing string if longer. + - Parameter length: Desired maximum lengths of a string + - Parameter trailing: A 'String' that will be appended after the truncation. + + - Returns: 'String' object. + */ + func trunc(length: Int, trailing: String = "…") -> String { + return (self.count > length) ? self.prefix(length) + trailing : self + } +} diff --git a/iroh-console-ios/iroh/Fonts/SpaceGrotesk-Bold.ttf b/iroh-console-ios/iroh/Fonts/SpaceGrotesk-Bold.ttf new file mode 100644 index 0000000..869a60f Binary files /dev/null and b/iroh-console-ios/iroh/Fonts/SpaceGrotesk-Bold.ttf differ diff --git a/iroh-console-ios/iroh/Fonts/SpaceGrotesk-Light.ttf b/iroh-console-ios/iroh/Fonts/SpaceGrotesk-Light.ttf new file mode 100644 index 0000000..76a195f Binary files /dev/null and b/iroh-console-ios/iroh/Fonts/SpaceGrotesk-Light.ttf differ diff --git a/iroh-console-ios/iroh/Fonts/SpaceGrotesk-Medium.ttf b/iroh-console-ios/iroh/Fonts/SpaceGrotesk-Medium.ttf new file mode 100644 index 0000000..667905f Binary files /dev/null and b/iroh-console-ios/iroh/Fonts/SpaceGrotesk-Medium.ttf differ diff --git a/iroh-console-ios/iroh/Fonts/SpaceGrotesk-Regular.ttf b/iroh-console-ios/iroh/Fonts/SpaceGrotesk-Regular.ttf new file mode 100644 index 0000000..792fe1b Binary files /dev/null and b/iroh-console-ios/iroh/Fonts/SpaceGrotesk-Regular.ttf differ diff --git a/iroh-console-ios/iroh/Fonts/SpaceGrotesk-SemiBold.ttf b/iroh-console-ios/iroh/Fonts/SpaceGrotesk-SemiBold.ttf new file mode 100644 index 0000000..0219302 Binary files /dev/null and b/iroh-console-ios/iroh/Fonts/SpaceGrotesk-SemiBold.ttf differ diff --git a/iroh-console-ios/iroh/Fonts/SpaceMono-Bold.ttf b/iroh-console-ios/iroh/Fonts/SpaceMono-Bold.ttf new file mode 100644 index 0000000..14aab33 Binary files /dev/null and b/iroh-console-ios/iroh/Fonts/SpaceMono-Bold.ttf differ diff --git a/iroh-console-ios/iroh/Fonts/SpaceMono-BoldItalic.ttf b/iroh-console-ios/iroh/Fonts/SpaceMono-BoldItalic.ttf new file mode 100644 index 0000000..3124efb Binary files /dev/null and b/iroh-console-ios/iroh/Fonts/SpaceMono-BoldItalic.ttf differ diff --git a/iroh-console-ios/iroh/Fonts/SpaceMono-Italic.ttf b/iroh-console-ios/iroh/Fonts/SpaceMono-Italic.ttf new file mode 100644 index 0000000..eb15c27 Binary files /dev/null and b/iroh-console-ios/iroh/Fonts/SpaceMono-Italic.ttf differ diff --git a/iroh-console-ios/iroh/Fonts/SpaceMono-Regular.ttf b/iroh-console-ios/iroh/Fonts/SpaceMono-Regular.ttf new file mode 100644 index 0000000..d713495 Binary files /dev/null and b/iroh-console-ios/iroh/Fonts/SpaceMono-Regular.ttf differ diff --git a/iroh-console-ios/iroh/Images/AppIcon.png b/iroh-console-ios/iroh/Images/AppIcon.png new file mode 100644 index 0000000..18bf611 Binary files /dev/null and b/iroh-console-ios/iroh/Images/AppIcon.png differ diff --git a/iroh-console-ios/iroh/Images/iroh logo purple.png b/iroh-console-ios/iroh/Images/iroh logo purple.png new file mode 100644 index 0000000..8f0ffe6 Binary files /dev/null and b/iroh-console-ios/iroh/Images/iroh logo purple.png differ diff --git a/iroh-console-ios/iroh/Images/iroh logo purple@2x.png b/iroh-console-ios/iroh/Images/iroh logo purple@2x.png new file mode 100644 index 0000000..62aa01d Binary files /dev/null and b/iroh-console-ios/iroh/Images/iroh logo purple@2x.png differ diff --git a/iroh-console-ios/iroh/Images/iroh logo purple@3x.png b/iroh-console-ios/iroh/Images/iroh logo purple@3x.png new file mode 100644 index 0000000..39eb69d Binary files /dev/null and b/iroh-console-ios/iroh/Images/iroh logo purple@3x.png differ diff --git a/iroh-console-ios/iroh/Info.plist b/iroh-console-ios/iroh/Info.plist new file mode 100644 index 0000000..cecaa1a --- /dev/null +++ b/iroh-console-ios/iroh/Info.plist @@ -0,0 +1,19 @@ + + + + + UIAppFonts + + SpaceMono-Regular.ttf + SpaceMono-Bold.ttf + SpaceGrotesk-Regular.ttf + SpaceGrotesk-SemiBold.ttf + SpaceGrotesk-Bold.ttf + + UILaunchScreen + + UIImageName + launch_screen + + + diff --git a/iroh-console-ios/iroh/IrohNodeManager.swift b/iroh-console-ios/iroh/IrohNodeManager.swift new file mode 100644 index 0000000..6c6db64 --- /dev/null +++ b/iroh-console-ios/iroh/IrohNodeManager.swift @@ -0,0 +1,275 @@ +// +// IrohNodeManager.swift +// iroh +// +// Created by Brendan O'Brien on 8/8/23. +// + +import SwiftUI +import IrohLib +import Foundation + +class IrohNodeManager: ObservableObject { + static let shared = IrohNodeManager() + + @Published var node: IrohNode? + @Published var nodeID: String = "" + @Published var author: AuthorId? + @Published var nodeStats: [String : CounterStats]? + @Published var historicalStats: [IrohAnchorAPI.Types.Response.EgressDaily]? + @Published var connections: [ConnectionInfoIdentifiable]? + @Published var connectionHistories: [String: [ConnHistory]] = [:] + + // stats we've collected for this process session + private var sessionHistoricalStat: IrohAnchorAPI.Types.Response.EgressDaily? = nil + // record of stats we've already written for this session, to avoid double-writing + private var lastWriteStats: IrohAnchorAPI.Types.Response.EgressDaily? = nil + + private var timer: Timer? + + func start() { +// IrohLib.setLogLevel(level: .debug) + do { + try IrohLib.startMetricsCollection() + let path = self.irohPath() + print(path.absoluteString) + self.node = try IrohNode(path: path.path) + nodeID = node?.nodeId() ?? "" + startConnectionMonitoring() + startStatsMonitoring() + initAuthor() + self.historicalStats = initHistoricalStats(nodeID) + print("created iroh node with node Id \(nodeID)") + } catch { + print("error creating iroh node \(error)") + } + } + + // TODO: using a single author for now. At some point we should add support for author selection + private func initAuthor() { + do { + self.author = try self.node?.authorCreate() + } catch { + print("couldn't create author \(error)") + return + } + } + + + private func startConnectionMonitoring() { + updateConnections() + DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { + self.startConnectionMonitoring() + } + } + + private func updateConnections() { + guard let node = node else { + print("Error: no node") + return + } + + do { + let connections = try node.connections() + + DispatchQueue.global(qos: .userInteractive).async { + let mapped = connections.map { (conn) -> ConnectionInfoIdentifiable in + let nodeId = conn.nodeId.toString() + let item = ConnHistory( + id: .now, + latency: conn.latency != nil ? conn.latency! * 1000 : 0, + connType: conn.connType) + + DispatchQueue.main.async { + if self.connectionHistories[nodeId] != nil { + self.connectionHistories[nodeId]?.append(item) + } else { + self.connectionHistories[nodeId] = [item] + } + + if self.connectionHistories[nodeId]?.count ?? 0 >= 300 { + let _ = self.connectionHistories[nodeId]!.popLast() + } + } + + return ConnectionInfoIdentifiable( + id: nodeId, + latency: conn.latency != nil ? conn.latency! * 1000 : nil, + connType: conn.connType) + } + + DispatchQueue.main.async { + self.connections = mapped + } + } + } catch { + print("error fetching connections") + } + } + + private func startStatsMonitoring() { + timer = Timer.scheduledTimer(withTimeInterval: 3, repeats: true) { _ in + do { + if let latest = try self.node?.stats() { + + DispatchQueue.main.async { + self.nodeStats = latest + } + + // todo: should be "bytes_sent", but that doesn't seem to increase? + if let sentStat = latest["send_data"] { + // update latest historical stats + self.sessionHistoricalStat = IrohAnchorAPI.Types.Response.EgressDaily( + date: self.currentHistoricalStatsDate(), + egress_bytes: Int(sentStat.value), + egress_blobs: 0, // TODO + requests: 0, // TODO + nodeId: self.nodeID) + self.writeHistoricalStatsToDisk() + } + + } + } catch (let error) { + print("error \(error)") + self.timer?.invalidate() + self.timer = nil + } + } + } + + private func initHistoricalStats(_ nodeID: String) -> [IrohAnchorAPI.Types.Response.EgressDaily] { + if var hs = readHistoricalStatsFromDisk() { + + if hs.count == 0 { + return[self.initStat(nodeID)] + } + + // add today to front of list if it doesn't exist + if hs.first?.date != currentHistoricalStatsDate() { + hs.insert(self.initStat(nodeID), at: 0) + } + + return hs + } + + return [self.initStat(nodeID)] + } + + private func irohPath() -> URL { + let paths = FileManager.default.urls(for: .libraryDirectory, in: .userDomainMask) + let irohPath = paths[0].appendingPathComponent("iroh") + mkdirP(path: irohPath.path) + return irohPath + } + + private func historicalStatsPath() -> URL { + let sessionTokenFilename = "iroh_node_stats.json" + let paths = FileManager.default.urls(for: .libraryDirectory, in: .userDomainMask) + return paths[0].appendingPathComponent(sessionTokenFilename) + } + + private func currentHistoricalStatsDate() -> Date { + var cal = Calendar.current + cal.timeZone = TimeZone(secondsFromGMT: 0)! + return cal.startOfDay(for: .now) + } + + private func initStat(_ nodeID: String) -> IrohAnchorAPI.Types.Response.EgressDaily { + return .init( + date: currentHistoricalStatsDate(), + egress_bytes: 0, + egress_blobs: 0, + requests: 0, + nodeId: nodeID) + } + + private func writeHistoricalStatsToDisk() { + if sessionHistoricalStat == lastWriteStats { + // nothing to write + return + } + + print("writing historical stats") + + do { + guard let latest = sessionHistoricalStat else { + return + } + + // re-read to ensure we're on the current date + var hs = self.initHistoricalStats(nodeID) + + var top = hs.first! + top.egress_bytes += latest.egress_bytes - (lastWriteStats?.egress_bytes ?? 0) + hs[0] = top + + let data = try JSONEncoder().encode(hs) + try data.write(to: self.historicalStatsPath()) + lastWriteStats = .init(date: currentHistoricalStatsDate(), + egress_bytes: latest.egress_bytes, + egress_blobs: latest.egress_blobs, + requests: latest.requests, + nodeId: nodeID) + self.historicalStats = hs + print("wrote historical stats to disk") + } catch { + print("error writing historical stats to file") + } + } + + private func readHistoricalStatsFromDisk() -> [IrohAnchorAPI.Types.Response.EgressDaily]? { + do { + if let jsonData = try String(contentsOf: self.historicalStatsPath(), encoding: String.Encoding.utf8).data(using: .utf8) { + let contents = try JSONDecoder().decode([IrohAnchorAPI.Types.Response.EgressDaily].self, from: jsonData) + return contents + } + } catch {} + return nil + } +} + +struct IrohNodeManagerEnvironmentKey: EnvironmentKey { + static var defaultValue: IrohNodeManager = IrohNodeManager.shared +} + +extension EnvironmentValues { + var irohNodeManager: IrohNodeManager { + get { self[IrohNodeManagerEnvironmentKey.self] } + set { self[IrohNodeManagerEnvironmentKey.self] = newValue } + } +} + +struct ConnectionInfoIdentifiable: Identifiable, Equatable { + var id: String + var latency: Double? + var connType: ConnectionType + + static func == (lhs: ConnectionInfoIdentifiable, rhs: ConnectionInfoIdentifiable) -> Bool { + lhs.id == rhs.id + } + + func latencyString() -> String { + if let latency = latency { + return String(format:"%.1f", latency) + } + return "?" + } +} + +struct ConnHistory: Identifiable { + var id: Date + var latency: Double + var connType: ConnectionType +} + +func mkdirP(path: String) { + let fileManager = FileManager.default + + do { + try fileManager.createDirectory(atPath: path, + withIntermediateDirectories: true, + attributes: nil) + } catch { + print("Error creating directory: \(error)") + } +} diff --git a/iroh-console-ios/iroh/Model/Document.swift b/iroh-console-ios/iroh/Model/Document.swift new file mode 100644 index 0000000..4155f67 --- /dev/null +++ b/iroh-console-ios/iroh/Model/Document.swift @@ -0,0 +1,39 @@ +// +// Document.swift +// iroh +// +// Created by Brendan O'Brien on 7/26/23. +// + +import Foundation + + +struct Document: Hashable, Codable { + var name : String + var created_by : String + var created_at : String + var updated_at : String + var public_key : String + var anchor_url : String +} + +enum DocEventType: String, Codable { + case DocumentCreated = "Document Created" + case YouSetKey = "Wrote Key" + case YouDeletedKey = "Key Deleted" + case PeerSetKey = "Key Set by a Peer" + case PeerDeletedKey = "Key Deleted by a Peer" + case PeerJoined = "Peer Joined" + case PeerLeft = "Peer Left" + case Error = "Error" +} + +struct DocEvent: Hashable, Codable { + var tipe : DocEventType + var ts : Date + var author_id : String + var author_name : String + var key : String + var value : String + var message : String +} diff --git a/iroh-console-ios/iroh/Model/MockData.swift b/iroh-console-ios/iroh/Model/MockData.swift new file mode 100644 index 0000000..974ec1a --- /dev/null +++ b/iroh-console-ios/iroh/Model/MockData.swift @@ -0,0 +1,21 @@ +// +// MockData.swift +// IrohApp +// +// Created by Brendan O'Brien on 9/13/23. +// + +import Foundation + +func mockEgress() -> IrohAnchorAPI.Types.Response.Egress { + return IrohAnchorAPI.Types.Response.Egress( + summary: IrohAnchorAPI.Types.Response.EgressSummary( + egress_bytes: 1234567, + egress_blobs: 100, + requests: 100), + daily: [ + .init(date: Date(timeIntervalSince1970: 1694476800), egress_bytes: 1190875, egress_blobs: 2, requests: 2), + .init(date: Date(timeIntervalSince1970: 1694563200), egress_bytes: 1173811, egress_blobs: 34, requests: 34), + ] + ) +} diff --git a/iroh-console-ios/iroh/Model/ModelData.swift b/iroh-console-ios/iroh/Model/ModelData.swift new file mode 100644 index 0000000..2c20bcf --- /dev/null +++ b/iroh-console-ios/iroh/Model/ModelData.swift @@ -0,0 +1,316 @@ +// +// ModelData.swift +// iroh +// +// Created by Brendan O'Brien on 7/4/23. +// + +import Foundation + + +var networks: [Network] = load("Networks.json") + +func load(_ filename: String) -> T { + let data: Data + + guard let file = Bundle.main.url(forResource: filename, withExtension: nil) + else { + fatalError("Couldn't find \(filename) in main bundle.") + } + + do { + data = try Data(contentsOf: file) + } catch { + fatalError("Couldn't load \(filename) from main bundle:\n\(error)") + } + + do { + let decoder = JSONDecoder() + return try decoder.decode(T.self, from: data) + } catch { + fatalError("Couldn't parse \(filename) as \(T.self):\n\(error)") + } +} + +//var mockDocEvents: [DocEvent] = load("DocEvents.json") + +var anchorEvents = [ + DocEvent(tipe: .YouSetKey, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .YouDeletedKey, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), +] + +var mockDocEvents: [DocEvent] = [ + DocEvent(tipe: .DocumentCreated, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .YouSetKey, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .YouSetKey, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .YouSetKey, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .YouSetKey, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .YouSetKey, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .PeerJoined, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .PeerJoined, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .PeerJoined, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .PeerJoined, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .PeerJoined, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .PeerLeft, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .PeerDeletedKey, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .PeerSetKey, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .PeerSetKey, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .PeerSetKey, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .PeerSetKey, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .PeerSetKey, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .PeerSetKey, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .PeerSetKey, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .PeerDeletedKey, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .Error, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .PeerJoined, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .DocumentCreated, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .PeerJoined, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .PeerJoined, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .PeerJoined, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .PeerJoined, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .YouSetKey, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .YouSetKey, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .YouSetKey, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .YouSetKey, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .YouSetKey, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .YouSetKey, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .YouSetKey, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .YouSetKey, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .YouSetKey, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .YouSetKey, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .YouSetKey, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .YouSetKey, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .YouSetKey, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .YouSetKey, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .YouSetKey, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .YouSetKey, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .YouSetKey, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .YouSetKey, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .YouSetKey, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .YouSetKey, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .YouSetKey, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .YouSetKey, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .YouSetKey, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .YouSetKey, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"),DocEvent(tipe: .YouSetKey, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .YouSetKey, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .YouSetKey, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .YouSetKey, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .YouSetKey, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"),DocEvent(tipe: .YouSetKey, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .YouSetKey, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .YouSetKey, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .YouSetKey, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .YouSetKey, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .YouSetKey, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .YouSetKey, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .YouSetKey, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .YouSetKey, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .YouSetKey, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .YouSetKey, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .YouSetKey, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .YouSetKey, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .YouSetKey, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .YouSetKey, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .YouSetKey, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .YouSetKey, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .YouSetKey, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .YouSetKey, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .YouSetKey, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .YouSetKey, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .YouSetKey, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .YouSetKey, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .YouSetKey, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .YouSetKey, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .YouSetKey, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .YouSetKey, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .YouSetKey, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .YouSetKey, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .YouSetKey, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .YouSetKey, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .YouSetKey, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .YouSetKey, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .YouSetKey, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .YouSetKey, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .YouSetKey, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .YouSetKey, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .YouSetKey, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .YouSetKey, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .YouSetKey, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .YouSetKey, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .YouSetKey, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .YouSetKey, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .YouSetKey, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .YouSetKey, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .YouSetKey, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .YouSetKey, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .YouSetKey, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .YouSetKey, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .YouSetKey, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .YouSetKey, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .YouSetKey, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .YouSetKey, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .YouSetKey, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .YouSetKey, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .YouSetKey, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .YouSetKey, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .YouSetKey, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .YouSetKey, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .YouSetKey, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .YouSetKey, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .YouSetKey, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .YouSetKey, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .YouSetKey, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .YouSetKey, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .YouSetKey, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .YouSetKey, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .YouSetKey, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .YouSetKey, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .YouSetKey, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .YouSetKey, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .YouSetKey, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .YouSetKey, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .YouSetKey, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .YouSetKey, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .PeerJoined, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .PeerLeft, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .PeerSetKey, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .PeerSetKey, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .PeerSetKey, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .PeerSetKey, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .PeerSetKey, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .PeerSetKey, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .PeerSetKey, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .PeerDeletedKey, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .Error, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .PeerJoined, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .DocumentCreated, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .PeerJoined, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .PeerJoined, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .PeerJoined, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .PeerJoined, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .PeerJoined, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .PeerLeft, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .PeerSetKey, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .PeerSetKey, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .PeerSetKey, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .PeerSetKey, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .PeerSetKey, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .PeerSetKey, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .PeerSetKey, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .PeerDeletedKey, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .Error, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .PeerJoined, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .DocumentCreated, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .PeerJoined, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .PeerJoined, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .PeerJoined, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .PeerJoined, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .PeerJoined, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .PeerLeft, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .PeerSetKey, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .PeerSetKey, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .PeerSetKey, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .PeerSetKey, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .PeerSetKey, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .PeerSetKey, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .PeerSetKey, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .YouSetKey, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .PeerDeletedKey, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .Error, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .PeerJoined, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .PeerSetKey, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .PeerSetKey, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .PeerSetKey, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .PeerSetKey, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .PeerSetKey, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .PeerSetKey, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .PeerSetKey, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .PeerSetKey, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .PeerSetKey, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .PeerSetKey, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .PeerSetKey, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .PeerSetKey, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .PeerSetKey, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .PeerSetKey, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), DocEvent(tipe: .PeerSetKey, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .PeerSetKey, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .PeerSetKey, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .PeerSetKey, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .PeerSetKey, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .PeerSetKey, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .PeerSetKey, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .PeerSetKey, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .PeerSetKey, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .PeerSetKey, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .PeerSetKey, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .PeerSetKey, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .PeerSetKey, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .PeerSetKey, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .PeerSetKey, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .PeerSetKey, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .PeerSetKey, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .PeerSetKey, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .PeerSetKey, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .PeerSetKey, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .PeerSetKey, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .PeerSetKey, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .PeerSetKey, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .PeerSetKey, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .PeerSetKey, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .PeerSetKey, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .PeerSetKey, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .PeerSetKey, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .PeerSetKey, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .PeerSetKey, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .PeerSetKey, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .PeerSetKey, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .PeerSetKey, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .PeerSetKey, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .PeerSetKey, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .PeerSetKey, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .PeerSetKey, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .PeerSetKey, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .PeerSetKey, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .PeerSetKey, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .PeerSetKey, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .PeerSetKey, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .PeerSetKey, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .PeerSetKey, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .PeerSetKey, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .PeerSetKey, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .PeerSetKey, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .PeerSetKey, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .PeerSetKey, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .PeerSetKey, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .PeerSetKey, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .PeerSetKey, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .PeerSetKey, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .PeerSetKey, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .PeerSetKey, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .PeerSetKey, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .PeerSetKey, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .PeerSetKey, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .PeerSetKey, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .PeerSetKey, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .PeerSetKey, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .PeerSetKey, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .PeerSetKey, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), DocEvent(tipe: .PeerSetKey, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .PeerSetKey, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .PeerSetKey, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .PeerSetKey, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .PeerSetKey, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .PeerSetKey, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .PeerSetKey, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .PeerSetKey, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .PeerSetKey, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .PeerSetKey, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .PeerSetKey, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .PeerSetKey, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .PeerSetKey, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .PeerSetKey, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .PeerSetKey, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .PeerSetKey, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .PeerSetKey, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .PeerSetKey, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .PeerSetKey, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .PeerSetKey, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), + DocEvent(tipe: .PeerSetKey, ts: Date(), author_id: "Qmfoo...", author_name: "dig", key: "/foo/bar/baz", value: "stuff", message: "oh hai"), +] diff --git a/iroh-console-ios/iroh/Model/Modes.swift b/iroh-console-ios/iroh/Model/Modes.swift new file mode 100644 index 0000000..095cbb9 --- /dev/null +++ b/iroh-console-ios/iroh/Model/Modes.swift @@ -0,0 +1,14 @@ +// +// Modes.swift +// iroh +// +// Created by Brendan O'Brien on 7/4/23. +// + +import Foundation + +enum AppMode { + case configure + case connect + case content +} diff --git a/iroh-console-ios/iroh/Model/Network.swift b/iroh-console-ios/iroh/Model/Network.swift new file mode 100644 index 0000000..bd39b53 --- /dev/null +++ b/iroh-console-ios/iroh/Model/Network.swift @@ -0,0 +1,25 @@ +// +// Configuration.swift +// iroh +// +// Created by Brendan O'Brien on 7/4/23. +// + +import Foundation + +struct Network: Hashable, Codable { + var network_id : String + var created_by : String + var created_at : String + var updated_at : String + var name : String + var anchor_url : String +} + +class NetworkModelController: ObservableObject { + @Published var anchor: Network + + init(anchor: Network) { + self.anchor = anchor + } +} diff --git a/iroh-console-ios/iroh/Model/UserSession.swift b/iroh-console-ios/iroh/Model/UserSession.swift new file mode 100644 index 0000000..b3c4652 --- /dev/null +++ b/iroh-console-ios/iroh/Model/UserSession.swift @@ -0,0 +1,162 @@ +// +// SessionInfo.swift +// IrohApp +// +// Created by Brendan O'Brien on 9/12/23. +// + +import Foundation + + +class UserSession: ObservableObject { + @Published var sessionToken: String = "" + @Published var anchorName: String = "" + + @Published var user: IrohAnchorAPI.Types.Response.User? = nil + @Published var anchorDialingInfo: IrohAnchorAPI.Types.Response.AnchorDialingInfo? = nil + @Published var projects: [IrohAnchorAPI.Types.Response.Project]? = nil + @Published var documents: [IrohAnchorAPI.Types.Response.DocumentInfo]? = nil + + init() { + if let session = self.readSessionFromDisk() { + if session.token != "" && session.anchorName != "" { + print("loaded session from disk") + self.sessionToken = session.token + self.anchorName = session.anchorName + self.fetchAll(sessionToken: self.sessionToken) + } + } + } + + private func session() -> SessionCodable { + return SessionCodable( + token: self.sessionToken, + anchorName: self.anchorName) + } + + private func sessionTokenPath() -> URL { + let sessionTokenFilename = "user_session.json" + let paths = FileManager.default.urls(for: .libraryDirectory, in: .userDomainMask) + return paths[0].appendingPathComponent(sessionTokenFilename) + } + + private func writeSessionToDisk() { + do { + let data = try JSONEncoder().encode(self.session()) + try data.write(to: self.sessionTokenPath()) + print("wrote session to disk") + } catch { + print("error writing token to file") + } + } + + private func readSessionFromDisk() -> SessionCodable? { + do { + if let jsonData = try String(contentsOf: self.sessionTokenPath(), encoding: String.Encoding.utf8).data(using: .utf8) { + let contents = try JSONDecoder().decode(SessionCodable.self, from: jsonData) + return contents + } + } catch (let failure) { + print("error reading session from disk: \(failure)") + } + return nil + } + + func login(username: String, password: String) { + // Send credentials + IrohAnchorAPI.Client.shared.login(username, password) { result in + switch result { + case .success(let success): + DispatchQueue.main.async { + self.sessionToken = success.token + self.writeSessionToDisk() + } + self.fetchAll(sessionToken: success.token) + case .failure(let failure): + print("problem logging in: \(failure.localizedDescription)") + } + } + } + + func logout() { + DispatchQueue.main.async { + self.sessionToken = "" + self.anchorName = "" + self.user = nil + self.anchorDialingInfo = nil + self.projects = nil + self.documents = nil + } + + writeSessionToDisk() + } + + func fetchAll(sessionToken: String) { + IrohAnchorAPI.Client.shared.me(sessionToken) { result in + switch result { + case .success(let me): + DispatchQueue.main.async { + self.user = me + } + case .failure(let failure): + switch failure { + case .generic: + print("problem getting my profile: \(failure.localizedDescription)") + case .unauthorized: + self.logout() + case .internal: + print("problem getting my profile: \(failure.localizedDescription)") + } + } + } + +// IrohAnchorAPI.Client.shared.anchorDetails(anchor, sessionToken) { result in +// switch result { +// case .success(let dialingInfo): +// DispatchQueue.main.async { +// self.anchorDialingInfo = dialingInfo +// } +// case .failure(let failure): +// print("problem fetching anchor details: \(failure.localizedDescription)") +// } +// } +// +// IrohAnchorAPI.Client.shared.anchorEgress(anchor, sessionToken) { result in +// switch result { +// case .success(let egress): +// DispatchQueue.main.async { +// self.anchorEgress = egress +// } +// case .failure(let failure): +// print("problem fetching anchor egress: \(failure.localizedDescription)") +// } +// } +// +// IrohAnchorAPI.Client.shared.myProjects(anchor, sessionToken) { result in +// switch result { +// case .success(let anchors): +// DispatchQueue.main.async { +// self.anchors = anchors +// } +// case .failure(let failure): +// print("problem fetching anchors: \(failure.localizedDescription)") +// } +// } +// +// IrohAnchorAPI.Client.shared.docs(anchor, sessionToken) { result in +// switch result { +// case .success(let docs): +// DispatchQueue.main.async { +// self.documents = docs +// } +// case .failure(let failure): +// print("problem fetching docs: \(failure.localizedDescription)") +// } +// } + } +} + +private struct SessionCodable: Codable { + var token: String + var anchorName: String +} diff --git a/iroh-console-ios/iroh/Preview Content/Preview Assets.xcassets/Contents.json b/iroh-console-ios/iroh/Preview Content/Preview Assets.xcassets/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/iroh-console-ios/iroh/Preview Content/Preview Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/iroh-console-ios/iroh/Views/Anchors/AddAnchor.swift b/iroh-console-ios/iroh/Views/Anchors/AddAnchor.swift new file mode 100644 index 0000000..bf9cdc9 --- /dev/null +++ b/iroh-console-ios/iroh/Views/Anchors/AddAnchor.swift @@ -0,0 +1,137 @@ +// +// AddNetwork.swift +// iroh +// +// Created by Brendan O'Brien on 7/11/23. +// + +import SwiftUI + +struct AddAnchor: View { + @State private var creating = false + + var body: some View { + VStack { + VStack { + Group { + if creating { + CreatingAnchor() + } else { + VStack() { + Text("New Anchor") + .font(Font.custom("Space Mono", size: 32).weight(.bold)) + .lineSpacing(48) + .foregroundColor(.white) + .frame(maxWidth: .infinity, alignment: .leading) + Text("Choose a name for your anchor. You can change this later.") + .font(Font.custom("Space Mono", size: 14)) + .foregroundColor(.white) + .frame(maxWidth: .infinity, alignment: .leading) + .fixedSize(horizontal: false, vertical: true) + } + + VStack(alignment: .leading, spacing: 0) { + Text("ANCHOR PRETTY NAME") + .font(Font.custom("Space Mono", size: 14)) + .lineSpacing(20) + .foregroundColor(Color(red: 0.40, green: 0.46, blue: 0.63)) + Text("Example") + .font(Font.custom("Space Mono", size: 28)) + .lineSpacing(36) + .foregroundColor(.white) + .frame(minWidth: 0, maxWidth: .infinity, alignment: .leading) + } + .padding(6) + .cornerRadius(2) + .overlay( + RoundedRectangle(cornerRadius: 2) + .inset(by: 0.50) + .stroke(Color(red: 0.21, green: 0.24, blue: 0.33), lineWidth: 1) + ); + + VStack(alignment: .leading, spacing: 2) { + Text("ANCHOR URL") + .font(Font.custom("Space Mono", size: 14)) + .foregroundColor(Color(red: 0.40, green: 0.46, blue: 0.63)) + HStack(spacing: 0) { + Text("example") + .font(Font.custom("Space Mono", size: 20)) + .foregroundColor(.white) + .frame(alignment: .leading) + .padding(0) + Text(".iroh.network") + .font(Font.custom("Space Mono", size: 20)) + .foregroundColor(Color(red: 0.40, green: 0.46, blue: 0.63)) + .frame(minWidth: 0, maxWidth: .infinity, alignment: .leading) + + } + } + .padding(6) + .cornerRadius(2) + .overlay( + RoundedRectangle(cornerRadius: 2) + .inset(by: 0.50) + .stroke(Color(red: 0.21, green: 0.24, blue: 0.33), lineWidth: 1) + ); + + Spacer() + Button(action: { + creating = true + }, label: { + Text("CREATE ANCHOR") + .font(Font.custom("Space Mono", size: 20)) + .foregroundColor(.white) + .padding(EdgeInsets(top: 10, leading: 10, bottom: 10, trailing: 10)) + .frame(minWidth: 0, maxWidth: .infinity) + .background(Color(red: 0.42, green: 0.42, blue: 0.85)) + .cornerRadius(2); + }) + } + } + } + .padding(20) + .frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity) + }.background(Color(red: 0.13, green: 0.15, blue: 0.20)) + + } +} + +struct CreatingAnchor: View { + @EnvironmentObject var anchor: NetworkModelController + @State private var progress: CGFloat = 0.0 + @State private var isCompleted = false + @Environment(\.dismiss) private var dismiss + + var body: some View { + VStack { + ProgressView("Creating Anchor:", value: progress, total: 1) + .padding() + .font(Font.custom("Space Mono", size: 20)) + .lineSpacing(48) + .foregroundColor(.white) + } + .onAppear { + startTimer() + } + } + + private func startTimer() { + Timer.scheduledTimer(withTimeInterval: 2, repeats: false) { _ in + withAnimation { + progress = 1.0 + isCompleted = true + dismiss() + let newAnchor = Network(network_id: "new netork", created_by: "b5", created_at: "now", updated_at: "now", name: "Example", anchor_url: "example.iroh.network") + networks.append(newAnchor) + anchor.anchor = newAnchor + + } + } + } +} + +struct AddAnchor_Previews: PreviewProvider { + static var previews: some View { + AddAnchor() + } +} diff --git a/iroh-console-ios/iroh/Views/Anchors/AnchorHome.swift b/iroh-console-ios/iroh/Views/Anchors/AnchorHome.swift new file mode 100644 index 0000000..c935aa5 --- /dev/null +++ b/iroh-console-ios/iroh/Views/Anchors/AnchorHome.swift @@ -0,0 +1,111 @@ +// +// ConfigurationHome.swift +// iroh +// +// Created by Brendan O'Brien on 7/4/23. +// + +import SwiftUI + +struct AnchorHome: View { + @EnvironmentObject var sessionInfo: UserSession + @EnvironmentObject var nodeManager: IrohNodeManager + + var body: some View { + Group { + if sessionInfo.sessionToken == "" { + Login() + } else { + VStack(alignment: .leading, spacing: 20) { + Topbar() + VStack(alignment: .leading, spacing: 0) { + Text(sessionInfo.anchorName) + .font(Font.custom("Space Mono", size: 32)) + .foregroundColor(.primary) + .frame(maxWidth: .infinity, alignment: .leading) + Text("\(sessionInfo.anchorName).iroh.network") + .font(Font.custom("Space Mono", size: 16)) + .lineSpacing(23) + .foregroundColor(.secondary) + .frame(maxWidth: .infinity, alignment: .leading) + } + .padding(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0)) + .frame(maxWidth: .infinity) +// if let egress = sessionInfo.anchorEgress { +// AnchorEgressSummaryStats(egress: egress) +// } +// if let egress = self.anchorAndLocalEgressHistory() { +// DailyEgressBarChart(title: "ANCHOR EGRESS", egress: egress) +// } + Spacer() +// if let docs = sessionInfo.documents { +// List { +// Section("docs") { +// ForEach(docs, id: \.self) { doc in +// Text(doc.doc_id.trunc(length: 15)) +// } +// } +// } +// } + } + .padding(20) + } + } + } + +// private func anchorAndLocalEgressHistory() -> [IrohAnchorAPI.Types.Response.EgressDaily]? { +// guard var anchorEgress = sessionInfo.anchorEgress?.daily else { +// return nil +// } +// +// // TODO b5 - hack to deal with server returning bonkers dates +// let calendar = Calendar.current +// +// for (index, stat) in anchorEgress.enumerated() { +// let year = calendar.component(.year, from: stat.date) +// if year > 2023 { +// var dateComponents = DateComponents() +// dateComponents.year = 2023 +// +// let originalComponents = calendar.dateComponents([.month, .day, .hour, .minute, .second], from: stat.date) +// dateComponents.month = originalComponents.month +// dateComponents.day = originalComponents.day +// dateComponents.hour = originalComponents.hour +// dateComponents.minute = originalComponents.minute +// dateComponents.second = originalComponents.second +// +// if let newDate = calendar.date(from: dateComponents) { +// var updatedStruct = stat +// updatedStruct.date = newDate +// anchorEgress[index] = updatedStruct +// } +// } +// } +// +// +// guard let localEgress = nodeManager.historicalStats else { +// return anchorEgress +// } +// +// return anchorEgress + localEgress +// } + + private func formatDate(date: Date) -> String { + let dateFormatter = DateFormatter() + dateFormatter.dateFormat = "HH:mm:ss" + dateFormatter.locale = Locale(identifier: "en_US_POSIX") + return dateFormatter.string(from: date) + } +} + +struct AnchorHome_Previews: PreviewProvider { + static var previews: some View { + let session = UserSession() + AnchorHome() + .environmentObject(session) + .environmentObject(IrohNodeManager.shared) + .onAppear() { + IrohNodeManager.shared.start() + } + } +} diff --git a/iroh-console-ios/iroh/Views/Anchors/AnchorsList.swift b/iroh-console-ios/iroh/Views/Anchors/AnchorsList.swift new file mode 100644 index 0000000..1e80457 --- /dev/null +++ b/iroh-console-ios/iroh/Views/Anchors/AnchorsList.swift @@ -0,0 +1,46 @@ +// +// ConfigPicker.swift +// iroh +// +// Created by Brendan O'Brien on 7/4/23. +// + +import SwiftUI + +struct AnchorsList: View { + @EnvironmentObject var anchor: NetworkModelController + @Environment(\.dismiss) private var dismiss + + var body: some View { + NavigationView { + VStack { + List(networks, id: \.network_id) { + network in + Button(network.name) { + anchor.anchor = network + dismiss() + } + } + NavigationLink("Add", destination: AddAnchor()) + Spacer() + } + } + } +} + +struct AnchorItem: View { + @EnvironmentObject var anchor: NetworkModelController + var network: Network + var body: some View { + NavigationLink(network.name) { + AnchorHome() + } + } +} + +struct NetworksList_Previews: PreviewProvider { + static var previews: some View { + AnchorsList() + } +} + diff --git a/iroh-console-ios/iroh/Views/Anchors/ScanNetworkQRCode.swift b/iroh-console-ios/iroh/Views/Anchors/ScanNetworkQRCode.swift new file mode 100644 index 0000000..605453a --- /dev/null +++ b/iroh-console-ios/iroh/Views/Anchors/ScanNetworkQRCode.swift @@ -0,0 +1,37 @@ +// +// ScanNetworkQRCode.swift +// iroh +// +// Created by Brendan O'Brien on 7/11/23. +// + +import SwiftUI + +struct QRCodeScannerExampleView: View { + @State private var isPresentingScanner = false + @State private var scannedCode: String? + + var body: some View { + VStack(spacing: 10) { + if let code = scannedCode { + NavigationLink("Next page", destination: NextView(scannedCode: code), isActive: .constant(true)).hidden() + } + + Button("Scan Code") { + isPresentingScanner = true + } + + Text("Scan a QR code to begin") + } + .sheet(isPresented: $isPresentingScanner) { + CodeScannerView(codeTypes: [.qr]) { response in + if case let .success(result) = response { + scannedCode = result.string + isPresentingScanner = false + } + } + } + } +} + + diff --git a/iroh-console-ios/iroh/Views/Config/EditConfig.swift b/iroh-console-ios/iroh/Views/Config/EditConfig.swift new file mode 100644 index 0000000..7a347d8 --- /dev/null +++ b/iroh-console-ios/iroh/Views/Config/EditConfig.swift @@ -0,0 +1,114 @@ +// +// EditConfig.swift +// iroh +// +// Created by Brendan O'Brien on 7/26/23. +// + +import SwiftUI + +struct EditConfig: View { + var network: Network + + var body: some View { + VStack{ + Topbar() + VStack(alignment: .leading, spacing: 0) { + Text("Anchor Config") + .font(Font.custom("Space Mono", size: 32)) + .foregroundColor(.primary) + .frame(maxWidth: .infinity, alignment: .leading) + Text("default.iroh.network") + .font(Font.custom("Space Mono", size: 16)) + .lineSpacing(23) + .foregroundColor(.secondary) + .frame(maxWidth: .infinity, alignment: .leading) + } + .padding(EdgeInsets(top: 0, leading: 20, bottom: 0, trailing: 20)) + .frame(maxWidth: .infinity) + Form{ + Section(header: Text("anchor configuration")) { + TextConfigItem(title: "Network Pretty Name", description: "The name displayed to users when they are asked to select a network.", key: "pretty_name", value: "My Network") + TextConfigItem(title: "Network Machine Name", description: "The name displayed to users when they are asked to select a network.", key: "pretty_name", value: "my-network") + BoolConfigItem(title: "Kubo Replication", description: "Publish all content to the kubo IPFS network", key: "kuboReplication") + PickerConfigItem(title: "Access", description: "How do you want to govern connections to your anchor?", key: "access", options: ["public", "private"]) + PickerConfigItem(title: "Data Availability", description: "What should happen when no online peers have content someone asks for?", key: "reproviding", options: ["full", "require token", "none"]) + } + } + } + } +} + +struct BoolConfigItem: View { + var title: String + var description: String + var key: String + @State private var active = false + + var body: some View { + VStack(alignment: .leading) { + Toggle(title, isOn: $active) + .font(Font.custom("Space Grotesk", size: 18).weight(.bold)) + .foregroundColor(.primary) + .frame(maxWidth: .infinity, alignment: .leading) + .toggleStyle(SwitchToggleStyle(tint: Color(red: 0.42, green: 0.42, blue: 0.85))) + Text(description) + .font(.subheadline) + .foregroundColor(.secondary) + .frame(maxWidth: .infinity, alignment: .leading) + } + } +} + +struct TextConfigItem: View { + var title: String + var description: String + var key: String + @State var value: String + + var body: some View { + VStack(alignment: .leading, spacing: 10) { + Text(title) + .font(Font.custom("Space Grotesk", size: 18).weight(.bold)) + .foregroundColor(.primary) + .frame(maxWidth: .infinity, alignment: .leading) + Text(description) + .font(.subheadline) + .foregroundColor(.secondary) + .frame(maxWidth: .infinity, alignment: .leading) + TextField("Network Pretty Name", text: $value) + } + } +} + +struct PickerConfigItem: View { + var title: String + var description: String + var key: String + var options: [String] = [] + @State private var index = 0 + + var body: some View { + VStack{ + Picker(selection: $index, label: Text(title)) { + ForEach(options, id: \.self) { option in + Text(option) + } + } + .font(Font.custom("Space Grotesk", size: 18).weight(.bold)) + .foregroundColor(.primary) + .frame(maxWidth: .infinity, alignment: .leading) + Text(description) + .font(.subheadline) + .foregroundColor(.secondary) + .frame(maxWidth: .infinity, alignment: .leading) + } + } + +} + +struct EditConfig_Previews: PreviewProvider { + static var previews: some View { + EditConfig(network: networks[0]) + } +} diff --git a/iroh-console-ios/iroh/Views/Console/Console.swift b/iroh-console-ios/iroh/Views/Console/Console.swift new file mode 100644 index 0000000..23a3256 --- /dev/null +++ b/iroh-console-ios/iroh/Views/Console/Console.swift @@ -0,0 +1,271 @@ +import SwiftUI +import IrohLib + +struct Console: View { + @EnvironmentObject var sessionInfo: UserSession + @EnvironmentObject var nodeManager: IrohNodeManager + + @State private var path = NavigationPath() + @State var doc: Doc? = nil + @State var entries: [HashableEntry]? = nil + + enum Field { + case key + case value + } + @State var key: String = "" + @State var value: String = "" + @FocusState private var focusedField: Field? + + @State private var image: UIImage? = nil + @State private var isImagePickerDisplaying: Bool = false + @State private var imageBytes: Data? = nil + @State private var showingInvite: Bool = false + + var body: some View { + NavigationStack(path: $path) { + Group { + if doc == nil { + PickDocument(join: self.join, setDoc: { (doc) -> () in + self.doc = doc + do { + try self.doc?.subscribe(cb: EventHandler(cb: self.update)) + } catch (let failure) { + print("error subscribing to new doc: \(failure)") + } + }) + } else { + VStack{ + Text(doc?.id().trunc(length: 15) ?? "") + HStack{ + Button("Leave") { + self.leave() + } + Spacer() + Button("Invite") { + showingInvite = true + }.popover(isPresented: $showingInvite, content: { + ShareDoc(doc: doc!, done: { () -> () in + showingInvite = false + }) + }) + } + Group { + List { + Section("Entries") { + if let entries = entries { + ForEach(entries) { entry in + NavigationLink(destination: DocEntry(doc: doc!, entry: entry), label: { + Text(entry.key) + .font(Font.custom("Space Mono", size: 14)) + }) + } + } + } + } + .listStyle(PlainListStyle()) // or .listStyle(PlainListStyle()) + .background(Color(UIColor.systemBackground)) // for light/dark mode compatibility + HStack { + VStack { + TextField("key", text: $key) + .autocapitalization(.none) + .overlay( + Rectangle() + .frame(height: 1) + .foregroundColor(.gray), + alignment: .bottom + ) + .focused($focusedField, equals: Field.key) + .submitLabel(.next) + TextField("value", text: $value) + .autocapitalization(.none) + .focused($focusedField, equals: Field.value) + .submitLabel(.send) + } + .background( + RoundedRectangle(cornerRadius: 4) // Rounded corners with radius 8 + .stroke(Color.gray, lineWidth: 1) // 1px gray border + ) + .onSubmit { + switch focusedField { + case .key: + focusedField = .value + default: + self.setTextEntry(key: key, value: value) + } + } + Button("PUT Text") { + self.setTextEntry(key: key, value: value) + } + } + Button("Take Photo") { + self.isImagePickerDisplaying = true + } + Button("Set Photo Entry") { + self.setPhotoEntry(key: key) + } + .disabled(imageBytes == nil) + } + .onAppear(perform: UIApplication.shared.handleKeyboard) + } + } + } + } + .padding() + .sheet(isPresented: $isImagePickerDisplaying, content: { + ImagePicker(image: $image, onDismiss: { image in + if let image = image { + imageBytes = image.jpegData(compressionQuality: 0.85) + } + }) + }) + } + + func join(_ ticket: String) { + if let node = nodeManager.node { + DispatchQueue.main.async { + do { + let docTicket = ticket.trimmingCharacters(in: .whitespacesAndNewlines) + self.doc = try node.docJoin(ticket: docTicket) + } catch { + print("no document :(") + return + } + + DispatchQueue.global(qos: .default).async { + do { + try self.doc?.subscribe(cb: EventHandler(cb: self.update)) + } catch (let failure) { + print("error subscribing to doc events: \(failure)") + return + } + } + + DispatchQueue.global(qos: .utility).asyncAfter(deadline: .now() + 1) { + self.update() + } + } + } + } + + func leave() { + do { + try doc?.close() + doc = nil + entries = nil + } catch { + print("couldn't stop syncing: \(error)") + } + } + + func setTextEntry(key: String, value: String) { + guard let doc = self.doc else { + print("Error: no doc") + return + } + guard let author = self.nodeManager.author else { + print("Error: no author") + return + } + let _ = try! doc.setBytes(author: author, key: key.data(using: .utf8)!, value: value.data(using: .utf8)!) + print("DOC SET: \(key): \(value)") + resetInputFields() + } + + func setPhotoEntry(key: String) { + let suffixedKey = ensureJPGSuffix(for: key) + guard let doc = self.doc else { + return + } + guard let author = self.nodeManager.author else { + return + } + let _ = try! doc.setBytes(author: author, key: suffixedKey.data(using: .utf8)!, value: imageBytes!) + print("DOC SET: \(key): ") + resetInputFields() + } + + func update() { + DispatchQueue.global(qos: .utility).async { + guard let doc = self.doc else { + return + } + do { + let query = Query.all(opts: QueryOptions(sortBy: SortBy.authorKey, direction: SortDirection.asc, offset: 0, limit: 1000)) + let entries = try doc.getMany(query: query) + + let mapped = entries.map { (entry) -> HashableEntry in + let author = entry.author().toString() + let k = String(decoding: entry.key(), as: UTF8.self) + + return HashableEntry( + id: author + k, + key: k, + authorId: author, + hash: entry.contentHash(), + entry: entry + ) + } + + DispatchQueue.main.async { + self.entries = mapped + } + } catch { + + } + } + } + + func ensureJPGSuffix(for key: String) -> String { + if key.hasSuffix(".jpg") { + return key + } else { + return "\(key).jpg" + } + } + + func resetInputFields() { + key = "" + value = "" + image = nil + imageBytes = nil + focusedField = nil + } +} + +class EventHandler: SubscribeCallback { + var cb: () -> Void + + init(cb: @escaping () -> Void) { + self.cb = cb + } + + func event(event: LiveEvent) throws { +// print("got event: \(event)") + cb() + } +} + +struct HashableEntry: Identifiable, Equatable { + var id: String + var key : String + var authorId: String + var hash: Hash + var entry: Entry + + static func == (lhs: HashableEntry, rhs: HashableEntry) -> Bool { + lhs.id == rhs.id + } +} + +struct Console_Previews: PreviewProvider { + static var previews: some View { + let session = UserSession() + Console() + .environmentObject(session) + .environmentObject(IrohNodeManager.shared) + .onAppear() { + IrohNodeManager.shared.start() + } + } +} diff --git a/iroh-console-ios/iroh/Views/Console/DocEntry.swift b/iroh-console-ios/iroh/Views/Console/DocEntry.swift new file mode 100644 index 0000000..f913737 --- /dev/null +++ b/iroh-console-ios/iroh/Views/Console/DocEntry.swift @@ -0,0 +1,84 @@ +// +// DocEntry.swift +// IrohApp +// +// Created by Brendan O'Brien on 8/27/23. +// + +import SwiftUI +import IrohLib + +struct DocEntry: View { + @EnvironmentObject var nodeManager: IrohNodeManager + var doc: Doc + var entry: HashableEntry + + @State var value: Data? + @State var fetchingValue: Bool = true + + var body: some View { + VStack(alignment: .leading, spacing: 10) { + Text("doc entry") + .font(Font.custom("Space Mono", size: 24)) + .foregroundColor(.primary) + .frame(maxWidth: .infinity, alignment: .leading) + + Text("KEY") + .font(Font.caption) + .foregroundColor(.secondary) + Text(entry.key) + .font(Font.custom("Space Mono", size: 16)) + .foregroundColor(.primary) + Text("HASH") + .font(Font.caption) + .foregroundColor(.secondary) + Text(entry.hash.toString()) + .font(Font.custom("Space Mono", size: 16)) + .foregroundColor(.secondary) + if let value = value { + Text("VALUE SIZE") + .font(Font.caption) + .foregroundColor(.secondary) + Text(ByteCountFormatter.string(fromByteCount: Int64(value.count), countStyle: .file)) + .font(Font.custom("Space Mono", size: 16)) + .foregroundColor(.primary) + Text("VALUE") + .font(Font.caption) + .foregroundColor(.secondary) + + if entry.key.hasSuffix(".jpg") { + if let uiImage = UIImage(data: value) { + Image(uiImage: uiImage) + .resizable() + .scaledToFit() + } else { + Text(".jpg extension is not a JPEG image") + } + } else { + if let string = String(data: value, encoding: .utf8) { + Text(string) + } else { + Text("value is not a utf-8 string") + } + } + } else if fetchingValue { + Text("loading value...") + } + Spacer() + } + .padding(20) + .onAppear() { + self.loadContent() + } + } + + private func loadContent() { + self.fetchingValue = true + do { + self.value = try self.nodeManager.node?.blobsReadToBytes(hash: entry.hash) + self.fetchingValue = false + } catch (let failure) { + print("error fetching \(entry.hash.toString()): \(failure.localizedDescription)") + } + } +} diff --git a/iroh-console-ios/iroh/Views/Console/NewDoc.swift b/iroh-console-ios/iroh/Views/Console/NewDoc.swift new file mode 100644 index 0000000..b9d6711 --- /dev/null +++ b/iroh-console-ios/iroh/Views/Console/NewDoc.swift @@ -0,0 +1,59 @@ +// +// NewDoc.swift +// IrohConsole +// +// Created by Brendan O'Brien on 9/21/23. +// + +import SwiftUI +import IrohLib + +struct NewDoc: View { + @EnvironmentObject var nodeManager: IrohNodeManager + @EnvironmentObject var sessionInfo: UserSession + var setDoc: (_ doc: Doc) -> () + + var body: some View { + VStack(alignment: .leading, spacing: 10) { + if let node = nodeManager.node { + Button(action: { + do { + let doc = try node.docCreate() + setDoc(doc) + } catch (let failure) { + print("couldn't create doc \(failure.localizedDescription)") + } + }, label: { + VStack(alignment: .leading, spacing: 10){ + Text("New Local Doc") + .font(Font.custom("Space Mono", size: 24)) + .foregroundColor(.accentColor) + Text("create document on this device") + .font(Font.custom("Space Mono", size: 14)) + .foregroundColor(.secondary) + } + .background(Color.init(uiColor: UIColor(red: 202, green: 201, blue: 255, alpha: 1))) + .cornerRadius(3.0) + .padding(10) + .frame(maxWidth: .infinity, alignment: .leading) + }) + + if sessionInfo.anchorName != "" { + Button(action: { + do { + let doc = try node.docCreate() + setDoc(doc) + } catch (let failure) { + print("couldn't create doc \(failure.localizedDescription)") + } + }, label: { + VStack{ + Text("New Anchor Doc") + Text("ask \(sessionInfo.anchorName).iroh.network to create a new document & share write access with this device") + } + }) + } + } + } + } +} diff --git a/iroh-console-ios/iroh/Views/Console/PickDocument.swift b/iroh-console-ios/iroh/Views/Console/PickDocument.swift new file mode 100644 index 0000000..a6d3fde --- /dev/null +++ b/iroh-console-ios/iroh/Views/Console/PickDocument.swift @@ -0,0 +1,95 @@ +// +// PickDocument.swift +// IrohApp +// +// Created by Brendan O'Brien on 9/14/23. +// + +import SwiftUI +import IrohLib +import CodeScanner + +struct PickDocument: View { + @EnvironmentObject var nodeManager: IrohNodeManager + @EnvironmentObject var sessionInfo: UserSession + @State private var ticketString: String = "" + @State private var showingCreateActionSheet: Bool = false + @State private var showingQRScanner: Bool = false + let join: (_ ticket: String) -> () + let setDoc: (_ doc: Doc) -> () + + var scannerSheet: some View { + CodeScannerView( + codeTypes: [.qr], + completion: { result in + if case let .success(code) = result { + self.ticketString = code.string + self.showingQRScanner = false + } + }) + } + + var body: some View { + VStack(spacing: 5) { + VStack { + Text("Console") + .font(Font.custom("Space Mono", size: 32)) + .foregroundColor(.primary) + .frame(maxWidth: .infinity, alignment: .leading) + Text("choose a document to get started") + .font(Font.custom("Space Mono", size: 14)) + .foregroundColor(.secondary) + .frame(maxWidth: .infinity, alignment: .leading) + }.padding(EdgeInsets(top: 0, leading: 20, bottom: 10, trailing: 20)) + + + List { + if let node = nodeManager.node { + Section("local docs") { + + } + } + + if let docs = sessionInfo.documents { + Section("iroh.network") { + ForEach(docs, id: \.self) { doc in + Button(doc.doc_id.trunc(length: 20)) { +// self.join(doc.ticket) + } + .font(Font.custom("Space Mono", size: 16)) + .foregroundColor(.accentColor) + } + } + } + } + .listStyle(PlainListStyle()) // or .listStyle(PlainListStyle()) + .background(Color(UIColor.systemBackground)) // for light/dark mode compatibility + + HStack{ + TextField("Paste Ticket", text: $ticketString, axis: .vertical) + .textFieldStyle(.roundedBorder) + .padding() + Button("Join") { + self.join(ticketString) + } + + Button { + showingQRScanner = true + } label: { + Image(systemName: "qrcode.viewfinder") + } + .sheet(isPresented: $showingQRScanner) { + self.scannerSheet + } + } + Button(action: { + showingCreateActionSheet = true + }, label: { + Text("New Document") + }) + } + .popover(isPresented: $showingCreateActionSheet, content: { + NewDoc(setDoc: self.setDoc) + }) + } +} diff --git a/iroh-console-ios/iroh/Views/Console/ShareDoc.swift b/iroh-console-ios/iroh/Views/Console/ShareDoc.swift new file mode 100644 index 0000000..0dd9420 --- /dev/null +++ b/iroh-console-ios/iroh/Views/Console/ShareDoc.swift @@ -0,0 +1,57 @@ +// +// ShareDoc.swift +// IrohApp +// +// Created by Brendan O'Brien on 9/14/23. +// + +import SwiftUI +import IrohLib + +struct ShareDoc: View { + var doc: Doc + var done: () -> () + + var body: some View { + + Button("Done") { + done() + } + + if let invite = try? doc.share(mode: ShareMode.write, addrOptions: AddrInfoOptions.id) { + VStack(alignment: .leading) { + Text("ticket:") + .font(Font.custom("Space Mono", size: 20)) + Text(invite) + .font(Font.custom("Space Mono", size: 14)) + .foregroundColor(.secondary) + Button { + UIPasteboard.general.string = invite + } label: { + VStack { + Image(systemName: "clipboard") + Text("copy ticket") + } + } + if let image = generateQRCode(from: invite) { + Image(uiImage: image) + } + }.padding(20) + } + } + + func generateQRCode(from string: String) -> UIImage? { + let data = string.data(using: String.Encoding.ascii) + + if let filter = CIFilter(name: "CIQRCodeGenerator") { + filter.setValue(data, forKey: "inputMessage") + let transform = CGAffineTransform(scaleX: 3, y: 3) + + if let output = filter.outputImage?.transformed(by: transform) { + return UIImage(ciImage: output) + } + } + + return nil + } +} diff --git a/iroh-console-ios/iroh/Views/Login.swift b/iroh-console-ios/iroh/Views/Login.swift new file mode 100644 index 0000000..cd7c0c3 --- /dev/null +++ b/iroh-console-ios/iroh/Views/Login.swift @@ -0,0 +1,59 @@ +// +// Login.swift +// iroh +// +// Created by Brendan O'Brien on 8/4/23. +// + +import SwiftUI + +struct Login: View { + @EnvironmentObject var sessionInfo: UserSession + +// @State private var anchor: String = "" + @State private var username: String = "" + @State private var password: String = "" + @State private var failure: String = "" + + var body: some View { + VStack(alignment: .leading){ +// Text("ANCHOR") +// .font(Font.caption) +// .foregroundColor(.secondary) +// HStack { +// TextField("anchor-name", text: $anchor) +// .autocapitalization(.none) +// .fixedSize(horizontal: true, vertical: false) +// .multilineTextAlignment(.center) +// Text(".iroh.network") +// .foregroundColor(.secondary) +// Spacer() +// } + + Text("USERNAME") + .font(Font.caption) + .foregroundColor(.secondary) + TextField("username", text: $username) + .autocapitalization(.none) + + Text("PASSWORD") + .font(Font.caption) + .foregroundColor(.secondary) + SecureField("password", text: $password) + .autocapitalization(.none) + + Button("Login") { + sessionInfo.login(username: username, password: password) + } + } + .padding(20) + } + +} + +struct Login_Previews: PreviewProvider { + static var previews: some View { + Login() + .environmentObject(UserSession()) + } +} diff --git a/iroh-console-ios/iroh/Views/NodeHome.swift b/iroh-console-ios/iroh/Views/NodeHome.swift new file mode 100644 index 0000000..3f76373 --- /dev/null +++ b/iroh-console-ios/iroh/Views/NodeHome.swift @@ -0,0 +1,206 @@ +// +// NodeHome.swift +// IrohApp +// +// Created by Brendan O'Brien on 9/14/23. +// + +import SwiftUI +import IrohLib +import Charts + +struct NodeHome: View { + @EnvironmentObject var nodeManager: IrohNodeManager + @State var showAdvancedStats: Bool = false + + var body: some View { + Group { + if nodeManager.node != nil { + VStack { + VStack { + Text("Node") + .font(Font.custom("Space Mono", size: 32)) + .foregroundColor(.primary) + .frame(maxWidth: .infinity, alignment: .leading) + Text("ID: \(nodeManager.nodeID.trunc(length: 12))") + .font(Font.custom("Space Mono", size: 16)) + .foregroundColor(.secondary) + .frame(maxWidth: .infinity, alignment: .leading) + }.padding(10) + + if let conns = nodeManager.connections { + List { + Section("Connections") { + if conns.count == 0 { + HStack { + Spacer() + Text("No Connections") + .font(Font.headline) + .foregroundColor(Color.gray) + Spacer() + } + .listRowBackground(Color.clear) + .listRowSeparator(.hidden) + } + ForEach(conns) { conn in + VStack { + HStack { + Text("\(conn.id.trunc(length: 12))") + .fontWeight(.bold) + Spacer() + Text("\(self.connTypeString(conn.connType)) \(conn.latencyString())ms") + } + if let data = nodeManager.connectionHistories[conn.id] { + Chart { + ForEach(data) { d in + LineMark( + x: .value("Date", d.id), + y: .value("Latency", d.latency) + ) + .foregroundStyle(by: .value("Connection Type", self.connTypeInt(d.connType))) + } + } + .frame(maxHeight: 60) + .chartLegend(.hidden) +// .chartYAxis { +// AxisMarks(values: .automatic(desiredCount: 4)) { value in +// if let bytes = value.as(Double.self) { +// AxisValueLabel() +// } +// AxisGridLine() +// AxisTick() +// } +// } + } + } + } + } + + if let hs = nodeManager.historicalStats { + Section("data served") { + if hs.count > 0 && hs[0].egress_bytes > 0 { + Chart { + ForEach(hs, id: \.date) { day in + BarMark( + x: .value("Date", day.date), + y: .value("Total Count", day.egress_bytes) + ) + } + } + .frame(maxHeight: 180) + .chartYAxis { + AxisMarks(values: .automatic(desiredCount: 4)) { value in + if let bytes = value.as(Int64.self) { + AxisValueLabel(ByteCountFormatter.string(fromByteCount: bytes, countStyle: .file)) + } + AxisGridLine() + AxisTick() + } + } + } else { + HStack { + Spacer() + Text("No Data Served") + .font(Font.headline) + .foregroundColor(Color.gray) + Spacer() + } + } + } + } + + if let stats = nodeManager.nodeStats { + Section("Stats") { + Button(showAdvancedStats ? "hide advanced stats" : "show advanced stats") { + showAdvancedStats = !showAdvancedStats + } + .font(Font.custom("Space Mono", size: 16)) + + if showAdvancedStats { + ForEach(Array(stats.keys.sorted()), id: \.self) { key in + if let stat = stats[key] { + HStack{ + VStack { + Text(key) + .font(Font.custom("Space Mono", size: 14)) + .foregroundColor(.primary) + .frame(maxWidth: .infinity, alignment: .leading) + Text(stat.description) + .font(.system(size: 12)) + .foregroundColor(.secondary) + .frame(maxWidth: .infinity, alignment: .leading) + } + Text("\(stat.value)") + .frame(maxWidth: 100, alignment: .trailing) + } + } + } + } else { + ForEach(["send_data", "recv_data_ipv4"], id: \.self) { key in + if let stat = stats[key] { + HStack{ + VStack { + Text(key) + .font(Font.custom("Space Mono", size: 14)) + .foregroundColor(.primary) + .frame(maxWidth: .infinity, alignment: .leading) + Text(stat.description) + .font(.system(size: 10)) + .foregroundColor(.secondary) + .frame(maxWidth: .infinity, alignment: .leading) + } + Text(ByteCountFormatter.string(fromByteCount: Int64(stat.value), countStyle: .file)) + .frame(maxWidth: 100, alignment: .trailing) + } + } + } + } + } + } + + } +// .listStyle(PlainListStyle()) + } + } + } else { + Text("No Node is running") + } + } + } + + func connTypeString(_ ct: ConnectionType) -> String { + switch ct.type() { + case .mixed: + return "mixed" + case .direct: + return "direct" + case .relay: + return "relay" + case .none: + return "none" + } + } + + func connTypeInt(_ ct: ConnectionType) -> Int { + switch ct.type() { + case .mixed: + return 3 + case .direct: + return 2 + case .relay: + return 1 + case .none: + return 0 + } + } +} + +struct NodeHome_Previews: PreviewProvider { + static var previews: some View { + NodeHome() + .environmentObject(IrohNodeManager.shared) + .onAppear() { + IrohNodeManager.shared.start() + } + } +} diff --git a/iroh-console-ios/iroh/Views/Onboarding/Signup.swift b/iroh-console-ios/iroh/Views/Onboarding/Signup.swift new file mode 100644 index 0000000..de494d6 --- /dev/null +++ b/iroh-console-ios/iroh/Views/Onboarding/Signup.swift @@ -0,0 +1,20 @@ +// +// Signup.swift +// iroh +// +// Created by Brendan O'Brien on 8/4/23. +// + +import SwiftUI + +struct Signup: View { + var body: some View { + Text(/*@START_MENU_TOKEN@*/"Hello, World!"/*@END_MENU_TOKEN@*/) + } +} + +struct Signup_Previews: PreviewProvider { + static var previews: some View { + Signup() + } +} diff --git a/iroh-console-ios/iroh/Views/Onboarding/Welcome.swift b/iroh-console-ios/iroh/Views/Onboarding/Welcome.swift new file mode 100644 index 0000000..c3045d1 --- /dev/null +++ b/iroh-console-ios/iroh/Views/Onboarding/Welcome.swift @@ -0,0 +1,74 @@ +// +// Welcome.swift +// iroh +// +// Created by Brendan O'Brien on 8/4/23. +// + +import SwiftUI + +struct Welcome: View { + var body: some View { + VStack(spacing: 0) { + VStack(alignment: .leading, spacing: 40) { + Spacer() + + VStack(alignment: .leading, spacing: 10) { + Text("Welcome") + .font(Font.custom("Space Mono", size: 32)) + .lineSpacing(48) + .foregroundColor(.white) + Text("You’ll need an invite to iroh.network to use this app. If you don’t have one, feel free to join the waitlist.") + .font(Font.custom("Space Mono", size: 16)) + .lineSpacing(23) + .foregroundColor(.white) + } + .frame(maxWidth: .infinity, minHeight: 150, maxHeight: 150) + + VStack(alignment: .leading, spacing: 10) { + HStack(spacing: 0) { + Text("SIGNUP") + .font(Font.custom("Space Mono", size: 20)) + .lineSpacing(48) + .foregroundColor(.white) + } + .padding(EdgeInsets(top: 6, leading: 10, bottom: 6, trailing: 10)) + .frame(maxWidth: .infinity) + .background(Color(red: 0.42, green: 0.42, blue: 0.85)) + .cornerRadius(2) + HStack(spacing: 0) { + Text("LOGIN") + .font(Font.custom("Space Mono", size: 20)) + .lineSpacing(48) + .foregroundColor(.white) + } + .padding(EdgeInsets(top: 6, leading: 10, bottom: 6, trailing: 10)) + .frame(maxWidth: .infinity) + .background(Color(red: 0.42, green: 0.42, blue: 0.85)) + .cornerRadius(2) + HStack(spacing: 0) { + Text("JOIN THE WAITLIST") + .font(Font.custom("Space Mono", size: 20)) + .lineSpacing(48) + .foregroundColor(.white) + } + .padding(EdgeInsets(top: 6, leading: 10, bottom: 6, trailing: 10)) + .frame(maxWidth: .infinity) + .background(Color(red: 0.42, green: 0.42, blue: 0.85)) + .cornerRadius(2) + } + .frame(maxWidth: .infinity, minHeight: 200, maxHeight: 200) + } + .padding(20) + .frame(maxWidth: .infinity, maxHeight: .infinity) + } + .frame(width: 390, height: 844) + .background(Color(red: 0.13, green: 0.15, blue: 0.20)) + } +} + +struct Welcome_Previews: PreviewProvider { + static var previews: some View { + Welcome() + } +} diff --git a/iroh-console-ios/iroh/Views/Subviews/AnchorEgressSummaryStats.swift b/iroh-console-ios/iroh/Views/Subviews/AnchorEgressSummaryStats.swift new file mode 100644 index 0000000..dec2c33 --- /dev/null +++ b/iroh-console-ios/iroh/Views/Subviews/AnchorEgressSummaryStats.swift @@ -0,0 +1,61 @@ +// +// AnchorEgressSummaryStats.swift +// IrohApp +// +// Created by Brendan O'Brien on 9/13/23. +// + +import SwiftUI + +struct AnchorEgressSummaryStats: View { + let egress: IrohAnchorAPI.Types.Response.Egress + + var body: some View { + HStack(alignment: .top, spacing: 0) { + Text("last 30 days") + .font(Font.custom("Space Grotesk", size: 15).weight(.light)) + .lineSpacing(22) + .foregroundColor(.black) + } + + HStack(alignment: .center, spacing: 20) { + VStack(spacing: 10) { + Text(ByteCountFormatter.string(fromByteCount: Int64(egress.summary.egress_bytes), countStyle: .file)) + .font(Font.custom("Space Mono", size: 24).weight(.bold)) + .lineSpacing(34) + .foregroundColor(Color(red: 0.14, green: 0.16, blue: 0.20)) + Text("BLOBS") + .font(Font.custom("Space Mono", size: 13)) + .lineSpacing(23) + .foregroundColor(.secondary) + } + VStack(spacing: 10) { + Text(String(format: "%d", egress.summary.requests)) + .font(Font.custom("Space Mono", size: 24).weight(.bold)) + .lineSpacing(34) + .foregroundColor(Color(red: 0.14, green: 0.16, blue: 0.20)) + Text("REQUESTS") + .font(Font.custom("Space Mono", size: 13)) + .lineSpacing(23) + .foregroundColor(.secondary) + } + VStack(spacing: 10) { + Text(String(format: "%d", egress.summary.egress_blobs)) + .font(Font.custom("Space Mono", size: 24).weight(.bold)) + .lineSpacing(34) + .foregroundColor(Color(red: 0.14, green: 0.16, blue: 0.20)) + Text("BLOBS") + .font(Font.custom("Space Mono", size: 13)) + .lineSpacing(23) + .foregroundColor(.secondary) + } + } + .frame(maxWidth: .infinity) + } +} + +struct AnchorEgressSummaryStats_Previews: PreviewProvider { + static var previews: some View { + AnchorEgressSummaryStats(egress: mockEgress()) + } +} diff --git a/iroh-console-ios/iroh/Views/Subviews/DailyEgressBarChart.swift b/iroh-console-ios/iroh/Views/Subviews/DailyEgressBarChart.swift new file mode 100644 index 0000000..b811a54 --- /dev/null +++ b/iroh-console-ios/iroh/Views/Subviews/DailyEgressBarChart.swift @@ -0,0 +1,50 @@ +// +// BarChart.swift +// iroh +// +// Created by Brendan O'Brien on 7/27/23. +// + +import SwiftUI +import Charts + +struct DailyEgressBarChart: View { + var title: String + var egress: [IrohAnchorAPI.Types.Response.EgressDaily] + + var body: some View { + Chart { + ForEach(egress, id: \.date) { day in + BarMark( + x: .value("Date", day.date), + y: .value("Total Count", day.egress_bytes) + ) + .foregroundStyle(by: .value("Node ID", self.nodeIdValue(day.nodeId))) + } + } + .frame(maxHeight: 180) + .chartYAxis { + AxisMarks(values: .automatic(desiredCount: 4)) { value in + if let bytes = value.as(Int64.self) { + AxisValueLabel(ByteCountFormatter.string(fromByteCount: bytes, countStyle: .file)) + } + AxisGridLine() + AxisTick() + } + } + } + + private func nodeIdValue(_ nodeId: String?) -> String { + if nodeId == nil || nodeId == "" { + return "remote" + } + + return "local" + } +} + +struct BarChart_Previews: PreviewProvider { + static var previews: some View { + DailyEgressBarChart(title: "chart", egress: mockEgress().daily) + } +} diff --git a/iroh-console-ios/iroh/Views/Subviews/ImagePicker.swift b/iroh-console-ios/iroh/Views/Subviews/ImagePicker.swift new file mode 100644 index 0000000..0a5ee6d --- /dev/null +++ b/iroh-console-ios/iroh/Views/Subviews/ImagePicker.swift @@ -0,0 +1,48 @@ +// +// ImagePicker.swift +// IrohApp +// +// Created by Brendan O'Brien on 9/13/23. +// + +import SwiftUI +import UIKit + +struct ImagePicker: UIViewControllerRepresentable { + @Binding var image: UIImage? + var onDismiss: (UIImage?) -> Void + + func makeCoordinator() -> Coordinator { + Coordinator(self) + } + + func makeUIViewController(context: UIViewControllerRepresentableContext) -> UIImagePickerController { + let picker = UIImagePickerController() + picker.delegate = context.coordinator + return picker + } + + func updateUIViewController(_ uiViewController: UIImagePickerController, context: UIViewControllerRepresentableContext) { + } + + class Coordinator: NSObject, UINavigationControllerDelegate, UIImagePickerControllerDelegate { + var parent: ImagePicker + + init(_ parent: ImagePicker) { + self.parent = parent + } + + func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) { + if let uiImage = info[.originalImage] as? UIImage { + parent.image = uiImage + parent.onDismiss(uiImage) + } + picker.dismiss(animated: true) + } + + func imagePickerControllerDidCancel(_ picker: UIImagePickerController) { + parent.onDismiss(nil) + picker.dismiss(animated: true) + } + } +} diff --git a/iroh-console-ios/iroh/Views/Subviews/Topbar.swift b/iroh-console-ios/iroh/Views/Subviews/Topbar.swift new file mode 100644 index 0000000..d5e645d --- /dev/null +++ b/iroh-console-ios/iroh/Views/Subviews/Topbar.swift @@ -0,0 +1,42 @@ +// +// Topbar.swift +// iroh +// +// Created by Brendan O'Brien on 7/27/23. +// + +import SwiftUI + +struct Topbar: View { + @State private var showingPopover = false + + var body: some View { + HStack(alignment: .bottom, spacing: 0) { + HStack(spacing: 0) { + Button(action: { + showingPopover = true + }) { + Image("IrohLogoPurple") + .resizable() + .frame(width: 90, height: 30) + } + .popover(isPresented: $showingPopover) { + AnchorsList() + } + } + .frame(width: 75, height: 25) + Rectangle() + .foregroundColor(.clear) + .frame(maxWidth: .infinity, minHeight: 15, maxHeight: 15) + UserProfileRound() + } + .padding(EdgeInsets(top: 0, leading: 0, bottom: 10, trailing: 0)) + } +} + +struct Topbar_Previews: PreviewProvider { + static var previews: some View { + Topbar() + .environmentObject(UserSession()) + } +} diff --git a/iroh-console-ios/iroh/Views/Subviews/UserProfileRound.swift b/iroh-console-ios/iroh/Views/Subviews/UserProfileRound.swift new file mode 100644 index 0000000..ac381b6 --- /dev/null +++ b/iroh-console-ios/iroh/Views/Subviews/UserProfileRound.swift @@ -0,0 +1,53 @@ +// +// UserProfileRound.swift +// IrohApp +// +// Created by Brendan O'Brien on 9/13/23. +// + +import SwiftUI + +struct UserProfileRound: View { + @EnvironmentObject var session: UserSession + @State var showingActionSheet: Bool = false + + var body: some View { + Group { + if let user = session.user { + Button(action: { showingActionSheet = true }, label: { + ZStack() { + Ellipse() + .foregroundColor(.clear) + .frame(width: 32, height: 32) + .background(Color(red: 0.42, green: 0.42, blue: 0.85)) + .offset(x: 0, y: 0) + .cornerRadius(32) + Text(user.name.trunc(length: 1, trailing: "")) + .font(Font.custom("Space Mono", size: 16)) + .lineSpacing(23) + .foregroundColor(.white) + .offset(x: 0, y: -0.50) + } + .frame(width: 32, height: 32) + }) + .popover(isPresented: $showingActionSheet, content: { + VStack(alignment: .leading, spacing: 10) { + UserProfile(user: user) + + Button("logout") { + showingActionSheet = false + session.logout() + } + }.padding(20) + }) + } + } + } +} + +struct UserProfileRound_Previews: PreviewProvider { + static var previews: some View { + UserProfileRound() + .environmentObject(UserSession()) + } +} diff --git a/iroh-console-ios/iroh/Views/Tests/DocSyncTest.swift b/iroh-console-ios/iroh/Views/Tests/DocSyncTest.swift new file mode 100644 index 0000000..38a47ec --- /dev/null +++ b/iroh-console-ios/iroh/Views/Tests/DocSyncTest.swift @@ -0,0 +1,308 @@ +// +// DocSyncTest.swift +// iroh +// +// Created by Brendan O'Brien on 7/26/23. +// + +import SwiftUI +import IrohLib + +struct DocSyncTest: View { + @EnvironmentObject var nodeManager: IrohNodeManager + @State private var ticketString: String = "" + @State private var contentList: String = "No document joined" + @State var currentDoc: Doc? + + @State private var events: [DocEvent] = [] + @State private var currentIndex: Int = 0 + @State private var countdownSeconds: Int = 3 + @State private var countdownTimer: Timer? = nil + @State private var timer: Timer? = nil + @State private var mode: testMode = .countdown + + @State private var relayedPeers: Int = 0 + @State private var directPeers: Int = 0 + @State private var writes: Int = 0 + @State private var messages: Int = 0 + @State private var entries: Int = 0 + @State private var errors: Int = 0 + + private func formatDate(date: Date) -> String { + let dateFormatter = DateFormatter() + dateFormatter.dateFormat = "HH:mm:ss" + dateFormatter.locale = Locale(identifier: "en_US_POSIX") + return dateFormatter.string(from: date) + } + + func join() { + if let node = nodeManager.node { + print("joining \(ticketString)") + let doc = try! node.docJoin(ticket: ticketString) + try! doc.subscribe(cb: CallbackHandler(view: self)) + currentDoc = doc + + DispatchQueue.main.asyncAfter(deadline: .now() + 1) { + try! loadContent() + } + } + } + + func loadContent() throws { +// if let doc = currentDoc { +// let content = try doc.getMany(filter: GetFilter.all()) +// print("got \(content.count) elements for \(doc.id())") +// +// let formattedList = content.map { (entry) -> String in +// let content: String = { +// do { +// let c = try doc.getContentBytes(entry: entry) +// return String(decoding: c, as: UTF8.self) +// } catch { +// return "N/A" +// } +// }() +// let key = String(decoding: entry.key(), as: UTF8.self) +// let author_s = entry.author().toString() +// return "Author: \(author_s) wrote '\(key)' -> '\(content)'" +// }.joined(separator: "\n") +// contentList = "\(doc.id()) (\(content.count))\n\n\(formattedList)" +// } + } + + private func startCountdown() { + countdownTimer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { _ in + if countdownSeconds > 0 { + countdownSeconds = countdownSeconds - 1 + } else { + switch mode { + case .countdown: + mode = .running + countdownSeconds = 30 + startTimer() + case .running: + mode = .complete + case .complete: + countdownTimer?.invalidate() + countdownTimer = nil + } + } + } + } + + private func startTimer() { + timer = Timer.scheduledTimer(withTimeInterval: randomInterval(), repeats: true) { _ in + if currentIndex < mockDocEvents.count { + var event = mockDocEvents[currentIndex] + event.ts = Date() + switch event.tipe { + case .DocumentCreated: + messages += 1 + case .YouSetKey: + entries += 1 + writes += 1 + case .YouDeletedKey: + entries -= 1 + case .PeerJoined: + messages += 3 + directPeers += 1 + case .PeerLeft: + messages += 1 + directPeers -= 1 + case .PeerSetKey: + entries += 1 + messages += 1 + case .PeerDeletedKey: + messages += 1 + entries -= 1 + case .Error: + errors += 1 + messages += 1 + } + events.append(event) + currentIndex += 1 + } else { + stopTimer() + } + } + } + + private func stopTimer() { + timer?.invalidate() + timer = nil + } + + private func randomInterval() -> TimeInterval { + let randomSeconds = Double.random(in: 0.001...0.1) + return randomSeconds + } + + var body: some View { + Group { + if countdownSeconds > 0 && mode == .countdown { + VStack { + Spacer() + Text("\(countdownSeconds)") + .font(Font.custom("Space Grotesk", size: 130).weight(.bold)) + .foregroundColor(Color(red: 0.42, green: 0.42, blue: 0.85)) + .frame(maxWidth: .infinity, alignment: .center) + Spacer() + } + .background(Color(red: 0.13, green: 0.15, blue: 0.20).opacity(0.92)) + .frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity, alignment: .topLeading) + } else { + VStack(alignment: .leading, spacing: 10) { + VStack(alignment: .leading, spacing: 0) { + Text("Test One") + .font(Font.custom("Space Grotesk", size: 32).weight(.bold)) + .lineSpacing(48) + .foregroundColor(.primary) + Text("doc sync test") + .font(Font.custom("Space Mono", size: 16)) + .lineSpacing(23) + .foregroundColor(.secondary) + } + .padding(20) + + VStack(spacing: 10) { + Group { + if countdownSeconds > 0 { + Text("00:" + String(format: "%02d", countdownSeconds)) + .font(Font.custom("Space Grotesk", size: 24).weight(.bold)) + .lineSpacing(34) + .foregroundColor(Color(red: 0.42, green: 0.42, blue: 0.85)) + } else { + Text("complete") + .font(Font.custom("Space Grotesk", size: 24).weight(.bold)) + .lineSpacing(34) + .foregroundColor(Color(red: 0.42, green: 0.42, blue: 0.85)) + } + } + Text("TIME REMAINING") + .font(Font.custom("Space Mono", size: 12)) + .foregroundColor(.secondary) + } + .padding(EdgeInsets(top: 0, leading: 5, bottom: 0, trailing: 5)) + .frame(width: 350, height: 87) + + HStack(alignment: .top, spacing: 10) { + VStack(spacing: 10) { + Text("\(writes)") + .font(Font.custom("Space Grotesk", size: 24).weight(.bold)) + .lineSpacing(34) + .foregroundColor(Color(red: 0.14, green: 0.16, blue: 0.20)) + Text("WRITES") + .font(Font.custom("Space Mono", size: 12)) + .foregroundColor(.secondary) + } + .padding(EdgeInsets(top: 0, leading: 5, bottom: 0, trailing: 5)) + .frame(width: 160) + VStack(spacing: 10) { + Text("\(entries)") + .font(Font.custom("Space Grotesk", size: 24).weight(.bold)) + .lineSpacing(34) + .foregroundColor(Color(red: 0.14, green: 0.16, blue: 0.20)) + Text("ENTRIES") + .font(Font.custom("Space Mono", size: 12)) + .foregroundColor(.secondary) + } + .padding(EdgeInsets(top: 0, leading: 5, bottom: 0, trailing: 5)) + .frame(width: 160) + } + .frame(maxWidth: .infinity) + + HStack(alignment: .top, spacing: 10) { + VStack(spacing: 10) { + Text("\(messages)") + .font(Font.custom("Space Grotesk", size: 24).weight(.bold)) + .lineSpacing(34) + .foregroundColor(Color(red: 0.14, green: 0.16, blue: 0.20)) + Text("MESSAGES") + .font(Font.custom("Space Mono", size: 12)) + .foregroundColor(.secondary) + } + .padding(EdgeInsets(top: 0, leading: 5, bottom: 0, trailing: 5)) + .frame(width: 160) + VStack(spacing: 10) { + Text("\(errors)") + .font(Font.custom("Space Mono", size: 24).weight(.bold)) + .foregroundColor(Color(red: 0.14, green: 0.16, blue: 0.20)) + Text("ERRORS") + .font(Font.custom("Space Mono", size: 12)) + .foregroundColor(.secondary) + } + .padding(EdgeInsets(top: 0, leading: 5, bottom: 0, trailing: 5)) + .frame(width: 160) + } + .frame(maxWidth: .infinity) + + VStack(alignment: .leading) { + Text("\(relayedPeers + directPeers) Connected Peers") + .font(Font.custom("Space Grotesk", size: 20).weight(.bold)) + .foregroundColor(.primary) + Text("\(relayedPeers) relayed | \(directPeers) direct") + .font(Font.custom("Space Mono", size: 12)) + .foregroundColor(.secondary) + } + .padding(EdgeInsets(top: 20, leading: 20, bottom: 0, trailing: 0)) + + Text("Event Log") + .font(Font.custom("Space Grotesk", size: 15).weight(.light)) + .lineSpacing(22) + .padding(EdgeInsets(top: 20, leading: 20, bottom: 0, trailing: 0)) + .foregroundColor(.primary) + List(events, id: \.self) { event in + HStack { + Text(formatDate(date: event.ts)) + .font(Font.custom("Space Grotesk", size: 12).weight(.light)) + .foregroundColor(.secondary) + Text("\(event.tipe.rawValue)") + .font(Font.custom("Space Grotesk", size: 12).weight(.light)) + .foregroundColor(.primary) + } + } + }.frame(maxWidth: .infinity, maxHeight: .infinity) + } + } + .onAppear { + startCountdown() + } + .onDisappear { + stopTimer() + } + } +} + +enum testMode { + case countdown + case running + case complete +} + +class CallbackHandler: SubscribeCallback { + @State private var view: DocSyncTest + + init(view: DocSyncTest) { + self.view = view + } + + func event(event: LiveEvent) throws { +// let _ = print("got event \(event)") + try! view.loadContent() + } +} + +extension StringProtocol { + var data: Data { .init(utf8) } + var bytes: [UInt8] { .init(utf8) } +} + +struct DocSyncTest_Previews: PreviewProvider { + static var previews: some View { + DocSyncTest() + .environmentObject(IrohNodeManager.shared) + .onAppear() { + IrohNodeManager.shared.start() + } + } +} diff --git a/iroh-console-ios/iroh/Views/Tests/JoinDocSyncTest.swift b/iroh-console-ios/iroh/Views/Tests/JoinDocSyncTest.swift new file mode 100644 index 0000000..03342e9 --- /dev/null +++ b/iroh-console-ios/iroh/Views/Tests/JoinDocSyncTest.swift @@ -0,0 +1,191 @@ +// +// JoinDocSyncTest.swift +// IrohApp +// +// Created by Brendan O'Brien on 8/9/23. +// +// +import SwiftUI +import IrohLib + +struct JoinDocSyncTest: View { + @EnvironmentObject var irohNodeManager: IrohNodeManager + @State private var ticketString: String = "" + @State private var contentList: String = "No document joined" + + + var body: some View { + VStack { + TextField("Enter Doc Ticket", text: $ticketString) + Button("Join Doc") { + } + Text("Current Document: \(contentList)") + } + .padding() + } + + func join() { + print("joining \(ticketString)") + let _ = try! irohNodeManager.node?.docJoin(ticket: ticketString) + for _ in 1...5 { + DispatchQueue.main.asyncAfter(deadline: .now() + 1) { +// let content = try! doc?.all() + // print("got \(content.count) elements for \(doc?.id())") + + // let formattedList = content.map { (entry) -> String in + // let content: String = { + // do { + // let c = try! doc.getContentBytes(entry: entry) + // return String(decoding: c, as: UTF8.self) + // } catch { + // return "N/A" + // } + // }() + // let key = String(decoding: entry.key(), as: UTF8.self) + // let author_s = entry.author().toString() + // return "Author: \(author_s) wrote '\(key)' -> '\(content)'" + // }.joined(separator: "\n") + // contentList = "\(doc.id()) (\(content.count))\n\n\(formattedList)" + } + } + } +} + +struct JoinDocSyncTest_Previews: PreviewProvider { + static var previews: some View { + let inm = IrohNodeManager.shared + JoinDocSyncTest() + .environmentObject(inm) + .onAppear() { + inm.start() + } + } +} + +//import SwiftUI +//import IrohLib +// +//struct JoinDocSyncTest: View { +// @State var state: IrohNode +// @State private var ticketString: String = "" +// @State private var contentList: String = "No document joined" +// @State var currentDoc: Doc? +// +// var body: some View { +// let peer_id = state.peerId() +// /*let doc = try! state.createDoc() +// let doc_id = doc.id() +// let doc_ticket = try! doc.shareWrite() +// let doc_ticket_string = doc_ticket.toString() +// +// let author = try! state.createAuthor() +// //print("created author: \(author.toString())") +// +// let hello_entry = try! doc.setBytes( +// author: author, +// key: "hello".data, +// value: "world".data +// ) +// let world_entry = try! doc.setBytes( +// author: author, +// key: "world".data, +// value: "foo".data +// ) +// let _ = print("inserted two items") +// let list = try! doc.latest() +// let _ = print("got \(list.count) items") +// +// let content_list = list.map { (entry) -> String in +// let content = String(decoding: try! doc.getContentBytes(entry: entry), as: UTF8.self) +// let _ = print("got content \(content)") +// let key = String(decoding: entry.key(), as: UTF8.self) +// let author_s = entry.author().toString() +// let _ = print("for key\(key)") +// let _ = print("and author \(author_s)") +// return "Author: \(author_s) wrote '\(key)' -> '\(content)'" +// } +// +// let list_print = content_list.joined(separator: ", ") +// +// let stats = try! state.stats() +// let stats_print = stats.map { "\($0.key): \($0.value)"}.joined(separator: "\n") +// let _ = print("Stats:\n\(stats_print)") +// */ +// +// VStack { +// Text("Hello \(peer_id)") +// TextField("Enter Doc Ticket", text: $ticketString) +// Button("Join Doc") { +// join() +// } +// Text("Current Document: \(contentList)") +// } +// .padding() +// } +// +// init() throws { +// _state = .init(wrappedValue: try IrohNode()) +// let _ = print("Starting..") +// +// let peer_id = state.peerId() +// let _ = print("created iroh node \(state) with peer id \(peer_id)") +// } +// +// func join() { +// print("joining \(ticketString)") +// let docTicket = try! DocTicket.fromString(content: ticketString) +// let doc = try! state.importDoc(ticket: docTicket) +// try! doc.subscribe(cb: CallbackHandler(view: self)) +// currentDoc = doc +// +// DispatchQueue.main.asyncAfter(deadline: .now() + 1) { +// try! loadContent() +// } +// } +// +// func loadContent() throws { +// if let doc = currentDoc { +// let content = try doc.latest() +// print("got \(content.count) elements for \(doc.id())") +// +// let formattedList = content.map { (entry) -> String in +// let content: String = { +// do { +// let c = try doc.getContentBytes(entry: entry) +// return String(decoding: c, as: UTF8.self) +// } catch { +// return "N/A" +// } +// }() +// let key = String(decoding: entry.key(), as: UTF8.self) +// let author_s = entry.author().toString() +// return "Author: \(author_s) wrote '\(key)' -> '\(content)'" +// }.joined(separator: "\n") +// contentList = "\(doc.id()) (\(content.count))\n\n\(formattedList)" +// } +// } +//} +// +//class CallbackHandler: SubscribeCallback { +// @State private var view: JoinDocSyncTest +// +// init(view: JoinDocSyncTest) { +// self.view = view +// } +// +// func event(event: LiveEvent) throws { +// let _ = print("got event \(event)") +// try! view.loadContent() +// } +//} +// +//struct JoinDocSyncTest_Previews: PreviewProvider { +// static var previews: some View { +// try! JoinDocSyncTest() +// } +//} +// +//extension StringProtocol { +// var data: Data { .init(utf8) } +// var bytes: [UInt8] { .init(utf8) } +//} diff --git a/iroh-console-ios/iroh/Views/Tests/NewDocSyncTest.swift b/iroh-console-ios/iroh/Views/Tests/NewDocSyncTest.swift new file mode 100644 index 0000000..512b056 --- /dev/null +++ b/iroh-console-ios/iroh/Views/Tests/NewDocSyncTest.swift @@ -0,0 +1,56 @@ +// +// NewDocSyncTest.swift +// iroh +// +// Created by Brendan O'Brien on 7/26/23. +// + +import SwiftUI + +struct NewDocSyncTest: View { + + var body: some View { + VStack(alignment: .leading, spacing: 10) { + Text("NEW NETWORK DOC SYNC TEST") + .font(Font.custom("Space Mono", size: 16).weight(.bold)) + .foregroundColor(.secondary) + + VStack(alignment: .leading, spacing: 0) { + Text("TEST NAME") + .font(Font.custom("Space Mono", size: 14)) + .lineSpacing(20) + .foregroundColor(Color(red: 0.21, green: 0.24, blue: 0.33)) + .frame(maxWidth: .infinity, alignment: .leading) + Text("Test One") + .font(Font.custom("Space Mono", size: 28).weight(.bold)) + .lineSpacing(36) + .foregroundColor(Color(red: 0.36, green: 0.42, blue: 0.58)) + .frame(maxWidth: .infinity, alignment: .leading) + } + .padding(EdgeInsets(top: 4, leading: 6, bottom: 4, trailing: 6)) + .frame(width: 350, height: 64) + .cornerRadius(2) + .overlay( + RoundedRectangle(cornerRadius: 2) + .inset(by: 0.50) + .stroke(Color(red: 0.21, green: 0.24, blue: 0.33), lineWidth: 0.50) + ) + Spacer() + NavigationLink(destination: TestLobby(), label: { + Text("CREATE TEST") + .font(Font.custom("Space Mono", size: 20)) + .foregroundColor(.white) + .padding(EdgeInsets(top: 6, leading: 10, bottom: 6, trailing: 10)) + .frame(width: 350, height: 60) + .background(Color(red: 0.42, green: 0.42, blue: 0.85)) + .cornerRadius(3); + }) + }.padding(20) + } +} + +struct NewDocSyncTest_Previews: PreviewProvider { + static var previews: some View { + NewDocSyncTest() + } +} diff --git a/iroh-console-ios/iroh/Views/Tests/TestHome.swift b/iroh-console-ios/iroh/Views/Tests/TestHome.swift new file mode 100644 index 0000000..87e30ae --- /dev/null +++ b/iroh-console-ios/iroh/Views/Tests/TestHome.swift @@ -0,0 +1,70 @@ +// +// TestHome.swift +// iroh +// +// Created by Brendan O'Brien on 7/26/23. +// + +import SwiftUI + +struct TestHome: View { + @EnvironmentObject var irohNodeManager: IrohNodeManager + @State private var path = NavigationPath() + @State private var showingTest = false + + var body: some View { + NavigationStack(path: $path) { + VStack(alignment: .leading, spacing: 10) { + Topbar() + VStack(alignment: .leading, spacing: 0) { + Text("Tests") + .font(Font.custom("Space Mono", size: 32)) + .lineSpacing(48) + .foregroundColor(.primary) + .frame(maxWidth: .infinity, alignment: .leading) + Text("default.iroh.network") + .font(Font.custom("Space Mono", size: 16)) + .lineSpacing(23) + .foregroundColor(.secondary) + .frame(maxWidth: .infinity, alignment: .leading) + } + .padding(EdgeInsets(top: 20, leading: 0, bottom: 0, trailing: 0)) + .frame(maxWidth: .infinity, minHeight: 91, maxHeight: 91) + + List { + Section("Documents") { + Button(action: { + showingTest = true + }, label: { + Text("New Doc Sync Test") + .font(Font.custom("Space Grotesk", size: 20).weight(.light)) + .foregroundColor(Color(red: 0.13, green: 0.15, blue: 0.20)) + }).popover(isPresented: $showingTest, content: { + NavigationView { + NewDocSyncTest() + } + }) + NavigationLink(destination: JoinDocSyncTest(), label: { + Text("Join Doc Sync Test") + .font(Font.custom("Space Grotesk", size: 20).weight(.light)) + .foregroundColor(Color(red: 0.13, green: 0.15, blue: 0.20)) + }) + } + } + Spacer() + } + .padding(20) + } + } +} + +struct TestHome_Previews: PreviewProvider { + static var previews: some View { + let inm = IrohNodeManager.shared + TestHome() + .environmentObject(inm) + .onAppear() { + inm.start() + } + } +} diff --git a/iroh-console-ios/iroh/Views/Tests/TestLobby.swift b/iroh-console-ios/iroh/Views/Tests/TestLobby.swift new file mode 100644 index 0000000..5ff3427 --- /dev/null +++ b/iroh-console-ios/iroh/Views/Tests/TestLobby.swift @@ -0,0 +1,109 @@ +// +// TestLobby.swift +// iroh +// +// Created by Brendan O'Brien on 7/26/23. +// + +import SwiftUI + +struct TestLobby: View { + @State private var peers: [String] = [] + @State private var currentIndex: Int = 0 + @State private var timer: Timer? = nil + + private let mockPeers = [ + "dig", + "ramfox", + "arqu", + "rklaehn", + "flub", + "divma", + "frando" + ] + + var body: some View { + VStack(alignment: .leading, spacing: 10) { + VStack { + Text("One") + .font(Font.custom("Space Mono", size: 32)) + .lineSpacing(48) + .foregroundColor(.primary) + .frame(maxWidth: .infinity, alignment: .leading) + Text("doc sync test") + .font(Font.custom("Space Mono", size: 16)) + .lineSpacing(23) + .foregroundColor(.secondary) + .frame(maxWidth: .infinity, alignment: .leading) + } + .padding(EdgeInsets(top: 20, leading: 20, bottom: 0, trailing: 20)) + + VStack { + ShareLink("Share Test Invite", item: URL(string: "https://iroh.network")!) + .font(Font.custom("Space Mono", size: 20).weight(.bold)) + .frame(maxWidth: .infinity, alignment: .leading) + } + .padding(EdgeInsets(top: 0, leading: 20, bottom: 0, trailing: 20)) + + List { + Section("Participants") { + ForEach(peers, id: \.self) { peer in + Text(peer) + } + } + } + Spacer() + + VStack { + NavigationLink(destination: DocSyncTest(), label: { + Text("START TEST") + .font(Font.custom("Space Mono", size: 20)) + .lineSpacing(48) + .foregroundColor(.white) + .padding(EdgeInsets(top: 6, leading: 10, bottom: 6, trailing: 10)) + .frame(width: 350, height: 60) + .background(Color(red: 0.42, green: 0.42, blue: 0.85)) + .cornerRadius(2); + }) + } + .padding(EdgeInsets(top: 0, leading: 30, bottom: 0, trailing: 30)) + } + .padding(EdgeInsets(top: 0, leading: 5, bottom: 20, trailing: 5)) + .onAppear { + DispatchQueue.main.asyncAfter(deadline: .now() + 5) { + startTimer() + } + } + .onDisappear { + stopTimer() + } + } + + private func startTimer() { + timer = Timer.scheduledTimer(withTimeInterval: randomInterval(), repeats: true) { _ in + if currentIndex < mockPeers.count { + let event = mockPeers[currentIndex] + peers.append(event) + currentIndex += 1 + } else { + stopTimer() + } + } + } + + private func stopTimer() { + timer?.invalidate() + timer = nil + } + + private func randomInterval() -> TimeInterval { + let randomSeconds = Double.random(in: 0.2...0.75) + return randomSeconds + } +} + +struct TestLobby_Previews: PreviewProvider { + static var previews: some View { + TestLobby() + } +} diff --git a/iroh-console-ios/iroh/Views/UserProfile.swift b/iroh-console-ios/iroh/Views/UserProfile.swift new file mode 100644 index 0000000..ffeb741 --- /dev/null +++ b/iroh-console-ios/iroh/Views/UserProfile.swift @@ -0,0 +1,26 @@ +// +// UserProfile.swift +// IrohConsole +// +// Created by Brendan O'Brien on 9/19/23. +// + +import SwiftUI + +struct UserProfile: View { + var user: IrohAnchorAPI.Types.Response.User + + var body: some View { + VStack(alignment: .leading, spacing: 5) { + Text("USERNAME") + .font(Font.caption) + .foregroundColor(.secondary) + Text(user.name) + + Text("USER_PUBLIC_KEY") + .font(Font.caption) + .foregroundColor(.secondary) + Text(user.pub_key) + } + } +} diff --git a/iroh-console-ios/iroh/irohApp.swift b/iroh-console-ios/iroh/irohApp.swift new file mode 100644 index 0000000..f7fd259 --- /dev/null +++ b/iroh-console-ios/iroh/irohApp.swift @@ -0,0 +1,23 @@ +// +// irohApp.swift +// iroh +// +// Created by Brendan O'Brien on 5/22/23. +// + +import SwiftUI + +@main +struct irohApp: App { + @StateObject private var irohNodeManager = IrohNodeManager.shared + + var body: some Scene { + WindowGroup { + ContentView() + .environmentObject(irohNodeManager) + .onAppear() { + irohNodeManager.start() + } + } + } +} diff --git a/iroh-console-ios/iroh/iroh_dot_network_api/IrohAnchorAPI.swift b/iroh-console-ios/iroh/iroh_dot_network_api/IrohAnchorAPI.swift new file mode 100644 index 0000000..2cf0458 --- /dev/null +++ b/iroh-console-ios/iroh/iroh_dot_network_api/IrohAnchorAPI.swift @@ -0,0 +1,11 @@ +// +// IrohNetworkAPI.swift +// IrohApp +// +// Created by Brendan O'Brien on 8/9/23. +// + +import Foundation + +// following advice from https://www.youtube.com/watch?v=xVj5xEEwTEk +enum IrohAnchorAPI {} diff --git a/iroh-console-ios/iroh/iroh_dot_network_api/IrohAnchorAPIClient.swift b/iroh-console-ios/iroh/iroh_dot_network_api/IrohAnchorAPIClient.swift new file mode 100644 index 0000000..836512e --- /dev/null +++ b/iroh-console-ios/iroh/iroh_dot_network_api/IrohAnchorAPIClient.swift @@ -0,0 +1,175 @@ +// +// IrohNetworkAPIClient.swift +// IrohApp +// +// Created by Brendan O'Brien on 8/9/23. +// + +import Foundation + +extension IrohAnchorAPI { + + class Client { + static let shared = Client() + private let encoder = JSONEncoder() + private let decoder = JSONDecoder() + + func fetch(_ endpoint: Types.Endpoint, + bearerToken: String = "", + method: Types.Method = .get, + body: Request? = nil, + then callback: ((Result) -> Void)? = nil + ) where Request: Encodable, Response: Decodable { + let url = endpoint.url() + print("\(method.rawValue): \(url)") + var urlRequest = URLRequest(url: url) + urlRequest.httpMethod = method.rawValue + urlRequest.setValue("application/json", forHTTPHeaderField: "Content-Type") + if bearerToken != "" { + urlRequest.setValue("Bearer: \(bearerToken)", forHTTPHeaderField: "Authorization") + } + if let body = body { + do { + urlRequest.httpBody = try self.encoder.encode(body) + } catch { + callback?(.failure(.internal(reason: "Could not encode body to JSON"))) + return + } + } + + // TODO(b5): return the dataTask so it can be cancelled + let dataTask = URLSession.shared + .dataTask(with: urlRequest) { data, response, error in + if let httpResponse = response as? HTTPURLResponse { + // Check if the request was successful (status code 200) + print("\(method.rawValue) \(url): \(httpResponse.statusCode)") + if httpResponse.statusCode == 401 { + callback?(.failure(.unauthorized)) + } + } + if let error = error { + print("fetch error: \(error)") + callback?(.failure(.generic(reason: "Could not fetch data from iroh.network API: \(error.localizedDescription)"))) + return + } else { + if let data = data { + do { +// let body = String(decoding: data, as: UTF8.self) +// print("BODY: \(body)") + let result = try self.decoder.decode(Response.self, from: data) + callback?(.success(result)) + } catch { + print("Decode error \(error) response body:") + print(String(decoding: data, as: UTF8.self)) + callback?(.failure(.generic(reason: "could not decode data: \(error.localizedDescription)"))) + } + } + } + } + dataTask.resume() + } + + func get(_ endpoint: Types.Endpoint, + then callback: ((Result) -> Void)? = nil + + ) where Response: Decodable { + let body: Types.Request.Empty? = nil + fetch(endpoint, body: body) { result in + callback?(result) + } + } + + func health( + then callback: ((Result) -> Void)? = nil + ) { + let body: Types.Request.Empty? = nil + fetch(.health, method: .get, body: body) { (result: Result) in + callback?(result) + } + } + func login( + _ username_or_email: String, + _ password: String, + then callback: ((Result) -> Void)? = nil) { + let body = IrohAnchorAPI.Types.Request.Login(name_or_email: username_or_email, password: password) + fetch(.login, method: .post, body: body) { (result: Result) in + callback?(result) + } + } + + func me( + _ sessionToken: String, + then callback: ((Result) -> Void)? = nil) { + let body: Types.Request.Empty? = nil + fetch(.me, bearerToken: sessionToken, body: body) { (result: Result) in + callback?(result) + } + } + + func myProjects( + _ sessionToken: String, + then callback: ((Result) -> Void)? = nil) { + let body: Types.Request.Empty? = nil + fetch(.myProjects, bearerToken: sessionToken, body: body) { (result: Result) in + callback?(result) + } + } + + func nodeStatus( + _ sessionToken: String, + then callback: ((Result) -> Void)? = nil) { + let body: Types.Request.Empty? = nil + fetch(.nodeStatus, bearerToken: sessionToken, body: body) { (result: Result) in + callback?(result) + } + } + + func egress( + _ username: String, + _ project: String, + _ sessionToken: String, + then callback: ((Result) -> Void)? = nil) { + let body: Types.Request.Empty? = nil + fetch(.egress(username: username, project: project), bearerToken: sessionToken, body: body) { (result: Result) in + callback?(result) + } + } + +// func teams( +// _ username: String, +// _ sessionToken: String, +// then callback: ((Result<[IrohAnchorAPI.Types.Response.AnchorInfo], IrohAnchorAPI.Types.Error>) -> Void)? = nil) { +// let body: Types.Request.Empty? = nil +// fetch(.teams(username: username), bearerToken: sessionToken, body: body) { (result: Result<[IrohAnchorAPI.Types.Response.AnchorInfo], IrohAnchorAPI.Types.Error>) in +// callback?(result) +// } +// } + + func docs( + _ username: String, + _ project: String, + _ sessionToken: String, + limit: Int = 100, + offset: Int = 0, + then callback: ((Result<[IrohAnchorAPI.Types.Response.DocumentInfo], IrohAnchorAPI.Types.Error>) -> Void)? = nil) { + let body: Types.Request.Empty? = nil + fetch(.docs(username: username, project: project, limit: limit, offset: offset), bearerToken: sessionToken, body: body) { (result: Result<[IrohAnchorAPI.Types.Response.DocumentInfo], IrohAnchorAPI.Types.Error>) in + callback?(result) + } + } + + func inviteToDocMutation( + _ username: String, + _ project: String, + _ sessionToken: String, + _ name: String, + _ ticket: String, + then callback: ((Result) -> Void)? = nil) { + let body = Types.Request.DocJoin(name: name, ticket: ticket) + fetch(.docJoin(username: username, project: project), bearerToken: sessionToken, method: .post, body: body) { (result: Result) in + callback?(result) + } + } + } + +} diff --git a/iroh-console-ios/iroh/iroh_dot_network_api/IrohAnchorAPITypes.swift b/iroh-console-ios/iroh/iroh_dot_network_api/IrohAnchorAPITypes.swift new file mode 100644 index 0000000..4c9b346 --- /dev/null +++ b/iroh-console-ios/iroh/iroh_dot_network_api/IrohAnchorAPITypes.swift @@ -0,0 +1,165 @@ +// +// IrohNetworkAPITypes.swift +// IrohApp +// +// Created by Brendan O'Brien on 8/9/23. +// + +import Foundation + +extension IrohAnchorAPI { + enum Types { + enum Request { + struct Empty: Encodable {} + + struct Login: Encodable { + var name_or_email: String + var password: String + } + + struct DocJoin: Encodable { + var name: String + var ticket: String + } + + } + + enum Response { + struct User: Decodable { + var user_id: String + var name: String + var user_role: String + var pub_key: String + } + + struct Session: Decodable { + var status: String + var token: String + } + + struct AnchorDialingInfo: Decodable { + var peer_id: String + var addr: String + var auth_token: String + } + + struct Project: Decodable, Hashable { + var project_id: String + var name: String + var owner_id: String + } + + struct UserProjects: Decodable { + static func == (lhs: IrohAnchorAPI.Types.Response.UserProjects, rhs: IrohAnchorAPI.Types.Response.UserProjects) -> Bool { + return lhs.user.user_id == rhs.user.user_id + } + + var user: IrohAnchorAPI.Types.Response.User + var projects: [IrohAnchorAPI.Types.Response.Project] + } + + struct DocumentInfo: Decodable, Hashable { + var doc_id: String + var name: String + } + + struct Egress: Decodable { + var summary: EgressSummary + var daily: [EgressDaily] + } + + struct EgressSummary: Decodable { + var egress_bytes: Int + var egress_blobs: Int + var requests: Int + } + + struct EgressDaily: Codable, Equatable { + var date: Date + var egress_bytes: Int + var egress_blobs: Int + var requests: Int + var nodeId: String? // TODO - this missing from iroh.network API, but used locally + + static func == (lhs: Self, rhs: Self) -> Bool { + return lhs.date == rhs.date && + lhs.egress_bytes == rhs.egress_bytes && + lhs.egress_blobs == rhs.egress_blobs && + lhs.requests == rhs.requests && + lhs.nodeId == rhs.nodeId + } + } + } + + enum Error: LocalizedError { + case generic(reason: String) + case unauthorized + case `internal`(reason: String) + + var errorDescription: String? { + switch self { + case .generic(let reason): + return reason + case .unauthorized: + return "unauthorized" + case .internal(let reason): + return "Internal Error: \(reason)" + } + } + } + + enum Endpoint { + case health + case nodeStatus + case login + case me + case myProjects +// case teams(username: String) + case docs(username: String, project: String, limit: Int, offset: Int) + case doc(username: String, project: String, docId: String) + case docJoin(username: String, project: String) + case blobs(username: String, project: String) + case egress(username: String, project: String) + + func url() -> URL { + var components = URLComponents() + components.host = "api.iroh.network" + components.scheme = "https" + + switch self { + case .health: + components.path = "/health" + case .nodeStatus: + components.path = "/node/status" + + case .login: + components.path = "/user/login" + case .me: + components.path = "/user/me" + case .myProjects: + components.path = "/user/me/projects" +// case .teams(let username): +// components.path = "/teams/\(username)" + case .docs(let username, let project, _, _): + components.path = "/docs/\(username)/\(project)" + case .doc(let username, let project, let docId): + components.path = "/docs/\(username)/\(project)/\(docId)" + case .docJoin(let username, let project): + components.path = "/docs/\(username)/\(project)/join" + + case .blobs(let username, let project): + components.path = "/blobs/\(username)/\(project)" + case .egress(let username, let project): + components.path = "/egress/\(username)/\(project)" + } + + return components.url! + } + } + + enum Method: String { + case get + case post + } + } +}