Skip to content

Commit

Permalink
feat: Set up detours channels
Browse files Browse the repository at this point in the history
  • Loading branch information
hannahpurcell committed Dec 11, 2024
1 parent 52748fe commit b55e03f
Show file tree
Hide file tree
Showing 4 changed files with 401 additions and 4 deletions.
244 changes: 244 additions & 0 deletions assets/src/hooks/useDetours.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,244 @@
import { Channel, Socket } from "phoenix"
import { SimpleDetour } from "../models/detoursList"
import { useEffect, useState } from "react"
import { reload } from "../models/browser"
import { userUuid } from "../util/userUuid"
import { RouteId } from "../schedule"

interface DetoursMap {
[key: number]: SimpleDetour
}

const subscribe = (
socket: Socket,
topic: string,
initializeChannel: React.Dispatch<React.SetStateAction<DetoursMap>>,
handleDrafted: ((data: SimpleDetour) => void) | undefined,
handleActivated: ((data: SimpleDetour) => void) | undefined,
handleDeactivated: ((data: SimpleDetour) => void) | undefined
): Channel => {
const channel = socket.channel(topic)

handleDrafted &&
channel.on("drafted", ({ data: data }) => handleDrafted(data))
handleActivated &&
channel.on("activated", ({ data: data }) => handleActivated(data))
handleDeactivated &&
channel.on("deactivated", ({ data: data }) => handleDeactivated(data))
channel.on("auth_expired", reload)

channel
.join()
.receive("ok", ({ data: data }: { data: SimpleDetour[] }) => {
const detoursMap = data.reduce(
(acc, detour) => ({ ...acc, [detour.id]: detour }),
{}
)
initializeChannel(detoursMap)
})

.receive("error", ({ reason }) => {
if (reason === "not_authenticated") {
reload()
} else {
// eslint-disable-next-line no-console
console.error(`joining topic ${topic} failed`, reason)
}
})
.receive("timeout", reload)

return channel
}

// This is to refresh the Detours List page. We need all active detours
export const useActiveDetours = (socket: Socket | undefined) => {
const topic = "detours:active"
const [activeDetours, setActiveDetours] = useState<DetoursMap>({})

const handleActivated = (data: SimpleDetour) => {
setActiveDetours((activeDetours) => ({ ...activeDetours, [data.id]: data }))
}

const handleDeactivated = (data: SimpleDetour) => {
setActiveDetours((activeDetours) => {
delete activeDetours[data.id]
return activeDetours
})
}

useEffect(() => {
let channel: Channel | undefined
if (socket) {
channel = subscribe(
socket,
topic,
setActiveDetours,
undefined,
handleActivated,
handleDeactivated
)
}

return () => {
if (channel !== undefined) {
channel.leave()
channel = undefined
}
}
}, [socket])
return activeDetours
}

// This is to refresh the Detours List page, past detours section
export const usePastDetours = (socket: Socket | undefined) => {
const topic = "detours:past"
const [pastDetours, setPastDetours] = useState<DetoursMap>({})

const handleDeactivated = (data: SimpleDetour) => {
setPastDetours((pastDetours) => ({ ...pastDetours, [data.id]: data }))
}

useEffect(() => {
let channel: Channel | undefined
if (socket) {
channel = subscribe(
socket,
topic,
setPastDetours,
undefined,
undefined,
handleDeactivated
)
}

return () => {
if (channel !== undefined) {
channel.leave()
channel = undefined
}
}
}, [socket])
return pastDetours
}

// This is to refresh the Detours List page, just the current user drafts
export const useDraftDetours = (socket: Socket | undefined) => {
const topic = "detours:draft:" + userUuid()
const [draftDetours, setDraftDetours] = useState<DetoursMap>({})

const handleDrafted = (data: SimpleDetour) => {
setDraftDetours((draftDetours) => ({ ...draftDetours, [data.id]: data }))
}

const handleActivated = (data: SimpleDetour) => {
setDraftDetours((draftDetours) => {
delete draftDetours[data.id]
return draftDetours
})
}

useEffect(() => {
let channel: Channel | undefined
if (socket) {
channel = subscribe(
socket,
topic,
setDraftDetours,
handleDrafted,
handleActivated,
undefined
)
}

return () => {
if (channel !== undefined) {
channel.leave()
channel = undefined
}
}
}, [socket])
return draftDetours
}

interface DetoursMapByRoute {
[key: string]: DetoursMap
}

const subscribeByRoute = (
socket: Socket,
topic: string,
routeId: string,
setDetours: React.Dispatch<React.SetStateAction<DetoursMapByRoute>>
): Channel => {
const channel = socket.channel(topic)

channel.on("activated", ({ data: data }) => {
setDetours((activeDetours) => ({
...activeDetours,
[routeId]: { ...activeDetours[routeId], [data.id]: data },
}))
})
channel.on("deactivated", ({ data: data }) => {
setDetours((activeDetours) => {
delete activeDetours[routeId][data.id]
return activeDetours
})
})
channel.on("auth_expired", reload)

channel
.join()
.receive("ok", ({ data: data }: { data: SimpleDetour[] }) => {
const detoursMap = {
[routeId]: data.reduce(
(acc, detour) => ({ ...acc, [detour.id]: detour }),
{}
),
}
setDetours(detoursMap)
})

.receive("error", ({ reason }) => {
if (reason === "not_authenticated") {
reload()
} else {
// eslint-disable-next-line no-console
console.error(`joining topic ${topic} failed`, reason)
}
})
.receive("timeout", reload)

return channel
}

// This is to refresh the Route Ladders
export const useActiveDetoursByRoute = (
socket: Socket | undefined,
routeIds: RouteId[]
) => {
const baseTopic = "detours:active:"
const [activeDetours, setActiveDetours] = useState<DetoursMapByRoute>({})

useEffect(() => {
let channel: Channel | undefined
if (socket) {
routeIds.forEach(
(routeId) =>
(channel = subscribeByRoute(
socket,
baseTopic,
routeId,
setActiveDetours
))
)
}

return () => {
if (channel !== undefined) {
channel.leave()
channel = undefined
}
}
}, [socket])
return activeDetours
}
98 changes: 94 additions & 4 deletions lib/skate/detours/detours.ex
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,22 @@ defmodule Skate.Detours.Detours do
|> Repo.all()
end

@doc """
Returns the list of detours by route id with author, sorted by updated_at
## Examples
iex> active_detours_by_route()
[%Detour{}, ...]
"""
def active_detours_by_route(route_id) do
list_detours()
|> Enum.filter(fn detour ->
categorize_detour(detour) == :active and get_detour_route(detour) == route_id
end)
|> Enum.map(fn detour -> db_detour_to_detour(detour) end)
end

@doc """
Returns the detours grouped by active, draft, and past.
Expand Down Expand Up @@ -114,9 +130,16 @@ defmodule Skate.Detours.Detours do
def categorize_detour(%{state: %{"value" => %{"Detour Drawing" => "Past"}}}, _user_id),
do: :past

def categorize_detour(_detour_context, nil = _user_id), do: :draft
def categorize_detour(%{author_id: author_id}, user_id) when author_id == user_id, do: :draft
def categorize_detour(_, _), do: nil
@doc """
Takes a `Skate.Detours.Db.Detour` struct and a `Skate.Settings.Db.User` id
and returns a `t:detour_type/0` based on the state of the detour.
otherwise returns `nil` if it is a draft but does not belong to the provided
user
"""
@spec get_detour_route(detour :: map()) :: String.t()
def get_detour_route(%{state: %{"context" => %{"route" => %{"name" => route_name}}}}),
do: route_name

@doc """
Gets a single detour.
Expand Down Expand Up @@ -214,7 +237,11 @@ defmodule Skate.Detours.Detours do

case detour_db_result do
{:ok, %Detour{} = new_record} ->
send_notification(new_record, previous_record, author_id)
new_record
|> categorize_detour()
|> broadcast_detour(new_record, author_id)

send_notification(new_record, previous_record)

_ ->
nil
Expand All @@ -223,6 +250,69 @@ defmodule Skate.Detours.Detours do
detour_db_result
end

@spec broadcast_detour(detour_type(), Detour.t(), User.id()) :: nil
defp broadcast_detour(:draft, detour, author_id) do
author_uuid =
author_id
|> User.get_by_id!()
|> Map.get(:uuid)

Phoenix.PubSub.broadcast(
Skate.PubSub,
"detours:draft:" <> author_uuid,
{:detour_drafted, db_detour_to_detour(detour)}
)
end

defp broadcast_detour(:active, detour, author_id) do
author_uuid =
author_id
|> User.get_by_id!()
|> Map.get(:uuid)

route_id = get_detour_route(detour)

Phoenix.PubSub.broadcast(
Skate.PubSub,
"detours:draft:" <> author_uuid,
{:detour_activated, db_detour_to_detour(detour)}
)

Phoenix.PubSub.broadcast(
Skate.PubSub,
"detours:active" <> route_id,
{:detour_activated, db_detour_to_detour(detour)}
)

Phoenix.PubSub.broadcast(
Skate.PubSub,
"detours:active",
{:detour_activated, db_detour_to_detour(detour)}
)
end

defp broadcast_detour(:past, detour, _author_id) do
route_id = get_detour_route(detour)

Phoenix.PubSub.broadcast(
Skate.PubSub,
"detours:active" <> route_id,
{:detour_deactivated, db_detour_to_detour(detour)}
)

Phoenix.PubSub.broadcast(
Skate.PubSub,
"detours:active",
{:detour_deactivated, db_detour_to_detour(detour)}
)

Phoenix.PubSub.broadcast(
Skate.PubSub,
"detours:past",
{:detour_deactivated, db_detour_to_detour(detour)}
)
end

@doc """
Retrieves a `Skate.Detours.Db.Detour` from the database by it's ID and then resolves the
detour's category via `categorize_detour/2`
Expand Down
Loading

0 comments on commit b55e03f

Please sign in to comment.