1
0
mirror of git://jb55.com/damus synced 2024-10-06 19:53:22 +00:00

Merge remote-tracking branch 'oleg/custom-profile-navbar'

Changelog-Added: Improved profile navbar
This commit is contained in:
OlegAba 2023-02-15 12:31:50 -08:00 committed by William Casarin
commit c100c6db47
5 changed files with 306 additions and 256 deletions

View File

@ -127,14 +127,6 @@ struct ImageView: View {
@State private var selectedIndex = 0 @State private var selectedIndex = 0
@State var showMenu = true @State var showMenu = true
var safeAreaInsets: UIEdgeInsets? {
return UIApplication
.shared
.connectedScenes
.flatMap { ($0 as? UIWindowScene)?.windows ?? [] }
.first { $0.isKeyWindow }?.safeAreaInsets
}
var navBarView: some View { var navBarView: some View {
VStack { VStack {
HStack { HStack {
@ -180,8 +172,8 @@ struct ImageView: View {
ZoomableScrollView { ZoomableScrollView {
ImageContainerView(url: urls[index]) ImageContainerView(url: urls[index])
.aspectRatio(contentMode: .fit) .aspectRatio(contentMode: .fit)
.padding(.top, safeAreaInsets?.top) .padding(.top, Theme.safeAreaInsets?.top)
.padding(.bottom, safeAreaInsets?.bottom) .padding(.bottom, Theme.safeAreaInsets?.bottom)
} }
.modifier(SwipeToDismissModifier(minDistance: 50, onDismiss: { .modifier(SwipeToDismissModifier(minDistance: 50, onDismiss: {
presentationMode.wrappedValue.dismiss() presentationMode.wrappedValue.dismiss()
@ -210,7 +202,7 @@ struct ImageView: View {
} }
} }
.animation(.easeInOut, value: showMenu) .animation(.easeInOut, value: showMenu)
.padding(.bottom, safeAreaInsets?.bottom) .padding(.bottom, Theme.safeAreaInsets?.bottom)
) )
} }
} }

View File

@ -25,4 +25,12 @@ class Theme {
UINavigationBar.appearance().tintColor = tintColor ?? titleColor ?? .black UINavigationBar.appearance().tintColor = tintColor ?? titleColor ?? .black
} }
static var safeAreaInsets: UIEdgeInsets? {
return UIApplication
.shared
.connectedScenes
.flatMap { ($0 as? UIWindowScene)?.windows ?? [] }
.first { $0.isKeyWindow }?.safeAreaInsets
}
} }

View File

@ -18,7 +18,7 @@ struct ProfileNameView: View {
var body: some View { var body: some View {
Group { Group {
if let real_name = profile?.display_name { if let real_name = profile?.display_name {
VStack(alignment: .leading) { VStack(alignment: .leading, spacing: 0) {
Text(real_name) Text(real_name)
.font(.title3.weight(.bold)) .font(.title3.weight(.bold))
HStack(alignment: .center, spacing: spacing) { HStack(alignment: .center, spacing: spacing) {
@ -30,6 +30,7 @@ struct ProfileNameView: View {
FollowsYou() FollowsYou()
} }
} }
Spacer()
KeyView(pubkey: pubkey) KeyView(pubkey: pubkey)
.pubkey_context_menu(bech32_pubkey: pubkey) .pubkey_context_menu(bech32_pubkey: pubkey)
} }

View File

@ -80,9 +80,24 @@ struct EditButton: View {
} }
} }
struct VisualEffectView: UIViewRepresentable {
var effect: UIVisualEffect?
func makeUIView(context: UIViewRepresentableContext<Self>) -> UIVisualEffectView {
UIVisualEffectView()
}
func updateUIView(_ uiView: UIVisualEffectView, context: UIViewRepresentableContext<Self>) {
uiView.effect = effect
}
}
struct ProfileView: View { struct ProfileView: View {
let damus_state: DamusState let damus_state: DamusState
let zoom_size: CGFloat = 350 let pfp_size: CGFloat = 90.0
let bannerHeight: CGFloat = 150.0
static let markdown = Markdown()
@State private var selected_tab: ProfileTab = .posts @State private var selected_tab: ProfileTab = .posts
@StateObject var profile: ProfileModel @StateObject var profile: ProfileModel
@ -92,20 +107,12 @@ struct ProfileView: View {
@State var is_zoomed: Bool = false @State var is_zoomed: Bool = false
@State var show_share_sheet: Bool = false @State var show_share_sheet: Bool = false
@State var action_sheet_presented: Bool = false @State var action_sheet_presented: Bool = false
@State var yOffset: CGFloat = 0
@Environment(\.dismiss) var dismiss @Environment(\.dismiss) var dismiss
@Environment(\.colorScheme) var colorScheme @Environment(\.colorScheme) var colorScheme
@Environment(\.openURL) var openURL @Environment(\.openURL) var openURL
@Environment(\.presentationMode) var presentationMode
// We just want to have a white "< Home" text here, however,
// setting the initialiser is causing issues, and it's late.
// Ref: https://blog.techchee.com/navigation-bar-title-style-color-and-custom-back-button-in-swiftui/
/*
init(damus_state: DamusState, zoom_size: CGFloat = 350) {
self.damus_state = damus_state
self.zoom_size = zoom_size
Theme.navigationBarColors(background: nil, titleColor: .white, tintColor: nil)
}*/
func fillColor() -> Color { func fillColor() -> Color {
colorScheme == .light ? Color("DamusLightGrey") : Color("DamusDarkGrey") colorScheme == .light ? Color("DamusLightGrey") : Color("DamusDarkGrey")
@ -115,7 +122,98 @@ struct ProfileView: View {
colorScheme == .light ? Color("DamusWhite") : Color("DamusBlack") colorScheme == .light ? Color("DamusWhite") : Color("DamusBlack")
} }
func LNButton(lnurl: String, profile: Profile) -> some View { func bannerBlurViewOpacity() -> Double {
let progress = -(yOffset + navbarHeight) / 100
return Double(-yOffset > navbarHeight ? progress : 0)
}
var bannerSection: some View {
GeometryReader { proxy -> AnyView in
let minY = proxy.frame(in: .global).minY
DispatchQueue.main.async {
self.yOffset = minY
}
return AnyView(
VStack(spacing: 0) {
ZStack {
BannerImageView(pubkey: profile.pubkey, profiles: damus_state.profiles)
.aspectRatio(contentMode: .fill)
.frame(width: proxy.size.width, height: minY > 0 ? bannerHeight + minY : bannerHeight)
.clipped()
VisualEffectView(effect: UIBlurEffect(style: .systemUltraThinMaterial)).opacity(bannerBlurViewOpacity())
}
Divider().opacity(bannerBlurViewOpacity())
}
.frame(height: minY > 0 ? bannerHeight + minY : nil)
.offset(y: minY > 0 ? -minY : -minY < navbarHeight ? 0 : -minY - navbarHeight)
)
}
.frame(height: bannerHeight)
}
var navbarHeight: CGFloat {
return 100.0 - (Theme.safeAreaInsets?.top ?? 0)
}
@ViewBuilder
func navImage(systemImage: String) -> some View {
Image(systemName: systemImage)
.frame(width: 33, height: 33)
.background(Color.black.opacity(0.6))
.clipShape(Circle())
}
var navBackButton: some View {
Button {
presentationMode.wrappedValue.dismiss()
} label: {
navImage(systemImage: "chevron.left")
}
}
var navActionSheetButton: some View {
Button(action: {
action_sheet_presented = true
}) {
navImage(systemImage: "ellipsis")
}
.confirmationDialog(NSLocalizedString("Actions", comment: "Title for confirmation dialog to either share, report, or block a profile."), isPresented: $action_sheet_presented) {
Button(NSLocalizedString("Share", comment: "Button to share the link to a profile.")) {
show_share_sheet = true
}
// Only allow reporting if logged in with private key and the currently viewed profile is not the logged in profile.
if profile.pubkey != damus_state.pubkey && damus_state.is_privkey_user {
Button(NSLocalizedString("Report", comment: "Button to report a profile."), role: .destructive) {
let target: ReportTarget = .user(profile.pubkey)
notify(.report, target)
}
Button(NSLocalizedString("Block", comment: "Button to block a profile."), role: .destructive) {
notify(.block, profile.pubkey)
}
}
}
}
var customNavbar: some View {
HStack {
navBackButton
Spacer()
navActionSheetButton
}
.padding(.top, 5)
.padding(.horizontal)
.accentColor(Color("DamusWhite"))
}
func lnButton(lnurl: String, profile: Profile) -> some View {
Button(action: { Button(action: {
if damus_state.settings.show_wallet_selector { if damus_state.settings.show_wallet_selector {
showing_select_wallet = true showing_select_wallet = true
@ -140,45 +238,7 @@ struct ProfileView: View {
} }
} }
static let markdown = Markdown() var dmButton: some View {
var ActionSheetButton: some View {
Button(action: {
action_sheet_presented = true
}) {
Image(systemName: "ellipsis.circle")
.profile_button_style(scheme: colorScheme)
}
.confirmationDialog(NSLocalizedString("Actions", comment: "Title for confirmation dialog to either share, report, or block a profile."), isPresented: $action_sheet_presented) {
Button(NSLocalizedString("Share", comment: "Button to share the link to a profile.")) {
show_share_sheet = true
}
// Only allow reporting if logged in with private key and the currently viewed profile is not the logged in profile.
if profile.pubkey != damus_state.pubkey && damus_state.is_privkey_user {
Button(NSLocalizedString("Report", comment: "Button to report a profile."), role: .destructive) {
let target: ReportTarget = .user(profile.pubkey)
notify(.report, target)
}
Button(NSLocalizedString("Block", comment: "Button to block a profile."), role: .destructive) {
notify(.block, profile.pubkey)
}
}
}
}
var ShareButton: some View {
Button(action: {
show_share_sheet = true
}) {
Image(systemName: "square.and.arrow.up.circle")
.profile_button_style(scheme: colorScheme)
}
}
var DMButton: some View {
let dm_model = damus_state.dms.lookup_or_create(profile.pubkey) let dm_model = damus_state.dms.lookup_or_create(profile.pubkey)
let dmview = DMChatView(damus_state: damus_state, pubkey: profile.pubkey) let dmview = DMChatView(damus_state: damus_state, pubkey: profile.pubkey)
.environmentObject(dm_model) .environmentObject(dm_model)
@ -188,43 +248,16 @@ struct ProfileView: View {
} }
} }
private func getScrollOffset(_ geometry: GeometryProxy) -> CGFloat { func actionSection(profile_data: Profile?) -> some View {
geometry.frame(in: .global).minY
}
private func getHeightForHeaderImage(_ geometry: GeometryProxy) -> CGFloat {
let offset = getScrollOffset(geometry)
let imageHeight = 150.0
if offset > 0 {
return imageHeight + offset
}
return imageHeight
}
private func getOffsetForHeaderImage(_ geometry: GeometryProxy) -> CGFloat {
let offset = getScrollOffset(geometry)
// Image was pulled down
if offset > 0 {
return -offset
}
return 0
}
func ActionSection(profile_data: Profile?) -> some View {
return Group { return Group {
ActionSheetButton
if let profile = profile_data { if let profile = profile_data {
if let lnurl = profile.lnurl, lnurl != "" { if let lnurl = profile.lnurl, lnurl != "" {
LNButton(lnurl: lnurl, profile: profile) lnButton(lnurl: lnurl, profile: profile)
} }
} }
DMButton dmButton
if profile.pubkey != damus_state.pubkey { if profile.pubkey != damus_state.pubkey {
FollowButtonView( FollowButtonView(
@ -241,49 +274,60 @@ struct ProfileView: View {
} }
} }
func NameSection(profile_data: Profile?) -> some View { func pfpOffset() -> CGFloat {
let progress = -yOffset / navbarHeight
let offset = (pfp_size / 4.0) * (progress < 1.0 ? progress : 1)
return offset > 0 ? offset : 0
}
func pfpScale() -> CGFloat {
let progress = -yOffset / navbarHeight
let scale = 1.0 - (0.5 * (progress < 1.0 ? progress : 1))
return scale < 1 ? scale : 1
}
func nameSection(profile_data: Profile?) -> some View {
return Group { return Group {
HStack(alignment: .center) { HStack(alignment: .center) {
ProfilePicView(pubkey: profile.pubkey, size: pfp_size, highlight: .custom(imageBorderColor(), 4.0), profiles: damus_state.profiles) ProfilePicView(pubkey: profile.pubkey, size: pfp_size, highlight: .custom(imageBorderColor(), 4.0), profiles: damus_state.profiles)
.padding(.top, -(pfp_size / 2.0))
.offset(y: pfpOffset())
.scaleEffect(pfpScale())
.onTapGesture { .onTapGesture {
is_zoomed.toggle() is_zoomed.toggle()
} }
.fullScreenCover(isPresented: $is_zoomed) { .fullScreenCover(isPresented: $is_zoomed) {
ProfileZoomView(pubkey: profile.pubkey, profiles: damus_state.profiles) } ProfileZoomView(pubkey: profile.pubkey, profiles: damus_state.profiles) }
.offset(y: -(pfp_size/2.0)) // Increase if set a frame
Spacer() Spacer()
ActionSection(profile_data: profile_data) actionSection(profile_data: profile_data)
.offset(y: -15.0) // Increase if set a frame
} }
let follows_you = profile.follows(pubkey: damus_state.pubkey) let follows_you = profile.follows(pubkey: damus_state.pubkey)
ProfileNameView(pubkey: profile.pubkey, profile: profile_data, follows_you: follows_you, damus: damus_state) ProfileNameView(pubkey: profile.pubkey, profile: profile_data, follows_you: follows_you, damus: damus_state)
//.padding(.bottom)
.padding(.top,-(pfp_size/2.0))
} }
} }
var pfp_size: CGFloat { var followersCount: some View {
return 90.0 HStack {
if followers.count == nil {
Image(systemName: "square.and.arrow.down")
Text("Followers", comment: "Label describing followers of a user.")
.font(.subheadline)
.foregroundColor(.gray)
} else {
let followerCount = followers.count!
Text("\(Text(String("\(followerCount)")).font(.subheadline.weight(.medium))) \(Text(String(format: NSLocalizedString("followers_count", comment: "Part of a larger sentence to describe how many people are following a user."), followerCount)).font(.subheadline).foregroundColor(.gray))", comment: "Sentence composed of 2 variables to describe how many people are following a user. In source English, the first variable is the number of followers, and the second variable is 'Follower' or 'Followers'.")
}
}
} }
var TopSection: some View { var aboutSection: some View {
ZStack(alignment: .top) {
GeometryReader { geometry in
BannerImageView(pubkey: profile.pubkey, profiles: damus_state.profiles)
.aspectRatio(contentMode: .fill)
.frame(width: geometry.size.width, height: self.getHeightForHeaderImage(geometry))
.clipped()
.offset(x: 0, y: self.getOffsetForHeaderImage(geometry))
}.frame(height: BANNER_HEIGHT)
VStack(alignment: .leading, spacing: 8.0) { VStack(alignment: .leading, spacing: 8.0) {
let profile_data = damus_state.profiles.lookup(id: profile.pubkey) let profile_data = damus_state.profiles.lookup(id: profile.pubkey)
NameSection(profile_data: profile_data) nameSection(profile_data: profile_data)
Text(ProfileView.markdown.process(profile_data?.about ?? "")) Text(ProfileView.markdown.process(profile_data?.about ?? ""))
.font(.subheadline).textSelection(.enabled) .font(.subheadline).textSelection(.enabled)
@ -300,7 +344,7 @@ struct ProfileView: View {
let following_model = FollowingModel(damus_state: damus_state, contacts: contacts) let following_model = FollowingModel(damus_state: damus_state, contacts: contacts)
NavigationLink(destination: FollowingView(damus_state: damus_state, following: following_model, whos: profile.pubkey)) { NavigationLink(destination: FollowingView(damus_state: damus_state, following: following_model, whos: profile.pubkey)) {
HStack { HStack {
Text("\(Text(String("\(profile.following)")).font(.subheadline.weight(.medium))) \(Text("Following", comment: "Part of a larger sentence to describe how many profiles a user is following.").font(.subheadline).foregroundColor(.gray))", comment: "Sentence composed of 2 variables to describe how many profiles a user is following. In source English, the first variable is the number of profiles being followed, and the second variable is 'Following'.") Text("\(Text("\(profile.following)", comment: "Number of profiles a user is following.").font(.subheadline.weight(.medium))) \(Text("Following", comment: "Part of a larger sentence to describe how many profiles a user is following.").font(.subheadline).foregroundColor(.gray))", comment: "Sentence composed of 2 variables to describe how many profiles a user is following. In source English, the first variable is the number of profiles being followed, and the second variable is 'Following'.")
} }
} }
.buttonStyle(PlainButtonStyle()) .buttonStyle(PlainButtonStyle())
@ -309,11 +353,11 @@ struct ProfileView: View {
.environmentObject(followers) .environmentObject(followers)
if followers.contacts != nil { if followers.contacts != nil {
NavigationLink(destination: fview) { NavigationLink(destination: fview) {
FollowersCount followersCount
} }
.buttonStyle(PlainButtonStyle()) .buttonStyle(PlainButtonStyle())
} else { } else {
FollowersCount followersCount
.onTapGesture { .onTapGesture {
UIImpactFeedbackGenerator(style: .light).impactOccurred() UIImpactFeedbackGenerator(style: .light).impactOccurred()
followers.contacts = [] followers.contacts = []
@ -323,7 +367,7 @@ struct ProfileView: View {
if let relays = profile.relays { if let relays = profile.relays {
// Only open relay config view if the user is logged in with private key and they are looking at their own profile. // Only open relay config view if the user is logged in with private key and they are looking at their own profile.
let relay_text = Text("\(Text(String("\(relays.keys.count)")).font(.subheadline.weight(.medium))) \(Text(String(format: NSLocalizedString("relays_count", comment: "Part of a larger sentence to describe how many relay servers a user is connected."), relays.keys.count)).font(.subheadline).foregroundColor(.gray))", comment: "Sentence composed of 2 variables to describe how many relay servers a user is connected. In source English, the first variable is the number of relay servers, and the second variable is 'Relay' or 'Relays'.") let relay_text = Text("\(Text("\(relays.keys.count)", comment: "Number of relay servers a user is connected.").font(.subheadline.weight(.medium))) \(Text(String(format: NSLocalizedString("relays_count", comment: "Part of a larger sentence to describe how many relay servers a user is connected."), relays.keys.count)).font(.subheadline).foregroundColor(.gray))", comment: "Sentence composed of 2 variables to describe how many relay servers a user is connected. In source English, the first variable is the number of relay servers, and the second variable is 'Relay' or 'Relays'.")
if profile.pubkey == damus_state.pubkey && damus_state.is_privkey_user { if profile.pubkey == damus_state.pubkey && damus_state.is_privkey_user {
NavigationLink(destination: RelayConfigView(state: damus_state)) { NavigationLink(destination: RelayConfigView(state: damus_state)) {
relay_text relay_text
@ -338,39 +382,31 @@ struct ProfileView: View {
} }
} }
} }
.padding(.horizontal,18) .padding(.horizontal)
//.offset(y:120)
.padding(.top,150)
}
}
var FollowersCount: some View {
HStack {
if followers.count == nil {
Image(systemName: "square.and.arrow.down")
Text("Followers", comment: "Label describing followers of a user.")
.font(.subheadline)
.foregroundColor(.gray)
} else {
let followerCount = followers.count!
Text("\(Text(String("\(followerCount)")).font(.subheadline.weight(.medium))) \(Text(String(format: NSLocalizedString("followers_count", comment: "Part of a larger sentence to describe how many people are following a user."), followerCount)).font(.subheadline).foregroundColor(.gray))", comment: "Sentence composed of 2 variables to describe how many people are following a user. In source English, the first variable is the number of followers, and the second variable is 'Follower' or 'Followers'.")
}
}
} }
var body: some View { var body: some View {
VStack(alignment: .leading) { ScrollView(.vertical) {
ScrollView { VStack(spacing: 0) {
TopSection bannerSection
.zIndex(1)
VStack() {
aboutSection
Divider() Divider()
InnerTimelineView(events: $profile.events, damus: damus_state, show_friend_icon: false, filter: { _ in true }) InnerTimelineView(events: $profile.events, damus: damus_state, show_friend_icon: false, filter: { _ in true })
} }
.frame(maxHeight: .infinity, alignment: .topLeading) .padding(.horizontal, Theme.safeAreaInsets?.left)
.zIndex(-yOffset > navbarHeight ? 0 : 1)
} }
.frame(maxWidth: .infinity, alignment: .topLeading) }
.ignoresSafeArea()
.navigationTitle("")
.navigationBarHidden(true)
.overlay(customNavbar, alignment: .top)
.onReceive(handle_notify(.switched_timeline)) { _ in .onReceive(handle_notify(.switched_timeline)) { _ in
dismiss() dismiss()
} }
@ -390,7 +426,6 @@ struct ProfileView: View {
} }
} }
} }
.ignoresSafeArea()
} }
} }
@ -403,7 +438,6 @@ struct ProfileView_Previews: PreviewProvider {
} }
} }
func test_damus_state() -> DamusState { func test_damus_state() -> DamusState {
let pubkey = "3efdaebb1d8923ebd99c9e7ace3b4194ab45512e2be79c1b7d68d9243e0d2681" let pubkey = "3efdaebb1d8923ebd99c9e7ace3b4194ab45512e2be79c1b7d68d9243e0d2681"
let damus = DamusState.empty let damus = DamusState.empty

View File

@ -5,84 +5,99 @@
// Created by scoder1747 on 12/27/22. // Created by scoder1747 on 12/27/22.
// //
import SwiftUI import SwiftUI
import Kingfisher
struct ProfileZoomView: View { private struct ImageContainerView: View {
@Environment(\.presentationMode) var presentationMode @ObservedObject var imageModel: KFImageModel
let pubkey: String
let profiles: Profiles
@GestureState private var scaleState: CGFloat = 1 @State private var image: UIImage?
@GestureState private var offsetState = CGSize.zero @State private var showShareSheet = false
@State private var offset = CGSize.zero init(url: URL?) {
@State private var scale: CGFloat = 1 self.imageModel = KFImageModel(
url: url,
func resetStatus(){ fallbackUrl: nil,
self.offset = CGSize.zero maxByteSize: 2000000, // 2 MB
self.scale = 1 downsampleSize: CGSize(width: 400, height: 400)
)
} }
var zoomGesture: some Gesture { private struct ImageHandler: ImageModifier {
MagnificationGesture() @Binding var handler: UIImage?
.updating($scaleState) { currentState, gestureState, _ in
gestureState = currentState
}
.onEnded { value in
scale *= value
}
}
var dragGesture: some Gesture { func modify(_ image: UIImage) -> UIImage {
DragGesture() handler = image
.updating($offsetState) { currentState, gestureState, _ in return image
gestureState = currentState.translation
}.onEnded { value in
offset.height += value.translation.height
offset.width += value.translation.width
}
}
var doubleTapGesture : some Gesture {
TapGesture(count: 2).onEnded { value in
resetStatus()
} }
} }
var body: some View { var body: some View {
ZStack(alignment: .topLeading) {
Color("DamusDarkGrey") // Or Color("DamusBlack")
.edgesIgnoringSafeArea(.all)
Button { KFAnimatedImage(imageModel.url)
presentationMode.wrappedValue.dismiss() .callbackQueue(.dispatch(.global(qos: .background)))
} label: { .processingQueue(.dispatch(.global(qos: .background)))
Image(systemName: "xmark") .cacheOriginalImage()
.foregroundColor(.white) .configure { view in
.font(.subheadline) view.framePreloadCount = 1
.padding(.leading, 20)
} }
.zIndex(1) .scaleFactor(UIScreen.main.scale)
.loadDiskFileSynchronously()
.fade(duration: 0.1)
.imageModifier(ImageHandler(handler: $image))
.onFailure { _ in
imageModel.downloadFailed()
}
.id(imageModel.refreshID)
.clipShape(Circle())
.modifier(ImageContextMenuModifier(url: imageModel.url, image: image, showShareSheet: $showShareSheet))
.sheet(isPresented: $showShareSheet) {
ShareSheet(activityItems: [imageModel.url])
}
}
}
VStack(alignment: .center) { struct ProfileZoomView: View {
let pubkey: String
let profiles: Profiles
@Environment(\.presentationMode) var presentationMode
var navBarView: some View {
HStack {
Button(action: {
presentationMode.wrappedValue.dismiss()
}, label: {
Image(systemName: "xmark")
.frame(width: 33, height: 33)
.background(.regularMaterial)
.clipShape(Circle())
})
Spacer() Spacer()
}
.padding()
}
ProfilePicView(pubkey: pubkey, size: 200.0, highlight: .none, profiles: profiles) var body: some View {
.padding(100) ZStack {
.scaledToFit() Color(.systemBackground)
.scaleEffect(self.scale * scaleState) .ignoresSafeArea()
.offset(x: offset.width + offsetState.width, y: offset.height + offsetState.height)
.gesture(SimultaneousGesture(zoomGesture, dragGesture)) ZoomableScrollView {
.gesture(doubleTapGesture) ImageContainerView(url: get_profile_url(picture: nil, pubkey: pubkey, profiles: profiles))
.modifier(SwipeToDismissModifier(minDistance: nil, onDismiss: { .aspectRatio(contentMode: .fit)
.padding(.top, Theme.safeAreaInsets?.top)
.padding(.bottom, Theme.safeAreaInsets?.bottom)
.padding(.horizontal)
}
.ignoresSafeArea()
.modifier(SwipeToDismissModifier(minDistance: 50, onDismiss: {
presentationMode.wrappedValue.dismiss() presentationMode.wrappedValue.dismiss()
})) }))
Spacer()
}
} }
.overlay(navBarView, alignment: .top)
} }
} }