Skip to content

Add shared profile viewer with deep link routing#4385

Open
dcamozzi wants to merge 10 commits into
trunkfrom
share-profile
Open

Add shared profile viewer with deep link routing#4385
dcamozzi wants to merge 10 commits into
trunkfrom
share-profile

Conversation

@dcamozzi

@dcamozzi dcamozzi commented May 21, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Adds SharedProfileView and SharedProfileViewModel — the in-app viewer for when someone opens a shared profile link (pca.st/user/<slug>)
  • Wires up universal link routing in AppDelegate+SiriShortcuts.swift to intercept /user/<slug> paths and present the viewer modally via PCHostingController
  • Followed podcasts displayed as horizontal scrolling cards (150px) with artwork overlay subscribe buttons matching the discover screen style (circular veil-background buttons)
  • Subscribe buttons support both follow and unfollow with haptic feedback and scale animation
  • Recent episodes section with custom play button matching MainEpisodeActionView visual style
  • Tapping a podcast or episode presents its detail page on top of the shared profile modal (modal stays open underneath)
  • Presented detail pages include a close button (standard "cancel" asset) in the top-left toolbar
  • Each section has a "Show All" drill-down via NavigationStack
  • Gated behind FeatureFlag.shareProfile
  • View model currently uses mock data (TODO: replace with real API call once backend endpoint is ready)

Demo and Designs

View.Shared.Profile.iOS.mov
image image

Test plan

  • Run xcrun simctl openurl booted "https://pca.st/user/dom" — SharedProfileView should present modally with mock data
  • Verify X button dismisses the modal
  • Verify podcast cards scroll horizontally and show artwork with subscribe overlay buttons
  • Tap subscribe button on a podcast card — should follow and show checkmark; tap again to unfollow
  • Tap a podcast card — should present podcast detail page on top of modal with close button in top-left
  • Tap close button on podcast detail — should dismiss back to shared profile
  • Tap an episode row — should present episode detail page on top of modal with close button
  • Tap play on an episode row — should load and stream the episode
  • Tap "Show All" on podcasts/episodes to verify drill-down navigation
  • Verify back button works on drill-down views
  • On "All Podcasts" page, verify subscribe buttons toggle between follow/unfollow
  • Verify the view does not appear when FeatureFlag.shareProfile is disabled
  • Verify existing share links (podcast/episode) still work correctly

🤖 Generated with Claude Code

Scaffold the in-app view for viewing someone else's shared profile,
presented modally when opening a pca.st/user/<slug> universal link.
Includes profile header, followed podcasts, and recent episodes
sections with AsyncImage artwork loading and "Show All" drill-downs.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@dcamozzi dcamozzi requested a review from a team as a code owner May 21, 2026 00:31
@dcamozzi dcamozzi requested review from SergioEstevao and removed request for a team May 21, 2026 00:31
@dangermattic

dangermattic commented May 21, 2026

Copy link
Copy Markdown
Collaborator
1 Warning
⚠️ This PR is larger than 500 lines of changes. Please consider splitting it into smaller PRs for easier and faster reviews.

Generated by 🚫 Danger

Podcast rows now show a "+" button to subscribe (or checkmark if already
subscribed). Episode rows show a play button that loads the podcast and
streams the episode.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@dcamozzi dcamozzi self-assigned this May 21, 2026
@dcamozzi dcamozzi marked this pull request as draft May 21, 2026 01:40
dcamozzi and others added 2 commits May 20, 2026 19:48
- Horizontal scrolling podcast cards with artwork overlay subscribe buttons
- Tappable podcasts and episodes that present detail pages on top of modal
- Close button on presented detail pages using standard cancel asset
- Custom play button and bidirectional follow/unfollow support
- Refined spacing and layout to match discover screen patterns

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add accessibility label to episode play button
- Fix stale subscribe button state by observing podcastAdded/podcastDeleted notifications
- Show podcast title in episode rows (e.g. "Only a Game · 40m")
- Fix placeholder circle visibility with systemGray5/systemGray2
- Add comment explaining forced view load in addCloseButton
- Remove unnecessary [self] capture in struct closure

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@dcamozzi dcamozzi marked this pull request as ready for review May 21, 2026 03:15
@dcamozzi dcamozzi added this to the Future milestone May 21, 2026
navigateToPodcast(uuid: podcast.uuid)
} label: {
VStack(alignment: .leading, spacing: 0) {
podcastArtwork(url: podcast.artworkURL, size: cardWidth)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While this work we have an existing PodcastImage component that can will display the podcast image based on the uuid of the podcast. He has advantage of supporting placeholder images and cache across the system

navigateToPodcast(uuid: podcast.uuid)
} label: {
HStack(spacing: 12) {
podcastArtwork(url: podcast.artworkURL, size: 52)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PodcastImage can also be used here

}

@ViewBuilder
private func allEpisodesView() -> some View {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be split to a separate file and component to show All Episodes, ex: SharedProfileEpisodeList

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 95b6693

// MARK: - Full List Views

@ViewBuilder
private func allPodcastsView() -> some View {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be split to a separate file and component to show All Podcasts, ex: SharedProfilePodcastsList

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in d301252

}

private extension UIViewController {
@objc func dismissAnimated() {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This extension method feels redudant why not just call the method above directly?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 8ea6da1 — replaced with UIAction-based UIBarButtonItem to eliminate the extension.

}
}

private struct SharedProfileSubscribeButton: View {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should go to a separate file. Ex: ShareProfileSubscribeButton

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in e3e0bd0 — moved SharedProfileSubscribeButton, EpisodePlayButton, PlayTriangle, and ScaleButtonStyle to SharedProfileComponents.swift.

Button {
viewModel.playEpisode(uuid: episode.uuid, podcastUuid: episode.podcastUuid)
} label: {
EpisodePlayButton()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should the play button change display after you press play? show a pause state? Will you be able to pause directly here?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good question — I think for this initial version, keeping it as a simple play button makes sense since we're just launching playback from a shared profile context. We can add pause state tracking in a follow-up once we wire up the real playback state observation.

@SergioEstevao SergioEstevao left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In general the functionality is working with the mock data.
I left some questions regarding code organization and about the play button states.
Tell me if you want to address it on this PR or do a follow up.

dcamozzi and others added 5 commits May 21, 2026 16:38
Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude <noreply@anthropic.com>
Use UIAction-based UIBarButtonItem instead of target-action pattern,
eliminating the need for a UIViewController extension.

Co-Authored-By: Claude <noreply@anthropic.com>
Extracts SharedProfileSubscribeButton, EpisodePlayButton, PlayTriangle,
and ScaleButtonStyle to a dedicated file.

Co-Authored-By: Claude <noreply@anthropic.com>
@dcamozzi

Copy link
Copy Markdown
Contributor Author

@SergioEstevao Re: using PodcastImage instead of AsyncImage for podcast card artwork and episode row artwork — Done in 669a59a

@dcamozzi

Copy link
Copy Markdown
Contributor Author

Review feedback addressed

  • SharedProfileView.swift (@SergioEstevao): Use PodcastImage instead of AsyncImage for podcast card artwork — Done in 669a59a
  • SharedProfileView.swift (@SergioEstevao): Use PodcastImage instead of AsyncImage for episode row artwork — Done in 669a59a
  • SharedProfileView.swift (@SergioEstevao): Extract allEpisodesView to separate file SharedProfileEpisodeList — Done in 95b6693
  • SharedProfileView.swift (@SergioEstevao): Extract allPodcastsView to separate file SharedProfilePodcastsList — Done in d301252
  • SharedProfileView.swift (@SergioEstevao): Remove redundant dismissAnimated extension — Done in 8ea6da1
  • SharedProfileView.swift (@SergioEstevao): Move SharedProfileSubscribeButton to separate file — Done in e3e0bd0
  • SharedProfileView.swift (@SergioEstevao): Should play button show pause state? — Not changed; keeping as play-only for initial version, will add playback state observation in a follow-up

@dcamozzi dcamozzi requested a review from SergioEstevao May 21, 2026 23:50
@pocketcasts

Copy link
Copy Markdown
Contributor
App Icon📲 You can test the changes from this Pull Request in Pocket Casts Prototype Build by scanning the QR code below to install the corresponding build.
App NamePocket Casts Prototype Build
Build Number15138
VersionPR #4385
Bundle IDau.com.shiftyjelly.podcasts.prototype
Commite3e0bd0
Installation URL64u5s877s8q48
Automatticians: You can use our internal self-serve MC tool to give yourself access to those builds if needed.

@SergioEstevao SergioEstevao left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code looking much better! :shipit:

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants