Skip to content

Commit 53fd993

Browse files
authored
Feat location (#927)
<!-- Please read https://github.com/SableClient/Sable/blob/dev/CONTRIBUTING.md before submitting your pull request --> ### Description There is a doc written for this under SableClient/docs#16 Add location modal, this adds a dependency to leaflet, it is a slightly outdated version to fit in with our react version (look of a location shared) <img width="1474" height="940" alt="image" src="https://github.com/user-attachments/assets/7838119b-37af-4ad4-b79b-34656f23b5f1" /> (look of the add menu) <img width="155" height="169" alt="image" src="https://github.com/user-attachments/assets/74eb1bf2-15c7-4a79-8c06-6ddca0b2ff8c" /> (look of the location sharing modal) <img width="503" height="646" alt="image" src="https://github.com/user-attachments/assets/05afa98c-28cb-49e4-bd49-e61818f95148" /> From here you can paste coords, or links to osm, geo:, or apple long links, i have decided against short links at this time as they seem to not be working reliably (look of the 2 new settings as it is a feature that makes external calls) <img width="808" height="689" alt="image" src="https://github.com/user-attachments/assets/428668f3-51dd-46b5-8ef9-1a7a5b72ee46" /> Fixes # #### Type of change - [ ] Bug fix (non-breaking change which fixes an issue) - [x] New feature (non-breaking change which adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) - [ ] This change requires a documentation update ### Checklist: - [x] My code follows the style guidelines of this project - [x] I have performed a self-review of my own code - [x] I have commented my code, particularly in hard-to-understand areas - [x] I have made corresponding changes to the documentation - [x] My changes generate no new warnings ### AI disclosure: - [ ] Partially AI assisted (clarify which code was AI assisted and briefly explain what it does). - [ ] Fully AI generated (explain what all the generated code does in moderate detail). <!-- Write any explanation required here, but do not generate the explanation using AI!! You must prove you understand what the code in this PR does. --> Written with the love of fancy accidental self-doxxing
2 parents 6bfd2b9 + 5f5be0f commit 53fd993

17 files changed

Lines changed: 713 additions & 76 deletions

.changeset/add_location.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
default: minor
3+
---
4+
5+
Add Location styling with a modal!

package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@
7171
"is-hotkey": "^0.2.0",
7272
"jotai": "^2.18.0",
7373
"katex": "^0.16.45",
74+
"leaflet": "1.9.4",
7475
"linkify-react": "^4.3.2",
7576
"linkifyjs": "^4.3.2",
7677
"marked": "^18.0.2",
@@ -84,6 +85,7 @@
8485
"react-dom": "^18.3.1",
8586
"react-google-recaptcha": "^2.1.0",
8687
"react-i18next": "^16.5.4",
88+
"react-leaflet": "^4.2.1",
8789
"react-range": "^1.10.0",
8890
"react-router-dom": "^6.30.3",
8991
"slate": "^0.123.0",
@@ -107,6 +109,7 @@
107109
"@types/chroma-js": "^3.1.2",
108110
"@types/file-saver": "^2.0.7",
109111
"@types/is-hotkey": "^0.1.10",
112+
"@types/leaflet": "^1.9.21",
110113
"@types/node": "24.10.13",
111114
"@types/react": "^18.3.28",
112115
"@types/react-dom": "^18.3.7",

pnpm-lock.yaml

Lines changed: 53 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/app/components/RenderMessageContent.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ type RenderMessageContentProps = {
6060
bundledPreview?: boolean;
6161
urlPreview?: boolean;
6262
clientUrlPreview?: boolean;
63+
showMaps?: boolean;
6364
highlightRegex?: RegExp;
6465
htmlReactParserOptions: HTMLReactParserOptions;
6566
linkifyOpts: Opts;
@@ -95,6 +96,7 @@ function RenderMessageContentInternal({
9596
bundledPreview,
9697
urlPreview,
9798
clientUrlPreview,
99+
showMaps,
98100
highlightRegex,
99101
htmlReactParserOptions,
100102
linkifyOpts,
@@ -430,7 +432,8 @@ function RenderMessageContentInternal({
430432
}
431433

432434
if (msgType === (MsgType.File as string)) return renderFile();
433-
if (msgType === (MsgType.Location as string)) return <MLocation content={content} />;
435+
if (msgType === (MsgType.Location as string))
436+
return <MLocation showMaps={showMaps} content={content} />;
434437
if (msgType === 'm.bad.encrypted') return <MBadEncrypted />;
435438

436439
// cute events
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { style } from '@vanilla-extract/css';
2+
import { color, config, toRem } from 'folds';
3+
4+
export const ReplyBend = style({
5+
flexShrink: 0,
6+
});
7+
8+
export const LocationRendererBody = style({
9+
maxWidth: toRem(500),
10+
backgroundColor: color.SurfaceVariant.Container,
11+
borderRadius: config.radii.R500,
12+
overflow: 'hidden',
13+
});
14+
15+
export const LocationRendererHeader = style({ padding: config.space.S200 });
16+
17+
export const LocationCoordsChip = style({});
18+
export const LocationExternalChip = style({ flexShrink: '0' });
19+
20+
export const LocationMapContainer = style({ height: toRem(400) });

src/app/components/message/MsgTypeRenderers.tsx

Lines changed: 59 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,11 @@ import { MessageTextBody } from './layout';
3535
import { unwrapForwardedContent } from './modals/MessageForward';
3636
import { LINKINPUTREGEX } from '$components/editor';
3737
import { MATRIX_TO_BASE } from '$plugins/matrix-to';
38+
import { copyToClipboard } from '$utils/dom';
39+
import { MapContainer, Marker, TileLayer } from 'react-leaflet';
40+
import type { LatLngExpression } from 'leaflet';
41+
import 'leaflet/dist/leaflet.css';
42+
import * as css from './MsgTypeRenderers.css';
3843

3944
export interface BundleContent extends IPreviewUrlResponse {
4045
matched_url: string;
@@ -636,30 +641,69 @@ export function MFile({ content, renderFileContent, outlined }: MFileProps) {
636641

637642
type MLocationProps = {
638643
content: IContent;
644+
showMaps?: boolean;
639645
};
640-
export function MLocation({ content }: MLocationProps) {
646+
export function MLocation({ content, showMaps }: MLocationProps) {
641647
const geoUri = content.geo_uri;
642648
if (typeof geoUri !== 'string') {
643649
return <BrokenContent body={typeof content.body === 'string' ? content.body : undefined} />;
644650
}
645651
const location = parseGeoUri(geoUri);
646652
if (!location) return <BrokenContent />;
647-
653+
const coords: LatLngExpression = [Number(location.latitude), Number(location.longitude)];
648654
return (
649-
<Box direction="Column" alignItems="Start" gap="100">
650-
<Text size="T400">{geoUri}</Text>
651-
<Chip
652-
as="a"
653-
size="400"
654-
href={`https://www.openstreetmap.org/?mlat=${location.latitude}&mlon=${location.longitude}#map=16/${location.latitude}/${location.longitude}`}
655-
target="_blank"
656-
rel="noreferrer noopener"
657-
variant="Primary"
658-
radii="Pill"
659-
before={<Icon src={Icons.External} size="50" />}
655+
<Box
656+
direction="Column"
657+
className={css.LocationRendererBody}
658+
onPointerMove={(evt) => evt.stopPropagation()}
659+
>
660+
<Box
661+
direction="Row"
662+
alignItems="Center"
663+
gap="100"
664+
justifyContent="SpaceBetween"
665+
className={css.LocationRendererHeader}
660666
>
661-
<Text size="B300">Open Location</Text>
662-
</Chip>
667+
<Chip
668+
size="400"
669+
variant="SurfaceVariant"
670+
onClick={() => copyToClipboard(`${location.latitude}, ${location.longitude}`)}
671+
before={<Icon size="50" src={Icons.Link} />}
672+
className={css.LocationCoordsChip}
673+
>
674+
<Text size="T400">{`${location.latitude}, ${location.longitude}`}</Text>
675+
</Chip>
676+
677+
<Chip
678+
as="a"
679+
size="400"
680+
href={`https://www.openstreetmap.org/?mlat=${location.latitude}&mlon=${location.longitude}#map=16/${location.latitude}/${location.longitude}`}
681+
target="_blank"
682+
rel="noreferrer noopener"
683+
variant="Primary"
684+
radii="Pill"
685+
className={css.LocationExternalChip}
686+
before={<Icon src={Icons.External} size="50" />}
687+
>
688+
<Text size="B300">Open Location</Text>
689+
</Chip>
690+
</Box>
691+
{showMaps && (
692+
<MapContainer
693+
center={coords}
694+
zoom={16}
695+
scrollWheelZoom={true}
696+
className={css.LocationMapContainer}
697+
attributionControl
698+
>
699+
<TileLayer
700+
attribution='&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>'
701+
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
702+
/>
703+
704+
<Marker position={coords}></Marker>
705+
</MapContainer>
706+
)}
663707
</Box>
664708
);
665709
}

src/app/features/room/RoomInput.tsx

Lines changed: 31 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,7 @@ import {
129129
convertPerMessageProfileToBeeperFormat,
130130
getCurrentlyUsedPerMessageProfileForRoom,
131131
} from '$hooks/usePerMessageProfile';
132-
import { Microphone, Stop } from '@phosphor-icons/react';
132+
import { Microphone, Stop, MapPinPlusIcon } from '@phosphor-icons/react';
133133
import { getSupportedAudioExtension } from '$plugins/voice-recorder-kit/supportedCodec';
134134
import { ErrorCode } from '../../cs-errorcode';
135135
import { sanitizeText } from '$utils/sanitize';
@@ -160,6 +160,7 @@ import type {
160160
import { AudioMessageRecorder } from './AudioMessageRecorder';
161161
import * as prefix from '$unstable/prefixes';
162162
import { PollDialog } from './poll-modals';
163+
import { LocationDialog } from './location-modal';
163164

164165
// Returns the event ID of the most recent non-reaction/non-edit event in a thread,
165166
// falling back to the thread root if no replies exist yet.
@@ -389,6 +390,7 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
389390
);
390391
const [AddMenuAnchor, setAddMenuAnchor] = useState<RectCords>();
391392
const [showPollPicker, setShowPollPicker] = useState(false);
393+
const [showLocationPicker, setShowLocationPicker] = useState(false);
392394
const [scheduleMenuAnchor, setScheduleMenuAnchor] = useState<RectCords>();
393395
const [showSchedulePicker, setShowSchedulePicker] = useState(false);
394396
const [silentReply, setSilentReply] = useState(!mentionInReplies);
@@ -822,16 +824,15 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
822824
} else if (commandName === Command.UnFlip) {
823825
plainText = `${UNFLIP} ${plainText}`;
824826
customHtml = `${UNFLIP} ${customHtml}`;
825-
} else if (commandName === Command.Poll) {
826-
setShowPollPicker(true);
827-
resetEditor(editor);
828-
resetEditorHistory(editor);
829-
sendTypingStatus(false);
830-
return;
831827
} else if (commandName) {
832-
const commandContent = commands[commandName as Command];
833-
if (commandContent) {
834-
commandContent.exe(plainText, customHtml);
828+
if ((commandName as Command) === Command.Poll) setShowPollPicker(true);
829+
else if ((commandName as Command) === Command.Location && plainText.trim().length === 0)
830+
setShowLocationPicker(true);
831+
else {
832+
const commandContent = commands[commandName as Command];
833+
if (commandContent) {
834+
commandContent.exe(plainText, customHtml);
835+
}
835836
}
836837
resetEditor(editor);
837838
resetEditorHistory(editor);
@@ -1561,6 +1562,17 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
15611562
>
15621563
<Text size="B300">Create Poll</Text>
15631564
</MenuItem>
1565+
<MenuItem
1566+
size="300"
1567+
radii="300"
1568+
onClick={() => {
1569+
setAddMenuAnchor(undefined);
1570+
setShowLocationPicker(true);
1571+
}}
1572+
before={<MapPinPlusIcon size="20" />}
1573+
>
1574+
<Text size="B300">Add Location</Text>
1575+
</MenuItem>
15641576
<MenuItem
15651577
size="300"
15661578
radii="300"
@@ -1843,6 +1855,15 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
18431855
clearReplyDraft={() => setReplyDraft(undefined)}
18441856
/>
18451857
)}
1858+
{showLocationPicker && (
1859+
<LocationDialog
1860+
onCancel={() => setShowLocationPicker(false)}
1861+
mx={mx}
1862+
room={room}
1863+
replyDraft={replyDraft}
1864+
clearReplyDraft={() => setReplyDraft(undefined)}
1865+
/>
1866+
)}
18461867
</div>
18471868
);
18481869
}

0 commit comments

Comments
 (0)