This commit is contained in:
lendry
2026-07-10 10:37:22 +03:00
parent caf12e64f7
commit a4b4577c55
13 changed files with 669 additions and 48 deletions

View File

@@ -15,6 +15,7 @@ import {
ImageIcon,
Loader2,
LogOut,
MapPin,
Mic,
Paperclip,
Pin,
@@ -60,6 +61,8 @@ import { InlineEditableTitle } from '@/components/chat/inline-editable-title';
import { RepliedMessagePreview, scrollToChatMessage } from '@/components/chat/replied-message-preview';
import { TypingIndicator } from '@/components/chat/typing-indicator';
import { VoiceMessagePlayer } from '@/components/chat/voice-message-player';
import { ChatLocationCard } from '@/components/chat/chat-location-card';
import { ChatLocationShareDialog, type ChatLocationSharePayload } from '@/components/chat/chat-location-share-dialog';
import { ChatVideoCaptureDialog, type ChatVideoCaptureMode } from '@/components/chat/chat-video-capture-dialog';
import { ChatVideoNotePlayer } from '@/components/chat/chat-video-note-player';
import { ChatVideoPlayer } from '@/components/chat/chat-video-player';
@@ -145,6 +148,7 @@ import {
detectChatMessageType,
fileIconLabel,
formatFileSize,
parseLocationMetadata,
parseMessageMetadata,
resolveChatContentType
} from '@/lib/chat-media';
@@ -320,6 +324,7 @@ export function FamilyGroupView({ groupId }: { groupId: string }) {
const [lightboxIndex, setLightboxIndex] = useState<number | null>(null);
const [videoCaptureOpen, setVideoCaptureOpen] = useState(false);
const [videoCaptureMode, setVideoCaptureMode] = useState<ChatVideoCaptureMode>('note');
const [locationShareOpen, setLocationShareOpen] = useState(false);
const mediaComposer = useChatMediaComposer();
@@ -1344,6 +1349,57 @@ export function FamilyGroupView({ groupId }: { groupId: string }) {
}
}
async function sendLocation(payload: ChatLocationSharePayload) {
if (!token || !activeRoomId || !activeRoom || sending) return;
setSending(true);
try {
const content =
payload.label?.trim() ||
payload.address?.trim() ||
`${payload.latitude.toFixed(6)}, ${payload.longitude.toFixed(6)}`;
const metadataJson = JSON.stringify({
latitude: payload.latitude,
longitude: payload.longitude,
address: payload.address,
label: payload.label
});
let messageContent = content;
let isEncrypted = false;
if (activeRoomIsE2E) {
messageContent = await encryptOutgoing({
kind: 'location',
text: content,
latitude: payload.latitude,
longitude: payload.longitude,
address: payload.address,
label: payload.label
});
isEncrypted = true;
}
const message = await sendChatMessage(
activeRoomId,
{
type: 'LOCATION',
content: messageContent,
isEncrypted,
metadataJson: activeRoomIsE2E ? undefined : metadataJson,
replyToId: replyToMessage?.id
},
token
);
appendMessage(message);
setLocationShareOpen(false);
setReplyToMessage(null);
void loadGroup();
} catch (error) {
showToast(getApiErrorMessage(error, 'Не удалось отправить геопозицию') ?? 'Ошибка');
} finally {
setSending(false);
}
}
async function uploadChatFile(file: File, options?: { voice?: boolean; videoNote?: boolean; durationMs?: number }) {
if (!token || !activeRoomId || !activeRoom) return;
const contentType = resolveChatContentType(file);
@@ -2051,6 +2107,22 @@ export function FamilyGroupView({ groupId }: { groupId: string }) {
);
})}
</div>
) : message.type === 'LOCATION' ? (
(() => {
const locationMeta = parseLocationMetadata(message.metadataJson, e2ePayload);
if (!locationMeta || locationMeta.latitude == null || locationMeta.longitude == null) {
return <p className="text-sm text-[#667085]">Геопозиция недоступна</p>;
}
return (
<ChatLocationCard
latitude={locationMeta.latitude}
longitude={locationMeta.longitude}
address={locationMeta.address}
label={locationMeta.label}
variant={mine ? 'mine' : 'theirs'}
/>
);
})()
) : message.storageKey ? (
(() => {
if (isAlbumGroup) {
@@ -2337,6 +2409,18 @@ export function FamilyGroupView({ groupId }: { groupId: string }) {
<Button size="sm" variant="ghost" className="h-10 w-10 rounded-xl p-0" onClick={() => fileInputRef.current?.click()}>
<Paperclip className="h-4 w-4" />
</Button>
{!activeRoomIsBot ? (
<Button
size="sm"
variant="ghost"
className="h-10 w-10 rounded-xl p-0"
onClick={() => setLocationShareOpen(true)}
title="Отправить геопозицию"
aria-label="Отправить геопозицию"
>
<MapPin className="h-4 w-4" />
</Button>
) : null}
{!activeRoomIsE2E ? (
<Button size="sm" variant="ghost" className="hidden h-10 w-10 rounded-xl p-0 sm:flex" onClick={() => setPollOpen(true)}>
<BarChart3 className="h-4 w-4" />
@@ -2465,6 +2549,14 @@ export function FamilyGroupView({ groupId }: { groupId: string }) {
}}
/>
<ChatLocationShareDialog
open={locationShareOpen}
sending={sending}
onOpenChange={setLocationShareOpen}
onSend={sendLocation}
onLocateError={(message) => showToast(message)}
/>
<ChatMediaComposeDialog
open={mediaComposer.open}
items={mediaComposer.items}