Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New gallery view #1418

Merged
merged 10 commits into from
Aug 22, 2024
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]

- Added a new image viewer that appears when you tap an image.
- Added a new gallery view that’s currently behind a feature flag.
- Included the npub in the properties list sent to analytics.
- Removed the like and repost counts from the Main and Profile feeds.
- Fixed an issue where the sheet asking users to set up a NIP-05 username would appear after reinstalling Nos, even if the profile already had a NIP-05 username.
Expand Down
42 changes: 31 additions & 11 deletions Nos.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,10 @@
037975C72C0E26FC00ADDF37 /* Font.swift in Sources */ = {isa = PBXBuildFile; fileRef = C987F85729BA981800B44E7A /* Font.swift */; };
037975D12C0E341500ADDF37 /* MockFeatureFlags.swift in Sources */ = {isa = PBXBuildFile; fileRef = 037975D02C0E341500ADDF37 /* MockFeatureFlags.swift */; };
037975EA2C0E695A00ADDF37 /* MockFeatureFlags.swift in Sources */ = {isa = PBXBuildFile; fileRef = 037975D02C0E341500ADDF37 /* MockFeatureFlags.swift */; };
0383CD712C76793E007DB0E4 /* GalleryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03E181382C75467C00886CC6 /* GalleryView.swift */; };
0383CD722C767966007DB0E4 /* LinearGradient+Planetary.swift in Sources */ = {isa = PBXBuildFile; fileRef = C95D68A8299E709800429F86 /* LinearGradient+Planetary.swift */; };
0383CD7B2C76798C007DB0E4 /* ImageButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03E1812E2C753C9B00886CC6 /* ImageButton.swift */; };
0383CD842C76799D007DB0E4 /* LinkView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03E181462C754BA300886CC6 /* LinkView.swift */; };
038863DE2C6FF51500B09797 /* ZoomableContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 038863DD2C6FF51500B09797 /* ZoomableContainer.swift */; };
038863DF2C6FF51500B09797 /* ZoomableContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 038863DD2C6FF51500B09797 /* ZoomableContainer.swift */; };
039C961F2C480F4100A8EB39 /* unsupported_kinds.json in Resources */ = {isa = PBXBuildFile; fileRef = 039C961E2C480F4100A8EB39 /* unsupported_kinds.json */; };
Expand All @@ -72,6 +76,9 @@
03D1B4292C3C1AC9001778CD /* NostrIdentifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03D1B4272C3C1A5D001778CD /* NostrIdentifier.swift */; };
03D1B42C2C3C1B0D001778CD /* TLVElement.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03D1B42B2C3C1B0D001778CD /* TLVElement.swift */; };
03D1B42D2C3C1B0D001778CD /* TLVElement.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03D1B42B2C3C1B0D001778CD /* TLVElement.swift */; };
03E1812F2C753C9B00886CC6 /* ImageButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03E1812E2C753C9B00886CC6 /* ImageButton.swift */; };
03E181392C75467C00886CC6 /* GalleryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03E181382C75467C00886CC6 /* GalleryView.swift */; };
03E181472C754BA300886CC6 /* LinkView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03E181462C754BA300886CC6 /* LinkView.swift */; };
03E9C6792C6FBBE400C9B843 /* ImageViewer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03C8B4952C6D065900A07CCD /* ImageViewer.swift */; };
03ED93472C46C48400C8D443 /* JSONEventTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03ED93462C46C48400C8D443 /* JSONEventTests.swift */; };
03F7C4F32C10DF79006FF613 /* URLSessionProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03F7C4F22C10DF79006FF613 /* URLSessionProtocol.swift */; };
Expand Down Expand Up @@ -572,6 +579,9 @@
03C8B4952C6D065900A07CCD /* ImageViewer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageViewer.swift; sourceTree = "<group>"; };
03D1B4272C3C1A5D001778CD /* NostrIdentifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NostrIdentifier.swift; sourceTree = "<group>"; };
03D1B42B2C3C1B0D001778CD /* TLVElement.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TLVElement.swift; sourceTree = "<group>"; };
03E1812E2C753C9B00886CC6 /* ImageButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageButton.swift; sourceTree = "<group>"; };
03E181382C75467C00886CC6 /* GalleryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GalleryView.swift; sourceTree = "<group>"; };
03E181462C754BA300886CC6 /* LinkView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LinkView.swift; sourceTree = "<group>"; };
03ED93462C46C48400C8D443 /* JSONEventTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JSONEventTests.swift; sourceTree = "<group>"; };
03F7C4F22C10DF79006FF613 /* URLSessionProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLSessionProtocol.swift; sourceTree = "<group>"; };
2D06BB9C2AE249D70085F509 /* ThreadRootView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ThreadRootView.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -664,7 +674,7 @@
A3B943D4299D514800A15A08 /* Follow+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Follow+CoreDataClass.swift"; sourceTree = "<group>"; };
C9032C2D2BAE31ED001F4EC6 /* ProfileFeedType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileFeedType.swift; sourceTree = "<group>"; };
C90352B92C1235CD000A5993 /* NosNavigationDestination.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NosNavigationDestination.swift; sourceTree = "<group>"; };
C905B0762A619E99009B8A78 /* LinkPreview.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LinkPreview.swift; sourceTree = "<group>"; };
C905B0762A619E99009B8A78 /* LinkPreview.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = LinkPreview.swift; path = ../../LinkPreview.swift; sourceTree = "<group>"; };
C90862BB29E9804B00C35A71 /* NosPerformanceTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = NosPerformanceTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
C90862BD29E9804B00C35A71 /* NosPerformanceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NosPerformanceTests.swift; sourceTree = "<group>"; };
C90B16B72AFED96300CB4B85 /* URLExtensionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLExtensionTests.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -1065,14 +1075,18 @@
path = Generated;
sourceTree = "<group>";
};
03C8B4902C6D061900A07CCD /* Images */ = {
03C8B4902C6D061900A07CCD /* Media */ = {
isa = PBXGroup;
children = (
03E181382C75467C00886CC6 /* GalleryView.swift */,
03E1812E2C753C9B00886CC6 /* ImageButton.swift */,
03C8B4952C6D065900A07CCD /* ImageViewer.swift */,
C905B0762A619E99009B8A78 /* LinkPreview.swift */,
03E181462C754BA300886CC6 /* LinkView.swift */,
C92DF80729C25FA900400561 /* SquareImage.swift */,
038863DD2C6FF51500B09797 /* ZoomableContainer.swift */,
);
path = Images;
path = Media;
sourceTree = "<group>";
};
03D1B42A2C3C1AE7001778CD /* NostrIdentifier */ = {
Expand Down Expand Up @@ -1140,7 +1154,7 @@
5B79F6402BA11618002DA9BE /* Components */ = {
isa = PBXGroup;
children = (
03C8B4902C6D061900A07CCD /* Images */,
03C8B4902C6D061900A07CCD /* Media */,
5B79F6122B98B145002DA9BE /* WizardNavigationStack.swift */,
5B79F6452BA11725002DA9BE /* WizardSheetVStack.swift */,
5B79F64B2BA119AE002DA9BE /* WizardSheetTitleText.swift */,
Expand Down Expand Up @@ -1519,17 +1533,17 @@
C9DFA974299C30CA006929C1 /* Assets */ = {
isa = PBXGroup;
children = (
C9D573472AB24B5800E06BB4 /* SwiftGen Stencils */,
C9A0DAEC29C6A66C00466635 /* Launch Screen.storyboard */,
C9DEBFDA298941020078B43A /* Assets.xcassets */,
9DF077732C63BEA200F6B14E /* Colors.xcassets */,
C9DFA977299C3189006929C1 /* Localization */,
C987F81F29BA94D400B44E7A /* Font */,
C94D39242ABDDFB60019C4D5 /* EmptySecrets.xcconfig */,
C94D39212ABDDDFE0019C4D5 /* Secrets.xcconfig */,
5BE4609F2BACAFEE004B83ED /* DevSecrets.xcconfig */,
C94D39242ABDDFB60019C4D5 /* EmptySecrets.xcconfig */,
5BE460A02BACAFEE004B83ED /* ProductionSecrets.xcconfig */,
C94D39212ABDDDFE0019C4D5 /* Secrets.xcconfig */,
5BE4609E2BACAFEE004B83ED /* StagingSecrets.xcconfig */,
C9A0DAEC29C6A66C00466635 /* Launch Screen.storyboard */,
C987F81F29BA94D400B44E7A /* Font */,
C9DFA977299C3189006929C1 /* Localization */,
C9D573472AB24B5800E06BB4 /* SwiftGen Stencils */,
);
path = Assets;
sourceTree = "<group>";
Expand Down Expand Up @@ -1605,7 +1619,6 @@
C9A0DAE329C69F0C00466635 /* HighlightedText.swift */,
2D4010A12AD87DF300F93AD4 /* KnownFollowersView.swift */,
C960C57029F3236200929990 /* LikeButton.swift */,
C905B0762A619E99009B8A78 /* LinkPreview.swift */,
5BFF66B52A58A8A000AA79DD /* MutesView.swift */,
030036AA2C5D872B002C71F5 /* NewNotesButton.swift */,
C9E8C1142B081EBE002D46B0 /* NIP05View.swift */,
Expand Down Expand Up @@ -1983,6 +1996,7 @@
C9C2B78229E0735400548B4A /* RelaySubscriptionManager.swift in Sources */,
3FFB1D9629A6BBEC002A755D /* Collection+SafeSubscript.swift in Sources */,
A34E439929A522F20057AFCB /* CurrentUser.swift in Sources */,
03E1812F2C753C9B00886CC6 /* ImageButton.swift in Sources */,
C9A0DADD29C689C900466635 /* NosNavigationBar.swift in Sources */,
3F30020529C1FDD9003D4F8B /* OnboardingStartView.swift in Sources */,
C936B4592A4C7B7C00DF1EB9 /* Nos.xcdatamodeld in Sources */,
Expand Down Expand Up @@ -2121,6 +2135,7 @@
030AE4292BE3D63C004DEE02 /* FeaturedAuthor.swift in Sources */,
C9B678E729F01A8500303F33 /* FullscreenProgressView.swift in Sources */,
C9F0BB6929A5039D000547FC /* Int+Bool.swift in Sources */,
03E181472C754BA300886CC6 /* LinkView.swift in Sources */,
C90352BA2C1235CD000A5993 /* NosNavigationDestination.swift in Sources */,
03D1B4282C3C1A5D001778CD /* NostrIdentifier.swift in Sources */,
DC5F203F2A6AE24200F8D73F /* ImagePickerButton.swift in Sources */,
Expand Down Expand Up @@ -2182,6 +2197,7 @@
CD09A74829A51EFC0063464F /* Router.swift in Sources */,
2D4010A22AD87DF300F93AD4 /* KnownFollowersView.swift in Sources */,
CD2CF38E299E67F900332116 /* CardButtonStyle.swift in Sources */,
03E181392C75467C00886CC6 /* GalleryView.swift in Sources */,
A336DD3C299FD78000A0CBA0 /* Filter.swift in Sources */,
DC2E54C82A700F1400C2CAAB /* UIDevice+Simulator.swift in Sources */,
C97B288A2C10B07100DC1FC0 /* NosNavigationStack.swift in Sources */,
Expand Down Expand Up @@ -2287,6 +2303,7 @@
C98A32282A05795E00E3FA13 /* Task+Timeout.swift in Sources */,
035729B12BE4167E005FEE85 /* ReportTests.swift in Sources */,
C973AB602A323167002AED16 /* AuthorReference+CoreDataProperties.swift in Sources */,
0383CD7B2C76798C007DB0E4 /* ImageButton.swift in Sources */,
5BE281CD2AE2CD4700880466 /* AvatarView.swift in Sources */,
C9F84C1A298DBB6300C6714D /* Data+Encoding.swift in Sources */,
C9DEC0642989541F0078B43A /* Bundle+Current.swift in Sources */,
Expand All @@ -2310,6 +2327,7 @@
5BD25E592C192BBC005CF884 /* NoteParserTests+Parse.swift in Sources */,
3FFF3BD029A9645F00DD0B72 /* AuthorReference+CoreDataClass.swift in Sources */,
030036942C5D3AD3002C71F5 /* RefreshController.swift in Sources */,
0383CD712C76793E007DB0E4 /* GalleryView.swift in Sources */,
035729B82BE416A6005FEE85 /* DirectMessageWrapperTests.swift in Sources */,
C9F0BB6C29A503D6000547FC /* PublicKey.swift in Sources */,
C9DEC06F2989668E0078B43A /* Relay+CoreDataClass.swift in Sources */,
Expand All @@ -2326,6 +2344,7 @@
C9646EAA29B7A506007239A4 /* Analytics.swift in Sources */,
C9736E5E2C13B718005BCE70 /* EventFixture.swift in Sources */,
035729AF2BE4167E005FEE85 /* KeyPairTests.swift in Sources */,
0383CD722C767966007DB0E4 /* LinearGradient+Planetary.swift in Sources */,
035729AE2BE4167E005FEE85 /* FollowTests.swift in Sources */,
5BFBB2952BD9D7EB002E909F /* URLParserTests.swift in Sources */,
C91400252B2A3ABF009B13B4 /* SQLiteStoreTestCase.swift in Sources */,
Expand All @@ -2351,6 +2370,7 @@
C9C547552A4F1CDB006B0741 /* SearchController.swift in Sources */,
3FFB1D9429A6BBCE002A755D /* EventReference+CoreDataClass.swift in Sources */,
C9BD919B2B61C4FB00FDA083 /* RawNostrID+Random.swift in Sources */,
0383CD842C76799D007DB0E4 /* LinkView.swift in Sources */,
C9A6C7452AD83FB0001F9500 /* NotificationViewModel.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0xA3",
"green" : "0x75",
"red" : "0x85"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0x24",
"green" : "0x0E",
"red" : "0x16"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
2 changes: 1 addition & 1 deletion Nos/Views/CompactNoteView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@ struct CompactNoteView: View {
}
if note.kind == EventKind.text.rawValue, showLinkPreviews, !note.contentLinks.isEmpty {
if featureFlags.newMediaDisplayEnabled {
EmptyView()
GalleryView(urls: note.contentLinks)
} else {
LinkPreviewCarousel(links: note.contentLinks)
}
Expand Down
106 changes: 106 additions & 0 deletions Nos/Views/Components/Media/GalleryView.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import SwiftUI

/// Displays an array of URLs in a tab view with custom paging indicators.
/// If only one URL is provided, displays an `ImageButton` or `LinkPreview` depending on the URL.
struct GalleryView: View {
/// The URLs of the content to display.
var urls: [URL]
joshuatbrown marked this conversation as resolved.
Show resolved Hide resolved

/// The currently-selected tab in the tab view.
@State private var selectedTab = 0

var body: some View {
if urls.count == 1, let url = urls.first {
if url.isImage {
ImageButton(url: url)
} else {
LinkView(url: url)
}
} else {
VStack {
TabView(selection: $selectedTab) {
ForEach(urls.indices, id: \.self) { index in
LinkView(url: urls[index])
.frame(maxWidth: .infinity)
.tag(index)
}
}
.tabViewStyle(.page(indexDisplayMode: .never))
.frame(height: 320)
.padding(.bottom, 10)

GalleryIndexView(numberOfPages: urls.count, currentIndex: selectedTab)
}
.padding(.bottom, 10)
}
}
}

// Thanks for the [example](https://betterprogramming.pub/custom-paging-ui-in-swiftui-13f1347cf529) Alexandru Turcanu!

/// Custom paging indicators for a `GalleryView`.
fileprivate struct GalleryIndexView: View {
/// The number of pages in the tab view.
let numberOfPages: Int

/// The currently-selected tab in the tab view.
let currentIndex: Int

/// The size of the circle representing the currently-selected tab.
private let circleSize: CGFloat = 8.0

/// The space between circles.
private let circleSpacing: CGFloat = 6.0

/// The fill style of the circle indicating which tab is selected.
private let primaryFill = LinearGradient.horizontalAccent

/// The fill style of the circles indicating tabs that are not selected.
private let secondaryFill = Color.galleryIndexDotSecondary

/// The scale of the circles representing tabs that aren't selected, relative to `circleSize`.
private let smallScale: CGFloat = 0.75

var body: some View {
HStack(spacing: circleSpacing) {
ForEach(0..<numberOfPages, id: \.self) { index in
Circle()
.fill(currentIndex == index ? AnyShapeStyle(primaryFill) : AnyShapeStyle(secondaryFill))
.scaleEffect(currentIndex == index ? 1 : smallScale)
.frame(width: circleSize, height: circleSize)
.transition(AnyTransition.opacity.combined(with: .scale))
.id(index)
}
}
.padding(8.0)
.background(
Color.galleryIndexViewBackground.cornerRadius(16.0)
)
}
}

#Preview("Multiple URLs") {
let urls = [
URL(string: "https://image.nostr.build/77713e6c2ef5186d516a6f16fb197fca53a20677c6756a9de1afc2d5e6d96548.jpg")!,
URL(string: "https://youtu.be/5qvdbyRH9wA?si=y_KTgLR22nH0-cs8")!,
URL(string: "https://image.nostr.build/d5e38e7d864a344872d922d7f78daf67b0d304932fcb7fe22d35263c2fcf11c2.jpg")!,
URL(string: "https://images.unsplash.com/photo-1715686529501-e097bd9caea7")!,
]
return VStack {
Spacer()
GalleryView(urls: urls)
Spacer()
}
.background(LinearGradient.cardBackground)
}

#Preview("One image URL") {
VStack {
GalleryView(urls: [
URL(
string: "https://image.nostr.build/d5e38e7d864a344872d922d7f78daf67b0d304932fcb7fe22d35263c2fcf11c2.jpg"
)!
])
}
.background(LinearGradient.cardBackground)
}
51 changes: 51 additions & 0 deletions Nos/Views/Components/Media/ImageButton.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import SDWebImageSwiftUI
import SwiftUI

/// A button that's filled entirely with an image and presents an `ImageViewer` when tapped.
struct ImageButton: View {
/// The URL of the image to display as the button label.
let url: URL

/// Whether the image viewer is presented or not.
@State private var isViewerPresented = false

var body: some View {
Button {
isViewerPresented = true
} label: {
WebImage(url: url)
.resizable()
.indicator(.activity)
.aspectRatio(contentMode: .fill)
.clipped()
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
.sheet(isPresented: $isViewerPresented) {
ImageViewer(url: url)
}
}
}

#Preview {
ImageButton(
url: URL(
string: "https://images.unsplash.com/photo-1723160004469-1b34c81272f3"
)!
)
}

#Preview {
ImageButton(
url: URL(
string: "https://images.unsplash.com/photo-1715686529501-e097bd9caea7"
)!
)
}

#Preview {
ImageButton(
url: URL(
string: "https://image.nostr.build/9640e78f03afc4927d80a15fd1c4bd1404dc654a8663efb92cc9ee1b8b0719a3.jpg"
)!
)
}
Loading
Loading