Vitron-Front/app/components/thread/MessageInput.tsx
Arda Samadi b4d5f8d8db fix(ios): respect safe-area insets so chrome clears the notch/home bar
Content was drawing under the iOS status bar and home indicator because
the app uses viewport-fit=cover + black-translucent but the fixed/sticky
chrome didn't reserve the safe areas.

- bottom nav (MyLayout): move the 50px row into an inner box and let the
  bar extend by env(safe-area-inset-bottom) so icons sit above the home
  indicator (previously the fixed height clipped the inset padding).
- top headers gain env(safe-area-inset-top): HomePageTopBar, ExploreTopBar,
  SearchTopBar, CartHeader, ChatHeader, and wrapper-padded HeaderWithSupport
  / ProfilePagesHeader (so their absolutely-positioned buttons shift too).
- chat input pads bottom by the inset; chat scroll area height accounts for
  the taller header/input.
- _index.tsx no longer overrides the viewport meta (it was dropping
  viewport-fit=cover on the home page); the global one in root.tsx wins.

env(safe-area-inset-*) is 0 on non-notch devices, so desktop/Android render
identically — only iOS gains the padding.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-18 13:42:51 +03:30

174 lines
5.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { ArrowRight, Loader2, Reply, SmilePlus, X } from "lucide-react";
import { Popover, PopoverContent, PopoverTrigger } from "../ui/popover";
import { useRef, useState } from "react";
import EmojiPicker from "./EmojiPicker";
import type { MessageList } from "../../../src/api/types/models";
// Message input component
const MessageInput = ({
onSend,
replyToMessage,
onCancelReply,
messageText,
setMessageText,
isSendingMessage,
setReplyToMessage,
}: {
onSend: (text: string) => void;
replyToMessage: MessageList | undefined;
onCancelReply: () => void;
inputRef: React.RefObject<HTMLInputElement>;
messageText: string;
setMessageText: React.Dispatch<React.SetStateAction<string>>;
isSendingMessage: boolean;
setReplyToMessage: (message: MessageList | undefined) => void;
}) => {
const [caretPosition, setCaretPosition] = useState<number | null>(null);
// Track current textarea height
const handleSend = () => {
if (!messageText.trim() || isSendingMessage) return;
onSend(messageText.trim());
};
// Handle emoji selection
const handleSelectEmoji = (selectedEmoji: string) => {
if (textareaRef.current) {
// Get current cursor position
const cursorPosition =
caretPosition !== null
? caretPosition
: textareaRef.current.selectionStart;
// Insert emoji at cursor position
const newText =
messageText.substring(0, cursorPosition) +
selectedEmoji +
messageText.substring(cursorPosition);
setMessageText(newText);
// After state update, set cursor position after the inserted emoji
setTimeout(() => {
if (textareaRef.current) {
const newPosition = cursorPosition + selectedEmoji.length;
textareaRef.current.focus();
textareaRef.current.setSelectionRange(newPosition, newPosition);
}
}, 0);
} else {
// Fallback if ref not available
setMessageText((prev) => prev + selectedEmoji);
}
};
// Store caret position on selection change
const handleTextareaSelect = () => {
if (textareaRef.current) {
setCaretPosition(textareaRef.current.selectionStart);
}
};
// Auto-growing textarea ref
const textareaRef = useRef<HTMLTextAreaElement>(null);
// Handle text change and update height
const handleTextChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
setMessageText(e.target.value);
};
return (
<>
{/* Reply indicator */}
{replyToMessage && (
<div className="max-w-md mx-auto bg-GRAY1 p-2 mb-2 rounded-lg flex items-center">
<div className="flex-1 text-GRAY overflow-hidden">
<p className="text-R12">در پاسخ به:</p>
<p className="text-R14 truncate">{replyToMessage.content}</p>
</div>
<button onClick={() => setReplyToMessage(undefined)} className="ml-2">
<X className="h-5 w-5 text-GRAY" />
</button>
</div>
)}
<div className="max-w-md mx-auto fixed px-2 bottom-0 pt-2 pb-[calc(0.5rem_+_env(safe-area-inset-bottom))] w-full bg-WHITE">
{replyToMessage && (
<div className="bg-GRAY3 rounded-lg p-2 flex justify-between items-center mb-2">
<div className="flex items-center">
<Reply className="w-4 h-4 mr-2" />
<span className="text-sm truncate max-w-[250px]">
{replyToMessage.content}
</span>
</div>
<button
onClick={onCancelReply}
className="text-GRAY hover:text-BLACK2"
aria-label="حذف پاسخ"
>
<X className="w-4 h-4" />
</button>
</div>
)}
<div className="relative rounded-[12px] bg-WHITE3 flex items-end">
<Popover>
<PopoverTrigger asChild>
<button
className="absolute right-3 bottom-3 w-6 h-6 flex items-center justify-center"
aria-label="انتخاب ایموجی"
>
<SmilePlus className="text-GRAY hover:text-BLACK" size={20} />
</button>
</PopoverTrigger>
<PopoverContent
align="start"
side="top"
className="w-[280px] p-0"
sideOffset={5}
>
<EmojiPicker onSelectEmoji={handleSelectEmoji} />
</PopoverContent>
</Popover>
<textarea
ref={textareaRef}
value={messageText}
onChange={handleTextChange}
onSelect={handleTextareaSelect}
placeholder={
replyToMessage
? "پاسخ خود را بنویسید..."
: "پیام خود را بنویسید..."
}
className="hide-scrollbar overflow-y-auto h-12 flex w-full rounded-[12px] border border-input bg-transparent px-3 pr-10 py-2 text-base shadow-sm resize-none outline-none transition-height duration-100"
style={{
paddingRight: "40px",
paddingLeft: "48px",
direction: "rtl",
}}
onKeyDown={(e) => {
if (e.key === "Enter" && !e.shiftKey) {
e.preventDefault();
handleSend();
}
}}
/>
<button
onClick={handleSend}
disabled={!messageText.trim()}
className="absolute left-3 bottom-2 p-2 bg-BLUE2 text-WHITE rounded-full flex items-center justify-center disabled:opacity-50"
aria-label="ارسال پیام"
>
{isSendingMessage ? (
<Loader2 className="w-4 h-4 animate-spin" />
) : (
<ArrowRight className="w-4 h-4 transform rotate-180" />
)}
</button>
</div>
</div>
</>
);
};
export default MessageInput;