Skip to content

Commit

Permalink
feat: London Studios SmartSigne integration (#1760)
Browse files Browse the repository at this point in the history
  • Loading branch information
casperiv0 authored Aug 19, 2023
1 parent ae1f4b6 commit 7810b93
Show file tree
Hide file tree
Showing 19 changed files with 245 additions and 44 deletions.
6 changes: 5 additions & 1 deletion apps/client/locales/en/leo.json
Original file line number Diff line number Diff line change
Expand Up @@ -326,7 +326,11 @@
"openModalAfterCreation": "Open Manage Incident modal after creation?",
"manageWeaponFlags": "Manage Weapon Flags",
"flags": "Flags",
"toggle": "Toggle..."
"toggle": "Toggle...",
"hideSmartSigns": "Hide Smart Signs",
"showSmartSigns": "Show Smart Signs",
"smartSignUpdated": "SmartSign Updated",
"smartSignUpdatedMessage": "We sent a request to the FXServer to update the SmartSign."
},
"Bolos": {
"activeBolos": "Active Bolos",
Expand Down
3 changes: 2 additions & 1 deletion apps/client/src/components/dispatch/map/calls/call-item.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,10 @@ interface CallItemProps extends Omit<MapCallProps, "toggledId" | "openItems" | "
export function CallItem({ call, hasMarker, setMarker }: CallItemProps) {
const t = useTranslations("Calls");
const common = useTranslations("Common");
const setCurrentlySelectedCall = useCall911State((state) => state.setCurrentlySelectedCall);

const { generateCallsign } = useGenerateCallsign();
const { openModal } = useModal();
const setCurrentlySelectedCall = useCall911State((state) => state.setCurrentlySelectedCall);

function handleEdit(call: Full911Call) {
openModal(ModalIds.Manage911Call);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,17 +23,18 @@ const CALL_ICON = leafletIcon({
});

export function RenderActiveCalls() {
const [openItems, setOpenItems] = React.useState<string[]>([]);

const map = useMap();
const t = useTranslations("Calls");
const { execute } = useFetch();

const hiddenItems = useDispatchMapState((state) => state.hiddenItems);
const { setCalls, calls } = useCall911State((state) => ({
setCalls: state.setCalls,
calls: state.calls,
}));

const t = useTranslations("Calls");
const [openItems, setOpenItems] = React.useState<string[]>([]);
const { hiddenItems } = useDispatchMapState();

const callsWithPosition = React.useMemo(() => {
return calls.filter((v) => v.gtaMapPosition || (v.position?.lat && v.position.lng));
}, [calls]);
Expand Down Expand Up @@ -158,9 +159,7 @@ export function RenderActiveCalls() {
<ActiveMapCalls
openItems={openItems}
setOpenItems={setOpenItems}
hasMarker={(callId: string) => {
return callsWithPosition.some((v) => v.id === callId);
}}
hasMarker={(callId: string) => callsWithPosition.some((v) => v.id === callId)}
setMarker={handleMarkerChange}
/>
</>
Expand Down
15 changes: 13 additions & 2 deletions apps/client/src/components/dispatch/map/map-actions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,15 @@ export function MapActions() {
const t = useTranslations();
const portalRef = usePortal("MapActions");
const { openModal } = useModal();
const mapState = useDispatchMapState();
const mapState = useDispatchMapState((state) => ({
hiddenItems: state.hiddenItems,
setItem: state.setItem,
}));
const status = useSocketStore((state) => state.status);

const { hasPermissions } = usePermission();
const hasManageUsersPermissions = hasPermissions([Permissions.ManageUsers]);
const hasManageSmartSignsPermissions = hasPermissions([Permissions.ManageSmartSigns]);

return (
portalRef &&
Expand All @@ -37,7 +41,14 @@ export function MapActions() {
<DropdownMenuTrigger asChild key="trigger">
<Button>{t("Leo.toggle")}</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="start" key="content">
<DropdownMenuContent className="min-w-[175px]" align="start" key="content">
{hasManageSmartSignsPermissions ? (
<DropdownMenuItem onClick={() => mapState.setItem(MapItem.SMART_SIGNS)}>
{mapState.hiddenItems[MapItem.SMART_SIGNS]
? t("Leo.showSmartSigns")
: t("Leo.hideSmartSigns")}
</DropdownMenuItem>
) : null}
<DropdownMenuItem onClick={() => mapState.setItem(MapItem.BLIPS)}>
{mapState.hiddenItems[MapItem.BLIPS] ? t("Leo.showBlips") : t("Leo.hideBlips")}
</DropdownMenuItem>
Expand Down
4 changes: 3 additions & 1 deletion apps/client/src/components/dispatch/map/map.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { RenderActiveCalls } from "./calls/render-active-map-calls";
import { MapActions } from "./map-actions";
import { RenderMapPlayers } from "./units/render-map-players";
import { SelectMapServerModal } from "./modals/select-map-server-modal";
import { RenderMapSmartSigns } from "./smart-signs/render-map-smart-signs";

const TILES_URL = "/tiles/minimap_sea_{y}_{x}.webp" as const;

Expand All @@ -21,7 +22,7 @@ export function Map() {
map?.setZoom(-2);
map?.getBounds();
}
}, [bounds, map]);
}, [bounds]); // eslint-disable-line react-hooks/exhaustive-deps

return (
<MapContainer
Expand All @@ -48,6 +49,7 @@ export function Map() {
<RenderMapBlips />
<RenderActiveCalls />
<MapActions />
<RenderMapSmartSigns />

<SelectMapServerModal />
</MapContainer>
Expand Down
23 changes: 6 additions & 17 deletions apps/client/src/components/dispatch/map/render-map-blips.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { MapItem, useDispatchMapState } from "state/mapState";
export function RenderMapBlips() {
const map = useMap();
const [blips, setBlips] = React.useState<Blip[]>([]);
const { hiddenItems } = useDispatchMapState();
const hiddenItems = useDispatchMapState((state) => state.hiddenItems);

const doBlips = React.useCallback(async () => {
setBlips(await generateBlips(map));
Expand All @@ -23,22 +23,11 @@ export function RenderMapBlips() {
return null;
}

return (
<>
{blips.map((blip, idx) => {
return (
<Marker
icon={blip.icon}
draggable={false}
key={`${blip.name}-${idx}`}
position={blip.pos}
>
<Tooltip direction="top">{blip.name}</Tooltip>
</Marker>
);
})}
</>
);
return blips.map((blip, idx) => (
<Marker icon={blip.icon} draggable={false} key={`${blip.name}-${idx}`} position={blip.pos}>
<Tooltip direction="top">{blip.name}</Tooltip>
</Marker>
));
}

async function generateBlips(map: L.Map) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { SmartSignsMarker } from "./smart-sign-marker";
import { useSmartSigns } from "./use-smart-signs";

export function RenderMapSmartSigns() {
const smartSigns = useSmartSigns();
return smartSigns.map((marker) => <SmartSignsMarker key={marker.id} marker={marker} />);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
import * as React from "react";
import { convertToMap } from "lib/map/utils";
import { Marker, Popup, Tooltip, useMap } from "react-leaflet";
import type { SmartSignMarker } from "types/map";
import { icon as leafletIcon } from "leaflet";
import { useTranslations } from "next-intl";
import { MapItem, useDispatchMapState, useSocketStore } from "state/mapState";
import { generateMarkerTypes } from "../render-map-blips";
import { Button, Input } from "@snailycad/ui";
import { Permissions, usePermission } from "hooks/usePermission";
import { toastMessage } from "lib/toastMessage";

interface Props {
marker: SmartSignMarker;
}

export function SmartSignsMarker({ marker }: Props) {
const map = useMap();
const socket = useSocketStore((state) => state.socket);
const [markerText, setMarkerText] = React.useState<
SmartSignMarker["defaultText"] & { editing?: boolean }
>(marker.defaultText);

const t = useTranslations("Leo");
const hiddenItems = useDispatchMapState((state) => state.hiddenItems);
const markerTypes = React.useMemo(generateMarkerTypes, []);

const { hasPermissions } = usePermission();
const hasManageSmartSignsPermissions = hasPermissions([Permissions.ManageSmartSigns]);

const pos = React.useMemo(() => convertToMap(marker.x, marker.y, map), [marker.x, marker.y]); // eslint-disable-line react-hooks/exhaustive-deps
const playerIcon = React.useMemo(() => {
// eslint-disable-next-line prefer-destructuring
const icon = markerTypes[780];

if (icon) {
return leafletIcon(icon);
}

return undefined;
}, [markerTypes]);

React.useEffect(() => {
if (!markerText.editing) setMarkerText(marker.defaultText);
}, [marker.defaultText, markerText.editing]);

function handleSubmit(event: React.FormEvent<HTMLFormElement>) {
event.preventDefault();
if (!socket?.connected) return;

socket.emit("sna-live-map:update-smart-sign", {
...marker,
defaultText: {
firstLine: markerText.firstLine.toLowerCase() || null,
secondLine: markerText.secondLine.toLowerCase() || null,
thirdLine: markerText.thirdLine.toLowerCase() || null,
},
});

toastMessage({
icon: "success",
title: t("smartSignUpdated"),
message: t("smartSignUpdatedMessage"),
});

setTimeout(() => {
setMarkerText({
...markerText,
editing: false,
});
}, 500); // clear editing state after 500ms
}

if (hiddenItems[MapItem.SMART_SIGNS]) {
return null;
}

return (
<Marker icon={playerIcon} key={marker.id} position={pos}>
<Tooltip direction="top">SmartSign {marker.id}</Tooltip>

<Popup minWidth={300}>
<p style={{ margin: 2 }}>
<strong>SmartSign {marker.id}</strong>
</p>

<form onSubmit={handleSubmit} className="mt-3">
<Input
name="firstLine"
readOnly={!hasManageSmartSignsPermissions}
className="rounded-b-none !text-amber-600 font-bold text-[15px] uppercase"
value={markerText.firstLine.toUpperCase()}
onChange={(event) =>
setMarkerText({ ...markerText, editing: true, firstLine: event.target.value })
}
maxLength={15}
/>
<Input
name="secondLine"
readOnly={!hasManageSmartSignsPermissions}
className="rounded-none -my-1 !text-amber-600 font-bold text-[15px] uppercase"
value={markerText.secondLine.toUpperCase()}
onChange={(event) =>
setMarkerText({ ...markerText, editing: true, secondLine: event.target.value })
}
maxLength={15}
/>
<Input
name="thirdLine"
readOnly={!hasManageSmartSignsPermissions}
className="rounded-t-none !text-amber-600 font-bold text-[15px] uppercase"
value={markerText.thirdLine.toUpperCase()}
onChange={(event) =>
setMarkerText({ ...markerText, editing: true, thirdLine: event.target.value })
}
maxLength={15}
/>

<Button className="mt-2" type="submit" disabled={!hasManageSmartSignsPermissions}>
Save
</Button>
</form>
</Popup>
</Marker>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import * as React from "react";
import { useDispatchMapState, useSocketStore } from "state/mapState";
import { SmartSignMarker } from "types/map";

export function useSmartSigns() {
const socket = useSocketStore((state) => state.socket);
const { smartSigns, setSmartSigns } = useDispatchMapState((state) => ({
smartSigns: state.smartSigns,
setSmartSigns: state.setSmartSigns,
}));

const onInitialize = React.useCallback((data: { smartSigns: SmartSignMarker[] }) => {
setSmartSigns(data.smartSigns);
}, []); // eslint-disable-line react-hooks/exhaustive-deps

React.useEffect(() => {
const s = socket;

if (s) {
s.on("sna-live-map:smart-signs", onInitialize);
}

return () => {
s?.off("sna-live-map:smart-signs", onInitialize);
};
}, [socket, onInitialize]);

return smartSigns;
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,7 @@ export function ActiveMapUnits({ openItems, setOpenItems }: Props) {
const [tempUnit, setTempUnit] = React.useState<
null | Officer | EmsFdDeputy | CombinedEmsFdUnit | CombinedLeoUnit
>(null);
const { players } = useMapPlayersStore();

const players = useMapPlayersStore((state) => state.players);
const portalRef = usePortal("ActiveMapCalls");
const t = useTranslations("Leo");

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,9 @@ const PLAYER_ICON = leafletIcon({
export function PlayerMarker({ player, handleToggle }: Props) {
const map = useMap();
const t = useTranslations("Leo");
const { hiddenItems } = useDispatchMapState();
const hiddenItems = useDispatchMapState((state) => state.hiddenItems);
const markerTypes = React.useMemo(generateMarkerTypes, []);

const { generateCallsign } = useGenerateCallsign();

const playerIcon = React.useMemo(() => {
Expand Down Expand Up @@ -106,7 +107,6 @@ export function PlayerMarker({ player, handleToggle }: Props) {
});

const hasUnit = isCADUser && Boolean(player.unit);

const showUnitsOnly = hiddenItems[MapItem.UNITS_ONLY];

if (showUnitsOnly) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import { useMapPlayers } from "hooks/realtime/use-map-players";

export function RenderMapPlayers() {
const [openItems, setOpenItems] = React.useState<string[]>([]);

const { players } = useMapPlayers();

function handleToggle(name: string) {
Expand All @@ -18,13 +17,11 @@ export function RenderMapPlayers() {
});
}

const playerValues = React.useMemo(() => {
return [...players.values()];
}, [players]);
const playerValues = React.useMemo(() => [...players.values()], [players]);

return (
<>
{Array.from(players.values()).map((player, idx) => (
{playerValues.map((player, idx) => (
<PlayerMarker
key={`${player.identifier}-${idx}`}
handleToggle={handleToggle}
Expand Down
10 changes: 7 additions & 3 deletions apps/client/src/hooks/realtime/use-map-players.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,14 @@ export const useMapPlayersStore = createWithEqualityFn<{
export function useMapPlayers() {
const { players, setPlayers } = useMapPlayersStore();

const currentMapServerURL = useDispatchMapState((state) => state.currentMapServerURL);
const { openModal, isOpen } = useModal();
const { currentMapServerURL } = useDispatchMapState();
const { socket, setStatus, setSocket } = useSocketStore();
const { state, execute } = useFetch();
const { socket, setStatus, setSocket } = useSocketStore((state) => ({
socket: state.socket,
setStatus: state.setStatus,
setSocket: state.setSocket,
}));

const getCADUsers = React.useCallback(
async (options: {
Expand Down Expand Up @@ -74,7 +78,7 @@ export function useMapPlayers() {
}
}

setPlayers(newPlayers);
setPlayers(options.map);
},
[state], // eslint-disable-line
);
Expand Down
Loading

0 comments on commit 7810b93

Please sign in to comment.