Skip to content

feat!: Add map sharing API#143

Merged
gmaclennan merged 58 commits intomainfrom
feat/map-sharing
Feb 26, 2026
Merged

feat!: Add map sharing API#143
gmaclennan merged 58 commits intomainfrom
feat/map-sharing

Conversation

@gmaclennan
Copy link
Copy Markdown
Member

@gmaclennan gmaclennan commented Nov 25, 2025

Map Sharing Hooks

Adds React hooks for sharing maps between devices. The API separates receiver and sender functionality.

Receiver Hooks

  • useManyReceivedMapShares() - List all received map shares. Updates when shares arrive or status changes (but not during download progress updates).
  • useSingleReceivedMapShare({ shareId }) - Get a specific received share with real-time updates including download progress.
  • useDownloadReceivedMapShare() - Accept and start downloading a share. Resolves when download starts, not when complete.
  • useDeclineReceivedMapShare() - Decline a pending share. Notifies the sender.
  • useAbortReceivedMapShareDownload() - Cancel an in-progress download.

Sender Hooks

  • useSendMapShare({ projectId }) - Send a map to another device. Resolves immediately with the share object (doesn't wait for recipient response).
  • useSingleSentMapShare({ shareId }) - Track a sent share's status in real-time (pending, downloading, declined, canceled).
  • useCancelSentMapShare() - Cancel a sent share before the recipient completes the download.

Example Usage

// Receiving a map share
function ReceivedShareScreen({ shareId }: { shareId: string }) {
  const share = useSingleReceivedMapShare({ shareId })
  const { mutate: download } = useDownloadReceivedMapShare()
  const { mutate: decline } = useDeclineReceivedMapShare()

  if (share.status === 'downloading') {
    return <div>Downloading: {share.downloadProgress}%</div>
  }

  return (
    <div>
      <div>{share.mapName} from {share.senderDeviceName}</div>
      <button onClick={() => download({ shareId })}>Accept</button>
      <button onClick={() => decline({ shareId, reason: 'user_rejected' })}>Decline</button>
    </div>
  )
}

// Sending a map share
function SendMapScreen({ projectId, deviceId }: { projectId: string; deviceId: string }) {
  const [shareId, setShareId] = useState<string | null>(null)
  const { mutate: send } = useSendMapShare({ projectId })

  const handleSend = () => {
    send(
      { receiverDeviceId: deviceId, mapId: 'custom' },
      { onSuccess: (share) => setShareId(share.shareId) }
    )
  }

  if (shareId) {
    return <SentShareStatus shareId={shareId} />
  }

  return <button onClick={handleSend}>Send Map</button>
}

function SentShareStatus({ shareId }: { shareId: string }) {
  const share = useSingleSentMapShare({ shareId })

  return (
    <div>
      {share.status === 'pending' && <div>Waiting for recipient...</div>}
      {share.status === 'downloading' && <div>Recipient downloading: {share.downloadProgress}%</div>}
      {share.status === 'completed' && <div>Map shared successfully</div>}
      {share.status === 'declined' && <div>Recipient declined</div>}
    </div>
  )
}

@gmaclennan gmaclennan self-assigned this Nov 25, 2025
@gmaclennan
Copy link
Copy Markdown
Member Author

I think maybe the useSendMapShare() API needs work - let me know @ErikSin & @cimigree how you are planning to implement this from the sender's side and we can maybe adjust this API so it works for that.

Copy link
Copy Markdown
Contributor

@ErikSin ErikSin left a comment

Choose a reason for hiding this comment

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

There are a few event emitters/listeners needed.

  1. The ui is designed to constantly be listening in the background and notify the user when a map has been shared. So there needs to be some listener function that invalidates the useManyMapShares function when a map get shared that lives at the top of the app
  2. Similar to invites, the person receiving the map needs to also listen in the background for when the person sending the map has cancelled sending the map. This has to be listener because it can happen at any time during the process.
  3. As mentioned in the comments, if we want to show progress of the map being downloaded, the map sharing function needs to be an event emitter. As it is now, there is no way to show the progress in the ui as it an async function that will just return one object when resolved at the end.

Copy link
Copy Markdown
Contributor

@cimigree cimigree left a comment

Choose a reason for hiding this comment

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

I agree with what Erik said. Thanks for the thorough review, Erik. I added a few things that I noticed as well.

@cimigree
Copy link
Copy Markdown
Contributor

cimigree commented Dec 9, 2025

Hi, @gmaclennan
I tried using the tgz file but found a few issues that I think need code changes in the PR before they are ready to really use.

  1. The map share hooks (useSendMapShare, useRequestCancelMapShare, useManyMapShares, useSingleMapShare, useAcceptMapShare, useRejectMapShare) exist in src/hooks/maps.ts but aren't exported from src/index.ts - only useMapStyleUrl is exported. Could you add them to the main index exports?
  2. Missing useSetUpInvitesListeners: This hooks seems to have disappeared? And that is causing a crash. I can't remember why that is and what I should do about it. It's not in any changes in here...
  3. Naming correction: The tgz file I have still has the old useRequestCancelInvite name.
    Once these are fixed, could you generate an updated tgz? For now, I'm importing the hooks directly from dist/esm/hooks/maps.js with a @ts-expect-error comment to test the UI. Thanks! Or please advise if there is anything I should be doing differently.

@achou11
Copy link
Copy Markdown
Member

achou11 commented Dec 9, 2025

@cimigree

1. The map share hooks (useSendMapShare, useRequestCancelMapShare, useManyMapShares, useSingleMapShare, useAcceptMapShare, useRejectMapShare) exist in src/hooks/maps.ts but aren't exported from src/index.ts - only useMapStyleUrl is exported. Could you add them to the main index exports?

I can update this branch to export those hooks!

2. Missing useSetUpInvitesListeners: This hooks seems to have disappeared? And that is causing a crash. I can't remember why that is and what I should do about it. It's not in any changes in here...

Yes this is branch is based off of main, which includes the changes from #141 . You no longer need to use that hook as it's done automatically when setting up the api client provider.

3. Naming correction: The tgz file I have still has the old useRequestCancelInvite name.
   Once these are fixed, could you generate an updated tgz? For now, I'm importing the hooks directly from dist/esm/hooks/maps.js with a @ts-expect-error comment to test the UI. Thanks! Or please advise if there is anything I should be doing differently.

Can fix this for you. Also worth noting that Gregor included instructions about generating the tarball yourself in the PR description, if you want to take it into your own hands. EDIT: Think I was confusing this for a different conversation I was having with Erik where I instructed him on how to generate it. In a nutshell:

  1. Clone this branch
  2. Run npm pack
  3. Use the generated tarball file based on the instructions Gregor wrote in the PR description

@achou11
Copy link
Copy Markdown
Member

achou11 commented Dec 9, 2025

@cimigree Updated the package exports via 371e486.

Shared a tarball with you on Slack, but if you need/prefer to generate your own, you can follow the steps I outlined in my previous comment

@gmaclennan
Copy link
Copy Markdown
Member Author

I didn't catch this today until now, but thank you @achou11 for responding to these questions. I think @cimigree you have what you need for now, but let me know otherwise.

@gmaclennan
Copy link
Copy Markdown
Member Author

I will try and re-visit this API tomorrow now that I've got further into the implementation. We will actually need two similar APIs I think for managing the map shares from the sender side and from the receiver side. They are subtly different and I think this current API design maybe conflates the two. It could be possible to make this current API design work for both I guess - I'll look into this.

@gmaclennan
Copy link
Copy Markdown
Member Author

@cimigree & @ErikSin : So after implementing this, I realize there is a slight confusion with this API with the distinction between sent map shares and received map shares, which I imagine you have encountered when you are implementing this? For the API we have a couple of options:

  1. We keep the current API, but add a discriminator on the MapShare objects, so that you can filter by it to get only sent or received map shares as needed.
  2. We have separate APIs for sent and received map shares, e.g. useSentMapShares() and useReceivedMapShares().

Which do you think would be preferable to work with? I am thinking (2) is better because you there would never be a use-case for reading both sent and received map shares at the same time, and it would complicate the effects (e.g. showing the share offer screen) when the value updates.

Copy link
Copy Markdown
Member

@achou11 achou11 left a comment

Choose a reason for hiding this comment

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

A few valid issues in CI that need to be fixed but overall the changes and their tests read well!

The PR title should be updated to categorize this as a breaking change.

Comment on lines -32 to -36
export function useMapStyleUrl({
refreshToken,
}: {
refreshToken?: string
} = {}) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

probably should update the PR title to reflect the existence of a breaking change

@@ -1,4 +1,4 @@
export { ClientApiProvider } from './contexts/ClientApi.js'
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Yeah definitely worth marking this PR as a breaking change.

@cimigree
Copy link
Copy Markdown
Contributor

@gmaclennan Is it to be expected that the new maps you added come in as default.smp? With no name? Just checking...

Copy link
Copy Markdown
Member

@achou11 achou11 left a comment

Choose a reason for hiding this comment

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

Couple of non-blocking suggestions. Otherwise implementation generally looks sound! Relying on @cimigree to give some feedback as to whether it fulfills app-usage needs.

Also please update the PR title to indicate a breaking change.

}

async function monitor(mapShareId: string, path: string) {
// TODO: add a timeout in case the download stalls and never completes
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

just checking - is it okay for this to be added as a follow-up?

Comment on lines +243 to +244
.json<{ downloadId: string }>()
.then(({ downloadId }) => downloadId)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

maybe too much of an edge case, but would feel better if an explicit runtime check for downloadId was done instead of trusting the provided type here.

it'd be helpful to surface if the map server API payload was incorrect before blindly using it.

.post('mapShares', {
json: { receiverDeviceId, mapId },
})
.json<ServerMapShareState>()
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

similar comment about maybe introducing a runtime validation check here. given how verbose a full check could be, maybe at least check that shareId exists since it's used explicitly in lines afterwards?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I wonder if validations here are necessary because this is being deployed together with the map server on the same device and we have the end to end tests for this. When map server to map server communication happens, where versions could differ and types could change, there is validation.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Yeah I guess I'm wondering if the map server dep here is updated and the payload changes, where does that misalignment get surfaced?

Again, pretty edge case since we control that dep but I think it's still technically possible that something like that could happen at runtime.

Copy link
Copy Markdown
Member

@achou11 achou11 Feb 26, 2026

Choose a reason for hiding this comment

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

I guess the assumption is that the type used for the payload in the server and the type used here are the same, which I guess is safe enough in the context of these things being deployed together as you mentioned.

Not adamant about introducing something, just wanted to inquire out of caution!

@gmaclennan gmaclennan changed the title feat: Add map sharing API feat!: Add map sharing API Feb 26, 2026
Copy link
Copy Markdown
Contributor

@cimigree cimigree left a comment

Choose a reason for hiding this comment

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

All tested great on my devices running from the frontend. I still get this error when adding and removing maps. I don't think it has anything to do with your code. And I can dismiss it and all works fine... Just noting here:
This was with the 213 MB map file, openfreemap-z5.smp
ERROR Mapbox [error] RNMBXMapView | Map load failed: {type=source, message=Source mapboxUserLocation is not in style, begin=180950462069}

@gmaclennan gmaclennan merged commit d200115 into main Feb 26, 2026
9 checks passed
@gmaclennan gmaclennan deleted the feat/map-sharing branch February 26, 2026 21:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Create map share API in @comapeo/core-react

4 participants