diff --git a/damus.xcodeproj/project.pbxproj b/damus.xcodeproj/project.pbxproj index dab074de..5a614828 100644 --- a/damus.xcodeproj/project.pbxproj +++ b/damus.xcodeproj/project.pbxproj @@ -531,6 +531,60 @@ 5C8498032D5D150000F74FEB /* ZapExplainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C8498012D5D14FA00F74FEB /* ZapExplainer.swift */; }; 5C8498042D5D150000F74FEB /* ZapExplainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C8498012D5D14FA00F74FEB /* ZapExplainer.swift */; }; 5C8711DE2C460C06007879C2 /* PostingTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C8711DD2C460C06007879C2 /* PostingTimelineView.swift */; }; + 5C8F970A2EB45E8C009399B1 /* LiveChatModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C8F97092EB45E85009399B1 /* LiveChatModel.swift */; }; + 5C8F970B2EB45E8C009399B1 /* LiveChatModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C8F97092EB45E85009399B1 /* LiveChatModel.swift */; }; + 5C8F970C2EB45E8C009399B1 /* LiveChatModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C8F97092EB45E85009399B1 /* LiveChatModel.swift */; }; + 5C8F970E2EB45F7C009399B1 /* LiveChatHomeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C8F970D2EB45F68009399B1 /* LiveChatHomeView.swift */; }; + 5C8F970F2EB45F7C009399B1 /* LiveChatHomeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C8F970D2EB45F68009399B1 /* LiveChatHomeView.swift */; }; + 5C8F97102EB45F7C009399B1 /* LiveChatHomeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C8F970D2EB45F68009399B1 /* LiveChatHomeView.swift */; }; + 5C8F97122EB45FAA009399B1 /* LiveChatTimeline.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C8F97112EB45FA3009399B1 /* LiveChatTimeline.swift */; }; + 5C8F97132EB45FAA009399B1 /* LiveChatTimeline.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C8F97112EB45FA3009399B1 /* LiveChatTimeline.swift */; }; + 5C8F97142EB45FAA009399B1 /* LiveChatTimeline.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C8F97112EB45FA3009399B1 /* LiveChatTimeline.swift */; }; + 5C8F97162EB45FD7009399B1 /* LiveChatView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C8F97152EB45FD1009399B1 /* LiveChatView.swift */; }; + 5C8F97172EB45FD7009399B1 /* LiveChatView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C8F97152EB45FD1009399B1 /* LiveChatView.swift */; }; + 5C8F97182EB45FD7009399B1 /* LiveChatView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C8F97152EB45FD1009399B1 /* LiveChatView.swift */; }; + 5C8F971C2EB4607B009399B1 /* LiveEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C8F971B2EB46078009399B1 /* LiveEvent.swift */; }; + 5C8F971D2EB4607B009399B1 /* LiveEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C8F971B2EB46078009399B1 /* LiveEvent.swift */; }; + 5C8F971E2EB4607B009399B1 /* LiveEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C8F971B2EB46078009399B1 /* LiveEvent.swift */; }; + 5C8F97202EB46097009399B1 /* LiveEventModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C8F971F2EB46093009399B1 /* LiveEventModel.swift */; }; + 5C8F97212EB46097009399B1 /* LiveEventModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C8F971F2EB46093009399B1 /* LiveEventModel.swift */; }; + 5C8F97222EB46097009399B1 /* LiveEventModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C8F971F2EB46093009399B1 /* LiveEventModel.swift */; }; + 5C8F97252EB460CA009399B1 /* LiveStreamBanner.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C8F97242EB460C2009399B1 /* LiveStreamBanner.swift */; }; + 5C8F97262EB460CA009399B1 /* LiveStreamBanner.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C8F97242EB460C2009399B1 /* LiveStreamBanner.swift */; }; + 5C8F97272EB460CA009399B1 /* LiveStreamBanner.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C8F97242EB460C2009399B1 /* LiveStreamBanner.swift */; }; + 5C8F97292EB460E6009399B1 /* LiveStreamProfile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C8F97282EB460DC009399B1 /* LiveStreamProfile.swift */; }; + 5C8F972A2EB460E6009399B1 /* LiveStreamProfile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C8F97282EB460DC009399B1 /* LiveStreamProfile.swift */; }; + 5C8F972B2EB460E6009399B1 /* LiveStreamProfile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C8F97282EB460DC009399B1 /* LiveStreamProfile.swift */; }; + 5C8F972D2EB46116009399B1 /* LiveStreamStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C8F972C2EB4610F009399B1 /* LiveStreamStatus.swift */; }; + 5C8F972E2EB46116009399B1 /* LiveStreamStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C8F972C2EB4610F009399B1 /* LiveStreamStatus.swift */; }; + 5C8F972F2EB46116009399B1 /* LiveStreamStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C8F972C2EB4610F009399B1 /* LiveStreamStatus.swift */; }; + 5C8F97312EB46126009399B1 /* LiveStreamViewers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C8F97302EB46121009399B1 /* LiveStreamViewers.swift */; }; + 5C8F97322EB46126009399B1 /* LiveStreamViewers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C8F97302EB46121009399B1 /* LiveStreamViewers.swift */; }; + 5C8F97332EB46126009399B1 /* LiveStreamViewers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C8F97302EB46121009399B1 /* LiveStreamViewers.swift */; }; + 5C8F97352EB46145009399B1 /* LiveStreamView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C8F97342EB46141009399B1 /* LiveStreamView.swift */; }; + 5C8F97362EB46145009399B1 /* LiveStreamView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C8F97342EB46141009399B1 /* LiveStreamView.swift */; }; + 5C8F97372EB46145009399B1 /* LiveStreamView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C8F97342EB46141009399B1 /* LiveStreamView.swift */; }; + 5C8F97392EB4616D009399B1 /* LiveStreamTimeline.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C8F97382EB46167009399B1 /* LiveStreamTimeline.swift */; }; + 5C8F973A2EB4616D009399B1 /* LiveStreamTimeline.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C8F97382EB46167009399B1 /* LiveStreamTimeline.swift */; }; + 5C8F973B2EB4616D009399B1 /* LiveStreamTimeline.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C8F97382EB46167009399B1 /* LiveStreamTimeline.swift */; }; + 5C8F973D2EB46197009399B1 /* LiveStreamPreview.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C8F973C2EB46192009399B1 /* LiveStreamPreview.swift */; }; + 5C8F973E2EB46197009399B1 /* LiveStreamPreview.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C8F973C2EB46192009399B1 /* LiveStreamPreview.swift */; }; + 5C8F973F2EB46197009399B1 /* LiveStreamPreview.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C8F973C2EB46192009399B1 /* LiveStreamPreview.swift */; }; + 5C8F97412EB461B2009399B1 /* LiveStreamHomeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C8F97402EB461AB009399B1 /* LiveStreamHomeView.swift */; }; + 5C8F97422EB461B2009399B1 /* LiveStreamHomeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C8F97402EB461AB009399B1 /* LiveStreamHomeView.swift */; }; + 5C8F97432EB461B2009399B1 /* LiveStreamHomeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C8F97402EB461AB009399B1 /* LiveStreamHomeView.swift */; }; + 5C8F97452EB461DB009399B1 /* EventTags.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C8F97442EB461D6009399B1 /* EventTags.swift */; }; + 5C8F97462EB461DB009399B1 /* EventTags.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C8F97442EB461D6009399B1 /* EventTags.swift */; }; + 5C8F97472EB461DB009399B1 /* EventTags.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C8F97442EB461D6009399B1 /* EventTags.swift */; }; + 5C8F97492EB4620A009399B1 /* Glow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C8F97482EB46208009399B1 /* Glow.swift */; }; + 5C8F974A2EB4620A009399B1 /* Glow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C8F97482EB46208009399B1 /* Glow.swift */; }; + 5C8F974B2EB4620A009399B1 /* Glow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C8F97482EB46208009399B1 /* Glow.swift */; }; + 5C8F974E2EBD704A009399B1 /* LabsToggleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C8F974D2EBD7044009399B1 /* LabsToggleView.swift */; }; + 5C8F974F2EBD704A009399B1 /* LabsToggleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C8F974D2EBD7044009399B1 /* LabsToggleView.swift */; }; + 5C8F97502EBD704A009399B1 /* LabsToggleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C8F974D2EBD7044009399B1 /* LabsToggleView.swift */; }; + 5C8F97522EBD7083009399B1 /* LabsExplainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C8F97512EBD7071009399B1 /* LabsExplainerView.swift */; }; + 5C8F97532EBD7083009399B1 /* LabsExplainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C8F97512EBD7071009399B1 /* LabsExplainerView.swift */; }; + 5C8F97542EBD7083009399B1 /* LabsExplainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C8F97512EBD7071009399B1 /* LabsExplainerView.swift */; }; 5CB017212D2D985E00A9ED05 /* CoinosButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CB017202D2D985800A9ED05 /* CoinosButton.swift */; }; 5CB017222D2D985E00A9ED05 /* CoinosButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CB017202D2D985800A9ED05 /* CoinosButton.swift */; }; 5CB017232D2D985E00A9ED05 /* CoinosButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CB017202D2D985800A9ED05 /* CoinosButton.swift */; }; @@ -552,9 +606,9 @@ 5CB645A12EA31E410018BD91 /* LabsLogoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CB645A02EA31E3D0018BD91 /* LabsLogoView.swift */; }; 5CB645A22EA31E410018BD91 /* LabsLogoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CB645A02EA31E3D0018BD91 /* LabsLogoView.swift */; }; 5CB645A32EA31E410018BD91 /* LabsLogoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CB645A02EA31E3D0018BD91 /* LabsLogoView.swift */; }; - 5CB645A92EAC01430018BD91 /* DamusLabsExpirements.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CB645A82EAC013A0018BD91 /* DamusLabsExpirements.swift */; }; - 5CB645AA2EAC01430018BD91 /* DamusLabsExpirements.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CB645A82EAC013A0018BD91 /* DamusLabsExpirements.swift */; }; - 5CB645AB2EAC01430018BD91 /* DamusLabsExpirements.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CB645A82EAC013A0018BD91 /* DamusLabsExpirements.swift */; }; + 5CB645A92EAC01430018BD91 /* DamusLabsExperiments.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CB645A82EAC013A0018BD91 /* DamusLabsExperiments.swift */; }; + 5CB645AA2EAC01430018BD91 /* DamusLabsExperiments.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CB645A82EAC013A0018BD91 /* DamusLabsExperiments.swift */; }; + 5CB645AB2EAC01430018BD91 /* DamusLabsExperiments.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CB645A82EAC013A0018BD91 /* DamusLabsExperiments.swift */; }; 5CC8529D2BD741CD0039FFC5 /* HighlightEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CC8529C2BD741CD0039FFC5 /* HighlightEvent.swift */; }; 5CC8529F2BD744F60039FFC5 /* HighlightView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CC8529E2BD744F60039FFC5 /* HighlightView.swift */; }; 5CC852A22BDED9B90039FFC5 /* HighlightDescription.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CC852A12BDED9B90039FFC5 /* HighlightDescription.swift */; }; @@ -2574,6 +2628,24 @@ 5C7389B02B6EFA7100781E0A /* ProxyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProxyView.swift; sourceTree = ""; }; 5C8498012D5D14FA00F74FEB /* ZapExplainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ZapExplainer.swift; sourceTree = ""; }; 5C8711DD2C460C06007879C2 /* PostingTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostingTimelineView.swift; sourceTree = ""; }; + 5C8F97092EB45E85009399B1 /* LiveChatModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveChatModel.swift; sourceTree = ""; }; + 5C8F970D2EB45F68009399B1 /* LiveChatHomeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveChatHomeView.swift; sourceTree = ""; }; + 5C8F97112EB45FA3009399B1 /* LiveChatTimeline.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveChatTimeline.swift; sourceTree = ""; }; + 5C8F97152EB45FD1009399B1 /* LiveChatView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveChatView.swift; sourceTree = ""; }; + 5C8F971B2EB46078009399B1 /* LiveEvent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveEvent.swift; sourceTree = ""; }; + 5C8F971F2EB46093009399B1 /* LiveEventModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveEventModel.swift; sourceTree = ""; }; + 5C8F97242EB460C2009399B1 /* LiveStreamBanner.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveStreamBanner.swift; sourceTree = ""; }; + 5C8F97282EB460DC009399B1 /* LiveStreamProfile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveStreamProfile.swift; sourceTree = ""; }; + 5C8F972C2EB4610F009399B1 /* LiveStreamStatus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveStreamStatus.swift; sourceTree = ""; }; + 5C8F97302EB46121009399B1 /* LiveStreamViewers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveStreamViewers.swift; sourceTree = ""; }; + 5C8F97342EB46141009399B1 /* LiveStreamView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveStreamView.swift; sourceTree = ""; }; + 5C8F97382EB46167009399B1 /* LiveStreamTimeline.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveStreamTimeline.swift; sourceTree = ""; }; + 5C8F973C2EB46192009399B1 /* LiveStreamPreview.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveStreamPreview.swift; sourceTree = ""; }; + 5C8F97402EB461AB009399B1 /* LiveStreamHomeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveStreamHomeView.swift; sourceTree = ""; }; + 5C8F97442EB461D6009399B1 /* EventTags.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventTags.swift; sourceTree = ""; }; + 5C8F97482EB46208009399B1 /* Glow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Glow.swift; sourceTree = ""; }; + 5C8F974D2EBD7044009399B1 /* LabsToggleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LabsToggleView.swift; sourceTree = ""; }; + 5C8F97512EBD7071009399B1 /* LabsExplainerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LabsExplainerView.swift; sourceTree = ""; }; 5CB017202D2D985800A9ED05 /* CoinosButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoinosButton.swift; sourceTree = ""; }; 5CB017242D42C5BD00A9ED05 /* TransactionsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransactionsView.swift; sourceTree = ""; }; 5CB0172C2D42C76600A9ED05 /* BalanceView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BalanceView.swift; sourceTree = ""; }; @@ -2581,7 +2653,7 @@ 5CB645972EA317CC0018BD91 /* DamusLabs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DamusLabs.swift; sourceTree = ""; }; 5CB6459B2EA31D750018BD91 /* LabsIntroduction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LabsIntroduction.swift; sourceTree = ""; }; 5CB645A02EA31E3D0018BD91 /* LabsLogoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LabsLogoView.swift; sourceTree = ""; }; - 5CB645A82EAC013A0018BD91 /* DamusLabsExpirements.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DamusLabsExpirements.swift; sourceTree = ""; }; + 5CB645A82EAC013A0018BD91 /* DamusLabsExperiments.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DamusLabsExperiments.swift; sourceTree = ""; }; 5CC8529C2BD741CD0039FFC5 /* HighlightEvent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HighlightEvent.swift; sourceTree = ""; }; 5CC8529E2BD744F60039FFC5 /* HighlightView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HighlightView.swift; sourceTree = ""; }; 5CC852A12BDED9B90039FFC5 /* HighlightDescription.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HighlightDescription.swift; sourceTree = ""; }; @@ -3663,6 +3735,7 @@ 5C513FCB2984ACA60072348F /* QRCodeView.swift */, 4C363A8B28236B92006E126D /* PubkeyView.swift */, 4C8D00CB29DF92DF0036AF10 /* Hashtags.swift */, + 5C8F97442EB461D6009399B1 /* EventTags.swift */, ); path = Components; sourceTree = ""; @@ -4049,6 +4122,7 @@ 5C78A7792E22FDFE00CF177D /* Features */ = { isa = PBXGroup; children = ( + 5C8F97042EB45E39009399B1 /* Live */, D5C1AFC22E5DFF040092F72F /* ContactCard */, 5C78A7BC2E304D7400CF177D /* Translations */, 5C78A7B52E3046F400CF177D /* NIP05 */, @@ -4884,12 +4958,100 @@ path = Views; sourceTree = ""; }; + 5C8F97042EB45E39009399B1 /* Live */ = { + isa = PBXGroup; + children = ( + 5C8F97062EB45E53009399B1 /* LiveStream */, + 5C8F97052EB45E4C009399B1 /* LiveChat */, + ); + path = Live; + sourceTree = ""; + }; + 5C8F97052EB45E4C009399B1 /* LiveChat */ = { + isa = PBXGroup; + children = ( + 5C8F97072EB45E5F009399B1 /* Models */, + 5C8F97082EB45E63009399B1 /* Views */, + ); + path = LiveChat; + sourceTree = ""; + }; + 5C8F97062EB45E53009399B1 /* LiveStream */ = { + isa = PBXGroup; + children = ( + 5C8F971A2EB4600C009399B1 /* Views */, + 5C8F97192EB46005009399B1 /* Models */, + ); + path = LiveStream; + sourceTree = ""; + }; + 5C8F97072EB45E5F009399B1 /* Models */ = { + isa = PBXGroup; + children = ( + 5C8F97092EB45E85009399B1 /* LiveChatModel.swift */, + ); + path = Models; + sourceTree = ""; + }; + 5C8F97082EB45E63009399B1 /* Views */ = { + isa = PBXGroup; + children = ( + 5C8F97152EB45FD1009399B1 /* LiveChatView.swift */, + 5C8F97112EB45FA3009399B1 /* LiveChatTimeline.swift */, + 5C8F970D2EB45F68009399B1 /* LiveChatHomeView.swift */, + ); + path = Views; + sourceTree = ""; + }; + 5C8F97192EB46005009399B1 /* Models */ = { + isa = PBXGroup; + children = ( + 5C8F971F2EB46093009399B1 /* LiveEventModel.swift */, + 5C8F971B2EB46078009399B1 /* LiveEvent.swift */, + ); + path = Models; + sourceTree = ""; + }; + 5C8F971A2EB4600C009399B1 /* Views */ = { + isa = PBXGroup; + children = ( + 5C8F97402EB461AB009399B1 /* LiveStreamHomeView.swift */, + 5C8F973C2EB46192009399B1 /* LiveStreamPreview.swift */, + 5C8F97382EB46167009399B1 /* LiveStreamTimeline.swift */, + 5C8F97342EB46141009399B1 /* LiveStreamView.swift */, + 5C8F97232EB460B8009399B1 /* Components */, + ); + path = Views; + sourceTree = ""; + }; + 5C8F97232EB460B8009399B1 /* Components */ = { + isa = PBXGroup; + children = ( + 5C8F97302EB46121009399B1 /* LiveStreamViewers.swift */, + 5C8F972C2EB4610F009399B1 /* LiveStreamStatus.swift */, + 5C8F97282EB460DC009399B1 /* LiveStreamProfile.swift */, + 5C8F97242EB460C2009399B1 /* LiveStreamBanner.swift */, + ); + path = Components; + sourceTree = ""; + }; + 5C8F974C2EBD703D009399B1 /* Components */ = { + isa = PBXGroup; + children = ( + 5C8F97512EBD7071009399B1 /* LabsExplainerView.swift */, + 5C8F974D2EBD7044009399B1 /* LabsToggleView.swift */, + ); + path = Components; + sourceTree = ""; + }; 5CB645952EA3106A0018BD91 /* Views */ = { isa = PBXGroup; children = ( - 5CB645A82EAC013A0018BD91 /* DamusLabsExpirements.swift */, + 5C8F974C2EBD703D009399B1 /* Components */, + 5CB645A82EAC013A0018BD91 /* DamusLabsExperiments.swift */, 5CB6459B2EA31D750018BD91 /* LabsIntroduction.swift */, 5CB645972EA317CC0018BD91 /* DamusLabs.swift */, + 5CB6459F2EA31E2C0018BD91 /* Detail */, ); path = Views; sourceTree = ""; @@ -4897,7 +5059,6 @@ 5CB645962EA3106A0018BD91 /* Labs */ = { isa = PBXGroup; children = ( - 5CB6459F2EA31E2C0018BD91 /* Detail */, 5CB645952EA3106A0018BD91 /* Views */, ); path = Labs; @@ -5144,6 +5305,7 @@ F7F0BA23297892AE009531F3 /* Modifiers */ = { isa = PBXGroup; children = ( + 5C8F97482EB46208009399B1 /* Glow.swift */, F7F0BA24297892BD009531F3 /* SwipeToDismiss.swift */, ); path = Modifiers; @@ -5519,9 +5681,11 @@ 4C3DCC762A9FE9EC0091E592 /* NdbTxn.swift in Sources */, 4CEF958D2A9CE650000F901B /* verifier.c in Sources */, D73BDB0E2D6FF5F600D69970 /* NostrNetworkManager.swift in Sources */, + 5C8F97262EB460CA009399B1 /* LiveStreamBanner.swift in Sources */, 4C32B9332A99845B00DC3548 /* Ndb.swift in Sources */, D7ADD3E22B538E3500F104C4 /* DamusPurpleVerifyNpubView.swift in Sources */, 4C4793082A993E8900489948 /* refmap.c in Sources */, + 5C8F972D2EB46116009399B1 /* LiveStreamStatus.swift in Sources */, 4C4793072A993E6200489948 /* emitter.c in Sources */, 4C4793062A993E5300489948 /* json_parser.c in Sources */, 4C4793052A993E3200489948 /* builder.c in Sources */, @@ -5539,6 +5703,7 @@ 4C285C8428385690008A31F1 /* CreateAccountView.swift in Sources */, 4CDD1AE22A6B3074001CD4DF /* NdbTagsIterator.swift in Sources */, 4C216F34286F5ACD00040376 /* DMView.swift in Sources */, + 5C8F97522EBD7083009399B1 /* LabsExplainerView.swift in Sources */, D7CB5D512B1174D100AD4105 /* FriendFilter.swift in Sources */, D74DEC8C2DA0A19B00E69FA6 /* Ndb+.swift in Sources */, D7CBD1D42B8D21DC00BFD889 /* DamusPurpleNotificationManagement.swift in Sources */, @@ -5558,6 +5723,7 @@ 4C363A8C28236B92006E126D /* PubkeyView.swift in Sources */, 4CDA128A29E9D10C0006FA5A /* SignalView.swift in Sources */, 4C12535C2A76CA540004F4B8 /* LoginNotify.swift in Sources */, + 5C8F97222EB46097009399B1 /* LiveEventModel.swift in Sources */, 4C5C7E68284ED36500A22DF5 /* SearchHomeModel.swift in Sources */, 4C54AA0C29A5543C003E4487 /* ZapGroup.swift in Sources */, 4C190F202A535FC200027FD5 /* CustomizeZapModel.swift in Sources */, @@ -5636,6 +5802,7 @@ 4CF0ABE32981BC7D00D66079 /* UserView.swift in Sources */, 4CE0E2AF29A2E82100DB4CA2 /* EventHolder.swift in Sources */, B51C1CEA2B55A60A00E312A9 /* AddMuteItemView.swift in Sources */, + 5C8F97332EB46126009399B1 /* LiveStreamViewers.swift in Sources */, 4C5D5C992A6AF8F80024563C /* NdbNote.swift in Sources */, 4CF0ABF029857E9200D66079 /* Bech32Object.swift in Sources */, D7D68FFA2C9E01BE0015A515 /* KFClickable.swift in Sources */, @@ -5663,6 +5830,7 @@ D5C1AFC62E5DFF700092F72F /* ContactCardManagerMock.swift in Sources */, 4C4DD3DB2A6CA7E8005B4E85 /* ContentParsing.swift in Sources */, F71694F22A67314D001F4053 /* SuggestedUserView.swift in Sources */, + 5C8F97172EB45FD7009399B1 /* LiveChatView.swift in Sources */, 4C9BB83429C12D9900FC4E37 /* EventProfileName.swift in Sources */, 4C7D09602A098C5D00943473 /* WalletView.swift in Sources */, 4CB8838F296F781C00DC99E7 /* ReactionsView.swift in Sources */, @@ -5755,6 +5923,7 @@ 7C95CAEE299DCEF1009DCB67 /* KFOptionSetter+.swift in Sources */, 4C7D09722A0AEF5E00943473 /* DamusGradient.swift in Sources */, 4C463CBF2B960B96008A8C36 /* PurpleBackdrop.swift in Sources */, + 5C8F97502EBD704A009399B1 /* LabsToggleView.swift in Sources */, BAB68BED29543FA3007BA466 /* SelectWalletView.swift in Sources */, 3169CAE6294E69C000EE4006 /* EmptyTimelineView.swift in Sources */, 4CF4803A2B631C0100F2B2C0 /* nostr_bech32.c in Sources */, @@ -5773,12 +5942,14 @@ 4C3A1D3729637E0500558C0F /* PreviewCache.swift in Sources */, D78F08142D7F78F900FC6C75 /* Response.swift in Sources */, 4C3AC7A12835A81400E1F516 /* SetupView.swift in Sources */, + 5C8F97372EB46145009399B1 /* LiveStreamView.swift in Sources */, 4C06670128FC7C5900038D2A /* RelayView.swift in Sources */, 4C285C8C28398BC7008A31F1 /* Keys.swift in Sources */, 5CC852A22BDED9B90039FFC5 /* HighlightDescription.swift in Sources */, 4C94D6432BA5AEFE00C26EFF /* QuoteRepostsView.swift in Sources */, D7EDED332B12ACAE0018B19C /* DamusUserDefaults.swift in Sources */, 4CA352AE2A76C1AC003BB08B /* FollowedNotify.swift in Sources */, + 5C8F97422EB461B2009399B1 /* LiveStreamHomeView.swift in Sources */, 4CACA9DC280C38C000D9BBE8 /* Profiles.swift in Sources */, 4CE879582996C45300F758CC /* ZapsView.swift in Sources */, 4C30AC7429A5680900E2BD5A /* EventGroupView.swift in Sources */, @@ -5829,11 +6000,13 @@ 4CA352A22A76AEC5003BB08B /* LikedNotify.swift in Sources */, 5CC8529F2BD744F60039FFC5 /* HighlightView.swift in Sources */, BA37598D2ABCCE500018D73B /* PhotoCaptureProcessor.swift in Sources */, + 5C8F97462EB461DB009399B1 /* EventTags.swift in Sources */, 3A515C562DF5371D002D3B34 /* TrustedNetworkButtonTipViewStyle.swift in Sources */, 5CC8529D2BD741CD0039FFC5 /* HighlightEvent.swift in Sources */, 4C75EFAF28049D350006080F /* NostrFilter.swift in Sources */, 4CF480422B631C0100F2B2C0 /* NdbProfile.swift in Sources */, 4CA9276C2A2910D10098A105 /* ReplyPart.swift in Sources */, + 5C8F97102EB45F7C009399B1 /* LiveChatHomeView.swift in Sources */, D7C6787E2B2D34CC00BCEAFB /* NIP98AuthenticatedRequest.swift in Sources */, 5CB017252D42C5C400A9ED05 /* TransactionsView.swift in Sources */, 4CE1399029F0661A00AC6A0B /* RepostAction.swift in Sources */, @@ -5860,11 +6033,13 @@ D73FA9E12DDC12AA00C706E1 /* OnboardingContentSettings.swift in Sources */, 5C42E78C29DB76D90086AAC1 /* EmptyUserSearchView.swift in Sources */, 4CB88396296F7F8B00DC99E7 /* ReactionView.swift in Sources */, + 5C8F97132EB45FAA009399B1 /* LiveChatTimeline.swift in Sources */, 50A16FFD2AA7525700DFEC1F /* DamusVideoPlayer.swift in Sources */, 4CF480552B631C4F00F2B2C0 /* wasm.c in Sources */, 50A16FFD2AA7525700DFEC1F /* DamusVideoPlayer.swift in Sources */, 4CFF8F6B29CD0079008DB934 /* RepostedEvent.swift in Sources */, D78CD5982B8990300014D539 /* DamusAppNotificationView.swift in Sources */, + 5C8F972B2EB460E6009399B1 /* LiveStreamProfile.swift in Sources */, D724D8272B64B40B00ABE789 /* DamusPurpleAccountView.swift in Sources */, 4C8682872814DE470026224F /* ProfileView.swift in Sources */, 5C0707D12A1ECB38004E7B51 /* DamusLogoGradient.swift in Sources */, @@ -5923,6 +6098,7 @@ 4C1253562A76C8C60004F4B8 /* BroadcastNotify.swift in Sources */, 4CF480392B631C0100F2B2C0 /* block.c in Sources */, 4C3BEFD42819DE8F00B3DE84 /* NostrKind.swift in Sources */, + 5C8F973F2EB46197009399B1 /* LiveStreamPreview.swift in Sources */, B533694E2B66D791008A805E /* MutelistManager.swift in Sources */, 4C32B9532A9AD44700DC3548 /* Verifier.swift in Sources */, 5C14C29D2BBBA40B00079FD2 /* RelayAdminDetail.swift in Sources */, @@ -5945,6 +6121,7 @@ E0EE9DD42B8E5FEA00F3002D /* ImageProcessing.swift in Sources */, 4CB883B0297705DD00DC99E7 /* NoteZapButton.swift in Sources */, D7DF58342DFCF18D00E9AD28 /* SendPaymentView.swift in Sources */, + 5C8F970A2EB45E8C009399B1 /* LiveChatModel.swift in Sources */, 4C363A922825FCF2006E126D /* ProfileUpdate.swift in Sources */, 4C3BEFDA281DCA1400B3DE84 /* LikeCounter.swift in Sources */, 4C32B9502A9AD44700DC3548 /* FlatBufferBuilder.swift in Sources */, @@ -5964,9 +6141,10 @@ 4C9BB83129C0ED4F00FC4E37 /* DisplayName.swift in Sources */, 7CFF6317299FEFE5005D382A /* SelectableText.swift in Sources */, D5C1AFD42E5EE2820092F72F /* FavoriteButtonView.swift in Sources */, - 5CB645AB2EAC01430018BD91 /* DamusLabsExpirements.swift in Sources */, + 5CB645AB2EAC01430018BD91 /* DamusLabsExperiments.swift in Sources */, D5C1AFCA2E5EE12B0092F72F /* ContactCardNotify.swift in Sources */, 4CA352A82A76B37E003BB08B /* NewMutesNotify.swift in Sources */, + 5C8F974A2EB4620A009399B1 /* Glow.swift in Sources */, 4CFF8F6929CC9ED1008DB934 /* ImageContainerView.swift in Sources */, 7527271E2A93FF0100214108 /* Block.swift in Sources */, 4C54AA0729A540BA003E4487 /* NotificationsModel.swift in Sources */, @@ -5995,9 +6173,11 @@ 4C1253622A76D00B0004F4B8 /* PostNotify.swift in Sources */, 4CACA9D5280C31E100D9BBE8 /* ReplyView.swift in Sources */, 5CB017232D2D985E00A9ED05 /* CoinosButton.swift in Sources */, + 5C8F973B2EB4616D009399B1 /* LiveStreamTimeline.swift in Sources */, F7908E92298B0F0700AB113A /* RelayDetailView.swift in Sources */, 4CE879552996BAB900F758CC /* RelayPaidDetail.swift in Sources */, 5CB0172F2D42C76A00A9ED05 /* BalanceView.swift in Sources */, + 5C8F971D2EB4607B009399B1 /* LiveEvent.swift in Sources */, 4C1253602A76CF890004F4B8 /* ScrollToTopNotify.swift in Sources */, 4CA3529E2A76AE67003BB08B /* FollowNotify.swift in Sources */, 4CF0ABD42980996B00D66079 /* Report.swift in Sources */, @@ -6132,7 +6312,7 @@ 5C4FA7FB2DC29C3800CE658C /* FollowPackView.swift in Sources */, 4C3624722D5EA18E00DD066E /* amount.c in Sources */, 4C3624712D5EA18300DD066E /* error.c in Sources */, - 5CB645A92EAC01430018BD91 /* DamusLabsExpirements.swift in Sources */, + 5CB645A92EAC01430018BD91 /* DamusLabsExperiments.swift in Sources */, 4C3624702D5EA17700DD066E /* utf8.c in Sources */, 4C36246F2D5EA16A00DD066E /* str.c in Sources */, 4C36246E2D5EA10400DD066E /* hash_u5.c in Sources */, @@ -6155,6 +6335,7 @@ 82D6FA9A2CD9820500C925F4 /* ShareViewController.swift in Sources */, 82D6FAA92CD99F7900C925F4 /* FbConstants.swift in Sources */, 82D6FAAA2CD99F7900C925F4 /* Offset.swift in Sources */, + 5C8F97322EB46126009399B1 /* LiveStreamViewers.swift in Sources */, 82D6FAAB2CD99F7900C925F4 /* Int+extension.swift in Sources */, 82D6FAAC2CD99F7900C925F4 /* FlatBufferBuilder.swift in Sources */, 82D6FAAD2CD99F7900C925F4 /* FlatbuffersErrors.swift in Sources */, @@ -6290,13 +6471,16 @@ 82D6FB2E2CD99F7900C925F4 /* BlurHashEncode.swift in Sources */, 82D6FB2F2CD99F7900C925F4 /* BlurHashDecode.swift in Sources */, 82D6FB302CD99F7900C925F4 /* PostBox.swift in Sources */, + 5C8F970B2EB45E8C009399B1 /* LiveChatModel.swift in Sources */, 82D6FB312CD99F7900C925F4 /* KFOptionSetter+.swift in Sources */, + 5C8F972F2EB46116009399B1 /* LiveStreamStatus.swift in Sources */, D73BDB162D71216500D69970 /* UserRelayListManager.swift in Sources */, 82D6FB322CD99F7900C925F4 /* FillAndStroke.swift in Sources */, 82D6FB332CD99F7900C925F4 /* Array.swift in Sources */, 82D6FB342CD99F7900C925F4 /* VectorMath.swift in Sources */, 5C8498022D5D150000F74FEB /* ZapExplainer.swift in Sources */, 82D6FB352CD99F7900C925F4 /* OffsetExtension.swift in Sources */, + 5C8F973A2EB4616D009399B1 /* LiveStreamTimeline.swift in Sources */, 82D6FB362CD99F7900C925F4 /* RelayFilters.swift in Sources */, 82D6FB372CD99F7900C925F4 /* RelayModelCache.swift in Sources */, 82D6FB382CD99F7900C925F4 /* RelayBootstrap.swift in Sources */, @@ -6343,6 +6527,7 @@ 82D6FB5F2CD99F7900C925F4 /* KeyboardVisible.swift in Sources */, 82D6FB602CD99F7900C925F4 /* StringUtil.swift in Sources */, D78F08172D7F7F7500FC6C75 /* NIP04.swift in Sources */, + 5C8F973E2EB46197009399B1 /* LiveStreamPreview.swift in Sources */, 82D6FB612CD99F7900C925F4 /* Router.swift in Sources */, 82D6FB622CD99F7900C925F4 /* Log.swift in Sources */, 82D6FB632CD99F7900C925F4 /* AVPlayer+Additions.swift in Sources */, @@ -6408,6 +6593,7 @@ 82D6FB9B2CD99F7900C925F4 /* MutedThreadsManager.swift in Sources */, 82D6FB9C2CD99F7900C925F4 /* WalletModel.swift in Sources */, 82D6FB9D2CD99F7900C925F4 /* ZapButtonModel.swift in Sources */, + 5C8F97432EB461B2009399B1 /* LiveStreamHomeView.swift in Sources */, 5C09FD142DF283D700823661 /* FollowPackModel.swift in Sources */, 82D6FB9E2CD99F7900C925F4 /* ContentFilters.swift in Sources */, 3A515C512DF4E100002D3B34 /* TrustedNetworkRepliesTip.swift in Sources */, @@ -6419,6 +6605,7 @@ 82D6FBA32CD99F7900C925F4 /* NewEventsBits.swift in Sources */, 82D6FBA42CD99F7900C925F4 /* FriendFilter.swift in Sources */, 82D6FBA52CD99F7900C925F4 /* MediaUploader.swift in Sources */, + 5C8F974F2EBD704A009399B1 /* LabsToggleView.swift in Sources */, 82D6FBA62CD99F7900C925F4 /* FollowState.swift in Sources */, 82D6FBA72CD99F7900C925F4 /* NoteContent.swift in Sources */, 82D6FBA82CD99F7900C925F4 /* LongformEvent.swift in Sources */, @@ -6470,6 +6657,7 @@ 82D6FBD62CD99F7900C925F4 /* WalletView.swift in Sources */, 82D6FBD72CD99F7900C925F4 /* NWCScannerView.swift in Sources */, 82D6FBD82CD99F7900C925F4 /* TrustedNetworkButton.swift in Sources */, + 5C8F971E2EB4607B009399B1 /* LiveEvent.swift in Sources */, 82D6FBD92CD99F7900C925F4 /* GradientFollowButton.swift in Sources */, 82D6FBDC2CD99F7900C925F4 /* DamusVideoPlayerView.swift in Sources */, 82D6FBDD2CD99F7900C925F4 /* DamusVideoPlayer.swift in Sources */, @@ -6481,6 +6669,7 @@ 82D6FBE32CD99F7900C925F4 /* KeySettingsView.swift in Sources */, 82D6FBE42CD99F7900C925F4 /* ZapSettingsView.swift in Sources */, 82D6FBE52CD99F7900C925F4 /* TranslationSettingsView.swift in Sources */, + 5C8F97472EB461DB009399B1 /* EventTags.swift in Sources */, 82D6FBE62CD99F7900C925F4 /* SearchSettingsView.swift in Sources */, 82D6FBE72CD99F7900C925F4 /* DeveloperSettingsView.swift in Sources */, 82D6FBE82CD99F7900C925F4 /* FirstAidSettingsView.swift in Sources */, @@ -6489,6 +6678,7 @@ D7F360272CEBBDC0009D34DA /* DamusVideoControlsView.swift in Sources */, 82D6FBEB2CD99F7900C925F4 /* ProfilePicImageView.swift in Sources */, 82D6FBEC2CD99F7900C925F4 /* ImageContainerView.swift in Sources */, + 5C8F97492EB4620A009399B1 /* Glow.swift in Sources */, 82D6FBED2CD99F7900C925F4 /* MediaView.swift in Sources */, 82D6FBEE2CD99F7900C925F4 /* PurpleViewPrimitives.swift in Sources */, 82D6FBEF2CD99F7900C925F4 /* MarketingContentView.swift in Sources */, @@ -6496,8 +6686,10 @@ 82D6FBF02CD99F7900C925F4 /* LogoView.swift in Sources */, 82D6FBF12CD99F7900C925F4 /* IAPProductStateView.swift in Sources */, D74DEC8B2DA0A19B00E69FA6 /* Ndb+.swift in Sources */, + 5C8F97252EB460CA009399B1 /* LiveStreamBanner.swift in Sources */, 82D6FBF22CD99F7900C925F4 /* PurpleBackdrop.swift in Sources */, 82D6FBF32CD99F7900C925F4 /* DamusPurpleView.swift in Sources */, + 5C8F972A2EB460E6009399B1 /* LiveStreamProfile.swift in Sources */, 82D6FBF42CD99F7900C925F4 /* DamusPurpleWelcomeView.swift in Sources */, 82D6FBF52CD99F7900C925F4 /* DamusPurpleTranslationSetupView.swift in Sources */, 82D6FBF62CD99F7900C925F4 /* DamusPurpleURLSheetView.swift in Sources */, @@ -6520,6 +6712,7 @@ 82D6FC062CD99F7900C925F4 /* ZapTypePicker.swift in Sources */, 82D6FC072CD99F7900C925F4 /* ZapUserView.swift in Sources */, 82D6FC082CD99F7900C925F4 /* ProfileZapLinkView.swift in Sources */, + 5C8F970E2EB45F7C009399B1 /* LiveChatHomeView.swift in Sources */, D71AD8FE2CEC176A002E2C3C /* AppAccessibilityIdentifiers.swift in Sources */, 82D6FC092CD99F7900C925F4 /* AboutView.swift in Sources */, D72C01332E78C10500AACB67 /* CondensedProfilePicturesViewModel.swift in Sources */, @@ -6568,8 +6761,10 @@ 82D6FC2E2CD99F7900C925F4 /* ReplyDescription.swift in Sources */, 82D6FC2F2CD99F7900C925F4 /* RelativeTime.swift in Sources */, 82D6FC302CD99F7900C925F4 /* ReplyPart.swift in Sources */, + 5C8F97532EBD7083009399B1 /* LabsExplainerView.swift in Sources */, 82D6FC312CD99F7900C925F4 /* ProxyView.swift in Sources */, 82D6FC322CD99F7900C925F4 /* SelectedEventView.swift in Sources */, + 5C8F97352EB46145009399B1 /* LiveStreamView.swift in Sources */, 82D6FC332CD99F7900C925F4 /* EventBody.swift in Sources */, D5C1AFC02E5DF7E60092F72F /* ContactCardManager.swift in Sources */, 82D6FC342CD99F7900C925F4 /* BuilderEventView.swift in Sources */, @@ -6585,6 +6780,7 @@ 82D6FC3D2CD99F7900C925F4 /* EventShell.swift in Sources */, 82D6FC3E2CD99F7900C925F4 /* MentionView.swift in Sources */, 82D6FC3F2CD99F7900C925F4 /* EventLoaderView.swift in Sources */, + 5C8F97122EB45FAA009399B1 /* LiveChatTimeline.swift in Sources */, 82D6FC402CD99F7900C925F4 /* RepostView.swift in Sources */, 82D6FC412CD99F7900C925F4 /* RepostedEvent.swift in Sources */, 82D6FC422CD99F7900C925F4 /* QuoteRepostsView.swift in Sources */, @@ -6609,6 +6805,7 @@ 82D6FC532CD99F7900C925F4 /* EmptyTimelineView.swift in Sources */, 82D6FC542CD99F7900C925F4 /* EmptyUserSearchView.swift in Sources */, D706C5B82D602A110027C627 /* QueueableNotify.swift in Sources */, + 5C8F97182EB45FD7009399B1 /* LiveChatView.swift in Sources */, 82D6FC552CD99F7900C925F4 /* EventView.swift in Sources */, 82D6FC562CD99F7900C925F4 /* EventDetailView.swift in Sources */, 82D6FC572CD99F7900C925F4 /* FollowButtonView.swift in Sources */, @@ -6625,6 +6822,7 @@ 82D6FC612CD99F7900C925F4 /* MainTabView.swift in Sources */, 82D6FC622CD99F7900C925F4 /* PubkeyView.swift in Sources */, D7F360252CEBBD7E009D34DA /* DamusFullScreenCover.swift in Sources */, + 5C8F97212EB46097009399B1 /* LiveEventModel.swift in Sources */, 82D6FC632CD99F7900C925F4 /* ReplyView.swift in Sources */, 82D6FC642CD99F7900C925F4 /* ParticipantsView.swift in Sources */, 82D6FC652CD99F7900C925F4 /* SaveKeysView.swift in Sources */, @@ -6660,6 +6858,7 @@ buildActionMask = 2147483647; files = ( 4C36247D2D5EA22300DD066E /* invoice.c in Sources */, + 5C8F974E2EBD704A009399B1 /* LabsToggleView.swift in Sources */, 4C36247C2D5EA21F00DD066E /* amount.c in Sources */, 4C36247B2D5EA21200DD066E /* hash_u5.c in Sources */, 4C36247A2D5EA20C00DD066E /* bech32.c in Sources */, @@ -6702,6 +6901,7 @@ D73E5E352C6A97F4007EB227 /* RelaysChangedNotify.swift in Sources */, D73E5E362C6A97F4007EB227 /* MuteThreadNotify.swift in Sources */, D73E5E372C6A97F4007EB227 /* ReconnectRelaysNotify.swift in Sources */, + 5C8F97202EB46097009399B1 /* LiveEventModel.swift in Sources */, D73E5E382C6A97F4007EB227 /* PurpleAccountUpdateNotify.swift in Sources */, D73E5E392C6A97F4007EB227 /* DamusDuration.swift in Sources */, D73E5E3A2C6A97F4007EB227 /* SwipeToDismiss.swift in Sources */, @@ -6745,6 +6945,7 @@ D73E5E622C6A97F4007EB227 /* BlurHashEncode.swift in Sources */, 5C09FD122DF283D700823661 /* FollowPackModel.swift in Sources */, D73E5E632C6A97F4007EB227 /* BlurHashDecode.swift in Sources */, + 5C8F97542EBD7083009399B1 /* LabsExplainerView.swift in Sources */, D74EC8522E1856B70091DC51 /* NonCopyableLinkedList.swift in Sources */, D73E5F952C6AA753007EB227 /* FullScreenCarouselView.swift in Sources */, D76BE18E2E0CF3DA004AD0C6 /* Interests.swift in Sources */, @@ -6783,6 +6984,7 @@ D73E5E7F2C6A97F4007EB227 /* LocalNotification.swift in Sources */, D73E5E802C6A97F4007EB227 /* CredentialHandler.swift in Sources */, D73E5E812C6A97F4007EB227 /* KeyboardVisible.swift in Sources */, + 5C8F971C2EB4607B009399B1 /* LiveEvent.swift in Sources */, D73E5E832C6A97F4007EB227 /* AVPlayer+Additions.swift in Sources */, 5C4FA7FC2DC29C3800CE658C /* FollowPackView.swift in Sources */, D73E5E842C6A97F4007EB227 /* Zaps+.swift in Sources */, @@ -6852,6 +7054,7 @@ D73E5EAD2C6A97F4007EB227 /* MutedThreadsManager.swift in Sources */, D73E5EAE2C6A97F4007EB227 /* WalletModel.swift in Sources */, D73E5EAF2C6A97F4007EB227 /* ZapButtonModel.swift in Sources */, + 5C8F97292EB460E6009399B1 /* LiveStreamProfile.swift in Sources */, D73E5EB02C6A97F4007EB227 /* ContentFilters.swift in Sources */, D73E5EB12C6A97F4007EB227 /* DamusCacheManager.swift in Sources */, D73E5EB22C6A97F4007EB227 /* NotificationsManager.swift in Sources */, @@ -6874,6 +7077,7 @@ D73E5EBE2C6A97F4007EB227 /* NostrLink.swift in Sources */, D73E5EBF2C6A97F4007EB227 /* WebSocket.swift in Sources */, D73E5F812C6AA07A007EB227 /* HighlighterExtensionAliases.swift in Sources */, + 5C8F97392EB4616D009399B1 /* LiveStreamTimeline.swift in Sources */, D73E5EC02C6A97F4007EB227 /* NostrEvent+.swift in Sources */, D73E5EC12C6A97F4007EB227 /* NIP98AuthenticatedRequest.swift in Sources */, D73E5EC22C6A97F4007EB227 /* NostrAuth.swift in Sources */, @@ -6899,6 +7103,7 @@ D73E5EDF2C6A97F4007EB227 /* KeySettingsView.swift in Sources */, 5C0567562C8B60E60073F23A /* OffsetExtension.swift in Sources */, D73E5EE02C6A97F4007EB227 /* ZapSettingsView.swift in Sources */, + 5C8F97142EB45FAA009399B1 /* LiveChatTimeline.swift in Sources */, D73E5F792C6A9C4C007EB227 /* HomeModel.swift in Sources */, D73E5EE12C6A97F4007EB227 /* TranslationSettingsView.swift in Sources */, D73E5EE22C6A97F4007EB227 /* SearchSettingsView.swift in Sources */, @@ -6921,6 +7126,7 @@ D73E5EF32C6A97F4007EB227 /* DamusPurpleVerifyNpubView.swift in Sources */, D73E5EF42C6A97F4007EB227 /* DamusPurpleAccountView.swift in Sources */, 5CB0172E2D42C76A00A9ED05 /* BalanceView.swift in Sources */, + 5C8F973D2EB46197009399B1 /* LiveStreamPreview.swift in Sources */, D73E5EF52C6A97F4007EB227 /* DamusPurpleNewUserOnboardingView.swift in Sources */, D73E5EF62C6A97F4007EB227 /* SearchingEventView.swift in Sources */, D73E5EF72C6A97F4007EB227 /* PullDownSearch.swift in Sources */, @@ -6929,6 +7135,7 @@ D73B74E32D8365BA0067BDBC /* ExtraFonts.swift in Sources */, D73E5EF92C6A97F4007EB227 /* EventGroupView.swift in Sources */, D73E5EFA2C6A97F4007EB227 /* NotificationItemView.swift in Sources */, + 5C8F97272EB460CA009399B1 /* LiveStreamBanner.swift in Sources */, D73E5EFB2C6A97F4007EB227 /* ProfilePicturesView.swift in Sources */, D73E5EFC2C6A97F4007EB227 /* DamusAppNotificationView.swift in Sources */, D73E5EFD2C6A97F4007EB227 /* InnerTimelineView.swift in Sources */, @@ -6996,6 +7203,7 @@ D73E5F312C6A97F4007EB227 /* EventMenu.swift in Sources */, D73E5F322C6A97F4007EB227 /* EventMutingContainerView.swift in Sources */, D73E5F332C6A97F4007EB227 /* ZapEvent.swift in Sources */, + 5C8F97362EB46145009399B1 /* LiveStreamView.swift in Sources */, D73E5F342C6A97F4007EB227 /* TextEvent.swift in Sources */, D73E5F352C6A97F4007EB227 /* WideEventView.swift in Sources */, D7D68FF92C9E01BE0015A515 /* KFClickable.swift in Sources */, @@ -7011,13 +7219,17 @@ D73E5F3D2C6A97F4007EB227 /* QuoteRepostsView.swift in Sources */, D73E5F3E2C6A97F4007EB227 /* ReactionView.swift in Sources */, D73E5F3F2C6A97F4007EB227 /* EventActionBar.swift in Sources */, + 5C8F97452EB461DB009399B1 /* EventTags.swift in Sources */, D73E5F402C6A97F5007EB227 /* EventDetailBar.swift in Sources */, + 5C8F972E2EB46116009399B1 /* LiveStreamStatus.swift in Sources */, D73E5F412C6A97F5007EB227 /* ShareAction.swift in Sources */, D73E5F422C6A97F5007EB227 /* RepostAction.swift in Sources */, D73E5F942C6AA74D007EB227 /* EULAView.swift in Sources */, D73E5F432C6A97F5007EB227 /* ShareActionButton.swift in Sources */, D73E5F442C6A97F5007EB227 /* BigButton.swift in Sources */, D73E5F8D2C6AA6D7007EB227 /* AddMuteItemView.swift in Sources */, + 5C8F970F2EB45F7C009399B1 /* LiveChatHomeView.swift in Sources */, + 5C8F97412EB461B2009399B1 /* LiveStreamHomeView.swift in Sources */, D73E5F452C6A97F5007EB227 /* AddRelayView.swift in Sources */, D73E5F472C6A97F5007EB227 /* BookmarksView.swift in Sources */, D73E5F482C6A97F5007EB227 /* CarouselView.swift in Sources */, @@ -7087,6 +7299,7 @@ D703D7762C670BCA00A400EA /* Verifier.swift in Sources */, D703D75A2C670A7900A400EA /* LNUrls.swift in Sources */, D703D74B2C6709C900A400EA /* NoteId.swift in Sources */, + 5C8F97162EB45FD7009399B1 /* LiveChatView.swift in Sources */, D703D7B52C67111C00A400EA /* CollectionExtension.swift in Sources */, D703D7722C670B8000A400EA /* FlatBufferBuilder.swift in Sources */, D703D7502C6709F500A400EA /* NdbTxn.swift in Sources */, @@ -7115,6 +7328,7 @@ D703D7692C670B2600A400EA /* Block.swift in Sources */, D703D77D2C670C0300A400EA /* FlatbuffersErrors.swift in Sources */, D703D7A62C670E5200A400EA /* builder.c in Sources */, + 5C8F974B2EB4620A009399B1 /* Glow.swift in Sources */, D703D78D2C670CAF00A400EA /* UpdateStatsNotify.swift in Sources */, D703D75C2C670A8400A400EA /* NdbNote.swift in Sources */, D703D7592C670A7300A400EA /* Profiles.swift in Sources */, @@ -7151,8 +7365,9 @@ D7E5B2D32EA0188200CF47AC /* StreamPipelineDiagnostics.swift in Sources */, D73E5E162C6A9619007EB227 /* PostView.swift in Sources */, D703D7872C670C7E00A400EA /* DamusPurpleEnvironment.swift in Sources */, + 5C8F970C2EB45E8C009399B1 /* LiveChatModel.swift in Sources */, D703D7892C670C8600A400EA /* DeepLPlan.swift in Sources */, - 5CB645AA2EAC01430018BD91 /* DamusLabsExpirements.swift in Sources */, + 5CB645AA2EAC01430018BD91 /* DamusLabsExperiments.swift in Sources */, D73E5E182C6A963D007EB227 /* AttachMediaUtility.swift in Sources */, D73E5F852C6AA628007EB227 /* LoadScript.swift in Sources */, D703D74E2C6709DA00A400EA /* Pubkey.swift in Sources */, @@ -7177,6 +7392,7 @@ D703D71E2C66E47100A400EA /* ActionViewController.swift in Sources */, D703D7472C67092700A400EA /* UserSettingsStore.swift in Sources */, D703D7852C670C6100A400EA /* Notify.swift in Sources */, + 5C8F97312EB46126009399B1 /* LiveStreamViewers.swift in Sources */, D703D7532C670A2600A400EA /* Wallet.swift in Sources */, D755B28F2D3E7D8800BBEEFA /* NIP37Draft.swift in Sources */, D703D75F2C670AA200A400EA /* NostrEvent.swift in Sources */, diff --git a/damus/Core/Nostr/NostrEvent.swift b/damus/Core/Nostr/NostrEvent.swift index 9ead4f6c..c8afb0b0 100644 --- a/damus/Core/Nostr/NostrEvent.swift +++ b/damus/Core/Nostr/NostrEvent.swift @@ -512,6 +512,15 @@ func make_like_event(keypair: FullKeypair, liked: NostrEvent, content: String = return NostrEvent(content: content, keypair: keypair.to_keypair(), kind: 7, tags: tags) } +func make_live_chat_event(keypair: FullKeypair, content: String, root: String, dtag: String, relayURL: RelayURL?) -> NostrEvent? { + //var tags = Array(boosted.referenced_pubkeys).map({ pk in pk.tag }) + var aTagBuilder = ["a", "30311:\(root):\(dtag)"] + + var tags: [[String]] = [aTagBuilder] + + return NostrEvent(content: content, keypair: keypair.to_keypair(), kind: 1311, tags: tags) +} + func generate_private_keypair(our_privkey: Privkey, id: NoteId, created_at: UInt32) -> FullKeypair? { let to_hash = our_privkey.hex() + id.hex() + String(created_at) guard let dat = to_hash.data(using: .utf8) else { diff --git a/damus/Core/Nostr/NostrKind.swift b/damus/Core/Nostr/NostrKind.swift index ffc16a39..0fff7b1c 100644 --- a/damus/Core/Nostr/NostrKind.swift +++ b/damus/Core/Nostr/NostrKind.swift @@ -18,6 +18,7 @@ enum NostrKind: UInt32, Codable { case boost = 6 case like = 7 case chat = 42 + case live_chat = 1311 case mute_list = 10000 case relay_list = 10002 case interest_list = 10015 @@ -30,6 +31,7 @@ enum NostrKind: UInt32, Codable { case nwc_request = 23194 case nwc_response = 23195 case http_auth = 27235 + case live = 30311 case status = 30315 case contact_card = 30_382 case follow_list = 39089 diff --git a/damus/Features/Events/Components/EventTop.swift b/damus/Features/Events/Components/EventTop.swift index bba3da04..85d26ae5 100644 --- a/damus/Features/Events/Components/EventTop.swift +++ b/damus/Features/Events/Components/EventTop.swift @@ -13,26 +13,32 @@ struct EventTop: View { let event: NostrEvent let pubkey: Pubkey let is_anon: Bool + let size: EventViewKind + let options: EventViewOptions - init(state: DamusState, event: NostrEvent, pubkey: Pubkey, is_anon: Bool) { + init(state: DamusState, event: NostrEvent, pubkey: Pubkey, is_anon: Bool, size: EventViewKind, options: EventViewOptions) { self.state = state self.event = event self.pubkey = pubkey self.is_anon = is_anon + self.size = size + self.options = options } func ProfileName(is_anon: Bool) -> some View { let pk = is_anon ? ANON_PUBKEY : self.pubkey - return EventProfileName(pubkey: pk, damus: state, size: .normal) + return EventProfileName(pubkey: pk, damus: state, size: size) } var body: some View { HStack(alignment: .center, spacing: 0) { ProfileName(is_anon: is_anon) TimeDot() - RelativeTime(time: state.events.get_cache_data(event.id).relative_time) + RelativeTime(time: state.events.get_cache_data(event.id).relative_time, size: size, font_size: state.settings.font_size) Spacer() - EventMenuContext(damus: state, event: event) + if !options.contains(.no_context_menu) { + EventMenuContext(damus: state, event: event) + } } .lineLimit(1) } @@ -40,6 +46,6 @@ struct EventTop: View { struct EventTop_Previews: PreviewProvider { static var previews: some View { - EventTop(state: test_damus_state, event: test_note, pubkey: test_note.pubkey, is_anon: false) + EventTop(state: test_damus_state, event: test_note, pubkey: test_note.pubkey, is_anon: false, size: .normal, options: []) } } diff --git a/damus/Features/Events/Components/RelativeTime.swift b/damus/Features/Events/Components/RelativeTime.swift index 802568eb..14fb2382 100644 --- a/damus/Features/Events/Components/RelativeTime.swift +++ b/damus/Features/Events/Components/RelativeTime.swift @@ -9,10 +9,12 @@ import SwiftUI struct RelativeTime: View { @ObservedObject var time: RelativeTimeModel + let size: EventViewKind + let font_size: Double var body: some View { Text(verbatim: "\(time.value)") - .font(.system(size: 16)) + .font(eventviewsize_to_font(size, font_size: font_size)) .foregroundColor(.gray) } } @@ -20,6 +22,6 @@ struct RelativeTime: View { struct RelativeTime_Previews: PreviewProvider { static var previews: some View { - RelativeTime(time: RelativeTimeModel()) + RelativeTime(time: RelativeTimeModel(), size: .normal, font_size: 1.0) } } diff --git a/damus/Features/Events/EventShell.swift b/damus/Features/Events/EventShell.swift index 76a880b8..526cee2a 100644 --- a/damus/Features/Events/EventShell.swift +++ b/damus/Features/Events/EventShell.swift @@ -63,9 +63,11 @@ struct EventShell: View { } VStack(alignment: .leading) { - EventTop(state: state, event: event, pubkey: pubkey, is_anon: is_anon) - - UserStatusView(status: state.profiles.profile_data(pubkey).status, show_general: state.settings.show_general_statuses, show_music: state.settings.show_music_statuses) + EventTop(state: state, event: event, pubkey: pubkey, is_anon: is_anon, size: options.contains(.small_text) ? .small : .normal, options: options) + + if !options.contains(.no_status) { + UserStatusView(status: state.profiles.profile_data(pubkey).status, show_general: state.settings.show_general_statuses, show_music: state.settings.show_music_statuses) + } if !options.contains(.no_replying_to) { ReplyPart(events: state.events, event: event, keypair: state.keypair, ndb: state.ndb) @@ -93,7 +95,7 @@ struct EventShell: View { Pfp(is_anon: is_anon) VStack(alignment: .leading, spacing: 2) { - EventTop(state: state, event: event, pubkey: pubkey, is_anon: is_anon) + EventTop(state: state, event: event, pubkey: pubkey, is_anon: is_anon, size: options.contains(.small_text) ? .small : .normal, options: options) UserStatusView(status: state.profiles.profile_data(pubkey).status, show_general: state.settings.show_general_statuses, show_music: state.settings.show_music_statuses) ReplyPart(events: state.events, event: event, keypair: state.keypair, ndb: state.ndb) ProxyView(event: event) diff --git a/damus/Features/Events/Models/LoadableNostrEventView.swift b/damus/Features/Events/Models/LoadableNostrEventView.swift index f252cf19..8ef25041 100644 --- a/damus/Features/Events/Models/LoadableNostrEventView.swift +++ b/damus/Features/Events/Models/LoadableNostrEventView.swift @@ -74,7 +74,7 @@ class LoadableNostrEventViewModel: ObservableObject { case .zap, .zap_request: guard let zap = await get_zap(from: ev, state: damus_state) else { return .not_found } return .loaded(route: Route.Zaps(target: zap.target)) - case .contacts, .metadata, .delete, .boost, .chat, .mute_list, .list_deprecated, .draft, .longform, .nwc_request, .nwc_response, .http_auth, .status, .relay_list, .follow_list, .interest_list, .contact_card: + case .contacts, .metadata, .delete, .boost, .chat, .mute_list, .list_deprecated, .draft, .longform, .nwc_request, .nwc_response, .http_auth, .status, .relay_list, .follow_list, .interest_list, .contact_card, .live, .live_chat: return .unknown_or_unsupported_kind } case .naddr(let naddr): diff --git a/damus/Features/Events/TextEvent.swift b/damus/Features/Events/TextEvent.swift index 09fa4502..5398e856 100644 --- a/damus/Features/Events/TextEvent.swift +++ b/damus/Features/Events/TextEvent.swift @@ -23,9 +23,13 @@ struct EventViewOptions: OptionSet { static let truncate_content_very_short = EventViewOptions(rawValue: 1 << 11) static let no_previews = EventViewOptions(rawValue: 1 << 12) static let no_show_more = EventViewOptions(rawValue: 1 << 13) + static let small_text = EventViewOptions(rawValue: 1 << 14) + static let no_status = EventViewOptions(rawValue: 1 << 15) + static let no_context_menu = EventViewOptions(rawValue: 1 << 16) static let embedded: EventViewOptions = [.no_action_bar, .small_pfp, .wide, .truncate_content, .nested] static let embedded_text_only: EventViewOptions = [.no_action_bar, .small_pfp, .wide, .truncate_content, .nested, .no_media, .truncate_content_very_short, .no_previews] + static let live_chat: EventViewOptions = [.no_action_bar, .small_pfp, .wide, .truncate_content, .no_previews, .nested, .small_text, .no_status, .no_context_menu] } struct TextEvent: View { @@ -51,6 +55,16 @@ struct TextEvent: View { func EvBody(options: EventViewOptions) -> some View { let blur_imgs = should_blur_images(settings: damus.settings, contacts: damus.contacts, ev: event, our_pubkey: damus.pubkey) + + if options.contains(.small_text) { + return NoteContentView( + damus_state: damus, + event: event, + blur_images: blur_imgs, + size: .small, + options: options) + } + return NoteContentView( damus_state: damus, event: event, diff --git a/damus/Features/Labs/Views/Components/LabsExplainerView.swift b/damus/Features/Labs/Views/Components/LabsExplainerView.swift new file mode 100644 index 00000000..64017a3a --- /dev/null +++ b/damus/Features/Labs/Views/Components/LabsExplainerView.swift @@ -0,0 +1,43 @@ +// +// LabsExplainerView.swift +// damus +// +// Created by eric on 11/6/25. +// + +import SwiftUI + +struct LabsExplainerView: View { + let labName: String + let systemImage: String + let labDescription: String + + var body: some View { + PurpleBackdrop { + VStack(alignment: .center) { + HStack { + Image(systemName: systemImage) + .resizable() + .foregroundColor(.white) + .frame(width: 25, height: 25) + Text(labName) + .font(.title) + .fontWeight(.bold) + .foregroundColor(.white) + } + + Spacer() + + Text(NSLocalizedString(labDescription, comment: "Description of the feature.")) + .foregroundColor(.white) + .multilineTextAlignment(.center) + + Spacer() + } + .padding() + } + .overlay(Rectangle().frame(width: nil, height: 1, alignment: .top).foregroundColor(DamusColors.purple), alignment: .top) + .presentationDragIndicator(.visible) + .presentationDetents([.height(300)]) + } +} diff --git a/damus/Features/Labs/Views/Components/LabsToggleView.swift b/damus/Features/Labs/Views/Components/LabsToggleView.swift new file mode 100644 index 00000000..ee66debb --- /dev/null +++ b/damus/Features/Labs/Views/Components/LabsToggleView.swift @@ -0,0 +1,40 @@ +// +// LabsToggleView.swift +// damus +// +// Created by eric on 11/6/25. +// + +import SwiftUI + +struct LabsToggleView: View { + let toggleName: String + let systemImage: String + @Binding var isOn: Bool + @Binding var showInfo: Bool + + var body: some View { + HStack { + HStack { + Toggle(toggleName, systemImage: systemImage, isOn: $isOn) + .toggleStyle(SwitchToggleStyle(tint: .accentColor)) + .font(.title2) + .foregroundColor(.white) + .fontWeight(.bold) + } + .padding(15) + .background(DamusColors.black) + .cornerRadius(20) + .overlay( + RoundedRectangle(cornerRadius: 20) + .stroke(isOn ? DamusColors.purple : DamusColors.neutral6, lineWidth: 2) + ) + + Image("info") + .foregroundColor(DamusColors.purple) + .onTapGesture { + showInfo.toggle() + } + } + } +} diff --git a/damus/Features/Labs/Views/DamusLabs.swift b/damus/Features/Labs/Views/DamusLabs.swift index 1249bf69..3406b1fd 100644 --- a/damus/Features/Labs/Views/DamusLabs.swift +++ b/damus/Features/Labs/Views/DamusLabs.swift @@ -27,7 +27,6 @@ struct DamusLabsView: View { PurpleBackdrop { VStack { MainContent - .padding(.top, 125) } } .navigationBarHidden(true) @@ -51,15 +50,13 @@ struct DamusLabsView: View { var MainContent: some View { VStack { - LabsLogoView() - if let purple_account, purple_account.active == true { - DamusLabsExpirements(damus_state: damus_state) + DamusLabsExperiments(damus_state: damus_state, settings: damus_state.settings) } else { + LabsLogoView() + .padding(.top, 125) LabsIntroductionView(damus_state: damus_state) } - - Spacer() } } } diff --git a/damus/Features/Labs/Views/DamusLabsExperiments.swift b/damus/Features/Labs/Views/DamusLabsExperiments.swift new file mode 100644 index 00000000..e6439a80 --- /dev/null +++ b/damus/Features/Labs/Views/DamusLabsExperiments.swift @@ -0,0 +1,67 @@ +// +// DamusLabsExpirements.swift +// damus +// +// Created by eric on 10/24/25. +// + +import SwiftUI + +struct DamusLabsExperiments: View { + + let damus_state: DamusState + @ObservedObject var settings: UserSettingsStore + @State var show_live_explainer: Bool = false + + var body: some View { + ScrollView { + + LabsLogoView() + + VStack(alignment: .leading, spacing: 30) { + PurpleViewPrimitives.SubtitleView(text: NSLocalizedString("As a subscriber, you’re getting an early look at new and innovative tools. These are beta features — still being tested and tuned. Try them out, share your thoughts, and help us perfect what’s next.", comment: "Damus Labs explainer")) + .multilineTextAlignment(.center) + .fixedSize(horizontal: false, vertical: true) + + + HStack { + Spacer() + Text(NSLocalizedString("More features coming soon!", comment: "")) + .font(.title2) + .foregroundColor(.white) + .fontWeight(.bold) + .padding(.bottom, 2) + Spacer() + } + .padding(15) + .background(DamusColors.black) + .cornerRadius(15) + .padding(.top, 10) + + LabsToggleView(toggleName: "Live", systemImage: "record.circle", isOn: $settings.live, showInfo: $show_live_explainer) + + } + .padding([.trailing, .leading], 20) + .padding(.bottom, 50) + + Image("damooseLabs") + .resizable() + .aspectRatio(contentMode: .fill) + + } + .ignoresSafeArea(edges: .bottom) + .sheet(isPresented: $show_live_explainer) { + LabsExplainerView( + labName: "Live", + systemImage: "record.circle", + labDescription: "This will allow you to see all the real-time live streams happening on Nostr! As well as let you view and interact in the Live Chat. Please keep in mind this is still a work in progress and issues are expected. When enabled you will see the Live option in your side menu.") + } + + } +} + +#Preview { + PurpleBackdrop { + DamusLabsExperiments(damus_state: test_damus_state, settings: test_damus_state.settings) + } +} diff --git a/damus/Features/Labs/Views/DamusLabsExpirements.swift b/damus/Features/Labs/Views/DamusLabsExpirements.swift deleted file mode 100644 index 97f40b01..00000000 --- a/damus/Features/Labs/Views/DamusLabsExpirements.swift +++ /dev/null @@ -1,52 +0,0 @@ -// -// DamusLabsExpirements.swift -// damus -// -// Created by eric on 10/24/25. -// - -import SwiftUI - - -struct DamusLabsExpirements: View { - - let damus_state: DamusState - - var body: some View { - VStack { - VStack(alignment: .leading, spacing: 30) { - PurpleViewPrimitives.SubtitleView(text: NSLocalizedString("As a subscriber, you’re getting an early look at new and innovative tools. These are beta features — still being tested and tuned. Try them out, share your thoughts, and help us perfect what’s next.", comment: "Damus Labs explainer")) - .multilineTextAlignment(.center) - - - HStack { - Spacer() - Text("Features coming soon!") - .font(.title2) - .foregroundColor(.white) - .fontWeight(.bold) - .padding(.bottom, 2) - Spacer() - } - .padding(15) - .background(DamusColors.neutral6) - .cornerRadius(15) - .padding(.top, 10) - - } - .padding([.trailing, .leading], 30) - .padding(.bottom, 20) - - Image("damooseLabs") - .resizable() - .aspectRatio(contentMode: .fill) - } - } -} - - -#Preview { - PurpleBackdrop { - DamusLabsExpirements(damus_state: test_damus_state) - } -} diff --git a/damus/Features/Labs/Detail/LabsLogoView.swift b/damus/Features/Labs/Views/Detail/LabsLogoView.swift similarity index 100% rename from damus/Features/Labs/Detail/LabsLogoView.swift rename to damus/Features/Labs/Views/Detail/LabsLogoView.swift diff --git a/damus/Features/Live/LiveChat/Models/LiveChatModel.swift b/damus/Features/Live/LiveChat/Models/LiveChatModel.swift new file mode 100644 index 00000000..1a680905 --- /dev/null +++ b/damus/Features/Live/LiveChat/Models/LiveChatModel.swift @@ -0,0 +1,99 @@ +// +// LiveChatModel.swift +// damus +// +// Created by eric on 8/7/25. +// + +import Foundation + +/// The data model for the LiveEventHome view +class LiveChatModel: ObservableObject { + var events: EventHolder + @Published var loading: Bool = false + + let damus_state: DamusState + let root: String + let dtag: String + var subscriptionTask: Task? = nil + let limit: UInt32 = 1000 + + init(damus_state: DamusState, root: String, dtag: String) { + self.damus_state = damus_state + self.root = root + self.dtag = dtag + self.events = EventHolder(on_queue: { ev in + preload_events(state: damus_state, events: [ev]) + }) + } + + @MainActor + func filter_muted() { + events.filter { should_show_event(state: damus_state, ev: $0) } + self.objectWillChange.send() + } + + @MainActor + func set(loading: Bool) { + self.loading = loading + } + + func subscribe() { + subscriptionTask?.cancel() + + subscriptionTask = Task { + await set(loading: true) + + let live_chat_filter = NostrFilter(kinds: [.live_chat]) + + let to_relays = await damus_state.nostrNetwork.ourRelayDescriptors + .map { $0.url } + .filter { !damus_state.relay_filters.is_filtered(timeline: .search, relay_id: $0) } + + for await item in damus_state.nostrNetwork.reader.advancedStream(filters: [live_chat_filter], to: to_relays) { + switch item { + case .event(let lender): + await lender.justUseACopy({ await handle_event(event: $0) }) + case .eose: + continue + case .ndbEose: + await set(loading: false) + case .networkEose: + continue + } + } + } + } + + @MainActor + func unsubscribe(to: RelayURL? = nil) { + set(loading: false) + subscriptionTask?.cancel() + } + + func handle_event(event: NostrEvent) async { + for tag in event.tags { + guard tag.count >= 2 else { continue } + switch tag[0].string() { + case "a": + let atag = tag[1].string() + let split = atag.split(separator: ":") + if root != split[1] { + return + } + if dtag != split[2] { + return + } + default: + break + } + } + await MainActor.run { + if should_show_event(state: damus_state, ev: event) { + if self.events.insert(event) { + self.objectWillChange.send() + } + } + } + } +} diff --git a/damus/Features/Live/LiveChat/Views/LiveChatHomeView.swift b/damus/Features/Live/LiveChat/Views/LiveChatHomeView.swift new file mode 100644 index 00000000..aab28fe6 --- /dev/null +++ b/damus/Features/Live/LiveChat/Views/LiveChatHomeView.swift @@ -0,0 +1,136 @@ +// +// LiveChatHomeView.swift +// damus +// +// Created by eric on 8/7/25. +// + +import SwiftUI + +struct LiveChatHomeView: View, KeyboardReadable { + let state: DamusState + let event: LiveEvent + @StateObject var model: LiveChatModel + @State private var chat_message = "" + @FocusState private var isTextFieldFocused: Bool + @Environment(\.colorScheme) var colorScheme + + func content_filter(_ fstate: FilterState) -> ((NostrEvent) -> Bool) { + var filters = ContentFilters.defaults(damus_state: state) + filters.append(fstate.filter) + return ContentFilters(filters: filters).filter + } + + var Footer: some View { + HStack(spacing: 0) { + ChatInput + + Button( + role: .none, + action: { + Task { await send_chat() } + } + ) { + Label("", image: "send") + .font(.title) + } + .disabled(chat_message.isEmpty) + } + .safeAreaInset(edge: .bottom) { + Color.clear.frame(height: 10) + } + } + + + func send_chat() async { + guard + let keypair = state.keypair.to_full(), + let liveChat = make_live_chat_event(keypair: keypair, content: chat_message, root: event.event.pubkey.hex(), dtag: event.uuid ?? "", relayURL: nil) + else { + return + } + await state.nostrNetwork.postbox.send(liveChat) + chat_message = "" + end_editing() + } + + var ChatInput: some View { + HStack{ + TextField(NSLocalizedString("Chat", comment: "Placeholder text to prompt entry of chat message."), text: $chat_message) + .autocorrectionDisabled(true) + .textInputAutocapitalization(.never) + .focused($isTextFieldFocused) + } + .padding(10) + .background(.secondary.opacity(0.2)) + .cornerRadius(20) + .padding(.horizontal, 15) + } + + func scroll_to_end(_ scroller: ScrollViewProxy, animated: Bool = false) { + if animated { + withAnimation { + scroller.scrollTo("endblock") + } + } else { + scroller.scrollTo("endblock") + } + } + + var Chat: some View { + ScrollViewReader { scroller in + ScrollView { + LazyVStack(alignment: .leading, spacing: 0) { + let events = model.events.events + ForEach(Array(zip(events, events.indices).reversed()).filter { should_show_event(state: state, ev: $0.0)}, id: \.0.id) { (ev, ind) in + TextEvent(damus: state, event: ev, pubkey: ev.pubkey, options: .live_chat) + } + EndBlock(height: 1) + } + } + .dismissKeyboardOnTap() + .onAppear { + scroll_to_end(scroller) + }.onChange(of: model.events.events.count) { _ in + scroll_to_end(scroller, animated: true) + } + .padding(.top, 5) + + Footer + .onReceive(keyboardPublisher) { visible in + guard visible else { + return + } + DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { + scroll_to_end(scroller, animated: true) + } + } + } + } + + var body: some View { + VStack(alignment: .leading, spacing: 0) { + HStack { + Text("Live Chat") + .fontWeight(.bold) + .padding(5) + + LiveStreamViewers(state: state, currentParticipants: event.currentParticipants ?? 0, preview: false) + } + + Divider() + + Chat + } + .onReceive(handle_notify(.new_mutes)) { _ in + self.model.filter_muted() + } + .onAppear { + model.subscribe() + } + .onDisappear { + model.unsubscribe() + } + + } +} diff --git a/damus/Features/Live/LiveChat/Views/LiveChatTimeline.swift b/damus/Features/Live/LiveChat/Views/LiveChatTimeline.swift new file mode 100644 index 00000000..3ed580a2 --- /dev/null +++ b/damus/Features/Live/LiveChat/Views/LiveChatTimeline.swift @@ -0,0 +1,136 @@ +// +// LiveChatTimeline.swift +// damus +// +// Created by eric on 8/7/25. +// + +import SwiftUI + +struct LiveChatTimelineView: View { + @ObservedObject var events: EventHolder + @Binding var loading: Bool + + let damus: DamusState + let show_friend_icon: Bool + let filter: (NostrEvent) -> Bool + let content: Content? + let apply_mute_rules: Bool + + init(events: EventHolder, loading: Binding, headerHeight: Binding, headerOffset: Binding, damus: DamusState, show_friend_icon: Bool, filter: @escaping (NostrEvent) -> Bool, apply_mute_rules: Bool = true, content: (() -> Content)? = nil) { + self.events = events + self._loading = loading + self.damus = damus + self.show_friend_icon = show_friend_icon + self.filter = filter + self.apply_mute_rules = apply_mute_rules + self.content = content?() + } + + init(events: EventHolder, loading: Binding, damus: DamusState, show_friend_icon: Bool, filter: @escaping (NostrEvent) -> Bool, apply_mute_rules: Bool = true, content: (() -> Content)? = nil) { + self.events = events + self._loading = loading + self.damus = damus + self.show_friend_icon = show_friend_icon + self.filter = filter + self.apply_mute_rules = apply_mute_rules + self.content = content?() + } + + func scroll_to_end(_ scroller: ScrollViewProxy, animated: Bool = false) { + if animated { + withAnimation { + scroller.scrollTo("endblock") + } + } else { + scroller.scrollTo("endblock") + } + } + + var body: some View { + ScrollViewReader { scroller in + ScrollView { + if let content { + content + } + + Color.clear + .id("startblock") + .frame(height: 0) + + LiveChatInnerView(events: events, damus: damus, filter: loading ? { _ in true } : filter, apply_mute_rules: self.apply_mute_rules) + .redacted(reason: loading ? .placeholder : []) + .shimmer(loading) + .disabled(loading) + .background { + GeometryReader { proxy -> Color in + handle_scroll_queue(proxy, queue: self.events) + return Color.clear + } + } + } + .coordinateSpace(name: "scroll") + .onReceive(handle_notify(.scroll_to_top)) { () in + events.flush() + self.events.set_should_queue(false) + scroll_to_event(scroller: scroller, id: "startblock", delay: 0.0, animate: true, anchor: .top) + } + } + .onAppear { + events.flush() + } + } +} + +struct LiveChatInnerView: View { + @ObservedObject var events: EventHolder + let state: DamusState + let filter: (NostrEvent) -> Bool + + init(events: EventHolder, damus: DamusState, filter: @escaping (NostrEvent) -> Bool, apply_mute_rules: Bool = true) { + self.events = events + self.state = damus + self.filter = apply_mute_rules ? { filter($0) && !damus.mutelist_manager.is_event_muted($0) } : filter + } + + var event_options: EventViewOptions { + if self.state.settings.truncate_timeline_text { + return [.wide, .truncate_content] + } + + return [.wide] + } + + var body: some View { + LazyVStack(spacing: 0) { + let events = self.events.events + if events.isEmpty { + EmptyTimelineView() + } else { + let evs = events.filter(filter) + let indexed = Array(zip(evs, 0...)) + ForEach(indexed, id: \.0.id) { tup in + let ev = tup.0 + let ind = tup.1 + if ev.kind == NostrKind.live_chat.rawValue { + LiveChatView(state: state, ev: ev) + .padding(.top, 7) + .onAppear { + let to_preload = + Array([indexed[safe: ind+1]?.0, + indexed[safe: ind+2]?.0, + indexed[safe: ind+3]?.0, + indexed[safe: ind+4]?.0, + indexed[safe: ind+5]?.0 + ].compactMap({ $0 })) + + preload_events(state: state, events: to_preload) + } + } + } + } + } + .padding(.bottom) + + } +} diff --git a/damus/Features/Live/LiveChat/Views/LiveChatView.swift b/damus/Features/Live/LiveChat/Views/LiveChatView.swift new file mode 100644 index 00000000..c7ce3b9c --- /dev/null +++ b/damus/Features/Live/LiveChat/Views/LiveChatView.swift @@ -0,0 +1,38 @@ +// +// LiveChatView.swift +// damus +// +// Created by eric on 8/7/25. +// + +import SwiftUI +import Kingfisher + +struct LiveChatView: View { + let state: DamusState + let event: NostrEvent + + @Environment(\.colorScheme) var colorScheme + + @ObservedObject var artifacts: NoteArtifactsModel + + init(state: DamusState, ev: NostrEvent) { + self.state = state + self.event = ev + + self._artifacts = ObservedObject(wrappedValue: state.events.get_cache_data(ev.id).artifacts_model) + } + + func content_filter(_ pubkeys: [Pubkey]) -> ((NostrEvent) -> Bool) { + var filters = ContentFilters.defaults(damus_state: self.state) + filters.append({ pubkeys.contains($0.pubkey) }) + return ContentFilters(filters: filters).filter + } + + var body: some View { + VStack(alignment: .leading) { + TextEvent(damus: state, event: event, pubkey: event.pubkey, options: [.no_action_bar,.small_pfp,.wide,.no_previews,.small_text]) + } + .padding(.bottom, 1) + } +} diff --git a/damus/Features/Live/LiveStream/Models/LiveEvent.swift b/damus/Features/Live/LiveStream/Models/LiveEvent.swift new file mode 100644 index 00000000..a8c914cd --- /dev/null +++ b/damus/Features/Live/LiveStream/Models/LiveEvent.swift @@ -0,0 +1,71 @@ +// +// LiveEvent.swift +// damus +// +// Created by eric on 7/10/25. +// + +import Foundation + +enum LiveEventStatus: String { + case planned = "SCHEDULED" + case live = "LIVE" + case ended = "ENDED" +} + +struct LiveEvent: Hashable { + let event: NostrEvent + var uuid: String? = nil + var title: String? = nil + var summary: String? = nil + var image: URL? = nil + var streaming: URL? = nil + var recording: URL? = nil + var starts: String? = nil + var ends: String? = nil + var status: LiveEventStatus? = nil + var currentParticipants: Int? = nil + var totalParticipants: Int? = nil + var pinned: String? = nil + var hashtags: [String]? = nil + var publicKeys: [Pubkey] = [] + + static func parse(from ev: NostrEvent) -> LiveEvent { + var liveEvent = LiveEvent(event: ev) + + for tag in ev.tags { + guard tag.count >= 2 else { continue } + switch tag[0].string() { + case "title": liveEvent.title = tag[1].string() + case "d": liveEvent.uuid = tag[1].string() + case "image": liveEvent.image = URL(string: tag[1].string()) + case "summary": liveEvent.summary = tag[1].string() + case "streaming": liveEvent.streaming = URL(string: tag[1].string()) + case "recording": liveEvent.recording = URL(string: tag[1].string()) + case "starts": liveEvent.starts = tag[1].string() + case "ends": liveEvent.ends = tag[1].string() + case "status": + if tag[1].string() == "planned" { + liveEvent.status = .planned + } else if tag[1].string() == "live" { + liveEvent.status = .live + } else if tag[1].string() == "ended" { + liveEvent.status = .ended + } + case "current_participants": liveEvent.currentParticipants = Int(tag[1].string()) + case "total_participants": liveEvent.totalParticipants = Int(tag[1].string()) + case "pinned": liveEvent.pinned = tag[1].string() + case "t": + if (liveEvent.hashtags?.append(tag[1].string())) == nil { + liveEvent.hashtags = [tag[1].string()] + } + case "p": + liveEvent.publicKeys.append(Pubkey(Data(hex: tag[1].string()))) + default: + break + } + } + + return liveEvent + } +} diff --git a/damus/Features/Live/LiveStream/Models/LiveEventModel.swift b/damus/Features/Live/LiveStream/Models/LiveEventModel.swift new file mode 100644 index 00000000..ce62bb05 --- /dev/null +++ b/damus/Features/Live/LiveStream/Models/LiveEventModel.swift @@ -0,0 +1,97 @@ +// +// LiveEventModel.swift +// damus +// +// Created by eric on 7/25/25. +// + +import Foundation + +/// The data model for the LiveEventHome view +class LiveEventModel: ObservableObject { + var events: EventHolder + @Published var loading: Bool = false + + let damus_state: DamusState + var subscriptionTask: Task? = nil + var seen_dtag: Set = Set() + + init(damus_state: DamusState) { + self.damus_state = damus_state + self.events = EventHolder(on_queue: { ev in + preload_events(state: damus_state, events: [ev]) + }) + } + + @MainActor + func filter_muted() { + events.filter { should_show_event(state: damus_state, ev: $0) } + self.objectWillChange.send() + } + + /// Helper function to set the `loading` member in the correct actor + @MainActor + private func set(loading: Bool) { + self.loading = loading + } + + func subscribe() { + subscriptionTask?.cancel() + + subscriptionTask = Task { + await self.set(loading: true) + + var live_event_filter = NostrFilter(kinds: [.live]) + live_event_filter.until = UInt32(Date.now.timeIntervalSince1970) + let calendar = Calendar.current + let twoWeeksAgo = calendar.date(byAdding: .day, value: -14, to: Date())! + live_event_filter.since = UInt32(twoWeeksAgo.timeIntervalSince1970) + + let to_relays = await damus_state.nostrNetwork.ourRelayDescriptors + .map { $0.url } + .filter { !damus_state.relay_filters.is_filtered(timeline: .search, relay_id: $0) } + + for await item in damus_state.nostrNetwork.reader.advancedStream(filters: [live_event_filter], to: to_relays) { + switch item { + case .event(let lender): + await lender.justUseACopy({ await handle_event(ev: $0) }) + case .eose: + continue + case .ndbEose: + await self.set(loading: false) + case .networkEose: + continue + } + } + } + } + + @MainActor + func unsubscribe() { + self.set(loading: false) + subscriptionTask?.cancel() + } + + func handle_event(ev: NostrEvent) async { + let should_show_event = await should_show_event(state: damus_state, ev: ev) + if ev.is_textlike && should_show_event && !ev.is_reply() + { + for tag in ev.tags { + guard tag.count >= 2 else { continue } + if tag[0].string() == "d" { + if seen_dtag.contains(tag[1].string()) { + return + } else { + seen_dtag.insert(tag[1].string()) + } + } + } + + await MainActor.run { + if self.events.insert(ev) { + self.objectWillChange.send() + } + } + } + } +} diff --git a/damus/Features/Live/LiveStream/Views/Components/LiveStreamBanner.swift b/damus/Features/Live/LiveStream/Views/Components/LiveStreamBanner.swift new file mode 100644 index 00000000..654092ed --- /dev/null +++ b/damus/Features/Live/LiveStream/Views/Components/LiveStreamBanner.swift @@ -0,0 +1,65 @@ +// +// LiveStreamBanner.swift +// damus +// +// Created by eric on 8/8/25. +// + +import SwiftUI +import Kingfisher + +struct LiveStreamBanner: View { + let state: DamusState + let options: EventViewOptions + var image: URL? = nil + var preview: Bool + + func Placeholder(url: URL, preview: Bool) -> some View { + Group { + if let meta = state.events.lookup_img_metadata(url: url), + case .processed(let blurhash) = meta.state { + Image(uiImage: blurhash) + .resizable() + .frame(minWidth: UIScreen.main.bounds.width, minHeight: preview ? 200 : 200, maxHeight: preview ? 200 : 200) + } else { + DamusColors.adaptableWhite + } + } + } + + func titleImage(url: URL, preview: Bool) -> some View { + KFAnimatedImage(url) + .callbackQueue(.dispatch(.global(qos:.background))) + .backgroundDecode(true) + .imageContext(.note, disable_animation: state.settings.disable_animation) + .image_fade(duration: 0.25) + .cancelOnDisappear(true) + .configure { view in + view.framePreloadCount = 3 + } + .background { + Placeholder(url: url, preview: preview) + } + .aspectRatio(contentMode: .fill) + .frame(minWidth: UIScreen.main.bounds.width, minHeight: preview ? 200 : 200, maxHeight: preview ? 200 : 200) + .kfClickable() + .cornerRadius(1) + } + + var body: some View { + if let url = image { + if (self.options.contains(.no_media)) { + EmptyView() + } else { + titleImage(url: url, preview: preview) + } + } else { + Text(NSLocalizedString("No cover image", comment: "Text letting user know there is no cover image.")) + .bold() + .foregroundColor(.white) + .frame(width: UIScreen.main.bounds.width, height: 200) + .background(DamusGradient.gradient.opacity(0.75)) + Divider() + } + } +} diff --git a/damus/Features/Live/LiveStream/Views/Components/LiveStreamProfile.swift b/damus/Features/Live/LiveStream/Views/Components/LiveStreamProfile.swift new file mode 100644 index 00000000..e9ee421b --- /dev/null +++ b/damus/Features/Live/LiveStream/Views/Components/LiveStreamProfile.swift @@ -0,0 +1,41 @@ +// +// LiveStreamProfile.swift +// damus +// +// Created by eric on 8/8/25. +// + +import SwiftUI + +struct LiveStreamProfile: View { + var state: DamusState + var pubkey: Pubkey + var size: CGFloat = 25 + + var body: some View { + HStack { + ProfilePicView(pubkey: pubkey, size: size, highlight: .custom(DamusColors.neutral3, 1.0), profiles: state.profiles, disable_animation: state.settings.disable_animation, show_zappability: true, damusState: state) + .onTapGesture { + state.nav.push(route: Route.ProfileByKey(pubkey: pubkey)) + } + let profile_txn = state.profiles.lookup(id: pubkey) + let profile = profile_txn?.unsafeUnownedValue + let displayName = Profile.displayName(profile: profile, pubkey: pubkey) + switch displayName { + case .one(let one): + Text(one) + .font(.subheadline).foregroundColor(.gray) + + case .both(username: let username, displayName: let displayName): + HStack(spacing: 6) { + Text(verbatim: displayName) + .font(.subheadline).foregroundColor(.gray) + + Text(verbatim: "@\(username)") + .font(.subheadline).foregroundColor(.gray) + } + } + } + .padding(5) + } +} diff --git a/damus/Features/Live/LiveStream/Views/Components/LiveStreamStatus.swift b/damus/Features/Live/LiveStream/Views/Components/LiveStreamStatus.swift new file mode 100644 index 00000000..e599a74d --- /dev/null +++ b/damus/Features/Live/LiveStream/Views/Components/LiveStreamStatus.swift @@ -0,0 +1,51 @@ +// +// LiveStreamStatus.swift +// damus +// +// Created by eric on 8/8/25. +// + +import SwiftUI + +struct LiveStreamStatus: View { + let status: LiveEventStatus + let starts: String? + + var body: some View { + HStack { + switch status { + case .planned: + Image("calendar") + .foregroundColor(Color.white) + + if let starts = starts { + Text("\(starts)") + .foregroundColor(Color.white) + .bold() + .glow() + } else { + Text("\(status.rawValue)") + .foregroundColor(Color.white) + .bold() + } + case .live: + Image("record") + .foregroundColor(Color.red) + .glow() + + Text("\(status.rawValue)") + .foregroundColor(DamusColors.adaptableWhite) + .bold() + case .ended: + Text("\(status.rawValue)") + .foregroundColor(DamusColors.adaptableWhite) + .bold() + } + } + .padding(.vertical, 2) + .padding(.horizontal, 7) + .background(DamusColors.adaptableBlack.opacity(0.5)) + .cornerRadius(10) + .padding(10) + } +} diff --git a/damus/Features/Live/LiveStream/Views/Components/LiveStreamViewers.swift b/damus/Features/Live/LiveStream/Views/Components/LiveStreamViewers.swift new file mode 100644 index 00000000..55581a82 --- /dev/null +++ b/damus/Features/Live/LiveStream/Views/Components/LiveStreamViewers.swift @@ -0,0 +1,36 @@ +// +// LiveStreamViewers.swift +// damus +// +// Created by eric on 8/8/25. +// + +import SwiftUI + +struct LiveStreamViewers: View { + let state: DamusState + var currentParticipants: Int + var preview: Bool + + var body: some View { + HStack(alignment: .center) { + let viewerCount = currentParticipants + let nounString = pluralizedString(key: "viewer_count", count: viewerCount) + let nounText = Text(verbatim: nounString).font(.subheadline).foregroundColor(DamusColors.adaptableWhite) + + if preview { + Text("\(Text(verbatim: viewerCount.formatted()).font(.subheadline.weight(.medium))) \(nounText)", comment: "Sentence composed of 2 variables to describe how many people are viewing the live event. In source English, the first variable is the number of viewers, and the second variable is 'viewer' or 'viewers'.") + .foregroundColor(DamusColors.adaptableWhite) + } else { + Image("user") + .resizable() + .frame(width: 15, height: 15) + Text("\(Text(verbatim: viewerCount.formatted()).font(.subheadline.weight(.medium)))", comment: "number") + } + } + .padding(.vertical, preview ? 2 : 0) + .padding(.horizontal, preview ? 7 : 0) + .background(preview ? DamusColors.adaptableBlack.opacity(0.5) : .clear) + .cornerRadius(preview ? 10 : 0) + } +} diff --git a/damus/Features/Live/LiveStream/Views/LiveStreamHomeView.swift b/damus/Features/Live/LiveStream/Views/LiveStreamHomeView.swift new file mode 100644 index 00000000..4222d684 --- /dev/null +++ b/damus/Features/Live/LiveStream/Views/LiveStreamHomeView.swift @@ -0,0 +1,50 @@ +// +// LiveStreamHomeView.swift +// damus +// +// Created by eric on 7/25/25. +// + +import SwiftUI +import CryptoKit +import NaturalLanguage + +struct LiveStreamHomeView: View { + let damus_state: DamusState + @StateObject var model: LiveEventModel + @Environment(\.colorScheme) var colorScheme + + func content_filter(_ fstate: FilterState) -> ((NostrEvent) -> Bool) { + var filters = ContentFilters.defaults(damus_state: damus_state) + filters.append(fstate.filter) + return ContentFilters(filters: filters).filter + } + + var body: some View { + VStack { + LiveStreamTimelineView(events: model.events, loading: $model.loading, damus: damus_state, filter:content_filter(FilterState.live)) + } + .padding(.bottom) + .refreshable { + // Fetch new information by unsubscribing and resubscribing to the relay + model.unsubscribe() + model.subscribe() + } + .onReceive(handle_notify(.new_mutes)) { _ in + self.model.filter_muted() + } + .onAppear { + model.subscribe() + } + .onDisappear { + model.unsubscribe() + } + } +} + +struct LiveStreamHomeView_Previews: PreviewProvider { + static var previews: some View { + let state = test_damus_state + LiveStreamHomeView(damus_state: state, model: LiveEventModel(damus_state: state)) + } +} diff --git a/damus/Features/Live/LiveStream/Views/LiveStreamPreview.swift b/damus/Features/Live/LiveStream/Views/LiveStreamPreview.swift new file mode 100644 index 00000000..6cbaf753 --- /dev/null +++ b/damus/Features/Live/LiveStream/Views/LiveStreamPreview.swift @@ -0,0 +1,127 @@ +// +// LiveStreamPreview.swift +// damus +// +// Created by eric on 7/10/25. +// + +import SwiftUI +import Kingfisher + +struct LiveStreamPreviewBody: View { + let state: DamusState + let event: LiveEvent + let options: EventViewOptions + let header: Bool + + @ObservedObject var artifacts: NoteArtifactsModel + + init(state: DamusState, ev: LiveEvent, options: EventViewOptions, header: Bool) { + self.state = state + self.event = ev + self.options = options + self.header = header + + self._artifacts = ObservedObject(wrappedValue: state.events.get_cache_data(ev.event.id).artifacts_model) + } + + init(state: DamusState, ev: NostrEvent, options: EventViewOptions, header: Bool) { + self.state = state + self.event = LiveEvent.parse(from: ev) + self.options = options + self.header = header + + self._artifacts = ObservedObject(wrappedValue: state.events.get_cache_data(ev.id).artifacts_model) + } + + var body: some View { + if event.status == .live { + if let streamingURL = event.streaming { + if streamingURL.absoluteString.hasSuffix(".m3u8") { + Group { + if options.contains(.wide) { + Main.padding(.horizontal) + } else { + Main + } + } + } + } + } + } + + var Main: some View { + VStack(alignment: .leading, spacing: 0) { + + ZStack(alignment: .topLeading) { + if state.settings.media_previews { + LiveStreamBanner(state: state, options: options, image: event.image, preview: true) + } + VStack { + if let status = event.status { + LiveStreamStatus(status: status, starts: event.starts) + } + + Spacer() + + LiveStreamViewers(state: state, currentParticipants: event.currentParticipants ?? 0, preview: true) + .padding(10) + } + } + .frame(minWidth: UIScreen.main.bounds.width, minHeight: 200, maxHeight: 200) + + VStack(alignment: .leading) { + LiveStreamProfile(state: state, pubkey: event.event.pubkey) + + Text(event.title ?? NSLocalizedString("Untitled", comment: "Title of follow list event if it is untitled.")) + .font(header ? .title : .headline) + .padding(.horizontal, 10) + + EventTags(tags: event.hashtags) + } + } + } +} + +struct LiveStreamPreview: View { + let state: DamusState + let event: LiveEvent + let options: EventViewOptions + + init(state: DamusState, ev: NostrEvent, options: EventViewOptions) { + self.state = state + self.event = LiveEvent.parse(from: ev) + self.options = options.union(.no_mentions) + } + + var body: some View { + let _ = print(event) + LiveStreamPreviewBody(state: state, ev: event, options: options, header: false) + } +} + +let test_live_event = LiveEvent.parse(from: NostrEvent( + content: "", + keypair: test_keypair, + kind: NostrKind.live.rawValue, + tags: [ + ["title", "DAMUSES MEETING"], + ["summary", "Damus Team Meeting"], + ["image", "https://damus.io/img/logo.png"], + ["streaming", "https://ome.mapboss.co.th/live/local_019865b4-6814-71af-b86d-17d0c96d7867/llhls.m3u8"], + ["recording", "https://damus.io"], + ["status", "live"], + ["t", "meeting"], + ["t", "damus"] + ])! +) + + +struct LiveStreamPreview_Previews: PreviewProvider { + static var previews: some View { + VStack { + LiveStreamPreview(state: test_damus_state, ev: test_live_event.event, options: []) + } + .frame(height: 400) + } +} diff --git a/damus/Features/Live/LiveStream/Views/LiveStreamTimeline.swift b/damus/Features/Live/LiveStream/Views/LiveStreamTimeline.swift new file mode 100644 index 00000000..7e817d51 --- /dev/null +++ b/damus/Features/Live/LiveStream/Views/LiveStreamTimeline.swift @@ -0,0 +1,140 @@ +// +// LiveStreamTimeline.swift +// damus +// +// Created by eric on 7/23/25. +// + +import SwiftUI + +struct LiveStreamTimelineView: View { + @ObservedObject var events: EventHolder + @Binding var loading: Bool + + let damus: DamusState + let filter: (NostrEvent) -> Bool + let content: Content? + let apply_mute_rules: Bool + + init(events: EventHolder, loading: Binding, headerHeight: Binding, headerOffset: Binding, damus: DamusState, filter: @escaping (NostrEvent) -> Bool, apply_mute_rules: Bool = true, content: (() -> Content)? = nil) { + self.events = events + self._loading = loading + self.damus = damus + self.filter = filter + self.apply_mute_rules = apply_mute_rules + self.content = content?() + } + + init(events: EventHolder, loading: Binding, damus: DamusState, filter: @escaping (NostrEvent) -> Bool, apply_mute_rules: Bool = true, content: (() -> Content)? = nil) { + self.events = events + self._loading = loading + self.damus = damus + self.filter = filter + self.apply_mute_rules = apply_mute_rules + self.content = content?() + } + + var body: some View { + ScrollViewReader { scroller in + ScrollView { + if let content { + content + } + + HStack { + VStack(alignment: .leading) { + Text("Happening Now") + .font(.title2) + .fontWeight(.bold) + Text("Live events going on right now") + .font(.caption) + .foregroundColor(.gray) + } + .padding(.horizontal, 5) + + Spacer() + } + + Color.clear + .id("startblock") + .frame(height: 0) + + LiveStreamInnerView(events: events, damus: damus, filter: loading ? { _ in true } : filter, apply_mute_rules: self.apply_mute_rules) + .redacted(reason: loading ? .placeholder : []) + .shimmer(loading) + .disabled(loading) + .background { + GeometryReader { proxy -> Color in + handle_scroll_queue(proxy, queue: self.events) + return Color.clear + } + } + } + .coordinateSpace(name: "scroll") + .onReceive(handle_notify(.scroll_to_top)) { () in + events.flush() + self.events.set_should_queue(false) + scroll_to_event(scroller: scroller, id: "startblock", delay: 0.0, animate: true, anchor: .top) + } + } + .onAppear { + events.flush() + } + } +} + +struct LiveStreamInnerView: View { + @ObservedObject var events: EventHolder + let state: DamusState + let filter: (NostrEvent) -> Bool + + init(events: EventHolder, damus: DamusState, filter: @escaping (NostrEvent) -> Bool, apply_mute_rules: Bool = true) { + self.events = events + self.state = damus + self.filter = apply_mute_rules ? { filter($0) && !damus.mutelist_manager.is_event_muted($0) } : filter + } + + var event_options: EventViewOptions { + if self.state.settings.truncate_timeline_text { + return [.wide, .truncate_content] + } + + return [.wide] + } + + var body: some View { + LazyVStack(spacing: 0) { + let events = self.events.events + if events.isEmpty { + EmptyTimelineView() + } else { + let evs = events.filter(filter) + let indexed = Array(zip(evs, 0...)) + ForEach(indexed, id: \.0.id) { tup in + let ev = tup.0 + let ind = tup.1 + if ev.kind == NostrKind.live.rawValue { + LiveStreamPreview(state: state, ev: ev, options: event_options) + .onTapGesture { + state.nav.push(route: Route.LiveEvent(LiveEvent: ev, model: LiveEventModel(damus_state: state))) + } + .padding(.top, 7) + .onAppear { + let to_preload = + Array([indexed[safe: ind+1]?.0, + indexed[safe: ind+2]?.0, + indexed[safe: ind+3]?.0, + indexed[safe: ind+4]?.0, + indexed[safe: ind+5]?.0 + ].compactMap({ $0 })) + + preload_events(state: state, events: to_preload) + } + } + } + } + } + .padding(.bottom, 50) + + } +} diff --git a/damus/Features/Live/LiveStream/Views/LiveStreamView.swift b/damus/Features/Live/LiveStream/Views/LiveStreamView.swift new file mode 100644 index 00000000..e5567780 --- /dev/null +++ b/damus/Features/Live/LiveStream/Views/LiveStreamView.swift @@ -0,0 +1,150 @@ +// +// LiveStreamView.swift +// damus +// +// Created by eric on 7/25/25. +// + +import SwiftUI +import Kingfisher + +struct LiveStreamView: View { + let state: DamusState + let event: LiveEvent + @StateObject var model: LiveEventModel + + @Environment(\.colorScheme) var colorScheme + @Environment(\.presentationMode) var presentationMode + + @ObservedObject var artifacts: NoteArtifactsModel + + + @State private var dragOffset: CGSize = .zero + @State private var isDragging = false + @State private var currentVideoModel: DamusVideoPlayer? + + init(state: DamusState, ev: LiveEvent, model: LiveEventModel) { + self.state = state + self.event = ev + self._model = StateObject(wrappedValue: model) + + self._artifacts = ObservedObject(wrappedValue: state.events.get_cache_data(ev.event.id).artifacts_model) + } + + init(state: DamusState, ev: NostrEvent, model: LiveEventModel) { + self.state = state + self.event = LiveEvent.parse(from: ev) + self._model = StateObject(wrappedValue: model) + + self._artifacts = ObservedObject(wrappedValue: state.events.get_cache_data(ev.id).artifacts_model) + } + + func content_filter(_ pubkeys: [Pubkey]) -> ((NostrEvent) -> Bool) { + var filters = ContentFilters.defaults(damus_state: self.state) + filters.append({ pubkeys.contains($0.pubkey) }) + return ContentFilters(filters: filters).filter + } + + func setupVideoModel() { + if let streamingURL = event.streaming { + currentVideoModel = state.video.get_player(for: streamingURL) +// currentVideoModel = state.video.get_player(for: streamingURL, title: event.title ?? "Untitled", link: streamingURL.absoluteString, artist: "Nostrich", artwork: event.image?.absoluteString ?? "") + } else if let recordingURL = event.recording { + currentVideoModel = model.damus_state.video.get_player(for: recordingURL) +// currentVideoModel = model.damus_state.video.get_player(for: recordingURL, title: event.title ?? "Untitled", link: recordingURL.absoluteString, artist: "Nostrich", artwork: event.image?.absoluteString ?? "") + } + } + + var dragGesture: some Gesture { + DragGesture() + .onChanged { value in + if value.translation.height > 0 { + dragOffset = value.translation + isDragging = true + } + } + .onEnded { value in + isDragging = false + + if value.translation.height > 100 { + withAnimation(.easeOut(duration: 0.3)) { + dragOffset.height = UIScreen.main.bounds.height + } + + DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { + presentationMode.wrappedValue.dismiss() + } + } else { + withAnimation(.spring()) { + dragOffset = .zero + } + } + } + } + + var body: some View { + VStack(spacing: 0) { + LiveEventHeader + .highPriorityGesture(dragGesture, including: .all) + + LiveChatHomeView(state: state, event: event, model: LiveChatModel(damus_state: state, root: event.event.pubkey.hex(), dtag: event.uuid ?? "")) + .scrollDismissesKeyboard(.immediately) + } + .offset(y: dragOffset.height) + .opacity(isDragging ? Double(1 - min(abs(dragOffset.height) / 250, 0.5)) : 1.0) + .animation(.interactiveSpring(), value: dragOffset) + .navigationBarBackButtonHidden(true) + .onAppear { + notify(.display_tabbar(false)) + model.subscribe() + setupVideoModel() + } + .onDisappear { + notify(.display_tabbar(true)) + model.unsubscribe() + } + } + + var LiveEventHeader: some View { + VStack(alignment: .leading, spacing: 0) { + + ZStack { + if let videoModel = currentVideoModel { + DamusVideoPlayerView( + model: videoModel, + coordinator: state.video, + style: .preview(on_tap: {}) + ) + } else { + LiveStreamBanner(state: state, options: EventViewOptions(), image: event.image, preview: false) + } + } + .frame(width: UIScreen.main.bounds.width, height: 250) + .fixedSize(horizontal: true, vertical: true) + .background(Color.black) + + if !event.publicKeys.isEmpty { + LiveStreamProfile(state: state, pubkey: event.publicKeys[0], size: 35) + } else { + LiveStreamProfile(state: state, pubkey: event.event.pubkey, size: 35) + } + + if let title = event.title { + Text(title) + .fixedSize(horizontal: false, vertical: true) + .fontWeight(.bold) + .padding(.horizontal, 5) + } + + // TO DO: Add description in sheet + } + } +} + + +struct LiveStreamView_Previews: PreviewProvider { + static var previews: some View { + LiveStreamView(state: test_damus_state, ev: test_live_event, model: LiveEventModel(damus_state: test_damus_state)) + .environmentObject(OrientationTracker()) + } +} diff --git a/damus/Features/Profile/Views/EventProfileName.swift b/damus/Features/Profile/Views/EventProfileName.swift index 0d017cec..7afc2f81 100644 --- a/damus/Features/Profile/Views/EventProfileName.swift +++ b/damus/Features/Profile/Views/EventProfileName.swift @@ -21,7 +21,7 @@ struct EventProfileName: View { let size: EventViewKind - init(pubkey: Pubkey, damus: DamusState, size: EventViewKind = .normal) { + init(pubkey: Pubkey, damus: DamusState, size: EventViewKind) { self.damus_state = damus self.pubkey = pubkey self.size = size @@ -68,11 +68,15 @@ struct EventProfileName: View { case .one(let one): Text(one) .font(.body.weight(.bold)) + .font(eventviewsize_to_font(size, font_size: damus_state.settings.font_size)) + .fontWeight(.bold) case .both(username: let username, displayName: let displayName): HStack(spacing: 6) { Text(verbatim: displayName) .font(.body.weight(.bold)) + .font(eventviewsize_to_font(size, font_size: damus_state.settings.font_size)) + .fontWeight(.bold) Text(verbatim: "@\(username)") .foregroundColor(.gray) @@ -86,17 +90,18 @@ struct EventProfileName: View { } */ - - if let frend = friend_type { - FriendIcon(friend: frend) + if size != .small { + if let frend = friend_type { + FriendIcon(friend: frend) + } + + if onlyzapper(profile) { + Image("zap-hashtag") + .frame(width: 14, height: 14) + } + + SupporterBadge(percent: self.supporter_percentage(), purple_account: self.purple_account, style: .compact) } - - if onlyzapper(profile) { - Image("zap-hashtag") - .frame(width: 14, height: 14) - } - - SupporterBadge(percent: self.supporter_percentage(), purple_account: self.purple_account, style: .compact) } .onReceive(handle_notify(.profile_updated)) { update in if update.pubkey != pubkey { @@ -132,6 +137,6 @@ struct EventProfileName: View { struct EventProfileName_Previews: PreviewProvider { static var previews: some View { - EventProfileName(pubkey: test_note.pubkey, damus: test_damus_state) + EventProfileName(pubkey: test_note.pubkey, damus: test_damus_state, size: .normal) } } diff --git a/damus/Features/Profile/Views/ProfileView.swift b/damus/Features/Profile/Views/ProfileView.swift index e9b37a3b..ffab68a3 100644 --- a/damus/Features/Profile/Views/ProfileView.swift +++ b/damus/Features/Profile/Views/ProfileView.swift @@ -127,6 +127,8 @@ struct ProfileView: View { filters.append({ profile.pubkey == $0.pubkey }) case .conversations: filters.append({ profile.conversation_events.contains($0.id) } ) + case .live, .live_chat: + break } return ContentFilters(filters: filters).filter } diff --git a/damus/Features/Settings/Models/UserSettingsStore.swift b/damus/Features/Settings/Models/UserSettingsStore.swift index 5831b4be..24391665 100644 --- a/damus/Features/Settings/Models/UserSettingsStore.swift +++ b/damus/Features/Settings/Models/UserSettingsStore.swift @@ -365,6 +365,11 @@ class UserSettingsStore: ObservableObject { } } + // MARK: Damus Labs Experiments + @Setting(key: "live", default_value: false) + var live: Bool + + // MARK: Internal, hidden settings // TODO: Get rid of this once we have NostrDB query capabilities integrated diff --git a/damus/Features/Timeline/Models/ContentFilters.swift b/damus/Features/Timeline/Models/ContentFilters.swift index 35530bc3..3c9e1553 100644 --- a/damus/Features/Timeline/Models/ContentFilters.swift +++ b/damus/Features/Timeline/Models/ContentFilters.swift @@ -28,6 +28,8 @@ enum FilterState : Int { case posts_and_replies = 1 case conversations = 2 case follow_list = 3 + case live = 4 + case live_chat = 5 func filter(ev: NostrEvent) -> Bool { switch self { @@ -39,6 +41,10 @@ enum FilterState : Int { return true case .follow_list: return ev.known_kind == .follow_list + case .live: + return ev.known_kind == .live + case .live_chat: + return ev.known_kind == .live_chat } } } diff --git a/damus/Features/Timeline/Models/HomeModel.swift b/damus/Features/Timeline/Models/HomeModel.swift index 5ba16e54..8a9f07ea 100644 --- a/damus/Features/Timeline/Models/HomeModel.swift +++ b/damus/Features/Timeline/Models/HomeModel.swift @@ -231,6 +231,8 @@ class HomeModel: ContactsDelegate, ObservableObject { break case .interest_list: break // Don't care for now + case .live, .live_chat: + break } } diff --git a/damus/Features/Timeline/Views/SideMenuView.swift b/damus/Features/Timeline/Views/SideMenuView.swift index dbf99de2..83410577 100644 --- a/damus/Features/Timeline/Views/SideMenuView.swift +++ b/damus/Features/Timeline/Views/SideMenuView.swift @@ -73,6 +73,12 @@ struct SideMenuView: View { } .frame(maxWidth: .infinity, alignment: .leading) } + + if damus_state.settings.live { + NavigationLink(value: Route.LiveEvents(model: LiveEventModel(damus_state: damus_state))) { + navLabel(title: NSLocalizedString("Live", comment: "Sidebar menu label for live events view."), img: "record") + } + } NavigationLink(value: Route.MuteList) { navLabel(title: NSLocalizedString("Muted", comment: "Sidebar menu label for muted users view."), img: "mute") diff --git a/damus/Shared/Components/EventTags.swift b/damus/Shared/Components/EventTags.swift new file mode 100644 index 00000000..8cfccfcd --- /dev/null +++ b/damus/Shared/Components/EventTags.swift @@ -0,0 +1,33 @@ +// +// EventTags.swift +// damus +// +// Created by eric on 8/8/25. +// + +import SwiftUI + +struct EventTags: View { + var tags: [String]? + + var body: some View { + ScrollView(.horizontal) { + HStack { + ForEach(tags ?? [], id: \.self) { tag in + Text(tag) + .font(.caption2) + .foregroundColor(.gray) + .padding(EdgeInsets(top: 2, leading: 8, bottom: 2, trailing: 8)) + .background(DamusColors.neutral1) + .cornerRadius(20) + .overlay( + RoundedRectangle(cornerRadius: 20) + .stroke(DamusColors.neutral3, lineWidth: 1) + ) + } + }.padding(.horizontal, 5) + } + .padding(.bottom, 5) + .scrollIndicators(.hidden) + } +} diff --git a/damus/Shared/Modifiers/Glow.swift b/damus/Shared/Modifiers/Glow.swift new file mode 100644 index 00000000..6288a87f --- /dev/null +++ b/damus/Shared/Modifiers/Glow.swift @@ -0,0 +1,31 @@ +// +// Glow.swift +// damus +// +// Created by eric on 7/26/25. +// + +import SwiftUI + +struct Glow: ViewModifier { + @State private var effect = false + + func body(content: Content) -> some View { + ZStack { + content + .blur(radius: effect ? 15 : 5) + .animation(.easeOut(duration: 0.5).repeatForever(), value: effect) + .onAppear { + effect.toggle() + } + + content + } + } +} + +extension View { + func glow() -> some View { + modifier(Glow()) + } +} diff --git a/damus/Shared/Utilities/Router.swift b/damus/Shared/Utilities/Router.swift index 9ab1b3fc..b48d2f18 100644 --- a/damus/Shared/Utilities/Router.swift +++ b/damus/Shared/Utilities/Router.swift @@ -50,6 +50,8 @@ enum Route: Hashable { case NIP05DomainEvents(events: NIP05DomainEventsModel, nip05_domain_favicon: FaviconURL?) case NIP05DomainPubkeys(domain: String, nip05_domain_favicon: FaviconURL?, pubkeys: [Pubkey]) case FollowPack(followPack: NostrEvent, model: FollowPackModel, blur_imgs: Bool) + case LiveEvents(model: LiveEventModel) + case LiveEvent(LiveEvent: NostrEvent, model: LiveEventModel) @ViewBuilder func view(navigationCoordinator: NavigationCoordinator, damusState: DamusState) -> some View { @@ -137,7 +139,12 @@ enum Route: Hashable { NIP05DomainPubkeysView(damus_state: damusState, domain: domain, nip05_domain_favicon: nip05_domain_favicon, pubkeys: pubkeys) case .FollowPack(let followPack, let followPackModel, let blur_imgs): FollowPackView(state: damusState, ev: followPack, model: followPackModel, blur_imgs: blur_imgs) + case .LiveEvents(let model): + LiveStreamHomeView(damus_state: damusState, model: model) + case .LiveEvent(let liveEvent, let liveEventModel): + LiveStreamView(state: damusState, ev: liveEvent, model: liveEventModel) } + } static func == (lhs: Route, rhs: Route) -> Bool { @@ -249,6 +256,11 @@ enum Route: Hashable { case .FollowPack(let followPack, let followPackModel, let blur_imgs): hasher.combine("followPack") hasher.combine(followPack.id) + case .LiveEvents(let model): + hasher.combine("liveEvents") + case .LiveEvent(let liveEvent, let liveEventModel): + hasher.combine("liveEvent") + hasher.combine(liveEvent.id) } } } diff --git a/damus/en-US.lproj/Localizable.stringsdict b/damus/en-US.lproj/Localizable.stringsdict index a436807b..2d0c7c72 100644 --- a/damus/en-US.lproj/Localizable.stringsdict +++ b/damus/en-US.lproj/Localizable.stringsdict @@ -2,6 +2,22 @@ + viewer_count + + NSStringLocalizedFormatKey + %#@VIEWERS@ + VIEWERS + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + d + one + viewer + other + viewers + + follow_pack_user_count NSStringLocalizedFormatKey diff --git a/nostrdb/NdbNote.swift b/nostrdb/NdbNote.swift index 080048e1..1291b46a 100644 --- a/nostrdb/NdbNote.swift +++ b/nostrdb/NdbNote.swift @@ -387,7 +387,7 @@ class NdbNote: Codable, Equatable, Hashable { extension NdbNote { var is_textlike: Bool { switch known_kind { - case .text, .chat, .longform, .highlight: + case .text, .chat, .longform, .highlight, .live, .live_chat: true default: false