Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions apps/native/android/app/capacitor.build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@ apply from: "../capacitor-cordova-android-plugins/cordova.variables.gradle"
dependencies {
implementation project(':capacitor-firebase-messaging')
implementation project(':capacitor-app')
implementation project(':capacitor-app-launcher')
implementation project(':capacitor-filesystem')
implementation project(':capacitor-geolocation')
implementation project(':capacitor-haptics')
implementation project(':capacitor-keyboard')
implementation project(':capacitor-preferences')
Expand Down
4 changes: 4 additions & 0 deletions apps/native/android/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -84,4 +84,8 @@
<uses-feature android:name="android.hardware.microphone" android:required="false" />
<!-- RavenHaptics drives the vibrator directly. -->
<uses-permission android:name="android.permission.VIBRATE" />
<!-- Location sharing: sent only when the user picks a spot; GPS stays optional. -->
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-feature android:name="android.hardware.location.gps" android:required="false" />
</manifest>
6 changes: 6 additions & 0 deletions apps/native/android/capacitor.settings.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,15 @@ project(':capacitor-firebase-messaging').projectDir = new File('../../../node_mo
include ':capacitor-app'
project(':capacitor-app').projectDir = new File('../../../node_modules/@capacitor/app/android')

include ':capacitor-app-launcher'
project(':capacitor-app-launcher').projectDir = new File('../../../node_modules/@capacitor/app-launcher/android')

include ':capacitor-filesystem'
project(':capacitor-filesystem').projectDir = new File('../../../node_modules/@capacitor/filesystem/android')

include ':capacitor-geolocation'
project(':capacitor-geolocation').projectDir = new File('../../../node_modules/@capacitor/geolocation/android')

include ':capacitor-haptics'
project(':capacitor-haptics').projectDir = new File('../../../node_modules/@capacitor/haptics/android')

Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 7 additions & 0 deletions apps/native/ios/App/App/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,11 @@
<string>$(CURRENT_PROJECT_VERSION)</string>
<key>ITSAppUsesNonExemptEncryption</key>
<false/>
<key>LSApplicationQueriesSchemes</key>
<array>
<string>comgooglemaps</string>
<string>waze</string>
</array>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>NSAppTransportSecurity</key>
Expand All @@ -62,6 +67,8 @@
<string>Raven uses the camera to take photos and videos to send in chat.</string>
<key>NSMicrophoneUsageDescription</key>
<string>Raven uses the microphone to record videos and voice messages.</string>
<key>NSLocationWhenInUseUsageDescription</key>
<string>Raven uses your location only when you choose to share it in a chat.</string>
<key>NSUserActivityTypes</key>
<array>
<string>INSendMessageIntent</string>
Expand Down
4 changes: 4 additions & 0 deletions apps/native/ios/App/CapApp-SPM/Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@ let package = Package(
.package(url: "https://github.com/ionic-team/capacitor-swift-pm.git", exact: "8.5.0"),
.package(name: "CapacitorFirebaseMessaging", path: "../../../../../node_modules/@capacitor-firebase/messaging"),
.package(name: "CapacitorApp", path: "../../../../../node_modules/@capacitor/app"),
.package(name: "CapacitorAppLauncher", path: "../../../../../node_modules/@capacitor/app-launcher"),
.package(name: "CapacitorFilesystem", path: "../../../../../node_modules/@capacitor/filesystem"),
.package(name: "CapacitorGeolocation", path: "../../../../../node_modules/@capacitor/geolocation"),
.package(name: "CapacitorHaptics", path: "../../../../../node_modules/@capacitor/haptics"),
.package(name: "CapacitorKeyboard", path: "../../../../../node_modules/@capacitor/keyboard"),
.package(name: "CapacitorPreferences", path: "../../../../../node_modules/@capacitor/preferences"),
Expand All @@ -34,7 +36,9 @@ let package = Package(
.product(name: "Cordova", package: "capacitor-swift-pm"),
.product(name: "CapacitorFirebaseMessaging", package: "CapacitorFirebaseMessaging"),
.product(name: "CapacitorApp", package: "CapacitorApp"),
.product(name: "CapacitorAppLauncher", package: "CapacitorAppLauncher"),
.product(name: "CapacitorFilesystem", package: "CapacitorFilesystem"),
.product(name: "CapacitorGeolocation", package: "CapacitorGeolocation"),
.product(name: "CapacitorHaptics", package: "CapacitorHaptics"),
.product(name: "CapacitorKeyboard", package: "CapacitorKeyboard"),
.product(name: "CapacitorPreferences", package: "CapacitorPreferences"),
Expand Down
2 changes: 2 additions & 0 deletions apps/native/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,10 @@
"@capacitor-firebase/messaging": "8.4.0",
"@capacitor/android": "8.5.0",
"@capacitor/app": "8.1.1",
"@capacitor/app-launcher": "8.0.1",
"@capacitor/core": "8.5.0",
"@capacitor/filesystem": "8.1.3",
"@capacitor/geolocation": "8.2.4",
"@capacitor/haptics": "8.0.2",
"@capacitor/ios": "8.5.0",
"@capacitor/keyboard": "8.0.5",
Expand Down
6 changes: 5 additions & 1 deletion apps/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,10 @@
"dependencies": {
"@capacitor-firebase/messaging": "8.4.0",
"@capacitor/app": "8.1.1",
"@capacitor/app-launcher": "8.0.1",
"@capacitor/core": "8.5.0",
"@capacitor/filesystem": "8.1.3",
"@capacitor/geolocation": "8.2.4",
"@capacitor/haptics": "8.0.2",
"@capacitor/keyboard": "8.0.5",
"@capacitor/preferences": "8.0.1",
Expand Down Expand Up @@ -65,8 +67,8 @@
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"cmdk": "^1.1.1",
"date-fns": "^4.4.0",
"cronstrue": "^3.27.0",
"date-fns": "^4.4.0",
"dayjs": "^1.11.21",
"dexie": "^4.4.3",
"dexie-react-hooks": "^4.4.0",
Expand All @@ -78,6 +80,7 @@
"jotai": "^2.20.0",
"jotai-family": "^1.0.2",
"js-cookie": "^3.0.8",
"leaflet": "^1.9.4",
"linkifyjs": "^4.3.3",
"lowlight": "^3.3.0",
"lucide-react": "^1.17.0",
Expand Down Expand Up @@ -116,6 +119,7 @@
"@eslint/js": "^9.39.1",
"@raven/types": "*",
"@types/js-cookie": "^3.0.6",
"@types/leaflet": "^1.9.22",
"@types/node": "^25.3.0",
"@types/react": "^19.2.15",
"@types/react-dom": "^19.2.3",
Expand Down
Binary file added apps/web/src/assets/brand_icons/apple-maps.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added apps/web/src/assets/brand_icons/google-maps.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added apps/web/src/assets/brand_icons/waze.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,9 @@ describe("searchResultToMessage", () => {
expect(message.message_type).toBe("Text")
expect(message.text).toContain("Voice note")
})

it("shows a location as a labelled placeholder, not its fallback link", () => {
const row = { id: "m2", author: "a@x.com", channel_id: "ch", creation: "2026-10-23 10:00:00", message_type: "Location", content: "https://www.google.com/maps/search/?api=1&query=1,2" } as unknown as SearchResult
expect(searchResultToMessage({ ...row, message_type: "Location" }).text).toBe("<p>📍 Location</p>")
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,9 @@ export function searchResultToMessage(r: SearchResult): Message {
if (messageType === 'Voice') {
return { ...base, message_type: 'Text', text: `<p>🎤 ${_("Voice note")}</p>` }
}
if (messageType === 'Location') {
return { ...base, message_type: 'Text', text: `<p>📍 ${_("Location")}</p>` }
}
if (messageType === 'Poll') {
return {
...base,
Expand Down
18 changes: 18 additions & 0 deletions apps/web/src/components/common/PinMarker.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { MapPin } from "lucide-react"
import { cn } from "@lib/utils"

/** The one map marker: Lucide's MapPin, filled, with its tip (y 22 of 24) on the container centre. */
export const PinMarker = ({ lifted = false, className }: { lifted?: boolean; className?: string }) => (
<MapPin
aria-hidden="true"
className={cn(
"pointer-events-none absolute left-1/2 top-1/2 size-9 fill-current text-ink-red-6 transition-transform duration-150 [&_circle]:fill-white",
className,
)}
style={{
transform: lifted
? "translate(-50%, calc(-22 / 24 * 100% - 8px))"
: "translate(-50%, calc(-22 / 24 * 100%))",
}}
/>
)
7 changes: 7 additions & 0 deletions apps/web/src/components/common/filters/ProviderFilter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,13 @@ export const PROVIDER_CATEGORIES: Array<{ label: string; options: ProviderOption
{ id: 'Zoom', label: 'Zoom', brand: BRAND.zoom },
],
},
{
label: 'Maps',
options: [
{ id: 'Google Maps', label: 'Google Maps', brand: BRAND.googleMaps },
{ id: 'Apple Maps', label: 'Apple Maps', brand: BRAND.appleMaps },
],
},
{
label: 'Workspace',
options: [
Expand Down
40 changes: 33 additions & 7 deletions apps/web/src/components/features/ChatInput/ChatInput.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { forwardRef, useCallback, useContext, useEffect, useMemo, useRef, useState } from "react"
import { forwardRef, lazy, Suspense, useCallback, useContext, useEffect, useMemo, useRef, useState } from "react"
import { EditorContent, useEditorState } from "@tiptap/react"
import { FrappeConfig, FrappeContext, useSWRConfig } from "frappe-react-sdk"
import { useAtom, useAtomValue, useSetAtom } from "jotai"
Expand Down Expand Up @@ -29,6 +29,10 @@ import { MobileComposerActions } from "./MobileComposerActions"
import { VoiceNoteButton } from "./VoiceNoteButton"
import { VoiceComposer } from "./VoiceComposer"
import { useVoiceRecording } from "./useVoiceRecording"
import { useLocationShare } from "./location/useLocationShare"
import { PickerBoundary } from "./location/PickerBoundary"
import { ShareLocationButton } from "./location/ShareLocationButton"
import { canShareLocation } from "@lib/locationSupport"
import { canRecordVoice } from "./recordingFormat"
import { loadDraft, saveDraft } from "./draft"
import { useTypingEmitter } from "@stores/typing/useTypingEmitter"
Expand All @@ -51,6 +55,9 @@ import { randomUUID } from "@lib/uuid"
import { Separator } from "@components/ui/separator"
import AttachFrappeDocumentDialog, { LinkedDocumentBanner } from "./AttachFrappeDocumentDialog"

// The map and its dialog stay out of the composer's initial chunk until first open.
const LocationPicker = lazy(() => import("./location/LocationPicker").then((m) => ({ default: m.LocationPicker })))

interface ChatInputProps {
channelID: string
/**
Expand Down Expand Up @@ -303,6 +310,14 @@ const ChatInput = forwardRef<HTMLFormElement, ChatInputProps>(({ channelID, isDi

const voice = useVoiceRecording({ channelID, editor, quietSendMode, stopTyping, covered })
const { recording } = voice
const clearReply = useCallback(() => setReplyTo(null), [setReplyTo])
const share = useLocationShare({ channelID, quietSendMode, stopTyping, replyTo, clearReply })

// Contains a failed picker load here; a lazy rejection stays cached until reload.
const handlePickerError = useCallback(() => {
share.setOpen(false)
toast.error(_("Couldn't open location. Reload the page and try again."))
}, [share.setOpen])

const handleSend = useCallback((opts?: { sendSilently?: boolean }) => {
if (!editor) return
Expand Down Expand Up @@ -438,6 +453,14 @@ const ChatInput = forwardRef<HTMLFormElement, ChatInputProps>(({ channelID, isDi
busy={scheduleBusy}
/>
)}
{/* Mounted only while open, so Leaflet stays off the typing path. */}
{share.open && (
<PickerBoundary onError={handlePickerError}>
<Suspense fallback={null}>
<LocationPicker open onOpenChange={share.setOpen} onSend={share.send} />
</Suspense>
</PickerBoundary>
)}
<QuietHoursBanner mode={quietSendMode} />
{/* Pending scheduled messages for this channel. Thread composers cannot schedule. */}
{!disableSchedule && <ScheduledMessagesBanner channelID={channelID} />}
Expand Down Expand Up @@ -495,7 +518,11 @@ const ChatInput = forwardRef<HTMLFormElement, ChatInputProps>(({ channelID, isDi
*/}
<div className={cn("flex items-end gap-0 pe-2 ps-1 py-2 border-t border-outline-gray-2 bg-surface-base mb-[var(--keyboard-height,0px)]", isMobile && !keyboardOpen && "standalone:pb-[max(var(--inset-bottom),0.75rem)]", recording && "hidden")}>
<div className="flex items-center justify-center h-10">
<MobileComposerActions channelID={channelID} onRecord={canRecordVoice() ? voice.start : undefined} />
<MobileComposerActions
channelID={channelID}
onRecord={canRecordVoice() ? voice.start : undefined}
onShareLocation={canShareLocation() ? share.openPicker : undefined}
/>
</div>
<div className="flex-1 min-w-0 dark:bg-surface-elevation-2 bg-surface-gray-1 rounded-2xl [&_.tiptap]:min-h-10 [&_.tiptap]:max-h-64 [&_.tiptap]:overflow-y-auto [&_.tiptap]:py-2">
<EditorContent editor={editor} />
Expand Down Expand Up @@ -579,12 +606,11 @@ const ChatInput = forwardRef<HTMLFormElement, ChatInputProps>(({ channelID, isDi
<Separator orientation="vertical" className="mx-1 h-4!" />
<CreatePollDialog channelID={channelID} />
<AttachFrappeDocumentDialog channelID={channelID} />
{canRecordVoice() && (
<>
<Separator orientation="vertical" className="mx-1 h-4!" />
<VoiceNoteButton onRecord={voice.start} />
</>
{(canRecordVoice() || canShareLocation()) && (
<Separator orientation="vertical" className="mx-1 h-4!" />
)}
{canRecordVoice() && <VoiceNoteButton onRecord={voice.start} />}
{canShareLocation() && <ShareLocationButton onClick={share.openPicker} />}
</div>
<div className="flex-1" />
{recording ? (
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { useCallback, useRef, useState } from "react"
import { Plus, Camera, Images, FileBox, type LucideIcon, FilesIcon, ChartBar, Video, Mic } from "lucide-react"
import { Plus, Camera, Images, FileBox, type LucideIcon, FilesIcon, ChartBar, Video, Mic, MapPin } from "lucide-react"
import { Button } from "@components/ui/button"
import { Drawer, DrawerContent, DrawerDescription, DrawerTitle, DrawerTrigger } from "@components/ui/drawer"
import { useSetAtom } from "jotai"
Expand Down Expand Up @@ -40,10 +40,13 @@ let pickCount = 0
export const MobileComposerActions = ({
channelID,
onRecord,
onShareLocation,
}: {
channelID: string
/** Starts a voice note; absent when this device can't record. */
onRecord?: () => void
/** Opens the location picker after the sheet closes; absent when this device can't share one. */
onShareLocation?: () => void
}) => {
const [sheetOpen, setSheetOpen] = useState(false)
const [pollOpen, setPollOpen] = useState(false)
Expand All @@ -54,14 +57,13 @@ export const MobileComposerActions = ({
const videoCaptureRef = useRef<HTMLInputElement>(null)
const galleryInputRef = useRef<HTMLInputElement>(null)
const fileInputRef = useRef<HTMLInputElement>(null)
// A voice note starts once the sheet is gone: the recorder's start-up work would stall
// the sheet's exit, and it snaps back into view until it unmounts.
const recordAfterClose = useRef<(() => void) | null>(null)
// A tile's follow-up starts once the sheet is gone: overlays fight for focus, and the recorder's start-up stalls the exit.
const afterClose = useRef<(() => void) | null>(null)
const sheetBody = useCallback(
() => () => {
const record = recordAfterClose.current
recordAfterClose.current = null
record?.()
const open = afterClose.current
afterClose.current = null
open?.()
},
[],
)
Expand Down Expand Up @@ -108,10 +110,13 @@ export const MobileComposerActions = ({
onSelect: () => {
// In the tap: iOS only lets audio start from a user gesture.
unlockAudioContext()
recordAfterClose.current = onRecord
afterClose.current = onRecord
},
}]
: []),
...(onShareLocation
? [{ icon: MapPin, label: _("Location"), onSelect: () => { afterClose.current = onShareLocation } }]
: []),
{ icon: ChartBar, label: _("Poll"), onSelect: () => setPollOpen(true) },
{ icon: FileBox, label: _("Document"), onSelect: () => setDocOpen(true) },
]
Expand Down
Loading
Loading