From 114dde7883f79f3e6e0bbd74e64cab3fbfcdf06e Mon Sep 17 00:00:00 2001 From: ericholguin Date: Mon, 17 Nov 2025 20:50:36 -0700 Subject: [PATCH] ui: Improved Load Media UI This PR improves the load media UI when a user has media previews off. Changelog-Changed: Changed load media UI Signed-off-by: ericholguin --- damus/Features/Events/NoteContentView.swift | 219 ++++++++++++++---- damus/en-US.lproj/Localizable.stringsdict | 16 ++ .../damus/en-US.lproj/Localizable.stringsdict | 16 ++ damusTests/LocalizationUtilTests.swift | 3 +- 4 files changed, 206 insertions(+), 48 deletions(-) diff --git a/damus/Features/Events/NoteContentView.swift b/damus/Features/Events/NoteContentView.swift index 557b057a..338543fb 100644 --- a/damus/Features/Events/NoteContentView.swift +++ b/damus/Features/Events/NoteContentView.swift @@ -46,6 +46,7 @@ struct NoteContentView: View { let event: NostrEvent @State var blur_images: Bool @State var load_media: Bool = false + @State private var showLinksDropdown = false let size: EventViewKind let preview_height: CGFloat? let options: EventViewOptions @@ -173,28 +174,30 @@ struct NoteContentView: View { let contentToRender = highlightedContent(artifacts.content) return VStack(alignment: .leading) { - if size == .selected { - if with_padding { - SelectableText(damus_state: damus_state, event: self.event, attributedString: contentToRender.attributed, size: self.size) - .padding(.horizontal) + if artifacts.content.attributed.characters.count != 0 { + if size == .selected { + if with_padding { + SelectableText(damus_state: damus_state, event: self.event, attributedString: contentToRender.attributed, size: self.size) + .padding(.horizontal) + } else { + SelectableText(damus_state: damus_state, event: self.event, attributedString: contentToRender.attributed, size: self.size) + } } else { - SelectableText(damus_state: damus_state, event: self.event, attributedString: contentToRender.attributed, size: self.size) + if with_padding { + truncatedText(content: contentToRender) + .padding(.horizontal) + } else { + truncatedText(content: contentToRender) + } } - } else { - if with_padding { - truncatedText(content: contentToRender) - .padding(.horizontal) - } else { - truncatedText(content: contentToRender) - } - } - if !options.contains(.no_translate) && (size == .selected || TranslationService.isAppleTranslationPopoverSupported || damus_state.settings.auto_translate) { - if with_padding { - translateView - .padding(.horizontal) - } else { - translateView + if !options.contains(.no_translate) && (size == .selected || TranslationService.isAppleTranslationPopoverSupported || damus_state.settings.auto_translate) { + if with_padding { + translateView + .padding(.horizontal) + } else { + translateView + } } } @@ -235,46 +238,111 @@ struct NoteContentView: View { } } + .padding(.top, artifacts.content.attributed.characters.count == 0 ? 7 : 0) } var has_previews: Bool { !options.contains(.no_previews) } - + func loadMediaButton(artifacts: NoteArtifactsSeparated) -> some View { - Button(action: { - load_media = true - }, label: { - VStack(alignment: .leading) { - HStack { - Image("images") - Text("Load media", comment: "Button to show media in note.") - .fontWeight(.bold) - .font(eventviewsize_to_font(size, font_size: damus_state.settings.font_size)) - } - .padding(EdgeInsets(top: 5, leading: 10, bottom: 0, trailing: 10)) + VStack(spacing: 0) { + HStack(spacing: 0) { - ForEach(artifacts.media.indices, id: \.self) { index in - Divider() - .frame(height: 1) - switch artifacts.media[index] { - case .image(let url), .video(let url): - Text(abbreviateURL(url)) + Button(action: { + load_media = true + }) { + HStack(spacing: 10) { + ZStack(alignment: .topTrailing) { + Image("images") + .foregroundStyle(DamusColors.neutral6) + .accessibilityHidden(true) + + if artifacts.media.count > 1 { + Text("\(artifacts.media.count)") + .font(.system(size: 10, weight: .semibold)) + .foregroundStyle(.white) + .padding(.horizontal, 4) + .padding(.vertical, 2) + .background( + Capsule() + .fill(DamusColors.neutral6) + ) + .offset(x: 6, y: -6) + .accessibilityHidden(true) + } + } + + Text("Load \(artifacts.media.count) \(pluralizedString(key: "media_count", count: artifacts.media.count))") .font(eventviewsize_to_font(size, font_size: damus_state.settings.font_size)) .foregroundStyle(DamusColors.neutral6) - .multilineTextAlignment(.leading) - .padding(EdgeInsets(top: 0, leading: 10, bottom: 5, trailing: 10)) + + Spacer() + + } + .padding(.vertical, 12) + .padding(.leading, 14) + .padding(.trailing, 8) + .contentShape(Rectangle()) + } + .buttonStyle(PlainButtonStyle()) + + Rectangle() + .fill(DamusColors.neutral3) + .frame(width: 1) + .padding(.vertical, 8) + + Button(action: { + withAnimation(.spring(response: 0.3, dampingFraction: 0.8)) { + showLinksDropdown.toggle() + } + }) { + Image(systemName: showLinksDropdown ? "chevron.up.circle.fill" : "chevron.down.circle") + .font(.system(size: 16)) + .foregroundStyle(DamusColors.neutral6) + .frame(width: 44, height: 44) + .contentShape(Rectangle()) + } + .buttonStyle(PlainButtonStyle()) + .accessibilityLabel(NSLocalizedString(showLinksDropdown ? "Hide media links" : "Show media links", comment: "Accessibility label for toggle button to show/hide media link list")) + } + .background( + RoundedRectangle(cornerRadius: 10) + .fill(DamusColors.neutral1.opacity(0.6)) + .overlay( + RoundedRectangle(cornerRadius: 10) + .stroke(DamusColors.neutral3, lineWidth: 1) + ) + .shadow(color: .black.opacity(0.05), radius: 2, y: 1) + ) + + if showLinksDropdown { + VStack(alignment: .leading, spacing: 0) { + ForEach(Array(artifacts.media.enumerated()), id: \.offset) { index, mediaItem in + if index > 0 { + Divider() + .background(DamusColors.neutral3) + } + + mediaLinkRow(for: mediaItem, at: index) } } + .background( + RoundedRectangle(cornerRadius: 10) + .fill(DamusColors.neutral1.opacity(0.4)) + .overlay( + RoundedRectangle(cornerRadius: 10) + .stroke(DamusColors.neutral3, lineWidth: 1) + ) + .shadow(color: .black.opacity(0.05), radius: 2, y: 1) + ) + .padding(.top, 6) + .transition(.asymmetric( + insertion: .opacity.combined(with: .scale(scale: 0.95, anchor: .top)), + removal: .opacity + )) } - .background(DamusColors.neutral1) - .frame(minWidth: nil, maxWidth: .infinity, alignment: .center) - .cornerRadius(8) - .overlay( - RoundedRectangle(cornerRadius: 8) - .stroke(DamusColors.neutral3, lineWidth: 1) - ) - }) + } .padding(.horizontal) } @@ -303,6 +371,63 @@ struct NoteContentView: View { } } + @ViewBuilder + private func mediaLinkRow(for mediaItem: MediaUrl, at index: Int) -> some View { + switch mediaItem { + case .image(let url), .video(let url): + Button(action: { + load_media = true + }) { + HStack(spacing: 10) { + + Image(systemName: "photo.circle.fill") + .font(.system(size: 14)) + .foregroundStyle(DamusColors.neutral6) + .accessibilityHidden(true) + + VStack(alignment: .leading, spacing: 2) { + Text(abbreviateURL(url)) + .font(.system(size: 13)) + .foregroundStyle(DamusColors.neutral6) + .lineLimit(1) + .truncationMode(.middle) + + if let domain = url.host { + Text(domain) + .font(.system(size: 11)) + .foregroundStyle(DamusColors.neutral6) + } + } + + Spacer(minLength: 8) + + HStack(spacing: 12) { + Button(action: { + UIPasteboard.general.string = url.absoluteString + let impactFeedback = UIImpactFeedbackGenerator(style: .light) + impactFeedback.impactOccurred() + }) { + Image(systemName: "doc.on.doc") + .font(.system(size: 14)) + .foregroundStyle(DamusColors.neutral6) + } + .buttonStyle(PlainButtonStyle()) + .accessibilityLabel(NSLocalizedString("Copy media link", comment: "Accessibility label for copy media link button")) + } + } + .padding(.horizontal, 14) + .padding(.vertical, 10) + .background( + RoundedRectangle(cornerRadius: 8) + .fill(Color.clear) + .contentShape(Rectangle()) + ) + } + .buttonStyle(PlainButtonStyle()) + .accessibilityLabel(NSLocalizedString("Load \(abbreviateURL(url))", comment: "Accessibility label for button to load specific media item")) + } + } + func load(force_artifacts: Bool = false) { if case .loading = damus_state.events.get_cache_data(event.id).artifacts_model.state { return diff --git a/damus/en-US.lproj/Localizable.stringsdict b/damus/en-US.lproj/Localizable.stringsdict index 9622c41c..01ee1f8c 100644 --- a/damus/en-US.lproj/Localizable.stringsdict +++ b/damus/en-US.lproj/Localizable.stringsdict @@ -2,6 +2,22 @@ + media_count + + NSStringLocalizedFormatKey + %#@MEDIA@ + MEDIA + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + d + one + media item + other + media items + + viewer_count NSStringLocalizedFormatKey diff --git a/damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.stringsdict b/damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.stringsdict index a436807b..4af9b9a6 100644 --- a/damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.stringsdict +++ b/damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.stringsdict @@ -98,6 +98,22 @@ Imports + media_count + + NSStringLocalizedFormatKey + %#@MEDIA@ + MEDIA + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + d + one + media item + other + media items + + notes_from_three_and_others NSStringLocalizedFormatKey diff --git a/damusTests/LocalizationUtilTests.swift b/damusTests/LocalizationUtilTests.swift index 2ad36c90..0dc63c4c 100644 --- a/damusTests/LocalizationUtilTests.swift +++ b/damusTests/LocalizationUtilTests.swift @@ -27,7 +27,8 @@ final class LocalizationUtilTests: XCTestCase { ["sats", "sats", "sat", "sats"], ["users_talking_about_it", "0 users talking about it", "1 user talking about it", "2 users talking about it"], ["word_count", "0 Words", "1 Word", "2 Words"], - ["zaps_count", "Zaps", "Zap", "Zaps"] + ["zaps_count", "Zaps", "Zap", "Zaps"], + ["media_count", "media items", "media item", "media items"] ] for key in keys {