Damus Live

This PR adds Live Streaming and Live Chat to Damus via Damus Labs.

Changelog-Added: Added live stream timeline
Changelog-Added: Added live chat timeline
Changelog-Added: Added ability to create live chat event
Changelog-Added: Damus Labs Toggle

Signed-off-by: ericholguin <ericholguin@apache.org>
This commit is contained in:
ericholguin
2025-11-06 17:36:04 -07:00
committed by Daniel D’Aquino
parent a31f6bce0e
commit b8c664d354
39 changed files with 1793 additions and 92 deletions

View File

@@ -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 = "<group>"; };
5C8498012D5D14FA00F74FEB /* ZapExplainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ZapExplainer.swift; sourceTree = "<group>"; };
5C8711DD2C460C06007879C2 /* PostingTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostingTimelineView.swift; sourceTree = "<group>"; };
5C8F97092EB45E85009399B1 /* LiveChatModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveChatModel.swift; sourceTree = "<group>"; };
5C8F970D2EB45F68009399B1 /* LiveChatHomeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveChatHomeView.swift; sourceTree = "<group>"; };
5C8F97112EB45FA3009399B1 /* LiveChatTimeline.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveChatTimeline.swift; sourceTree = "<group>"; };
5C8F97152EB45FD1009399B1 /* LiveChatView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveChatView.swift; sourceTree = "<group>"; };
5C8F971B2EB46078009399B1 /* LiveEvent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveEvent.swift; sourceTree = "<group>"; };
5C8F971F2EB46093009399B1 /* LiveEventModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveEventModel.swift; sourceTree = "<group>"; };
5C8F97242EB460C2009399B1 /* LiveStreamBanner.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveStreamBanner.swift; sourceTree = "<group>"; };
5C8F97282EB460DC009399B1 /* LiveStreamProfile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveStreamProfile.swift; sourceTree = "<group>"; };
5C8F972C2EB4610F009399B1 /* LiveStreamStatus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveStreamStatus.swift; sourceTree = "<group>"; };
5C8F97302EB46121009399B1 /* LiveStreamViewers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveStreamViewers.swift; sourceTree = "<group>"; };
5C8F97342EB46141009399B1 /* LiveStreamView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveStreamView.swift; sourceTree = "<group>"; };
5C8F97382EB46167009399B1 /* LiveStreamTimeline.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveStreamTimeline.swift; sourceTree = "<group>"; };
5C8F973C2EB46192009399B1 /* LiveStreamPreview.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveStreamPreview.swift; sourceTree = "<group>"; };
5C8F97402EB461AB009399B1 /* LiveStreamHomeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveStreamHomeView.swift; sourceTree = "<group>"; };
5C8F97442EB461D6009399B1 /* EventTags.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventTags.swift; sourceTree = "<group>"; };
5C8F97482EB46208009399B1 /* Glow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Glow.swift; sourceTree = "<group>"; };
5C8F974D2EBD7044009399B1 /* LabsToggleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LabsToggleView.swift; sourceTree = "<group>"; };
5C8F97512EBD7071009399B1 /* LabsExplainerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LabsExplainerView.swift; sourceTree = "<group>"; };
5CB017202D2D985800A9ED05 /* CoinosButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoinosButton.swift; sourceTree = "<group>"; };
5CB017242D42C5BD00A9ED05 /* TransactionsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransactionsView.swift; sourceTree = "<group>"; };
5CB0172C2D42C76600A9ED05 /* BalanceView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BalanceView.swift; sourceTree = "<group>"; };
@@ -2581,7 +2653,7 @@
5CB645972EA317CC0018BD91 /* DamusLabs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DamusLabs.swift; sourceTree = "<group>"; };
5CB6459B2EA31D750018BD91 /* LabsIntroduction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LabsIntroduction.swift; sourceTree = "<group>"; };
5CB645A02EA31E3D0018BD91 /* LabsLogoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LabsLogoView.swift; sourceTree = "<group>"; };
5CB645A82EAC013A0018BD91 /* DamusLabsExpirements.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DamusLabsExpirements.swift; sourceTree = "<group>"; };
5CB645A82EAC013A0018BD91 /* DamusLabsExperiments.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DamusLabsExperiments.swift; sourceTree = "<group>"; };
5CC8529C2BD741CD0039FFC5 /* HighlightEvent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HighlightEvent.swift; sourceTree = "<group>"; };
5CC8529E2BD744F60039FFC5 /* HighlightView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HighlightView.swift; sourceTree = "<group>"; };
5CC852A12BDED9B90039FFC5 /* HighlightDescription.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HighlightDescription.swift; sourceTree = "<group>"; };
@@ -3663,6 +3735,7 @@
5C513FCB2984ACA60072348F /* QRCodeView.swift */,
4C363A8B28236B92006E126D /* PubkeyView.swift */,
4C8D00CB29DF92DF0036AF10 /* Hashtags.swift */,
5C8F97442EB461D6009399B1 /* EventTags.swift */,
);
path = Components;
sourceTree = "<group>";
@@ -4049,6 +4122,7 @@
5C78A7792E22FDFE00CF177D /* Features */ = {
isa = PBXGroup;
children = (
5C8F97042EB45E39009399B1 /* Live */,
D5C1AFC22E5DFF040092F72F /* ContactCard */,
5C78A7BC2E304D7400CF177D /* Translations */,
5C78A7B52E3046F400CF177D /* NIP05 */,
@@ -4884,12 +4958,100 @@
path = Views;
sourceTree = "<group>";
};
5C8F97042EB45E39009399B1 /* Live */ = {
isa = PBXGroup;
children = (
5C8F97062EB45E53009399B1 /* LiveStream */,
5C8F97052EB45E4C009399B1 /* LiveChat */,
);
path = Live;
sourceTree = "<group>";
};
5C8F97052EB45E4C009399B1 /* LiveChat */ = {
isa = PBXGroup;
children = (
5C8F97072EB45E5F009399B1 /* Models */,
5C8F97082EB45E63009399B1 /* Views */,
);
path = LiveChat;
sourceTree = "<group>";
};
5C8F97062EB45E53009399B1 /* LiveStream */ = {
isa = PBXGroup;
children = (
5C8F971A2EB4600C009399B1 /* Views */,
5C8F97192EB46005009399B1 /* Models */,
);
path = LiveStream;
sourceTree = "<group>";
};
5C8F97072EB45E5F009399B1 /* Models */ = {
isa = PBXGroup;
children = (
5C8F97092EB45E85009399B1 /* LiveChatModel.swift */,
);
path = Models;
sourceTree = "<group>";
};
5C8F97082EB45E63009399B1 /* Views */ = {
isa = PBXGroup;
children = (
5C8F97152EB45FD1009399B1 /* LiveChatView.swift */,
5C8F97112EB45FA3009399B1 /* LiveChatTimeline.swift */,
5C8F970D2EB45F68009399B1 /* LiveChatHomeView.swift */,
);
path = Views;
sourceTree = "<group>";
};
5C8F97192EB46005009399B1 /* Models */ = {
isa = PBXGroup;
children = (
5C8F971F2EB46093009399B1 /* LiveEventModel.swift */,
5C8F971B2EB46078009399B1 /* LiveEvent.swift */,
);
path = Models;
sourceTree = "<group>";
};
5C8F971A2EB4600C009399B1 /* Views */ = {
isa = PBXGroup;
children = (
5C8F97402EB461AB009399B1 /* LiveStreamHomeView.swift */,
5C8F973C2EB46192009399B1 /* LiveStreamPreview.swift */,
5C8F97382EB46167009399B1 /* LiveStreamTimeline.swift */,
5C8F97342EB46141009399B1 /* LiveStreamView.swift */,
5C8F97232EB460B8009399B1 /* Components */,
);
path = Views;
sourceTree = "<group>";
};
5C8F97232EB460B8009399B1 /* Components */ = {
isa = PBXGroup;
children = (
5C8F97302EB46121009399B1 /* LiveStreamViewers.swift */,
5C8F972C2EB4610F009399B1 /* LiveStreamStatus.swift */,
5C8F97282EB460DC009399B1 /* LiveStreamProfile.swift */,
5C8F97242EB460C2009399B1 /* LiveStreamBanner.swift */,
);
path = Components;
sourceTree = "<group>";
};
5C8F974C2EBD703D009399B1 /* Components */ = {
isa = PBXGroup;
children = (
5C8F97512EBD7071009399B1 /* LabsExplainerView.swift */,
5C8F974D2EBD7044009399B1 /* LabsToggleView.swift */,
);
path = Components;
sourceTree = "<group>";
};
5CB645952EA3106A0018BD91 /* Views */ = {
isa = PBXGroup;
children = (
5CB645A82EAC013A0018BD91 /* DamusLabsExpirements.swift */,
5C8F974C2EBD703D009399B1 /* Components */,
5CB645A82EAC013A0018BD91 /* DamusLabsExperiments.swift */,
5CB6459B2EA31D750018BD91 /* LabsIntroduction.swift */,
5CB645972EA317CC0018BD91 /* DamusLabs.swift */,
5CB6459F2EA31E2C0018BD91 /* Detail */,
);
path = Views;
sourceTree = "<group>";
@@ -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 */,

View File

@@ -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 {

View File

@@ -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

View File

@@ -13,33 +13,39 @@ 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()
if !options.contains(.no_context_menu) {
EventMenuContext(damus: state, event: event)
}
}
.lineLimit(1)
}
}
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: [])
}
}

View File

@@ -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)
}
}

View File

@@ -63,9 +63,11 @@ struct EventShell<Content: View>: View {
}
VStack(alignment: .leading) {
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)
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<Content: View>: 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)

View File

@@ -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):

View File

@@ -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,

View File

@@ -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)])
}
}

View File

@@ -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()
}
}
}
}

View File

@@ -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()
}
}
}

View File

@@ -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, youre 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 whats 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)
}
}

View File

@@ -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, youre 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 whats 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)
}
}

View File

@@ -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<Void, any Error>? = 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()
}
}
}
}
}

View File

@@ -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()
}
}
}

View File

@@ -0,0 +1,136 @@
//
// LiveChatTimeline.swift
// damus
//
// Created by eric on 8/7/25.
//
import SwiftUI
struct LiveChatTimelineView<Content: View>: 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<Bool>, headerHeight: Binding<CGFloat>, headerOffset: Binding<CGFloat>, 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<Bool>, 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)
}
}

View File

@@ -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)
}
}

View File

@@ -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
}
}

View File

@@ -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<Void, any Error>? = nil
var seen_dtag: Set<String> = 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()
}
}
}
}
}

View File

@@ -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()
}
}
}

View File

@@ -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)
}
}

View File

@@ -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)
}
}

View File

@@ -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)
}
}

View File

@@ -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<AnyView>(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))
}
}

View File

@@ -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)
}
}

View File

@@ -0,0 +1,140 @@
//
// LiveStreamTimeline.swift
// damus
//
// Created by eric on 7/23/25.
//
import SwiftUI
struct LiveStreamTimelineView<Content: View>: 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<Bool>, headerHeight: Binding<CGFloat>, headerOffset: Binding<CGFloat>, 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<Bool>, 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)
}
}

View File

@@ -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())
}
}

View File

@@ -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,7 +90,7 @@ struct EventProfileName: View {
}
*/
if size != .small {
if let frend = friend_type {
FriendIcon(friend: frend)
}
@@ -98,6 +102,7 @@ struct EventProfileName: View {
SupporterBadge(percent: self.supporter_percentage(), purple_account: self.purple_account, style: .compact)
}
}
.onReceive(handle_notify(.profile_updated)) { update in
if update.pubkey != pubkey {
return
@@ -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)
}
}

View File

@@ -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
}

View File

@@ -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

View File

@@ -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
}
}
}

View File

@@ -231,6 +231,8 @@ class HomeModel: ContactsDelegate, ObservableObject {
break
case .interest_list:
break // Don't care for now
case .live, .live_chat:
break
}
}

View File

@@ -74,6 +74,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")
}

View File

@@ -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)
}
}

View File

@@ -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())
}
}

View File

@@ -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)
}
}
}

View File

@@ -2,6 +2,22 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>viewer_count</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
<string>%#@VIEWERS@</string>
<key>VIEWERS</key>
<dict>
<key>NSStringFormatSpecTypeKey</key>
<string>NSStringPluralRuleType</string>
<key>NSStringFormatValueTypeKey</key>
<string>d</string>
<key>one</key>
<string>viewer</string>
<key>other</key>
<string>viewers</string>
</dict>
</dict>
<key>follow_pack_user_count</key>
<dict>
<key>NSStringLocalizedFormatKey</key>

View File

@@ -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