Mobile notifications for iOS (#210)

* mobile: wire notification registration and listener

* implement backend components for push notifications

* refactor: agentic comment cleanup

* docs: use proper module name for particle processor

* set required env variables for push notifications

* bump version

* fix: always upsert push token on mobile start

* Revert "fix: always upsert push token on mobile start"

This reverts commit 90ff18a788.

* send push notifications regardless of online status
This commit was merged in pull request #210.
This commit is contained in:
Arjun Patel
2026-05-18 12:44:31 -07:00
committed by GitHub
parent a564ea819b
commit d262f734f0
61 changed files with 1682 additions and 531 deletions
+7 -10
View File
@@ -10,20 +10,19 @@ import (
var ErrUnauthorized = errors.New("unauthorized")
// Authorizer validates whether a user can access a given channel.
type Authorizer struct {
networkReader network.Reader
}
// NewAuthorizer creates a new channel authorizer.
func NewAuthorizer(networkReader network.Reader) *Authorizer {
return &Authorizer{networkReader: networkReader}
}
// Authorize checks if the given humanID is allowed to subscribe to the channel.
// Channel formats:
// - network:{networkId}
// - stream:{networkId}:{streamId}
// Authorize accepts channel IDs of the form:
//
// network:{networkId}
// stream:{networkId}:{streamId}
// _presence:{humanId}
func (a *Authorizer) Authorize(ctx context.Context, channelID, humanID string) error {
parts := strings.SplitN(channelID, ":", 2)
if len(parts) < 2 {
@@ -39,8 +38,7 @@ func (a *Authorizer) Authorize(ctx context.Context, channelID, humanID string) e
case "stream":
return a.authorizeStream(ctx, rest, humanID)
case "_presence":
// Always allowed — used for global online presence tracking.
// The channel ID is _presence:{humanId}, so verify the humanId matches.
// Only the owning human may subscribe to their presence channel.
if rest != humanID {
return ErrUnauthorized
}
@@ -61,8 +59,7 @@ func (a *Authorizer) authorizeNetwork(ctx context.Context, networkID, humanID st
return nil
}
// authorizeStream expects rest to be "{networkId}:{streamId}".
// We only check network membership — stream visibility is handled by network access.
// rest is "{networkId}:{streamId}"; stream-level visibility is enforced by network access.
func (a *Authorizer) authorizeStream(ctx context.Context, rest, humanID string) error {
parts := strings.SplitN(rest, ":", 2)
if len(parts) < 2 {